EntityFrameworkCore Seeding 组件





5.00/5 (1投票)
向EntityFrameworkCore添加Seeding Framework,该框架可以集成到dotnet core Web应用程序或WebAPI中
引言
EntityFrameworkCore 相较于过去构建模型层并对其进行描述的时代,已经有了巨大的进步。 我发现一个普遍的不足之处,就像大多数人一样,是无法实现数据种子(无论是查找表还是某些枚举(标志 - 被标志枚举的各种排列组合仍在进行中))。 我将在后续文章中讨论 DbEnum 的用例。 我见过许多文章中开发者使用迁移来填充数据。 恕我直言,这只是一个更大的问题的变通方法。 在接下来的代码片段中,这些代码是我编写并在多个项目中复用的,我们将努力解决这个问题。
要理解这一点,最低要求是了解如何模拟 MS 团队使用的类,然后以通用的方式开始注入/使用它们来满足我们的需求。
这绝非完美,欢迎任何反馈。 (即使您想让我将其发布到 GitHub。)
请注意,我尽量采用最简洁的设计,不向项目中添加过多包。
这是一篇相当长的文章,请耐心阅读代码。
背景
示例是使用 VS 2017 完成的,大多数项目使用 netstandard1.6,Web API 项目使用 netcoreapp1.1。
使用代码
请检查 appsettings.development.cs 中的数据库连接字符串。
EntityFrameworkCore
我发现 DbContext 的问题在于它只是一个类,问题在于:为什么不有一个接口? 这个问题我纠结了很多年。 嗯,很简单,我们获取 DbContext 并确定我们需要从中获得什么,然后构建一个接口。 现在的问题是:为什么? 嗯,我不想将 EntityFrameworkCore 包加载到我拥有的每个库中,为什么不将其放在一个简单的程序集中,然后让项目依赖关系自行解决。
IDbContext
public interface IDbContext
{
DbSet<TDomainObject> Set<TDomainObject>() where TDomainObject : class;
int SaveChanges();
Task<int> SaveChangesAsync(CancellationToken cancellationToken);
void Dispose();
EntityEntry<TEntity> Add<TEntity>(TEntity entity) where TEntity : class;
EntityEntry<TEntity> Update<TEntity>(TEntity entity) where TEntity : class;
EntityEntry<TEntity> Attach<TEntity>(TEntity entity) where TEntity : class;
ChangeTracker ChangeTracker { get; }
EntityEntry<TEntity> Entry<TEntity>(TEntity entity) where TEntity : class;
}
该接口将用于您创建的每个 ContextModel,因为 DbContext 与 IDbContext 接口具有相同的签名,所以它与 EntityFrameworkCore 兼容。
附加接口
ISeedData & IDbEnum(后者将用于构建类以反映 DbContext 和迁移操作的枚举)
public interface ISeedData
{
Task Seed(string environmentName);
void SeedEnums(string environmentName); //although this is available, please check the next article
}
BaseSeeding.cs 是一个基类,它公开了大量重载方法,用于从已实现/派生的类中填充数据。
AddSeeding 扩展方法,用于 IServiceCollection
这将用于 Startup.cs,将 BaseSeeding 的实现作为 ISeedData 进行挂钩。
public static class SeedingCollectionExtensions
{
public static IServiceCollection AddSeeding<TSeeding>(this IServiceCollection serviceCollection)
where TSeeding : ISeedData
{
ServiceCollectionDescriptorExtensions.TryAdd(serviceCollection, new ServiceDescriptor(typeof(TSeeding), typeof(TSeeding), ServiceLifetime.Singleton));
return serviceCollection;
}
}
实际的基类实现
Action<TEntity> 允许您返回对象以在保存数据之前执行“预处理”。 典型用例是读取父记录,然后设置值或执行其他数据完整性检查。 本文不包括如何使用 Action<T>。 在此示例中,我将说明如何使用它来设置 Modified 日期属性。
非常重要:此处枚举的使用是为了排除 0 的 int 序数。 如果您希望 Id 字段包含 0,请删除该行(稍后可能会更改为使用 bool 或其他指示符进行重载)。 更新默认设置为 true,因为代码应该是数据的唯一真正来源。
public abstract class BaseSeeding<TContext> : ISeedData where TContext : DbContext, IDbContext
{
public IServiceProvider Provider { get; }
public BaseSeeding(IServiceProvider provider)
{
Provider = provider;
}
public async Task AddOrUpdateAsync<TEntity>(IEnumerable<TEntity> entities, params Func<TEntity,
object>[] propertiesToMatch)
where TEntity : class
{
await InternalAddOrUpdateAsync(entities);
}
public async Task AddOrUpdateAsync<TEntity>(IEnumerable<TEntity> entities, Action<TEntity> action,
params Func<TEntity, object>[] propertiesToMatch)
where TEntity : class
{
await InternalAddOrUpdateAsync(entities, action, true, propertiesToMatch);
}
public async Task AddAsync<TEntity>(IEnumerable<TEntity> entities, params Func<TEntity,
object>[] propertiesToMatch)
where TEntity : class
{
await InternalAddOrUpdateAsync(entities, false, propertiesToMatch);
}
public async Task AddAsync<TEntity>(IEnumerable<TEntity> entities, Action<TEntity> action,
params Func<TEntity, object>[] propertiesToMatch)
where TEntity : class
{
await InternalAddOrUpdateAsync(entities, action, false, propertiesToMatch);
}
public void AddOrUpdate<TEntity>(IEnumerable<TEntity> entities, params Func<TEntity,
object>[] propertiesToMatch)
where TEntity : class
{
InternalAddOrUpdate(entities, true, propertiesToMatch);
}
public void AddOrUpdate<TEntity>(IEnumerable<TEntity> entities, Action<TEntity> action,
params Func<TEntity, object>[] propertiesToMatch)
where TEntity : class
{
InternalAddOrUpdate(entities, action, true, propertiesToMatch);
}
public void Add<TEntity>(IEnumerable<TEntity> entities, params Func<TEntity,
object>[] propertiesToMatch)
where TEntity : class
{
InternalAddOrUpdate(entities, false, propertiesToMatch);
}
public void Add<TEntity>(IEnumerable<TEntity> entities, Action<TEntity> action,
params Func<TEntity, object>[] propertiesToMatch)
where TEntity : class
{
InternalAddOrUpdate(entities, action, false, propertiesToMatch);
}
private void InternalAddOrUpdate<TEntity>(IEnumerable<TEntity> entities, bool update = true,
params Func<TEntity, object>[] propertiesToMatch)
where TEntity : class
{
InternalAddOrUpdate(entities, null, update, propertiesToMatch);
}
private void InternalAddOrUpdate<TEntity>(IEnumerable<TEntity> entities, Action<TEntity> action,
bool update = true, params Func<TEntity, object>[] propertiesToMatch)
where TEntity : class
{
using (var serviceScope = Provider.GetRequiredService<IServiceScopeFactory>().CreateScope())
{
var context = serviceScope.ServiceProvider.GetService<TContext>();
InternalEntityState(entities, action, update, propertiesToMatch, context);
context.SaveChanges();
}
}
private async Task InternalAddOrUpdateAsync<TEntity>(IEnumerable<TEntity> entities,
bool update = true, params Func<TEntity, object>[] propertiesToMatch)
where TEntity : class
{
await InternalAddOrUpdateAsync(entities, null, update, propertiesToMatch);
}
private async Task InternalAddOrUpdateAsync<TEntity>(IEnumerable<TEntity> entities,
Action<TEntity> action, bool update = true, params Func<TEntity, object>[] propertiesToMatch)
where TEntity : class
{
using (var serviceScope = Provider.GetRequiredService<IServiceScopeFactory>().CreateScope())
{
var context = serviceScope.ServiceProvider.GetService<TContext>();
InternalEntityState(entities, action, update, propertiesToMatch, context);
await context.SaveChangesAsync();
}
}
private static void InternalEntityState<TEntity>(IEnumerable<TEntity> entities,
Action<TEntity> action, bool update, Func<TEntity, object>[] propertiesToMatch, TContext context)
where TEntity : class
{
var existing = context.Set<TEntity>().AsNoTracking().ToList(); //ajb: do as no tracking to avoid errors
foreach (var item in entities)
{
var match = FindMatch(existing, item, propertiesToMatch);
var citem = context.Entry(item);
if (match != null)
citem.Property("Id").CurrentValue = context.Entry(match).Property("Id").CurrentValue;
context.Entry(item).State = update
? (match != null ? EntityState.Modified : EntityState.Added)
: (match != null ? EntityState.Unchanged : EntityState.Added);
if (item is IModifyObject)
{
if (((IModifyObject)item).Modified == DateTime.MinValue)
((IModifyObject)item).Modified = DateTime.Now;
}
action?.Invoke(citem.Entity);
}
}
private static TEntity FindMatch<TEntity>(List<TEntity> existing, TEntity item, params Func<TEntity,
object>[] propertiesToMatch)
{
return existing.FirstOrDefault(g =>
{
var r = true;
foreach (var ptm in propertiesToMatch)
{
var rptm = ptm(g);
if (rptm != null)
r &= ptm(g).Equals(ptm(item));
}
return r;
});
}
public Type[] GetDbEnums()
{
var types = Provider.GetService<TContext>().Model.GetEntityTypes().Select(t => t.ClrType);
var result = types.Where(l => typeof(IDbEnum).GetTypeInfo().IsAssignableFrom(l));
return result.ToArray();
}
public void SeedEnum(params Type[] types)
{
types.ToList().ForEach(t => InternalTypedSeedEnum(t));
}
public void SeedEnum<TClass>(bool update = true) where TClass : class, IDbEnum
{
InternalSeedEnum<TClass>(update);
}
private void InternalSeedEnum<TClass>(bool update = true) where TClass : class, IDbEnum
{
var type = typeof(TClass).GetTypeInfo();
while (type.BaseType != null)
{
type = type.BaseType.GetTypeInfo();
if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(DbEnum<>))
{
var arg = (type.GetGenericArguments()[0]).GetTypeInfo();
var instance = typeof(TClass).GetTypeInfo().GetConstructor(new Type[] { arg.AsType() });
var list = new List<TClass>();
if (arg.IsEnum)
{
var values = Enum.GetValues(arg.AsType());
foreach (var val in values)
{
if ((int)Convert.ChangeType(val, typeof(int)) == 0) //this is to exclude the 0 index
continue;
var obj = instance.Invoke(new[] { val });
list.Add((TClass)obj);
}
InternalAddOrUpdate<TClass>(list, update, i => i.Id);
}
}
}
}
private void InternalTypedSeedEnum(Type type, bool update = true)
{
var mi = GetType().GetTypeInfo().BaseType.GetTypeInfo().GetMethod("InternalSeedEnum",
BindingFlags.NonPublic | BindingFlags.Instance);
var gm = mi.MakeGenericMethod(type);
gm.Invoke(this, new object[] { update });
}
public abstract Task Seed(string environmentName);
public virtual void SeedEnums(string environmentName)
{
SeedEnum(GetDbEnums());
}
}
关于“框架组件”的内容就到这里,接下来是如何使用。
1. 我们来构建要使用的类。
public enum TitleEnum
{
[Display(Description="Mister")]
Mr = 1,
Mrs,
Miss,
[Display(Description="Doctor")]
Dr,
Ds,
[Display(Description="Professor")]
Prof
}
[Table(nameof(Country))]
public class Country : BaseDomainObject<Int16>
{
public string Description { get; set; }
public string Alpha2Code { get; set; }
}
[Table(nameof(Customer))]
public class Customer : BaseDomainObject<int>
{
public TitleEnum Title { get; set; }
[MaxLength(100)]
public string Firstname { get; set; }
[MaxLength(100)]
public string Surname { get; set; }
[Required]
public Int16 CountryId { get; set; }
[ForeignKey(nameof(CountryId))]
public Country Country { get; set; }
public IList<Order> Orders { get; set; }
}
[Table(nameof(Order))]
public class Order : BaseDomainObject<int>
{
[MaxLength(8)]
public string Number { get; set; }
public int CustomerId { get; set; }
[ForeignKey(nameof(CustomerId))]
public Customer Customer { get; set; }
}
[Table(nameof(OrderItem))]
public class OrderItem : BaseDomainObject<long>
{
[Required]
public int OrderId { get; set; }
[Required]
public int ProductId { get; set; }
[Range(1, 100)]
public int Qty { get; set; }
[ForeignKey(nameof(OrderId))]
public Order Order { get; set; }
[ForeignKey(nameof(ProductId))]
public Product Product { get; set; }
}
[Table(nameof(Product))]
public class Product : BaseDomainObject<int>
{
[MaxLength(100)]
public string Description { get; set; }
[Range(1, 9999999)]
public decimal Cost { get; set; }
}
2. 实现 Title 类用于 DbEnum<TitleEnum>。
请注意: 枚举和类之间没有隐式操作,也无法通过类型规范完成,需要为每个派生的 DbEnum<T> 类实现。
[Table(nameof(Title))]
public class Title : DbEnum<TitleEnum>
{
public Title() : this(default(TitleEnum)) { }
public Title(TitleEnum value) : base(value) { }
public static implicit operator Title(TitleEnum value)
{
return new Title(value);
}
public static implicit operator TitleEnum(Title value)
{
return value.ToEnum();
}
}
3. 数据库上下文,例如 SampleContext。
请注意,在第一个部分类中,我从 DbContext 派生,然后包含 IDbContext,因为 DbContext 已部分提取到 IDbContext,所以我们不必实现该接口。
如果您在代码(如果已下载)中看不到第二个部分类,请展开第一个。
public partial class SampleContext : DbContext, IDbContext { public SampleContext(DbContextOptions<SampleContext> options) : base(options) { } public virtual DbSet<Country> Countries { get; set; } public virtual DbSet<Customer> Customers { get; set; } public virtual DbSet<Order> Orders { get; set; } public virtual DbSet<OrderItem> OrderItem { get; set; } public virtual DbSet<Product> Products { get; set; } } public partial class SampleContext { public virtual DbSet<Title> Titles { get; set; } }
4. Add-Migration(下载文件中已完成)。
5. ERD。
一旦运行应用程序并构建 SQL ERD,就会发现由于类和属性已正确装饰,所有关系都已就位。
唯一缺少的是 Title 表,原因是我们希望查找数据存储在数据库中,而严格来说,它不是相关对象,它只是代码中的一个枚举。 客户表中的列不是 TitleId,而是 Title,其值为数字!
6. 添加种子源数据。
public static class Data
{
private static Dictionary<string, Country> _countries;
internal static Dictionary<string, Country> Countries
{
get
{
if (_countries != null)
return _countries;
var list = new List<Country>
{
new Country{Description = "British Indian Ocean Territory",Alpha2Code = "IO"},
new Country{Description = "British Virgin Islands",Alpha2Code = "VG"},
new Country{Description = "Burundi",Alpha2Code = "BI"},
new Country{Description = "Cambodia",Alpha2Code = "KH"},
new Country{Description = "Cameroon",Alpha2Code = "CM"},
new Country{Description = "Canada",Alpha2Code = "CA"},
new Country{Description = "Central African Republic",Alpha2Code = "CF"},
new Country{Description = "Chad",Alpha2Code = "TD"},
new Country{Description = "Chile",Alpha2Code = "CL"},
new Country{Description = "China",Alpha2Code = "CN"},
new Country{Description = "Christmas Island",Alpha2Code = "CX"}
};
return _countries = list.ToDictionary(l => l.Alpha2Code);
}
}
private static Dictionary<string, Customer> _customers;
internal static Dictionary<string, Customer> Customers
{
get
{
if (_customers != null)
return _customers;
var list = new List<Customer>
{
new Customer{Firstname="Joe", Surname="Blogs", Country = Countries["CA"]},
new Customer{Firstname="Mary", Surname="Summer", Country = Countries["TD"]},
new Customer{Firstname="Chris", Surname="Exhausted", Country = Countries["VG"]}
};
//If we did countries first, then the id will be populated and we can use it for assignment
list.ForEach(l => l.CountryId = l.Country.Id);
return _customers = list.ToDictionary(l => $"{l.Firstname} {l.Surname}".Trim());
}
}
private static Dictionary<string, Product> _products;
internal static Dictionary<string, Product> Products
{
get
{
if (_products != null)
return _products;
var list = new List<Product>
{
new Product{Description="Socks", Cost=5m},
new Product{Description="Shirt", Cost=10m},
new Product{Description="Pants", Cost=20m}
};
return _products = list.ToDictionary(l => l.Description);
}
}
private static Dictionary<string, Order> _orders;
internal static Dictionary<string, Order> Orders
{
get
{
if (_orders != null)
return _orders;
var counter = 0;
counter++;
var list = new List<Order>
{
new Order
{
Customer = Customers["Joe Blogs"],
Number = $"{new string('0',8) + counter++}".GetLast(8),
},
new Order
{
Customer = Customers["Chris Exhausted"],
Number = $"{new string('0',8) + counter++}".GetLast(8),
},
new Order
{
Customer = Customers["Mary Summer"],
Number = $"{new string('0',8) + counter++}".GetLast(8),
}
};
return _orders = list.ToDictionary(l => l.Number);
}
}
internal static List<OrderItem> GetSomeOrderItems()
{
var list = new List<OrderItem>();
var counter = 0;
counter++;
list.Add(new OrderItem { Product = Products["Socks"], Qty=1, Order = Orders[$"{new string('0', 8) + counter}".GetLast(8)] });
list.Add(new OrderItem { Product = Products["Shirt"], Qty=2, Order = Orders[$"{new string('0', 8) + counter}".GetLast(8)] });
counter++;
list.Add(new OrderItem { Product = Products["Socks"], Qty = 6, Order = Orders[$"{new string('0', 8) + counter}".GetLast(8)] });
list.Add(new OrderItem { Product = Products["Shirt"], Qty = 4, Order = Orders[$"{new string('0', 8) + counter}".GetLast(8)] });
list.Add(new OrderItem { Product = Products["Pants"], Qty = 2, Order = Orders[$"{new string('0', 8) + counter}".GetLast(8)] });
counter++;
list.Add(new OrderItem { Product = Products["Shirt"], Qty = 9, Order = Orders[$"{new string('0', 8) + counter}".GetLast(8)] });
list.ForEach(l =>
{
l.ProductId = l.Product.Id;
l.OrderId = l.Order.Id;
});
return list;
}
}
7. BaseSeeding 的实现。
表的填充顺序取决于其重要性,如果不在允许的环境中,则其余的不会执行。
订单先于订单项填充,这显然仍在进行中,但足以完成工作。
public class SeedSampleContext : BaseSeeding<SampleContext>
{
private string[] _allowed = new[] { "Development", "Staging" };
public SeedSampleContext(IServiceProvider provider) : base(provider) { }
public override Task Seed(string environmentName)
{
//see that on the second parameter we are updating the modified date
AddOrUpdate(Data.Countries.Select(c => c.Value),
action: c => c.Modified = DateTime.Now, propertiesToMatch: c => c.Alpha2Code);
//one property only
Add(Data.Products.Select(p => p.Value), p => p.Description);
if (!_allowed.Contains(environmentName))
return null;
//two properties will be checked for uniqueness
Add(Data.Customers.Select(c => c.Value), c => c.Firstname, c => c.Surname);
Add(Data.Orders.Select(o => o.Value), o => o.Number);
Add(Data.GetSomeOrderItems(), oi => oi.ProductId, oi => oi.OrderId);
return null;
}
}
8. Startup.cs 的更改。
这是我经常用于连接数据库的方法。 我曾处理过一些需要连接到多个数据库并进行模块化设计的应用程序。
private void ConfigureDatabase<TContext>(IServiceCollection services, Action<DbContextOptionsBuilder> action) where TContext : DbContext, IDbContext { services.AddDbContext<TContext>(action); services.AddScoped<IDbContext, TContext>(provider => provider.GetService<TContext>()); }
现在,在 ConfigureServices(IServiceCollection services) 中添加以下行:
第一个是用于数据库,第二个是实际添加种子实现(如 7 所示)。
ConfigureDatabase<SampleContext>(services, o => o.UseSqlServer(Configuration.GetConnectionString("DefaultConnection"), b => b.MigrationsAssembly("pare.Sample"))); services.AddSeeding<SeedSampleContext>() .AddSingleton<ISeedData, SeedSampleContext>(provider => provider.GetService<SeedSampleContext>());
在 Configure(app,env,loggerfactory) 中,我们将添加:
using (var serviceScope = app.ApplicationServices.GetService<IServiceScopeFactory>().CreateScope())
{
var contexts = serviceScope.ServiceProvider.GetServices<IDbContext>();
foreach (var context in contexts)
((DbContext)context).Database.Migrate();
var seeding = serviceScope.ServiceProvider.GetServices<ISeedData>();
foreach (var item in seeding)
{
item.SeedEnums(env.EnvironmentName);
item.Seed(env.EnvironmentName);
}
}
上半部分将对所有 IDbContext 实现进行迁移,并且由于我们知道自己实现了 DbContext,因此可以安全地将其强制转换为 DbContext。
顺便说一句:GetServices<DbContext> 在 dotnet core 的预发布版本中曾有效,但后来失效了,接口的使用也解决了这个 bug。
第二部分是获取 ISeedData,然后填充枚举并执行在 7 中实现的 Seed 方法。 通过传递环境名称,我们可以有条件地为开发或生产环境填充数据。
DbEnum 可在 @ https://codeproject.org.cn/Reference/1186336/DbEnum 找到。
EnumHelper 可在 @ https://codeproject.org.cn/Reference/1186338/EnumHelper-for-DbEnumc 找到。
总结
迁移是在应用程序空间中完成的,无论是 WebApi、Console 还是 WebApp。 主要原因是保持程序集的数据库独立性,并且应用程序实际上使用配置文件中指定的数据库进行工作。 在本示例中,它在一个 WebAPI 中,但每个文件夹都可以是一个程序集,甚至可以有多个。 如果您选择多个,则可以针对不同的架构、数据库等,并为每个“模块”保留种子(如果我有时间,我肯定会写一篇关于它的文章)。
您可能会问,为什么不通过迁移来完成。 答案是为什么要这样做,它并非错误,但这样我们就能确保数据每次都被加载和检查。 Add 操作将根据您在检查中定义的条件,将匹配的数据保留不变,并仅设置对象中的 Id 值供下一个相关对象使用;如果未找到匹配项,它将用新数据填充数据库。 AddOrUpdate 的功能与 Add 操作相同,只不过如果在找到匹配项时,它将忽略任何外部于种子填充的更改(例如 DBA、应用程序等),并将其设置回原始种子状态。
它是一份用于填充数据的单一来源,而不是分散在多个迁移中,如果团队规模较大,查找和修复起来会很麻烦。 让迁移处理结构性更改,这正是 EF Code-First 的强大之处。 记住 SOLID 原则,让迁移做它最擅长的事情,为填充数据做其他事情***微笑***。
有人可能会说,这将在每次启动时运行。 是的,您百分之百正确,但这是一个坏事吗?它只在第一个应用程序池、服务等启动时运行,您将确保数据库始终是您期望的。 对于长时间运行的应用程序,从头开始运行它的成本应该很小,以至于不容置疑。 实际上,每次我们在开发框上启动应用程序时都会运行,而且相当频繁,我估计平均每天运行 20-30 次(甚至更多),而且并不算太糟。
欢迎任何评论和改进,如果您愿意,GitHub 上有一个主仓库。
https://github.com/Grompot77/pare.EntityFrameworkCore.Seeding