---
description: This guide provides opinionated, actionable best practices for building high-performance, maintainable, and secure ASP.NET Core 9+ applications, focusing on modern C# patterns and common pitfalls.
globs: **/*
---
# asp-net Best Practices

This document outlines the definitive best practices for developing ASP.NET Core applications within our team. Adhere to these guidelines to ensure consistent, performant, and maintainable backend services.

## 1. Code Organization and Structure

Organize your projects by feature or bounded context, not by technical layer. This improves cohesion and reduces coupling.

### ✅ GOOD: Feature-First Organization

```csharp
// Project structure for a "Product" feature
src/
├── MyProject.Api/
│   ├── Program.cs
│   ├── Features/
│   │   ├── Products/
│   │   │   ├── ProductEndpoints.cs // Minimal API definitions
│   │   │   ├── ProductController.cs // API Controller (if used)
│   │   │   ├── GetProduct.cs      // Request/Response DTOs, Handler (e.g., MediatR)
│   │   │   ├── CreateProduct.cs
│   │   │   └── ProductService.cs  // Feature-specific business logic
│   ├── Common/                  // Shared infrastructure, middleware, extensions
│   │   ├── Filters/
│   │   ├── Middleware/
│   │   └── Extensions/
├── MyProject.Application/       // Application-specific services, interfaces, DTOs
│   ├── Interfaces/
│   ├── Services/
│   └── DTOs/
├── MyProject.Domain/            // Core domain entities, value objects, interfaces
├── MyProject.Infrastructure/    // Data access (EF Core), external services
│   ├── Data/
│   ├── Repositories/
│   └── ExternalServices/
└── MyProject.Tests/             // Unit and Integration tests
```

### ❌ BAD: Layer-First Organization

```csharp
// Avoid this structure for larger applications
src/
├── MyProject.Api/
│   ├── Controllers/ // All controllers here
│   ├── Services/    // All services here
│   ├── Repositories/ // All repositories here
│   └── DTOs/        // All DTOs here
```

## 2. API Design: Minimal APIs vs. API Controllers

**Always prefer Minimal APIs** for new endpoints due to their simplicity and directness. Use API Controllers only when you require advanced features like automatic model binding from multiple sources, built-in validation attributes, or complex attribute routing that Minimal APIs don't easily provide.

### ✅ GOOD: Minimal API for Simplicity

```csharp
// Program.cs or dedicated endpoint file
app.MapGet("/products/{id}", async (Guid id, IProductService service) =>
{
    var product = await service.GetProductByIdAsync(id);
    return product is not null ? Results.Ok(product) : Results.NotFound();
})
.WithName("GetProductById")
.Produces<ProductDto>(StatusCodes.Status200OK)
.Produces(StatusCodes.Status404NotFound);
```

### ✅ GOOD: API Controller for Complex Scenarios

```csharp
[ApiController]
[Route("api/[controller]")]
public class OrdersController : ControllerBase
{
    private readonly IOrderService _orderService;

    public OrdersController(IOrderService orderService) => _orderService = orderService;

    [HttpPost]
    [ProducesResponseType(StatusCodes.Status201Created)]
    [ProducesResponseType(StatusCodes.Status400BadRequest)]
    public async Task<IActionResult> CreateOrder([FromBody] CreateOrderRequest request)
    {
        if (!ModelState.IsValid) return BadRequest(ModelState);
        var order = await _orderService.CreateOrderAsync(request);
        return CreatedAtAction(nameof(GetOrderById), new { id = order.Id }, order);
    }
}
```

## 3. Performance Considerations

### 3.1. Avoid Blocking Calls

**Always use `async/await`** for I/O-bound operations (database, network, file system). Never block the thread pool.

### ❌ BAD: Blocking Calls

```csharp
// Synchronous I/O in an async context
public Product GetProduct(Guid id)
{
    // This blocks the calling thread while waiting for the DB
    return _dbContext.Products.Find(id);
}

// Blocking on an async task
var result = SomeAsyncTask().Result; // Blocks!
SomeAsyncTask().Wait();             // Blocks!
```

### ✅ GOOD: Asynchronous I/O

```csharp
public async Task<Product?> GetProductAsync(Guid id)
{
    return await _dbContext.Products.FindAsync(id);
}

// Correctly awaiting an async task
var result = await SomeAsyncTask();
```

### 3.2. Return Paged Collections

**Always paginate large collections** to prevent `OutOfMemoryException` and slow responses.

### ❌ BAD: Returning Large Collections Unpaged

```csharp
app.MapGet("/products", async (IProductService service) =>
{
    var allProducts = await service.GetAllProductsAsync(); // Potentially huge
    return Results.Ok(allProducts);
});
```

### ✅ GOOD: Paging Collections

```csharp
app.MapGet("/products", async (int page = 1, int pageSize = 20, IProductService service) =>
{
    var pagedProducts = await service.GetPagedProductsAsync(page, pageSize);
    return Results.Ok(pagedProducts);
});
```

### 3.3. Minimize Large Object Allocations

Cache frequently used large objects. Avoid creating large objects (>= 85KB) in hot code paths to reduce GC pressure. Use `ArrayPool<T>` for large arrays.

## 4. Dependency Injection (DI)

**Always use ASP.NET Core's built-in DI container.** Register services with the correct lifetime.

*   **Singleton**: For stateless services, configuration objects, or services that manage shared state.
*   **Scoped**: For services tied to a single HTTP request (e.g., `DbContext`, `IUnitOfWork`).
*   **Transient**: For lightweight services that should be new for every injection (rarely needed for typical backend services).

### ✅ GOOD: Correct Service Lifetimes

```csharp
public void ConfigureServices(IServiceCollection services)
{
    // Caching service can be shared across the app
    services.AddSingleton<ICacheService, RedisCacheService>();

    // Database context is scoped to the request
    services.AddDbContext<AppDbContext>(options => /* ... */, ServiceLifetime.Scoped);
    services.AddScoped<IProductRepository, ProductRepository>();

    // Business logic service, typically scoped
    services.AddScoped<IProductService, ProductService>();
}
```

## 5. Security Best Practices

### 5.1. Authentication and Authorization

**Always secure your API endpoints.** Use JWT Bearer tokens for API authentication. Define and enforce authorization policies.

### ✅ GOOD: Policy-Based Authorization

```csharp
// Program.cs
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
    .AddJwtBearer(options => { /* ... configure JWT ... */ });
