Dependency injection
Register NextORM contexts with
AddNextOrmContext, configure the provider and logging on a scopedDataContextBuilder, and resolve a single context instance per scope.
Prerequisites: Installation · Quickstart.
Overview
DI registration lives in NextORM.Core (ServiceCollectionExtensions) and works with any
Microsoft.Extensions.DependencyInjection container. There are two registration paths:
- Type path -
AddNextOrmContextregisters the concrete context type as scoped and forwardsIDataContextto it. The container constructsTContext, so its constructor must be resolvable from the container (for example the parameterlessInMemoryDataContext). - Options path -
AddNextOrmContext(Action<DataContextBuilder>)(or theAction<IServiceProvider, DataContextBuilder>overload) registers a scopedDataContextBuilderthat the options delegate configures, plus anIDataContextfactory that calls `` This is the path used with the providerUse…methods.
The guarantees come from the XML docs on ServiceCollectionExtensions:
- a concrete context type is registered once per scope, and resolving the concrete type and
IDataContextyields the same instance; - the options delegate is mandatory for factory-based registration - passing
nullthrowsArgumentNullExceptionat registration time, not at resolution time; - the
DataContextBuilderitself is scoped, so every scope gets a fresh builder.
Keyed variants (AddKeyedNextOrmContext) register the builder and IDataContext under a service key,
so several differently-configured contexts can coexist.
Registering a context
The generic registration is the shortest form:
var services = new ServiceCollection();
services.AddNextOrmContext<InMemoryDataContext>();
using var provider = services.BuildServiceProvider();
using var scope = provider.CreateScope();
var viaInterface = scope.ServiceProvider.GetRequiredService<IDataContext>();
var viaConcrete = scope.ServiceProvider.GetRequiredService<InMemoryDataContext>();
// viaInterface and viaConcrete are the same instance
To configure a database provider, use the options overload:
services.AddNextOrmContext(builder => builder.UseSqlite("app.db"));
The IServiceProvider overload lets you pull dependencies such as ILoggerFactory out of the
container while configuring:
services.AddNextOrmContext((sp, builder) =>
{
var loggerFactory = sp.GetRequiredService<ILoggerFactory>();
builder
.UseSqlServer(connectionString)
.UseLoggerFactory(loggerFactory);
});
A keyed registration keeps multiple contexts apart:
services.AddKeyedNextOrmContext(builder => builder.UsePostgres(reportingConnectionString), "reporting");
services.AddKeyedNextOrmContext<InMemoryDataContext>("audit"); // generic keyed type path
using var scope = provider.CreateScope();
var reporting = scope.ServiceProvider.GetRequiredKeyedService<IDataContext>("reporting");
Note: the options path registers only
IDataContext(and the scopedDataContextBuilder); it does not register the concreteSqliteDataContext/SqlServerDataContext/PostgresDataContexttype. ResolveIDataContextfrom the options path. The type path is the one that makes the concrete type and the interface resolve to the same instance.
DI-registered interceptors
An interceptor can be registered by its service type instead of a pre-built instance; the builder resolves it from the scope that created the builder:
services.AddScoped<TimingInterceptor>();
services.AddNextOrmContext(builder => builder
.UseSqlite("app.db")
.AddInterceptor<TimingInterceptor>()); // or .AddInterceptor(typeof(TimingInterceptor))
Resolution happens once per context creation, so scoped, singleton and transient lifetimes are honoured and the container keeps ownership of disposal. A type registration requires a DI-backed builder - it fails at CreateDataContext() on a standalone builder, and a service that is not registered fails with the container's own GetRequiredService error before the context factory runs. See Interceptors for ordering, deduplication and the error contract.
Configuring the builder directly
DataContextBuilder can also be used without a container - this is exactly what the DI factory calls:
var builder = new DataContextBuilder()
.UseSqlite("app.db")
.UseLoggerFactory(loggerFactory)
.LogSensitiveData(false);
using var dataContext = builder.CreateDataContext();
// Without a Use… call (or an assigned Factory) CreateDataContext() throws
// InvalidOperationException("Context is not set").
| Member | Purpose |
|---|---|
Factory |
Func<DataContextBuilder, IDataContext>; set by the Use… extensions, or assign your own to bypass providers. |
UseLoggerFactory(ILoggerFactory) |
Creates the context, command and enumerator loggers from the factory. |
LogSensitiveData(bool) |
Controls whether parameter values are written to the logs. |
CreateDataContext |
Invokes Factory; throws InvalidOperationException("Context is not set") when no provider was configured. |
The provider extensions (UseSqlite/UseSqlServer/UsePostgres, in the provider packages) each accept
either a connection string or a DbConnection:
builder.UseSqlite("app.db"); // context creates and owns the connection
builder.UseSqlite(connection); // context reuses the supplied connection
builder.UseSqlServer("Server=localhost;Database=app;Trusted_Connection=True;");
builder.UsePostgres("Host=localhost;Database=app");
A context created from a supplied connection leaves that connection open when the context is disposed;
a context that created its own connection closes it. In DEBUG builds UseSqlite(string) also
verifies that the file exists.
Lifetimes
| Registration | Service | Lifetime | Notes |
|---|---|---|---|
AddNextOrmContext |
T |
scoped | once per scope |
AddNextOrmContext |
IDataContext |
scoped | forwards to the same T instance |
AddNextOrmContext(Action<…>) |
DataContextBuilder |
scoped | built fresh per scope |
AddNextOrmContext(Action<…>) |
IDataContext |
scoped | builder.CreateDataContext() |
AddKeyedNextOrmContext(…, key) |
keyed IDataContext / DataContextBuilder |
scoped | resolved with the key |
Because IDataContext is scoped and implements IDisposable/IAsyncDisposable, disposing the scope
disposes the context (and, for contexts that own it, the connection). Do not resolve contexts from the
root provider - always create a scope.
Provider differences
UseLoggerFactory and LogSensitiveData behave identically across providers; only the Use… extension
that selects the provider differs.
| Provider | Extension | Overloads |
|---|---|---|
| SQLite | UseSqlite |
string filepath, DbConnection |
| SQL Server | UseSqlServer |
string connectionString, DbConnection |
| PostgreSQL | UsePostgres |
string connectionString, DbConnection |
| MySQL | UseMySql |
string connectionString, DbConnection |
| MariaDB | UseMariaDb |
string connectionString, DbConnection |
| ClickHouse | UseClickHouse |
string connectionString, DbConnection |
| In-memory | none | register InMemoryDataContext directly or assign Factory |
See also
Source: tests/nextorm.core.tests/DependencyInjection/DependencyInjectionTests.cs:13,
tests/nextorm.core.tests/DependencyInjection/DependencyInjectionTests.cs:43,
tests/nextorm.core.tests/DependencyInjection/DependencyInjectionTests.cs:58,
tests/nextorm.core.tests/DependencyInjection/DependencyInjectionTests.cs:71;
src/nextorm.core/DI/ServiceCollectionExtensions.cs:73;
src/nextorm.core/DI/DataContextBuilder.cs:20;
src/nextorm.sqlite/DI/SqliteDataContextOptionsBuilderExtensions.cs:8;
src/nextorm.sqlserver/DI/SqlServerDataContextOptionsBuilderExtensions.cs:8;
src/nextorm.postgres/DI/PostgresDataContextOptionsBuilderExtensions.cs:8.