Entity Framework Core نگاشتهای Fluent API را در کلاسهای جداگانهای که IEntityTypeConfiguration<TEntity> را پیادهسازی میکنند، پیشنهاد میدهد. این رویکرد مقیاسپذیر است، اما برای هر configuration جدید باید بهصورت دستی modelBuilder.ApplyConfiguration(...) را فراخوانی کنید. در پروژههای بزرگ — و بهویژه وقتی چند DbContext دارید — فراموش کردن این کار آسان است.
این نوشته یک helper کوچک نشان میدهد که configurationها را در runtime پیدا و اعمال میکند، نتیجه را cache میکند و در صورت نیاز آنها را برای هر context فیلتر میکند.
چرا ثبت خودکار؟
دو انگیزه رایج:
- کمتر boilerplate — کلاس
IEntityTypeConfiguration<T>جدید اضافه میکنید و خودکار شناسایی میشود؛ نیازی به ویرایشOnModelCreatingنیست. - چند context — میتوانید یک
DbContextسبک برای runtime و یک context جدا برای migration (یا contextهای تخصصی دیگر) داشته باشید، بدون تکرار منطق ثبت. این کار میتواند ساختDbContextرا سبکتر کند وقتی runtime فقط به زیرمجموعهای از mappingها نیاز دارد.
Reflection ابزار مناسبی است. EF Core هنگام ساخت model کار زیادی انجام میدهد، بنابراین هزینه یکباره scan کردن assembly معمولاً ناچیز است — بهویژه اگر delegate را compile و cache کنید، همانطور که پایینتر آمده.
Applier
این helper یکبار Action<ModelBuilder> میسازد و در هر فراخوانی OnModelCreating از آن استفاده میکند:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using Microsoft.EntityFrameworkCore;
public class EntityConfigurationsApplier
{
public static Action<ModelBuilder> CreateAutoApplier(Type targetContextType)
{
var configurations = targetContextType.Assembly
.GetTypes()
.Where(t => t.GetInterfaces()
.Any(i => i.IsGenericType &&
i.GetGenericTypeDefinition() ==
typeof(IEntityTypeConfiguration<>)))
.ToArray();
var applyConfigMethod = typeof(ModelBuilder)
.GetMethods(BindingFlags.Public | BindingFlags.Instance)
.Single(m =>
m.Name == nameof(ModelBuilder.ApplyConfiguration) &&
m.IsGenericMethodDefinition);
var modelBuilderParameter = Expression.Parameter(typeof(ModelBuilder), "modelBuilder");
var commands = new List<Expression>();
foreach (var config in configurations)
{
var entityType = config.GetInterfaces()
.Single(i => i.IsGenericType &&
i.GetGenericTypeDefinition() ==
typeof(IEntityTypeConfiguration<>))
.GetGenericArguments()
.Single();
var target = applyConfigMethod.MakeGenericMethod(entityType);
var instance = Expression.New(config);
var call = Expression.Call(modelBuilderParameter, target, instance);
commands.Add(call);
}
var body = Expression.Block(commands);
var lambda = Expression.Lambda<Action<ModelBuilder>>(body, modelBuilderParameter);
return lambda.Compile();
}
}
نحوه کار
CreateAutoApplier نوع DbContext را میگیرد، assembly آن را برای کلاسهای IEntityTypeConfiguration<> اسکن میکند و یک delegate کامپایلشده میسازد که ApplyConfiguration را برای هر کدام فراخوانی میکند.
بهجای حلقه reflection ساده از expression tree استفاده شده تا delegate یکبار compile شود و بعد ارزان اجرا شود. یک foreach با Activator.CreateInstance هم کار میکند؛ compile کردن expression از reflection مکرر در هر model build — وقتی delegate cache نشده — جلوگیری میکند.
فیلتر configuration برای هر context
وقتی چند context یک assembly را به اشتراک میگذارند، هر configuration نباید روی همه contextها اعمال شود. یک attribute این موضوع را شفاف میکند:
1
2
3
4
5
6
7
8
9
10
[AttributeUsage(AttributeTargets.Class, AllowMultiple = true)]
public class ApplyOnContextAttribute : Attribute
{
public Type TargetContext { get; }
public ApplyOnContextAttribute(Type targetContext)
{
TargetContext = targetContext;
}
}
قبل از ساخت expressionها، نوعهای پیدا شده را فیلتر کنید:
1
2
3
4
configurations = configurations
.Where(t => t.GetCustomAttributes<ApplyOnContextAttribute>()
.Any(a => a.TargetContext == targetContextType))
.ToArray();
مثال configuration مشترک بین runtime و migration:
1
2
3
4
5
6
7
8
9
10
[ApplyOnContext(typeof(DataContext))]
[ApplyOnContext(typeof(MigrationContext))]
public class IdentityUserEntityConfiguration
: IEntityTypeConfiguration<IdentityUserEntity>
{
public void Configure(EntityTypeBuilder<IdentityUserEntity> builder)
{
// mapping rules
}
}
اتصال به DbContext
delegate کامپایلشده را در یک فیلد static readonly cache کنید تا discovery و compilation فقط یکبار در هر application domain انجام شود:
1
2
3
4
5
6
7
8
9
10
11
12
public class DataContext : DbContext
{
private static readonly Lazy<Action<ModelBuilder>> EntityConfigurationApplier =
new Lazy<Action<ModelBuilder>>(
() => EntityConfigurationsApplier.CreateAutoApplier(typeof(DataContext)));
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
EntityConfigurationApplier.Value(modelBuilder);
}
}
جمعبندی
- نگاشتهای Fluent را مثل همیشه در کلاسهای
IEntityTypeConfiguration<T>قرار دهید. EntityConfigurationsApplierآنها را پیدا و اعمال کند، با فیلتر اختیاری[ApplyOnContext].- delegate را compile و cache کنید تا هزینه ثبت فقط یکبار پرداخت شود.
این الگو در پروژههای EF Core چند context که فراخوانی دستی ApplyConfiguration دردسر maintenance بود، برای من خوب جواب داده است.