Generates audit trail infrastructure for entities. Implements IAuditable interface, EF Core SaveChanges interceptor, and automatic population of CreatedAt, UpdatedAt, CreatedBy, and UpdatedBy fields.
한국어 요약은 준비 중입니다. 원문 설명을 표시하고 있습니다.
Generates Entity Framework Core configurations using Fluent API. Maps domain entities to database tables with proper relationships, constraints, and conventions.
한국어 요약은 준비 중입니다. 원문 설명을 표시하고 있습니다.
설치하기 전에 에이전트에게 전달되는 지시 내용을 확인할 수 있습니다.
This skill generates Entity Framework Core configurations using Fluent API:
| Configuration | Use |
|---|---|
ToTable() | Table name |
HasKey() | Primary key |
Property() | Column configuration |
HasOne/HasMany() | Relationships |
OwnsOne() | Value objects |
HasIndex() | Database indexes |
/Infrastructure/Configurations/
├── {Entity}Configuration.cs
├── {ChildEntity}Configuration.cs
├── {ValueObject}Configuration.cs
└── OutboxMessageConfiguration.cs
// src/{name}.infrastructure/Configurations/{Entity}Configuration.cs
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
using {name}.domain.{aggregate};
namespace {name}.infrastructure.configurations;
internal sealed class {Entity}Configuration : IEntityTypeConfiguration<{Entity}>
{
public void Configure(EntityTypeBuilder<{Entity}> builder)
{
// ═══════════════════════════════════════════════════════════════
// TABLE MAPPING
// ═══════════════════════════════════════════════════════════════
builder.ToTable("{entity}"); // snake_case table name
// ═══════════════════════════════════════════════════════════════
// PRIMARY KEY
// ═══════════════════════════════════════════════════════════════
builder.HasKey(e => e.Id);
builder.Property(e => e.Id)
.ValueGeneratedNever(); // App generates GUIDs
// ═══════════════════════════════════════════════════════════════
// PROPERTIES
// ═══════════════════════════════════════════════════════════════
builder.Property(e => e.Name)
.HasMaxLength(100)
.IsRequired();
builder.Property(e => e.Description)
.HasColumnType("text"); // Unlimited length
builder.Property(e => e.IsActive)
.HasDefaultValue(true)
.IsRequired();
builder.Property(e => e.CreatedAt)
.IsRequired()
.HasDefaultValueSql("CURRENT_TIMESTAMP AT TIME ZONE 'UTC'");
builder.Property(e => e.UpdatedAt)
.IsRequired()
.HasDefaultValueSql("CURRENT_TIMESTAMP AT TIME ZONE 'UTC'");
// ═══════════════════════════════════════════════════════════════
// INDEXES
// ═══════════════════════════════════════════════════════════════
builder.HasIndex(e => e.Name)
.IsUnique();
builder.HasIndex(e => e.OrganizationId);
builder.HasIndex(e => new { e.OrganizationId, e.Name })
.IsUnique();
}
}
// src/{name}.infrastructure/Configurations/{Entity}Configuration.cs
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
using {name}.domain.{aggregate};
namespace {name}.infrastructure.configurations;
internal sealed class {Entity}Configuration : IEntityTypeConfiguration<{Entity}>
{
public void Configure(EntityTypeBuilder<{Entity}> builder)
{
builder.ToTable("{entity}");
builder.HasKey(e => e.Id);
// ═══════════════════════════════════════════════════════════════
// FOREIGN KEY PROPERTIES
// ═══════════════════════════════════════════════════════════════
builder.Property(e => e.OrganizationId)
.IsRequired();
builder.Property(e => e.ParentId); // Nullable FK
// ═══════════════════════════════════════════════════════════════
// ONE-TO-MANY: Parent has many children
// ═══════════════════════════════════════════════════════════════
builder.HasMany(e => e.{ChildEntities})
.WithOne(c => c.{Entity})
.HasForeignKey(c => c.{Entity}Id)
.OnDelete(DeleteBehavior.Cascade);
// ═══════════════════════════════════════════════════════════════
// MANY-TO-ONE: Entity belongs to Organization
// ═══════════════════════════════════════════════════════════════
builder.HasOne(e => e.Organization)
.WithMany(o => o.{Entities})
.HasForeignKey(e => e.OrganizationId)
.OnDelete(DeleteBehavior.Restrict); // Prevent cascade delete
// ═══════════════════════════════════════════════════════════════
// SELF-REFERENCING: Entity has optional parent
// ═══════════════════════════════════════════════════════════════
builder.HasOne(e => e.Parent)
.WithMany(e => e.Children)
.HasForeignKey(e => e.ParentId)
.OnDelete(DeleteBehavior.Restrict)
.IsRequired(false);
}
}
// src/{name}.infrastructure/Configurations/{ChildEntity}Configuration.cs
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
using {name}.domain.{aggregate};
namespace {name}.infrastructure.configurations;
internal sealed class {ChildEntity}Configuration : IEntityTypeConfiguration<{ChildEntity}>
{
public void Configure(EntityTypeBuilder<{ChildEntity}> builder)
{
builder.ToTable("{child_entity}");
builder.HasKey(c => c.Id);
builder.Property(c => c.Id)
.ValueGeneratedNever();
builder.Property(c => c.{Parent}Id)
.IsRequired();
builder.Property(c => c.Name)
.HasMaxLength(100)
.IsRequired();
builder.Property(c => c.SortOrder)
.IsRequired()
.HasDefaultValue(0);
builder.Property(c => c.CreatedAt)
.IsRequired()
.HasDefaultValueSql("CURRENT_TIMESTAMP AT TIME ZONE 'UTC'");
// Relationship defined from parent side, but can also define here
builder.HasOne(c => c.{Parent})
.WithMany(p => p.{ChildEntities})
.HasForeignKey(c => c.{Parent}Id)
.OnDelete(DeleteBehavior.Cascade);
// Composite unique constraint
builder.HasIndex(c => new { c.{Parent}Id, c.Name })
.IsUnique();
builder.HasIndex(c => new { c.{Parent}Id, c.SortOrder });
}
}
// src/{name}.infrastructure/Configurations/User{Entity}Configuration.cs
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
using {name}.domain.{aggregate};
namespace {name}.infrastructure.configurations;
// Join entity for many-to-many
internal sealed class User{Entity}Configuration : IEntityTypeConfiguration<User{Entity}>
{
public void Configure(EntityTypeBuilder<User{Entity}> builder)
{
builder.ToTable("user_{entity}");
// Composite primary key
builder.HasKey(ue => new { ue.UserId, ue.{Entity}Id });
// Or with separate ID
// builder.HasKey(ue => ue.Id);
// builder.HasIndex(ue => new { ue.UserId, ue.{Entity}Id }).IsUnique();
builder.Property(ue => ue.UserId)
.IsRequired();
builder.Property(ue => ue.{Entity}Id)
.IsRequired();
builder.Property(ue => ue.IsManager)
.HasDefaultValue(false);
builder.Property(ue => ue.CreatedAt)
.HasDefaultValueSql("CURRENT_TIMESTAMP AT TIME ZONE 'UTC'");
// Relationships
builder.HasOne(ue => ue.User)
.WithMany(u => u.User{Entities})
.HasForeignKey(ue => ue.UserId)
.OnDelete(DeleteBehavior.Cascade);
builder.HasOne(ue => ue.{Entity})
.WithMany(e => e.User{Entities})
.HasForeignKey(ue => ue.{Entity}Id)
.OnDelete(DeleteBehavior.Cascade);
}
}
// src/{name}.infrastructure/Configurations/{Entity}Configuration.cs
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
using {name}.domain.{aggregate};
namespace {name}.infrastructure.configurations;
internal sealed class {Entity}Configuration : IEntityTypeConfiguration<{Entity}>
{
public void Configure(EntityTypeBuilder<{Entity}> builder)
{
builder.ToTable("{entity}");
builder.HasKey(e => e.Id);
// ═══════════════════════════════════════════════════════════════
// VALUE OBJECT: Email (stored in same table)
// ═══════════════════════════════════════════════════════════════
builder.OwnsOne(e => e.Email, emailBuilder =>
{
emailBuilder.Property(email => email.Value)
.HasColumnName("email")
.HasMaxLength(255)
.IsRequired();
emailBuilder.HasIndex(email => email.Value)
.IsUnique();
});
// ═══════════════════════════════════════════════════════════════
// VALUE OBJECT: Address (multiple columns)
// ═══════════════════════════════════════════════════════════════
builder.OwnsOne(e => e.Address, addressBuilder =>
{
addressBuilder.Property(a => a.Street)
.HasColumnName("address_street")
.HasMaxLength(200);
addressBuilder.Property(a => a.City)
.HasColumnName("address_city")
.HasMaxLength(100);
addressBuilder.Property(a => a.State)
.HasColumnName("address_state")
.HasMaxLength(50);
addressBuilder.Property(a => a.ZipCode)
.HasColumnName("address_zip_code")
.HasMaxLength(20);
addressBuilder.Property(a => a.Country)
.HasColumnName("address_country")
.HasMaxLength(100);
});
// ═══════════════════════════════════════════════════════════════
// VALUE OBJECT: Money
// ═══════════════════════════════════════════════════════════════
builder.OwnsOne(e => e.Price, priceBuilder =>
{
priceBuilder.Property(m => m.Amount)
.HasColumnName("price_amount")
.HasColumnType("numeric(18,2)")
.IsRequired();
priceBuilder.Property(m => m.Currency)
.HasColumnName("price_currency")
.HasMaxLength(3)
.IsRequired();
});
}
}
// src/{name}.infrastructure/Configurations/{Entity}Configuration.cs
internal sealed class {Entity}Configuration : IEntityTypeConfiguration<{Entity}>
{
public void Configure(EntityTypeBuilder<{Entity}> builder)
{
// ═══════════════════════════════════════════════════════════════
// ENUM AS STRING
// ═══════════════════════════════════════════════════════════════
builder.Property(e => e.Status)
.HasConversion<string>() // Store as string
.HasMaxLength(50)
.IsRequired();
// ═══════════════════════════════════════════════════════════════
// ENUM AS INTEGER (default)
// ═══════════════════════════════════════════════════════════════
builder.Property(e => e.Priority)
.HasConversion<int>(); // Store as int
// ═══════════════════════════════════════════════════════════════
// CUSTOM CONVERSION
// ═══════════════════════════════════════════════════════════════
builder.Property(e => e.Type)
.HasConversion(
v => v.ToString().ToLowerInvariant(),
v => Enum.Parse<EntityType>(v, true))
.HasMaxLength(50);
}
}
// src/{name}.infrastructure/Configurations/{Entity}Configuration.cs
internal sealed class {Entity}Configuration : IEntityTypeConfiguration<{Entity}>
{
public void Configure(EntityTypeBuilder<{Entity}> builder)
{
builder.ToTable("{entity}");
// ═══════════════════════════════════════════════════════════════
// SHADOW PROPERTIES (not on domain entity)
// ═══════════════════════════════════════════════════════════════
builder.Property<DateTime>("CreatedAt")
.IsRequired()
.HasDefaultValueSql("CURRENT_TIMESTAMP AT TIME ZONE 'UTC'");
builder.Property<DateTime>("UpdatedAt")
.IsRequired()
.HasDefaultValueSql("CURRENT_TIMESTAMP AT TIME ZONE 'UTC'");
builder.Property<string>("CreatedBy")
.HasMaxLength(100);
builder.Property<string>("UpdatedBy")
.HasMaxLength(100);
builder.Property<uint>("Version")
.IsRowVersion(); // Concurrency token
}
}
// src/{name}.infrastructure/Configurations/{Entity}Configuration.cs
internal sealed class {Entity}Configuration : IEntityTypeConfiguration<{Entity}>
{
public void Configure(EntityTypeBuilder<{Entity}> builder)
{
builder.ToTable("{entity}");
// Soft delete property
builder.Property(e => e.IsDeleted)
.HasDefaultValue(false)
.IsRequired();
builder.Property(e => e.DeletedAt);
// ═══════════════════════════════════════════════════════════════
// GLOBAL QUERY FILTER (excludes soft-deleted)
// ═══════════════════════════════════════════════════════════════
builder.HasQueryFilter(e => !e.IsDeleted);
// Index for soft delete queries
builder.HasIndex(e => e.IsDeleted);
}
}
// src/{name}.infrastructure/Configurations/{Entity}Configuration.cs
internal sealed class {Entity}Configuration : IEntityTypeConfiguration<{Entity}>
{
public void Configure(EntityTypeBuilder<{Entity}> builder)
{
builder.ToTable("{entity}");
// ═══════════════════════════════════════════════════════════════
// JSON COLUMN (PostgreSQL JSONB)
// ═══════════════════════════════════════════════════════════════
builder.Property(e => e.Metadata)
.HasColumnType("jsonb")
.HasConversion(
v => JsonSerializer.Serialize(v, (JsonSerializerOptions?)null),
v => JsonSerializer.Deserialize<Dictionary<string, object>>(v, (JsonSerializerOptions?)null)!);
// Or for owned types in JSON
builder.OwnsOne(e => e.Settings, settingsBuilder =>
{
settingsBuilder.ToJson(); // EF Core 7+
settingsBuilder.Property(s => s.NotificationsEnabled);
settingsBuilder.Property(s => s.Theme);
});
}
}
// src/{name}.infrastructure/Configurations/OutboxMessageConfiguration.cs
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
using {name}.infrastructure.outbox;
namespace {name}.infrastructure.configurations;
internal sealed class OutboxMessageConfiguration : IEntityTypeConfiguration<OutboxMessage>
{
public void Configure(EntityTypeBuilder<OutboxMessage> builder)
{
builder.ToTable("outbox_message");
builder.HasKey(o => o.Id);
builder.Property(o => o.Id)
.ValueGeneratedNever();
builder.Property(o => o.OccurredOnUtc)
.IsRequired();
builder.Property(o => o.Type)
.HasMaxLength(500)
.IsRequired();
builder.Property(o => o.Content)
.HasColumnType("jsonb")
.IsRequired();
builder.Property(o => o.ProcessedOnUtc);
builder.Property(o => o.Error)
.HasColumnType("text");
// Index for processing unprocessed messages
builder.HasIndex(o => o.ProcessedOnUtc)
.HasFilter("processed_on_utc IS NULL");
}
}
// In DependencyInjection.cs
services.AddDbContext<ApplicationDbContext>(options =>
{
options.UseNpgsql(connectionString)
.UseSnakeCaseNamingConvention(); // Requires EFCore.NamingConventions
});
| C# Type | PostgreSQL Type | Configuration |
|---|---|---|
string | text | .HasColumnType("text") |
string (limited) | varchar(n) | .HasMaxLength(n) |
decimal | numeric(p,s) | .HasColumnType("numeric(18,2)") |
DateTime | timestamp | .HasColumnType("timestamp") |
DateTimeOffset | timestamptz | .HasColumnType("timestamptz") |
Guid | uuid | (automatic) |
bool | boolean | (automatic) |
byte[] | bytea | (automatic) |
Dictionary | jsonb | .HasColumnType("jsonb") |
// src/{name}.infrastructure/ApplicationDbContext.cs
using Microsoft.EntityFrameworkCore;
using {name}.domain.abstractions;
namespace {name}.infrastructure;
public sealed class ApplicationDbContext : DbContext, IUnitOfWork
{
public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options)
: base(options)
{
}
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
// Apply all configurations from assembly
modelBuilder.ApplyConfigurationsFromAssembly(
typeof(ApplicationDbContext).Assembly);
base.OnModelCreating(modelBuilder);
}
public override async Task<int> SaveChangesAsync(CancellationToken cancellationToken = default)
{
// Add domain events to outbox
AddDomainEventsAsOutboxMessages();
// Update audit fields
UpdateAuditFields();
return await base.SaveChangesAsync(cancellationToken);
}
private void UpdateAuditFields()
{
var entries = ChangeTracker.Entries()
.Where(e => e.State is EntityState.Added or EntityState.Modified);
foreach (var entry in entries)
{
if (entry.State == EntityState.Added)
{
entry.Property("CreatedAt").CurrentValue = DateTime.UtcNow;
}
entry.Property("UpdatedAt").CurrentValue = DateTime.UtcNow;
}
}
}
IEntityTypeConfiguration<T>// ❌ WRONG: Data annotations on domain
public class User
{
[Key]
public Guid Id { get; set; }
[Required]
[MaxLength(100)]
public string Name { get; set; } // Pollutes domain!
}
// ✅ CORRECT: Fluent API in configuration
builder.Property(u => u.Name).HasMaxLength(100).IsRequired();
// ❌ WRONG: Not specifying string length
builder.Property(e => e.Name); // Defaults to MAX!
// ✅ CORRECT: Always specify max length
builder.Property(e => e.Name).HasMaxLength(100);
// ❌ WRONG: Auto-increment for GUIDs
builder.Property(e => e.Id).ValueGeneratedOnAdd();
// ✅ CORRECT: App generates GUIDs
builder.Property(e => e.Id).ValueGeneratedNever();
// ❌ WRONG: No cascade strategy
builder.HasMany(e => e.Children).WithOne();
// ✅ CORRECT: Explicit delete behavior
builder.HasMany(e => e.Children)
.WithOne(c => c.Parent)
.OnDelete(DeleteBehavior.Cascade);
dotnet-domain-entity-generator - Generate entities to configuredotnet-repository-pattern - Use configurations with repositoriesdotnet-clean-architecture - Infrastructure layer placement아직 리뷰가 없습니다. 사용해 본 소감을 남겨 주세요.
로그인 없이 쓸 수 있습니다. 사용 후기, 질문, 작동하지 않은 환경 등을 편하게 남겨 주세요.
아직 댓글이 없습니다. 첫 댓글을 남겨 보세요.
Generates audit trail infrastructure for entities. Implements IAuditable interface, EF Core SaveChanges interceptor, and automatic population of CreatedAt, UpdatedAt, CreatedBy, and UpdatedBy fields.
한국어 요약은 준비 중입니다. 원문 설명을 표시하고 있습니다.
Scaffolds a complete .NET solution following Clean Architecture principles with proper layer separation (API, Application, Domain, Infrastructure). Creates project structure, dependency injection setup, and cross-cutting concerns configuration.
한국어 요약은 준비 중입니다. 원문 설명을 표시하고 있습니다.
Generates CQRS Commands with Handlers, Validators, and Request DTOs following Clean Architecture patterns. Commands represent actions that modify state and return Result types for proper error handling.
한국어 요약은 준비 중입니다. 원문 설명을 표시하고 있습니다.
Generates CQRS Queries with Handlers and Response DTOs for read operations. Uses Dapper for optimized read queries, bypassing the domain model for better performance.
한국어 요약은 준비 중입니다. 원문 설명을 표시하고 있습니다.
Generates optimized read queries using Dapper. Includes multi-mapping for joins, pagination, dynamic filtering, CTEs, and best practices for high-performance data access.
한국어 요약은 준비 중입니다. 원문 설명을 표시하고 있습니다.
Generates Domain Entities following DDD principles with factory methods, private setters, domain events, and proper encapsulation. Supports aggregate roots, child entities, and value objects.
한국어 요약은 준비 중입니다. 원문 설명을 표시하고 있습니다.