Claude
Skills
Sign in
Back

data-dotnet

Included with Lifetime
$97 forever

# EF Core Data Persistence Layer - Hexagonal Architecture Adapter

General

What this skill does

# EF Core Data Persistence Layer - Hexagonal Architecture Adapter

## Overview

This skill provides a comprehensive guide to implementing the data persistence layer using Entity Framework Core as an adapter in a hexagonal architecture (ports and adapters pattern). The data layer is a **library that encapsulates its implementation internally** and **exposes DI registration publicly** for wiring into applications.

This skill complements the `ddd-dotnet-basics.md` skill and assumes familiarity with Domain layer patterns.

### Key Architecture Principles

```
┌─────────────────────────────────────────────┐
│        Domain (Ports)                       │
│  - IRepository<TAggregate, TKey>            │
│  - IQueryRepository<TAggregate>             │
│  - IUnitOfWork                              │
│  - Aggregates & Entities                    │
└─────────────────────────────────────────────┘
                     ▲
                     │ (implements)
                     │
┌─────────────────────────────────────────────┐
│        Data (Adapter - Infrastructure)      │
│  - DbContext                                │
│  - Entity Configurations                    │
│  - Repository Implementations               │
│  - UnitOfWork Implementation                │
│  - Service Registrations (DI)               │
└─────────────────────────────────────────────┘
```

**Key Benefits:**
- **Testability**: In-memory database for unit/integration tests
- **Separation of Concerns**: Domain knows nothing about EF Core
- **CQRS Support**: Separate read/write repository base classes
- **Schema Organization**: Multiple database schemas for bounded contexts
- **Convention-based Registration**: Automatic repository discovery and registration

---

## Project Structure

```
Pikot.LMP.Adapters.Data/
├── DataContext.cs                    # Main DbContext
├── UnitOfWork.cs                     # Transaction coordinator
├── ServiceRegistrations.cs           # DI setup (public API)
├── EntityConfigurations/             # Fluent configurations
│   ├── ConfigHelper.cs               # Reusable config utilities
│   ├── ShipperOrderConfiguration.cs
│   ├── PackageConfiguration.cs
│   └── ...
├── Repositories/                     # Repository implementations
│   ├── RepositoryBase.cs             # Write repository base
│   ├── QueryRepositoryBase.cs        # Read repository base
│   ├── ShipperOrderRepository.cs
│   ├── ShipperOrderQueryRepository.cs
│   └── ...
└── Migrations/                       # EF Core migrations
    └── ...
```

---

## DbContext Configuration

### Main DbContext

The `DataContext` is the central EF Core context with specific patterns for production and testing.

```csharp
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;

namespace Pikot.LMP.Adapters.Data;

public class DataContext : DbContext
{
    public DataContext(DbContextOptions options) : base(options) { }

    public DataContext() { }  // Parameterless for design-time tools

    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    {
        // Fallback configuration for design-time tools (migrations)
        if (!optionsBuilder.IsConfigured)
        {
            optionsBuilder.UseNpgsql("Server=127.0.0.1;Database=lmp-nia;User Id=admin;Password=Password1!");
        }

        base.OnConfiguring(optionsBuilder);
    }

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        // Auto-discover and apply all IEntityTypeConfiguration<T> from assembly
        modelBuilder.ApplyConfigurationsFromAssembly(typeof(DataContext).Assembly);

        base.OnModelCreating(modelBuilder);
    }

    // Migration runner with retry logic (for application startup)
    public static async Task RunDbMigrationsAsync(IServiceProvider serviceProvider)
    {
        using var scope = serviceProvider.CreateScope();
        var logger = scope.ServiceProvider.GetRequiredService<ILogger<DataContext>>();

        const int maxAttempts = 5;
        var attempt = 1;
        var success = false;
        Exception? startupException = null;

        while (attempt < maxAttempts && !success)
        {
            try
            {
                logger.LogInformation("Attempting db migration run on startup. Attempt [{StartupMigrationAttempt}]",
                    attempt);
                using var dbScope = scope.ServiceProvider.CreateScope();
                var db = dbScope.ServiceProvider.GetRequiredService<DataContext>();
                await db.Database.MigrateAsync();
                logger.LogInformation("Db migration run successful");
                success = true;
                startupException = null;
            }
            catch (Exception ex)
            {
                startupException = ex;
                logger.LogError(ex, "Error accessing db for startup migration run on attempt [{StartupMigrationAttempt}]",
                    attempt);
                attempt++;
                await ExponentialBackoffAsync(attempt);
            }
        }

        if (attempt < maxAttempts || startupException == null)
        {
            return;
        }

        logger.LogCritical("Db migration run failed. Service run FAILED");
        throw new Exception("Critical stop", startupException);
    }

    private static Task ExponentialBackoffAsync(int attempt)
    {
        var waitInterval = TimeSpan.FromSeconds(Fibonacci(attempt));
        return Task.Delay(waitInterval);
    }

    private static int Fibonacci(int n)
    {
        if (n is 0 or 1)
        {
            return n;
        }
        return Fibonacci(n - 1) + Fibonacci(n - 2);
    }
}
```

**Key Patterns:**
- **Two constructors**: Parameterized for runtime, parameterless for design-time
- **Conditional configuration**: Fallback for migration tools
- **Assembly scanning**: Auto-discovers entity configurations
- **Migration runner**: Resilient startup with exponential backoff
- **Logging**: Track migration attempts and failures

---

## Entity Configurations

Entity configurations use the Fluent API to map domain objects to database tables. Each entity gets its own configuration class.

### Configuration Helper

Reusable utilities for common configuration patterns:

```csharp
using System.Linq.Expressions;
using Microsoft.EntityFrameworkCore.Metadata.Builders;

namespace Pikot.LMP.Adapters.Data.EntityConfigurations;

internal static class ConfigHelper
{
    // Bulk set multiple properties as required
    public static void SetRequired<TEntityType>(this EntityTypeBuilder<TEntityType> builder,
        params Expression<Func<TEntityType, object>>[] setters) where TEntityType : class
    {
        foreach (var setter in setters)
        {
            var member = setter.Body;
            if (member == null)
            {
                throw new ArgumentNullException(nameof(setter.Body));
            }

            var propInfo = member is UnaryExpression expression
                ? expression.Operand as MemberExpression
                : member as MemberExpression;

            builder.Property(propInfo.Member.Name).IsRequired();
        }
    }

    public static void SetMaxLength<TEntityType, TProperty>(this EntityTypeBuilder<TEntityType> builder,
        Expression<Func<TEntityType, TProperty>> property, int maxLength)
        where TEntityType : class
    {
        builder.Property(property).HasMaxLength(maxLength);
    }

    // Database schemas for bounded contexts
    public const string DispatcherScheme  = "Dispatcher";
}
```

### Aggregate Configuration Example

Configuration for an aggregate root with child entities:

```csharp
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
using Pikot.LMP.Domain.Shipment;
using Pikot.LMP.Domain.Shipment.ShipperOrders;

namespace Pikot.LMP.Adapters.Data.EntityConfigurations;

internal class ShipperOrderConfiguration : IEntityTypeConfiguration<ShipperO
Files: 3
Size: 37.0 KB
Complexity: 34/100
Category: General

Related in General