backend-csharp

SKILL.md

C#/.NET Backend Development Patterns

Dependency Injection

Service Registration

// Program.cs or Startup.cs
var builder = WebApplication.CreateBuilder(args);

// Transient: New instance every time
builder.Services.AddTransient<IEmailService, EmailService>();

// Scoped: One instance per HTTP request
builder.Services.AddScoped<IProductService, ProductService>();
builder.Services.AddScoped<IOrderService, OrderService>();

// Singleton: One instance for application lifetime
builder.Services.AddSingleton<ICacheService, MemoryCacheService>();

// With factory
builder.Services.AddScoped<IDbContext>(provider =>
{
    var config = provider.GetRequiredService<IConfiguration>();
    return new DbContext(config.GetConnectionString("Default"));
});

Constructor Injection

public class OrderService : IOrderService
{
    private readonly IOrderRepository _orderRepository;
    private readonly IProductService _productService;
    private readonly IEmailService _emailService;
    private readonly ILogger<OrderService> _logger;

    public OrderService(
        IOrderRepository orderRepository,
        IProductService productService,
        IEmailService emailService,
        ILogger<OrderService> logger)
    {
        _orderRepository = orderRepository;
        _productService = productService;
        _emailService = emailService;
        _logger = logger;
    }

    public async Task<Order> CreateOrderAsync(CreateOrderRequest request, CancellationToken ct)
    {
        _logger.LogInformation("Creating order for customer {CustomerId}", request.CustomerId);
        
        // Implementation
    }
}

Interface Segregation

// Focused interfaces
public interface IProductReader
{
    Task<Product?> GetByIdAsync(Guid id, CancellationToken ct);
    Task<IReadOnlyList<Product>> GetAllAsync(CancellationToken ct);
    Task<IReadOnlyList<Product>> SearchAsync(string query, CancellationToken ct);
}

public interface IProductWriter
{
    Task<Product> CreateAsync(CreateProductRequest request, CancellationToken ct);
    Task UpdateAsync(Guid id, UpdateProductRequest request, CancellationToken ct);
    Task DeleteAsync(Guid id, CancellationToken ct);
}

// Combined interface for convenience
public interface IProductService : IProductReader, IProductWriter { }

Service Layer Architecture

Service Interface

public interface IProductService
{
    Task<Product?> GetByIdAsync(Guid id, CancellationToken ct = default);
    Task<PagedResult<Product>> GetPagedAsync(int page, int pageSize, CancellationToken ct = default);
    Task<Product> CreateAsync(CreateProductRequest request, CancellationToken ct = default);
    Task UpdateAsync(Guid id, UpdateProductRequest request, CancellationToken ct = default);
    Task DeleteAsync(Guid id, CancellationToken ct = default);
}

Service Implementation

public class ProductService : IProductService
{
    private readonly IProductRepository _repository;
    private readonly IValidator<CreateProductRequest> _createValidator;
    private readonly ILogger<ProductService> _logger;

    public ProductService(
        IProductRepository repository,
        IValidator<CreateProductRequest> createValidator,
        ILogger<ProductService> logger)
    {
        _repository = repository;
        _createValidator = createValidator;
        _logger = logger;
    }

    public async Task<Product?> GetByIdAsync(Guid id, CancellationToken ct = default)
    {
        return await _repository.GetByIdAsync(id, ct);
    }

    public async Task<PagedResult<Product>> GetPagedAsync(
        int page, 
        int pageSize, 
        CancellationToken ct = default)
    {
        var skip = (page - 1) * pageSize;
        var items = await _repository.GetPagedAsync(skip, pageSize, ct);
        var total = await _repository.CountAsync(ct);

        return new PagedResult<Product>
        {
            Items = items,
            Page = page,
            PageSize = pageSize,
            TotalCount = total,
            TotalPages = (int)Math.Ceiling(total / (double)pageSize)
        };
    }

