.NET 6中EF Core DbContext导出表到CSV遇DbSet构造函数缺失异常
我需要开发一个Blazor ASP.NET Core应用,把MS SQL Server数据库的每张表导出为独立的.csv文件。我尝试从DbContext的属性中提取DbSet到泛型列表,但在将泛型对象转换为DbSet类时出现了以下异常:
An unhandled exception occurred while processing the request.
MissingMethodException: Constructor on type
'Microsoft.EntityFrameworkCore.DbSet`1[[DatabaseTableExport.Data.LoginDbModels.AccountPasswordHistory,
DatabaseTableExport, Version=1.0.0.0, Culture=neutral,
PublicKeyToken=null]]' not found.
System.RuntimeType.CreateInstanceImpl(BindingFlags bindingAttr, Binder
binder, object[] args, CultureInfo culture)
请问如何修复这个错误,或者有没有更好的从DbContext提取DbSet的方法?
用户提供的代码:
using DatabaseTableExport.Data; using DatabaseTableExport.Data.LoginDbModels; using Microsoft.AspNetCore.Components; using Microsoft.EntityFrameworkCore; using System; using System.Collections.Generic; using System.Linq; using System.Reflection; namespace DatabaseTableExport.Pages { public partial class Index : ComponentBase { [Inject] private LoginDbContext LoginDbContext { get; set; } [Inject] private ApplicationDbContext ApplicationDbContext { get; set; } protected override void OnInitialized() { List<DbSet<object>> dbSets = GetDbSets(LoginDbContext); // iterate through each DbSet instance foreach (var dbSet in dbSets) { // export to csv here } } private static List<DbSet<object>> GetDbSets(DbContext loginDbContext) { // Retrieve all the DbSet properties declared on the context class. var dbSetProperties = loginDbContext.GetType() .GetProperties(BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly) .Where(p => p.PropertyType.IsGenericType && p.PropertyType.GetGenericTypeDefinition() == typeof(DbSet<>)) .ToList(); // Iterate over each DbSet property and add it to the list of dbsets. var dbSets = new List<DbSet<object>>(); foreach (var property in dbSetProperties) { // If the type of the DbSet is null, skip it. if (property.PropertyType.GenericTypeArguments.Length <= 0) { continue; } // Get the generic type arguments and create a corresponding DbSet instance. var dbsetGenericType = property.PropertyType.GenericTypeArguments[0]; var convertedDbSet = Activator.CreateInstance(typeof(DbSet<>).MakeGenericType(dbsetGenericType), property.GetValue(loginDbContext)); dbSets.Add((DbSet<object>)convertedDbSet); } // Return the list of DbSet objects. return dbSets; } } }
错误原因
核心问题是DbSetActivator.CreateInstance直接创建实例。而且DbContext中定义的DbSet属性本身已经是EF Core初始化好的实例,完全不需要重新创建,只需直接获取这些实例并转换为通用类型即可。
修复后的GetDbSets方法
修改GetDbSets方法,跳过不必要的实例创建步骤,直接获取DbContext中已有的DbSet实例:
private static List<DbSet<object>> GetDbSets(DbContext dbContext) { var dbSetProperties = dbContext.GetType() .GetProperties(BindingFlags.Public | BindingFlags.Instance) .Where(p => p.PropertyType.IsGenericType && p.PropertyType.GetGenericTypeDefinition() == typeof(DbSet<>)) .ToList(); var dbSets = new List<DbSet<object>>(); foreach (var property in dbSetProperties) { // 直接获取DbContext中已有的DbSet实例 var dbSetInstance = property.GetValue(dbContext); // 将DbSet<T>转为DbSet<object> var objectDbSet = (DbSet<object>)typeof(DbSet<>).MakeGenericType( property.PropertyType.GenericTypeArguments[0]) .GetMethod("AsQueryable")? .Invoke(dbSetInstance, null); if (objectDbSet != null) { dbSets.Add(objectDbSet); } } return dbSets; }
更优的处理方式
如果仅需导出CSV,无需强行转换成DbSet<object>,用IQueryable<object>处理更简洁:
private static IEnumerable<(Type EntityType, IQueryable<object> Data)> GetDbSetData(DbContext dbContext) { foreach (var property in dbContext.GetType() .GetProperties(BindingFlags.Public | BindingFlags.Instance) .Where(p => p.PropertyType.IsGenericType && p.PropertyType.GetGenericTypeDefinition() == typeof(DbSet<>))) { var entityType = property.PropertyType.GenericTypeArguments[0]; var dbSet = property.GetValue(dbContext); var queryable = (IQueryable<object>)typeof(Queryable) .GetMethod("AsQueryable") .MakeGenericMethod(entityType) .Invoke(null, new[] { dbSet }); yield return (entityType, queryable); } }
使用示例:
protected override void OnInitialized() { var dbSetDataList = GetDbSetData(LoginDbContext).ToList(); foreach (var (entityType, data) in dbSetDataList) { // 用实体类型名作为CSV文件名 string fileName = $"{entityType.Name}.csv"; // 获取所有数据行 var records = data.ToList(); // 执行CSV导出逻辑 } }
补充说明
- 移除
BindingFlags.DeclaredOnly可获取DbContext基类中定义的DbSet,若仅需当前类定义的DbSet可保留该标记。 - 导出CSV时,可通过反射获取实体属性名作为表头,遍历实体对象获取属性值生成行数据。
内容的提问来源于stack exchange,提问作者Tyson Gibby

