是的,你绝对可以做到这一点。事实上,即使您的类库与 ASP.NET Core 实现在 same 程序集中,最好让您的库完全不知道您的特定依赖项和您正在使用的任何依赖项注入容器.
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。或者,您可以在 composition root 中手动连接您的依赖关系图。这里重要的是,您的 .NET Standard 类库不知道也不关心,只要 something 为其提供依赖项。
程序集参考
在上面的示例中,您的程序集结构可能如下所示:
-
My.Library.dll
-
My.Library.Sql.dll(参考My.Library.dll)
-
My.Web.dll(参考My.Library.dll,My.Library.Sql.dll)
-
My.Console.dll(参考My.Library.dll,My.Library.Sql.dll)
请注意,My.Library 完全不知道具体实现(例如,My.Library.Sql.dll)或将实现它的应用程序(例如,My.Web.dll、My.Console.dll)。它只知道IUserRepository 在构造时会被注入MyClass。
重要提示:这些可能都在同一个程序集中。但即使它们是,将它们考虑分开来保持依赖注入容器、核心业务逻辑和具体实现之间的松散耦合也是有用的。
最佳实践
虽然不是严格要求,但最佳做法是让您的依赖项成为 必需 参数,这些参数被您的类视为 只读。如果您公开可选的依赖项或可以在对象的生命周期内替换的依赖项,您可能会为自己制造很多问题。
上面的类结构通过需要参数演示了理想的实现:
public MyClass(IUserRepository userRepository)
添加保护子句以防止空值:
_userRepository = userRepository?? throw new ArgumentNullException(nameof(userRepository));
最后,将其分配给 readonly 字段,这样以后就不能用不同的实现来替换它:
private readonly IUserRepository _userRepository;