    public async Task<Product> CreateAsync(
        CreateProductRequest request, 
        CancellationToken ct = default)
    {
        var validationResult = await _createValidator.ValidateAsync(request, ct);
        if (!validationResult.IsValid)
        {
            throw new ValidationException(validationResult.Errors);
        }

        var product = new Product
        {
            Id = Guid.NewGuid(),
            Name = request.Name,
            Description = request.Description,
            Price = request.Price,
            CreatedAt = DateTime.UtcNow
        };

        await _repository.AddAsync(product, ct);
        
        _logger.LogInformation("Created product {ProductId}: {ProductName}", 
            product.Id, product.Name);

        return product;
    }

    public async Task UpdateAsync(
        Guid id, 
        UpdateProductRequest request, 
        CancellationToken ct = default)
    {
        var product = await _repository.GetByIdAsync(id, ct)
            ?? throw new NotFoundException($"Product {id} not found");

        product.Name = request.Name ?? product.Name;
        product.Description = request.Description ?? product.Description;
        product.Price = request.Price ?? product.Price;
        product.UpdatedAt = DateTime.UtcNow;

        await _repository.UpdateAsync(product, ct);
    }

    public async Task DeleteAsync(Guid id, CancellationToken ct = default)
    {
        var exists = await _repository.ExistsAsync(id, ct);
        if (!exists)
        {
            throw new NotFoundException($"Product {id} not found");
        }

        await _repository.DeleteAsync(id, ct);
        
        _logger.LogInformation("Deleted product {ProductId}", id);
    }
}

Repository Pattern

Repository Interface

public interface IRepository<T> where T : class
{
    Task<T?> GetByIdAsync(Guid id, CancellationToken ct = default);
    Task<IReadOnlyList<T>> GetAllAsync(CancellationToken ct = default);
    Task<IReadOnlyList<T>> GetPagedAsync(int skip, int take, CancellationToken ct = default);
    Task<int> CountAsync(CancellationToken ct = default);
    Task<bool> ExistsAsync(Guid id, CancellationToken ct = default);
    Task AddAsync(T entity, CancellationToken ct = default);
    Task UpdateAsync(T entity, CancellationToken ct = default);
    Task DeleteAsync(Guid id, CancellationToken ct = default);
}

public interface IProductRepository : IRepository<Product>
{
    Task<IReadOnlyList<Product>> GetByCategoryAsync(string category, CancellationToken ct = default);
    Task<IReadOnlyList<Product>> SearchAsync(string query, CancellationToken ct = default);
}

Repository Implementation

public class ProductRepository : IProductRepository
{
    private readonly AppDbContext _context;

    public ProductRepository(AppDbContext context)
    {
        _context = context;
    }

    public async Task<Product?> GetByIdAsync(Guid id, CancellationToken ct = default)
    {
        return await _context.Products
            .AsNoTracking()
            .FirstOrDefaultAsync(p => p.Id == id, ct);
    }

    public async Task<IReadOnlyList<Product>> GetPagedAsync(
        int skip, 
        int take, 
        CancellationToken ct = default)
    {
        return await _context.Products
            .AsNoTracking()
            .OrderByDescending(p => p.CreatedAt)
            .Skip(skip)
            .Take(take)
            .ToListAsync(ct);
    }

    public async Task<IReadOnlyList<Product>> SearchAsync(
        string query, 
        CancellationToken ct = default)
    {
        return await _context.Products
            .AsNoTracking()
            .Where(p => p.Name.Contains(query) || p.Description.Contains(query))
            .OrderBy(p => p.Name)
            .Take(100)
            .ToListAsync(ct);
    }

    public async Task AddAsync(Product entity, CancellationToken ct = default)
    {
        await _context.Products.AddAsync(entity, ct);
        await _context.SaveChangesAsync(ct);
    }

    public async Task UpdateAsync(Product entity, CancellationToken ct = default)
    {
        _context.Products.Update(entity);
        await _context.SaveChangesAsync(ct);
    }

