DEV Community

Alex
Alex

Posted on

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

1. Custom In-Memory Provider Registration and Discovery in EF Core

Core Goal: The objective of this section is to establish a proper EF Core provider registration entry point, allowing EF Core to discover the custom provider, integrate it into the options pipeline, and later invoke its service registration logic during DbContext initialization.

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: Use Use[ProviderName] (e.g., UseCustomMemoryDb)—the standard convention for EF Core provider activation methods.

// 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;
}
Enter fullscreen mode Exit fullscreen mode

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 the moment where provider-specific services (query compilation, execution pipeline, database abstractions, etc.) are wired into the internal DI container.

And we can use it by:

services.AddDbContext<AppDbContext>(
    options =>
        options.UseCustomMemoryDb("TestDatabase"));
Enter fullscreen mode Exit fullscreen mode

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 (e.g. 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. This is important: 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();  
}

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<ITypeMappingSource, CustomMemoryTypeMappingSource>();  
    builder.TryAdd<LoggingDefinitions, CustomMemoryLoggingDefinitions>();  
    builder.TryAdd<IQueryableMethodTranslatingExpressionVisitorFactory,  
        CustomMemoryQueryableMethodTranslatingExpressionVisitorFactory>();  
    builder.TryAdd<IShapedQueryCompilingExpressionVisitorFactory,  
        CustomMemoryShapedQueryCompilingExpressionVisitorFactory>();  
    builder.TryAdd<IValueGeneratorSelector, CustomMemoryValueGeneratorSelector>();  
    builder.TryAdd<IDatabaseProvider, CustomMemoryDatabaseProvider>();
    // Required: provider must supply an IDatabase implementation 
    builder.TryAdd<IDatabase, CustomMemoryEfDatabase>(); 
    builder.TryAdd<IQueryContextFactory, CustomMemoryQueryContextFactory>();

    // 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<IEntityFinderSource, CustomMemoryEntityFinderSource>()  
    );

    // Step 2: provider-specific extension services (provider-only abstractions)
    builder.TryAddProviderSpecificServices(p =>  
    {  
        p.TryAddScoped<IEntityFinderFactory, CustomMemoryEntityFinderFactory>();  
        p.TryAddScoped<ICustomMemoryDatabaseProvider, CustomMemoryDatabaseProvider>();  
    });

    // Step 3: provider storage services (actual in-memory database implementation)
    serviceCollection.TryAddSingleton(new MemoryDatabaseRoot());  
    serviceCollection.TryAddScoped<IMemoryDatabase>(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;      
}
Enter fullscreen mode Exit fullscreen mode

Provider registers config as singleton (services.AddSingleton(new CustomMemoryDbConfig(_databaseName, _clearOnCreate));). Because EF Core can cache or reuse the internal service provider based on the options fingerprint.

EF Core assumes your provider fully participates in the internal pipeline. That pipeline has specific 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, and materializing results.

TryAddCoreServices() fills many of those slots with EF Core defaults. That’s useful, but it also means 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.

  • IQueryableMethodTranslatingExpressionVisitorFactory

    This is the core LINQ-to-provider translation entry. 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 what LINQ patterns are supported and how they are interpreted, this is one of the most important extension points.

  • IShapedQueryCompilingExpressionVisitorFactory

    After translation, EF Core still needs to 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

    the function of this Selector is value generation. By default, EF Core’s built-in ValueGeneratorSelector only knows how to generate: Guid, string, byte[]. It does not generate int identities unless a provider supplies that behavior.

Top comments (0)