如何在 .NET Standard 项目中使用依赖注入?

How to use dependency injection in .NET Standard project?

是否可以在没有 ASP.NET 核心 Web 应用程序项目的独立 .NET Standard 项目中使用依赖项注入?

由于没有 startup.cs 文件,我很想知道这是否可行,如果可行,如何实现?

正如我们所知,Class 库无法自行执行。您必须从控制台或 ASP.NET 核心项目中引用它——让我们调用这些可执行文件——然后它们将调用您的库。

您的依赖注入配置在 class 库中的实际代码之前运行。在库中,我们没有任何入口点来配置依赖注入容器;我们必须制作一个然后从我们的可执行文件的 StartupMain.

中调用它

例如,EF Core 只是一个库,但它有一个扩展方法(用作入口点),允许您使用 DI 对其进行配置:

public static IServiceCollection AddDbContext<TContext>(this IServiceCollection serviceCollection, Action<DbContextOptionsBuilder> optionsAction = null, ServiceLifetime contextLifetime = ServiceLifetime.Scoped, ServiceLifetime optionsLifetime = ServiceLifetime.Scoped) where TContext : DbContext
{
    library configurations
}

您可以在代码中采用同样的方法。

假设您正在编写一个从多个来源获取数据的日志 reader 库:

public interface IDataProvider
{
    Task<(IEnumerable<LogModel>, int)> FetchDataAsync(
        int page,
        int count,
        string level = null,
        string searchCriteria = null
    );
}

并且您对上述接口有多个实现:

public class SqlServerDataProvider : IDataProvider
{
    private readonly RelationalDbOptions _options;

    public SqlServerDataProvider(RelationalDbOptions options)
    {
        _options = options;
    }

    public async Task<(IEnumerable<LogModel>, int)> FetchDataAsync(
        int page,
        int count,
        string logLevel = null,
        string searchCriteria = null
    )
    { ... }

这里是 RelationalDbOptions class

public class RelationalDbOptions
{
    public string ConnectionString { get; set; }

    public string TableName { get; set; }