    public async Task DeleteAsync(Guid id, CancellationToken ct = default)
    {
        var entity = await _context.Products.FindAsync(new object[] { id }, ct);
        if (entity != null)
        {
            _context.Products.Remove(entity);
            await _context.SaveChangesAsync(ct);
        }
    }
}

Controller Patterns

API Controller

[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]
    [ProducesResponseType(typeof(PagedResult<ProductDto>), StatusCodes.Status200OK)]
    public async Task<IActionResult> GetAll(
        [FromQuery] int page = 1,
        [FromQuery] int pageSize = 10,
        CancellationToken ct = default)
    {
        var result = await _productService.GetPagedAsync(page, pageSize, ct);
        return Ok(result);
    }

    [HttpGet("{id:guid}")]
    [ProducesResponseType(typeof(ProductDto), StatusCodes.Status200OK)]
    [ProducesResponseType(StatusCodes.Status404NotFound)]
    public async Task<IActionResult> GetById(Guid id, CancellationToken ct)
    {
        var product = await _productService.GetByIdAsync(id, ct);
        if (product == null)
        {
            return NotFound();
        }
        return Ok(product);
    }

    [HttpPost]
    [ProducesResponseType(typeof(ProductDto), StatusCodes.Status201Created)]
    [ProducesResponseType(typeof(ValidationProblemDetails), StatusCodes.Status400BadRequest)]
    public async Task<IActionResult> Create(
        [FromBody] CreateProductRequest request,
        CancellationToken ct)
    {
        var product = await _productService.CreateAsync(request, ct);
        return CreatedAtAction(
            nameof(GetById), 
            new { id = product.Id }, 
            product);
    }

    [HttpPut("{id:guid}")]
    [ProducesResponseType(StatusCodes.Status204NoContent)]
    [ProducesResponseType(StatusCodes.Status404NotFound)]
    public async Task<IActionResult> Update(
        Guid id,
        [FromBody] UpdateProductRequest request,
        CancellationToken ct)
    {
        await _productService.UpdateAsync(id, request, ct);
        return NoContent();
    }

    [HttpDelete("{id:guid}")]
    [ProducesResponseType(StatusCodes.Status204NoContent)]
    [ProducesResponseType(StatusCodes.Status404NotFound)]
    public async Task<IActionResult> Delete(Guid id, CancellationToken ct)
    {
        await _productService.DeleteAsync(id, ct);
        return NoContent();
    }
}

MVC Controller

public class ProductsController : Controller
{
    private readonly IProductService _productService;
    private readonly ICategoryService _categoryService;

    public ProductsController(
        IProductService productService,
        ICategoryService categoryService)
    {
        _productService = productService;
        _categoryService = categoryService;
    }

    [HttpGet]
    public async Task<IActionResult> Index(int page = 1, CancellationToken ct = default)
    {
        var products = await _productService.GetPagedAsync(page, 12, ct);
        return View(products);
    }

    [HttpGet]
    public async Task<IActionResult> Details(Guid id, CancellationToken ct)
    {
        var product = await _productService.GetByIdAsync(id, ct);
        if (product == null)
        {
            return NotFound();
        }
        return View(product);
    }

    [HttpGet]
    public async Task<IActionResult> Create(CancellationToken ct)
    {
        ViewBag.Categories = await _categoryService.GetAllAsync(ct);
        return View(new CreateProductRequest());
    }

    [HttpPost]
    [ValidateAntiForgeryToken]
    public async Task<IActionResult> Create(
        CreateProductRequest request,
        CancellationToken ct)
    {
        if (!ModelState.IsValid)
        {
            ViewBag.Categories = await _categoryService.GetAllAsync(ct);
            return View(request);
        }

        var product = await _productService.CreateAsync(request, ct);
        TempData["SuccessMessage"] = "Product created successfully.";
        return RedirectToAction(nameof(Details), new { id = product.Id });
    }
}

Async/Await Best Practices

Async All the Way

// BAD: Blocking on async code
public Product GetProduct(Guid id)
{
    return _repository.GetByIdAsync(id).Result; // Deadlock risk!
}

