Roslyn & EF Core: membangun DbContext dalam runtime

Entity Framework Core dapat menghasilkan kode model dan DbContext untuk database yang ada menggunakan perintah konsol dotnet ef dbcontext scaffold. Mengapa kita tidak mencoba membuat DbContext di runtime?


Dalam artikel ini saya akan memberi tahu cara di runtime di aplikasi saya:


  1. Hasilkan kode DbContext menggunakan EF Core.
  2. Kompilasi dalam memori menggunakan Roslyn.
  3. Unduh rakitan yang dihasilkan.
  4. Buat instance dari DbContext yang dihasilkan.
  5. Bekerja dengan database melalui DbContext yang diterima.

Contoh tersedia di github.


Persiapan untuk bekerja


Platform untuk aplikasi adalah NET Core 3.1.3.


Sebagai contoh, saya akan menggunakan database MS SQL, kita memerlukan string koneksi untuk itu. Namun, pendekatan itu sendiri berfungsi untuk setiap mesin basis data yang didukung oleh EF Core (saya menguji sqlite dan postregs).


Mari kita buat aplikasi konsol, tambahkan paket yang diperlukan untuk itu:


<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>

Pembuat kode ada dalam paket Microsoft.EntityFrameworkCore.Design. Jika Anda menginstal paket ini melalui konsol manajer paket, kode berikut akan ditambahkan ke * .csproj Anda:


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

Kode ini mengatakan [ 1 ] bahwa paket hanya diperlukan selama pengembangan, dan tidak digunakan dalam runtime. Kami akan membutuhkannya di runtime, jadi kami perlu mengimpor paket seperti ini:


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

1. Hasilkan kodenya


Di Entity Framework Core, kode menghasilkan layanan IReverseEngineerScaffolder:


interface IReverseEngineerScaffolder
{
    ScaffoldedModel ScaffoldModel(
        string connectionString, 

        //   
        DatabaseModelFactoryOptions databaseOptions, 

        //   
        ModelReverseEngineerOptions modelOptions, 

        //  ,  
        ModelCodeGenerationOptions codeOptions);
}

Cara termudah untuk membuat turunan dari layanan ini adalah membuat Dependency Injection Container untuknya.


:


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] Mendukung Assemblies Dynamic Assemblies # 473


[5] Cara menggunakan dan men-debug unloadability assembly di .NET Core


Terima kasih atas waktu Anda!


Saya memiliki beberapa topik lagi yang ingin saya tulis. Saya akan berterima kasih jika Anda menunjukkan dalam topik survei yang menarik bagi Anda.


All Articles