Endatix.Framework 0.8.0

dotnet add package Endatix.Framework --version 0.8.0
                    
NuGet\Install-Package Endatix.Framework -Version 0.8.0
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="Endatix.Framework" Version="0.8.0" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="Endatix.Framework" Version="0.8.0" />
                    
Directory.Packages.props
<PackageReference Include="Endatix.Framework" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add Endatix.Framework --version 0.8.0
                    
#r "nuget: Endatix.Framework, 0.8.0"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package Endatix.Framework@0.8.0
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=Endatix.Framework&version=0.8.0
                    
Install as a Cake Addin
#tool nuget:?package=Endatix.Framework&version=0.8.0
                    
Install as a Cake Tool

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

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:

  1. Core (always) — AppDbContext and AppIdentityDbContext are migrated automatically; no contributor registration required.
  2. Modules (opt-in) — each module with its own DbContext calls AddDbContextWithMigrations<TContext> in ConfigureServices (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
  1. Create {Name}Module : IEndatixModule, IHasDbMigrations (+ IHasFeatureFlag when optional). All DI belongs on the module class; do not add Setup.cs.
  2. Base context. An abstract {Name}DbContextBase owns the DbSets, the module's own schema and the shared model. The first statement of its OnModelCreating calls an abstract EnsureSupportedProvider(), 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 abstract ApplyProviderConfigurations(modelBuilder), and finishes with modelBuilder.ApplySnowflakeIdValueGenerators(Database) and modelBuilder.ApplyModuleTableNames().
  3. Derived contexts. One sealed context per provider ({Name}PostgreSqlDbContext, {Name}SqlServerDbContext). Each implements EnsureSupportedProvider() as Database.EnsureNpgsql(typeof(TDerived)) (or EnsureSqlServer), which throws a NotSupportedException naming the context and the configured provider instead of failing at the first query, and overrides ApplyProviderConfigurations with modelBuilder.ApplyConfigurationsFor<TDerived>(typeof(TDerived).Assembly).
  4. 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 abstract BackgroundJobProviderConfiguration; Audience passes an AudienceSqlDialect to its shared configurations, and its derived contexts override a SqlDialect property instead of ApplyProviderConfigurations. Use the per-provider subclass when one entity differs, and a dialect object when several do.
  5. Design-time factory. One IDesignTimeDbContextFactory<TDerived> per derived context. It pins the provider (UseNpgsql or UseSqlServer) instead of reading DefaultConnection_DbProvider, sets the migrations assembly to the module assembly, and puts the migrations history table in the module schema. It reads ConnectionStrings: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 add does not connect to the database.
  6. Runtime registration. In ConfigureServices, pick the derived context with DatabaseProviderResolver.IsPostgreSql(configuration), register it with builder.AddDbContextWithMigrations<TDerived>(...) from Endatix.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 on EfUnitOfWorkBase<TContext>, which is constrained to DbContext), register the base to the derived context once and alias the interface to the base, so a single line names the provider.
  7. Migrations namespaces. Set both PostgreSqlMigrationsNamespace and SqlServerMigrationsNamespace to 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 add writes 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

See high-performance logging.

More Information:

For detailed installation instructions, please visit Endatix Installation Guide.

Product 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. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

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