如何在 .NET 标准项目中使用依赖关系注入



是否可以在没有 ASP.NET Core Web 应用程序项目的情况下在独立的 .NET Standard 项目中使用依赖关系注入?

由于没有startup.cs文件,我很想知道这是否可能,以及是否可以做到这一点?

是的,你绝对可以做到这一点。事实上,即使您的类库与 ASP.NET Core 实现位于同一程序集中,也最好让您的库完全不知道您的特定依赖项和您正在使用的任何依赖项注入容器。

TL;DR:您的依赖项和您使用的任何依赖项注入容器都应在应用程序的组成根(例如Startup)中配置,而不是在您的库中配置。请继续阅读以了解详细信息。

核心概念

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

鉴于此,应编写 .NET 标准类库以允许从实现它的任何应用程序注入依赖项,同时与正在使用的依赖项甚至容器完全无关。通常,这是通过构造函数注入完成的,其中依赖项作为类构造函数中的参数公开。在这种情况下,您只想通过构造函数的抽象(即通过描述实现的接口)公开构造函数中的依赖项。

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

假设您需要为用户访问某种类型的数据持久性层(如数据库),但您不希望类库与任何单个实现(如 SQL Server 数据库)紧密耦合。

抽象化

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

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

构造函数注入

然后,对于依赖于该服务的任何类,您将实现一个构造函数,该构造函数允许将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
}
}

至关重要的是,此实现可以位于一个完全独立的程序集中;包含IUserRepository的程序集MyClass根本不需要知道它。

依赖注入

那么实际的依赖注入在哪里发生呢?在实现 .NET 标准类库的任何应用程序中。因此,例如,如果您有一个 ASP.NET Core 应用程序,您可以通过Startup类配置依赖项注入容器,以便为依赖于IUserRepository的任何类注入SqlUserRepository实例:

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

在这方面,前端应用程序在 .NET 标准类库和它所依赖的任何服务之间提供了粘合剂

重要提示:这同样可以是一个控制台应用程序。它可以使用第三方依赖注入容器,例如 Ninject。或者,您可以在组合根中手动连接依赖项图。这里重要的是,你的 .NET Standard 类库不知道或不关心,只要某些东西给它提供依赖关系。

程序集引用

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

  • 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中。

重要:它们都可以位于同一程序集中。但是,即使它们是,将它们视为独立的也很有用,以便保持依赖项注入容器、核心业务逻辑和具体实现之间的松散耦合。

最佳实践

虽然不是严格要求的,但最佳做法是将依赖项视为必需参数,这些参数被类视为只读。如果您公开了可选的依赖项或可以在对象的生存期内替换的依赖项,则可能会为自己创建很多问题。

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

public MyClass(IUserRepository userRepository) 

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

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

最后,将其分配给readonly字段,以便以后无法将其替换为其他实现:

private readonly IUserRepository _userRepository;

假设您正在编写一个日志读取器库,该库从多个源获取数据:

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

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;
}
}

现在,如果要将日志读取器库与 Core 或控制台应用程序一起使用 ASP.NET 则可以在ConfigureServices方法或创建ServiceCollection的任何位置注册日志读取器依赖项AddSqlServerLogReader调用

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

这是注册库依赖项的常见模式。在此处查看实际实现。

众所周知,类库不能执行自己的类库。您必须从控制台或 ASP.NET Core 项目(我们调用这些可执行文件)引用它,然后这些可执行文件将调用您的库。

依赖项注入配置在类库中的实际代码之前运行。在库中,我们没有任何入口点来配置依赖注入容器;我们必须创建一个,然后从可执行文件的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
}

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

最新更新