Create EF Core entity type configurations (mappers), static data seeders, and infra services following this project's auto-discovery conventions. Use after creating a domain entity.
Create the persistence-layer configuration for a domain entity — mapper, static data, and infra service implementations.
"CustomerProfiles", "Orders"DomainSchemas (e.g., DomainSchemas.Profile)I{Service} interfaces from Domains layer?DefaultEntityTypeConfiguration<TEntity> (NOT raw IEntityTypeConfiguration<T>)internal sealed — Scrutor auto-discovery requires thisbase.Configure(builder) first — it configures base AuditedEntity fields (Id, CreatedBy, CreatedAt, etc.)UseAutoConfigModel([typeof(CoreDbContext).Assembly]) in InfraSetup.cssrc/ApiEndpoints/Minimal.Infra/
├── Features/
│ └── {Feature}/
│ ├── Mappers/
│ │ └── {Entity}Configs.cs ← Entity type configuration
│ ├── StaticData/
│ │ └── {Entity}StaticData.cs ← Seed data (optional)
│ └── ExternalEvents/
│ └── {Event}Handler.cs ← External event consumers (optional)
├── Services/
│ ├── {Service}.cs ← Domain service implementations
│ └── EventPublisher.cs ← DO NOT MODIFY
├── Contexts/
│ ├── CoreDbContext.cs ← DO NOT MODIFY
│ └── OwnedDataContext.cs ← Register owned types here
└── Extensions/
├── InfraSetup.cs ← DO NOT MODIFY (auto-scans)
└── ServiceBusSetup.cs ← DO NOT MODIFY
Services are auto-registered when they meet ALL of:
sealed.Repos OR .ServicesCreate src/ApiEndpoints/Minimal.Infra/Features/{Feature}/Mappers/{Entity}Configs.cs:
using Minimal.Domains.Features.{Feature}.Entities;
namespace Minimal.Infra.Features.{Feature}.Mappers;
internal sealed class {Entity}Configs : DefaultEntityTypeConfiguration<{Entity}>
{
#region Methods
public override void Configure(EntityTypeBuilder<{Entity}> builder)
{
// MUST call base first — configures Id, CreatedBy, CreatedAt, UpdatedBy, UpdatedAt, IsDeleted
base.Configure(builder);
// Unique indexes
builder.HasIndex(p => p.{UniqueField}).IsUnique();
// Property configurations
builder.Property(p => p.{StringProp}).HasMaxLength({max}).IsRequired();
builder.Property(p => p.{OptionalProp}).HasMaxLength({max}).IsRequired(false);
builder.Property(p => p.{DateProp}).HasColumnType("Date");
// Table mapping with schema
builder.ToTable("{TableName}", DomainSchemas.{Feature});
}
#endregion
}
If your entity has owned types like Address or Company, register them in
src/ApiEndpoints/Minimal.Infra/Contexts/OwnedDataContext.cs:
// Inside OwnedDataContext, add to the existing ConfigureConventions or OnModelCreating:
builder.Entity<{Entity}>().OwnsOne(e => e.{OwnedProp}, owned =>
{
owned.Property(p => p.{Prop}).HasMaxLength({max});
});
Create src/ApiEndpoints/Minimal.Infra/Features/{Feature}/StaticData/{Entity}StaticData.cs:
using DKNet.EfCore.Extensions.DataSeeding;
using Minimal.Domains.Features.{Feature}.Entities;
namespace Minimal.Infra.Features.{Feature}.StaticData;
internal sealed class {Entity}StaticData : SqlDataSeeding<{Entity}>
{
protected override IEnumerable<{Entity}> Data =>
[
new({params for seed row 1}),
new({params for seed row 2}),
];
}
Auto-discovered by UseAutoDataSeeding([typeof(InfraSetup).Assembly]).
Create src/ApiEndpoints/Minimal.Infra/Services/{Service}.cs:
using Minimal.Domains.Services;
namespace Minimal.Infra.Services;
/// <summary>
/// Implementation of <see cref="I{Service}"/>.
/// </summary>
internal sealed class {Service} : I{Service}
{
private readonly ISequenceServices _sequence;
public {Service}(ISequenceServices sequence)
{
_sequence = sequence;
}
public async Task<string> NextValueAsync()
{
var seq = await _sequence.NextValueAsync(Sequences.{Entity}Seq);
return $"{PREFIX}-{seq:D6}";
}
}
Critical: Class MUST be sealed and in the Minimal.Infra.Services namespace for Scrutor auto-registration.
cd src/ApiEndpoints
./add-migration.sh {MigrationName}
Verify the generated migration in src/ApiEndpoints/Minimal.Infra/Migrations/.
internal sealed class CustomerProfileConfigs : DefaultEntityTypeConfiguration<CustomerProfile>
{
public override void Configure(EntityTypeBuilder<CustomerProfile> builder)
{
base.Configure(builder);
builder.HasIndex(p => p.Email).IsUnique();
builder.HasIndex(p => p.MembershipNo).IsUnique();
builder.Property(p => p.Avatar).HasMaxLength(50);
builder.Property(p => p.BirthDay).HasColumnType("Date");
builder.Property(p => p.Email).HasMaxLength(150).IsRequired();
builder.Property(p => p.MembershipNo).HasMaxLength(50).IsRequired();
builder.Property(p => p.Name).HasMaxLength(150).IsRequired();
builder.Property(p => p.Phone).HasMaxLength(50).IsRequired(false);
builder.ToTable("CustomerProfiles", DomainSchemas.Profile);
}
}
internal sealed class MembershipService(ISequenceServices sequence) : IMembershipService
{
public async Task<string> NextValueAsync()
{
var seq = await sequence.NextValueAsync(Sequences.MembershipSeq);
return $"MEM-{seq:D6}";
}
}
DefaultEntityTypeConfiguration<{Entity}> (not raw IEntityTypeConfiguration)internal sealed (required for auto-discovery)base.Configure(builder) is called FIRST in Configure methodHasMaxLength()IsRequired() / IsRequired(false)ToTable("{Name}", DomainSchemas.{Schema}) set with correct schemaMinimal.Infra/Features/{Feature}/Mappers/internal sealed in .Services namespace./add-migration.sh {Name}dotnet build src/DKNet.Templates.sln -c Release passes| Mistake | Fix |
|---|---|
Inheriting IEntityTypeConfiguration directly | Use DefaultEntityTypeConfiguration<T> — it configures audit fields |
Forgetting base.Configure(builder) | Must be first line — sets up Id, audit trail, IsDeleted filter |
Making mapper public | Must be internal sealed for Scrutor auto-discovery |
Placing mapper outside Mappers/ folder | Scrutor scans by namespace — must be in correct folder |
Missing HasMaxLength on strings | SQL Server defaults to nvarchar(max) — always constrain |
Service not sealed or wrong namespace | Must be sealed + in .Services or .Repos namespace |
After configuring EF Core, proceed to: → dknet-appservices-actions skill to create CRUD actions and business logic