Endatix.Framework
0.8.0
dotnet add package Endatix.Framework --version 0.8.0
NuGet\Install-Package Endatix.Framework -Version 0.8.0
<PackageReference Include="Endatix.Framework" Version="0.8.0" />
<PackageVersion Include="Endatix.Framework" Version="0.8.0" />
<PackageReference Include="Endatix.Framework" />
paket add Endatix.Framework --version 0.8.0
#r "nuget: Endatix.Framework, 0.8.0"
#:package Endatix.Framework@0.8.0
#addin nuget:?package=Endatix.Framework&version=0.8.0
#tool nuget:?package=Endatix.Framework&version=0.8.0
Endatix.Framework
This package provides common plugin and extensibility points used to extend or customize the Endatix Platform. It allows developers to build and integrate custom functionality within the platform.
Endatix Platform is an open-source data collection and management library for .NET. It is designed for building secure, scalable, and integrated form-centric applications that work with SurveyJS. Endatix empowers business users with advanced workflows, automation, and meaningful insights.
Installation:
dotnet add package Endatix.Framework
Recommended Usage:
For running and hosting the Endatix Platform, Endatix.Api.Host is the recommended main package as it simplifies the installation and setup process.
dotnet add package Endatix.Api.Host
Module kernel
Optional platform modules implement IEndatixModule. The host registers them via EndatixBuilder.UseModule({Name}Module.Instance), which scans the module assembly for MediatR handlers and FastEndpoints and calls ConfigureServices at finalization.
| Contract | Purpose |
|---|---|
IEndatixModule |
Required entry point — Assembly + ConfigureServices |
IHasFeatureFlag |
Optional — module skipped when flag is disabled in Endatix:FeatureFlags |
IHasDbMigrations |
Optional marker — module ships EF migrations; host warns if no contributor was registered |
IDbContextMigrationContributor |
Opt-in startup migration contract for module/custom DbContexts |
Startup migrations (two phases)
When Endatix:Data:EnableAutoMigrations is true, DatabaseMigrationService runs:
- Core (always) —
AppDbContextandAppIdentityDbContextare migrated automatically; no contributor registration required. - Modules (opt-in) — each module with its own DbContext calls
AddDbContextWithMigrations<TContext>inConfigureServices(registers DbContext + migration contributor).
Module persistence
Endatix has two persistence patterns. Every new module uses the module pattern.
| Monolith pattern | Module pattern | |
|---|---|---|
| Used by | AppDbContext, AppIdentityDbContext |
Reporting, Audience and Jobs. The SaaS modules are moving to it. |
| Context types | One context type for every provider | An abstract base context with the shared model, plus one sealed derived context per provider |
| Where migrations live | A separate assembly per provider: Endatix.Persistence.PostgreSql, Endatix.Persistence.SqlServer |
The module assembly, under Persistence/Migrations/<Provider>/ |
| Model snapshot | One per provider assembly, for the shared context type | One per derived context: <Derived>ModelSnapshot.cs |
| When to use | The core contexts only; do not add new ones | Every module that owns tables |
Module recipe
- Create
{Name}Module : IEndatixModule, IHasDbMigrations(+IHasFeatureFlagwhen optional). All DI belongs on the module class; do not addSetup.cs. - Base context. An abstract
{Name}DbContextBaseowns theDbSets, the module's own schema and the shared model. The first statement of itsOnModelCreatingcalls an abstractEnsureSupportedProvider(), so every derived context has to state its provider and a wrong one fails before any mapping is applied. It then applies the shared configurations explicitly (modelBuilder.ApplyConfiguration(new XConfiguration())), because an[ApplyConfigurationFor<T>]attribute can name only one context. It then calls an abstractApplyProviderConfigurations(modelBuilder), and finishes withmodelBuilder.ApplySnowflakeIdValueGenerators(Database)andmodelBuilder.ApplyModuleTableNames(). - Derived contexts. One sealed context per provider (
{Name}PostgreSqlDbContext,{Name}SqlServerDbContext). Each implementsEnsureSupportedProvider()asDatabase.EnsureNpgsql(typeof(TDerived))(orEnsureSqlServer), which throws aNotSupportedExceptionnaming the context and the configured provider instead of failing at the first query, and overridesApplyProviderConfigurationswithmodelBuilder.ApplyConfigurationsFor<TDerived>(typeof(TDerived).Assembly). - Provider configurations. Column types, JSON columns and filtered-index SQL live in
Persistence/Config/<Provider>/, each class marked[ApplyConfigurationFor<TDerived>]. When the providers differ only in spelling (identifier quoting, the boolean literal, the JSON column type, a collation), declare each index and constraint once and let the provider supply the spelling, so the copies cannot drift: Jobs derives one class per provider from an abstractBackgroundJobProviderConfiguration; Audience passes anAudienceSqlDialectto its shared configurations, and its derived contexts override aSqlDialectproperty instead ofApplyProviderConfigurations. Use the per-provider subclass when one entity differs, and a dialect object when several do. - Design-time factory. One
IDesignTimeDbContextFactory<TDerived>per derived context. It pins the provider (UseNpgsqlorUseSqlServer) instead of readingDefaultConnection_DbProvider, sets the migrations assembly to the module assembly, and puts the migrations history table in the module schema. It readsConnectionStrings:DefaultConnection(ModuleDesignTimeConfiguration) and fails when it is missing, so set a PostgreSQL connection string to scaffold PostgreSQL migrations and a SQL Server one to scaffold SQL Server migrations.migrations adddoes not connect to the database. - Runtime registration. In
ConfigureServices, pick the derived context withDatabaseProviderResolver.IsPostgreSql(configuration), register it withbuilder.AddDbContextWithMigrations<TDerived>(...)fromEndatix.Infrastructure.Data, and map the module interface to it:builder.Services.AddScoped<I{Name}DbContext>(sp => sp.GetRequiredService<TDerived>()). Consumers depend on the interface only, so nothing downstream branches on the provider. When code also needs the base type (a unit of work onEfUnitOfWorkBase<TContext>, which is constrained toDbContext), register the base to the derived context once and alias the interface to the base, so a single line names the provider. - Migrations namespaces. Set both
PostgreSqlMigrationsNamespaceandSqlServerMigrationsNamespaceto the module's migrations root namespace (Endatix.Modules.<Name>.Persistence.Migrations), never to a provider folder. Registration requires the active provider's namespace, and two equal values keep namespace filtering off: each derived context already finds its own migrations by their[DbContext]attribute.
Adding a migration
Run from the repository root, with Endatix.WebHost as the startup project.
# PostgreSQL
ConnectionStrings__DefaultConnection="Host=localhost;Database=endatix;Username=postgres;Password=..." \
dotnet ef migrations add <MigrationName> \
--startup-project src/Endatix.WebHost \
--project src/Endatix.Modules.<Name> \
--context <Name>PostgreSqlDbContext \
--output-dir Persistence/Migrations/PostgreSql
# SQL Server
ConnectionStrings__DefaultConnection="Server=localhost;Database=endatix;User Id=sa;Password=...;TrustServerCertificate=True" \
dotnet ef migrations add <MigrationName> \
--startup-project src/Endatix.WebHost \
--project src/Endatix.Modules.<Name> \
--context <Name>SqlServerDbContext \
--output-dir Persistence/Migrations/SqlServer
Always pass --output-dir. A context without a snapshot yet (the first migration of a chain) otherwise lands in the
project's default Migrations/ folder with the namespace Endatix.Modules.<Name>.Migrations, and later migrations of
the chain that omit --output-dir follow it there. With both migrations namespaces set to the root (step 7), namespace
filtering is off and EF still finds them by their [DbContext] attribute, so the cost is files in the wrong folder and
namespace that no check reports. Only a module that sets provider-specific migrations namespaces filters by that prefix,
and it would silently skip them.
Module SQL Server migrations need SQL Server 2025 or later, or Azure SQL Database.
Why not one context with two migration folders
One context type cannot own both providers' migrations in one assembly, because of two EF Core rules:
- Discovery. EF finds a context's migrations by matching
Context.GetType()against each migration's[DbContext(typeof(...))]attribute. With one context type, both folders match, and each provider would see the other's migrations. - Snapshot placement. EF reads a context's snapshot by its
[DbContext(typeof(...))]attribute, taking the first match in the migrations assembly.migrations addwrites it by file name,{ContextName}ModelSnapshot.cs, overwriting that file wherever it sits in the project. Either way a context type has one snapshot, so two providers would overwrite one snapshot.
The monolith pattern avoids both by putting each provider's migrations in its own assembly.
Applied migrations: retarget, never regenerate
When an existing context moves to this pattern, change only the [DbContext(typeof(...))] attribute in its .Designer.cs files and its snapshot to the derived type. The migration id, Up() and Down() stay byte-identical. A regenerated migration gets a new id, and every database that applied the old one would run it again. Deleting the other provider's ValueGenerationStrategy annotation from a designer is also safe: the designer's target model reaches only the active provider's SQL generator, which ignores the other provider's annotations, so the migration's SQL does not change.
Check a snapshot with a throwaway migration, then delete it:
dotnet ef migrations add Probe --context <Derived> --output-dir Persistence/Migrations/<Provider> \
--startup-project src/Endatix.WebHost --project src/Endatix.Modules.<Name>
The check passes when the generated Up() and Down() are empty and git status --porcelain lists only the two new *_Probe*.cs files, so the committed snapshot is unchanged. Delete both files; never commit them. CI runs the same comparison for every migrated context (see below), but the probe shows what differs.
Id annotations in snapshots
ApplySnowflakeIdValueGenerators(Database) writes only the active provider's ValueGenerationStrategy annotation on each long Id key, so migrations add scaffolds snapshots and designers that need no hand edits. The parameterless ApplySnowflakeIdValueGenerators() writes both annotations, so every scaffolded snapshot and designer also carries the other provider's annotation. That line names the other provider's EF types, so it compiles only while the project references both EF providers. Deleting it by hand is easy to get wrong: removing the wrong line of the chained annotation drops the provider's own annotation without any error. The parameterless overload is obsolete and will be removed after the next stable release.
Every committed snapshot and designer in this repository carries only its own provider's annotation. CommittedModelProviderAnnotationsTests in Endatix.Infrastructure.Tests fails CI when one carries the other provider's annotation; has-pending-model-changes cannot catch that, because it compares relational models only. CommittedSnapshotDriftTests checks every snapshot against its context's current model. It builds every migrated context the way the host registers it, without a connection, and fails CI when the context has pending model changes (a snapshot that lost its own provider's annotation, for example) or when migrations add would write a different snapshot (stale entity order, for example). Add a new migrated context to its MigratedContexts list; the test fails until you do.
Model snapshots never contain the ValueGeneratorFactory annotation: EF Core filters it out of every snapshot by design and does not compare it. Do not restore it by hand.
Observability (startup)
Startup logging uses Microsoft.Extensions.Logging source generation — not Serilog APIs — so hosts can switch to OpenTelemetry without changing call sites.
Layer responsibilities
| Layer | Responsibility |
|---|---|
| Framework | EndatixEventIds (global registry), EndatixLoggerExtensions (generic operation lifecycle only) |
| Infrastructure / Hosting | Domain-specific *LoggerExtensions collocated with the feature (e.g. MigrationLoggerExtensions, EndatixBuilderLoggerExtensions) |
Framework exposes only generic primitives:
EndatixEventIds— platform-wide stable EventId registry (claim a range before adding domain extensions)EndatixLoggerExtensions—LogOperationStarted,LogOperationCompleted,LogOperationSkipped,LogOperationFailed
Domain-specific messages (migrations, seeding, host builders) belong in collocated *LoggerExtensions types in Infrastructure or Hosting — not in Framework.
Event ID registry
EndatixEventIds follows the same grouping pattern as Actions — nested static classes per area, plus utility collections:
EndatixEventIds.Lifecycle.OperationStarted // Framework
EndatixEventIds.Migrations.DbContextMigrated // Infrastructure
EndatixEventIds.Seeding.SampleDataSeeded // Infrastructure
EndatixEventIds.IdentitySeed.UserCreated // Infrastructure
EndatixEventIds.Hosting.ModuleRegistered // Hosting
EndatixEventIds.Ranges.MigrationsStart // claim before adding IDs
EndatixEventIds.Migrations.All // all IDs in a group
EndatixEventIds.IsMigration(eventId) // range helpers
| Nested class | Range | Owner |
|---|---|---|
Lifecycle |
1000–1003 | Framework |
Migrations |
1004–1099 | Infrastructure |
Seeding / IdentitySeed |
1100–1199 | Infrastructure |
Hosting |
1200–1299 | Hosting |
Ranges.* |
2000+ | Reserved (Auth, Forms, Webhooks) |
Claim a block in EndatixEventIds.Ranges before adding domain extensions.
Usage
Always pass the caller's typed ILogger (e.g. ILogger<DatabaseMigrationService>) so logger category is preserved for tracing.
Use Framework primitives for lifecycle; domain wrappers for specialized messages:
// Framework — generic lifecycle
_logger.LogOperationStarted(MigrationOperations.ApplyDbMigrations);
// Infrastructure — domain-specific (collocated)
_logger.LogDbContextMigrated(nameof(AppDbContext), durationMs);
Existing domain wrappers:
| Package | Location |
|---|---|
| Infrastructure | Data/Logging/MigrationLoggerExtensions.cs, Data/Logging/DataSeedingLoggerExtensions.cs |
| Infrastructure | Identity/Seed/IdentitySeedLoggerExtensions.cs |
| Hosting | Builders/Logging/EndatixBuilderLoggerExtensions.cs |
OTEL attribute mapping
MEL placeholder → future log attribute:
| Placeholder | OTEL alignment |
|---|---|
Operation |
operation.name / custom dimension |
DbContext |
Application-specific; pairs with future db.operation.name spans |
DurationMs |
Duration in milliseconds |
DbSystem |
db.system (e.g. SqlServer, PostgreSql) |
Reason |
Skip/disable reason |
EventId |
Stable correlation via EndatixEventIds |
More Information:
For detailed installation instructions, please visit Endatix Installation Guide.
| 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
- Ardalis.GuardClauses (>= 5.0.0)
- Microsoft.EntityFrameworkCore (>= 10.0.0)
- Microsoft.EntityFrameworkCore.Relational (>= 10.0.0)
- Microsoft.Extensions.Configuration.Abstractions (>= 10.0.0)
- Microsoft.Extensions.DependencyInjection.Abstractions (>= 10.0.0)
- Microsoft.Extensions.Logging.Abstractions (>= 10.0.0)
- Microsoft.Extensions.Options.DataAnnotations (>= 10.0.0)
- Microsoft.FeatureManagement.AspNetCore (>= 4.4.0)
NuGet packages (10)
Showing the top 5 NuGet packages that depend on Endatix.Framework:
| Package | Downloads |
|---|---|
|
Endatix.Infrastructure
This package provides implementations of the application's dependencies and external resources. It includes infrastructure-level services like database access, messaging, and other integrations. Endatix Platform is an open-source data collection and management library for .NET. |
|
|
Endatix.Api
This package contains the key presentation layer of the Endatix Platform, including both Public and Management APIs. Endatix Platform is an open-source data collection and management library for .NET. |
|
|
Endatix.Persistence.SqlServer
This package provides implementations of the application's dependencies and external resources. It includes infrastructure-level services like database access, messaging, and other integrations. Endatix Platform is an open-source data collection and management library for .NET. |
|
|
Endatix.Persistence.PostgreSql
This package provides implementations of the application's dependencies and external resources. It includes infrastructure-level services like database access, messaging, and other integrations. Endatix Platform is an open-source data collection and management library for .NET. |
|
|
Endatix.Hosting
Package Description |
GitHub repositories
This package is not used by any popular GitHub repositories.
| Version | Downloads | Last Updated |
|---|---|---|
| 0.8.0 | 207 | 10/13/2026 |
| 0.7.8 | 280 | 10/2/2026 |
| 0.7.7 | 165 | 10/1/2026 |
| 0.7.6 | 301 | 9/9/2026 |
| 0.7.5 | 389 | 8/30/2026 |
| 0.7.4 | 227 | 8/26/2026 |
| 0.7.3 | 315 | 8/14/2026 |
| 0.7.2 | 295 | 7/31/2026 |
| 0.7.0 | 382 | 7/8/2026 |
| 0.6.0 | 880 | 2/16/2026 |
| 0.5.1 | 461 | 12/5/2025 |
| 0.5.0 | 247 | 11/28/2025 |
| 0.4.2 | 4,389 | 6/27/2025 |
| 0.4.1 | 287 | 5/9/2025 |
| 0.4.0 | 288 | 4/25/2025 |
| 0.3.0 | 323 | 3/27/2025 |
| 0.2.0 | 296 | 2/10/2025 |
| 0.1.0 | 3,342 | 8/23/2024 |