.NET Learning Notes: Custom In-Memory Provider(1) - Registration and Discovery

Published: (February 12, 2026 at 11:28 PM EST)
5 min read
Source: Dev.to

Source: Dev.to

Alex

Key Implementations

1.1 Provider Extension Entry

I implement EF Core‑style extension methods as the entry point for the custom provider. Following EF Core’s naming conventions, the activation method is named Use[ProviderName] (e.g., UseCustomMemoryDb).

// Configures the DbContext to use the custom in‑memory database provider
public static DbContextOptionsBuilder UseCustomMemoryDb(
    this DbContextOptionsBuilder builder,
    string databaseName,
    bool clearOnCreate = false)
{
    if (builder == null)
    {
        throw new ArgumentNullException(
            nameof(builder),
            "DbContextOptionsBuilder cannot be null.");
    }

    // Create extension instance with user‑provided configuration
    var extension = builder.Options.FindExtension<CustomMemoryDbContextOptionsExtension>()
                  ?? new CustomMemoryDbContextOptionsExtension(databaseName, clearOnCreate);

    // Add or update the extension in EF Core's options (EF Core internal API)
    ((IDbContextOptionsBuilderInfrastructure)builder).AddOrUpdateExtension(extension);

    return builder;
}

AddOrUpdateExtension injects the custom IDbContextOptionsExtension into EF Core’s immutable options model.
During service‑provider construction EF Core scans registered extensions and invokes their ApplyServices method. This is where provider‑specific services (query compilation, execution pipeline, database abstractions, etc.) are wired into the internal DI container.

Usage

services.AddDbContext(options =>
    options.UseCustomMemoryDb("TestDatabase"));

This step only integrates the extension into EF Core’s configuration pipeline. The actual behavior is defined later in the IDbContextOptionsExtension.ApplyServices implementation.

1.2 Provider Extension Class

The CustomMemoryDbContextOptionsExtension class implements IDbContextOptionsExtension. This extension is the provider’s configuration carrier and the formal signal EF Core uses to discover and activate a database provider. EF Core recognizes it as a database provider via the DbContextOptionsExtensionInfo metadata (IsDatabaseProvider => true), and later calls ApplyServices as the entry hook to register provider services.

ApplyServices runs during EF Core’s internal service‑provider construction for a given DbContextOptions instance. Provider services are registered into EF Core’s internal service collection for that context (a scoped “internal container”), not the application’s global DI container. In other words, the provider wiring is isolated to the DbContext’s internal dependency graph.

public void ApplyServices(IServiceCollection services)
{
    // Register configuration as singleton (available to all provider services)
    services.AddSingleton(new CustomMemoryDbConfig(_databaseName, _clearOnCreate));
    services.AddEntityFrameworkCustomMemoryDatabase();
}

Extension Method that Registers Provider Services

public static IServiceCollection AddEntityFrameworkCustomMemoryDatabase(
    this IServiceCollection serviceCollection)
{
    // Validate input (align with official provider patterns)
    ArgumentNullException.ThrowIfNull(serviceCollection, nameof(serviceCollection));

    // NEW: register SnapshotValueBufferFactory for compiling visitor factory
    serviceCollection.TryAddSingleton<SnapshotValueBufferFactory>();

    // Step 1: register EF Core framework‑facing services (core pipeline slots)
    var builder = new EntityFrameworkServicesBuilder(serviceCollection);

    // These are EF Core "framework services" that must be present for a database provider
    builder.TryAdd<IRelationalConnection>();
    builder.TryAdd<IDatabaseProvider>();
    builder.TryAdd<IQueryCompiler>();
    builder.TryAdd<IQueryContextFactory>();
    builder.TryAdd<IQueryTranslationPreprocessorFactory>();
    builder.TryAdd<IQueryTranslationPostprocessorFactory>();
    // Required: provider must supply an IDatabase implementation
    builder.TryAdd<IDatabase>();
    builder.TryAdd<IProviderSpecificService>();

    // Register EF Core core services (fills remaining defaults)
    builder.TryAddCoreServices();

    // Override specific EF services when default behavior is not compatible with this provider
    serviceCollection.Replace(
        ServiceDescriptor.Scoped<IValueGeneratorSelector, CustomValueGeneratorSelector>());

    // Step 2: provider‑specific extension services (provider‑only abstractions)
    builder.TryAddProviderSpecificServices(p =>
    {
        p.TryAddScoped<IQueryableMethodTranslatingExpressionVisitorFactory,
                       CustomQueryableMethodTranslatingExpressionVisitorFactory>();
        p.TryAddScoped<IShapedQueryCompilingExpressionVisitorFactory,
                       CustomShapedQueryCompilingExpressionVisitorFactory>();
    });

    // Step 3: provider storage‑related services (truncated in original snippet)
    // ... (continue with storage‑related registrations)

    return serviceCollection;
}

