You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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消息
    }
}

注意事项

  1. 插件兼容性:外部插件DLL必须引用与主项目一致的Device基类类库,避免类型不匹配。
  2. 数据库扩展性:若插件设备有自定义属性,EF Core会自动在Device表中添加对应列;若需更灵活的扩展,可将自定义属性序列化为JSON存储在单个字段中。
  3. 热加载限制:EF Core的模型是缓存的,若需运行时热加载插件,需重启应用或实现自定义模型缓存逻辑(不推荐生产环境使用)。

内容的提问来源于stack exchange,提问作者Ibrahim Timimi

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.03 17:35:30