.NET & C# Development Skill
Expert guidance for building modern .NET 8+ applications with C#, ASP.NET Core, Entity Framework Core, and best practices.
📑 Table of Contents
📋 Quick Reference
.NET CLI Commands
| Command |
Description |
dotnet new webapi |
Create new Web API project |
dotnet new mvc |
Create new MVC project |
dotnet new classlib |
Create class library |
dotnet new xunit |
Create xUnit test project |
dotnet build |
Build the project |
dotnet run |
Run the application |
dotnet test |
Run tests |
dotnet publish -c Release |
Publish for deployment |
dotnet ef migrations add <Name> |
Create EF migration |
dotnet ef database update |
Apply migrations |
Project Templates
| Template |
Use Case |
webapi |
REST API with controllers or minimal APIs |
web |
Empty ASP.NET Core project |
mvc |
MVC web application |
razor |
Razor Pages application |
blazorserver |
Blazor Server app |
blazorwasm |
Blazor WebAssembly app |
worker |
Background service |
grpc |
gRPC service |
C# 12 Features (.NET 8+)
| Feature |
Example |
| Primary Constructors |
class Person(string name, int age) |
| Collection Expressions |
int[] nums = [1, 2, 3]; |
| Alias Any Type |
using Point = (int X, int Y); |
| Default Lambda Parameters |
var add = (int a, int b = 1) => a + b; |
| Inline Arrays |
[InlineArray(10)] struct Buffer { int _element; } |
🚀 Project Setup
Solution Structure
MySolution/
├── src/
│ ├── MyApp.Api/ # Web API project
│ │ ├── Controllers/
│ │ ├── Endpoints/ # Minimal API endpoints
│ │ ├── Middleware/
│ │ ├── Program.cs
│ │ └── appsettings.json
│ ├── MyApp.Core/ # Domain/business logic
│ │ ├── Entities/
│ │ ├── Interfaces/
│ │ └── Services/
│ ├── MyApp.Infrastructure/ # Data access, external services
│ │ ├── Data/
│ │ ├── Repositories/
│ │ └── Services/
│ └── MyApp.Shared/ # Shared DTOs, utilities
│ ├── DTOs/
│ └── Extensions/
├── tests/
│ ├── MyApp.UnitTests/
│ ├── MyApp.IntegrationTests/
│ └── MyApp.FunctionalTests/
├── Directory.Build.props # Shared build properties
├── Directory.Packages.props # Central package management
└── MySolution.sln
Directory.Build.props
<Project>
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<AnalysisLevel>latest-recommended</AnalysisLevel>
</PropertyGroup>
</Project>
Central Package Management (Directory.Packages.props)
<Project>
<PropertyGroup>
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
</PropertyGroup>
<ItemGroup>
<PackageVersion Include="Microsoft.EntityFrameworkCore" Version="8.0.0" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.SqlServer" Version="8.0.0" />
<PackageVersion Include="Swashbuckle.AspNetCore" Version="6.5.0" />
<PackageVersion Include="FluentValidation" Version="11.9.0" />
<PackageVersion Include="Serilog.AspNetCore" Version="8.0.0" />
<PackageVersion Include="xunit" Version="2.6.4" />
<PackageVersion Include="Moq" Version="4.20.70" />
</ItemGroup>
</Project>
🌐 ASP.NET Core
Program.cs (Modern Setup)
using MyApp.Api.Middleware;
using MyApp.Core.Interfaces;
using MyApp.Infrastructure.Data;
using MyApp.Infrastructure.Services;
var builder = WebApplication.CreateBuilder(args);
// Configuration
builder.Configuration
.AddJsonFile("appsettings.json", optional: false)
.AddJsonFile($"appsettings.{builder.Environment.EnvironmentName}.json", optional: true)
.AddEnvironmentVariables()
.AddUserSecrets<Program>(optional: true);
// Services
builder.Services.AddControllers();
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
// Database
builder.Services.AddDbContext<AppDbContext>(options =>
options.UseSqlServer(builder.Configuration.GetConnectionString("DefaultConnection")));
// Application services
builder.Services.AddScoped<IUserService, UserService>();
builder.Services.AddScoped<IEmailService, EmailService>();
// Health checks
builder.Services.AddHealthChecks()
.AddDbContextCheck<AppDbContext>();
var app = builder.Build();
// Middleware pipeline
if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI();
}
app.UseHttpsRedirection();
app.UseAuthentication();
app.UseAuthorization();
app.UseMiddleware<ExceptionHandlingMiddleware>();
app.MapControllers();
app.MapHealthChecks("/health");
app.Run();
// Make Program class accessible for integration tests
public partial class Program { }
Controller Pattern
using Microsoft.AspNetCore.Mvc;
namespace MyApp.Api.Controllers;
[ApiController]
[Route("api/[controller]")]
[Produces("application/json")]
public class UsersController : ControllerBase
{
private readonly IUserService _userService;
private readonly ILogger<UsersController> _logger;
public UsersController(IUserService userService, ILogger<UsersController> logger)
{
_userService = userService;
_logger = logger;
}
/// <summary>
/// Gets all users with optional filtering
/// </summary>
[HttpGet]
[ProducesResponseType(typeof(IEnumerable<UserDto>), StatusCodes.Status200OK)]
public async Task<ActionResult<IEnumerable<UserDto>>> GetUsers(
[FromQuery] string? search = null,
[FromQuery] int page = 1,
[FromQuery] int pageSize = 10,
CancellationToken cancellationToken = default)
{
var users = await _userService.GetUsersAsync(search, page, pageSize, cancellationToken);
return Ok(users);
}
/// <summary>
/// Gets a specific user by ID
/// </summary>
[HttpGet("{id:guid}")]
[ProducesResponseType(typeof(UserDto), StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<ActionResult<UserDto>> GetUser(
Guid id,
CancellationToken cancellationToken = default)
{
var user = await _userService.GetByIdAsync(id, cancellationToken);
if (user is null)
{
return NotFound();
}
return Ok(user);
}
/// <summary>
/// Creates a new user
/// </summary>
[HttpPost]
[ProducesResponseType(typeof(UserDto), StatusCodes.Status201Created)]
[ProducesResponseType(typeof(ValidationProblemDetails), StatusCodes.Status400BadRequest)]
public async Task<ActionResult<UserDto>> CreateUser(
[FromBody] CreateUserRequest request,
CancellationToken cancellationToken = default)
{
var user = await _userService.CreateAsync(request, cancellationToken);
_logger.LogInformation("Created user {UserId}", user.Id);
return CreatedAtAction(
nameof(GetUser),
new { id = user.Id },
user);
}
/// <summary>
/// Updates an existing user
/// </summary>
[HttpPut("{id:guid}")]
[ProducesResponseType(typeof(UserDto), StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<ActionResult<UserDto>> UpdateUser(
Guid id,
[FromBody] UpdateUserRequest request,
CancellationToken cancellationToken = default)
{
var user = await _userService.UpdateAsync(id, request, cancellationToken);
if (user is null)
{
return NotFound();
}
return Ok(user);
}
/// <summary>
/// Deletes a user
/// </summary>
[HttpDelete("{id:guid}")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<IActionResult> DeleteUser(
Guid id,
CancellationToken cancellationToken = default)
{
var deleted = await _userService.DeleteAsync(id, cancellationToken);
if (!deleted)
{
return NotFound();
}
return NoContent();
}
}
⚡ Minimal APIs
Basic Setup
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
builder.Services.AddScoped<IUserService, UserService>();
var app = builder.Build();
if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI();
}
// Map endpoints
app.MapUserEndpoints();
app.MapProductEndpoints();
app.Run();
Organized Endpoints
namespace MyApp.Api.Endpoints;
public static class UserEndpoints
{
public static void MapUserEndpoints(this IEndpointRouteBuilder routes)
{
var group = routes.MapGroup("/api/users")
.WithTags("Users")
.WithOpenApi();
group.MapGet("/", GetUsers)
.WithName("GetUsers")
.WithSummary("Gets all users");
group.MapGet("/{id:guid}", GetUserById)
.WithName("GetUserById")
.WithSummary("Gets a user by ID");
group.MapPost("/", CreateUser)
.WithName("CreateUser")
.WithSummary("Creates a new user")
.AddEndpointFilter<ValidationFilter<CreateUserRequest>>();
group.MapPut("/{id:guid}", UpdateUser)
.WithName("UpdateUser")
.RequireAuthorization();
group.MapDelete("/{id:guid}", DeleteUser)
.WithName("DeleteUser")
.RequireAuthorization("AdminOnly");
}
private static async Task<IResult> GetUsers(
IUserService userService,
[AsParameters] PaginationQuery pagination,
CancellationToken cancellationToken)
{
var users = await userService.GetUsersAsync(
pagination.Page,
pagination.PageSize,
cancellationToken);
return Results.Ok(users);
}
private static async Task<IResult> GetUserById(
Guid id,
IUserService userService,
CancellationToken cancellationToken)
{
var user = await userService.GetByIdAsync(id, cancellationToken);
return user is null
? Results.NotFound()
: Results.Ok(user);
}
private static async Task<IResult> CreateUser(
CreateUserRequest request,
IUserService userService,
CancellationToken cancellationToken)
{
var user = await userService.CreateAsync(request, cancellationToken);
return Results.CreatedAtRoute(
"GetUserById",
new { id = user.Id },
user);
}
private static async Task<IResult> UpdateUser(
Guid id,
UpdateUserRequest request,
IUserService userService,
CancellationToken cancellationToken)
{
var user = await userService.UpdateAsync(id, request, cancellationToken);
return user is null
? Results.NotFound()
: Results.Ok(user);
}
private static async Task<IResult> DeleteUser(
Guid id,
IUserService userService,
CancellationToken cancellationToken)
{
var deleted = await userService.DeleteAsync(id, cancellationToken);
return deleted
? Results.NoContent()
: Results.NotFound();
}
}
// Parameter binding class
public record PaginationQuery(
[FromQuery] int Page = 1,
[FromQuery] int PageSize = 10);
Typed Results (Better OpenAPI)
public static class UserEndpoints
{
public static void MapUserEndpoints(this IEndpointRouteBuilder routes)
{
routes.MapGet("/api/users/{id:guid}", GetUserById);
}
private static async Task<Results<Ok<UserDto>, NotFound>> GetUserById(
Guid id,
IUserService userService,
CancellationToken cancellationToken)
{
var user = await userService.GetByIdAsync(id, cancellationToken);
return user is null
? TypedResults.NotFound()
: TypedResults.Ok(user);
}
}
🗄️ Entity Framework Core
DbContext
using Microsoft.EntityFrameworkCore;
namespace MyApp.Infrastructure.Data;
public class AppDbContext : DbContext
{
public AppDbContext(DbContextOptions<AppDbContext> options)
: base(options)
{
}
public DbSet<User> Users => Set<User>();
public DbSet<Order> Orders => Set<Order>();
public DbSet<Product> Products => Set<Product>();
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
// Apply all configurations from assembly
modelBuilder.ApplyConfigurationsFromAssembly(
typeof(AppDbContext).Assembly);
// Global query filters
modelBuilder.Entity<User>()
.HasQueryFilter(u => !u.IsDeleted);
base.OnModelCreating(modelBuilder);
}
public override async Task<int> SaveChangesAsync(
CancellationToken cancellationToken = default)
{
// Audit trail
foreach (var entry in ChangeTracker.Entries<IAuditable>())
{
switch (entry.State)
{
case EntityState.Added:
entry.Entity.CreatedAt = DateTime.UtcNow;
break;
case EntityState.Modified:
entry.Entity.UpdatedAt = DateTime.UtcNow;
break;
}
}
return await base.SaveChangesAsync(cancellationToken);
}
}
Entity Configuration
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
namespace MyApp.Infrastructure.Data.Configurations;
public class UserConfiguration : IEntityTypeConfiguration<User>
{
public void Configure(EntityTypeBuilder<User> builder)
{
builder.ToTable("Users");
builder.HasKey(u => u.Id);
builder.Property(u => u.Id)
.ValueGeneratedOnAdd();
builder.Property(u => u.Email)
.IsRequired()
.HasMaxLength(256);
builder.Property(u => u.FirstName)
.IsRequired()
.HasMaxLength(100);
builder.Property(u => u.LastName)
.IsRequired()
.HasMaxLength(100);
builder.Property(u => u.PasswordHash)
.IsRequired();
// Indexes
builder.HasIndex(u => u.Email)
.IsUnique();
// Relationships
builder.HasMany(u => u.Orders)
.WithOne(o => o.User)
.HasForeignKey(o => o.UserId)
.OnDelete(DeleteBehavior.Cascade);
// Value objects
builder.OwnsOne(u => u.Address, address =>
{
address.Property(a => a.Street).HasMaxLength(200);
address.Property(a => a.City).HasMaxLength(100);
address.Property(a => a.ZipCode).HasMaxLength(20);
address.Property(a => a.Country).HasMaxLength(100);
});
// Seed data
builder.HasData(
new User
{
Id = Guid.Parse("11111111-1111-1111-1111-111111111111"),
Email = "admin@example.com",
FirstName = "Admin",
LastName = "User"
});
}
}
Repository Pattern
namespace MyApp.Core.Interfaces;
public interface IRepository<T> where T : class
{
Task<T?> GetByIdAsync(Guid id, CancellationToken cancellationToken = default);
Task<IReadOnlyList<T>> GetAllAsync(CancellationToken cancellationToken = default);
Task<T> AddAsync(T entity, CancellationToken cancellationToken = default);
Task UpdateAsync(T entity, CancellationToken cancellationToken = default);
Task DeleteAsync(T entity, CancellationToken cancellationToken = default);
}
public interface IUserRepository : IRepository<User>
{
Task<User?> GetByEmailAsync(string email, CancellationToken cancellationToken = default);
Task<IReadOnlyList<User>> GetActiveUsersAsync(CancellationToken cancellationToken = default);
}
namespace MyApp.Infrastructure.Repositories;
public class Repository<T> : IRepository<T> where T : class
{
protected readonly AppDbContext _context;
protected readonly DbSet<T> _dbSet;
public Repository(AppDbContext context)
{
_context = context;
_dbSet = context.Set<T>();
}
public virtual async Task<T?> GetByIdAsync(
Guid id,
CancellationToken cancellationToken = default)
{
return await _dbSet.FindAsync([id], cancellationToken);
}
public virtual async Task<IReadOnlyList<T>> GetAllAsync(
CancellationToken cancellationToken = default)
{
return await _dbSet.ToListAsync(cancellationToken);
}
public virtual async Task<T> AddAsync(
T entity,
CancellationToken cancellationToken = default)
{
await _dbSet.AddAsync(entity, cancellationToken);
await _context.SaveChangesAsync(cancellationToken);
return entity;
}
public virtual async Task UpdateAsync(
T entity,
CancellationToken cancellationToken = default)
{
_dbSet.Update(entity);
await _context.SaveChangesAsync(cancellationToken);
}
public virtual async Task DeleteAsync(
T entity,
CancellationToken cancellationToken = default)
{
_dbSet.Remove(entity);
await _context.SaveChangesAsync(cancellationToken);
}
}
public class UserRepository : Repository<User>, IUserRepository
{
public UserRepository(AppDbContext context) : base(context)
{
}
public async Task<User?> GetByEmailAsync(
string email,
CancellationToken cancellationToken = default)
{
return await _dbSet
.FirstOrDefaultAsync(u => u.Email == email, cancellationToken);
}
public async Task<IReadOnlyList<User>> GetActiveUsersAsync(
CancellationToken cancellationToken = default)
{
return await _dbSet
.Where(u => u.IsActive)
.OrderBy(u => u.LastName)
.ThenBy(u => u.FirstName)
.ToListAsync(cancellationToken);
}
}
Specification Pattern
namespace MyApp.Core.Specifications;
public abstract class Specification<T>
{
public abstract Expression<Func<T, bool>> ToExpression();
public bool IsSatisfiedBy(T entity)
{
var predicate = ToExpression().Compile();
return predicate(entity);
}
public Specification<T> And(Specification<T> specification)
{
return new AndSpecification<T>(this, specification);
}
public Specification<T> Or(Specification<T> specification)
{
return new OrSpecification<T>(this, specification);
}
}
public class ActiveUserSpecification : Specification<User>
{
public override Expression<Func<User, bool>> ToExpression()
{
return user => user.IsActive && !user.IsDeleted;
}
}
public class UserByEmailSpecification : Specification<User>
{
private readonly string _email;
public UserByEmailSpecification(string email)
{
_email = email;
}
public override Expression<Func<User, bool>> ToExpression()
{
return user => user.Email == _email;
}
}
💉 Dependency Injection
Service Registration
namespace MyApp.Api.Extensions;
public static class ServiceCollectionExtensions
{
public static IServiceCollection AddApplicationServices(
this IServiceCollection services)
{
// Transient - new instance every time
services.AddTransient<IEmailService, EmailService>();
// Scoped - one instance per request
services.AddScoped<IUserService, UserService>();
services.AddScoped<IOrderService, OrderService>();
services.AddScoped(typeof(IRepository<>), typeof(Repository<>));
// Singleton - one instance for app lifetime
services.AddSingleton<ICacheService, MemoryCacheService>();
return services;
}
public static IServiceCollection AddInfrastructure(
this IServiceCollection services,
IConfiguration configuration)
{
// Database
services.AddDbContext<AppDbContext>(options =>
options.UseSqlServer(
configuration.GetConnectionString("DefaultConnection"),
sqlOptions =>
{
sqlOptions.EnableRetryOnFailure(
maxRetryCount: 3,
maxRetryDelay: TimeSpan.FromSeconds(30),
errorNumbersToAdd: null);
sqlOptions.CommandTimeout(30);
}));
// Repositories
services.AddScoped<IUserRepository, UserRepository>();
services.AddScoped<IOrderRepository, OrderRepository>();
// HTTP clients
services.AddHttpClient<IExternalApiClient, ExternalApiClient>(client =>
{
client.BaseAddress = new Uri(configuration["ExternalApi:BaseUrl"]!);
client.Timeout = TimeSpan.FromSeconds(30);
})
.AddPolicyHandler(GetRetryPolicy())
.AddPolicyHandler(GetCircuitBreakerPolicy());
return services;
}
private static IAsyncPolicy<HttpResponseMessage> GetRetryPolicy()
{
return HttpPolicyExtensions
.HandleTransientHttpError()
.WaitAndRetryAsync(3, retryAttempt =>
TimeSpan.FromSeconds(Math.Pow(2, retryAttempt)));
}
private static IAsyncPolicy<HttpResponseMessage> GetCircuitBreakerPolicy()
{
return HttpPolicyExtensions
.HandleTransientHttpError()
.CircuitBreakerAsync(5, TimeSpan.FromSeconds(30));
}
}
Keyed Services (.NET 8+)
// Registration
services.AddKeyedScoped<INotificationService, EmailNotificationService>("email");
services.AddKeyedScoped<INotificationService, SmsNotificationService>("sms");
services.AddKeyedScoped<INotificationService, PushNotificationService>("push");
// Injection
public class NotificationController : ControllerBase
{
public NotificationController(
[FromKeyedServices("email")] INotificationService emailService,
[FromKeyedServices("sms")] INotificationService smsService)
{
// Use specific implementations
}
}
Factory Pattern
public interface IPaymentProcessorFactory
{
IPaymentProcessor Create(PaymentMethod method);
}
public class PaymentProcessorFactory : IPaymentProcessorFactory
{
private readonly IServiceProvider _serviceProvider;
public PaymentProcessorFactory(IServiceProvider serviceProvider)
{
_serviceProvider = serviceProvider;
}
public IPaymentProcessor Create(PaymentMethod method)
{
return method switch
{
PaymentMethod.CreditCard => _serviceProvider
.GetRequiredService<CreditCardProcessor>(),
PaymentMethod.PayPal => _serviceProvider
.GetRequiredService<PayPalProcessor>(),
PaymentMethod.BankTransfer => _serviceProvider
.GetRequiredService<BankTransferProcessor>(),
_ => throw new ArgumentException($"Unknown payment method: {method}")
};
}
}
// Registration
services.AddScoped<IPaymentProcessorFactory, PaymentProcessorFactory>();
services.AddScoped<CreditCardProcessor>();
services.AddScoped<PayPalProcessor>();
services.AddScoped<BankTransferProcessor>();
⚙️ Configuration & Options
appsettings.json Structure
{
"ConnectionStrings": {
"DefaultConnection": "Server=localhost;Database=MyApp;Trusted_Connection=true;",
"Redis": "localhost:6379"
},
"Jwt": {
"Secret": "your-secret-key-here",
"Issuer": "MyApp",
"Audience": "MyApp",
"ExpirationMinutes": 60
},
"Email": {
"SmtpServer": "smtp.example.com",
"Port": 587,
"UseSsl": true,
"FromAddress": "noreply@example.com",
"FromName": "MyApp"
},
"Features": {
"EnableNewDashboard": true,
"MaxUploadSizeMb": 10
},
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning",
"Microsoft.EntityFrameworkCore": "Warning"
}
}
}
Options Pattern
namespace MyApp.Core.Options;
public class JwtOptions
{
public const string SectionName = "Jwt";
public string Secret { get; set; } = string.Empty;
public string Issuer { get; set; } = string.Empty;
public string Audience { get; set; } = string.Empty;
public int ExpirationMinutes { get; set; } = 60;
}
public class EmailOptions
{
public const string SectionName = "Email";
public string SmtpServer { get; set; } = string.Empty;
public int Port { get; set; } = 587;
public bool UseSsl { get; set; } = true;
public string FromAddress { get; set; } = string.Empty;
public string FromName { get; set; } = string.Empty;
}
Options Validation
public class JwtOptionsValidator : IValidateOptions<JwtOptions>
{
public ValidateOptionsResult Validate(string? name, JwtOptions options)
{
var failures = new List<string>();
if (string.IsNullOrWhiteSpace(options.Secret))
{
failures.Add("JWT Secret is required");
}
else if (options.Secret.Length < 32)
{
failures.Add("JWT Secret must be at least 32 characters");
}
if (string.IsNullOrWhiteSpace(options.Issuer))
{
failures.Add("JWT Issuer is required");
}
if (options.ExpirationMinutes <= 0)
{
failures.Add("JWT ExpirationMinutes must be positive");
}
return failures.Count > 0
? ValidateOptionsResult.Fail(failures)
: ValidateOptionsResult.Success;
}
}
// Registration
services.AddOptions<JwtOptions>()
.Bind(configuration.GetSection(JwtOptions.SectionName))
.ValidateDataAnnotations()
.ValidateOnStart();
services.AddSingleton<IValidateOptions<JwtOptions>, JwtOptionsValidator>();
Using Options
public class TokenService : ITokenService
{
private readonly JwtOptions _jwtOptions;
// Use IOptions for singleton-scoped options
public TokenService(IOptions<JwtOptions> jwtOptions)
{
_jwtOptions = jwtOptions.Value;
}
// Use IOptionsSnapshot for scoped options (reloads on change)
public TokenService(IOptionsSnapshot<JwtOptions> jwtOptions)
{
_jwtOptions = jwtOptions.Value;
}
// Use IOptionsMonitor for singleton services that need to react to changes
public TokenService(IOptionsMonitor<JwtOptions> jwtOptions)
{
_jwtOptions = jwtOptions.CurrentValue;
jwtOptions.OnChange(newOptions =>
{
// React to configuration changes
});
}
}
🔧 Middleware & Filters
Custom Middleware
namespace MyApp.Api.Middleware;
public class ExceptionHandlingMiddleware
{
private readonly RequestDelegate _next;
private readonly ILogger<ExceptionHandlingMiddleware> _logger;
public ExceptionHandlingMiddleware(
RequestDelegate next,
ILogger<ExceptionHandlingMiddleware> logger)
{
_next = next;
_logger = logger;
}
public async Task InvokeAsync(HttpContext context)
{
try
{
await _next(context);
}
catch (Exception ex)
{
await HandleExceptionAsync(context, ex);
}
}
private async Task HandleExceptionAsync(HttpContext context, Exception exception)
{
_logger.LogError(exception, "An unhandled exception occurred");
var (statusCode, message) = exception switch
{
NotFoundException => (StatusCodes.Status404NotFound, exception.Message),
ValidationException validationEx => (
StatusCodes.Status400BadRequest,
string.Join("; ", validationEx.Errors)),
UnauthorizedAccessException => (
StatusCodes.Status401Unauthorized,
"Unauthorized"),
ForbiddenException => (
StatusCodes.Status403Forbidden,
"Forbidden"),
_ => (
StatusCodes.Status500InternalServerError,
"An error occurred processing your request")
};
context.Response.StatusCode = statusCode;
context.Response.ContentType = "application/problem+json";
var problemDetails = new ProblemDetails
{
Status = statusCode,
Title = GetTitle(statusCode),
Detail = message,
Instance = context.Request.Path
};
await context.Response.WriteAsJsonAsync(problemDetails);
}
private static string GetTitle(int statusCode) => statusCode switch
{
400 => "Bad Request",
401 => "Unauthorized",
403 => "Forbidden",
404 => "Not Found",
_ => "Server Error"
};
}
// Extension method
public static class MiddlewareExtensions
{
public static IApplicationBuilder UseExceptionHandling(
this IApplicationBuilder app)
{
return app.UseMiddleware<ExceptionHandlingMiddleware>();
}
}
Request Logging Middleware
public class RequestLoggingMiddleware
{
private readonly RequestDelegate _next;
private readonly ILogger<RequestLoggingMiddleware> _logger;
public RequestLoggingMiddleware(
RequestDelegate next,
ILogger<RequestLoggingMiddleware> logger)
{
_next = next;
_logger = logger;
}
public async Task InvokeAsync(HttpContext context)
{
var stopwatch = Stopwatch.StartNew();
var requestId = Activity.Current?.Id ?? context.TraceIdentifier;
_logger.LogInformation(
"Request started: {Method} {Path} [{RequestId}]",
context.Request.Method,
context.Request.Path,
requestId);
try
{
await _next(context);
}
finally
{
stopwatch.Stop();
_logger.LogInformation(
"Request completed: {Method} {Path} [{RequestId}] - {StatusCode} in {ElapsedMs}ms",
context.Request.Method,
context.Request.Path,
requestId,
context.Response.StatusCode,
stopwatch.ElapsedMilliseconds);
}
}
}
Action Filters
namespace MyApp.Api.Filters;
public class ValidationFilter : IAsyncActionFilter
{
public async Task OnActionExecutionAsync(
ActionExecutingContext context,
ActionExecutionDelegate next)
{
if (!context.ModelState.IsValid)
{
var errors = context.ModelState
.Where(x => x.Value?.Errors.Count > 0)
.ToDictionary(
x => x.Key,
x => x.Value!.Errors.Select(e => e.ErrorMessage).ToArray());
context.Result = new BadRequestObjectResult(
new ValidationProblemDetails(context.ModelState));
return;
}
await next();
}
}
// Minimal API filter
public class ValidationFilter<T> : IEndpointFilter where T : class
{
public async ValueTask<object?> InvokeAsync(
EndpointFilterInvocationContext context,
EndpointFilterDelegate next)
{
var validator = context.HttpContext.RequestServices
.GetService<IValidator<T>>();
if (validator is null)
{
return await next(context);
}
var argument = context.Arguments
.OfType<T>()
.FirstOrDefault();
if (argument is null)
{
return await next(context);
}
var validationResult = await validator.ValidateAsync(argument);
if (!validationResult.IsValid)
{
return Results.ValidationProblem(
validationResult.ToDictionary());
}
return await next(context);
}
}
Exception Filter
public class GlobalExceptionFilter : IExceptionFilter
{
private readonly ILogger<GlobalExceptionFilter> _logger;
private readonly IHostEnvironment _environment;
public GlobalExceptionFilter(
ILogger<GlobalExceptionFilter> logger,
IHostEnvironment environment)
{
_logger = logger;
_environment = environment;
}
public void OnException(ExceptionContext context)
{
_logger.LogError(context.Exception, "Unhandled exception occurred");
var problemDetails = new ProblemDetails
{
Status = StatusCodes.Status500InternalServerError,
Title = "An error occurred",
Detail = _environment.IsDevelopment()
? context.Exception.Message
: "An error occurred processing your request"
};
context.Result = new ObjectResult(problemDetails)
{
StatusCode = StatusCodes.Status500InternalServerError
};
context.ExceptionHandled = true;
}
}
// Registration
services.AddControllers(options =>
{
options.Filters.Add<GlobalExceptionFilter>();
});
🔐 Authentication & Authorization
JWT Authentication
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.IdentityModel.Tokens;
public static class AuthenticationExtensions
{
public static IServiceCollection AddJwtAuthentication(
this IServiceCollection services,
IConfiguration configuration)
{
var jwtOptions = configuration
.GetSection(JwtOptions.SectionName)
.Get<JwtOptions>()!;
services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer(options =>
{
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidateAudience = true,
ValidateLifetime = true,
ValidateIssuerSigningKey = true,
ValidIssuer = jwtOptions.Issuer,
ValidAudience = jwtOptions.Audience,
IssuerSigningKey = new SymmetricSecurityKey(
Encoding.UTF8.GetBytes(jwtOptions.Secret)),
ClockSkew = TimeSpan.Zero
};
options.Events = new JwtBearerEvents
{
=>
{
if (context.Exception is SecurityTokenExpiredException)
{
context.Response.Headers.Append(
"Token-Expired", "true");
}
return Task.CompletedTask;
}
};
});
return services;
}
}
Token Service
public interface ITokenService
{
string GenerateAccessToken(User user);
string GenerateRefreshToken();
ClaimsPrincipal? ValidateToken(string token);
}
public class TokenService : ITokenService
{
private readonly JwtOptions _options;
public TokenService(IOptions<JwtOptions> options)
{
_options = options.Value;
}
public string GenerateAccessToken(User user)
{
var claims = new List<Claim>
{
new(ClaimTypes.NameIdentifier, user.Id.ToString()),
new(ClaimTypes.Email, user.Email),
new(ClaimTypes.Name, $"{user.FirstName} {user.LastName}"),
new("role", user.Role.ToString())
};
var key = new SymmetricSecurityKey(
Encoding.UTF8.GetBytes(_options.Secret));
var credentials = new SigningCredentials(
key, SecurityAlgorithms.HmacSha256);
var token = new JwtSecurityToken(
issuer: _options.Issuer,
audience: _options.Audience,
claims: claims,
expires: DateTime.UtcNow.AddMinutes(_options.ExpirationMinutes),
signingCredentials: credentials);
return new JwtSecurityTokenHandler().WriteToken(token);
}
public string GenerateRefreshToken()
{
var randomBytes = new byte[64];
using var rng = RandomNumberGenerator.Create();
rng.GetBytes(randomBytes);
return Convert.ToBase64String(randomBytes);
}
public ClaimsPrincipal? ValidateToken(string token)
{
var tokenHandler = new JwtSecurityTokenHandler();
var key = Encoding.UTF8.GetBytes(_options.Secret);
try
{
return tokenHandler.ValidateToken(token,
new TokenValidationParameters
{
ValidateIssuerSigningKey = true,
IssuerSigningKey = new SymmetricSecurityKey(key),
ValidateIssuer = true,
ValidIssuer = _options.Issuer,
ValidateAudience = true,
ValidAudience = _options.Audience,
ValidateLifetime = false // Allow expired tokens for refresh
}, out _);
}
catch
{
return null;
}
}
}
Authorization Policies
public static class AuthorizationExtensions
{
public static IServiceCollection AddAuthorizationPolicies(
this IServiceCollection services)
{
services.AddAuthorization(options =>
{
// Role-based policies
options.AddPolicy("AdminOnly", policy =>
policy.RequireRole("Admin"));
options.AddPolicy("ManagerOrAdmin", policy =>
policy.RequireRole("Manager", "Admin"));
// Claim-based policies
options.AddPolicy("VerifiedEmail", policy =>
policy.RequireClaim("email_verified", "true"));
// Custom requirement policies
options.AddPolicy("MinimumAge", policy =>
policy.AddRequirements(new MinimumAgeRequirement(18)));
// Resource-based authorization
options.AddPolicy("OwnerOnly", policy =>
policy.AddRequirements(new ResourceOwnerRequirement()));
// Combined polic
…(truncated)