C#对动态加载的DLL进行依赖注入,并对DLL注入服务

发布于:2025-02-10 ⋅ 阅读:(86) ⋅ 点赞:(0)

什么是依赖注入

  • 概念

依赖注入(Dependency Injection,简称 DI)是一种软件设计模式,用于解耦软件组件之间的依赖关系。在 C# 开发中,它主要解决的是类与类之间的强耦合问题。例如,一个类 A 依赖于另一个类 B,如果不使用依赖注入,那么在类 A 内部可能会直接实例化类 B,这就使得类 A 和类 B 紧密地耦合在一起。而依赖注入的方式是将类 B 的实例通过外部(通常是在类 A 的构造函数、属性或者方法参数中)传递给类 A,从而降低它们之间的耦合程度。

  • 作用
    提高可维护性:当系统规模变大时,如果各个组件之间耦合紧密,修改其中一个组件可能会牵一发而动全身。通过依赖注入,组件之间的依赖关系更加清晰,维护起来更加容易。
    方便单元测试:在进行单元测试时,可以方便地模拟依赖对象,而不是依赖于真实的复杂对象,从而使测试更加简单和准确。

常用的依赖注入实现

  • Microsoft.Extensions.DependencyInjection
using Microsoft.Extensions.DependencyInjection;
class MyClassA
{
    private MyClassB _dependency;
    public MyClassA(MyClassB dependency)
    {
        _dependency = dependency;
    }
    public void DoSomething()=>_dependency.SomeMethod();
}
class MyClassB
{
    public void SomeMethod()=>Console.WriteLine("MyClassB's method is called.");
}
class Program
{
    static void Main()
    {
        // 创建服务容器
        var serviceCollection = new ServiceCollection();
        // 注册MyClassB为服务,每次请求MyClassB时会创建一个新的实例
        serviceCollection.AddTransient<MyClassB>();
        // 注册MyClassA为服务,并且注入MyClassB
        serviceCollection.AddTransient<MyClassA>();
        // 构建服务提供器
        var serviceProvider = serviceCollection.BuildServiceProvider();
        // 获取MyClassA的实例,此时会自动注入MyClassB的实例
        var a = serviceProvider.GetService<MyClassA>();
        a.DoSomething();
    }
}
  • Autofac
using Autofac;
class MyClassA
{
    private MyClassB _dependency;
    public MyClassA(MyClassB dependency)
    {
        _dependency = dependency;
    }
    public void DoSomething()=>_dependency.SomeMethod();
}
class MyClassB
{
    public void SomeMethod()=>Console.WriteLine("MyClassB's method is called.");
}
class Program
{
    static void Main()
    {
        var builder = new ContainerBuilder();
        builder.RegisterType<MyClassB>().AsSelf();
        builder.RegisterType<MyClassA>().AsSelf();
        var container = builder.Build();
        var a = container.Resolve<MyClassA>();
        a.DoSomething();
    }
}

什么是动态加载

  • 定义

在 C# 中,动态加载程序集允许程序在运行时加载和使用程序集,而不是在编译时静态引用它们。这为程序提供了更高的灵活性,例如可以根据不同的条件或用户需求加载不同的功能模块,或者在程序运行时更新某些功能而无需重新编译整个程序

  • 示例

using System.Reflection;

class Program
{
    static void Main()
    {
        // 加载程序集,参数为程序集的完整名称
        Assembly assembly = Assembly.Load("xxx.dll");
        // 从程序集中获取类型
        Type type = assembly.GetType("MyNamespace.MyClass");
        // 创建类型的实例
        object instance = Activator.CreateInstance(type);
        // 调用类型的方法(假设该类型有一个无参数的方法叫做 DoSomething)
        MethodInfo method = type.GetMethod("DoSomething");
        method.Invoke(instance, null);
    }
}

注意Assembly.Load加载的程序集在进行卸载前,被加载的dll将会一直处于被占用状态,若要在非占用状态下进行加载程序集,比如进行动态更新之类的,可以采用以下方式

byte[] rawAssembly = File.ReadAllBytes(dllPath);
Assembly assembly = Assembly.Load(rawAssembly);

对动态加载的DLL进行依赖注入

这里采用Autofac进行服务的注入和管理

  1. 加载程序集
byte[] rawAssembly = File.ReadAllBytes(dllPath);
Assembly assembly = Assembly.Load(rawAssembly);
  1. 获取程序集中定义的类型,并过滤出要进行依赖注入的类型,假设要为打了Inject标签同时继承ITool接口的类型进行服务提供
var types = assembly.DefinedTypes.ToList();

[Inject("工具2", "类别1")]
public class MainFrame : ITool

[AttributeUsage(AttributeTargets.All, AllowMultiple = false)]
public class InjectAttribute(string name, string category) : Attribute
{
    public string Name { get; } = name;
    public string Category { get; } = category;
}

public interface ITool
{
    void DoWork();
    void ShowDialog();
}
  1. 创建ContainerBuilder
  2. ContainerBuilder进行依赖注入,若存在同类型多个不同实例的注入,可以采用Key或Token的方式来注入和解析服务
var builder = new ContainerBuilder();
types = types.Where(IsToolType).ToList();
foreach (var type in types)
{
    IList<CustomAttributeTypedArgument> constructorArguments = type
        .CustomAttributes.First(a => a.AttributeType == typeof(InjectAttribute))
        .ConstructorArguments;
    var (name, categoryName) = (
        constructorArguments[0].Value.ToString(),
        constructorArguments[1].Value.ToString()
    );
    string key = $"{categoryName}_{name}";
    builder.RegisterType(type).Named<ITool>(key);
}
  1. 创建Container
  2. 解析服务
var container = builder.Build();
var tool1 = container.ResolveNamed<ITool>("类别1_工具1");
tool1.DoWork();
var tool2 = container.ResolveNamed<ITool>("类别1_工具2");
tool2.ShowDialog();
var tool3 = container.ResolveNamed<ITool>("类别2_工具1");
tool3.ShowDialog();
Console.ReadLine();

至此,多个ITool实例但都对应不同实现的类型都被注入了,要使用时直接Resolve即可

若被注入的类型存在对其他类型的引用,则依赖注入容器会帮我们自动解析。例如对日志和数据库功能的引用,需要该服务的类型只需要引用定义,而不需要关心实现,如下
ILogger可能有多种实现,比如FileLoggerConsoleLoggerDBLogger
IDatabase可能有多种实现,比如MysqlSqlitePostgresql

实现的内容由主框架确定,其他工具dll只需要引用ILoggerIDatabase即可使用,示例代码如下

public interface IDatabase
{
    List<string> Select();
    void Insert<T>(T entity);
}

public interface ILogger
{
    void Log(string message);
}

单例方式注入

var builder = new ContainerBuilder();
builder.RegisterType<ConsoleLogger>().SingleInstance().As<ILogger>();
builder.RegisterType<MysqlMocker>().SingleInstance().As<IDatabase>();

使用

[Inject("工具1", "类别1")]
public class Tool1: ITool
{
    internal readonly ILogger logger;
    internal readonly IDatabase database;

    public MainFrame(ILogger logger, IDatabase database)
    {
        this.logger = logger;
        this.database = database;
    }

    public void DoWork()
    {
        logger?.Log("This is a log message from Tool1");
        logger?.Log("Doing work in MainFrame");
        var data = database?.Select();
        var str = JsonConvert.SerializeObject(data);
        logger?.Log($"Data from database: {str}");
    }

    public void ShowDialog() { }
}

网站公告

今日签到

点亮在社区的每一天
去签到