The code above follows the official pattern for EF Core database providers:

  1. Framework services – required by any provider.
  2. Core service overrides – replace default implementations when needed.
  3. Provider‑specific services – abstractions that exist only for this provider.

With the extension method registered, EF Core will automatically discover the custom in‑memory provider whenever UseCustomMemoryDb is called, and the provider’s services will be available to the DbContext during its lifetime.

In‑Memory Database Registration

// Register the in‑memory database (actual in‑memory database implementation)
serviceCollection.TryAddSingleton(new MemoryDatabaseRoot());

serviceCollection.TryAddScoped(sp =>
{
    var cfg  = sp.GetRequiredService<CustomMemoryDbConfig>();
    var root = sp.GetRequiredService<MemoryDatabaseRoot>();
    var db   = root.GetOrAdd(cfg.DatabaseName);

    if (cfg.ClearOnCreate)
    {
        db.ClearAllTables();
    }

    return db;
});

serviceCollection.TryAddScoped(typeof(IMemoryTable<>), typeof(MemoryTable<>));

return serviceCollection;

Provider Configuration

The provider registers its configuration as a singleton:

services.AddSingleton(
    new CustomMemoryDbConfig(_databaseName, _clearOnCreate));

EF Core can cache or reuse the internal service provider based on the options fingerprint.
Therefore EF Core assumes the provider fully participates in the internal pipeline, which contains many required service slots (translation, compilation, execution, tracking helpers, value generation, type mapping, etc.). These slots are not optional in practice—EF Core will hit them during normal operations such as:

  • Translating LINQ
  • Compiling shaped queries
  • Creating query contexts
  • Generating keys
  • Materializing results

TryAddCoreServices() fills many of those slots with EF Core defaults. This is useful, but you must register your provider implementations before calling TryAddCoreServices() when you intend to override defaults (or when defaults are provider‑incompatible). Otherwise EF Core may wire up default services that either do not work for your provider or silently bypass your implementation.

Key Extension Points

IQueryableMethodTranslatingExpressionVisitorFactory

  • Core LINQ‑to‑provider translation entry point.
  • EF Core parses LINQ into expression trees, then uses this factory to create a translator visitor that converts LINQ method calls (Where, Select, Join, etc.) into a provider‑specific query representation.
  • If you want to control which LINQ patterns are supported and how they are interpreted, this is one of the most important extension points.

IShapedQueryCompilingExpressionVisitorFactory

  • After translation, EF Core must compile a shaped query: a runnable delegate that materializes results into entity instances (or projections), handles tracking, identity resolution, and include fix‑up.
  • This factory is where a provider plugs into the compilation phase and controls how snapshots/rows become materialized objects.

IValueGeneratorSelector

  • Responsible for value generation.
  • By default, EF Core’s built‑in ValueGeneratorSelector only knows how to generate Guid, string, and byte[] values.
  • It does not generate integer identities unless a provider supplies that behavior.

Keep the registration order in mind: register provider‑specific services before invoking TryAddCoreServices() to ensure your implementations are used instead of EF Core defaults.

0 views
Back to Blog

Related posts

Read more »

Rock ✊ Paper ✋ Scissors ✌️

What is WebForms Core? WebForms Corehttps://github.com/webforms-core is a new multi‑platform technology from Elanathttps://elanat.net/ that is designed to compe...

17. C# (Char)

The Real Goal of This Lesson > “When you only need a single character, why should you use char instead of string?” This lesson is not about syntax. It is about...