// GOOD: Async all the way
public async Task<Product?> GetProductAsync(Guid id, CancellationToken ct)
{
    return await _repository.GetByIdAsync(id, ct);
}

CancellationToken Propagation

public async Task<Order> ProcessOrderAsync(
    CreateOrderRequest request,
    CancellationToken ct = default)
{
    // Pass cancellation token to all async operations
    var customer = await _customerService.GetByIdAsync(request.CustomerId, ct);
    var products = await _productService.GetByIdsAsync(request.ProductIds, ct);
    
    var order = new Order { /* ... */ };
    await _orderRepository.AddAsync(order, ct);
    
    // Background work that should continue even if request is cancelled
    _ = _emailService.SendOrderConfirmationAsync(order.Id, CancellationToken.None);
    
    return order;
}

Parallel Operations

public async Task<DashboardData> GetDashboardAsync(CancellationToken ct)
{
    // Run independent queries in parallel
    var ordersTask = _orderService.GetRecentAsync(10, ct);
    var productsTask = _productService.GetTopSellingAsync(5, ct);
    var statsTask = _analyticsService.GetSummaryAsync(ct);

    await Task.WhenAll(ordersTask, productsTask, statsTask);

    return new DashboardData
    {
        RecentOrders = await ordersTask,
        TopProducts = await productsTask,
        Stats = await statsTask
    };
}

ConfigureAwait

// In library code, use ConfigureAwait(false) to avoid context capture
public async Task<string> FetchDataAsync(string url, CancellationToken ct)
{
    using var client = new HttpClient();
    var response = await client.GetAsync(url, ct).ConfigureAwait(false);
    return await response.Content.ReadAsStringAsync(ct).ConfigureAwait(false);
}

// In ASP.NET Core controllers/services, it's generally not needed
// The framework handles synchronization context properly

LINQ Patterns

Query Syntax vs Method Syntax

// Method syntax (preferred for most cases)
var activeProducts = products
    .Where(p => p.IsActive)
    .OrderBy(p => p.Name)
    .Select(p => new ProductDto(p.Id, p.Name, p.Price))
    .ToList();

// Query syntax (useful for complex joins)
var orderDetails = 
    from order in orders
    join customer in customers on order.CustomerId equals customer.Id
    join product in products on order.ProductId equals product.Id
    where order.Date >= startDate
    orderby order.Date descending
    select new OrderDetailDto
    {
        OrderId = order.Id,
        CustomerName = customer.Name,
        ProductName = product.Name,
        Total = order.Quantity * product.Price
    };

Common LINQ Operations

// Filtering
var active = products.Where(p => p.IsActive);
var expensive = products.Where(p => p.Price > 100);

// Projection
var names = products.Select(p => p.Name);
var dtos = products.Select(p => new ProductDto(p));

// Aggregation
var totalValue = products.Sum(p => p.Price * p.Stock);
var averagePrice = products.Average(p => p.Price);
var maxPrice = products.Max(p => p.Price);
var count = products.Count(p => p.IsActive);

// Grouping
var byCategory = products
    .GroupBy(p => p.Category)
    .Select(g => new 
    { 
        Category = g.Key, 
        Products = g.ToList(),
        Count = g.Count()
    });

// First/Single
var first = products.FirstOrDefault(p => p.Id == id);
var single = products.SingleOrDefault(p => p.Sku == sku);

// Any/All
var hasActive = products.Any(p => p.IsActive);
var allActive = products.All(p => p.IsActive);

EF Core Specific

// Include related data
var ordersWithItems = await _context.Orders
    .Include(o => o.Customer)
    .Include(o => o.Items)
        .ThenInclude(i => i.Product)
    .ToListAsync(ct);

// Explicit loading
await _context.Entry(order)
    .Collection(o => o.Items)
    .LoadAsync(ct);

