Roslyn & EF Core: building a DbContext in runtime

Entity Framework Core can generate model code and DbContext for an existing database using a console command dotnet ef dbcontext scaffold. Why don't we try generating a DbContext in runtime?


In the article I will tell how in runtime in my application:


  1. Generate DbContext code using EF Core.
  2. Compile it in memory using Roslyn.
  3. Download the resulting assembly.
  4. Create an instance of the generated DbContext.
  5. Work with the database through the received DbContext.

An example is available on github.


Preparation for work


The platform for the application will be NET Core 3.1.3.


For example, I will use the MS SQL database, we need a connection string to it. However, the approach itself works for any database engine supported by EF Core (I tested sqlite and postregs).


Let's create a console application, add the necessary packages to it:


<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>netcoreapp3.1</TargetFramework>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="Microsoft.CodeAnalysis" Version="3.5.0" />
    <PackageReference Include="Microsoft.EntityFrameworkCore" Version="3.1.3" />
    <PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="3.1.3" />
    <PackageReference Include="Microsoft.EntityFrameworkCore.Proxies" Version="3.1.3" />
    <PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="3.1.3" />
    <PackageReference Include="Bricelam.EntityFrameworkCore.Pluralizer" Version="1.0.0" />
  </ItemGroup>

</Project>

The code generator is in the package Microsoft.EntityFrameworkCore.Design. If you install this package through the package manager console, the following code will be added to your * .csproj:


<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="3.1.3">
  <PrivateAssets>all</PrivateAssets>
  <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
</PackageReference>

This code says [ 1 ] that the package is needed only during development, and is not used in runtime. We will need it in runtime, so we need to import the package like this:


<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="3.1.3" />

1. Generate the code


In the Entity Framework Core, the code generates a service IReverseEngineerScaffolder:


interface IReverseEngineerScaffolder
{
    ScaffoldedModel ScaffoldModel(
        string connectionString, 

        //   
        DatabaseModelFactoryOptions databaseOptions, 

        //   
        ModelReverseEngineerOptions modelOptions, 

        //  ,  
        ModelCodeGenerationOptions codeOptions);
}

The easiest way to create an instance of this service is to create a Dependency Injection Container for it.


:


IReverseEngineerScaffolder CreateMssqlScaffolder() => 
    new ServiceCollection()
        .AddEntityFrameworkSqlServer()
        .AddLogging()
        .AddEntityFrameworkDesignTimeServices()
        .AddSingleton<LoggingDefinitions, SqlServerLoggingDefinitions>()
        .AddSingleton<IRelationalTypeMappingSource, SqlServerTypeMappingSource>()
        .AddSingleton<IAnnotationCodeGenerator, AnnotationCodeGenerator>()
        .AddSingleton<IDatabaseModelFactory, SqlServerDatabaseModelFactory>()
        .AddSingleton<IProviderConfigurationCodeGenerator, SqlServerCodeGenerator>()
        .AddSingleton<IScaffoldingModelFactory, RelationalScaffoldingModelFactory>()
        .AddSingleton<IPluralizer, Bricelam.EntityFrameworkCore.Design.Pluralizer>()
        .BuildServiceProvider()
        .GetRequiredService<IReverseEngineerScaffolder>();

IPluralizer . , .


PostgreSQL
private IReverseEngineerScaffolder CreatePostgreScaffolder() => 
    new ServiceCollection()
        .AddEntityFrameworkNpgsql()
        .AddLogging()
        .AddEntityFrameworkDesignTimeServices()
        .AddSingleton<LoggingDefinitions, NpgsqlLoggingDefinitions>()
        .AddSingleton<IRelationalTypeMappingSource, NpgsqlTypeMappingSource>()
        .AddSingleton<IAnnotationCodeGenerator, AnnotationCodeGenerator>()
        .AddSingleton<IDatabaseModelFactory, NpgsqlDatabaseModelFactory>()
        .AddSingleton<IProviderConfigurationCodeGenerator, NpgsqlCodeGenerator>()
        .AddSingleton<IScaffoldingModelFactory, RelationalScaffoldingModelFactory>()
        .AddSingleton<IPluralizer, Bricelam.EntityFrameworkCore.Design.Pluralizer>()
        .BuildServiceProvider()
        .GetRequiredService<IReverseEngineerScaffolder>();

sqlite
private IReverseEngineerScaffolder CreateSqliteScaffolder() => 
    new ServiceCollection()
        .AddEntityFrameworkSqlite()
        .AddLogging()
        .AddEntityFrameworkDesignTimeServices()
        .AddSingleton<LoggingDefinitions, SqliteLoggingDefinitions>()
        .AddSingleton<IRelationalTypeMappingSource, SqliteTypeMappingSource>()
        .AddSingleton<IAnnotationCodeGenerator, AnnotationCodeGenerator>()
        .AddSingleton<IDatabaseModelFactory, SqliteDatabaseModelFactory>()
        .AddSingleton<IProviderConfigurationCodeGenerator, SqliteCodeGenerator>()
        .AddSingleton<IScaffoldingModelFactory, RelationalScaffoldingModelFactory>()
        .AddSingleton<IPluralizer, Bricelam.EntityFrameworkCore.Design.Pluralizer>()
        .BuildServiceProvider()
        .GetRequiredService<IReverseEngineerScaffolder>();

:


