# Building Dotnet Backend

> ASP.NET Core Minimal APIs, Controllers, dependency injection, middleware, background services, options pattern, health checks

- Skill: `gitwalter/building-dotnet-backend` (Agent Skill, multi-file: 6 files)
- Install (CLI): `npx skillmds@latest add gitwalter/building-dotnet-backend`
- Raw SKILL.md: https://api.skillmd.com/api/skills/gitwalter/building-dotnet-backend/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: AI & ML
- Author: gitwalter (https://skillmd.com/u/gitwalter)
- Updated: 2026-09-10
- Page: https://skillmd.com/skills/gitwalter/building-dotnet-backend

---

# Dotnet Backend

ASP.NET Core Minimal APIs, Controllers, dependency injection, middleware, background services, options pattern, health checks

Build production ASP.NET Core applications using Minimal APIs, Controllers, dependency injection, middleware, and background services.

## Process

1. Review the task requirements.
2. Apply the skill's methodology.
3. Validate the output against the defined criteria.
### Step 1: Minimal APIs Setup

Minimal APIs provide a lightweight way to build HTTP APIs:

```csharp
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

// Simple GET endpoint
app.MapGet("/", () => "Hello World!");

// GET with route parameter
app.MapGet("/users/{id:int}", (int id) =>
    Results.Ok(new { UserId = id, Name = "John Doe" }));

// POST with request body
app.MapPost("/users", (User user) =>
{
    // Process user
    return Results.Created($"/users/{user.Id}", user);
});

// PUT endpoint
app.MapPut("/users/{id:int}", (int id, User user) =>
{
    // Update user
    return Results.Ok(user);
});

// DELETE endpoint
app.MapDelete("/users/{id:int}", (int id) =>
{
    // Delete user
    return Results.NoContent();
});

app.Run();
```

### Step 2: Controllers Pattern

Traditional MVC controllers for complex scenarios:

```csharp
[ApiController]
[Route("api/[controller]")]
public class ProductsController : ControllerBase
{
    private readonly IProductService _productService;
    private readonly ILogger<ProductsController> _logger;

    public ProductsController(
        IProductService productService,
        ILogger<ProductsController> logger)
    {
        _productService = productService;
        _logger = logger;
    }

    [HttpGet]
    public async Task<ActionResult<IEnumerable<ProductDto>>> GetProducts(
        CancellationToken cancellationToken)
    {
        var products = await _productService.GetAllAsync(cancellationToken);
        return Ok(products);
    }

    [HttpGet("{id:int}")]
    public async Task<ActionResult<ProductDto>> GetProduct(
        int id,
        CancellationToken cancellationToken)
    {
        var product = await _productService.GetByIdAsync(id, cancellationToken);

        if (product == null)
            return NotFound();

        return Ok(product);
    }

    [HttpPost]
    public async Task<ActionResult<ProductDto>> CreateProduct(
        CreateProductDto dto,
        CancellationToken cancellationToken)
    {
        var product = await _productService.CreateAsync(dto, cancellationToken);
        return CreatedAtAction(
            nameof(GetProduct),
            new { id = product.Id },
            product);
    }

    [HttpPut("{id:int}")]
    public async Task<IActionResult> UpdateProduct(
        int id,
        UpdateProductDto dto,
        CancellationToken cancellationToken)
    {
        await _productService.UpdateAsync(id, dto, cancellationToken);
        return NoContent();
    }

    [HttpDelete("{id:int}")]
    public async Task<IActionResult> DeleteProduct(
        int id,
        CancellationToken cancellationToken)
    {
        await _productService.DeleteAsync(id, cancellationToken);
        return NoContent();
    }
}
```

### Step 3: Dependency Injection Configuration

Configure services with appropriate lifetimes:

```csharp
var builder = WebApplication.CreateBuilder(args);

// Singleton - Single instance for application lifetime
builder.Services.AddSingleton<ICacheService, CacheService>();

// Scoped - One instance per HTTP request (most common)
builder.Services.AddScoped<IProductService, ProductService>();
builder.Services.AddScoped<IProductRepository, ProductRepository>();

// Transient - New instance every time
builder.Services.AddTransient<IValidator<CreateProductDto>, CreateProductDtoValidator>();

// Register DbContext (scoped)
builder.Services.AddDbContext<ApplicationDbContext>(options =>
    options.UseSqlServer(builder.Configuration.GetConnectionString("DefaultConnection")));

// Register HttpClient (use IHttpClientFactory)
builder.Services.AddHttpClient<IExternalApiService, ExternalApiService>(client =>
{
    client.BaseAddress = new Uri("https://api.example.com");
    client.Timeout = TimeSpan.FromSeconds(30);
});

var app = builder.Build();
```

### Step 4: Middleware Pipeline

Configure middleware in order:

```csharp
var app = builder.Build();

// Exception handling (early in pipeline)
app.UseExceptionHandler("/error");

// HTTPS redirection
app.UseHttpsRedirection();

// CORS
app.UseCors(policy => policy
    .WithOrigins("https://example.com")
    .AllowAnyMethod()
    .AllowAnyHeader()
    .AllowCredentials());

// Authentication & Authorization
app.UseAuthentication();
app.UseAuthorization();

// Custom middleware
app.Use(async (context, next) =>
{
    // Before request
    var stopwatch = Stopwatch.StartNew();

    await next();

    // After request
    stopwatch.Stop();
    context.Response.Headers.Add("X-Response-Time", stopwatch.ElapsedMilliseconds.ToString());
});

// Static files
app.UseStaticFiles();

// Routing
app.MapControllers();
app.MapMinimalApiEndpoints();

app.Run();
```

### Step 5: Background Services

Long-running background tasks:

```csharp
public class EmailBackgroundService : BackgroundService
{
    private readonly IServiceProvider _serviceProvider;
    private readonly ILogger<EmailBackgroundService> _logger;

    public EmailBackgroundService(
        IServiceProvider serviceProvider,
        ILogger<EmailBackgroundService> logger)
    {
        _serviceProvider = serviceProvider;
        _logger = logger;
    }

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        while (!stoppingToken.IsCancellationRequested)
        {
            try
            {
                // Create scope for scoped services
                using var scope = _serviceProvider.CreateScope();
                var emailService = scope.ServiceProvider
                    .GetRequiredService<IEmailService>();

                await emailService.ProcessPendingEmailsAsync(stoppingToken);
            }
            catch (Exception ex)
            {
                _logger.LogError(ex, "Error processing emails");
            }

            // Wait 5 minutes before next run
            await Task.Delay(TimeSpan.FromMinutes(5), stoppingToken);
        }
    }
}

// Register in Program.cs
builder.Services.AddHostedService<EmailBackgroundService>();
```

### Step 6: Options Pattern

Strongly-typed configuration:

```csharp
// appsettings.json
{
  "EmailSettings": {
    "SmtpServer": "smtp.example.com",
    "Port": 587,
    "FromEmail": "noreply@example.com",
    "ApiKey": "your-api-key"
  }
}

// Options class
public class EmailSettings
{
    public const string SectionName = "EmailSettings";

    public string SmtpServer { get; set; } = string.Empty;
    public int Port { get; set; }
    public string FromEmail { get; set; } = string.Empty;
    public string ApiKey { get; set; } = string.Empty;
}

// Register options
builder.Services.Configure<EmailSettings>(
    builder.Configuration.GetSection(EmailSettings.SectionName));

// Validate options
builder.Services.AddOptions<EmailSettings>()
    .Bind(builder.Configuration.GetSection(EmailSettings.SectionName))
    .ValidateDataAnnotations()
    .ValidateOnStart();

// Use in service
public class EmailService
{
    private readonly EmailSettings _settings;

    public EmailService(IOptions<EmailSettings> options)
    {
        _settings = options.Value;
    }
}
```

### Step 7: Health Checks

Monitor application health:

```csharp
// Register health checks
builder.Services.AddHealthChecks()
    .AddCheck("self", () => HealthCheckResult.Healthy())
    .AddDbContextCheck<ApplicationDbContext>()
    .AddCheck<DatabaseHealthCheck>("database")
    .AddCheck<ExternalApiHealthCheck>("external-api");

// Custom health check
public class DatabaseHealthCheck : IHealthCheck
{
    private readonly ApplicationDbContext _context;

    public DatabaseHealthCheck(ApplicationDbContext context)
    {
        _context = context;
    }

    public async Task<HealthCheckResult> CheckHealthAsync(
        HealthCheckContext context,
        CancellationToken cancellationToken = default)
    {
        try
        {
            await _context.Database.CanConnectAsync(cancellationToken);
            return HealthCheckResult.Healthy("Database is available");
        }
        catch (Exception ex)
        {
            return HealthCheckResult.Unhealthy("Database is unavailable", ex);
        }
    }
}

// Map health check endpoints
app.MapHealthChecks("/health");
app.MapHealthChecks("/health/ready", new HealthCheckOptions
{
    Predicate = check => check.Tags.Contains("ready")
});
app.MapHealthChecks("/health/live", new HealthCheckOptions
{
    Predicate = _ => false
});
```

## Best Practices

- Use `async Task` for all I/O operations
- Register services with appropriate lifetimes (Scoped for DbContext, Singleton for caches)
- Use `IHttpClientFactory` instead of `HttpClient` directly
- Implement proper error handling with exception middleware
- Use cancellation tokens for async operations
- Configure CORS properly for production
- Use Options pattern for configuration
- Implement health checks for monitoring
- Use structured logging with `ILogger<T>`
- Add request/response logging middleware
- Implement rate limiting for public APIs
- Use `Results` class for Minimal API responses

## Anti-Patterns

| Anti-Pattern | Fix |
|--|--|
| `HttpClient` as singleton | Use `IHttpClientFactory` |
| Synchronous I/O in async methods | Use `async/await` throughout |
| DbContext as singleton | Use scoped lifetime |
| Missing cancellation tokens | Pass `CancellationToken` to async methods |
| Hardcoded configuration | Use Options pattern |
| No error handling | Add exception middleware |

## Related

- Knowledge: `{directories.knowledge}/dotnet-patterns.json`
- Skill: `ef-core-patterns` for data access
- Skill: `authenticating-dotnet` for authentication

## When to Use
This skill should be used when strict adherence to the defined process is required.

## Prerequisites
- Basic understanding of the agent factory context.
- Access to the necessary tools and resources.

