EF Core precompiled modelCompiled Model
Foreword
Recently I am still fighting with npgsql
and EF Core
. Since EF Core
does not support AOT yet, it is used in AOT applications. When using EF Core, a question will be prompted:
Listening to this, it seems that using Compiled Model
can solve the problem, so I studied this function of EF Core again.
In EF Core, models are built based on entity classes and configuration, and by default, EF Core builds the model every time a new DbContext
instance is created. This may cause performance issues for applications that require frequent creation of DbContext
instances.
The precompiled model of Entity Framework Core (EF Core) provides an optimization. This feature was added for the first time in EF Core 6 preview 5, which allows designers to precompile the model to avoid subsequent execution. Dynamically generate models at query time.
Advantages of precompiled models
- Performance improvements: By precompiling models, you can reduce application startup overhead, especially for large models.
The startup time here refers to the first startup time of
DbContext
. Due to the delayed query mechanism, generally DbContext will not complete startup when a new object is created, but when inserting or querying is performed for the first time. Complete this process.
Refer to the image below (from reference 1):
Obviously, as the size of the model increases, the startup time increases linearly; however, after using the precompiled model, the startup time is basically independent of the model size and remains at an extremely low level.
- Consistency: Ensures that each
DbContext
instance uses the same model configuration.
Use precompiled model
- Generate compiled model:
Use the EF Core command line tool, command:
dotnet ef dbcontext optimize
This will generate a precompiled model of DbContext
. I only have one POCO class that generates 3 files and the class name is the file name.
[DbContext(typeof(DataContext))]
public partial class DataContextModel : RuntimeModel
{
static DataContextModel()
{
var model = new DataContextModel();
model.Initialize();
model.Customize();
_instance = model;
}
private static DataContextModel _instance;
public static IModel Instance => _instance;
partial void Initialize();
partial void Customize();
}
public partial class DataContextModel
{
partial void Initialize()
{
var deviceDatum = DeviceDatumEntityType.Create(this);
DeviceDatumEntityType.CreateAnnotations(deviceDatum);
AddAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn);
AddAnnotation("ProductVersion", "8.0.0-rc.2.23480.1");
AddAnnotation("Relational:MaxIdentifierLength", 63);
AddRuntimeAnnotation("Relational:RelationalModel", CreateRelationalModel());
}
private IRelationalModel CreateRelationalModel()
{
// There are a lot of codes describing types here, so I won’t write them all to save space.
var relationalModel = new RelationalModel(this);
var deviceDatum = FindEntityType("AspireSample.DeviceDatum")!;
var defaultTableMappings = new List<TableMappingBase>();
deviceDatum.SetRuntimeAnnotation("Relational:DefaultMappings", defaultTableMappings);
....
return relationalModel.MakeReadOnly();
}
}
internal partial class DeviceDatumEntityType
{
public static RuntimeEntityType Create(RuntimeModel model, RuntimeEntityType baseEntityType = null)
{
var runtimeEntityType = model.AddEntityType(
"AspireSample.DeviceDatum",
typeof(DeviceDatum),
baseEntityType);
var id = runtimeEntityType.AddProperty(
"Id",
typeof(string),
propertyInfo: typeof(DeviceDatum).GetProperty("Id", BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly),
fieldInfo: typeof(DeviceDatum).GetField("k__BackingField", BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly),
afterSaveBehavior: PropertySaveBehavior.Throw);
id.TypeMapping = StringTypeMapping.Default.Clone(
comparer: new ValueComparer((string v1, string v2) => v1 == v2,
(string v) => v.GetHashCode(),
(string v) => v),
keyComparer: new ValueComparer(
(string v1, string v2) => v1 == v2,
(string v) => v.GetHashCode(),
(string v) => v),
providerValueComparer: new ValueComparer(
(string v1, string v2) => v1 == v2,
(string v) => v.GetHashCode(),
(string v) => v),
mappingInfo: new RelationalTypeMappingInfo(
dbType: System.Data.DbType.String));
...
var key = runtimeEntityType.AddKey(
new[] { id });
runtimeEntityType.SetPrimaryKey(key);
return runtimeEntityType;
}
public static void CreateAnnotations(RuntimeEntityType runtimeEntityType)
{
runtimeEntityType.AddAnnotation("Relational:FunctionName", null);
runtimeEntityType.AddAnnotation("Relational:Schema", null);
runtimeEntityType.AddAnnotation("Relational:SqlQuery", null);
runtimeEntityType.AddAnnotation("Relational:TableName", "devicedata");
runtimeEntityType.AddAnnotation("Relational:ViewName", null);
runtimeEntityType.AddAnnotation("Relational:ViewSchema", null);
Customize(runtimeEntityType);
}
static partial void Customize(RuntimeEntityType runtimeEntityType);
}
As you can see, the optimization tool helped us generate a lot of code, especially the code related to type description. Therefore, if we modify the model, we must re-execute the corresponding generation instructions. .
- Modify DbContext:
Modify yourDbContext
class to use this precompiled model.
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
if (!optionsBuilder.IsConfigured)
{
//Specify the use of compiled models
optionsBuilder.UseModel(CompiledModels.MyCompiledModel.Instance);
}
}
Weigh the pros and cons
Core advantages:
- Improve startup speed, especially for
DbContext
with many entity types.
Disadvantages:
- Global query filtering,
Lazy loading proxies
,Change tracking proxies
and customIModelCacheKeyFactory
are not supported. - The optimization code must be regenerated every time the model is modified.
There are many things that are not supported, and it is very troublesome to regenerate the model every time. Therefore, if the startup speed is not really slow, it is not recommended to use.
Postscript
I was still prompted with the same error after using EF Core’s Compiled Model. Later I found that the error was caused by Reflection-related classes, not EF Core-related classes. So the concept of Compiled Model mentioned in the error is different from that of EF Core. It should mean that AOT does not support dynamic loading in reflection and needs to be compiled in advance. EF Core is not quite ready yet, so, to reiterate, AOT is not supported in EF Core 8 yet.
reference
- Announcing Entity Framework Core 6.0 Preview 5: Compiled Models – .NET Blog (microsoft.com)
- Advanced Performance Topics | Microsoft Learn