随着时间的推移,控制器会产生很多依赖关系,并且为每个请求创建控制器实例变得过于昂贵(尤其是使用 DI)。有没有办法让控制器成为单例?
2 回答
创建控制器实例是非常快速和简单的操作。变得过于昂贵的是为每个请求创建依赖关系。因此,您真正需要的是许多共享相同依赖项实例的控制器。
例如,您有以下控制器
public class SalesController : Controller
{
private IProductRepository productRepository;
private IOrderRepository orderRepository;
public SalesController(IProductRepository productRepository,
IOrderRepository orderRepository)
{
this.productRepository = productRepository;
this.orderRepository = orderRepository;
}
// ...
}
您应该将依赖注入框架配置为对所有应用程序使用相同的存储库实例(请记住,您可能会遇到同步问题)。现在创建依赖项不再昂贵。所有依赖项都只实例化一次,并为所有请求重用。
如果您有许多依赖项,并且您担心获取每个依赖项实例的引用并将这些引用提供给控制器实例的成本(我认为这不会很昂贵),那么您可以对依赖项进行分组(类似于 Introduce Parameter对象重构):
public class SalesController : Controller
{
private ISalesService salesService;
public SalesController(ISalesService salesService)
{
this.salesService = salesService;
}
// ...
}
public class SalesService : ISalesService
{
private IProductRepository productRepository;
private IOrderRepository orderRepository;
public SalesService(IProductRepository productRepository,
IOrderRepository orderRepository)
{
this.productRepository = productRepository;
this.orderRepository = orderRepository;
}
// ...
}
现在你有了单一的依赖,它会很快被注入。如果您将配置依赖注入框架以使用单例 SalesService,那么所有 SalesController 将重用相同的服务实例。创建控制器和提供依赖关系将非常快。
所以首先回答原始问题:
public void ConfigureServices(IServiceCollection services) {
// put other services bindings here
// bind all Controller classes as singletons
services.AddSingleton<HomeController, HomeController>();
// tell framework to obtain Controller instances from ServiceProvider.
services.AddMvc().AddControllersAsServices();
}
如原始问题中所述,如果控制器具有主要由请求范围或瞬态依赖项组成的大型依赖关系树,那么为每个请求单独创建它们可能会对应用程序的可伸缩性产生一些影响(例如,在 Java 中,Servlet 实例默认情况下是单例的这个原因)。虽然创建大型依赖树所需的 CPU 和实时时间通常可以忽略不计(除非您在组件的构造函数中有一些繁重的计算或网络通信,这对于瞬态或请求范围的组件几乎不是一个好主意),内存使用足迹是不可忽视的。对于常见的 DB-Web 应用程序,内存是限制单个机器节点可以处理的并发请求数量的主要因素。如果每个请求都有一个大依赖树的单独副本,
接受的答案 1220560 也解决了这个问题,但我认为这是一个丑陋的 hack,它有一些缺点:您需要创建这个人工单例服务,该服务将被您的控制器用作服务定位器或其他服务的代理. 如果您的所有控制器只有一个这样的单例对象,那么您实际上隐藏了控制器的真正依赖项:例如,如果有人想为您的控制器编写单元测试,他需要仔细分析其实现以查看它实际上是哪些依赖项使用,以便他知道他需要在测试设置中提供哪些模拟/伪造。如果稍后您更改了您的控制器,并且由于您更改了您的控制器使用的服务子集,您的控制器使用的服务子集也发生了变化,那么很容易忘记更新测试设置。这有时可能会导致难以跟踪的错误。与此相反,如果您的依赖项显式声明为构造函数参数,您将立即在测试设置中收到编译器错误。您可以做的另一件事是为每个控制器设置一个单独的这样的单例代理/服务定位器,但这基本上很麻烦。
无论您使用我提出的解决方案还是答案#1220560 中的解决方案,在将请求范围依赖项注入单例对象时都必须小心,如https://docs.microsoft.com/en-us/aspnet/core/fundamentals中所述/dependency-injection#registering-your-own-services就在“注册你自己的服务”部分的末尾。您可以在此处找到此问题的可能解决方案:如何在 C#/ASP 中的单例中使用作用域依赖 另一个需要注意的是并发问题:单例对象可能由处理不同并发请求的多个线程同时访问,因此请确保添加正确同步到您的单例使用的任何非线程安全资源。
编辑:
我刚刚意识到最初的问题是关于 ASP.NET 的,而这个答案是针对 ASP.NET Core 的,所以它可能不适用于“非核心”。