var scaffolder = CreateMssqlScaffolder();

:


//     
var dbOpts = new DatabaseModelFactoryOptions();

//       
var modelOpts = new ModelReverseEngineerOptions(); 

var codeGenOpts = new ModelCodeGenerationOptions()
{
    //   
    RootNamespace = "TypedDataContext",
    ContextName = "DataContext",
    ContextNamespace = "TypedDataContext.Context",
    ModelNamespace = "TypedDataContext.Models",

    //        ,
    //       runtime
    SuppressConnectionStringWarning = true
};

,


ScaffoldedModel scaffoldedModelSources =    
    scaffolder.ScaffoldModel(onnectionString, dbOpts, modelOpts, codeGenOpts);

:


//  
class ScaffoldedModel
{
    //   DbContext
    public virtual ScaffoldedFile ContextFile { get; set; }

    //     
    public virtual IList<ScaffoldedFile> AdditionalFiles { get; }
}

Lazy Loading, UseLazyLoadingProxies() :


var contextFile = scaffoldedModelSources.ContextFile.Code
    .Replace(".UseSqlServer", ".UseLazyLoadingProxies().UseSqlServer");

, , .


2. Roslyn


Roslyn, :


CSharpCompilation GenerateCode(List<string> sourceFiles)
{
    var options = CSharpParseOptions.Default.WithLanguageVersion(LanguageVersion.CSharp8);
    var parsedSyntaxTrees = sourceFiles
        .Select(f => SyntaxFactory.ParseSyntaxTree(f, options));

    return CSharpCompilation.Create($"DataContext.dll",
        parsedSyntaxTrees,
        references: GetCompilationReferences(),
        options: new CSharpCompilationOptions(
            OutputKind.DynamicallyLinkedLibrary,
            optimizationLevel: OptimizationLevel.Release));
}

, :


List<MetadataReference> CompilationReferences()
{
    var refs = new List<MetadataReference>();

    //  ,        ,
    //      
    var referencedAssemblies = Assembly.GetExecutingAssembly().GetReferencedAssemblies();
    refs.AddRange(referencedAssemblies.Select(a =>
        MetadataReference.CreateFromFile(Assembly.Load(a).Location)));

    //  ,    :
    refs.Add(MetadataReference.CreateFromFile(
        typeof(object).Assembly.Location));
    refs.Add(MetadataReference.CreateFromFile(
        Assembly.Load("netstandard, Version=2.0.0.0").Location));
    refs.Add(MetadataReference.CreateFromFile(
        typeof(System.Data.Common.DbConnection).Assembly.Location));
    refs.Add(MetadataReference.CreateFromFile(
        typeof(System.Linq.Expressions.Expression).Assembly.Location))

    //     LazyLoading,     :
    // refs.Add(MetadataReference.CreateFromFile(
    //     typeof(ProxiesExtensions).Assembly.Location));

    return refs;
}

:


MemoryStream peStream = new MemoryStream();
EmitResult emitResult = GenerateCode(sourceFiles).Emit(peStream);

, emitResult.Success true, peStream .


- , . emitResult .


3.


, , :


var assemblyLoadContext = new AssemblyLoadContext("DbContext", isCollectible);

var assembly = assemblyLoadContext.LoadFromStream(peStream);

isCollectible. , . NET Core 3 [2].


, , . [5]:


assemblyLoadContext.Unload();

LazyLoading, EF Core Proxy- , DefaultLoadContext, collectible. NonCollectible- collectible-, collectible LazyLoading. [3][4], .


4. DbContext


, DbContext.


var type = assembly.GetType("TypedDataContext.Context.DataContext");

var constructor = type.GetConstructor(Type.EmptyTypes);

DbContext dynamicContext = (DbContext)constructor.Invoke(null);

, :


public static class DynamicContextExtensions
{
    public static IQueryable Query(this DbContext context, string entityName) =>
        context.Query(context.Model.FindEntityType(entityName).ClrType);

    static readonly MethodInfo SetMethod =
        typeof(DbContext).GetMethod(nameof(DbContext.Set), 1, Array.Empty<Type>()) ??
        throw new Exception($"Type not found: DbContext.Set");

    public static IQueryable Query(this DbContext context, Type entityType) =>
        (IQueryable)SetMethod.MakeGenericMethod(entityType)?.Invoke(context, null) ??
        throw new Exception($"Type not found: {entityType.FullName}");
}

Reflection Set<> DbContext.


, :


foreach (var entityType in dynamicContext.Model.GetEntityTypes())
{
    var items = (IQueryable<object>)dynamicContext.Query(entityType.Name);

    Console.Write($"Entity type: {entityType.ClrType.Name} ");
    Console.WriteLine($"contains {items.Count()} items");
}


, , , .


ASP.NET Core Blazor , , SQL, MS SQL, PostrgreSQL, sqlite,





c#




, EF Core DbContext runtime. NET Core — collectible assemblies, , .



github.


[1] (PackageReference)


[2] Collectible assemblies in .NET Core 3.0


[3] Lazy loading proxy doesn't support entity inside collectible assembly #18272


[4] Support Collectible Dynamic Assemblies # 473


[5] How to use and debug assembly unloadability in .NET Core


Thank you for your time!


I have a few more topics that I would like to write about. I would be grateful if you indicate in the survey topics that would be of interest to you.


All Articles