
Software Csharp Backend
- 84 installs
- 73 repo stars
- Updated July 13, 2026
- vasilyu1983/ai-agents-public
Helps with backend & apis tasks.
About
software-csharp-backend is a Claude Code skill for backend & apis. It helps solo builders move faster with AI-assisted development.
- software-csharp-backend
- Backend & APIs
- AI-coding skill
Software Csharp Backend by the numbers
- 84 all-time installs (skills.sh)
- +2 installs in the week ending Jul 27, 2026 (Skillselion tracking)
- Ranked #3,035 of 4,347 Backend & APIs skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/vasilyu1983/ai-agents-public --skill software-csharp-backendAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 84 |
|---|---|
| repo stars | ★ 73 |
| Last updated | July 13, 2026 |
| Repository | vasilyu1983/ai-agents-public ↗ |
What it does
Helps with backend & apis tasks.
Files
C# Backend
Quick Reference
- Start by defining boundary: API, application service, domain logic, infrastructure, or background worker.
- Select runtime profile early: controller API, minimal API, background worker, or mixed host.
- For systems with evolving domain boundaries, prefer modular architecture over premature service splits.
- Apply C# language rules first: clarity, nullability, immutability, explicit failures, cancellation-aware async.
- Keep dependency direction inward and keep I/O at boundaries.
- Choose persistence per use case: Dapper for query-heavy SQL paths, EF Core for aggregate-heavy relational writes, Mongo for document-first modules.
- Treat reliability, observability, and security as default behavior, not follow-up work.
- Use iterative quality loop:
code -> build -> run tests -> fix -> repeat. - If deep controller/CQRS endpoint design is required, switch to
$csharp-api-cqrsand use its MediatR + FluentResults handler template. - If the task is primarily NUnit fixture design, WireMock/Testcontainers setup, or flake reduction, switch to
$qa-testing-nunit. - If the task is primarily
nuke/Build.cs, CI target sequencing, or artifact publication, switch to$ops-nuke-cicd. - If the task is primarily legacy
ILoggeror Serilog rewrite automation, switch to$dev-structured-logs.
Workflow
1. Classify the requested change (new feature, refactor, bug fix, review). 2. Choose runtime shape before implementation details. Load references/scenario-guides.md and, for HTTP services, references/aspnet-core-api-patterns.md. 3. Apply language and coding standards. Load references/csharp-language-practices.md and references/dotnet-coding-standards.md. 4. Confirm architecture and boundaries before editing internals. Load references/backend-architecture-principles.md and references/modular-architecture-principles.md. 5. Choose persistence and consistency strategy from query/write shape. Load references/data-access-patterns.md; if EF Core is selected, load references/efcore-persistence-patterns.md. 6. Add resilience behavior for outbound I/O and long-running work. Load references/reliability-and-resilience.md and references/resilience-policy-defaults.md. 7. Define tests by risk and boundary. Load references/testing-practices.md. 8. Add logs, traces, metrics, health probes, and operability defaults. Load references/observability-standards.md, and for API/runtime deployment defaults load references/runtime-ops-checklist.md. 9. Validate auth, validation, and secrets handling. Load references/security-baseline.md. 10. Run feedback loop validation for changed behavior. For NUKE-based repositories, run BuildAll, LocalUnitTest, ApiTest (when relevant), and TestAll; use $ops-nuke-cicd for pipeline-target edits. 11. Run final review against anti-pattern checklist. Load references/code-review-checklist.md.
Decision Tree
- If the issue is naming, nullability, exception usage, or async flow, read
references/csharp-language-practices.md. - If the issue is project layout, DI, configuration, or layering, read
references/dotnet-coding-standards.md. - If the issue is service boundaries or clean architecture drift, read
references/backend-architecture-principles.md. - If the issue is module boundaries, composition hosts, or modular-monolith tradeoffs, read
references/modular-architecture-principles.md. - If the issue is API style, middleware ordering, validation behavior, health/readiness, or graceful shutdown, read
references/aspnet-core-api-patterns.md. - If the issue is SQL/Mongo/EF access, pagination, transactions, idempotency, or N+1, read
references/data-access-patterns.md. - If EF Core is selected for relational persistence, also read
references/efcore-persistence-patterns.md. - If the issue is system profile specific (high-throughput API, event-driven worker, multi-tenant service), read
references/scenario-guides.md. - If the issue is retry/timeout/circuit behavior, cancellation propagation, or worker reliability, read
references/reliability-and-resilience.mdandreferences/resilience-policy-defaults.md. - If the issue is container/runtime readiness, startup config validation, caching defaults, or deployment reliability gates, read
references/runtime-ops-checklist.md. - If the issue is target framework constraints, package/API compatibility, multi-targeting, or migration between
.NET Framework/netstandard2.0and modern.NET, readreferences/version-compatibility-notes.md. - If the issue is flaky tests or weak coverage strategy, read
references/testing-practices.md. - If the issue is logs/traces/metrics/health checks, read
references/observability-standards.md. - If the issue is input validation, auth boundaries, secret handling, or secure defaults, read
references/security-baseline.md. - If the task is reviewing a PR for backend quality risks, read
references/code-review-checklist.md. - If the task is designing or fixing build-test pipeline loop behavior, use
$ops-nuke-cicd. - If the task needs deep HTTP controller + CQRS error-contract design, use
$csharp-api-cqrswith MediatR + FluentResults conventions.
Do / Avoid
Do
- Keep application services small and explicit about dependencies.
- Return deterministic domain/application results for expected failures.
- Pass
CancellationTokenthrough every async layer and external call. - Model options with validation and fail fast on invalid startup config.
- Choose API style intentionally and keep middleware ordering explicit.
- Keep persistence choices aligned to use-case shape, not team habit.
- Make telemetry and security checks part of definition of done.
Avoid
- Coupling domain logic directly to HTTP, DB driver types, or framework-specific classes.
- Using retries without timeout and idempotency guarantees.
- Swallowing exceptions or replacing root causes with vague error messages.
- Mixing unit and integration concerns in the same test fixture.
- Using
async voidoutside event handlers. - Sharing one
DbContextinstance across concurrent operations/threads. - Captive dependencies (singleton depending on scoped service).
- Shipping endpoints without structured logs, traces, metrics, and health signals.
Resources
- C# Language Practices
- Dotnet Coding Standards
- Backend Architecture Principles
- Modular Architecture Principles
- ASP.NET Core API Patterns
- Data Access Patterns
- EF Core Persistence Patterns
- Scenario Guides
- Reliability and Resilience
- Resilience Policy Defaults
- Testing Practices
- Observability Standards
- Runtime Ops Checklist
- Version Compatibility Notes
- Security Baseline
- Code Review Checklist
- Skill Data: curated Microsoft references plus modular-architecture example context.
Templates
- Service Class Template
- Options Configuration Template
- Resilient HTTP Client Template
- Dapper Query Handler Template
- Mongo Repository Template
- Test Data Builder Template
- Pull Request Checklist Template
interface:
display_name: "C# Backend"
short_description: "C#/.NET backend implementation and review guidance"
default_prompt: "Use $software-csharp-backend for C#/.NET backend service implementation, refactoring, and backend-focused reviews. Use specialized skills for deep NUnit fixture work, NUKE pipelines, or deterministic logging rewrites."
using System.Collections.Generic;
using System.Data;
using System.Threading;
using System.Threading.Tasks;
using Dapper;
namespace Company.Product.Reporting;
public sealed record FindCustomersQuery(string CountryCode, int PageSize, string? AfterCustomerCode);
public sealed record CustomerListItem(string CustomerCode, string DisplayName, string CountryCode);
public interface IFindCustomersQueryHandler
{
Task<IReadOnlyList<CustomerListItem>> HandleAsync(FindCustomersQuery query, CancellationToken cancellationToken);
}
public sealed class FindCustomersQueryHandler : IFindCustomersQueryHandler
{
private readonly IDbConnectionFactory _connectionFactory;
public FindCustomersQueryHandler(IDbConnectionFactory connectionFactory)
{
_connectionFactory = connectionFactory;
}
public async Task<IReadOnlyList<CustomerListItem>> HandleAsync(FindCustomersQuery query, CancellationToken cancellationToken)
{
await using var connection = await _connectionFactory.OpenReadOnlyAsync(cancellationToken);
const string sql =
"""
select customer_code as CustomerCode,
display_name as DisplayName,
country_code as CountryCode
from customers
where country_code = @CountryCode
and (@AfterCustomerCode is null or customer_code > @AfterCustomerCode)
order by customer_code
limit @PageSize;
""";
var command = new CommandDefinition(
sql,
new
{
query.CountryCode,
query.PageSize,
query.AfterCustomerCode,
},
cancellationToken: cancellationToken);
var rows = await connection.QueryAsync<CustomerListItem>(command);
return rows.AsList();
}
}
public interface IDbConnectionFactory
{
Task<IDbConnection> OpenReadOnlyAsync(CancellationToken cancellationToken);
}
using System;
using System.Threading;
using System.Threading.Tasks;
using MongoDB.Bson.Serialization.Attributes;
using MongoDB.Driver;
namespace Company.Product.Persistence;
public interface IInvoiceRepository
{
Task<InvoiceDocument?> FindByIdAsync(Guid invoiceId, CancellationToken cancellationToken);
Task<bool> TryInsertAsync(InvoiceDocument document, CancellationToken cancellationToken);
}
public sealed class InvoiceRepository : IInvoiceRepository
{
private readonly IMongoCollection<InvoiceDocument> _collection;
public InvoiceRepository(IMongoDatabase database)
{
_collection = database.GetCollection<InvoiceDocument>("invoices");
}
public async Task<InvoiceDocument?> FindByIdAsync(Guid invoiceId, CancellationToken cancellationToken)
{
var filter = Builders<InvoiceDocument>.Filter.Eq(x => x.InvoiceId, invoiceId);
return await _collection.Find(filter).FirstOrDefaultAsync(cancellationToken);
}
public async Task<bool> TryInsertAsync(InvoiceDocument document, CancellationToken cancellationToken)
{
try
{
await _collection.InsertOneAsync(document, cancellationToken: cancellationToken);
return true;
}
catch (MongoWriteException ex) when (ex.WriteError?.Category == ServerErrorCategory.DuplicateKey)
{
return false;
}
}
public static async Task EnsureIndexesAsync(IMongoCollection<InvoiceDocument> collection, CancellationToken cancellationToken)
{
var byInvoiceId = new CreateIndexModel<InvoiceDocument>(
Builders<InvoiceDocument>.IndexKeys.Ascending(x => x.InvoiceId),
new CreateIndexOptions { Unique = true, Name = "ux_invoice_id" });
var byCustomerUpdated = new CreateIndexModel<InvoiceDocument>(
Builders<InvoiceDocument>.IndexKeys
.Ascending(x => x.CustomerId)
.Descending(x => x.UpdatedAt),
new CreateIndexOptions { Name = "ix_customer_updated" });
await collection.Indexes.CreateManyAsync(new[] { byInvoiceId, byCustomerUpdated }, cancellationToken);
}
}
public sealed class InvoiceDocument
{
[BsonId]
public Guid InvoiceId { get; init; }
public Guid CustomerId { get; init; }
public decimal Amount { get; init; }
public DateTimeOffset UpdatedAt { get; init; }
}
using System;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
namespace Company.Product.Configuration;
public sealed class PaymentsOptions
{
public const string SectionName = "Payments";
public string BaseUrl { get; init; } = string.Empty;
public int TimeoutSeconds { get; init; } = 5;
public int MaxRetries { get; init; } = 2;
}
public sealed class PaymentsOptionsValidator : IValidateOptions<PaymentsOptions>
{
public ValidateOptionsResult Validate(string? name, PaymentsOptions options)
{
if (string.IsNullOrWhiteSpace(options.BaseUrl))
{
return ValidateOptionsResult.Fail("Payments:BaseUrl is required.");
}
if (!Uri.TryCreate(options.BaseUrl, UriKind.Absolute, out _))
{
return ValidateOptionsResult.Fail("Payments:BaseUrl must be an absolute URI.");
}
if (options.TimeoutSeconds <= 0 || options.TimeoutSeconds > 60)
{
return ValidateOptionsResult.Fail("Payments:TimeoutSeconds must be in range 1..60.");
}
if (options.MaxRetries < 0 || options.MaxRetries > 5)
{
return ValidateOptionsResult.Fail("Payments:MaxRetries must be in range 0..5.");
}
return ValidateOptionsResult.Success;
}
}
public static class PaymentsOptionsRegistration
{
public static IServiceCollection AddPaymentsOptions(this IServiceCollection services, IConfiguration configuration)
{
services
.AddOptions<PaymentsOptions>()
.Bind(configuration.GetSection(PaymentsOptions.SectionName))
.ValidateOnStart();
services.AddSingleton<IValidateOptions<PaymentsOptions>, PaymentsOptionsValidator>();
return services;
}
}
Backend Pull Request Checklist
Scope and architecture
- [ ] The change has a clear use-case boundary and does not expand unrelated service responsibilities.
- [ ] Dependency direction and layer boundaries remain correct.
- [ ] API/CQRS endpoint changes use
$csharp-api-cqrsconventions when applicable.
Code quality
- [ ] Naming, nullability, and async/cancellation handling follow team standards.
- [ ] Expected business failures are modeled explicitly (not exception-driven).
- [ ] Configuration is strongly typed and validated on startup.
Data access
- [ ] Query/write shape is intentional, and N+1 risks are addressed.
- [ ] Pagination strategy is explicit and stable.
- [ ] Transaction/idempotency behavior is documented for retries and duplicate delivery.
Reliability and operations
- [ ] Timeouts/retries/circuit settings are explicit for outbound dependencies.
- [ ] Structured logs, traces, and metrics were added or updated for new behavior.
- [ ] Health checks still represent real readiness/liveness semantics.
Security
- [ ] Input validation and authorization checks are present at correct boundaries.
- [ ] No secrets, tokens, or sensitive data are exposed in code or telemetry.
- [ ] Secure defaults are preserved (least privilege, production-safe settings).
Testing and delivery
- [ ] Unit/component/integration tests match the risk of the change.
- [ ] New behavior includes positive and failure-path coverage.
- [ ] Flaky patterns were avoided (no arbitrary sleeps, no shared mutable fixture state).
- [ ] Validation commands and outcomes are listed in the PR description.
using System;
using System.Net;
using System.Net.Http;
using System.Net.Http.Json;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Http.Resilience;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
namespace Company.Product.Integrations;
public static class OrdersApiClientRegistration
{
public static IServiceCollection AddOrdersApiClient(this IServiceCollection services)
{
services
.AddHttpClient<IOrdersApiClient, OrdersApiClient>((sp, client) =>
{
var options = sp.GetRequiredService<IOptions<OrdersApiOptions>>().Value;
client.BaseAddress = new Uri(options.BaseUrl);
client.Timeout = TimeSpan.FromSeconds(options.TimeoutSeconds);
})
.AddStandardResilienceHandler();
return services;
}
}
public sealed class OrdersApiOptions
{
public string BaseUrl { get; init; } = string.Empty;
public int TimeoutSeconds { get; init; } = 5;
}
public interface IOrdersApiClient
{
Task<OrderStatusResponse> GetStatusAsync(Guid orderId, CancellationToken cancellationToken);
}
public sealed class OrdersApiClient : IOrdersApiClient
{
private readonly HttpClient _httpClient;
private readonly ILogger<OrdersApiClient> _logger;
public OrdersApiClient(HttpClient httpClient, ILogger<OrdersApiClient> logger)
{
_httpClient = httpClient;
_logger = logger;
}
public async Task<OrderStatusResponse> GetStatusAsync(Guid orderId, CancellationToken cancellationToken)
{
using var response = await _httpClient.GetAsync($"/orders/{orderId}/status", cancellationToken);
if (response.StatusCode == HttpStatusCode.NotFound)
{
return new OrderStatusResponse(orderId, "not_found");
}
response.EnsureSuccessStatusCode();
var payload = await response.Content.ReadFromJsonAsync<OrderStatusResponse>(cancellationToken: cancellationToken);
if (payload is null)
{
_logger.LogError("Orders API returned empty payload for order {OrderId}", orderId);
throw new InvalidOperationException("Orders API response payload was empty.");
}
return payload;
}
}
public sealed record OrderStatusResponse(Guid OrderId, string Status);
using System;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
namespace Company.Product.Feature;
public interface IProcessOrderService
{
Task<ProcessOrderResult> ExecuteAsync(ProcessOrderCommand command, CancellationToken cancellationToken);
}
public sealed record ProcessOrderCommand(Guid OrderId, string RequestedBy);
public sealed record ProcessOrderResult(bool Success, string? ErrorCode)
{
public static ProcessOrderResult Ok() => new(true, null);
public static ProcessOrderResult Fail(string errorCode) => new(false, errorCode);
}
public sealed class ProcessOrderService : IProcessOrderService
{
private readonly IOrderRepository _orderRepository;
private readonly ILogger<ProcessOrderService> _logger;
public ProcessOrderService(IOrderRepository orderRepository, ILogger<ProcessOrderService> logger)
{
_orderRepository = orderRepository;
_logger = logger;
}
public async Task<ProcessOrderResult> ExecuteAsync(ProcessOrderCommand command, CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(command);
var order = await _orderRepository.GetByIdAsync(command.OrderId, cancellationToken);
if (order is null)
{
return ProcessOrderResult.Fail("order_not_found");
}
if (!order.CanProcess())
{
return ProcessOrderResult.Fail("order_invalid_state");
}
order.MarkProcessed(command.RequestedBy);
await _orderRepository.SaveAsync(order, cancellationToken);
_logger.LogInformation("Order {OrderId} processed by {RequestedBy}", command.OrderId, command.RequestedBy);
return ProcessOrderResult.Ok();
}
}
public interface IOrderRepository
{
Task<OrderAggregate?> GetByIdAsync(Guid orderId, CancellationToken cancellationToken);
Task SaveAsync(OrderAggregate order, CancellationToken cancellationToken);
}
public sealed class OrderAggregate
{
public bool CanProcess() => true;
public void MarkProcessed(string requestedBy)
{
_ = requestedBy;
}
}
using System;
namespace Company.Product.Tests.Builders;
public sealed class OrderBuilder
{
private Guid _orderId = Guid.NewGuid();
private string _state = "Created";
private string _requestedBy = "test-user";
public OrderBuilder WithOrderId(Guid orderId)
{
_orderId = orderId;
return this;
}
public OrderBuilder WithState(string state)
{
_state = state;
return this;
}
public OrderBuilder WithRequestedBy(string requestedBy)
{
_requestedBy = requestedBy;
return this;
}
public TestOrder Build()
{
return new TestOrder(_orderId, _state, _requestedBy);
}
}
public sealed record TestOrder(Guid OrderId, string State, string RequestedBy);
using System;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection;
using NUnit.Framework;
namespace Company.Product.Tests.Component;
[TestFixture]
[Category("ComponentTests")]
public sealed class ProcessOrderServiceTests
{
private ServiceProvider _serviceProvider = null!;
private CancellationTokenSource _cancellationTokenSource = null!;
[SetUp]
public async Task SetUpAsync()
{
_cancellationTokenSource = new CancellationTokenSource(TimeSpan.FromSeconds(10));
var services = new ServiceCollection();
services.AddLogging();
services.AddScoped<IOrderRepository, InMemoryOrderRepository>();
services.AddScoped<IProcessOrderService, ProcessOrderService>();
_serviceProvider = services.BuildServiceProvider(validateScopes: true);
await SeedAsync(_serviceProvider, _cancellationTokenSource.Token);
}
[TearDown]
public async Task TearDownAsync()
{
_cancellationTokenSource.Cancel();
_cancellationTokenSource.Dispose();
await _serviceProvider.DisposeAsync();
}
[Test]
public async Task ExecuteAsync_WhenOrderExists_ReturnsSuccess()
{
using var scope = _serviceProvider.CreateScope();
var service = scope.ServiceProvider.GetRequiredService<IProcessOrderService>();
var result = await service.ExecuteAsync(new ProcessOrderCommand(Guid.Parse("11111111-1111-1111-1111-111111111111"), "tester"), _cancellationTokenSource.Token);
Assert.That(result.Success, Is.True);
Assert.That(result.ErrorCode, Is.Null);
}
private static Task SeedAsync(IServiceProvider serviceProvider, CancellationToken cancellationToken)
{
_ = serviceProvider;
_ = cancellationToken;
return Task.CompletedTask;
}
private sealed class InMemoryOrderRepository : IOrderRepository
{
public Task<OrderAggregate?> GetByIdAsync(Guid orderId, CancellationToken cancellationToken)
{
_ = cancellationToken;
var exists = orderId == Guid.Parse("11111111-1111-1111-1111-111111111111");
return Task.FromResult(exists ? new OrderAggregate() : null);
}
public Task SaveAsync(OrderAggregate order, CancellationToken cancellationToken)
{
_ = order;
_ = cancellationToken;
return Task.CompletedTask;
}
}
}
{
"topic": "modular_architecture",
"external_references": [
{
"title": "Evolution of Software Architecture: From Monoliths",
"url": "https://dzone.com/articles/evolution-of-software-architecture-from-monoliths"
},
{
"title": "Modular Monolith Primer",
"url": "https://www.kamilgrzybek.com/blog/posts/modular-monolith-primer"
}
],
"microsoft_references": [
{
"title": "C# Coding Conventions",
"url": "https://learn.microsoft.com/en-us/dotnet/csharp/fundamentals/coding-style/coding-conventions"
},
{
"title": ".NET Dependency Injection",
"url": "https://learn.microsoft.com/en-us/dotnet/core/extensions/dependency-injection"
},
{
"title": ".NET Options Pattern",
"url": "https://learn.microsoft.com/en-us/dotnet/core/extensions/options"
},
{
"title": ".NET Logging",
"url": "https://learn.microsoft.com/en-us/dotnet/core/extensions/logging"
},
{
"title": "Entity Framework Core",
"url": "https://learn.microsoft.com/en-us/ef/core/"
},
{
"title": ".NET Source Code",
"url": "https://github.com/dotnet"
}
],
"example_context": [
{
"name": "pricing_modular_application",
"description": "Pricing is one example where business modules can be hosted inside a single deployment/composition unit while keeping module boundaries explicit.",
"notes": [
"Use module-specific contracts for interactions.",
"Keep business behavior inside modules.",
"Keep composition host focused on wiring and transport concerns."
]
}
]
}
ASP.NET Core API Patterns
API style selection
- Use Controller-based APIs when you need filters, richer conventions, attribute routing control, or large teams maintaining many endpoints.
- Use Minimal APIs for smaller surface areas, vertical slices, and lightweight handlers with explicit dependency injection in delegates.
- Keep one style per module by default; mix styles only with clear boundaries.
Middleware ordering baseline
- Keep error handling at the outer edge (
UseExceptionHandleror equivalent middleware). - Apply transport/security middlewares early (
HSTS,HTTPS redirectionwhere relevant). - Place authentication before authorization.
- Place rate limiting and CORS before endpoint execution.
- Keep endpoint mapping (
MapControllers,MapGroup) near the end.
Validation and error contracts
- Validate at the transport boundary and return deterministic ProblemDetails responses.
- Use typed request models with explicit constraints; reject invalid input early.
- Keep domain/application failure mapping centralized (middleware/filter), not repeated per endpoint.
- Avoid leaking stack traces or internal exception details in production responses.
Endpoint design defaults
- Propagate
CancellationTokenfrom endpoints to downstream services. - Return stable response schemas and explicit status mappings (
200/201/400/404/409at minimum). - Use pagination contracts with stable sort keys for list endpoints.
- Require idempotency for retry-prone external write endpoints.
Health checks and graceful shutdown
- Expose separate liveness and readiness endpoints.
- Keep liveness dependency-free; include critical dependencies in readiness.
- Configure shutdown timeout and ensure in-flight/background work drains safely.
- Ensure startup fails fast on invalid configuration before serving traffic.
Security and traffic controls
- Restrict CORS origins explicitly per environment.
- Apply endpoint/policy-based authorization close to business actions.
- Configure rate limiting for externally exposed and expensive endpoints.
- Enforce secure headers and disable development-only diagnostics in production.
Backend Architecture Principles
Service design goals
- Optimize for maintainability and correctness first, then performance.
- Keep use cases explicit and independently testable.
- Make invariants enforceable inside domain/application boundaries.
Clean architecture application
- Presentation: protocol concerns (HTTP/messages), auth entry, request/response mapping.
- Application: orchestration of use cases, transaction scope coordination, domain policy calls.
- Domain: pure business rules and invariants.
- Infrastructure: database, messaging, external APIs, filesystem, cache implementations.
Boundary contracts
- Define small interfaces at application boundary based on use-case needs.
- Return domain/app results that encode expected errors.
- Keep retry/timeouts out of domain logic; place them in infrastructure decorators.
Maintainable service patterns
- Use one service/handler per use case with clear input/output model.
- Prefer composition over inheritance for workflow assembly.
- Keep branching shallow by extracting named private steps.
- Introduce decorators for cross-cutting policies (authorization, metrics, retries).
- For CQRS handlers, prefer MediatR + FluentResults with
Handleboundary andDoHandlefunctional pipeline.
Evolution rules
- Add new behavior by extending use cases, not by growing god services.
- Require architectural tests or review checks for forbidden dependencies.
- Refactor duplicated orchestration into reusable components only after repeated need.
When to delegate
- For deep API controller and CQRS endpoint patterns, use
$csharp-api-cqrs.
Code Review Checklist
Severity rubric
Blocker: correctness or security defect with production impact or data-risk potential.High: high-confidence regression, reliability break, or major operability gap.Medium: maintainability issue likely to create future defects.Low: stylistic or minor clarity improvements.
Correctness and clarity
- Is behavior understandable without reading unrelated files?
- Are edge cases and failure modes explicitly handled?
- Do method and type names match business intent?
Architecture and boundaries
- Does dependency direction remain inward?
- Is business logic isolated from transport and persistence concerns?
- Is cross-cutting logic implemented centrally rather than duplicated?
- For CQRS handlers, does
Handleremain boundary-only with a small functionalDoHandlepipeline?
Data and performance
- Does query shape avoid N+1 and over-fetching?
- Are indexes and pagination strategy aligned with access patterns?
- Are transactions scoped and idempotency considerations explicit?
Reliability
- Are timeout, retry, and circuit policies explicit for outbound I/O?
- Is cancellation propagated through async calls?
- Are background jobs idempotent and observable?
Testing
- Do tests cover happy path plus expected failures?
- Are tests deterministic and free from unnecessary sleeps/time coupling?
- Is the test scope appropriate (unit vs integration vs component)?
Observability and operations
- Are logs structured and free from sensitive data?
- Are metrics/traces sufficient to diagnose production failures?
- Are health checks meaningful for orchestration decisions?
Security
- Are input validation and authorization checks complete?
- Are secrets handled via secure providers and never hard-coded?
- Are secure defaults preserved (least privilege, TLS, safe headers)?
Common anti-patterns to block
- God services with mixed orchestration, domain, and infrastructure logic.
- Catch-all exception swallowing or broad retries without safeguards.
- Static/global state in request processing paths.
- Tests that depend on execution order or shared mutable fixtures.
C# Language Practices
Naming and readability
- Use domain terms in class and method names; avoid transport terms inside domain/application layers.
- Prefer explicit types when inferred type hides intent; use
varonly when the right side is obvious. - Keep methods focused: one behavior, one reason to change.
- Replace boolean flag arguments with explicit methods or options types.
Immutability and state control
- Prefer immutable request/response models (
recordwithinitor constructor-only properties). - Make mutable state private and minimal; expose behavior, not setters.
- Use readonly collections for outputs from domain and query handlers.
Nullability discipline
- Enable nullable reference types and treat warnings as real defects.
- Validate boundary inputs immediately; keep internal code mostly null-free.
- Use guard clauses for required values and fail with precise argument messages.
- Avoid pervasive
!; fix source nullability contracts instead.
Exceptions and failure modeling
- Throw exceptions for unexpected/technical faults, not normal domain outcomes.
- Use typed/domain error results for validation and business-rule failures.
- Preserve stack trace (
throw;) when rethrowing. - Add context once at boundary logs; do not log-and-rethrow repeatedly.
Async/await and cancellation
- Pass
CancellationTokento every async dependency supporting cancellation. - Never block on async (
.Result,.Wait()); keep call chains async end-to-end. - Use
Task.WhenAllfor independent I/O operations. - Configure explicit timeouts for network and external dependencies.
- Avoid fire-and-forget in request paths; route background work through durable workers.
Practical checks
- Are all public async methods cancellation-aware?
- Are expected failures represented without exceptions?
- Can a new engineer infer behavior from names alone?
Data Access Patterns
Persistence selector
| Need | Prefer | Why |
|---|---|---|
| Query-heavy SQL reads with tight control over SQL text | Dapper | Lowest overhead and precise SQL ownership |
| Relational aggregate writes with evolving domain model | EF Core | Strong mapping/modeling support and transaction boundaries |
| Document-centric aggregate with flexible schema | Mongo | Natural aggregate storage and projection-friendly reads |
| Mixed workload by module | Polyglot | Choose per module/use case, not globally |
Query/write separation
- Keep read models optimized for query shape; do not force domain aggregates into every query.
- Keep write models focused on invariants and consistency.
- Define repository/query handlers around use cases, not tables/collections alone.
Dapper SQL patterns
- Select only required columns.
- Use keyset pagination for large ordered datasets; use offset pagination only for small/admin screens.
- Batch related reads to avoid N+1 (joins, IN queries, preloaded maps).
- Keep transaction scopes short and explicit; avoid long business logic inside a transaction.
EF Core patterns
- Use
AsNoTracking()for read-only flows and keep tracking enabled only where changes are committed. - Use projection (
Select) early to prevent over-fetching entities. - Use explicit
Includepaths intentionally; avoid accidental lazy-loading behavior. - Keep
DbContextscoped per request/unit of work; do not share across concurrent operations. - Use DbContext pooling only after measuring startup/throughput gain.
- Prefer
ExecuteUpdate/ExecuteDeletefor bulk updates when full aggregate loading is unnecessary. - For advanced relational scenarios and EF Core 10 capabilities, load
references/efcore-persistence-patterns.md.
Mongo patterns
- Model documents around aggregate boundaries and read/write access patterns.
- Index by exact filter/sort patterns used in production queries.
- Use projection queries for list endpoints.
- Use optimistic concurrency/version checks when concurrent edits are possible.
Idempotency and consistency
- Require idempotency keys for externally triggered writes that can be retried.
- Persist idempotency outcome with deterministic replay behavior.
- Use outbox pattern for transactionally consistent event publication.
- Document exactly-once assumptions; default to at-least-once handling.
Pagination and performance
- Return stable sort keys and continuation token for cursor paging.
- Enforce sensible max page size.
- Measure query latency and rows/documents scanned in telemetry.
Data-access checklist
- Is the chosen persistence mode aligned to this use case (Dapper/EF Core/Mongo)?
- Is N+1 impossible by design for this path?
- Are indexes aligned to filters and sorts?
- Is write behavior safe under retries and duplicate delivery?
Dotnet Coding Standards
Project structure
- Organize by capability and layer, not by technical type only.
- Keep API/presentation thin; place use-case logic in application layer.
- Keep infrastructure adapters isolated behind interfaces.
Dependency boundaries
- Enforce inward dependency direction: presentation -> application -> domain.
- Prevent domain from referencing infrastructure packages.
- Avoid static service locators and hidden global dependencies.
Dependency injection
- Register dependencies by role and lifetime:
Singletonfor stateless/shared expensive resources.Scopedfor request-bound services and data sessions.Transientfor lightweight stateless components.- Validate container at startup when possible.
- Prefer constructor injection; avoid method/property injection except narrow framework cases.
Configuration and options
- Bind strongly typed options per bounded context.
- Validate options on startup (
ValidateOnStart) for critical settings. - Keep secrets outside source control and outside plain config files.
- Separate runtime policies (timeouts, retry counts, limits) into options.
Layering rules
- Keep transport DTOs and persistence documents outside core domain models.
- Map data at boundaries; do not leak ORM/driver entities into domain/application.
- Keep cross-cutting concerns (logging, metrics, auth) in decorators/middleware where possible.
Implementation checklist
- Does each project have one clear responsibility?
- Does each dependency point inward?
- Are options typed, validated, and environment-safe?
EF Core Persistence Patterns
When EF Core fits
- Prefer EF Core for relational modules with rich aggregates, transactional writes, and evolving domain models.
- Prefer Dapper for read-heavy SQL paths where query text ownership and low overhead are primary.
- Use EF Core selectively per module in polyglot systems.
Setup defaults
- Configure
DbContextwith explicit command timeout and connection resiliency settings. - Keep
DbContextlifetime scoped to request/unit-of-work. - Add DbContext pooling only after measuring benefit; validate no shared mutable state in context services.
- Keep provider-specific behaviors explicit (for example, Npgsql retry and timeout settings).
Modeling and configuration
- Keep entity configuration in
IEntityTypeConfiguration<T>classes. - Define key, length, nullability, precision, and index constraints explicitly.
- Use query filters intentionally for soft-delete and tenant isolation.
- Keep aggregate invariants in domain/application logic, not only in EF configuration.
Query and performance patterns
- Use
AsNoTracking()for read paths and projections for API payload shaping. - Avoid unbounded
Includechains; design query handlers per endpoint use case. - Use compiled queries only for proven hot paths.
- Prevent N+1 via explicit includes, joins, or batched secondary queries.
- Use bulk operations (
ExecuteUpdate/ExecuteDelete) when full entity materialization is unnecessary.
Consistency and transactions
- Keep transactions short and centered around state transitions.
- Combine idempotency and transaction boundaries for externally retried writes.
- Avoid cross-service distributed transaction assumptions; prefer outbox/eventual consistency.
Migrations and rollout safety
- Keep migrations small, deterministic, and reversible where possible.
- Generate idempotent SQL for controlled production rollout when required by operations.
- Coordinate schema/application rollout for backward compatibility across deploy windows.
EF Core 10 feature notes
- Named query filters improve selective filter disable behavior; use clear filter names.
LeftJoin/RightJoinreduce verbose join composition for readability.- Simplified
ExecuteUpdateflow improves conditional bulk update readability. - Treat new features as optional upgrades; prioritize compatibility with active repository standards.
Pitfalls to avoid
- Sharing one
DbContextacross threads. - Long-lived transactions with outbound network calls inside them.
- Blindly enabling lazy loading in latency-sensitive paths.
- Treating migrations as a deployment afterthought.
Modular Architecture Principles
Purpose
Use this guide when a backend should keep modular boundaries inside one deployable unit before splitting into independent services.
When This Fits
- Domain boundaries are still evolving.
- Teams need stronger isolation than a layered monolith but lower operational overhead than microservices.
- You want explicit module contracts, independent module testing, and incremental extraction paths.
Core Principles
- Organize by business modules, not technical layers alone.
- Keep each module internally layered (presentation/application/domain/infrastructure as needed).
- Treat inter-module interaction as integration: explicit contracts, versioned DTOs/events, no direct internal-type coupling.
- Use one composition/deployment host to wire modules and cross-cutting concerns.
- Keep business logic inside modules; keep host projects focused on transport, composition, and runtime wiring.
- Share only stable abstractions in a small shared kernel; avoid dumping business logic into shared libraries.
Design Checklist
- Define module ownership: entities, use cases, data stores, and integration contracts.
- Enforce boundaries with project references and architectural tests/review checks.
- Choose sync vs async integration per use case and consistency requirements.
- Keep module APIs explicit and independently testable.
- Plan extraction seams early: contracts, anti-corruption adapters, and idempotent integration flows.
Concrete Example
One modular-architecture shape is a pricing system where business behavior is split into dedicated modules (for example, administration and calculation) and a separate API host acts as the deployment/composition unit.
External References
See ../Data/data.json for curated external links.
Observability Standards
Structured logging
- Use structured logs with stable property names.
- Include correlation fields (
TraceId,SpanId,RequestId, tenant/customer identifiers when allowed). - Log at appropriate levels: Debug for diagnostics, Information for state transitions, Warning/Error for failures.
- Never log secrets, tokens, or regulated sensitive data.
Tracing
- Create spans around inbound requests, outbound I/O, and key internal operations.
- Propagate trace context across HTTP, messaging, and background processing.
- Annotate spans with business-relevant tags (operation type, dependency name, retry attempt).
Metrics
- Emit counters for requests, errors, retries, and dependency failures.
- Emit histograms for latency and payload size where relevant.
- Track saturation/concurrency for critical pools and workers.
- Define SLI/SLO-aligned metrics per service.
Health checks
- Provide liveness check for process health.
- Provide readiness check for critical dependencies needed to serve traffic.
- Fail readiness when essential dependencies are unavailable.
- Keep health endpoints fast and side-effect free.
Operability checklist
- Can operators answer "what failed, where, and why" from logs + traces + metrics?
- Are alerts based on actionable symptoms, not noisy low-level signals?
- Do dashboards include latency, error rate, and dependency health at minimum?
Reliability and Resilience
Timeout strategy
- Set explicit timeout per outbound dependency based on SLO and dependency behavior.
- Keep request timeout budgeted across retries and downstream calls.
- Fail fast for degraded dependencies when work is non-critical.
Retry strategy
- Retry only transient failures (timeouts, 5xx, throttling).
- Use bounded exponential backoff with jitter.
- Never retry non-idempotent writes without idempotency guarantees.
- Emit retry metrics/log events with attempt count and reason.
Circuit breakers and load protection
- Use circuit breakers for unstable dependencies to prevent resource exhaustion.
- Use concurrency limits/bulkheads for expensive outbound calls.
- Define fallback behavior per use case (cached response, partial result, fail closed).
Cancellation and request lifecycle
- Propagate
CancellationTokenthrough all async work. - Stop downstream work quickly after cancellation.
- Ensure disposal and cleanup paths are cancellation-safe.
- Never catch
OperationCanceledExceptionin the normal failure path. Consumer and worker loops need an explicit shutdown path that exits cleanly before any failure routing (retry, DLQ, pause) kicks in. - Classify exceptions as retryable vs non-retryable before entering generic retry logic. Letting non-retryable exceptions flow through retry loops repeats side effects and misleads operators.
Background jobs and workers
- Make job handlers idempotent and resumable.
- Store job progress/checkpoints for long-running workflows.
- Separate retry policy for jobs from online request policy.
- Add dead-letter handling and operator-visible failure diagnostics.
Reliability review checklist
- Are timeouts defined in config and tested?
- Are retries bounded, observable, and idempotent-safe?
- Is worker behavior recoverable after crash/restart?
Resilience Policy Defaults
Purpose
- Use these defaults as starting points.
- Tune from real latency/error telemetry per dependency.
HTTP internal service calls
- Timeout:
2sto5s. - Retries: up to
2attempts for transient failures. - Backoff: exponential with jitter (
100ms,300ms,800msranges). - Circuit breaker: open on high error ratio over short rolling window.
HTTP third-party APIs
- Timeout:
5sto15sdepending on provider SLA. - Retries:
1to2attempts; honor vendor rate-limit signals. - Backoff: jittered exponential with longer initial delay.
- Circuit breaker: open quickly to avoid quota burn on outage.
Database operations
- Query timeout:
1sto3sfor hot paths, higher for batch/admin paths. - Retries: avoid generic retries for writes unless idempotent and safe.
- Connection pool: set conservative max connections and monitor saturation.
Message processing
- Handler timeout: explicit per message type.
- Retries: bounded with dead-letter after max attempts.
- Idempotency: required for any handler retry path.
Background jobs
- Retry budget: bounded by business tolerance and downstream impact.
- Backoff: progressive with caps; avoid retry storms.
- Concurrency: cap per queue and dependency capacity.
Safe rollout steps
- Start with conservative retries.
- Add telemetry for timeout, retry attempts, circuit open events.
- Load test before increasing concurrency or retry budgets.
Runtime Ops Checklist
Startup and configuration
- Validate critical options at startup and fail fast on invalid config.
- Keep secrets out of source control; load from environment/secret manager.
- Separate functional config from security-sensitive config.
- Keep environment-specific defaults explicit and documented.
Logging and tracing
- Use structured logs with stable keys for service, operation, and correlation identifiers.
- Propagate trace context across HTTP, messaging, and background execution.
- Redact secrets and regulated fields from logs, traces, and exception payloads.
- Add request/operation scope fields to support incident triage.
Metrics and health
- Emit request/error/retry counters and latency histograms for critical flows.
- Expose liveness and readiness endpoints with clear dependency semantics.
- Track dependency health and pool saturation for outbound clients/workers.
- Align alerts to user-facing symptoms and actionable thresholds.
Caching defaults
- Use cache only for read paths with clear invalidation ownership.
- Prefer tag/key-scoped invalidation over broad cache flush.
- Define TTL and staleness behavior explicitly per cache entry category.
- Guard against cache stampede and dependency outage fallback behavior.
Resilience defaults
- Set explicit timeout on every outbound dependency call.
- Retry only transient failures with bounded backoff and jitter.
- Add circuit breaker and concurrency limits for unstable/slow dependencies.
- Require idempotency guarantees before retrying writes.
Deployment and runtime hardening
- Use non-root container images and minimal runtime footprint.
- Configure graceful shutdown timeout and verify drain behavior in tests.
- Keep readiness false during migration/startup windows that cannot serve traffic.
- Gate rollout with smoke checks for health, error rate, and latency regression.
Operational readiness questions
- Can an operator identify failing dependency and impact within minutes?
- Can the service restart safely without manual cleanup?
- Are failure modes documented for degraded dependency behavior?
Scenario Guides
High-throughput HTTP APIs
- Use request-thin handlers and keep per-request allocations low.
- Use keyset pagination for hot list endpoints.
- Batch dependent reads and avoid per-item downstream calls.
- Set strict timeout budgets and bounded retries for every outbound dependency.
- Prefer asynchronous pipelines and avoid blocking sync-over-async.
Event-driven workers
- Make consumers idempotent by business key or explicit idempotency key.
- Keep handlers resumable with checkpointing for long workflows.
- Separate poison/dead-letter handling from transient retry paths.
- Include operation IDs and message metadata in logs/traces.
- Verify duplicate and out-of-order delivery behavior in tests.
- Classify non-retryable exceptions before entering generic retry logic, not inside it. A
KafkaNonRetryableExceptionthat flows through the retry loop repeats side effects and violates operator expectations. - Treat cancellation as control flow, not failure handling.
OperationCanceledExceptionmust exit the consumer loop before failure routing kicks in. Treating shutdown as a processing failure can incorrectly commit messages, route them to retry/DLQ, or pause partitions during normal host stop. - Isolate failure-routing failures to the affected partition. A retry/DLQ publish failure should pause only that partition, not kill the whole consumer task or sleep the entire loop. Produce visible host-level faulting instead of leaving a dead background task behind.
- Treat retry/DLQ adoption as a contract change: switching from "commit in finally" to "commit only after success or retry/DLQ publish" changes runtime semantics even if public APIs stay the same. New behavior must be explicitly opt-in.
- Add regression coverage for legacy consumer behaviors (shared fan-out, custom
IMessageSubscription) whenever core consumer internals change. New failure-handling modes are not safe if they break older extension points.
Multi-tenant services
- Resolve tenant context at boundary and propagate through all layers.
- Enforce tenant isolation in query filters and cache keys.
- Keep per-tenant limits and quotas configurable.
- Ensure telemetry includes tenant identifiers only when policy allows.
- Validate data migration and backfill plans for tenant-scoped schema changes.
Selection checklist
- If latency and p99 are dominant constraints, use the high-throughput profile.
- If replay/retry/ordering dominate, use the event-driven profile.
- If isolation and tenant policy dominate, use the multi-tenant profile.
Security Baseline
Input and boundary validation
- Validate and normalize all external inputs at entry boundaries.
- Reject invalid payloads early with explicit error responses.
- Enforce allow-lists for enum-like user inputs.
- Guard against over-posting by mapping explicit request models.
Authentication and authorization boundaries
- Authenticate at transport edge.
- Authorize per use case, not only per endpoint path.
- Re-check authorization in background processing where user context is replayed.
- Deny by default when policy resolution is ambiguous.
Secret and configuration hygiene
- Store secrets in managed secret stores/environment, never in source control.
- Rotate secrets and support zero-downtime reload where possible.
- Keep security-sensitive options separate from functional configuration.
Secure defaults
- Use TLS for all networked communication.
- Disable debug/developer endpoints and verbose errors in production.
- Minimize privilege for service identities and database users.
- Set conservative defaults for CORS, headers, and cookie/token handling.
Dependency and data protection
- Keep dependencies patched and monitor advisories.
- Hash passwords with modern adaptive algorithms.
- Encrypt sensitive data at rest and in transit.
- Redact sensitive fields in logs, traces, and exceptions.
Security review checklist
- Can untrusted input reach dynamic query/path construction unsafely?
- Are authz checks close to business action boundaries?
- Are secrets and tokens fully excluded from telemetry?
Testing Practices
Scope by risk and boundary
- Unit tests: validate branch logic and invariants with deterministic inputs.
- Component tests: validate collaborating classes with near-real wiring.
- Integration/API tests: validate contracts, persistence, and infrastructure behavior.
- For API full-cycle tests, use Testcontainers for owned infrastructure and WireMock for external HTTP dependencies.
Test design rules
- Keep one behavioral assertion focus per test.
- Name tests as behavior + condition + expected outcome.
- Use builders/fixtures to reduce setup noise.
- Avoid hidden shared mutable state across tests.
- Default to a two-file handler test structure:
<Feature>Fixture.cs+<Feature>Tests.cs. - Split fixture/tests into additional
partialfiles only when scenario matrix grows large (for example by route/provider). - For API suites, keep one base
ApiTest.cs+ApiFixture.csand split scenarios into partial files (.Positive.cs,.Negative.cs,.Validation.cs) when needed. - Use dedicated request builders and
TestCaseSourcesfor large input matrices.
Anti-flaky tactics
- Control time (fake clock where practical).
- Avoid sleeping; use polling assertions with bounded timeout.
- Use isolated test data per case.
- Keep external dependency setup deterministic (Testcontainers/WireMock).
Async and eventual consistency
- Await all async operations.
- Assert eventual outcomes with retry-until-timeout helper methods.
- Keep timeouts tight enough to fail fast but realistic for CI variance.
Coverage expectations
- Test happy path and expected failures for every critical use case.
- Add regression tests for every production defect fix.
- Prefer meaningful scenario coverage over raw line-coverage chasing.
Suite maintenance
- Quarantine and fix flaky tests immediately.
- Keep category tags consistent for CI routing.
- Remove redundant tests when behavior is already covered at lower cost.
- Prefer shared fixture APIs (
Given...,Send...) over repeated arrange logic inside tests.
Version Compatibility Notes
Scope and intent
- Use this guide when framework compatibility materially affects design decisions.
- Prefer compatibility-safe defaults first, then adopt newer features where targets allow.
- Treat
.NET Framework 4.8andnetstandard2.0as maintenance/interoperability paths; prefer modern.NETfor new services.
Target matrix
| Target | Support posture | Language ceiling (practical) | Primary use | Risk notes | Fallback strategy |
|---|---|---|---|---|---|
.NET Framework 4.8 (net48) | Legacy maintenance | C# 7.3 typical | Existing enterprise apps/libraries | Older BCL and package constraints | Keep adapters thin, isolate modern APIs behind interfaces, multi-target where feasible |
netstandard2.0 | Compatibility contract | Feature set constrained by consumer runtimes | Shared libraries consumed by mixed runtimes | Limited API surface vs modern .NET | Keep core abstractions here, add runtime-specific implementations in net8.0+ targets |
.NET 8 (net8.0) | Current stable baseline in many orgs | C# 12+ | New services and library modern baseline | Feature drift vs latest docs | Use incremental opt-in; guard newer APIs with multi-targeting |
.NET 9 (net9.0) | Shorter lifecycle/transition target | C# 13+ | Transitional adoption | Support window tradeoffs | Keep easy down-target path to net8.0 if platform policy changes |
.NET 10 (net10.0) | Latest long-term direction | C# 14 | Advanced features and newest platform APIs | Team/environment may lag SDK runtime | Feature-gate by TFM and provide net8.0/netstandard2.0 fallbacks |
Language feature gates
- Treat C# 12/13/14 features as optional unless all targets support them.
- Keep shared-domain models compatible with lower targets when libraries are multi-targeted.
- Avoid introducing syntax/features that force unnecessary target upgrades.
- Prefer behavior-preserving fallback patterns when down-targeting:
- Primary constructors -> explicit constructors
- Newest collection/syntax sugar -> standard object/collection initialization
- TFM-specific APIs -> interface abstraction + target-specific implementation
ASP.NET Core feature compatibility
.NET 8+is the realistic baseline for modern ASP.NET Core hosts.netstandard2.0is not a host target; use it for shared abstractions and helpers only.- For API projects, keep middleware and endpoint design stable across targets:
- deterministic error contracts,
- explicit health/readiness behavior,
- explicit authentication/authorization and rate-limiting configuration.
- When using latest framework features, ensure endpoints still compile/run under the lowest supported host target.
Data access compatibility
- EF Core version must match the host runtime and provider support matrix.
- Dapper is broadly compatible and useful for cross-target SQL access.
- MongoDB driver support can vary by runtime generation; verify package/runtime matrix before upgrades.
- Keep repository interfaces target-agnostic; isolate provider/runtime-specific APIs in infrastructure implementations.
Resilience and observability package compatibility
Microsoft.Extensions.*and modern resilience packages vary by target; pin versions explicitly.- Prefer policy abstractions in application code and framework/package-specific wiring in composition root.
- Keep telemetry contracts stable (log properties, metric names, trace tags) regardless of target runtime.
- If a modern package is unavailable for a lower target, provide a minimal fallback policy with explicit limitations.
Multi-targeting patterns
- Use multi-targeting for reusable libraries that need broad compatibility.
- Typical library examples:
TargetFrameworks: netstandard2.0;net8.0TargetFrameworks: net48;netstandard2.0TargetFrameworks: net48;net8.0;net10.0- Use conditional compilation only for target-specific behavior, not core business logic:
#if NETSTANDARD2_0#if NET48#if NET8_0_OR_GREATER
Migration guidance
- Prefer staged migration over big-bang upgrades:
1. Stabilize test coverage and public contracts. 2. Multi-target shared libraries (netstandard2.0 + modern TFM). 3. Move hosts to modern .NET runtime. 4. Remove legacy-only compatibility code once consumers are migrated.
- Validate package compatibility and runtime behavior at each stage.
- Keep deployment rollback-compatible across mixed-version windows.
Compatibility review checklist
- Are required targets explicitly documented for this component?
- Does new code compile and test on every declared target?
- Are framework-specific APIs isolated from shared abstractions?
- Is there a fallback path for the lowest supported target?
- Are package versions pinned to a known compatible range per target?