Entity Framework Core¶
Cargonerds persists data with Entity Framework Core 10 on SQL Server, following the
ABP Framework conventions.
The solution defines three DbContext types across two physical databases, wires module
repositories together with [ReplaceDbContext], and layers a second-level query cache and a
context pool over the large logistics ("Hub") database.
This page explains the persistence stack concept-by-concept and points at the concrete classes, file paths, configuration keys and gotchas that implement it.
DbContexts at a glance¶
| DbContext | Project | Connection string | Role |
|---|---|---|---|
CargonerdsDbContext |
src/Cargonerds.EntityFrameworkCore |
Default (ConnectionStringNames.SparkDb) |
The ABP host / "Spark" database. Aggregates almost every ABP framework module (Identity Pro, OpenIddict Pro, SaaS, Permission/Setting/Feature management, audit logging, background jobs, BLOB storing, CMS Kit + CMS Kit Pro, Chat, file & text-template management, language management, GDPR) plus app entities (ClientSetting, ApiKey, OrganizationUnitOrgCode, OrganizationUnitOwnerUser, notifications, Excel-export metadata). |
HubDbContext |
modules/hub/src/Hub.EntityFrameworkCore |
Hub (ConnectionStringNames.HubDb) |
The large freight/logistics domain: shipments, consols, containers, organizations, invoices/revenue, orders, tracking, customs, pricing, and master-data/code tables (~200 DbSets). |
PricingDbContext |
modules/pricing/src/Pricing.EntityFrameworkCore |
Pricing (PricingDbProperties.ConnectionStringName) |
A thin context whose OnModelCreating only calls builder.ConfigurePricing() and declares no DbSets. Pricing entities are physically mapped into HubDbContext instead. |
Connection-string names are centralised in
src/Cargonerds.Domain.Shared/Configuration/ConnectionStringNames.cs:
public static class ConnectionStringNames
{
public const string HubServiceBus = "hubServiceBus";
public const string HubDb = "Hub";
public const string SparkDb = "Default";
public const string BlobStorage = nameof(BlobStorage);
}
The Pricing module keeps its own constant in
modules/pricing/src/Pricing.Domain/PricingDbProperties.cs (ConnectionStringName = "Pricing",
DbTablePrefix = "Pricing", DbSchema = null).
Only Default is checked in
The single connection string present in committed appsettings.json (DbMigrator and
HttpApi.Host) is Default, e.g. in src/Cargonerds.DbMigrator/appsettings.json:
"ConnectionStrings": {
"Default": "Server=(LocalDb)\\MSSQLLocalDB;Database=CargonerdsAppDb;Trusted_Connection=True;TrustServerCertificate=true"
}
The Hub (and nominal Pricing) connection strings are injected at runtime by .NET Aspire
or the environment — see .NET Aspire Integration and
appsettings reference.
flowchart LR
subgraph App["Host application (HttpApi.Host)"]
C[CargonerdsDbContext]
H[HubDbContext]
P[PricingDbContext]
end
C -- "[ReplaceDbContext]" --> ID[IIdentityProDbContext]
C -- "[ReplaceDbContext]" --> SA[ISaasDbContext]
C -- "[ReplaceDbContext]" --> PM[IPermissionManagementDbContext]
C -- "Default" --> DB1[(SQL Server: Default / Spark DB)]
H -- "Hub" --> DB2[(SQL Server: Hub DB)]
P -. "model folded into Hub" .-> H
H -- pooled 256 + 2nd-level cache --> DB2
Why two databases¶
CargonerdsDbContext is the ABP host context. It uses
[ReplaceDbContext] so that
the Identity, SaaS and permission-management module
repositories resolve to this context (against the Default database) instead of each module's
own context. This makes cross-module JOINs over repositories possible. The Hub database is a
separate, pre-existing logistics schema accessed through HubDbContext.
Multi-tenancy / SaaS is disabled
MultiTenancyConsts.IsEnabled = false
(src/Cargonerds.Domain.Shared/MultiTenancy/MultiTenancyConsts.cs). The SaaS module is still
installed and CargonerdsDbContext still replaces ISaasDbContext, but the per-tenant loop in
the migration service is effectively dead code. See
Multi-tenancy for the ABP
concept.
CargonerdsDbContext (host / Default DB)¶
Declared in src/Cargonerds.EntityFrameworkCore/EntityFrameworkCore/CargonerdsDbContext.cs:
[ReplaceDbContext(typeof(IIdentityProDbContext))]
[ReplaceDbContext(typeof(ISaasDbContext))]
[ReplaceDbContext(typeof(IPermissionManagementDbContext))]
[ConnectionStringName(ConnectionStringNames.SparkDb)] // "Default"
public class CargonerdsDbContext
: AbpDbContext<CargonerdsDbContext>,
ISaasDbContext,
IIdentityProDbContext,
IPermissionManagementDbContext
{
public DbSet<ClientSetting> ClientSettings { get; set; }
public DbSet<OrganizationUnitOrgCode> OrganizationUnitOrgCodes { get; set; }
public DbSet<ApiKey> ApiKeys { get; set; }
// ... Identity, SaaS, Notifications, Permission DbSets ...
}
Model creation¶
OnModelCreating calls every ABP module's builder.ConfigureXxx() extension
(ConfigurePermissionManagement, ConfigureSettingManagement, ConfigureBackgroundJobs,
ConfigureAuditLogging, ConfigureFeatureManagement, ConfigureIdentityPro,
ConfigureOpenIddictPro, ConfigureLanguageManagement, ConfigureFileManagement,
ConfigureSaas, ConfigureChat, ConfigureTextTemplateManagement, ConfigureGdpr,
ConfigureCmsKit, ConfigureCmsKitPro, ConfigureBlobStoring) and builder.ConfigurePricing().
ConfigureHub() is deliberately commented out
Inside OnModelCreating the line //builder.ConfigureHub(); is present but disabled. Hub
entities are intentionally not mapped into CargonerdsDbContext — they live only in
HubDbContext. This is why the Hub schema has no EF migrations (see
Database Migrations).
App tables are mapped explicitly with ToTable + ABP's ConfigureByConvention() (which applies
audit/soft-delete/multi-tenant base columns): AppClientSettings, AppApiKeys, AppOuOrgCodes,
and AppOuOwnerUser. Notification entities are mapped
via ApplyConfiguration(new ...TypeConfiguration()).
A SQL Server stored computed column is added to IdentityUser for country filtering:
builder.Entity<IdentityUser>(b =>
{
b.Property<string>(UserConsts.ComputedColumns.CountryIso)
.HasComputedColumnSql("LTRIM(RTRIM(JSON_VALUE(ExtraProperties, '$.country')))", stored: true);
b.HasIndex(UserConsts.ComputedColumns.CountryIso);
});
To make this filter apply uniformly to listing, counting and Excel/CSV export, ABP's
IIdentityUserRepository is replaced with CargonerdsIdentityUserRepository
(registered as ServiceDescriptor.Transient in the EF module).
Module registration and provider config¶
CargonerdsEntityFrameworkCoreModule (same project) wires up the DbContext, repositories and the
SQL Server provider. It [DependsOn] every Abp*EntityFrameworkCore module plus
PricingEntityFrameworkCoreModule and HubEntityFrameworkCoreModule, so all three contexts are
available in the host:
context.Services.AddAbpDbContext<CargonerdsDbContext>(options =>
{
// includeAllEntities -> default repositories for every entity, not just aggregate roots
options.AddDefaultRepositories(includeAllEntities: true);
options.AddRepository<ApiKey, EfCoreApiKeyRepository>();
});
// Country filter applies on the shared query level (list/count/export)
context.Services.Replace(
ServiceDescriptor.Transient<IIdentityUserRepository, CargonerdsIdentityUserRepository>()
);
Configure<AbpDbContextOptions>(options =>
{
options.Configure(ctx =>
{
ctx.UseSqlServer(b => b.UseParameterizedCollectionMode(ParameterTranslationMode.Parameter));
ctx.DbContextOptions.AddInterceptors(new ArithAbortConnectionInterceptor());
});
});
Provider + interceptors must share one Configure action
The provider and the interceptor are registered inside a single options.Configure(...)
call on purpose. An inline comment in the module explains that a separate Configure call
after UseSqlServer overwrites the SQL Server provider configuration and yields a
"No database provider configured" error. The Cargonerds context also opts into the .NET 10 /
EF 10 toggle UseParameterizedCollectionMode(ParameterTranslationMode.Parameter) — this is
not set on the Hub context.
HubDbContext (Hub DB)¶
modules/hub/src/Hub.EntityFrameworkCore/EntityFrameworkCore/HubDbContext.cs is the workhorse of
the logistics domain. It is [ConnectionStringName(ConnectionStringNames.HubDb)] and implements
IHubDbContext. The constructor injects AppConfiguration and an
IAbpLazyServiceProvider, and sets a 2-minute command timeout:
public HubDbContext(
DbContextOptions<HubDbContext> options,
IAbpLazyServiceProvider lazyServiceProvider,
AppConfiguration appConfig)
: base(options)
{
_appConfig = appConfig;
LazyServiceProvider = lazyServiceProvider;
this.Database.SetCommandTimeout(TimeSpan.FromMinutes(2));
}
Model building¶
OnModelCreating relies on custom convention helpers in HubDbContextModelCreatingExtensions:
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.ApplyConfigurationsFromTypeAssembly(GetType()).AddInheritedTypes();
modelBuilder.Owned<Dimension<TemperatureUnit>>();
modelBuilder.Owned<Dimension<WeightUnit>>();
modelBuilder.Owned<Dimension<VolumeUnit>>();
modelBuilder.Owned<Dimension<LengthUnit>>();
modelBuilder.Owned<OrganizationAddressContactRelation>();
modelBuilder.Owned<ShipmentLegLocation>();
modelBuilder.Owned<Coordinates>();
base.OnModelCreating(modelBuilder);
modelBuilder.ConfigureIdentity();
modelBuilder.ApplyMultiTypeConfigurationsFromContextAssembly(GetType());
}
| Helper | What it does |
|---|---|
ApplyConfigurationsFromTypeAssembly(GetType()) |
Applies every IEntityTypeConfiguration<T> found in the Hub EF assembly (~70 classes under EntityFrameworkCore/TypeConfigurations/{Hub,Pricing,Abp}/). |
AddInheritedTypes() |
For every root entity (no base type), reflects over the assembly and registers every non-abstract subclass — automatic TPH inheritance mapping. |
ApplyMultiTypeConfigurationsFromContextAssembly(GetType()) |
Runs each IMultiTypeConfiguration (e.g. CustomerOwnedTypesConfiguration) across every entity assignable to a base type, via a reflected generic Configure<T>. See EntityFrameworkCore/TypeConfigurations/Utils/MultiTypeConfiguration.cs. |
The Owned<> value objects above are an ABP/EF
value-object
mapping detail — see Value Objects.
TPH example — ShipmentTypeConfiguration¶
EntityFrameworkCore/TypeConfigurations/Hub/ShipmentTypeConfiguration.cs maps the ShipmentBase
hierarchy to a single Shipments table with a discriminator and alternate keys, and it declares an
existing SQL trigger and online indexes:
builder.ToTable("Shipments");
builder.HasAlternateKey(s => s.HouseBill);
builder.HasAlternateKey(s => s.CargonerdsNumber);
builder.HasDiscriminator(s => s.Discriminator); // HubDbContext.DiscriminatorColumnName = "Discriminator"
builder.ToTable(tb => tb.HasTrigger("TR_Shipments-ActualDeparture_Update"));
builder.HasIndex(s => s.CreationTime).IsCreatedOnline();
builder
.HasIndex(s => new { s.IsActive, s.State, s.TransportModeId, s.ContainerModeId, s.EstimatedArrival })
.IncludeProperties(s => new { s.ActualArrival }); // composite covering index
The presence of a hand-declared trigger and IsCreatedOnline() / IncludeProperties indexes is a
strong signal that the EF model mirrors a hand-managed schema rather than driving it.
Conventions — enums as strings¶
ConfigureConventions applies model-wide conventions:
protected override void ConfigureConventions(ModelConfigurationBuilder configurationBuilder)
{
base.ConfigureConventions(configurationBuilder);
configurationBuilder.Properties<decimal>().HavePrecision(18, 6);
configurationBuilder.Properties<Enum>().HaveConversion<string>().HaveMaxLength(128);
configurationBuilder.Properties<Type>().HaveConversion<TypeValueConverter>().HaveMaxLength(400);
}
- All
decimalcolumns use precision(18, 6). - All enums are persisted as strings in the Hub DB (max length 128).
Typeproperties are persisted asAssemblyQualifiedNameviaTypeValueConverter(ValueConverter<Type, string>, inEntityFrameworkCore/ValueConverters/TypeValueConverter.cs).EnumDescriptionConverter<T>(ValueConverters/EnumDescriptionConverter.cs) exists for enums that should map to their[Description]text instead of the member name; it throws on an unknown description.
Two different enum representations
Over HTTP, ABP's auto-API serialises enums as integers by default. In the Hub database
they are stored as strings. These are independent layers — the API serializer vs. the EF
model convention — so the same enum can be an int on the wire and a varchar in the table.
Save-time logic¶
HubDbContext overrides HandlePropertiesBeforeSave() to enforce domain rules before each save:
- Freezable entities (
IFreezable) are reverted toUnchangedunless code is inside anAllowFreezableEntityModification()scope. - Customer-owned entities (
ICustomerOwned/IOptionallyCustomerOwned): new rows are stamped withCargonerdsCustomerId = _appConfig.CargonerdsCustomerId(gated by the mutable flagsCargonerdsCustomerInjectionEnabled/AllowMasterDataCreation); attempting to save a row that belongs to a different customer throwsInvalidOperationException. - Cache invalidation:
UpdateVersionTrackers()bumpsVersionTrackerrows for modified cacheable types (see Caching).
Concurrency stamps are disabled on the Hub
UpdateConcurrencyStamp and SetConcurrencyStampIfNull are overridden to no-ops ("the
property does not exist in the hub"). There is no optimistic concurrency on Hub entities, even
though ABP normally provides it.
Sequences¶
The Hub exposes raw SQL Server sequence operations rather than EF-managed values:
NextValueForSequence(Sequence)runsSELECT @result = (NEXT VALUE FOR [...]).ResetSequence(Sequence, newStartValue)runsALTER SEQUENCE ... RESTART WITH ...and tracks a daily reset inSequenceDate.
The sequence name is read from a [Description] attribute on the Sequence enum member; a missing
attribute throws.
Second-level cache + DbContextPool (Hub only)¶
The Hub context is pooled and cached. Both are configured in
HubEntityFrameworkCoreModule.ConfigureServices:
context.Services.AddEFSecondLevelCache(options =>
{
options
.UseMemoryCacheProvider()
.ConfigureLogging(true)
.UseCacheKeyPrefix("EF_")
.UseDbCallsIfCachingProviderIsDown(TimeSpan.FromMinutes(1));
options.CacheAllQueries(CacheExpirationMode.Absolute, TimeSpan.FromMinutes(1));
});
context.Services.AddDbContextPool<HubDbContext>(
(sp, opts) =>
{
var cs = sp.GetRequiredService<IConfiguration>().GetConnectionString(ConnectionStringNames.HubDb);
opts.UseSqlServer(cs, sql =>
{
sql.EnableRetryOnFailure();
sql.CommandTimeout((int)TimeSpan.FromMinutes(3).TotalSeconds);
});
opts.AddInterceptors(
sp.GetRequiredService<SecondLevelCacheInterceptor>(),
sp.GetRequiredService<CommandLoggingInterceptor>(),
sp.GetRequiredService<FacetJoinInterceptor>(),
sp.GetRequiredService<ArithAbortConnectionInterceptor>(),
sp.GetRequiredService<RecompileCommandInterceptor>()
);
},
poolSize: 256
);
CacheAllQueries(..., 1 min)means every Hub query is cached for one minute by default via theEFCoreSecondLevelCacheInterceptorwith an in-memory provider. Invalidation depends on the customVersionTrackermechanism.AddDbContextPool<HubDbContext>(poolSize: 256)keeps a pool of 256 reusable contexts; theHubconnection string is read fromIConfigurationinside the pool delegate, withEnableRetryOnFailure()and a 3-minute command timeout.- Interceptors are also (re)registered in
HubDbContext.OnConfiguring(second-level cache, command logging, facet-join,IgnoreFrozenEntityInterceptor), andEnableSensitiveDataLogging()is on.
Cache details, the VersionTracker table and invalidation are documented in
Caching. The ABP distributed-cache concept (used elsewhere) is described under
ABP caching.
Interceptors¶
| Interceptor | File | Purpose |
|---|---|---|
ArithAbortConnectionInterceptor |
EntityFrameworkCore/ArithAbortConnectionInterceptor.cs |
Re-applies SET ARITHABORT ON on every ConnectionOpened, because sp_reset_connection from pooling clears SET options. Used by both contexts. |
RecompileCommandInterceptor |
EntityFrameworkCore/RecompileCommandInterceptor.cs |
Appends OPTION (RECOMPILE) only when the SQL contains the literal -- recompile tag (added via EF TagWith("recompile")). |
FacetJoinInterceptor |
EntityFrameworkCore/FacetJoinInterceptor.cs |
Text-replaces LEFT JOIN → INNER JOIN for commands tagged with the facet prefix. |
CommandLoggingInterceptor |
EntityFrameworkCore/CommandLogging/CommandLoggingInterceptor.cs |
Command logging (scoped via CommandLoggingScope<HubDbContext>). |
IgnoreFrozenEntityInterceptor |
EntityFrameworkCore/IgnoreFrozenEntityInterceptor.cs |
Companion to the IFreezable save-time logic. |
Interceptor footguns
RecompileCommandInterceptoris fragile with OData$apply: aTagWith("recompile")in the OData controller base previously caused aNullReferenceExceptionon groupby/aggregate widgets — see OData filtering.FacetJoinInterceptoris a textual SQL rewrite that assumes the facet builder already filtered nulls; it is brittle if the query shape changes.
Query filters — org isolation via custom IQueryFilter¶
This is the most non-obvious area of the persistence layer. The Hub does not use ABP's
IDataFilter for
organization scoping. Instead it defines its own IQueryFilter abstraction
(modules/hub/src/Hub.Domain/Filters/IQueryFilter.cs) with two application paths.
public interface IQueryFilter
{
bool IsAutoActive { get; } // true -> applied to all queries automatically
Type EntityType { get; }
LambdaExpression CreateFilterExpression();
}
public abstract class QueryFilterBase<T> : IQueryFilter<T>, IScopedDependency
{
public abstract Expression<Func<T, bool>> CreateFilterExpression();
public virtual bool IsAutoActive => true; // default: auto-applied
Type IQueryFilter.EntityType => typeof(T);
}
Path 1 — auto-applied as real EF global query filters¶
HubDbContext overrides ABP's global-query-filter extension points so that any auto-active
IQueryFilter becomes a genuine EF Core global filter:
protected override bool ShouldFilterEntity<TEntity>(IMutableEntityType entityType)
=> GetFilterProviders<TEntity>().Any() || base.ShouldFilterEntity<TEntity>(entityType);
protected override Expression<Func<TEntity, bool>>? CreateFilterExpression<TEntity>(
ModelBuilder modelBuilder, EntityTypeBuilder<TEntity> entityTypeBuilder)
{
var expression = base.CreateFilterExpression(modelBuilder, entityTypeBuilder); // soft-delete etc.
foreach (var provider in GetFilterProviders<TEntity>())
{
var typedLambda = provider.CreateFilterExpression().AdaptExpression<TEntity>();
expression = expression is null
? typedLambda
: QueryFilterExpressionHelper.CombineExpressions(expression, typedLambda);
}
return expression;
}
private IEnumerable<IQueryFilter> GetFilterProviders<TEntity>()
=> LazyServiceProvider
.GetServices<IQueryFilter>()
.Where(p => p.IsAutoActive && p.EntityType.IsAssignableFrom(typeof(TEntity)));
Providers registered this way (via [ExposeServices(typeof(IQueryFilter))]) include
SoftDeleteFilter, CustomerOwnedFilter and OptionallyCustomerOwnedFilter (in
Hub.EntityFrameworkCore/Filter/). For example:
[ExposeServices(typeof(IQueryFilter))]
public class SoftDeleteFilter : QueryFilterBase<ISoftDelete>
{
public override Expression<Func<ISoftDelete, bool>> CreateFilterExpression() => e => !e.IsDeleted;
}
[ExposeServices(typeof(IQueryFilter))]
public class CustomerOwnedFilter(AppConfiguration appConfig) : QueryFilterBase<CustomerOwnedGuidEntity>
{
public override Expression<Func<CustomerOwnedGuidEntity, bool>> CreateFilterExpression() =>
e => e.CargonerdsCustomerId == appConfig.CargonerdsCustomerId;
}
Note the auto path matches by IsAssignableFrom, so it covers inheritance.
Path 2 — manually applied via FilterAll¶
Org-scoping filters that should be applied explicitly set IsAutoActive => false and are invoked
through IQueryable<T>.FilterAll(serviceProvider)
(Hub.Domain/Extensions/QueryableExtensions.cs). HubDbContext.FilteredSet<T>() is the convenient
entry point:
public IQueryable<T> FilteredSet<T>() where T : class => Set<T>().FilterAll(LazyServiceProvider);
// QueryableExtensions
public static IQueryable<T> FilterAll<T>(this IQueryable<T> queryable, IServiceProvider serviceProvider)
where T : class
{
var filters = serviceProvider
.GetServices<IQueryFilter>()
.Where(f => !f.IsAutoActive && f.EntityType == typeof(T)); // exact type, NOT IsAssignableFrom
foreach (var f in filters)
queryable = queryable.Where(f.CreateFilterExpression());
return queryable;
}
FilterAll is used widely in app services, OData, Excel export and search providers. The org
context itself comes from CurrentFilterContextProvider
(Hub.Domain/Services/CurrentFilterContextProvider.cs, an ISingletonDependency holding an
AsyncLocal<IFilterContext?>), which exposes ActiveOrgIds, ActiveOrgUnitIds,
DefaultOrgUnitId, UserId and the user-accessible sets that org filters such as
ShipmentOrganizationFilter read to build their expressions. For SQL-level filtering,
HubBaseRepository serialises ActiveOrgIds to JSON and builds OPENJSON-based EXISTS clauses
for Dapper-backed queries.
Two filter paths, easy to confuse
- Auto-active (
IsAutoActive => true) → a real global filter, always applied; bypass withIgnoreQueryFilters(). Matches byIsAssignableFrom(covers subclasses). - Non-auto (
IsAutoActive => false) → applied only when you call.FilterAll()/FilteredSet<T>(). Matches by exactEntityType == typeof(T)(no inheritance). ForgettingFilterAllsilently leaks cross-org data.
Full org-filtering details are in OData filtering.
Repositories¶
Both contexts use ABP repositories:
- Cargonerds:
AddDefaultRepositories(includeAllEntities: true)plus a customAddRepository<ApiKey, EfCoreApiKeyRepository>(). Other custom repos live undersrc/Cargonerds.EntityFrameworkCore/Repositories/(CargonerdsIdentityUserRepository,CommentRepository,IdentitySessionRepository,OrganizationUnitOwnerUserRepository). - Hub:
AddDefaultRepositories<IHubDbContext>(includeAllEntities: true), defaultWithDetailsinclude graphs viaoptions.Entity<T>(opt => opt.DefaultWithDetailsFunc = e => e.IncludeDefaultDetails())(Shipment, Consol, CustomsDeclaration, Order, Address, Tag, Quotation, Offer), and custom reposOrganizationRepository,MarginRepository,ShipmentBaseRepository,CalculatedShipmentRepository.
The Hub repository base is generic over the context:
public class HubBaseRepository<TDbContext, TEntity, TId>(
IDbContextProvider<TDbContext> dbContextProvider,
IServiceProvider serviceProvider,
CurrentFilterContextProvider filterContextProvider)
: EfCoreRepository<TDbContext, TEntity, TId>(dbContextProvider), IHubRepository<TEntity, TId>
where TDbContext : HubDbContext
where TEntity : HubBaseGuidEntity, IEntity<TId>
{
// exposes raw Dapper connections + OPENJSON org filters alongside EF
}
It mixes EF Core with Dapper (raw SQL) for hot/aggregate queries. Compiled queries for hot
paths live in Hub.EntityFrameworkCore/CompiledQueries/HubCompiledQueries.cs (e.g.
ShipmentFindByIdWithDetails, OrganizationFindById, AssociationsWithOrganizations) and
src/Cargonerds.EntityFrameworkCore/CompiledQueries/CargonerdsCompiledQueries.cs. See
Repositories for the full repository story.
Design-time factory and migrations¶
CargonerdsDbContextFactory (src/Cargonerds.EntityFrameworkCore/EntityFrameworkCore/CargonerdsDbContextFactory.cs)
implements IDesignTimeDbContextFactory<CargonerdsDbContext> so the EF Core CLI (dotnet ef,
Add-Migration, Update-Database) can build the context. It reads the Default connection string
from src/Cargonerds.DbMigrator/appsettings.json:
public CargonerdsDbContext CreateDbContext(string[] args)
{
var configuration = BuildConfiguration(); // base path "../Cargonerds.DbMigrator/"
CargonerdsEfCoreEntityExtensionMappings.Configure();
var builder = new DbContextOptionsBuilder<CargonerdsDbContext>()
.UseSqlServer(configuration.GetConnectionString("Default"));
return new CargonerdsDbContext(builder.Options);
}
Only the Default database is EF-migrated
CargonerdsDbContext is the only project with a Migrations/ folder. HubDbContext and
PricingDbContext ship no migrations, and only EntityFrameworkCoreCargonerdsDbSchemaMigrator
is registered, so CargonerdsDbMigrationService.MigrateAsync() migrates the Default DB
exclusively. The migration service also enforces no pending model changes before applying
(ValidateNoPendingModelChangesAsync() → Database.HasPendingModelChanges()), used in CI. The
Hub schema is created and evolved outside EF Core (external DB project / scripts). Full
details: Database Migrations.
PricingDbContext (effectively empty)¶
modules/pricing/src/Pricing.EntityFrameworkCore/EntityFrameworkCore/PricingDbContext.cs declares
no DbSets and only folds the Pricing model into itself:
[ConnectionStringName(PricingDbProperties.ConnectionStringName)] // "Pricing"
public class PricingDbContext : AbpDbContext<PricingDbContext>, IPricingDbContext
{
protected override void OnModelCreating(ModelBuilder builder)
{
base.OnModelCreating(builder);
builder.ConfigurePricing();
}
}
In practice the Pricing entities (Quotation, Offer, Margin, ChargeLine, …) are physically
mapped and owned by HubDbContext (their type configs live under
Hub.EntityFrameworkCore/.../TypeConfigurations/Pricing/). CargonerdsDbContext also calls
builder.ConfigurePricing(). Do not expect a separate Pricing schema to be produced by
PricingDbContext. See Pricing module and Hub module.
ABP patterns used here¶
AbpDbContext<T>base for all three contexts, with[ConnectionStringName]per context.[ReplaceDbContext]to foldIIdentityProDbContext,ISaasDbContextandIPermissionManagementDbContextintoCargonerdsDbContext.- Dependency injection
registration markers:
IScopedDependency/ISingletonDependency/ITransientDependencyand[ExposeServices]for the query-filter providers. - Repositories
via
AddDefaultRepositories/AddRepository<TEntity, TRepo>andEfCoreRepository<...>. ConfigureByConvention()for ABP audit/soft-delete/multi-tenant base columns, and the ABPObjectExtensionManagerEF mapping hook (CargonerdsEfCoreEntityExtensionMappings, run inPreConfigureServicesand the design-time factory).- ABP global-query-filter extension points (
ShouldFilterEntity,CreateFilterExpression) populated with a customIQueryFiltersystem rather than ABP'sIDataFilter.
Gotchas summary¶
Things to keep in mind
- Hub DB has no EF migrations and is not migrated by the app; its schema is managed
externally (
ConfigureHub()is commented out, noMigrations/folder, no Hub schema migrator). - Everything in the Hub is cached for 60s by default (
CacheAllQueries, absolute 1 min). Stale reads are expected unless invalidated;UpdateVersionTrackersthrowsUnexpectedNullExceptionif aVersionTrackerrow for a modified cacheable type is missing. - Hub concurrency stamps are disabled (no-op overrides) — no optimistic concurrency on Hub entities.
- DbContextPool(256) + mutable per-request flags.
HubDbContextcarries mutable state (AllowMasterDataCreation,CargonerdsCustomerInjectionEnabled,_allowFreezableEntityModification). Pooling resets contexts via ABP, but custom mutable state on a pooled context is a known footgun — confirm it is reset between requests. ArithAbortConnectionInterceptorreappliesSET ARITHABORT ONon every connection open (not once) to survivesp_reset_connection; without it the plan-cache slot differs from SSMS/Rider.- Two org-filter paths with different matching semantics (assignable vs. exact type) —
forgetting
.FilterAll()on the manual path leaks cross-org data. UseParameterizedCollectionModeis set only on the Cargonerds context, not the Hub.- Pricing context is effectively empty; its entities live in the Hub DB/context.