.NET, C#, And ASP.NET Good Practices
Purpose
This page gives Simpro developers practical guidance for working in .NET solutions using C# and ASP.NET Core.
The goal is not to memorize rules. The goal is to create code that is readable, testable, secure, observable, performant, and easy for the next developer to change without needing a dramatic soundtrack.
Solution Structure
A healthy .NET solution should make responsibilities visible.
Common structure:
src/
Simpro.Product.Api/
Simpro.Product.Application/
Simpro.Product.Domain/
Simpro.Product.Infrastructure/
tests/
Simpro.Product.UnitTests/
Simpro.Product.IntegrationTests/
Guidance:
- Keep domain rules away from controllers and infrastructure.
- Keep ASP.NET controllers/minimal APIs thin.
- Put use cases/application services in the application layer.
- Put database, queue, file, email, external API, and infrastructure adapters in infrastructure.
- Keep DTOs and contracts stable and explicit.
- Use dependency injection intentionally, not as a hiding place for every object.
C# Coding Practices
Use modern C# where it improves clarity:
- Prefer clear names over clever abbreviations.
- Use
async/awaitfor I/O-bound operations. - Avoid blocking async work with
.Result,.Wait(), or sync-over-async patterns. - Use nullable reference types and treat warnings seriously.
- Use records for immutable data shapes where appropriate.
- Use pattern matching when it improves readability.
- Keep methods small enough that their purpose is obvious.
- Catch only exceptions you can handle meaningfully.
- Do not use exceptions for normal control flow.
- Use
varwhen the type is obvious; use explicit types when clarity improves.
Naming:
- Use
PascalCasefor types, methods, properties, and public members. - Use
camelCasefor local variables and parameters. - Use interfaces when they represent meaningful abstraction, not just because every class wants a matching hat.
ASP.NET Core Practices
Keep Request Paths Fast
ASP.NET Core applications should avoid blocking calls on hot paths. Use asynchronous database, HTTP, file, and queue APIs where available.
Avoid:
var result = service.GetDataAsync().Result;
Prefer:
var result = await service.GetDataAsync();
Control Data Returned
Do not return unbounded collections from APIs. Use pagination, filtering, sorting, and projection.
Good API design asks:
- What fields does the caller actually need?
- What is the maximum page size?
- What happens when there are no results?
- How are errors represented?
- Is the endpoint safe under load?
Use HttpClientFactory
Do not create and dispose HttpClient repeatedly in hot paths. Use IHttpClientFactory for outgoing HTTP calls so connection pooling and resilience are handled properly.
Validate Inputs At Boundaries
Validate request DTOs, route parameters, query parameters, headers, and file uploads. Validation belongs at the boundary; business rules belong in the domain/application layer.
Make APIs Observable
Every service should support:
- Structured logs.
- Correlation IDs.
- Health checks.
- Metrics for latency, error rate, throughput, and dependency failures.
- Distributed tracing where practical.
Data Access
Entity Framework Core can be excellent when used with discipline.
Guidance:
- Use async database APIs.
- Use
AsNoTracking()for read-only queries. - Project only required fields.
- Avoid N+1 queries.
- Keep migrations reviewed and repeatable.
- Keep transaction boundaries explicit.
- Do not leak EF entities as external API contracts.
- Use repository abstractions only when they add real clarity; do not wrap EF mechanically everywhere.
Testing
Minimum expectations:
- Unit tests for domain logic and application services.
- Integration tests for database behavior, API endpoints, and important infrastructure adapters.
- Contract tests where services depend on each other.
- Test data builders for readable tests.
- CI runs tests reliably.
Good tests explain behavior. Bad tests worship implementation details and complain after every refactor.
Security
Developers should check:
- Authentication and authorization are explicit.
- Sensitive data is not logged.
- Secrets are never committed.
- Input is validated.
- Output encoding is handled in UI/API consumers.
- Dependencies are scanned.
- File uploads have size/type limits.
- Error responses do not leak internals.
Pull Request Checklist
- Does the change have one clear purpose?
- Are API contracts clear and documented where needed?
- Are domain rules tested?
- Are async calls handled correctly?
- Are database queries efficient?
- Are errors handled in a user/support-friendly way?
- Are logs useful without exposing secrets?
- Is there a migration/rollback consideration?
Further Study
- C# coding conventions: https://learn.microsoft.com/en-us/dotnet/csharp/fundamentals/coding-style/coding-conventions
- ASP.NET Core best practices: https://learn.microsoft.com/en-us/aspnet/core/fundamentals/best-practices
- Architect modern web applications with ASP.NET Core and Azure: https://learn.microsoft.com/en-us/dotnet/architecture/modern-web-apps-azure/
- .NET microservices architecture guide: https://learn.microsoft.com/en-us/dotnet/architecture/microservices/
- Testing in .NET: https://learn.microsoft.com/en-us/dotnet/core/testing/
- Entity Framework Core documentation: https://learn.microsoft.com/en-us/ef/core/
Team Reference Guide
Guidelines For Teams
- Keep controllers thin and business rules visible.
- Prefer explicit contracts over accidental object exposure.
- Treat performance, security, and observability as part of feature delivery.
- Review data access carefully.
- Use analyzers, formatting, and CI checks to make standards repeatable.
Reflection Questions
- Where is business logic currently hiding?
- Which API could fail under larger data volume?
- Which service would be hard to debug in production today?
- What rule should become an analyzer or CI check instead of a reminder?