Roslyn & EF Core: بناء DbContext في وقت التشغيل

يمكن لـ Entity Framework Core إنشاء رمز نموذج و DbContext لقاعدة بيانات موجودة باستخدام أمر وحدة التحكم dotnet ef dbcontext scaffold. لماذا لا نحاول إنشاء DbContext في وقت التشغيل؟


في المقالة سأخبرك كيف في وقت التشغيل في طلبي:


  1. إنشاء رمز DbContext باستخدام EF Core.
  2. تجميعه في الذاكرة باستخدام Roslyn.
  3. قم بتنزيل التجميع الناتج.
  4. إنشاء مثيل من DbContext الذي تم إنشاؤه.
  5. العمل مع قاعدة البيانات من خلال DbContext المتلقاة.

مثال متاح على جيثب.


التحضير للعمل


سيكون النظام الأساسي للتطبيق هو NET Core 3.1.3.


على سبيل المثال ، سأستخدم قاعدة بيانات MS SQL ، نحتاج إلى سلسلة اتصال بها. ومع ذلك ، يعمل النهج نفسه مع أي محرك قاعدة بيانات يدعمه EF Core (لقد اختبرت sqlite و postregs).


لنقم بإنشاء تطبيق وحدة تحكم ، أضف الحزم الضرورية إليه:


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

منشئ الرمز في الحزمة Microsoft.EntityFrameworkCore.Design. إذا قمت بتثبيت هذه الحزمة من خلال وحدة تحكم مدير الحزم ، فستتم إضافة التعليمات البرمجية التالية إلى * .csproj الخاص بك:


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

يقول هذا الرمز [ 1 ] أن الحزمة مطلوبة فقط أثناء التطوير ، ولا يتم استخدامها في وقت التشغيل. سنحتاج إليها في وقت التشغيل ، لذلك نحتاج إلى استيراد الحزمة مثل هذه:


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

1. إنشاء رمز


في Core Ent Framework Framework ، يُنشئ الرمز خدمة IReverseEngineerScaffolder:


interface IReverseEngineerScaffolder
{
    ScaffoldedModel ScaffoldModel(
        string connectionString, 

        //   
        DatabaseModelFactoryOptions databaseOptions, 

        //   
        ModelReverseEngineerOptions modelOptions, 

        //  ,  
        ModelCodeGenerationOptions codeOptions);
}

أسهل طريقة لإنشاء مثيل من هذه الخدمة هي إنشاء حاوية حقن التبعية لها.


:


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] دعم التجميعات الديناميكية القابلة للتحصيل # 473


[5] كيفية استخدام وتصحيح عدم تحميل التجميع في .NET Core


شكرا لك على وقتك!


لدي بعض الموضوعات الأخرى التي أود أن أكتب عنها. وسأكون ممتنا إذا أشرت في مواضيع الاستطلاع التي تهمك.


All Articles