DRN.Framework.EntityFramework
0.10.0-preview001
Prefix Reserved
dotnet add package DRN.Framework.EntityFramework --version 0.10.0-preview001
NuGet\Install-Package DRN.Framework.EntityFramework -Version 0.10.0-preview001
<PackageReference Include="DRN.Framework.EntityFramework" Version="0.10.0-preview001" />
<PackageVersion Include="DRN.Framework.EntityFramework" Version="0.10.0-preview001" />
<PackageReference Include="DRN.Framework.EntityFramework" />
paket add DRN.Framework.EntityFramework --version 0.10.0-preview001
#r "nuget: DRN.Framework.EntityFramework, 0.10.0-preview001"
#:package DRN.Framework.EntityFramework@0.10.0-preview001
#addin nuget:?package=DRN.Framework.EntityFramework&version=0.10.0-preview001&prerelease
#tool nuget:?package=DRN.Framework.EntityFramework&version=0.10.0-preview001&prerelease
DRN.Framework.EntityFramework
Convention-based Entity Framework Core integration with automatic configuration, migrations, and Source-Known entity lifecycle support.
Features
DrnContext<TContext>provides attribute-based registration, model conventions, and design-time migration support.- Source-Known entities receive internal IDs during tracking and external identity and lifecycle initialization before saving.
SourceKnownRepository<TContext, TEntity>provides validated lookups, query filters, cancellation scopes, and cursor pagination.- Startup validation can apply migrations in Development and Staging. Prototype mode can recreate a disposable Development database.
Production auto-migration and domain-event publishing are not implemented by this package.
Table of Contents
- QuickStart: Beginner
- QuickStart: Advanced
- Identity System
- DrnContext
- Context-Specific Migrations
- Seeding
- Identity Naming Conventions
- SourceKnownRepository
- Entity Configuration
- Attributes & Configuration
- Prototype Mode
- Connection String Resolution by Environment
- Configuration Settings Reference
- Global Usings
- Related Packages
QuickStart: Beginner
Define a DbContext and entity with automatic ID generation:
using DRN.Framework.EntityFramework.Context;
using DRN.Framework.SharedKernel.Domain;
using Microsoft.EntityFrameworkCore;
// 1. Define your application partition and entity
public readonly struct MyApp : IAppId
{
public const byte Value = 1;
public static byte AppId => Value;
}
[EntityType<MyApp>(1)] // Unique byte within MyApp partition
public class User : AggregateRoot
{
public string Username { get; set; } = "";
public bool IsActive { get; set; } = true;
}
// 2. Create your context inheriting from DrnContext
public class AppContext : DrnContext<AppContext>
{
public AppContext(DbContextOptions<AppContext> options) : base(options) { }
public AppContext() : base(null) { } // Required for migrations
public DbSet<User> Users => Set<User>();
}
// 3. Save a new entity
public class UserService(AppContext context)
{
public async Task CreateUserAsync(string username)
{
var user = new User { Username = username };
context.Users.Add(user); // Assigns the internal Id
await context.SaveChangesAsync(); // Initializes external identity and lifecycle state
}
}
Register the assembly containing the context during application startup and configure the matching AppId in appsettings.json:
using DRN.Framework.Utils.DependencyInjection;
builder.Services.AddServicesWithAttributes(typeof(AppContext).Assembly);
{
"NexusAppSettings": {
"AppId": 1
}
}
DRN.Framework.Hosting runs startup validation automatically. Migration and seeding follow the environment settings described below. Standalone hosts must integrate the framework startup validation lifecycle explicitly.
Configure a connection using Connection String Resolution by Environment. The named key for this example is ConnectionStrings:AppContext. Use the additional imports in Global Usings for the examples below. Each example is an alternative or extension, not a second declaration to add to the same project.
QuickStart: Advanced
Place public DTOs in the consuming application's *.Contract project. This example extends the beginner entity and context with a repository and controller:
public sealed class UserDto(SourceKnownEntity? entity = null) : Dto(entity)
{
public required string Username { get; init; }
}
// Repository with custom query methods
public interface IUserRepository : ISourceKnownRepository<User>
{
Task<User[]> GetActiveUsersAsync();
}
[Scoped<IUserRepository>]
public class UserRepository(AppContext context, IEntityUtils utils)
: SourceKnownRepository<AppContext, User>(context, utils), IUserRepository
{
public async Task<User[]> GetActiveUsersAsync()
{
return await EntitiesWithAppliedSettings()
.Where(u => u.IsActive)
.ToArrayAsync(CancellationToken);
}
}
// Controller with pagination
[ApiController, Route("api/users")]
public class UserController(IUserRepository repository) : ControllerBase
{
[HttpGet]
public async Task<PaginationResultModel<UserDto>> GetAsync([FromQuery] PaginationRequest request)
{
var result = await repository.PaginateAsync(request);
return result.ToModel(u => new UserDto(u) { Username = u.Username });
}
[HttpGet("{id:guid}")]
public async Task<UserDto> GetByIdAsync(Guid id)
{
var user = await repository.GetAsync(id); // Validates ID automatically
return new UserDto(user) { Username = user.Username };
}
}
Scan the repository assembly with AddServicesWithAttributes too if it differs from the context assembly. Apply your application's authorization policy to these endpoints; ID validation does not grant access to a record.
Identity System
The framework uses a database-optimized internal identifier and exposes Guid EntityId for external use. SourceKnownEntityId supports domain and repository identity operations; do not expose it in public contracts.
External Identity Rule: Always use Guid EntityId (mapped as Id in DTOs) for public contracts, API route parameters, and external lookups. Internal numeric IDs must never be exposed outside domain and infrastructure boundaries.
The internal long is the database key used for indexes and joins. External IDs are validated against the expected entity type and application partition. Secure IDs reduce predictability but do not replace authorization or rate limiting.
DrnContext
Derive from DrnContext<TContext> and provide both constructors shown in the beginner example. The options constructor is used by dependency injection; the public parameterless constructor supports IDesignTimeDbContextFactory<TContext>.
Standard Attributes (Inherited)
Every DrnContext inherits registration, provider, and performance defaults. Customize database and performance settings with attributes derived from NpgsqlDbContextOptionsAttribute or NpgsqlPerformanceSettingsAttribute; service registration is inherited and should not be reapplied.
// The base class defines these defaults:
[DrnContextServiceRegistration, DrnContextDefaults, DrnContextPerformanceDefaults]
public abstract class DrnContext<TContext> : DbContext, IDrnContext<TContext>
where TContext : DrnContext<TContext>, new()
{
// ...
}
| Attribute | Description |
|---|---|
DrnContextServiceRegistration |
Context registration after assembly scanning; startup validation and migration management |
DrnContextDefaults |
Npgsql defaults, JSON configuration, logging setup |
DrnContextPerformanceDefaults |
Connection pooling, auto-prepare, command timeouts |
Model Conventions
- The context's short name selects the connection string, such as
ConnectionStrings:QAContext. - Its name converted to
snake_caseis the default schema. IEntityTypeConfiguration<T>classes are discovered in the context assembly when their namespace equals the context namespace or is a child namespace.ExtendedPropertiesis an optionaljsonbcolumn. JSON models use owned JSON mapping.DomainEventandIDomainEventare excluded from the model.
Entity ID Generation
Entities inheriting from SourceKnownEntity receive internal IDs when EF begins tracking them. Save processing supplies a fallback if the internal ID is still zero and initializes external identity and lifecycle state before persistence:
| Stage | Behavior |
|---|---|
| Added to tracking | SourceKnownIdValueGenerator assigns a non-temporary internal long ID if it is zero |
| Saving an added entity | DrnSaveChangesInterceptor supplies a missing internal ID, initializes missing external identity and EntityIdOps, sets ModifiedAt to CreatedAt, and invokes the created hook |
| Saving a modified entity | Sets ModifiedAt to the current UTC time and invokes the modified hook |
| Saving a deleted entity | Invokes the deleted hook; this is not automatic soft deletion |
| Materializing a query result | DrnMaterializationInterceptor initializes EntityIdSource and EntityIdOps, enabling entity ID conversion operations |
CreatedAt is derived from the Source-Known ID. It is not a separately assigned creation timestamp. Lifecycle hooks can collect domain events; this package does not publish them.
For mapped inheritance, the key and shared properties belong to the EF hierarchy root. This supports table-per-hierarchy (TPH), table-per-type (TPT), and table-per-concrete-type (TPC) mappings. Derived entities inherit the key and ID generator. Each concrete entity still declares its own entity-type metadata. Abstract bases need no attribute, and ordinary CLR inheritance with an unmapped base retains the same conventions.
Startup Validation
When the framework startup validation lifecycle runs, registered contexts are validated:
- Registered contexts must resolve from dependency injection.
- Concrete, non-private Source-Known entities require unique
(EntityType, AppId)pairs. A different application partition may reuse the entity byte. - A single context cannot contain multiple non-test
AppIdpartitions.NexusAppSettings:AppIdmust match its partition or another registered host partition. - Abstract and effectively private entities are excluded from model and assembly discovery. An entity is effectively private if it or any enclosing type is private, matching analyzer eligibility.
- Pending model changes fail validation even when auto-migration is disabled, unless the prototype recreation conditions are satisfied.
- Eligible automatic migration runs invoke seeding, including when no migrations remain.
Context-Specific Migrations
DrnMigrationsScaffolder places migrations under the context's namespace relative to its assembly name, followed by Migrations. For example, a context in Sample.Infra.QA within the Sample.Infra assembly uses QA/Migrations. Contexts in the same namespace share that default location. An explicit output directory overrides the default location.
Use the project containing the context as the startup project, and keep the context namespace rooted at its assembly name. From the repository root:
dotnet ef migrations add AddUsers --context QAContext --project Sample.Infra --startup-project Sample.Infra
dotnet ef database update --context QAContext --project Sample.Infra --startup-project Sample.Infra -- "<connection-string>"
The design-time factory accepts the connection string as its first forwarded argument. Its options hooks receive a null service provider, and it does not install attribute seeding without application DI. Replace the connection placeholder with the target database connection.
Seeding
Override NpgsqlDbContextOptionsAttribute.SeedAsync to seed a DI-configured context. See the custom options example.
| Initialization path | Attribute seeding |
|---|---|
| Eligible automatic startup migration | Runs under EF's migration lock, including when no migrations remain; a later eligible startup can retry a failed seed |
Explicit Migrate / MigrateAsync |
Runs through EF callbacks for DI-configured contexts |
Explicit EnsureCreated / EnsureCreatedAsync, including prototype creation |
Runs through EF creation callbacks; does not provide the migration path's concurrency guarantee |
| Design-time options without an application service provider | Does not install attribute seeding |
| Test helpers | Seeds when the helper performs a migration or creation operation; shared migration helpers can skip already-migrated context types |
Custom EF callbacks run before attribute seeding. Synchronous initialization waits for SeedAsync. Each path prefers its matching custom callback and falls back to the other callback when only one is configured.
Reapplying context options preserves custom callbacks and replaces DRN wrappers with callbacks bound to the latest supplied provider. It does not duplicate attribute seeding. Reconfiguration without a provider restores only custom callbacks.
Seeds must tolerate repeated or partially completed runs and use the same scoped context for database work. The attribute hook has no cancellation-token parameter. Cancellation is checked before each attribute but cannot interrupt an attribute already running.
See EF Core Data Seeding Guidance for the underlying EF callbacks.
Identity Naming Conventions
DrnContextIdentity<TContext, TUser> supports ASP.NET Core Identity users derived from IdentityUser. It inherits registration and provider defaults, applies the model conventions, and renames these tables:
| Original Table | DRN Table Name |
|---|---|
AspNetUsers |
users |
AspNetUserLogins |
user_logins |
AspNetUserClaims |
user_claims |
AspNetRoles |
roles |
AspNetUserRoles |
user_roles |
AspNetRoleClaims |
role_claims |
AspNetUserTokens |
user_tokens |
It requires the same two constructors as DrnContext. Unlike DrnContext, it does not inherit DrnContextPerformanceDefaults.
SourceKnownRepository
SourceKnownRepository<TContext, TEntity> implements ISourceKnownRepository<TEntity>. The context must implement IDrnContext, and the entity must derive from AggregateRoot.
Public CRUD, query, and pagination methods are virtual; protected pagination overloads are not.
IEntityUtils
The repository constructor takes IEntityUtils from DRN.Framework.Utils:
| Member | Purpose |
|---|---|
Id |
Numeric identity generation and parsing |
EntityId |
Guid and SourceKnownEntityId conversion, including ToSecure and ToPlain |
Cancellation |
Root cancellation and child scopes |
Pagination |
Pagination helpers |
DateTime, UtcNow |
Entity date operations and the captured UTC time |
ScopedLog |
Operation timing and diagnostics |
Repository Cancellation
Repository cancellation scope is configured via Settings.ScopeKey:
- When
Settings.ScopeKeyisnull(default), repository cancellation usesUtils.Cancellation.Root.CancelChanges()cancelsRoot, affecting all scope-wide operations. - When
Settings.ScopeKeyis set to aCancellationScopeKey, operations use that child scope:CancellationTokenreturns the child scope token.CancelWhen(token)links a lifetime token to the repository group.CancelChanges()cancels only repositories sharing that scope key.
repository.Settings.ScopeKey = CancellationScopeKey.For<UserRepository>("shared-writes");
// For one operation, use this token in the custom EF query.
using var operationSource = CancellationTokenSource.CreateLinkedTokenSource(
repository.CancellationToken, operationToken);
Names are optional, case-sensitive developer-defined constants limited to 128 characters. Use one only when a type owns multiple intentional groups.
Never derive keys from request data, user input, instance IDs, or operation IDs. For operation-only cancellation, link the operation token locally instead of adding it to the repository group. See Scoped Cancellation for key and lifetime rules.
RepositorySettings
Configure repository behavior through Settings:
| Property | Default | Effect |
|---|---|---|
AsNoTracking |
false |
Disables tracking for retrieval queries |
IgnoreAutoIncludes |
false |
Suppresses model-configured automatic includes for retrieval queries |
ScopeKey |
null |
Selects a child cancellation scope; null uses the root |
Filters |
Empty | Read-only dictionary of named predicates; modify with AddFilter, RemoveFilter, and ClearFilters |
The tenant and soft-delete predicates below assume your entity defines TenantId and nullable DeletedAt. Neither property is supplied by AggregateRoot:
// Read-only queries
repository.Settings.AsNoTracking = true;
repository.Settings.IgnoreAutoIncludes = true;
// Applies to repository reads and ID-based bulk deletes
repository.Settings.AddFilter("TenantId",
entity => entity.TenantId == currentTenantId);
// Soft delete filter
repository.Settings.AddFilter("NotDeleted",
entity => entity.DeletedAt == null);
// Remove a filter
repository.Settings.RemoveFilter("TenantId");
// Clear all filters
repository.Settings.ClearFilters();
Pagination
Use PaginateAsync for cursor-based pagination:
// Basic pagination
var request = PaginationRequest.DefaultWith(size: 20);
var result = await repository.PaginateAsync(request);
// Access results
foreach (var user in result.Items)
{
Console.WriteLine(user.Username);
}
// Navigate to next page
if (result.Info.HasNext)
{
var nextRequest = result.Info.RequestNextPage();
var nextPage = await repository.PaginateAsync(nextRequest);
}
// Navigate to previous page
if (result.Info.HasPrevious)
{
var prevRequest = result.Info.RequestPreviousPage();
var prevPage = await repository.PaginateAsync(prevRequest);
}
// Filter by creation date
var filter = EntityCreatedFilter.After(DateTimeOffset.UtcNow.AddDays(-7));
var recentUsers = await repository.PaginateAsync(request, filter);
// Map to DTOs while preserving pagination
var dtoResult = result.ToModel(user => new UserDto(user)
{
Username = user.Username
});
Query Composition
Use EntitiesWithAppliedSettings() for custom retrieval queries, as in the advanced quickstart. The protected pagination overload accepts a composed query. This example assumes your User entity also defines a string Role property:
public class UserRepository(AppContext context, IEntityUtils utils)
: SourceKnownRepository<AppContext, User>(context, utils)
{
public async Task<PaginationResultModel<User>> GetUsersByRoleAsync(
string role,
PaginationRequest request)
{
var query = EntitiesWithAppliedSettings()
.Where(u => u.Role == role);
return await PaginateAsync(query, request);
}
}
Customizing Retrieval Queries
Override EntitiesWithAppliedSettings to customize repository retrieval queries, such as adding explicit navigation loading.
Use Settings.Filters for constraints that must also apply to ID-based bulk deletes. An EntitiesWithAppliedSettings override customizes retrieval queries only.
This override assumes your model defines User.Posts, Post.Comments, and User.Profile. Add it to the repository from the advanced quickstart:
protected override IQueryable<User> EntitiesWithAppliedSettings(string? caller = null)
{
return base.EntitiesWithAppliedSettings(caller)
.Include(u => u.Posts)
.ThenInclude(p => p.Comments)
.Include(u => u.Profile);
}
Reads and Writes
| Operation | Behavior |
|---|---|
AnyAsync, AllAsync, CountAsync |
Evaluate retrieval queries with repository settings |
GetAsync(id) |
Returns one entity or throws NotFoundException |
GetOrDefaultAsync(id) |
Returns null when no matching entity exists; validates the ID by default |
GetAsync(ids) |
Returns matching entities; an empty input returns an empty array |
GetAllAsync() |
Loads every matching entity; use only for bounded result sets |
Add, Remove |
Change tracking state; require a later save |
CreateAsync(entities), DeleteAsync(entities) |
Add or remove tracked entities, then save |
DeleteAsync(ids) |
Executes a database delete immediately with Settings.Filters, without fetching entities or invoking save interceptors |
SaveChangesAsync() |
Saves all pending changes in the context, including changes outside this repository |
Filters do not authorize tracked writes. Validate ownership and access before adding, modifying, or removing entities. ID-based bulk deletes bypass retrieval overrides and tracked lifecycle hooks. CancelChanges() cancels operations; it does not clear the change tracker or undo completed writes.
Validation
The repository validates IDs against the expected entity type and application partition before query execution by default:
// Throws ValidationException for an invalid ID or a mismatched entity type/partition
var userId = repository.GetEntityId(someGuid, validate: true);
var user = await repository.GetAsync(userId);
// Validate multiple IDs
var userIds = repository.GetEntityIds(guidList, validate: true);
var users = await repository.GetAsync(userIds);
Secure ↔ Plain Conversion
Repositories expose idempotent conversion between encrypted and plaintext entity IDs:
var secureId = repository.ToSecure(entityId);
var plainId = repository.ToPlain(secureId);
Entity Configuration
The framework supports both attribute-based and Fluent API configuration.
Attribute-Based Configuration (Preferred)
Use attributes for constraints and indexes that they can express. This is an alternative definition of the beginner User entity:
[EntityType<MyApp>(1)]
[Table("users")]
[Index(nameof(Username), IsUnique = true)]
public class User : AggregateRoot
{
[MaxLength(100)]
[Required]
public string Username { get; set; } = string.Empty;
[MaxLength(255)]
public string Email { get; set; } = string.Empty;
public bool IsActive { get; set; } = true;
}
Fluent API Configuration (Complex Cases)
Use IEntityTypeConfiguration<T> for relationships, owned types, or conditional mapping. This fragment assumes an application model with Posts, Author, AuthorId, TenantId, and an owned Address with Street and City properties:
public class UserConfiguration : IEntityTypeConfiguration<User>
{
public void Configure(EntityTypeBuilder<User> builder)
{
// Complex relationships
builder.HasMany(u => u.Posts)
.WithOne(p => p.Author)
.HasForeignKey(p => p.AuthorId)
.OnDelete(DeleteBehavior.Cascade);
// Composite indexes
builder.HasIndex(u => new { u.TenantId, u.Username })
.IsUnique();
// Owned entities
builder.OwnsOne(u => u.Address, address =>
{
address.Property(a => a.Street).HasMaxLength(200);
address.Property(a => a.City).HasMaxLength(100);
});
}
}
Auto-Discovery
Configurations are automatically discovered and applied if they:
- Reside in the same assembly as the context
- Share the context's namespace (or a sub-namespace)
Sample.Infra/
├── QAContext.cs # Namespace: Sample.Infra
├── Configurations/
│ ├── UserConfiguration.cs # Namespace: Sample.Infra.Configurations ✓
│ └── QuestionConfiguration.cs # Namespace: Sample.Infra.Configurations ✓
JSON Models
Source-Known entities implementing IEntityWithModel<TModel> have their public Model property mapped as an owned JSON object with OwnsOne(...).ToJson(). PostgreSQL stores it as jsonb:
[EntityType<MyApp>(2)]
public class Question : AggregateRoot<QuestionModel>
{
public Question() => Model = new QuestionModel();
}
public class QuestionModel
{
public string Title { get; set; } = string.Empty;
public string Body { get; set; } = string.Empty;
public List<string> Tags { get; set; } = [];
}
Attributes & Configuration
DrnContextServiceRegistrationAttribute
Registers discovered contexts when their assembly is scanned and participates in startup validation. It is inherited from the context base class; do not reapply it.
DrnContextDefaultsAttribute
| Setting | Default |
|---|---|
| Query splitting | SplitQuery |
| Migrations assembly | Context's assembly |
| Migration history | __entity_migrations.{context_name}_history, with the context name in snake_case |
| PostgreSQL compatibility version | 18.6 |
| Parameter logging | Disabled |
| JSON options | JsonConventions.DefaultOptions |
| Application name | Preserves an existing value; otherwise {ApplicationName}_{ContextName} with DI or {ContextName} without it |
| Table and column naming | snake_case |
| EF logging | Warning level and above to IScopedLog; console output when no scoped log is available |
DrnContextPerformanceDefaultsAttribute
DrnContext inherits these Npgsql connection settings. To override them, derive a custom attribute from NpgsqlPerformanceSettingsAttribute.
| Constructor parameter | Default |
|---|---|
maxAutoPrepare |
200 |
autoPrepareMinUsages |
5 |
minPoolSize |
1 |
maxPoolSize |
15 |
readBufferSize |
8192 bytes |
writeBufferSize |
8192 bytes |
commandTimeout |
30 seconds |
NpgsqlDbContextOptionsAttribute
Derive an attribute to configure provider options, data-source options, general EF options, or seeding. Framework-defined attributes run before custom attributes. Options hooks must handle a null service provider at design time.
using Npgsql;
using Npgsql.EntityFrameworkCore.PostgreSQL.Infrastructure;
public class MyContextOptions : NpgsqlDbContextOptionsAttribute
{
public override void ConfigureNpgsqlOptions<TContext>(
NpgsqlDbContextOptionsBuilder builder,
IServiceProvider? serviceProvider)
{
builder.CommandTimeout(60);
builder.UseQuerySplittingBehavior(QuerySplittingBehavior.SingleQuery);
}
public override void ConfigureNpgsqlDataSource<TContext>(
NpgsqlDataSourceBuilder builder,
IServiceProvider? serviceProvider)
{
builder.ConnectionStringBuilder.ApplicationName = typeof(TContext).Name;
}
public override void ConfigureDbContextOptions<TContext>(
DbContextOptionsBuilder builder,
IServiceProvider? serviceProvider)
{
base.ConfigureDbContextOptions<TContext>(builder, serviceProvider);
// The base method configures the EF warning used by prototype mode.
}
public override async Task SeedAsync(
IServiceProvider serviceProvider,
IAppSettings appSettings)
{
var context = serviceProvider.GetRequiredService<MyDbContext>();
if (!await context.Users.AnyAsync(user => user.Username == "example-user"))
{
context.Users.Add(new User { Username = "example-user" });
await context.SaveChangesAsync();
}
}
}
Apply the attribute to a context with both required constructors:
[MyContextOptions(UsePrototypeMode = true)]
public class MyDbContext : DrnContext<MyDbContext>
{
public MyDbContext(DbContextOptions<MyDbContext> options) : base(options) { }
public MyDbContext() : base(null) { }
public DbSet<User> Users => Set<User>();
}
The seed inserts an application record, not an authenticated account. See Seeding for callback ordering, retries, concurrency, and cancellation limits.
NpgsqlPerformanceSettingsAttribute
Create a custom performance attribute and apply [HighThroughputSettings] to your context. These example values are overrides, not workload recommendations:
public class HighThroughputSettings : NpgsqlPerformanceSettingsAttribute
{
public HighThroughputSettings() : base(
maxAutoPrepare: 500,
autoPrepareMinUsages: 3,
minPoolSize: 10,
maxPoolSize: 100,
readBufferSize: 16384,
writeBufferSize: 16384,
commandTimeout: 60)
{
}
}
Prototype Mode
Prototype mode creates or recreates a Development database from the current model during startup validation. It avoids temporary migrations while prototyping.
Prototype mode deletes the configured database. Use it only with a disposable, isolated Development database. Staging auto-migration never enables prototype recreation.
Conditions and Configuration
All conditions must hold:
- The application runs in Development.
DrnDevelopmentSettings:AutoMigrateDevelopment = true.- Pending model changes exist.
- A context options attribute has
UsePrototypeMode = true. DrnDevelopmentSettings:Prototype = true.- No migrations have been applied, or
UsePrototypeModeWhenMigrationExists = truepermits recreation despite applied migrations.
Apply [MyContextOptions(UsePrototypeMode = true)] as shown above, then configure appsettings.Development.json:
{
"DrnDevelopmentSettings": {
"AutoMigrateDevelopment": true,
"Prototype": true
}
}
If any condition is false, this startup path does not recreate the database. LaunchExternalDependencies provides container isolation but is not a recreation condition. Isolate prototyping to one context and a disposable database; deletion affects the whole configured database.
For an existing database with tables, the workflow calls EnsureDeletedAsync before EnsureCreatedAsync. A missing or empty database is created without that deletion step. Creation invokes the EF seed callback.
Applied migrations are read from the target database independently of the local migration assembly. If migration files or the model snapshot are missing while the database still contains migration history, the database is treated as migrated and prototype recreation remains blocked unless UsePrototypeModeWhenMigrationExists = true.
Prototype Mode with Applied Migrations
Declared migrations that have not been applied do not block prototyping. To override the applied-migration guard, replace the attribute on MyDbContext with:
[MyContextOptions(
UsePrototypeMode = true,
UsePrototypeModeWhenMigrationExists = true
)]
// Keep the MyDbContext declaration and constructors shown above.
Connection String Resolution by Environment
Connection strings vary by environment. The startup schema behavior below occurs when the framework startup validation lifecycle runs; DRN.Framework.Hosting invokes it automatically.
| Environment | Connection string | Startup schema behavior |
|---|---|---|
| Production | Explicit ConnectionStrings:{ContextName} |
Never auto-migrates |
| Staging | Explicit ConnectionStrings:{ContextName} |
Applies pending migrations only when AutoMigrateStaging=true |
| Development | Explicit named connection string, an injected Testcontainers connection, or generation from postgres-password and DrnContext_Dev* settings |
Applies pending migrations when AutoMigrateDevelopment=true; prototype mode may recreate the database |
DrnTestContext |
Injected container connection | Migration and database-creation helpers perform only the requested operation |
Startup rejects pending model changes in every environment unless the prototype path handles them. Disabling automatic migration does not disable this validation. When automatic migration is enabled and no pending model changes exist, startup calls MigrateAsync even with zero pending migrations. See Seeding for the callback behavior.
Set Environment in base configuration, an environment variable, mounted configuration, or a command-line argument. An environment-specific settings file cannot select itself.
Non-Development (Production/Staging)
The framework calls appSettings.GetRequiredConnectionString(contextName). Supply ConnectionStrings:{ContextName} through configuration; the password below is a placeholder:
{
"ConnectionStrings": {
"QAContext": "Host=prod-db.example.com;Port=5432;Database=qa_prod;Username=qa_user;Password=<password>"
}
}
postgres-password and all DrnContext_Dev* settings are ignored in non-Development environments. Missing connection strings will throw ConfigurationException.
Staging
IAppSettings.IsStagingEnvironment is derived from Environment=Staging; it is not a separate configuration switch. AutoMigrateStaging defaults to false and enables migrations only, never prototype recreation.
Example appsettings.Staging.json, assuming Staging was selected by base configuration or an override:
{
"ConnectionStrings": {
"QAContext": "Host=staging-db;Port=5432;Database=qa_staging;Username=qa_user;Password=<password>"
},
"DrnDevelopmentSettings": {
"AutoMigrateStaging": true
}
}
AutoMigrateDevelopment defaults to true; AutoMigrateStaging defaults to false. Enable staging migration only when the deployment should apply migrations at startup. Otherwise, apply them through your deployment process.
Local Debug with LaunchExternalDependencies
In Development, call LaunchExternalDependenciesAsync with DrnDevelopmentSettings:LaunchExternalDependencies = true to start PostgreSQL through Testcontainers. The helper skips temporary applications and applications running inside DrnTestContext.
Setup: Add a Debug-only DRN.Framework.Testing package reference and keep all DRN Framework package versions aligned:
<ItemGroup Condition="'$(Configuration)' == 'Debug'">
<PackageReference Include="DRN.Framework.Testing" Version="0.10.0" />
</ItemGroup>
Implementation (see SampleProgramActions.cs):
#if DEBUG
using DRN.Framework.Hosting.DrnProgram;
using DRN.Framework.Testing.Contexts.Postgres;
using DRN.Framework.Testing.Extensions;
using DRN.Framework.Utils.Logging;
using DRN.Framework.Utils.Settings;
using Microsoft.AspNetCore.Builder;
public class SampleProgramActions : DrnProgramActions
{
public override async Task ApplicationBuilderCreatedAsync<TProgram>(
TProgram program, WebApplicationBuilder builder,
IAppSettings appSettings, IScopedLog scopedLog)
{
var launchOptions = new ExternalDependencyLaunchOptions
{
PostgresContainerSettings = new PostgresContainerSettings
{
Reuse = true, // Keep container across restarts
HostPort = 6432 // Avoid port conflicts
}
};
await builder.LaunchExternalDependenciesAsync(scopedLog, appSettings, launchOptions);
}
}
#endif
Example appsettings.Development.json, assuming Development was selected by base configuration or an override:
{
"DrnDevelopmentSettings": {
"LaunchExternalDependencies": true,
"AutoMigrateDevelopment": true,
"Prototype": true
}
}
Containers use PostgresContainerSettings rather than postgres-password or DrnContext_Dev*. The default password is "drn". The launch helper injects named connection strings into configuration. Reuse = true keeps the container running across application restarts.
Containerized Development (Docker Compose / Kubernetes)
For development with external database containers (Docker Compose, Kubernetes, Podman), use postgres-password to trigger auto-connection string generation.
Docker Compose Example:
services:
app:
build: .
environment:
- Environment=Development
- postgres-password=dev-password
- DrnContext_DevHost=postgres
- DrnDevelopmentSettings__AutoMigrateDevelopment=true
depends_on:
postgres:
condition: service_healthy
postgres:
image: postgres:18.6-alpine3.24@sha256:d3e1620b530c944afa6e887d22eb899824da68e19c52024bf98f5220c88a65b2
environment:
POSTGRES_USER: drn
POSTGRES_PASSWORD: dev-password
POSTGRES_DB: drn
PGDATA: /data/postgres
ports:
- "5432:5432"
volumes:
- postgres-data:/data/postgres
healthcheck:
test: ["CMD-SHELL", "pg_isready -U drn -d drn"]
interval: 5s
timeout: 5s
retries: 10
volumes:
postgres-data:
Kubernetes ConfigMap/Secret:
Inject these values as environment variables from the ConfigMap and Secret.
apiVersion: v1
kind: ConfigMap
metadata:
name: app-config
data:
Environment: "Development"
DrnContext_DevHost: "postgres-service"
DrnDevelopmentSettings__AutoMigrateDevelopment: "true"
---
apiVersion: v1
kind: Secret
metadata:
name: app-secrets
stringData:
postgres-password: "dev-password"
An explicit ConnectionStrings:{ContextName} value takes precedence in Development. Otherwise, postgres-password enables generation from the DrnContext_Dev* settings. Missing both a named connection and a password causes ConfigurationException.
DrnTestContext (Integration Tests)
For integration tests, ContainerContext manages Postgres containers automatically. This method fragment uses the repository's Sample.Infra module; add your database assertions after resolving the context:
Use DRN.Framework.Testing.Contexts, DRN.Framework.Testing.DataAttributes, Sample.Infra, Sample.Infra.QA, and Xunit imports in the test file.
[Theory]
[DataInline]
public async Task Integration_Test(DrnTestContext context)
{
context.ServiceCollection.AddSampleInfraServices();
await context.ContainerContext.Postgres.ApplyMigrationsAsync();
var dbContext = context.GetRequiredService<QAContext>();
// Add assertions for the operation under test.
}
Container connections are injected automatically. These tests use PostgresContainerSettings, not DrnContext_Dev* settings. See Seeding for helper callbacks and the shared migration skip behavior.
Configuration Settings Reference
Generated Development Connection Settings
These settings provide the Development fallback when no explicit named connection string or Testcontainers connection is available.
| Setting | Default | Purpose |
|---|---|---|
DrnContext_DevHost |
drn |
Database host |
DrnContext_DevPort |
5432 |
Database port |
DrnContext_DevUsername |
drn |
Database username |
DrnContext_DevDatabase |
drn |
Database name |
postgres-password |
(none) | Enables generated connection strings |
Migration and Prototype Settings
These keys are under DrnDevelopmentSettings:
| Setting | Default | Purpose |
|---|---|---|
AutoMigrateDevelopment |
true |
Auto-migrate in Development |
AutoMigrateStaging |
false |
Auto-migrate in Staging; migrations only, no prototype recreation |
Prototype |
false |
Enables Development-only database recreation on model changes |
LaunchExternalDependencies |
false |
Launches local PostgreSQL Testcontainers |
Testcontainers Defaults
When using LaunchExternalDependencies or ContainerContext, these PostgreSQL values are used:
| Property | Default | Notes |
|---|---|---|
DefaultPassword |
"drn" |
Container password |
DefaultImage |
"postgres" |
Docker image |
DefaultVersion |
"18.6-alpine3.24" |
Image tag |
DefaultDigest |
"sha256:d3e1620b530c944afa6e887d22eb899824da68e19c52024bf98f5220c88a65b2" |
Immutable image index digest |
Database |
"drn" |
Container database |
Username |
"drn" |
Container user |
The default image/tag pair is resolved with DefaultDigest. Custom image tags remain tag-based unless Digest is set explicitly.
The complete prototype conditions apply regardless of how the Development connection is supplied.
DrnDevelopmentSettings Class
public class DrnDevelopmentSettings
{
public bool SkipValidation { get; init; }
public bool TemporaryApplication { get; init; }
public bool LaunchExternalDependencies { get; init; }
public bool AutoMigrateDevelopment { get; init; } = true;
public bool AutoMigrateStaging { get; init; } = false;
public bool Prototype { get; init; }
public bool BreakForUserUnhandledException { get; init; }
}
Global Usings
Common imports for the entity, repository, configuration, and controller examples:
global using System.ComponentModel.DataAnnotations;
global using System.ComponentModel.DataAnnotations.Schema;
global using DRN.Framework.EntityFramework.Attributes;
global using DRN.Framework.EntityFramework.Context;
global using DRN.Framework.EntityFramework.Domain;
global using DRN.Framework.SharedKernel.Cancellation;
global using DRN.Framework.SharedKernel.Domain;
global using DRN.Framework.SharedKernel.Domain.Pagination;
global using DRN.Framework.SharedKernel.Domain.Repository;
global using DRN.Framework.Utils.DependencyInjection;
global using DRN.Framework.Utils.DependencyInjection.Attributes;
global using DRN.Framework.Utils.Entity;
global using DRN.Framework.Utils.Settings;
global using Microsoft.AspNetCore.Mvc;
global using Microsoft.EntityFrameworkCore;
global using Microsoft.EntityFrameworkCore.Metadata.Builders;
global using Microsoft.Extensions.DependencyInjection;
Related Packages
- DRN.Framework.SharedKernel - Domain primitives and exceptions
- DRN.Framework.Utils - Configuration and DI utilities
- DRN.Framework.Hosting - Web application hosting
- DRN.Framework.Testing - Testing utilities
For persistence examples, see Sample.Infra; for hosting setup, see Sample.Hosted.
Documented with the assistance of DiSC OS
Semper Progressivus: Always Progressive
Commit Info
Author: Duran Serkan KILIÇ
Date: 2026-09-07 19:35:53 +0300
Hash: 98ae3dc2cf2bd27a854ca5f0cd436466fcca509c
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net10.0 is compatible. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
-
net10.0
- DRN.Framework.Utils (>= 0.10.0-preview001)
- EFCore.NamingConventions (>= 10.0.1)
- Microsoft.AspNetCore.Identity.EntityFrameworkCore (>= 10.0.11)
- Microsoft.EntityFrameworkCore.Design (>= 10.0.11)
- Microsoft.EntityFrameworkCore.Tools (>= 10.0.11)
- Npgsql.DependencyInjection (>= 10.0.3)
- Npgsql.EntityFrameworkCore.PostgreSQL (>= 10.0.3)
NuGet packages (1)
Showing the top 1 NuGet packages that depend on DRN.Framework.EntityFramework:
| Package | Downloads |
|---|---|
|
DRN.Framework.Testing
DRN.Framework.Testing package encapsulates testing dependencies and provides practical, effective helpers such as resourceful data attributes and test context. This package enables a new encouraging testing technique called as DTT(Duran's Testing Technique). With DTT, any developer can write clean and hassle-free unit and integration tests without complexity. ## Commit Info Author: Duran Serkan KILIÇ Date: 2026-09-07 19:35:53 +0300 Hash: 98ae3dc2cf2bd27a854ca5f0cd436466fcca509c |
GitHub repositories
This package is not used by any popular GitHub repositories.
| Version | Downloads | Last Updated |
|---|---|---|
| 0.10.0-preview001 | 49 | 9/7/2026 |
| 0.9.9-preview004 | 87 | 8/27/2026 |
| 0.9.9-preview003 | 86 | 8/26/2026 |
| 0.9.9-preview002 | 90 | 8/20/2026 |
| 0.9.9-preview001 | 98 | 8/16/2026 |
| 0.9.8 | 104 | 8/12/2026 |
| 0.9.8-preview004 | 92 | 8/12/2026 |
| 0.9.8-preview003 | 94 | 8/9/2026 |
| 0.9.8-preview002 | 90 | 8/8/2026 |
| 0.9.8-preview001 | 102 | 8/8/2026 |
| 0.9.7 | 120 | 7/29/2026 |
| 0.9.6 | 121 | 7/15/2026 |
| 0.9.6-preview004 | 127 | 7/7/2026 |
| 0.9.6-preview003 | 223 | 7/1/2026 |
| 0.9.6-preview002 | 132 | 6/29/2026 |
| 0.9.6-preview001 | 121 | 6/28/2026 |
| 0.9.5 | 137 | 6/14/2026 |
| 0.9.5-preview011 | 117 | 6/14/2026 |
| 0.9.5-preview010 | 122 | 6/14/2026 |
| 0.9.5-preview009 | 121 | 6/14/2026 |
## Version 0.10.0
### Breaking Changes
* **Partition-Scoped Entity Type Validation**: Startup validation enforces partition cardinality per `DbContext`.
* Treats `(EntityType, AppId)` as the uniqueness key, aligning with SharedKernel analyzers.
* Single `DbContext` instances cannot combine multiple production `AppId` partitions.
* Configured `NexusAppSettings.AppId` must match the context's partition or a recognized registered host partition.
* *Migration*: Split multi-partition DbContexts into dedicated single-partition contexts and align `NexusAppSettings.AppId` with the registered partition.
### Changed
* **Repository Overrides**: Made public CRUD, query, and pagination methods virtual.
* **PostgreSQL Defaults**: Updated Npgsql context defaults to PostgreSQL 18.6, matching digest-pinned container defaults.
* **Compiled Delegate ID Dispatch**: Replaced dynamic reflection in `DrnSaveChangesInterceptor` and `SourceKnownIdValueGenerator` with compiled delegate dispatch via `ISourceKnownIdUtils.Next(SourceKnownEntity)` and startup `SourceKnownIdUtils.Warmup`, eliminating allocations on entity insertion hot paths.
### Bug Fixes
* **NuGet Release Notes**: Package metadata includes only the latest version section, excluding historical releases and the documentation footer. Packing rejects missing version sections and release notes over 35,000 characters; the bundled Markdown retains the full history.
* **Repeatable Seeding Configuration**: Reapplying context options preserves custom EF callbacks without nesting DRN seed wrappers, so each seeding operation invokes attribute seeding once using the latest supplied provider. Reconfiguration without a provider restores only custom callbacks.
* **Private Container Entity Discovery**: Runtime model and assembly discovery now exclude entities nested at any depth inside private types, matching analyzer eligibility and preventing missing-metadata startup failures for private helpers.
* **Mapped Entity Inheritance**: Configure Source-Known keys and shared properties on EF hierarchy roots instead of derived types for TPH, TPT, and TPC. Model validation now excludes abstract bases and nested private helper entities, matching assembly discovery, while retaining concrete entity metadata validation and inherited ID generation.
* **Migration-Locked Seeding and Recovery**: DI-configured contexts invoke attribute `SeedAsync` through EF initialization callbacks. Automatic migration enters EF's migration lock even with no pending migrations, allowing later startups to retry failed seeds. Explicit DI migration/database-creation operations also seed; synchronous operations wait for the asynchronous hook. Custom EF callbacks are preserved, and design-time contexts without DI remain unchanged. Seed implementations must remain idempotent.
* **Startup Pending Model Changes Validation**: `PostStartupValidationAsync` verifies pending EF Core model changes regardless of whether auto-migration is enabled, failing fast on unmigrated schema drift.
* **Prototype Migration-History Guard**: Reads applied migrations directly from the target database, safely handling missing databases (`InvalidCatalogName`), preventing unmigrated databases from being dropped during prototype checks.
* **Design-Time Data-Source Hooks**: `DbContextExtensions.CreateDbContext` invokes `ConfigureNpgsqlDataSource` with safe null fallback when running outside DI.
* **Visible DbContext Discovery**: `AddDbContextsWithConventions` filters domain scanning to public/visible `DbContext` types (`IsVisible: true`).
---
Documented with the assistance of [DiSC OS](https://github.com/duranserkan/DRN-Project/blob/develop/.agent/rules/DiSCOS.md)
---
**Semper Progressivus: Always Progressive**