builder.Services.AddAuthorization(options =>
{
    options.AddPolicy("CanManageProducts", policy =>
        policy.RequireRole("Admin", "ProductManager"));
});

// Minimal API endpoint
app.MapPost("/products", async (CreateProductRequest request, IProductService service) =>
{
    // ...
}).RequireAuthorization("CanManageProducts");

// Controller action
[Authorize(Policy = "CanManageProducts")]
[HttpPost]
public async Task<IActionResult> CreateProduct([FromBody] CreateProductRequest request)
{ /* ... */ }
```

### 5.2. Input Validation

**Always validate all incoming request data.** Use Data Annotations or FluentValidation for robust validation.

### ✅ GOOD: DTO Validation

```csharp
public record CreateProductRequest(
    [Required] [StringLength(100)] string Name,
    [Range(0.01, 10000.00)] decimal Price,
    [Url] string? ImageUrl
);

// In Minimal API or Controller, validation is often automatic with [ApiController]
// For Minimal APIs, you might explicitly check if not using a framework that handles it:
app.MapPost("/products", (CreateProductRequest request) =>
{
    var validationContext = new ValidationContext(request);
    var validationResults = new List<ValidationResult>();
    if (!Validator.TryValidateObject(request, validationContext, validationResults, true))
    {
        return Results.ValidationProblem(validationResults.ToDictionary(r => r.MemberNames.First(), r => new[] { r.ErrorMessage }));
    }
    // ... process valid request
});
```

## 6. Error Handling

**Implement global exception handling middleware.** Provide consistent, developer-friendly error responses without exposing sensitive details.

### ✅ GOOD: Global Exception Handler

```csharp
// Program.cs
app.UseExceptionHandler(appBuilder =>
{
    appBuilder.Run(async context =>
    {
        var exceptionHandlerPathFeature = context.Features.Get<IExceptionHandlerPathFeature>();
        var exception = exceptionHandlerPathFeature?.Error;

        var problemDetails = new ProblemDetails
        {
            Status = StatusCodes.Status500InternalServerError,
            Title = "An error occurred while processing your request.",
            Detail = "Please try again later. If the problem persists, contact support.",
            Instance = context.Request.Path
        };

        // Log the exception details for internal monitoring
        var logger = context.RequestServices.GetRequiredService<ILogger<Program>>();
        logger.LogError(exception, "Unhandled exception for request {Path}", context.Request.Path);

        context.Response.StatusCode = problemDetails.Status.Value;
        context.Response.ContentType = "application/problem+json";
        await context.Response.WriteAsJsonAsync(problemDetails);
    });
});
```

## 7. Structured Logging

**Always use structured logging** to enable easy querying and analysis of logs. Prefer `ILogger<T>` for context-specific logging.

### ✅ GOOD: Structured Logging

```csharp
public class ProductService : IProductService
{
    private readonly ILogger<ProductService> _logger;

