ASP.NET Core Best Practices
#dotnet #aspnetcore #bestpractices #architecture #performance
Last Updated: May 18, 2025 Related: .NET vs Laravel Complete Developer Guide
Quick Navigation
- [[#Project Structure & Architecture]]
- [[#Configuration Management]]
- [[#Dependency Injection]]
- [[#Error Handling & Logging]]
- [[#Security Best Practices]]
- [[#Performance Optimization]]
- [[#Testing Strategies]]
Project Structure & Architecture
๐๏ธ Clean Architecture Structure
Solution/
โโโ src/
โ โโโ API/ # Web API Controllers
โ โโโ Application/ # Business Logic
โ โ โโโ Commands/
โ โ โโโ Queries/
โ โ โโโ Services/
โ โโโ Domain/ # Entities & Business Rules
โ โ โโโ Entities/
โ โ โโโ ValueObjects/
โ โ โโโ Interfaces/
โ โโโ Infrastructure/ # Data Access & External Services
โ โโโ Data/
โ โโโ Services/
โ โโโ Repositories/
โโโ tests/
โ โโโ UnitTests/
โ โโโ IntegrationTests/
โ โโโ ApiTests/
โโโ docs/
๐ Folder Organization Best Practices
// โ
Good: Organized by feature
Controllers/
โโโ Users/
โ โโโ UsersController.cs
โ โโโ UserDto.cs
โ โโโ UserValidator.cs
โโโ Orders/
โ โโโ OrdersController.cs
โ โโโ OrderDto.cs
โ โโโ OrderValidator.cs
// โ Bad: Organized by type
Controllers/
โโโ UsersController.cs
โโโ OrdersController.cs
DTOs/
โโโ UserDto.cs
โโโ OrderDto.cs
Configuration Management
โ๏ธ Configuration Hierarchy
// appsettings.json (base)
{
"ConnectionStrings": {
"DefaultConnection": "Server=localhost;Database=MyApp;Trusted_Connection=true;"
},
"Logging": {
"LogLevel": {
"Default": "Information"
}
}
}
// appsettings.Development.json (overrides)
{
"Logging": {
"LogLevel": {
"Default": "Debug",
"Microsoft.AspNetCore": "Warning"
}
}
}
// appsettings.Production.json (overrides)
{
"Logging": {
"LogLevel": {
"Default": "Warning"
}
}
}
๐ Secrets Management
// โ
Good: Use User Secrets for development
// dotnet user-secrets init
// dotnet user-secrets set "ConnectionStrings:DefaultConnection" "secret-connection"
// โ
Good: Use Azure Key Vault for production
public static void Main(string[] args)
{
CreateHostBuilder(args).Build().Run();
}
public static IHostBuilder CreateHostBuilder(string[] args) =>
Host.CreateDefaultBuilder(args)
.ConfigureAppConfiguration((context, config) =>
{
if (context.HostingEnvironment.IsProduction())
{
var keyVaultEndpoint = new Uri(Environment.GetEnvironmentVariable("VaultUri"));
config.AddAzureKeyVault(keyVaultEndpoint, new DefaultAzureCredential());
}
});
๐ท๏ธ Strongly Typed Configuration
// Configuration class
public class DatabaseSettings
{
public const string SectionName = "Database";
public string ConnectionString { get; set; } = string.Empty;
public int CommandTimeout { get; set; } = 30;
public bool EnableSensitiveDataLogging { get; set; } = false;
}
// Registration in Program.cs
builder.Services.Configure<DatabaseSettings>(
builder.Configuration.GetSection(DatabaseSettings.SectionName));
// Usage in services
public class UserService
{
private readonly DatabaseSettings _dbSettings;
public UserService(IOptions<DatabaseSettings> dbSettings)
{
_dbSettings = dbSettings.Value;
}
}
Dependency Injection
๐ Service Registration Patterns
// Program.cs - Modern minimal API style
var builder = WebApplication.CreateBuilder(args);
// โ
Good: Register by lifetime
// Transient: New instance every time
builder.Services.AddTransient<IEmailService, EmailService>();
// Scoped: One instance per request
builder.Services.AddScoped<IUserRepository, UserRepository>();
// Singleton: One instance for application lifetime
builder.Services.AddSingleton<ICacheService, RedisCacheService>();
// โ
Good: Use factory pattern for complex dependencies
builder.Services.AddSingleton<IHttpClientFactory>(provider =>
{
var factory = new HttpClientFactory();
factory.ConfigureClient("api", client =>
{
client.BaseAddress = new Uri("https://api.example.com");
client.DefaultRequestHeaders.Add("ApiKey", "secret");
});
return factory;
});
๐ฏ Service Locator Anti-Pattern
// โ Bad: Service locator pattern
public class OrderService
{
public void ProcessOrder(Order order)
{
var provider = HttpContext.RequestServices;
var emailService = provider.GetService<IEmailService>();
var paymentService = provider.GetService<IPaymentService>();
// ... processing logic
}
}
// โ
Good: Constructor injection
public class OrderService
{
private readonly IEmailService _emailService;
private readonly IPaymentService _paymentService;
public OrderService(IEmailService emailService, IPaymentService paymentService)
{
_emailService = emailService;
_paymentService = paymentService;
}
public void ProcessOrder(Order order)
{
// ... processing logic
}
}
Error Handling & Logging
๐จ Global Exception Handling
// Custom exception middleware
public class GlobalExceptionMiddleware
{
private readonly RequestDelegate _next;
private readonly ILogger<GlobalExceptionMiddleware> _logger;
public GlobalExceptionMiddleware(RequestDelegate next, ILogger<GlobalExceptionMiddleware> logger)
{
_next = next;
_logger = logger;
}
public async Task InvokeAsync(HttpContext context)
{
try
{
await _next(context);
}
catch (Exception ex)
{
_logger.LogError(ex, "An unhandled exception occurred");
await HandleExceptionAsync(context, ex);
}
}
private static async Task HandleExceptionAsync(HttpContext context, Exception exception)
{
context.Response.ContentType = "application/json";
var response = exception switch
{
NotFoundException => new { error = "Resource not found", status = 404 },
ValidationException => new { error = exception.Message, status = 400 },
UnauthorizedAccessException => new { error = "Unauthorized", status = 401 },
_ => new { error = "An error occurred", status = 500 }
};
context.Response.StatusCode = response.status;
await context.Response.WriteAsync(JsonSerializer.Serialize(response));
}
}
// Register in Program.cs
app.UseMiddleware<GlobalExceptionMiddleware>();
๐ Structured Logging
// โ
Good: Structured logging with Serilog
public static void Main(string[] args)
{
Log.Logger = new LoggerConfiguration()
.WriteTo.Console()
.WriteTo.File("logs/log-.txt", rollingInterval: RollingInterval.Day)
.WriteTo.Seq("http://localhost:5341") // Optional: Seq for log analysis
.CreateLogger();
try
{
Log.Information("Starting web application");
CreateHostBuilder(args).Build().Run();
}
catch (Exception ex)
{
Log.Fatal(ex, "Application terminated unexpectedly");
}
finally
{
Log.CloseAndFlush();
}
}
// Usage in controllers
[ApiController]
[Route("api/[controller]")]
public class UsersController : ControllerBase
{
private readonly ILogger<UsersController> _logger;
[HttpGet("{id}")]
public async Task<ActionResult<User>> GetUser(int id)
{
_logger.LogInformation("Getting user with ID: {UserId}", id);
try
{
var user = await _userService.GetUserAsync(id);
_logger.LogInformation("Successfully retrieved user {UserId}", id);
return Ok(user);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error retrieving user {UserId}", id);
throw;
}
}
}
Security Best Practices
๐ Authentication & Authorization
// JWT Configuration
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidateAudience = true,
ValidateLifetime = true,
ValidateIssuerSigningKey = true,
ValidIssuer = builder.Configuration["Jwt:Issuer"],
ValidAudience = builder.Configuration["Jwt:Audience"],
IssuerSigningKey = new SymmetricSecurityKey(
Encoding.UTF8.GetBytes(builder.Configuration["Jwt:Key"]))
};
});
// Policy-based authorization
builder.Services.AddAuthorization(options =>
{
options.AddPolicy("RequireAdminRole", policy =>
policy.RequireRole("Admin"));
options.AddPolicy("MinimumAge", policy =>
policy.Requirements.Add(new MinimumAgeRequirement(18)));
});
// Custom authorization requirement
public class MinimumAgeRequirement : IAuthorizationRequirement
{
public int MinimumAge { get; }
public MinimumAgeRequirement(int minimumAge)
{
MinimumAge = minimumAge;
}
}
๐ก๏ธ Security Headers
// Security headers middleware
app.Use(async (context, next) =>
{
context.Response.Headers.Add("X-Frame-Options", "DENY");
context.Response.Headers.Add("X-Content-Type-Options", "nosniff");
context.Response.Headers.Add("X-XSS-Protection", "1; mode=block");
context.Response.Headers.Add("Strict-Transport-Security", "max-age=31536000; includeSubDomains");
context.Response.Headers.Add("Content-Security-Policy",
"default-src 'self'; script-src 'self' 'unsafe-inline'");
await next();
});
// Or use NWebsec package
builder.Services.AddHsts(options =>
{
options.Preload = true;
options.IncludeSubDomains = true;
options.MaxAge = TimeSpan.FromDays(365);
});
๐ Data Protection
// Input validation with FluentValidation
public class CreateUserValidator : AbstractValidator<CreateUserRequest>
{
public CreateUserValidator()
{
RuleFor(x => x.Email)
.NotEmpty()
.EmailAddress()
.MaximumLength(256);
RuleFor(x => x.Password)
.NotEmpty()
.MinimumLength(8)
.Matches(@"^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]")
.WithMessage("Password must contain uppercase, lowercase, number, and special character");
}
}
// SQL injection prevention with parameterized queries
public async Task<User> GetUserByEmailAsync(string email)
{
// โ
Good: Parameterized query
var sql = "SELECT * FROM Users WHERE Email = @Email";
return await _connection.QuerySingleOrDefaultAsync<User>(sql, new { Email = email });
// โ Bad: String concatenation
// var sql = $"SELECT * FROM Users WHERE Email = '{email}'";
}
Performance Optimization
โก Async/Await Best Practices
// โ
Good: Async all the way
public class UserController : ControllerBase
{
[HttpGet]
public async Task<ActionResult<IEnumerable<User>>> GetUsersAsync()
{
var users = await _userService.GetAllUsersAsync();
return Ok(users);
}
}
// โ
Good: Configure await false in libraries
public async Task<User> GetUserAsync(int id)
{
var user = await _repository.GetByIdAsync(id).ConfigureAwait(false);
return user;
}
// โ Bad: Blocking async calls
public User GetUserSync(int id)
{
return _userService.GetUserAsync(id).Result; // Can cause deadlocks
}
๐๏ธ Database Optimization
// Entity Framework optimizations
public class ApplicationDbContext : DbContext
{
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
// Connection pooling
optionsBuilder.EnableServiceProviderCaching();
optionsBuilder.EnableSensitiveDataLogging(false);
// Query optimization
optionsBuilder.EnableSplitQueries();
}
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
// Indexes for performance
modelBuilder.Entity<User>()
.HasIndex(u => u.Email)
.IsUnique();
// Query filters for soft delete
modelBuilder.Entity<User>()
.HasQueryFilter(u => !u.IsDeleted);
}
}
// โ
Good: Projection instead of full entities
public async Task<IEnumerable<UserDto>> GetUserSummariesAsync()
{
return await _context.Users
.Where(u => u.IsActive)
.Select(u => new UserDto
{
Id = u.Id,
Name = u.Name,
Email = u.Email
})
.ToListAsync();
}
// โ
Good: Use AsNoTracking for read-only queries
public async Task<IEnumerable<User>> GetReadOnlyUsersAsync()
{
return await _context.Users
.AsNoTracking()
.ToListAsync();
}
๐พ Caching Strategies
// Memory caching
builder.Services.AddMemoryCache();
public class UserService
{
private readonly IMemoryCache _cache;
private readonly IUserRepository _repository;
public async Task<User> GetUserAsync(int id)
{
var cacheKey = $"user_{id}";
if (_cache.TryGetValue(cacheKey, out User cachedUser))
{
return cachedUser;
}
var user = await _repository.GetByIdAsync(id);
_cache.Set(cacheKey, user, TimeSpan.FromMinutes(30));
return user;
}
}
// Distributed caching with Redis
builder.Services.AddStackExchangeRedisCache(options =>
{
options.Configuration = builder.Configuration.GetConnectionString("Redis");
});
public class DistributedUserService
{
private readonly IDistributedCache _cache;
public async Task<User> GetUserAsync(int id)
{
var cacheKey = $"user_{id}";
var cachedUserJson = await _cache.GetStringAsync(cacheKey);
if (!string.IsNullOrEmpty(cachedUserJson))
{
return JsonSerializer.Deserialize<User>(cachedUserJson);
}
var user = await _repository.GetByIdAsync(id);
var userJson = JsonSerializer.Serialize(user);
await _cache.SetStringAsync(cacheKey, userJson, new DistributedCacheEntryOptions
{
AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(30)
});
return user;
}
}
Testing Strategies
๐งช Unit Testing Setup
// Test project structure
Tests/
โโโ UnitTests/
โ โโโ Controllers/
โ โโโ Services/
โ โโโ Repositories/
โโโ IntegrationTests/
โ โโโ Controllers/
โ โโโ Endpoints/
โโโ TestUtilities/
โโโ Fixtures/
โโโ Builders/
// Example unit test with xUnit and Moq
public class UserServiceTests
{
private readonly Mock<IUserRepository> _mockRepository;
private readonly Mock<ILogger<UserService>> _mockLogger;
private readonly UserService _userService;
public UserServiceTests()
{
_mockRepository = new Mock<IUserRepository>();
_mockLogger = new Mock<ILogger<UserService>>();
_userService = new UserService(_mockRepository.Object, _mockLogger.Object);
}
[Fact]
public async Task GetUserAsync_WithValidId_ReturnsUser()
{
// Arrange
var userId = 1;
var expectedUser = new User { Id = userId, Name = "John Doe" };
_mockRepository.Setup(r => r.GetByIdAsync(userId))
.ReturnsAsync(expectedUser);
// Act
var result = await _userService.GetUserAsync(userId);
// Assert
Assert.NotNull(result);
Assert.Equal(expectedUser.Id, result.Id);
Assert.Equal(expectedUser.Name, result.Name);
}
[Fact]
public async Task GetUserAsync_WithInvalidId_ThrowsException()
{
// Arrange
var userId = -1;
_mockRepository.Setup(r => r.GetByIdAsync(userId))
.ThrowsAsync(new ArgumentException("Invalid user ID"));
// Act & Assert
await Assert.ThrowsAsync<ArgumentException>(() => _userService.GetUserAsync(userId));
}
}
๐ Integration Testing
// Custom web application factory
public class CustomWebApplicationFactory<TStartup> : WebApplicationFactory<TStartup>
where TStartup : class
{
protected override void ConfigureWebHost(IWebHostBuilder builder)
{
builder.ConfigureServices(services =>
{
// Remove the app's ApplicationDbContext registration
var descriptor = services.SingleOrDefault(
d => d.ServiceType == typeof(DbContextOptions<ApplicationDbContext>));
if (descriptor != null)
{
services.Remove(descriptor);
}
// Add ApplicationDbContext using an in-memory database for testing
services.AddDbContext<ApplicationDbContext>(options =>
{
options.UseInMemoryDatabase("InMemoryDbForTesting");
});
});
}
}
// Integration test example
public class UsersControllerIntegrationTests : IClassFixture<CustomWebApplicationFactory<Program>>
{
private readonly CustomWebApplicationFactory<Program> _factory;
private readonly HttpClient _client;
public UsersControllerIntegrationTests(CustomWebApplicationFactory<Program> factory)
{
_factory = factory;
_client = _factory.CreateClient();
}
[Fact]
public async Task GetUsers_ReturnsSuccessAndCorrectContentType()
{
// Act
var response = await _client.GetAsync("/api/users");
// Assert
response.EnsureSuccessStatusCode();
Assert.Equal("application/json; charset=utf-8",
response.Content.Headers.ContentType?.ToString());
}
}
Development Workflow
๐ Code Quality Tools
<!-- .editorconfig -->
root = true
[*.cs]
# Indentation preferences
indent_style = space
indent_size = 4
# Code style rules
csharp_new_line_before_open_brace = all
csharp_new_line_before_else = true
csharp_new_line_before_catch = true
# Naming conventions
dotnet_naming_rule.private_members_with_underscore.symbols = private_fields
dotnet_naming_rule.private_members_with_underscore.style = prefix_underscore
dotnet_naming_symbols.private_fields.applicable_kinds = field
dotnet_naming_symbols.private_fields.applicable_accessibilities = private
dotnet_naming_style.prefix_underscore.capitalization = camel_case
dotnet_naming_style.prefix_underscore.required_prefix = _
๐ Health Checks
// Health checks setup
builder.Services.AddHealthChecks()
.AddDbContext<ApplicationDbContext>()
.AddRedis(builder.Configuration.GetConnectionString("Redis"))
.AddCheck<CustomHealthCheck>("custom_check");
app.MapHealthChecks("/health");
app.MapHealthChecks("/health/detail", new HealthCheckOptions
{
ResponseWriter = UIResponseWriter.WriteHealthCheckUIResponse
});
// Custom health check
public class CustomHealthCheck : IHealthCheck
{
public Task<HealthCheckResult> CheckHealthAsync(
HealthCheckContext context,
CancellationToken cancellationToken = default)
{
// Custom health check logic
var isHealthy = true; // Your check here
return Task.FromResult(isHealthy
? HealthCheckResult.Healthy("Service is healthy")
: HealthCheckResult.Unhealthy("Service is unhealthy"));
}
}
Related Notes
Quick Reference Commands
Development Commands
# Create new project with template
dotnet new webapi -n MyApi --framework net8.0
# Add packages
dotnet add package Microsoft.EntityFrameworkCore.SqlServer
dotnet add package Serilog.AspNetCore
# Run with specific environment
dotnet run --environment Development
# Watch for changes
dotnet watch run
Testing Commands
# Run all tests
dotnet test
# Run tests with coverage
dotnet test --collect:"XPlat Code Coverage"
# Run specific test
dotnet test --filter "FullyQualifiedName~UserServiceTests"
Tags
#aspnetcore #dotnet #bestpractices #architecture #security #performance #testing
Keep this guide updated as new patterns and practices emerge in the .NET ecosystem.