nesbo/dotnet-claude-code-skills/data-dotnet

Claude Code and agent skill by nesbo/dotnet-claude-code-skills

Trust tier 2
  • community
Source check pending

Checking that the SKILL.md file is still in the repository.

Install

npx vskill@latest install nesbo/dotnet-claude-code-skills/data-dotnet

SKILL.md

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<ShipperOrder>
{
public void Configure(EntityTypeBuilder<ShipperOrder> builder)
{
// Table mapping with schema
builder.ToTable("ShipperOrders", ConfigHelper.ShipmentScheme)
.HasKey(p => p.Id);
// Primary key with custom column name
builder.Property(p => p.Id)
.HasColumnName("ShipperOrderId");
// Bulk required properties
builder.SetRequired(
d => d.ShipperId,
d => d.ShipperCompanyName);
// String length constraints
builder.Property(d => d.ShipperCompanyName).HasMaxLength(128);
builder.Property(d => d.OrderNumber).HasMaxLength(64);
// Nullable properties
builder.Property(d => d.DispatcherId);
builder.Property(d => d.DispatcherCompanyName).HasMaxLength(128);
// One-to-many relationships (owned collections)
builder.HasMany(d => d.Changes)
.WithOne()
.HasForeignKey(c => c.ShipperOrderId);
builder.HasMany(d => d.Versions)
.WithOne()
.HasForeignKey(c => c.ShipperOrderId);
builder.HasMany(d => d.Packages)
.WithOne()
.HasForeignKey(p => p.ShipperOrderId);
// Indexes
builder.HasIndex(d => new { d.ShipperId, d.OrderNumber }).IsUnique();
builder.HasIndex(d => d.Guid);
}
}
Entity Configuration Example

Configuration for 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 PackageConfiguration : IEntityTypeConfiguration<Package>
{
public void Configure(EntityTypeBuilder<Package> builder)
{
builder.ToTable("Packages", ConfigHelper.ShipmentScheme)
.HasKey(p => p.Id);
builder.Property(p => p.Id)
.HasColumnName("PackageId");
// Database-generated GUID
builder.Property(p => p.Guid)
.HasDefaultValueSql("gen_random_uuid()");
// Custom column types
builder.Property(p => p.Numeration)
.HasColumnType("smallint");
// Enum to byte conversion
builder.Property(p => p.Status)
.HasColumnType("smallint")
.HasConversion<byte>();
builder.SetRequired(
d => d.Kg,
d => d.ShipperOrderId,
d => d.Guid,
d => d.Numeration,
d => d.Status);
// Decimal precision
builder.Property(d => d.Kg)
.HasColumnType("decimal(10,2)");
}
}
Simple Aggregate Configuration

Configuration for aggregates without child entities:

csharp
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
using Pikot.LMP.Domain.Dispatcher;
using Pikot.LMP.Domain.Dispatcher.Dispatchers;
namespace Pikot.LMP.Adapters.Data.EntityConfigurations;
internal class DispatcherConfiguration : IEntityTypeConfiguration<Dispatcher>
{
public void Configure(EntityTypeBuilder<Dispatcher> builder)
{
builder.ToTable("Dispatchers", ConfigHelper.DispatcherScheme)
.HasKey(p => p.Id);
builder.Property(p => p.Id)
.HasColumnName("DispatcherId");
builder.SetRequired(
d => d.Name,
d => d.Itin,
d => d.CreatedAt,
d => d.InsertedAt,
d => d.IsActive);
builder.Property(d => d.Name).HasMaxLength(128);
builder.Property(d => d.Itin).HasMaxLength(15);
builder.Property(d => d.ContactEmail).HasMaxLength(128);
builder.Property(d => d.ContactName).HasMaxLength(64);
builder.Property(d => d.ContactPhone).HasMaxLength(16);
builder.HasIndex(d => d.Itin).IsUnique();
}
}
One-to-One Relationship Configuration

This is the start of the file. Read the full SKILL.md on GitHub.

Security

  • Security scanPassed, 100 of 100Pattern checks on the skill's files.
  • Model reviewPassed

Quality evaluation

  • Task evaluationNot runNo evaluation yet. Run one in Skill Studio.

Source

Popularity

7-day trend score
0

Refreshed 17 Mar 2026, 7 months ago.

Works with

Any agent vskill installs to: 56 targets in all. These 39 have a page on this site.

  • Claude Code
  • Cursor
  • GitHub Copilot
  • Windsurf
  • Codex
  • Gemini CLI
  • Cline
  • Amp
  • Roo Code
  • Goose
  • Continue
  • Kilo Code
  • Junie
  • Trae
  • OpenHands
  • Qwen Code
  • AI Agent
  • Tabnine Chat
  • Sourcegraph Cody
  • Amazon Q
  • Firebase Genkit
  • Sweep
  • Devin
  • Phind
  • Mentat
  • Claude Desktop
  • Aider
  • GPT Pilot
  • Plandex
  • bolt.new
  • v0
  • Replit Agent
  • Anse
  • TypingMind
  • BetterChatGPT
  • LibreChat
  • Warp AI
  • Pieces
  • Fig AI
Embed badge
vskill VERIFIED badge
[![vskill VERIFIED](https://verified-skill.com/api/v1/skills/nesbo/dotnet-claude-code-skills/data-dotnet/badge)](https://verified-skill.com/skills/nesbo/dotnet-claude-code-skills/data-dotnet)

Something wrong with this skill or its page? Tell us.