// Projection (more efficient)
var orderSummaries = await _context.Orders
    .Select(o => new OrderSummaryDto
    {
        Id = o.Id,
        CustomerName = o.Customer.Name,
        Total = o.Items.Sum(i => i.Quantity * i.UnitPrice),
        ItemCount = o.Items.Count
    })
    .ToListAsync(ct);

Configuration Pattern

Options Pattern

// Configuration class
public class EmailSettings
{
    public const string SectionName = "Email";
    
    public string SmtpHost { get; set; } = string.Empty;
    public int SmtpPort { get; set; } = 587;
    public string FromAddress { get; set; } = string.Empty;
    public string FromName { get; set; } = string.Empty;
    public bool UseSsl { get; set; } = true;
}

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

// Usage with IOptions
public class EmailService : IEmailService
{
    private readonly EmailSettings _settings;

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

// Usage with IOptionsSnapshot (reloads on change)
public class EmailService : IEmailService
{
    private readonly IOptionsSnapshot<EmailSettings> _options;

    public EmailService(IOptionsSnapshot<EmailSettings> options)
    {
        _options = options;
    }

    public void Send()
    {
        var settings = _options.Value; // Gets current value
    }
}

Validation

public class EmailSettingsValidation : IValidateOptions<EmailSettings>
{
    public ValidateOptionsResult Validate(string? name, EmailSettings options)
    {
        var failures = new List<string>();

        if (string.IsNullOrWhiteSpace(options.SmtpHost))
        {
            failures.Add("SmtpHost is required");
        }

        if (string.IsNullOrWhiteSpace(options.FromAddress))
        {
            failures.Add("FromAddress is required");
        }

        return failures.Count > 0
            ? ValidateOptionsResult.Fail(failures)
            : ValidateOptionsResult.Success;
    }
}

// Registration
builder.Services.AddSingleton<IValidateOptions<EmailSettings>, EmailSettingsValidation>();

Logging

Structured Logging

public class OrderService : IOrderService
{
    private readonly ILogger<OrderService> _logger;

    public async Task<Order> CreateOrderAsync(CreateOrderRequest request, CancellationToken ct)
    {
        // Structured logging with named parameters
        _logger.LogInformation(
            "Creating order for customer {CustomerId} with {ItemCount} items",
            request.CustomerId,
            request.Items.Count);

        try
        {
            var order = await ProcessOrderAsync(request, ct);
            
            _logger.LogInformation(
                "Order {OrderId} created successfully. Total: {Total:C}",
                order.Id,
                order.Total);
            
            return order;
        }
        catch (Exception ex)
        {
            _logger.LogError(ex,
                "Failed to create order for customer {CustomerId}",
                request.CustomerId);
            throw;
        }
    }
}

Log Levels

// Trace - Very detailed, typically only in development
_logger.LogTrace("Entering method {MethodName}", nameof(ProcessOrder));

// Debug - Debugging information
_logger.LogDebug("Processing item {ItemId} with quantity {Quantity}", item.Id, item.Quantity);

// Information - General flow
_logger.LogInformation("Order {OrderId} shipped to {Address}", order.Id, order.ShippingAddress);

// Warning - Something unexpected but not an error
_logger.LogWarning("Payment retry {Attempt} for order {OrderId}", attempt, orderId);

// Error - An error occurred but application can continue
_logger.LogError(ex, "Failed to send email for order {OrderId}", orderId);

// Critical - Application failure
_logger.LogCritical(ex, "Database connection failed");

Scopes

public async Task ProcessOrderAsync(Order order, CancellationToken ct)
{
    using (_logger.BeginScope(new Dictionary<string, object>
    {
        ["OrderId"] = order.Id,
        ["CustomerId"] = order.CustomerId
    }))
    {
        _logger.LogInformation("Processing order");
        // All logs within this scope will include OrderId and CustomerId
        await ProcessPaymentAsync(order, ct);
        await ProcessShippingAsync(order, ct);
    }
}

Exception Handling

Custom Exceptions

public class NotFoundException : Exception
{
    public string ResourceType { get; }
    public string ResourceId { get; }

