Table of Contents

Dependency injection

Register NextORM contexts with AddNextOrmContext, configure the provider and logging on a scoped DataContextBuilder, 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 - AddNextOrmContext registers the concrete context type as scoped and forwards IDataContext to it. The container constructs TContext, so its constructor must be resolvable from the container (for example the parameterless InMemoryDataContext).
  • Options path - AddNextOrmContext(Action<DataContextBuilder>) (or the Action<IServiceProvider, DataContextBuilder> overload) registers a scoped DataContextBuilder that the options delegate configures, plus an IDataContext factory that calls `` This is the path used with the provider Use… 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 IDataContext yields the same instance;
  • the options delegate is mandatory for factory-based registration - passing null throws ArgumentNullException at registration time, not at resolution time;
  • the DataContextBuilder itself 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 scoped DataContextBuilder); it does not register the concrete SqliteDataContext/SqlServerDataContext/PostgresDataContext type. Resolve IDataContext from 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.