Claude Agent Skill · by Wshobson

Dotnet Backend Patterns

The dotnet-backend-patterns skill equips .NET developers with production-grade patterns and best practices for building robust C# APIs, MCP servers, and enterpr

Install
Terminal · npx
$npx skills add https://github.com/wshobson/agents --skill dotnet-backend-patterns
Works with Paperclip

How Dotnet Backend Patterns fits into a Paperclip company.

Dotnet Backend Patterns drops into any Paperclip agent that handles this kind of work. Assign it to a specialist inside a pre-configured PaperclipOrg company and the skill becomes available on every heartbeat — no prompt engineering, no tool wiring.

S
SaaS FactoryPaired

Pre-configured AI company — 18 agents, 18 skills, one-time purchase.

$27$59
Explore pack
Source file
SKILL.md810 lines
Expand
---name: dotnet-backend-patternsdescription: Master C#/.NET backend development patterns for building robust APIs, MCP servers, and enterprise applications. Covers async/await, dependency injection, Entity Framework Core, Dapper, configuration, caching, and testing with xUnit. Use when developing .NET backends, reviewing C# code, or designing API architectures.--- # .NET Backend Development Patterns Master C#/.NET patterns for building production-grade APIs, MCP servers, and enterprise backends with modern best practices (2024/2025). ## When to Use This Skill - Developing new .NET Web APIs or MCP servers- Reviewing C# code for quality and performance- Designing service architectures with dependency injection- Implementing caching strategies with Redis- Writing unit and integration tests- Optimizing database access with EF Core or Dapper- Configuring applications with IOptions pattern- Handling errors and implementing resilience patterns ## Core Concepts ### 1. Project Structure (Clean Architecture) ```src/├── Domain/                     # Core business logic (no dependencies)│   ├── Entities/│   ├── Interfaces/│   ├── Exceptions/│   └── ValueObjects/├── Application/                # Use cases, DTOs, validation│   ├── Services/│   ├── DTOs/│   ├── Validators/│   └── Interfaces/├── Infrastructure/             # External implementations│   ├── Data/                   # EF Core, Dapper repositories│   ├── Caching/                # Redis, Memory cache│   ├── External/               # HTTP clients, third-party APIs│   └── DependencyInjection/    # Service registration└── Api/                        # Entry point    ├── Controllers/            # Or MinimalAPI endpoints    ├── Middleware/    ├── Filters/    └── Program.cs``` ### 2. Dependency Injection Patterns ```csharp// Service registration by lifetimepublic static class ServiceCollectionExtensions{    public static IServiceCollection AddApplicationServices(        this IServiceCollection services,        IConfiguration configuration)    {        // Scoped: One instance per HTTP request        services.AddScoped<IProductService, ProductService>();        services.AddScoped<IOrderService, OrderService>();         // Singleton: One instance for app lifetime        services.AddSingleton<ICacheService, RedisCacheService>();        services.AddSingleton<IConnectionMultiplexer>(_ =>            ConnectionMultiplexer.Connect(configuration["Redis:Connection"]!));         // Transient: New instance every time        services.AddTransient<IValidator<CreateOrderRequest>, CreateOrderValidator>();         // Options pattern for configuration        services.Configure<CatalogOptions>(configuration.GetSection("Catalog"));        services.Configure<RedisOptions>(configuration.GetSection("Redis"));         // Factory pattern for conditional creation        services.AddScoped<IPriceCalculator>(sp =>        {            var options = sp.GetRequiredService<IOptions<PricingOptions>>().Value;            return options.UseNewEngine                ? sp.GetRequiredService<NewPriceCalculator>()                : sp.GetRequiredService<LegacyPriceCalculator>();        });         // Keyed services (.NET 8+)        services.AddKeyedScoped<IPaymentProcessor, StripeProcessor>("stripe");        services.AddKeyedScoped<IPaymentProcessor, PayPalProcessor>("paypal");         return services;    }} // Usage with keyed servicespublic class CheckoutService{    public CheckoutService(        [FromKeyedServices("stripe")] IPaymentProcessor stripeProcessor)    {        _processor = stripeProcessor;    }}``` ### 3. Async/Await Patterns ```csharp// ✅ CORRECT: Async all the way downpublic async Task<Product> GetProductAsync(string id, CancellationToken ct = default){    return await _repository.GetByIdAsync(id, ct);} // ✅ CORRECT: Parallel execution with WhenAllpublic async Task<(Stock, Price)> GetStockAndPriceAsync(    string productId,    CancellationToken ct = default){    var stockTask = _stockService.GetAsync(productId, ct);    var priceTask = _priceService.GetAsync(productId, ct);     await Task.WhenAll(stockTask, priceTask);     return (await stockTask, await priceTask);} // ✅ CORRECT: ConfigureAwait in librariespublic async Task<T> LibraryMethodAsync<T>(CancellationToken ct = default){    var result = await _httpClient.GetAsync(url, ct).ConfigureAwait(false);    return await result.Content.ReadFromJsonAsync<T>(ct).ConfigureAwait(false);} // ✅ CORRECT: ValueTask for hot paths with cachingpublic ValueTask<Product?> GetCachedProductAsync(string id){    if (_cache.TryGetValue(id, out Product? product))        return ValueTask.FromResult(product);     return new ValueTask<Product?>(GetFromDatabaseAsync(id));} // ❌ WRONG: Blocking on async (deadlock risk)var result = GetProductAsync(id).Result;  // NEVER do thisvar result2 = GetProductAsync(id).GetAwaiter().GetResult(); // Also bad // ❌ WRONG: async void (except event handlers)public async void ProcessOrder() { }  // Exceptions are lost // ❌ WRONG: Unnecessary Task.Run for already async codeawait Task.Run(async () => await GetDataAsync());  // Wastes thread``` ### 4. Configuration with IOptions ```csharp// Configuration classespublic class CatalogOptions{    public const string SectionName = "Catalog";     public int DefaultPageSize { get; set; } = 50;    public int MaxPageSize { get; set; } = 200;    public TimeSpan CacheDuration { get; set; } = TimeSpan.FromMinutes(15);    public bool EnableEnrichment { get; set; } = true;} public class RedisOptions{    public const string SectionName = "Redis";     public string Connection { get; set; } = "localhost:6379";    public string KeyPrefix { get; set; } = "mcp:";    public int Database { get; set; } = 0;} // appsettings.json{    "Catalog": {        "DefaultPageSize": 50,        "MaxPageSize": 200,        "CacheDuration": "00:15:00",        "EnableEnrichment": true    },    "Redis": {        "Connection": "localhost:6379",        "KeyPrefix": "mcp:",        "Database": 0    }} // Registrationservices.Configure<CatalogOptions>(configuration.GetSection(CatalogOptions.SectionName));services.Configure<RedisOptions>(configuration.GetSection(RedisOptions.SectionName)); // Usage with IOptions (singleton, read once at startup)public class CatalogService{    private readonly CatalogOptions _options;     public CatalogService(IOptions<CatalogOptions> options)    {        _options = options.Value;    }} // Usage with IOptionsSnapshot (scoped, re-reads on each request)public class DynamicService{    private readonly CatalogOptions _options;     public DynamicService(IOptionsSnapshot<CatalogOptions> options)    {        _options = options.Value;  // Fresh value per request    }} // Usage with IOptionsMonitor (singleton, notified on changes)public class MonitoredService{    private CatalogOptions _options;     public MonitoredService(IOptionsMonitor<CatalogOptions> monitor)    {        _options = monitor.CurrentValue;        monitor.OnChange(newOptions => _options = newOptions);    }}``` ### 5. Result Pattern (Avoiding Exceptions for Flow Control) ```csharp// Generic Result typepublic class Result<T>{    public bool IsSuccess { get; }    public T? Value { get; }    public string? Error { get; }    public string? ErrorCode { get; }     private Result(bool isSuccess, T? value, string? error, string? errorCode)    {        IsSuccess = isSuccess;        Value = value;        Error = error;        ErrorCode = errorCode;    }     public static Result<T> Success(T value) => new(true, value, null, null);    public static Result<T> Failure(string error, string? code = null) => new(false, default, error, code);     public Result<TNew> Map<TNew>(Func<T, TNew> mapper) =>        IsSuccess ? Result<TNew>.Success(mapper(Value!)) : Result<TNew>.Failure(Error!, ErrorCode);     public async Task<Result<TNew>> MapAsync<TNew>(Func<T, Task<TNew>> mapper) =>        IsSuccess ? Result<TNew>.Success(await mapper(Value!)) : Result<TNew>.Failure(Error!, ErrorCode);} // Usage in servicepublic async Task<Result<Order>> CreateOrderAsync(CreateOrderRequest request, CancellationToken ct){    // Validation    var validation = await _validator.ValidateAsync(request, ct);    if (!validation.IsValid)        return Result<Order>.Failure(            validation.Errors.First().ErrorMessage,            "VALIDATION_ERROR");     // Business rule check    var stock = await _stockService.CheckAsync(request.ProductId, request.Quantity, ct);    if (!stock.IsAvailable)        return Result<Order>.Failure(            $"Insufficient stock: {stock.Available} available, {request.Quantity} requested",            "INSUFFICIENT_STOCK");     // Create order    var order = await _repository.CreateAsync(request.ToEntity(), ct);     return Result<Order>.Success(order);} // Usage in controller/endpointapp.MapPost("/orders", async (    CreateOrderRequest request,    IOrderService orderService,    CancellationToken ct) =>{    var result = await orderService.CreateOrderAsync(request, ct);     return result.IsSuccess        ? Results.Created($"/orders/{result.Value!.Id}", result.Value)        : Results.BadRequest(new { error = result.Error, code = result.ErrorCode });});``` ## Data Access Patterns ### Entity Framework Core ```csharp// DbContext configurationpublic class AppDbContext : DbContext{    public DbSet<Product> Products => Set<Product>();    public DbSet<Order> Orders => Set<Order>();     protected override void OnModelCreating(ModelBuilder modelBuilder)    {        // Apply all configurations from assembly        modelBuilder.ApplyConfigurationsFromAssembly(typeof(AppDbContext).Assembly);         // Global query filters        modelBuilder.Entity<Product>().HasQueryFilter(p => !p.IsDeleted);    }} // Entity configurationpublic class ProductConfiguration : IEntityTypeConfiguration<Product>{    public void Configure(EntityTypeBuilder<Product> builder)    {        builder.ToTable("Products");         builder.HasKey(p => p.Id);        builder.Property(p => p.Id).HasMaxLength(40);        builder.Property(p => p.Name).HasMaxLength(200).IsRequired();        builder.Property(p => p.Price).HasPrecision(18, 2);         builder.HasIndex(p => p.Sku).IsUnique();        builder.HasIndex(p => new { p.CategoryId, p.Name });         builder.HasMany(p => p.OrderItems)            .WithOne(oi => oi.Product)            .HasForeignKey(oi => oi.ProductId);    }} // Repository with EF Corepublic class ProductRepository : IProductRepository{    private readonly AppDbContext _context;     public async Task<Product?> GetByIdAsync(string id, CancellationToken ct = default)    {        return await _context.Products            .AsNoTracking()            .FirstOrDefaultAsync(p => p.Id == id, ct);    }     public async Task<IReadOnlyList<Product>> SearchAsync(        ProductSearchCriteria criteria,        CancellationToken ct = default)    {        var query = _context.Products.AsNoTracking();         if (!string.IsNullOrWhiteSpace(criteria.SearchTerm))            query = query.Where(p => EF.Functions.Like(p.Name, $"%{criteria.SearchTerm}%"));         if (criteria.CategoryId.HasValue)            query = query.Where(p => p.CategoryId == criteria.CategoryId);         if (criteria.MinPrice.HasValue)            query = query.Where(p => p.Price >= criteria.MinPrice);         if (criteria.MaxPrice.HasValue)            query = query.Where(p => p.Price <= criteria.MaxPrice);         return await query            .OrderBy(p => p.Name)            .Skip((criteria.Page - 1) * criteria.PageSize)            .Take(criteria.PageSize)            .ToListAsync(ct);    }}``` ### Dapper for Performance ```csharppublic class DapperProductRepository : IProductRepository{    private readonly IDbConnection _connection;     public async Task<Product?> GetByIdAsync(string id, CancellationToken ct = default)    {        const string sql = """            SELECT Id, Name, Sku, Price, CategoryId, Stock, CreatedAt            FROM Products            WHERE Id = @Id AND IsDeleted = 0            """;         return await _connection.QueryFirstOrDefaultAsync<Product>(            new CommandDefinition(sql, new { Id = id }, cancellationToken: ct));    }     public async Task<IReadOnlyList<Product>> SearchAsync(        ProductSearchCriteria criteria,        CancellationToken ct = default)    {        var sql = new StringBuilder("""            SELECT Id, Name, Sku, Price, CategoryId, Stock, CreatedAt            FROM Products            WHERE IsDeleted = 0            """);         var parameters = new DynamicParameters();         if (!string.IsNullOrWhiteSpace(criteria.SearchTerm))        {            sql.Append(" AND Name LIKE @SearchTerm");            parameters.Add("SearchTerm", $"%{criteria.SearchTerm}%");        }         if (criteria.CategoryId.HasValue)        {            sql.Append(" AND CategoryId = @CategoryId");            parameters.Add("CategoryId", criteria.CategoryId);        }         if (criteria.MinPrice.HasValue)        {            sql.Append(" AND Price >= @MinPrice");            parameters.Add("MinPrice", criteria.MinPrice);        }         if (criteria.MaxPrice.HasValue)        {            sql.Append(" AND Price <= @MaxPrice");            parameters.Add("MaxPrice", criteria.MaxPrice);        }         sql.Append(" ORDER BY Name OFFSET @Offset ROWS FETCH NEXT @PageSize ROWS ONLY");        parameters.Add("Offset", (criteria.Page - 1) * criteria.PageSize);        parameters.Add("PageSize", criteria.PageSize);         var results = await _connection.QueryAsync<Product>(            new CommandDefinition(sql.ToString(), parameters, cancellationToken: ct));         return results.ToList();    }     // Multi-mapping for related data    public async Task<Order?> GetOrderWithItemsAsync(int orderId, CancellationToken ct = default)    {        const string sql = """            SELECT o.*, oi.*, p.*            FROM Orders o            LEFT JOIN OrderItems oi ON o.Id = oi.OrderId            LEFT JOIN Products p ON oi.ProductId = p.Id            WHERE o.Id = @OrderId            """;         var orderDictionary = new Dictionary<int, Order>();         await _connection.QueryAsync<Order, OrderItem, Product, Order>(            new CommandDefinition(sql, new { OrderId = orderId }, cancellationToken: ct),            (order, item, product) =>            {                if (!orderDictionary.TryGetValue(order.Id, out var existingOrder))                {                    existingOrder = order;                    existingOrder.Items = new List<OrderItem>();                    orderDictionary.Add(order.Id, existingOrder);                }                 if (item != null)                {                    item.Product = product;                    existingOrder.Items.Add(item);                }                 return existingOrder;            },            splitOn: "Id,Id");         return orderDictionary.Values.FirstOrDefault();    }}``` ## Caching Patterns ### Multi-Level Cache with Redis ```csharppublic class CachedProductService : IProductService{    private readonly IProductRepository _repository;    private readonly IMemoryCache _memoryCache;    private readonly IDistributedCache _distributedCache;    private readonly ILogger<CachedProductService> _logger;     private static readonly TimeSpan MemoryCacheDuration = TimeSpan.FromMinutes(1);    private static readonly TimeSpan DistributedCacheDuration = TimeSpan.FromMinutes(15);     public async Task<Product?> GetByIdAsync(string id, CancellationToken ct = default)    {        var cacheKey = $"product:{id}";         // L1: Memory cache (in-process, fastest)        if (_memoryCache.TryGetValue(cacheKey, out Product? cached))        {            _logger.LogDebug("L1 cache hit for {CacheKey}", cacheKey);            return cached;        }         // L2: Distributed cache (Redis)        var distributed = await _distributedCache.GetStringAsync(cacheKey, ct);        if (distributed != null)        {            _logger.LogDebug("L2 cache hit for {CacheKey}", cacheKey);            var product = JsonSerializer.Deserialize<Product>(distributed);             // Populate L1            _memoryCache.Set(cacheKey, product, MemoryCacheDuration);            return product;        }         // L3: Database        _logger.LogDebug("Cache miss for {CacheKey}, fetching from database", cacheKey);        var fromDb = await _repository.GetByIdAsync(id, ct);         if (fromDb != null)        {            var serialized = JsonSerializer.Serialize(fromDb);             // Populate both caches            await _distributedCache.SetStringAsync(                cacheKey,                serialized,                new DistributedCacheEntryOptions                {                    AbsoluteExpirationRelativeToNow = DistributedCacheDuration                },                ct);             _memoryCache.Set(cacheKey, fromDb, MemoryCacheDuration);        }         return fromDb;    }     public async Task InvalidateAsync(string id, CancellationToken ct = default)    {        var cacheKey = $"product:{id}";         _memoryCache.Remove(cacheKey);        await _distributedCache.RemoveAsync(cacheKey, ct);         _logger.LogInformation("Invalidated cache for {CacheKey}", cacheKey);    }} // Stale-while-revalidate patternpublic class StaleWhileRevalidateCache<T>{    private readonly IDistributedCache _cache;    private readonly TimeSpan _freshDuration;    private readonly TimeSpan _staleDuration;     public async Task<T?> GetOrCreateAsync(        string key,        Func<CancellationToken, Task<T>> factory,        CancellationToken ct = default)    {        var cached = await _cache.GetStringAsync(key, ct);         if (cached != null)        {            var entry = JsonSerializer.Deserialize<CacheEntry<T>>(cached)!;             if (entry.IsStale && !entry.IsExpired)            {                // Return stale data immediately, refresh in background                _ = Task.Run(async () =>                {                    var fresh = await factory(CancellationToken.None);                    await SetAsync(key, fresh, CancellationToken.None);                });            }             if (!entry.IsExpired)                return entry.Value;        }         // Cache miss or expired        var value = await factory(ct);        await SetAsync(key, value, ct);        return value;    }     private record CacheEntry<TValue>(TValue Value, DateTime CreatedAt)    {        public bool IsStale => DateTime.UtcNow - CreatedAt > _freshDuration;        public bool IsExpired => DateTime.UtcNow - CreatedAt > _staleDuration;    }}``` ## Testing Patterns ### Unit Tests with xUnit and Moq ```csharppublic class OrderServiceTests{    private readonly Mock<IOrderRepository> _mockRepository;    private readonly Mock<IStockService> _mockStockService;    private readonly Mock<IValidator<CreateOrderRequest>> _mockValidator;    private readonly OrderService _sut; // System Under Test     public OrderServiceTests()    {        _mockRepository = new Mock<IOrderRepository>();        _mockStockService = new Mock<IStockService>();        _mockValidator = new Mock<IValidator<CreateOrderRequest>>();         // Default: validation passes        _mockValidator            .Setup(v => v.ValidateAsync(It.IsAny<CreateOrderRequest>(), It.IsAny<CancellationToken>()))            .ReturnsAsync(new ValidationResult());         _sut = new OrderService(            _mockRepository.Object,            _mockStockService.Object,            _mockValidator.Object);    }     [Fact]    public async Task CreateOrderAsync_WithValidRequest_ReturnsSuccess()    {        // Arrange        var request = new CreateOrderRequest        {            ProductId = "PROD-001",            Quantity = 5,            CustomerOrderCode = "ORD-2024-001"        };         _mockStockService            .Setup(s => s.CheckAsync("PROD-001", 5, It.IsAny<CancellationToken>()))            .ReturnsAsync(new StockResult { IsAvailable = true, Available = 10 });         _mockRepository            .Setup(r => r.CreateAsync(It.IsAny<Order>(), It.IsAny<CancellationToken>()))            .ReturnsAsync(new Order { Id = 1, CustomerOrderCode = "ORD-2024-001" });         // Act        var result = await _sut.CreateOrderAsync(request);         // Assert        Assert.True(result.IsSuccess);        Assert.NotNull(result.Value);        Assert.Equal(1, result.Value.Id);         _mockRepository.Verify(            r => r.CreateAsync(It.Is<Order>(o => o.CustomerOrderCode == "ORD-2024-001"),            It.IsAny<CancellationToken>()),            Times.Once);    }     [Fact]    public async Task CreateOrderAsync_WithInsufficientStock_ReturnsFailure()    {        // Arrange        var request = new CreateOrderRequest { ProductId = "PROD-001", Quantity = 100 };         _mockStockService            .Setup(s => s.CheckAsync(It.IsAny<string>(), It.IsAny<int>(), It.IsAny<CancellationToken>()))            .ReturnsAsync(new StockResult { IsAvailable = false, Available = 5 });         // Act        var result = await _sut.CreateOrderAsync(request);         // Assert        Assert.False(result.IsSuccess);        Assert.Equal("INSUFFICIENT_STOCK", result.ErrorCode);        Assert.Contains("5 available", result.Error);         _mockRepository.Verify(            r => r.CreateAsync(It.IsAny<Order>(), It.IsAny<CancellationToken>()),            Times.Never);    }     [Theory]    [InlineData(0)]    [InlineData(-1)]    [InlineData(-100)]    public async Task CreateOrderAsync_WithInvalidQuantity_ReturnsValidationError(int quantity)    {        // Arrange        var request = new CreateOrderRequest { ProductId = "PROD-001", Quantity = quantity };         _mockValidator            .Setup(v => v.ValidateAsync(request, It.IsAny<CancellationToken>()))            .ReturnsAsync(new ValidationResult(new[]            {                new ValidationFailure("Quantity", "Quantity must be greater than 0")            }));         // Act        var result = await _sut.CreateOrderAsync(request);         // Assert        Assert.False(result.IsSuccess);        Assert.Equal("VALIDATION_ERROR", result.ErrorCode);    }}``` ### Integration Tests with WebApplicationFactory ```csharppublic class ProductsApiTests : IClassFixture<WebApplicationFactory<Program>>{    private readonly WebApplicationFactory<Program> _factory;    private readonly HttpClient _client;     public ProductsApiTests(WebApplicationFactory<Program> factory)    {        _factory = factory.WithWebHostBuilder(builder =>        {            builder.ConfigureServices(services =>            {                // Replace real database with in-memory                services.RemoveAll<DbContextOptions<AppDbContext>>();                services.AddDbContext<AppDbContext>(options =>                    options.UseInMemoryDatabase("TestDb"));                 // Replace Redis with memory cache                services.RemoveAll<IDistributedCache>();                services.AddDistributedMemoryCache();            });        });         _client = _factory.CreateClient();    }     [Fact]    public async Task GetProduct_WithValidId_ReturnsProduct()    {        // Arrange        using var scope = _factory.Services.CreateScope();        var context = scope.ServiceProvider.GetRequiredService<AppDbContext>();         context.Products.Add(new Product        {            Id = "TEST-001",            Name = "Test Product",            Price = 99.99m        });        await context.SaveChangesAsync();         // Act        var response = await _client.GetAsync("/api/products/TEST-001");         // Assert        response.EnsureSuccessStatusCode();        var product = await response.Content.ReadFromJsonAsync<Product>();        Assert.Equal("Test Product", product!.Name);    }     [Fact]    public async Task GetProduct_WithInvalidId_Returns404()    {        // Act        var response = await _client.GetAsync("/api/products/NONEXISTENT");         // Assert        Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);    }}``` ## Best Practices ### DO 1. **Use async/await** all the way through the call stack2. **Inject dependencies** through constructor injection3. **Use IOptions<T>** for typed configuration4. **Return Result types** instead of throwing exceptions for business logic5. **Use CancellationToken** in all async methods6. **Prefer Dapper** for read-heavy, performance-critical queries7. **Use EF Core** for complex domain models with change tracking8. **Cache aggressively** with proper invalidation strategies9. **Write unit tests** for business logic, integration tests for APIs10. **Use record types** for DTOs and immutable data ### DON'T 1. **Don't block on async** with `.Result` or `.Wait()`2. **Don't use async void** except for event handlers3. **Don't catch generic Exception** without re-throwing or logging4. **Don't hardcode** configuration values5. **Don't expose EF entities** directly in APIs (use DTOs)6. **Don't forget** `AsNoTracking()` for read-only queries7. **Don't ignore** CancellationToken parameters8. **Don't create** `new HttpClient()` manually (use IHttpClientFactory)9. **Don't mix** sync and async code unnecessarily10. **Don't skip** validation at API boundaries ## Common Pitfalls - **N+1 Queries**: Use `.Include()` or explicit joins- **Memory Leaks**: Dispose IDisposable resources, use `using`- **Deadlocks**: Don't mix sync and async, use ConfigureAwait(false) in libraries- **Over-fetching**: Select only needed columns, use projections- **Missing Indexes**: Check query plans, add indexes for common filters- **Timeout Issues**: Configure appropriate timeouts for HTTP clients- **Cache Stampede**: Use distributed locks for cache population