    public NotFoundException(string resourceType, string resourceId)
        : base($"{resourceType} with ID '{resourceId}' was not found")
    {
        ResourceType = resourceType;
        ResourceId = resourceId;
    }
}

public class ValidationException : Exception
{
    public IReadOnlyList<ValidationError> Errors { get; }

    public ValidationException(IEnumerable<ValidationError> errors)
        : base("One or more validation errors occurred")
    {
        Errors = errors.ToList();
    }
}

public class BusinessRuleException : Exception
{
    public string Code { get; }

    public BusinessRuleException(string code, string message)
        : base(message)
    {
        Code = code;
    }
}

Global Exception Handler

public class GlobalExceptionHandler : IExceptionHandler
{
    private readonly ILogger<GlobalExceptionHandler> _logger;

    public GlobalExceptionHandler(ILogger<GlobalExceptionHandler> logger)
    {
        _logger = logger;
    }

    public async ValueTask<bool> TryHandleAsync(
        HttpContext httpContext,
        Exception exception,
        CancellationToken ct)
    {
        _logger.LogError(exception, "An unhandled exception occurred");

        var (statusCode, response) = exception switch
        {
            NotFoundException ex => (
                StatusCodes.Status404NotFound,
                new ProblemDetails
                {
                    Status = 404,
                    Title = "Resource not found",
                    Detail = ex.Message
                }),
            ValidationException ex => (
                StatusCodes.Status400BadRequest,
                new ValidationProblemDetails
                {
                    Status = 400,
                    Title = "Validation failed",
                    Errors = ex.Errors.ToDictionary(
                        e => e.PropertyName,
                        e => new[] { e.Message })
                }),
            _ => (
                StatusCodes.Status500InternalServerError,
                new ProblemDetails
                {
                    Status = 500,
                    Title = "An error occurred",
                    Detail = "An unexpected error occurred. Please try again later."
                })
        };

        httpContext.Response.StatusCode = statusCode;
        await httpContext.Response.WriteAsJsonAsync(response, ct);
        return true;
    }
}

Unit of Work Pattern

public interface IUnitOfWork : IDisposable
{
    IProductRepository Products { get; }
    IOrderRepository Orders { get; }
    ICustomerRepository Customers { get; }
    
    Task<int> SaveChangesAsync(CancellationToken ct = default);
    Task BeginTransactionAsync(CancellationToken ct = default);
    Task CommitTransactionAsync(CancellationToken ct = default);
    Task RollbackTransactionAsync(CancellationToken ct = default);
}

public class UnitOfWork : IUnitOfWork
{
    private readonly AppDbContext _context;
    private IDbContextTransaction? _transaction;

    public IProductRepository Products { get; }
    public IOrderRepository Orders { get; }
    public ICustomerRepository Customers { get; }

    public UnitOfWork(AppDbContext context)
    {
        _context = context;
        Products = new ProductRepository(context);
        Orders = new OrderRepository(context);
        Customers = new CustomerRepository(context);
    }

    public async Task<int> SaveChangesAsync(CancellationToken ct = default)
    {
        return await _context.SaveChangesAsync(ct);
    }

    public async Task BeginTransactionAsync(CancellationToken ct = default)
    {
        _transaction = await _context.Database.BeginTransactionAsync(ct);
    }

    public async Task CommitTransactionAsync(CancellationToken ct = default)
    {
        if (_transaction != null)
        {
            await _transaction.CommitAsync(ct);
            await _transaction.DisposeAsync();
            _transaction = null;
        }
    }

    public async Task RollbackTransactionAsync(CancellationToken ct = default)
    {
        if (_transaction != null)
        {
            await _transaction.RollbackAsync(ct);
            await _transaction.DisposeAsync();
            _transaction = null;
        }
    }

    public void Dispose()
    {
        _transaction?.Dispose();
        _context.Dispose();
    }
}
Weekly Installs
4
GitHub Stars
1
First Seen
13 days ago
Installed on
opencode4
gemini-cli4
github-copilot4
codex4
amp4
cline4