    public ProductService(ILogger<ProductService> logger) => _logger = logger;

    public async Task<ProductDto> CreateProductAsync(CreateProductRequest request)
    {
        _logger.LogInformation("Creating product {ProductName} with price {ProductPrice}", request.Name, request.Price);
        // ...
        _logger.LogInformation("Product {ProductId} created successfully.", newProduct.Id);
        return newProductDto;
    }
}
```

### ❌ BAD: Unstructured Logging

```csharp
_logger.LogInformation("Creating product with name: " + request.Name + " and price: " + request.Price);
```

## 8. Testing Approaches

**Prioritize unit and integration tests.**

*   **Unit Tests**: Focus on isolated business logic (services, domain models). Mock all external dependencies.
*   **Integration Tests**: Verify API endpoints, middleware, and database interactions. Use `WebApplicationFactory<TStartup>` for in-memory testing.

### ✅ GOOD: Unit Test Example

```csharp
public class ProductServiceTests
{
    [Fact]
    public async Task CreateProductAsync_ValidRequest_ReturnsProductDto()
    {
        // Arrange
        var mockRepo = new Mock<IProductRepository>();
        mockRepo.Setup(r => r.AddAsync(It.IsAny<Product>())).ReturnsAsync(new Product { Id = Guid.NewGuid(), Name = "Test", Price = 10m });
        var service = new ProductService(mockRepo.Object, Mock.Of<ILogger<ProductService>>());
        var request = new CreateProductRequest("Test Product", 10.00m, null);

        // Act
        var result = await service.CreateProductAsync(request);

        // Assert
        Assert.NotNull(result);
        Assert.Equal("Test Product", result.Name);
        mockRepo.Verify(r => r.AddAsync(It.IsAny<Product>()), Times.Once);
    }
}
```

### ✅ GOOD: Integration Test Example

```csharp
public class ProductsApiTests : IClassFixture<CustomWebApplicationFactory<Program>>
{
    private readonly HttpClient _client;

    public ProductsApiTests(CustomWebApplicationFactory<Program> factory)
    {
        _client = factory.CreateClient();
    }

    [Fact]
    public async Task GetProducts_ReturnsSuccessAndCorrectContentType()
    {
        // Act
        var response = await _client.GetAsync("/products");

        // Assert
        response.EnsureSuccessStatusCode(); // Status Code 200-299
        Assert.Equal("application/json; charset=utf-8", response.Content.Headers.ContentType?.ToString());
    }
}
```
