ASP.NET Core非常适合在云上部署,因为它对内存的占用很小,并且具有很高的吞吐量。所以不需要强大的服务器即可流畅运行,非常适合云的特点。特别是近来随着.net的开源以及对Linux平台的支持和Docker Container的支持,.Net也越来越在国外流行起来成为主流开发技术和平台。
在具体的前端架框架上,我们可以选择传统的Web Apps (又分为MVC和Razor)、SPA(如Angular)和Blazor(后者仍然在不断完善中)。
当我们设计应用程序架构时,需要关注以下几点:
Seperation of concerns: 也就是需要根据做的事情的不同来划分模块或功能,比如如果要选择并按照一定格式选择某种商品呈现给用户,那么选择的逻辑和格式化的逻辑应该分布在不同的模块里。
Encapsulation(封装):模块的逻辑应该都封装在内部,和外部的交互仅仅通过external contract即可完成。这个理论上很好理解,但真正做到并不容易,典型的例子是在DDD中,为了保证领域层的纯粹性,往往会把一些仓储操作放到仓储层,造成部分业务逻辑也随着搬移到仓储层。比如确保用户修改的邮件地址需要在整个系统中唯一。
Dependency inversion (依赖反转):这个又和依赖注入密切相关。比如之前我们的User Class会直接UserRepository, 现在在User所在的Layer定义IRepository接口,然后将该接口注入到User中,并且UserRepository也实现该接口,这样依赖的方向就反转了。如此一来可以极大的提高程序的可维护性、模块化和可测试性。
如果我们按照依赖反转的原则和Domain-Driven Design设计我们的架构,那么最后的架构往往是非常相似的,这样的架构近来被称为Clean Architecture。这样的架构把业务逻辑和应用模型作为整个架构的核心。它的依赖反转体现在infrastructure层依赖于Application Core. 其实也就是在Application层定义该层需要的接口,然后infrastructure层来实现。如下图所示,UI和基础架构层都依赖于Application Core.
这样的架构也非常便于测试,比如因为UI层对基础架构(比如数据库等)层没有直接的依赖,那么测试时可以替换具体的实现。
Controller的调停者模式
在开发ASP.NET页面时,我们可以选择Razor,也可以选择MVC. 使用MVC时,由于单个controller可以包含多个View和Action, 那我们需要注意不要让单个Controller变得太臃肿。(这一点和在设计Repository时要避免方法过多类似)。具体方法,除了换成Razor Page外,还可以考虑使用mediator模式(调停者)。我们的问题可以进一步描述如下:
controller中action太多,导致太多的dependency, 从而又导致需要把大量的关联类注入到controller中。
引入mediator后,controller只需要依赖一个mediator的实例,然后在controller的action中需要通过该mediator发送message, 该message再有单独的handler处理。所以该模式的实质是把controller的责任优雅的通过mediator分配到不同的handler中处理,下面是一个例子:
public class OrderController Controller
{
private readonly IMediator mediator;
public OrderController(IMediator mediator)
{
mediator mediator;
}
[HttpGet]
public async Task<IActionResult> MyOrders()
{
var viewModel await mediator.Send(new GetMyorders(User.Identity.Name));
return View(viewModel);
}
//other actions implemented similarly
在这个例子中,action MyOrders用于查询当前用户的order. 该请求封装在GetMyOrders类中,传递给mediator, 并由下面的GetMyOrdersHandler处理。
public class GetMyordersHandler IRequestHandler<GetMyorders,IEnumerable<OrderViewModel>>
{
private readonly IOrderRepository orderRepository;
public GetMyordersHandler(IOrderRepository orderRepository)
{
_orderRepository orderRepository;
}
public async Task<IEnumerable<OrderViewModel>>Handle(GetMyOrders request,
CancellationToken cancellationToken)
{
var specification new CustomerOrdersWithItemsSpecification(request.UserName);
var orders await orderRepository.ListAsync(specification);
return orders.Select(o =new OrderViewModel
{
OrderDate o.OrderDate,
OrderItems o.OrderItems?.Select(oi =new OrderItemViewModel()
{
PictureUrl = oi.Itemordered.PictureUri,
ProductId = oi.Itemordered.CatalogItemId,
ProductName = oi.ItemOrdered.ProductName,
UnitPrice = oi.UnitPrice,
这样controller可以重新聚焦在它的本质工作:路由和模型绑定上。而处理每个end point请求的重任则落在在每个handler身上。
最后我们再来看看依赖注入。
陷阱之一:Static Cling
在设计系统的依赖时,我们要避免Static Cling。所谓的Static Cling,就是代码直接调用静态方法产生强耦合。这样因为无法替换具体实现而对测试造成影响。特别是当这段代码并不是核心业务逻辑不需要测试时,因为难以绕开而产生干扰。比如下面的例子。
public class CheckoutController
{
public void Checkout(Order order)
{
// verify payment
// verify inventory
LogHelper.LogOrder(order);
}
}
public static class LogHelper
{
public static void LogOrder(Order order)
{
using (System.IO.StreamWriter file =
new System.IO.StreamWriter(@"C:\\Users\\Steve\\OrderLog.txt", true))
{
file.WriteLine("{0} checked out.", order.Id);
}
}
}
public class Order
{
public int Id { get; set; }
}
这段代码如何重构?当然是把静态方法改成实例方法并注入,但是如果该静态方法无法修改呢?那我们可以通过再封装一层adaptor来调用,进而实现重构,代码如下:
public class CheckoutController
{
private readonly IOrderLoggerAdapter _orderLoggerAdapter;
public CheckoutController(IOrderLoggerAdapter orderLoggerAdapter)
{
_orderLoggerAdapter = orderLoggerAdapter;
}
public CheckoutController()
: this(new FileOrderLoggerAdapter())
{
}
public void Checkout(Order order)
{
// verify payment
// verify inventory
_orderLoggerAdapter.LogOrder(order);
}
}
public static class LogHelper
{
public static void LogOrder(Order order)
{
using (System.IO.StreamWriter file =
new System.IO.StreamWriter(@"C:\\Users\\Steve\\OrderLog.txt", true))
{
file.WriteLine("{0} checked out.", order.Id);
}
}
}
public interface IOrderLoggerAdapter
{
void LogOrder(Order order);
}
public class FileOrderLoggerAdapter : IOrderLoggerAdapter
{
public void LogOrder(Order order)
{
LogHelper.LogOrder(order);
}
}
public class Order
{
public int Id { get; set; }
}
陷阱之二:new is glue
也就是避免使用new,从而产生耦合。new就像一纸婚约,把两个class牢牢的绑定在了一起。
在.net 6和Visual Studio 2022中,我们默认在Program.cs中声明我们所有的dependencies.