    public string Schema { get; set; }
}

让我们创建一个ServiceCollection扩展方法来注册依赖项:

public static class ServiceCollectionExtensions
{
    public static IServiceCollection AddSqlServerLogReader(
        this IServiceCollection services,
        Action<RelationalDbOptions> options
    )
    {
        if (services == null)
            throw new ArgumentNullException(nameof(services));

        if (optionsBuilder == null)
            throw new ArgumentNullException(nameof(optionsBuilder));

        var relationalDbOptions = new RelationalDbOptions();
        options.Invoke(relationalDbOptions );
        services.AddSingleton(relationalDbOptions);

        Services.AddScoped<IDataProvider, SqlServerDataProvider>();

        return services;
    }
}

现在,如果您想将日志 reader 库与 ASP.NET 核心或控制台应用程序一起使用,您可以注册日志 reader 依赖项 ba 在 [= 中调用 AddSqlServerLogReader 18=] 方法或您正在创建的任何地方 ServiceCollection:

public void ConfigureServices(IServiceCollection services)
{
    ...
    services.AddSqlServerLogReader(options => { 
           options.ConnectionString = ""; 
           options.LogTableName = ""
    });
    ...
}

这是注册库依赖项的常见模式。检查实际实施 here.

是的,你绝对可以做到。事实上,即使您的 class 库在 相同的 程序集中作为 ASP.NET 核心实现,最好让您的库完全不知道您的特定依赖项以及您正在使用的任何依赖项注入容器。

TL;DR: Both your dependencies and any dependency injection container you’re using should be configured in your application’s composition root (e.g., Startup), not in your library. Read on for details.

核心概念

首先,有必要记住,从根本上说,依赖注入是一组用于实现松散耦合代码的模式和实践,不是特定的依赖注入容器。这一点在这里很重要,因为您不希望您的库依赖于您可能选择使用的任何依赖注入容器,例如 ASP.NET Core 的内置容器。容器只是一个工具,可以帮助自动化构建、管理依赖项并将其注入到您的库中。

鉴于此,您的 .NET Standard Class 库应该被写入 允许 依赖项从它正在实现的任何应用程序中注入,同时完全不可知到正在使用的依赖项甚至容器。通常这是通过 构造函数注入 完成的,其中依赖项作为参数暴露在 class 的构造函数中。在这种情况下,您只想通过抽象来公开构造函数中的依赖项——即通过描述实现的接口。

通过示例更容易理解这些概念。

例子

假设您需要为用户访问某种类型的数据持久层(例如数据库),但您不希望 class 库与任何单一实现(例如一个 SQL 服务器数据库)。

抽象

在这种情况下,您可以创建一个 IUserRepository 抽象,您的具体实现可以实现它。这是一个简单的示例,它公开了一个 GetUser() 方法:

public interface IUserRepository 
{
    User GetUser(int userId);
}

构造函数注入

然后,对于依赖该服务的任何 classes,您将实现一个允许将 IUserRepository 抽象注入其中的构造函数:

public class MyClass 
{
    private readonly IUserRepository _userRepository;
    public MyClass(IUserRepository userRepository)
    {
        _userRepository = userRepository?? throw new ArgumentNullException(nameof(userRepository));
    }
    public string GetUserName(int userId) => _userRepository.GetUser(userId).Name;
}

具体实现

现在,您可以创建 IUserRepository 的具体实现——让我们说 SqlUserRepository

public class SqlUserRepository: IUserRepository
{
    private readonly string _connectionString;
    public SqlUserRepository(string connectionString) 
    {
        _connectionString = connectionString?? throw new ArgumentNullException(nameof(connectionString));
    }
    public GetUser(int userId) 
    {
        //Implementation
    }
}

关键是,此实现可以在一个完全独立的程序集中;包含 IUserRepositoryMyClass 的程序集根本不需要知道它。

依赖注入

那么实际的依赖注入发生在哪里呢?在实现 .NET Standard Class 库的任何应用程序中。因此,例如,如果您有一个 ASP.NET 核心应用程序,您可以通过 Startup class 配置依赖注入容器,为任何 classes 取决于 IUserRepository:

public void ConfigureServices(IServiceCollection services) 
{
    //Register dependencies
    services.AddScoped<IUserRepository>(c => new SqlRepository("my connection string"));
}

在这方面,您的前端应用程序在您的 .NET Standard Class 库和它所依赖的任何服务之间提供 胶水

Important: This could just as easily be a console application instead. It could use a third-party dependency injection container, such as Ninject. Or you could manually wire up your dependency graph in your composition root. What’s important here is that your .NET Standard class library doesn’t know or care, so long as something gives it its dependencies.

程序集引用

在上面的示例中,您的程序集结构可能如下所示:

  • My.Library.dll
    • IUserRepository
    • MyClass
  • My.Library.Sql.dll(参考文献My.Library.dll
    • SqlUserRepository
  • My.Web.dll(参考文献My.Library.dllMy.Library.Sql.dll
  • My.Console.dll(参考文献My.Library.dllMy.Library.Sql.dll

请注意,My.Library 完全不知道具体实现(例如,My.Library.Sql.dll)或将实现它的应用程序(例如,My.Web.dllMy.Console.dll ).它所知道的是 IUserRepository 在构建时将被注入 MyClass

Important: These could all be in the same assembly. But even if they are, it’s useful to think of them as separate in order to maintain loose coupling between your dependency injection container, your core business logic, and your concrete implementations.

最佳实践

虽然没有严格要求,但最佳做法是让您的依赖项成为 必需的 参数,这些参数被您视为 只读 class。如果公开可选的依赖项或可以在对象的生命周期内替换的依赖项,则可能会给自己带来很多问题。

上面的class结构通过要求参数演示了理想的实现:

public MyClass(IUserRepository userRepository) 

添加保护子句以防止空值:

_userRepository = userRepository?? throw new ArgumentNullException(nameof(userRepository));

最后,将它分配给一个 readonly 字段,这样以后就不能用不同的实现替换它:

private readonly IUserRepository _userRepository;