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:
- Generate DbContext code using EF Core.
- Compile it in memory using Roslyn.
- Download the resulting assembly.
- Create an instance of the generated DbContext.
- 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
. , .
PostgreSQLprivate 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>();
sqliteprivate 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",
SuppressConnectionStringWarning = true
};
,
ScaffoldedModel scaffoldedModelSources =
scaffolder.ScaffoldModel(onnectionString, dbOpts, modelOpts, codeGenOpts);
:
class ScaffoldedModel
{
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))
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,
, 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.