EF Core实现派生类单表存储并支持运行时加载自定义类
解决方案:EF Core 运行时动态扩展 Device 子类的单表存储实现
核心思路
放弃硬编码的鉴别器配置,改为动态加载Device子类+运行时配置EF Core模型,同时通过插件化方式加载外部DLL中的自定义设备类型,满足上线后无需修改代码即可扩展的需求。
步骤1:重构Device基类与子类设计
统一鉴别器字段到基类,避免子类重复定义:
// 基类:定义鉴别器字段与抽象行为 public abstract class Device { public int Id { get; set; } // 鉴别器字段,由子类在构造函数中赋值 public string Type { get; protected set; } public abstract void StartInstance(); public event EventHandler<string> OnMessageSend; protected void RaiseMessageSend(string message) { OnMessageSend?.Invoke(this, message); } } // 子类示例:BarcodeDevice public class BarcodeDevice : Device { public BarcodeDevice() { Type = nameof(BarcodeDevice); } public string BarcodeValue { get; set; } private SerialPort _serial { get; set; } public override void StartInstance() { // 设备启动逻辑 } }
步骤2:实现动态设备类型加载器
创建类型提供者,负责加载内置设备类型与外部插件DLL中的设备类型:
public interface IDeviceTypeProvider { IEnumerable<Type> GetDeviceTypes(); } public class DeviceTypeProvider : IDeviceTypeProvider { private readonly IEnumerable<Type> _deviceTypes; public DeviceTypeProvider() { // 加载内置设备类型 var builtInTypes = new List<Type> { typeof(BarcodeDevice), typeof(LightDevice) }; // 加载Plugins目录下的外部设备插件 var pluginTypes = LoadPluginDeviceTypes(); _deviceTypes = builtInTypes.Concat(pluginTypes).ToList(); } private IEnumerable<Type> LoadPluginDeviceTypes() { var pluginDir = Path.Combine(AppContext.BaseDirectory, "Plugins"); if (!Directory.Exists(pluginDir)) return Enumerable.Empty<Type>(); var pluginTypes = new List<Type>(); foreach (var dllPath in Directory.GetFiles(pluginDir, "*.dll")) { try { var assembly = Assembly.LoadFrom(dllPath); // 筛选所有继承自Device的非抽象类 var types = assembly.GetTypes() .Where(t => typeof(Device).IsAssignableFrom(t) && !t.IsAbstract && t != typeof(Device)); pluginTypes.AddRange(types); } catch (Exception ex) { // 日志记录加载异常 Console.WriteLine($"加载插件失败: {dllPath}, 错误: {ex.Message}"); } } return pluginTypes; } public IEnumerable<Type> GetDeviceTypes() => _deviceTypes; }
步骤3:重构DbContext实现动态鉴别器配置
注入类型提供者,在模型创建阶段动态注册所有设备子类的鉴别器:
public class ApplicationDbContext : IdentityDbContext { private readonly IDeviceTypeProvider _deviceTypeProvider; public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options, IDeviceTypeProvider deviceTypeProvider) : base(options) { _deviceTypeProvider = deviceTypeProvider; } public DbSet<Device> Devices { get; set; } protected override void OnModelCreating(ModelBuilder modelBuilder) { base.OnModelCreating(modelBuilder); // 配置基类的鉴别器 var discriminatorBuilder = modelBuilder.Entity<Device>() .HasDiscriminator<string>(d => d.Type) .IsRequired(); // 动态添加所有子类的鉴别器映射 foreach (var deviceType in _deviceTypeProvider.GetDeviceTypes()) { discriminatorBuilder.HasValue(deviceType, deviceType.Name); } } }
步骤4:服务注册与初始化
在Program.cs中注册类型提供者与DbContext:
var builder = WebApplication.CreateBuilder(args); // 注册设备类型提供者(单例,启动时加载一次) builder.Services.AddSingleton<IDeviceTypeProvider, DeviceTypeProvider>(); // 注册DbContext builder.Services.AddDbContext<ApplicationDbContext>(options => options.UseSqlServer(builder.Configuration.GetConnectionString("DefaultConnection"))); builder.Services.AddScoped<IDeviceService, DeviceService>(); // 其他服务注册...
步骤5:验证查询与实例化
DeviceService中查询数据库时,EF Core会自动根据Type字段实例化对应的具体设备子类:
public class DeviceService : IDeviceService { private readonly IServiceScopeFactory _serviceScopeFactory; private IResolverMqttClient _mqttClient; public List<Device> Devices { get; private set; } public DeviceService(IServiceScopeFactory serviceScopeFactory) { _serviceScopeFactory = serviceScopeFactory; using var scope = serviceScopeFactory.CreateScope(); var dbContext = scope.ServiceProvider.GetRequiredService<ApplicationDbContext>(); // 查询结果自动包含具体子类实例(BarcodeDevice/LightDevice/插件类型) Devices = dbContext.Devices.ToList(); foreach (var device in Devices) { device.StartInstance(); device.OnMessageSend += Device_OnMessageSend; } _mqttClient = scope.ServiceProvider.GetRequiredService<IResolverMqttClient>(); _mqttClient.OnMqttMessageReceived += MqttClient_OnMqttMessageReceived; } private void Device_OnMessageSend(object sender, string e) { // 处理设备消息 } private void MqttClient_OnMqttMessageReceived(object sender, string e) { // 处理MQTT消息 } }
注意事项
- 插件兼容性:外部插件DLL必须引用与主项目一致的Device基类类库,避免类型不匹配。
- 数据库扩展性:若插件设备有自定义属性,EF Core会自动在Device表中添加对应列;若需更灵活的扩展,可将自定义属性序列化为JSON存储在单个字段中。
- 热加载限制:EF Core的模型是缓存的,若需运行时热加载插件,需重启应用或实现自定义模型缓存逻辑(不推荐生产环境使用)。
内容的提问来源于stack exchange,提问作者Ibrahim Timimi
相关产品推荐
相关产品推荐

