《Architecting Modern Web Apps with ASP.NET Core2 and Azure》之七
Developing ASP.NET Core Apps
“It’s not important to get it right the first time. It’s vitally important to get it right the last time.”-----------------Andrew Hunt and David Thomas
Mapping Request to Responses(映射请求到响应上)
本质上,ASP.NET Core程序是将进来的请求映射到出去的响应上。在底层上,是中间件来做这件事情,简单的ASP.NET Core程序和微服务可以包括单独的自定义中间件。从pages、handlers、routes、controllers、actions角度考虑,使用ASP.NET Core MVC你可以工作在更高的级别上。每一个进来的请求,首先都会与程序里的路由表进行匹配,如果有匹配的路由,会调用相关的页面处理程序(属于Razor Page)或者动作方法(属于controller)处理响应。如果没有发现匹配的路由,error handler会被调用。
Razor Page基于约定的路由系统(基于相对于Pages文件夹的路径)和HTTP谓词。例如,Pages文件夹的根目录里存在一个Index.cshtml文件,它将会被路由到http://www.xxx.com/,或http://www.xxx.com/Index。如果也有一个Customers.cshtml文件,则会被路由到http://www.xxx.com/Customers。如果你想的话,你也可以在Startup中自定义Razor Page的路由。
ASP.NET Core MVC可以使用基于约定的路由、基于特性(attribute)的路由或者两者兼有。约定路由需要在代码中定义,定义路由约定的语法类似如下:
app.UseMvc(routes =>
{
routes.MapRoute("default","{controller=Home}/{action=Index}/{id?}");
});
在这里一个名为default的路由已经加到了路由表里。它使用contorller,action,id这样的占位符定义了一个路由模板。默认的controller是Home,默认的action是Index,而id后面有一个?表示是可选的。这条约定表明:请求的第一部分应该对应于controller的名称,第二部分应对应于action的名称,如果有第三部分的话这部分则表示id这个参数。约定路由只在程序的一个位置定义好就行了,比如是Startup类的Configure方法。
特性路由直接应用到controller或者action上,而不是作为全局配置。它的优点就是当你在看某一个方法时,能够直观的知道它的路由是什么。而且可以让action有多个路由,也可以结合controller的路由,比如:
[Route("Home")]
public class HomeController : Controller
{
[Route("")] // Combines to define the route template "Home"
[Route("Index")] // Combines to define route template "Home/Index"
[Route("/")] // Does not combine, defines the route template ""
public IActionResult Index() {}
也可以直接使用类似[HttpGet]的方式来使用特性路由,而不用每次都在controller上标记controller的名字,比如:
[Route("[controller]")]
public class ProductsController : Controller
{
[Route("")] // Matches 'Products'
[Route("Index")] // Matches 'Products/Index'
public IActionResult Index()
}
Razor Page无法使用特性路由,但是你可以通过@page
来给Razor Page指定一个路由模板:
@page “{id:int}”
上述代码表示,这个page匹配一个int型的id参数,比如在Pages文件夹中的Products.cshtml文件包含有这么一段代码,那它的路由将是:“/Products/123”。
一单某个请求能够正确匹配到路由,在action(或handler)被调用之前,会先调用模型绑定(model binding)和模型验证(model validation)。模型绑定负责将请求的HTTP 数据转换成对应的.NET类型。例如,如果一个action期望有个int类型的id参数,模型绑定会试图从请求的数据中获取这个参数的值,并提供给action。为了实现这个功能,模板绑定会查找post过来的值、路由中的值、query string中的值。假设id这个值找到了,在调用action之前会将其转为int类型。
在模型绑定之后和调用action方法之前,会进行模型验证(model validation)。模型验证通过在属性上添加不同的特性(或者叫注解)来实现,确保请求来的数据符合特定的要求。属性上的注解可以限制是否可空、长度限制、数值范围等。如果不符合这些要求,ModelState.IsValid
的值为false,具体不符合哪些规则会发送到请求方。
如果你需要使用模型验证,那么在每一个action上都要判断ModelState.IsValid是否为true,这很繁琐。可以通过过滤器(filter)来实现。如:
public class ValidateModelAttribute : ActionFilterAttribute
{
public override void OnActionExecuting(ActionExecutingContext context)
{
if (!context.ModelState.IsValid)
{
context.Result = new
BadRequestObjectResult(context.ModelState);
}
}
}
对于web api和mvc来说因为支持内容协商(content negotiation),允许指定请求的响应类型。根据不同的请求头,action可以返回JSON、XML或其他的响应。
Web API上应该使用**[ApiController]**特性,它可以用在controller上,base controller上或者整个assembly上(参见这里)。这个特性可自动进行模型验证,任何验证不通过的模型都会返回BadRequest和具体的错误信息。这个特性要求所有的action都有特性路由而不是约定路由。
Resilient Connections
使用SQL数据库时有时可能会遇到不可用的情况,这时我们应该增加重试机制。在ConfigureServices启用SQL的重试:
// Startup.cs from any ASP.NET Core Web API
public class Startup
{
public IServiceProvider ConfigureServices(IServiceCollection services)
{
//...
services.AddDbContext<OrderingContext>(options =>
{
options.UseSqlServer(Configuration["ConnectionString"],
sqlServerOptionsAction: sqlOptions =>
{
sqlOptions.EnableRetryOnFailure(
maxRetryCount: 5,
maxRetryDelay: TimeSpan.FromSeconds(30),
errorNumbersToAdd: null);
});
});
}
//...
}
Caching
ASP.NET Core Response Caching
支持两种级别的相应缓存。
第一种:服务端不做任何缓存,但是通过添加HTTP头的方式让客户端或代理服务器进行缓存,实现方式通过添加ResponseCache特性:
[ResponseCache(Duration = 60)]
public IActionResult Contact()
{ }
ViewData["Message"] = "Your contact page.";
return View();
}
会在相应头里添加以下代码Cache-Control: public,max-age=60
,客户端收到头之后会把数据缓存60s。
第二种:服务端缓存,添加Microsoft.AspNetCore.ResponseCaching
nuget包。然后添加缓存的中间件,如下:
public void ConfigureServices(IServiceCollection services)
{
services.AddResponseCaching();
}
public void Configure(IApplicationBuilder app)
{
app.UseResponseCaching();
}
然后这个中间件会根据一些列的条件进行缓存,当然你也可以自定义这些条件。默认只有GET或者HEAD请求且返回状态是200(OK)的响应才会被缓存。详见:complte list of caching conditions used by the response caching middleware.
Data Cachinig
除了缓存整个响应之外,我们更多的是只缓存某些数据。可以使用memory chach或者distributed cache。以下章节讲述如何实现memroy cache。
首先安装Microsoft.Extensions.Caching.Memory
nuget包,然后在ConfigureServices添加对应的支持:
public void ConfigureServices(IServiceCollection services)
{
services.AddMemoryCache();
services.AddMvc();
}
在任何要使用的地方都可以通过依赖注入获得ImemoryCache对象。在下面的例子中CachedCatalogService使用代理模式:
public class CachedCatalogService : ICatalogService
{
private readonly IMemoryCache _cache;
private readonly CatalogService _catalogService;
private static readonly string _brandsKey = "brands";
private static readonly string _typesKey = "types";
private static readonly string _itemsKeyTemplate = "items-{0}-{1}-{2}-{3}";
private static readonly TimeSpan _defaultCacheDuration =TimeSpan.FromSeconds(30);
public CachedCatalogService(IMemoryCache cache,CatalogService catalogService)
{
_cache = cache;
_catalogService = catalogService;
}
public async Task<IEnumerable<SelectListItem>> GetBrands()
{
return await _cache.GetOrCreateAsync(_brandsKey, async entry=>
{
entry.SlidingExpiration = _defaultCacheDuration;
return await _catalogService.GetBrands();
});
}
public async Task<Catalog> GetCatalogItems(int pageIndex, int itemsPage, int? brandID, int? typeId)
{
string cacheKey = String.Format(_itemsKeyTemplate,pageIndex, itemsPage, brandID, typeId);
return await _cache.GetOrCreateAsync(cacheKey, async entry=>
{
entry.SlidingExpiration = _defaultCacheDuration;
return await _catalogService.GetCatalogItems(pageIndex,itemsPage, brandID, typeId);
});
}
public async Task<IEnumerable<SelectListItem>> GetTypes()
{
return await _cache.GetOrCreateAsync(_typesKey, async entry=>
{
entry.SlidingExpiration = _defaultCacheDuration;
return await _catalogService.GetTypes();
});
}
}
为了让程序能够使用带有缓存的服务,也能使用不带有缓存的服务,应在ConfigureServices加入以下代码:
services.AddMemoryCache();
services.AddScoped<ICatalogService, CachedCatalogService>();
services.AddScoped<CatalogService>();
一旦使用了缓存就会存在旧数据的问题,即数据已经改变了但缓存的还是之前的。简单的解决方法就是设置缓存的失效时间,另一种办法就是手动移除旧的缓存,如_cache.Remove(cacheKey);
。
如果你的程序提供了一个方法用来更新数据(这些数据被缓存),当更新的时候你可以通过chacheKey移除相关的缓存,但是如果你要移除的缓存有很多呢?可以使用CancelllationChangeToken。你可以取消这个token,让所有相关的缓存都全部过期:
// configure CancellationToken and add entry to cache
var cts = new CancellationTokenSource();
_cache.Set(“cts”, cts);
_cache.Set(cacheKey,itemToCache,new CancellationChangeToken(cts.Token));
...
// elsewhere, expire the cache by cancelling the token
_cache.Get<CancellationTokenSource>(“cts”).Cancel();
在使用缓存之前一定要认证评估数据访问和页面性能,在你知道的需要明确提升的地方使用缓存。缓存会消耗服务器的内存、增加程序的复杂度,所以不要在过早的时候就使用缓存,只有在合适的时候。
上一篇: 多个项目公用组件库设计
下一篇: 小游戏后端服务架构设计心得