九、路由
在本章中,我们将介绍:
- 使用约定路由创建路由
- 使用属性路由创建路由
- 使用 IRouter 创建自定义路由
- 创建路径约束
- 创建域路由
- 创建 seo 友好的路线
在 ASP.NET Core
在 MVC 之前,URL 代表一个物理的.ASPX文件,但是 MVC 添加了一个抽象层来映射 URL 与控制器和动作。 其目标是以 REST 的方式调用调用资源的方法,而不是调用页面。
在 MVC 框架中,无论使用何种技术,路由系统都将传入的 URL 映射为对应于动作控制器的 URL 路由模式。 路由系统不能处理物理文件。
所有的 URL 模式都存储在一个名为routecollection的对象中。
如果没有匹配,将返回一个 HTTP 404 错误。
路由系统与 IIS 连接。
HTTP.sys是 IIS 中的监听进程,它被重定向到UrlRouting模块,而UrlRouting模块又被重定向到 asp.net.NET 和 asp.net.NET MVC 管道。
因为 ASP.NET Core
现在,它是不同的。 缺省情况下不创建缺省路由,当路由不匹配时,缺省情况下不返回 404 错误。 路由系统作为中间件添加到 HTTP 管道中。
在 Windows 上,IIS 只会使用 IIS 映射处理程序上的aspnetcore模块重定向到kestrel或任何其他 web 服务器。
如果客户端请求的 URL 匹配路由集合中的现有路由,则调用其处理程序的RouteAsync方法,如果有异常处理请求,则尝试下一个路由模式,而不是直接返回 404 错误。
MVC 和 Web API 路由
最初,MVC 和 Web API 并不共享相同的路由框架。 默认情况下,Web API 路由是 RESTful。
因为 ASP.NET Core、MVC 和 Web API 已经统一为一个框架。 MVC 和 Web API 都继承自相同的控制器基类,它们的路由系统也被统一用于许多情况:
- 自托管
- 内存中的本地请求(对单元测试的)
- WebServer 主机(IIS, NGinx, Azure 等)
路线顺序
路由添加到路由表的顺序很重要。
最佳实践是按照以下顺序添加路由:
- 忽略路线
- 具体的路线
- 一般的路线
路由与 ASP.NET Core
为了在应用中使用路由中间件,我们将Microsoft.AspNetCore.Routing添加到project.json中,并将services.AddRouting()添加到Startup.cs的ConfigureServices方法中。
使用约定路由创建路由
在本食谱中,我们将学习如何使用约定路由创建路由。
怎么做……
我们有两种方法。 使用约定路由创建路由的第一种方法如下:
- 将
services.AddMvc()加到ConfigureServices:
app.AddMvc();
- 我们添加
app.UseMvc()来配置路由定义:
app.UseMvc(routes =>
{
routes.MapRoute(
name: "default",
template: "{controller=Home}/{action=Index}/{id?}");
});
使用约定路由创建路由的第二种方法如下:
- 将
services.AddMvc()加到ConfigureServices:
app.AddMvc();
- 现在,是时候在
Configure()方法中添加app.UseMvc()方法调用了:
app.UseMvc();
- 我们将属性路由添加到控制器和/或动作:
[Route("api/[controller]")]
public class ValuesController : Controller
它是如何工作的…
我们可以通过在configure方法中向routescollection添加路由,并在动作级别用属性路由完成或覆盖路由定义,从而将第一种方法与第二种方法混合在一起。
使用属性路由创建路由
在本食谱中,我们将学习如何使用属性路由创建路由。
准备
我们创建一个带有action方法的控制器,用路由属性装饰它。
怎么做……
属性路由是通过在控制器的action方法之上添加定义路由的属性来定义路由的能力。
- 首先,让我们为一个指定
id参数的action方法添加一个路由属性:
//Route: /Laptop/10
[Route("Products/{id}")]
public ActionResult Details(int id)
- 接下来,让我们添加一个可选参数:
//Route: /Products/10/Computers or /Products/10
[Route("Products/{id}/{category?}")]
public ActionResult Details(int id, string category)
We can see how to do that for a RESTfull Web API method :
// GET api/values/5
[HttpGet("{id?}")]
public string Get(int? id)
- 现在,让我们加上
RoutePrefix。 这个属性将应用于这个控制器的每个动作:
[Route("Products")]
public class HomeController : Controller
{
//Route: /Products/10
[Route("{id?}")]
public ActionResult Details(int? id)
{
return View();
}
}
- 最后,我们添加一个
Order属性。 属性路由中的Order参数与按照惯例添加路由时routecollection参数插入的顺序一致。Name参数允许我们覆盖ActionMethod名称:
[Route("Products")]
public class HomeController : Controller
{
[Route("Index", Name = "ProductList", Order = 2)]
public ActionResult Index() { return View(); }
[Route("{id?}", Name = "ProductDetail", Order = 1)]
public ActionResult Details(int? id) { return View(); }
}
使用 IRouter 创建自定义路由
在本教程中,我们将学习如何创建自定义路由。
准备
在 MVC 6 中,我们将使用 IRouter 而不是 IRouteHandler 和 IHttpHandler 来定义路由。 ASP.NET Core 使用 IRouter 的递归实现。 在较高的级别上,它可以编写 HTTP 响应,在较低的级别上,它将我的路由表添加到路由中间件。
怎么做……
- 首先,我们创建的类必须实现 IRouter 和
RouteAsync方法:
public class ProductsRouter : IRouter
{
private readonly Route _innerRoute;
public VirtualPathData GetVirtualPath
(VirtualPathContext context)
{ return null; }
public Task RouteAsync(RouteContext context)
{
// Test QueryStrings
var qs = context.HttpContext.Request.Query;
var price = qs["price"];
if(string.IsNullOrEmpty(price))
{ return Task.FromResult(0); }
var routeData = new RouteData();
routeData.Values["controller"] = "Products";
routeData.Values["action"] = "Details";
routeData.DataTokens["price"] = price;
context.RouteData = routeData;
return _innerRoute.RouteAsync(context);
}
}
- 接下来,让我们创建一个
extension方法,将该路由添加到routeBuilder:
public static class Extensions {
public static IRouteBuilder AddProductsRoute(
this IRouteBuilder routeBuilder, IApplicationBuilder app)
{
routeBuilder.Routes.Add(
new Route(new ProductsRouter(),
"products/{lang:regex(^([a-z]{{2}})-([A-Z]{{2}})$)}/
{category:alpha}/{subcategory:alpha}/{id:guid}", app.ApplicationServices.GetService(
typeof(IInlineConstraintResolver))
as IInlineConstraintResolver
));
return routeBuilder;
}
}
Regex makes it relatively simple to check whether a string value matches certain rules. Also, we can get certain parts of a string value after some rules have been applied to it. With Regex, we can create a rule to be applied to a route URL. You can learn more at https://regexr.com/.
- 最后,让我们在
Startup.cs的Configure方法中将该路由添加到路由集合中:
public void Configure(IApplicationBuilder app)
{
var routeBuilder = new RouteBuilder(app);
routeBuilder.AddProductsRoute(app);
app.UseRouter(routeBuilder.Build());
}
- 我们必须匹配这个动作方法签名:
public IActionResult Details
(string lang, string category, string subcategory, Guid id)
创建路径约束
在本教程中,我们将学习如何使用约定路由创建路由约束。
准备
一个路由约束被插入一个内联语法,称为内联约束,形式如下:{parameter:constraint}。
有几种方法可以创建路径约束:
- 以常规方式创建路由时插入约束
- 在创建属性路由时插入约束
- 创建实现
IRouteConstraint的类
怎么做……
- 这是第一种方法,在常规创建路由时插入约束:
app.UseMvc(routes =>
{
routes.MapRoute(
name: "default",
template: "{controller=Home}/{action=Index}/{id?}"
);
routes.MapRoute(
name: "products",
template: "Products/{id=1}",
defaults: new { controller = "Product", action = "Details" }
);
});
- 这是第二种方式,通过属性路由插入约束:
// for an api controller
[Route("api/[controller]")]
public class ProductValuesController : Controller
{
// GET api/productvalues/5
[HttpGet("{id}")]
public string Get(int id)
{
return "value";
}
// PUT api/productvalues/laptop
[HttpPut("{name:alpha:length(3)}")]
public void UpdateProductName(string name) { }
}
// for a MVC controller
[Route("[controller]")]
public class HomeController : Controller
{
// GET home/index/5
[Route("{id?}")]
public IActionResult Index(int? id) { return View(); }
[Route("{name:alpha:length(3)}")]
public IActionResult UpdateProductName(string name)
{ return View(); }
}
- 这是第三种方法,创建一个实现
IRouteConstrains的类:
public class ValuesConstraint : IRouteConstraint
{
private readonly string[] validOptions;
public ValuesConstraint(string options)
{
validOptions = options.Split('|');
}
public bool Match(HttpContext httpContext, IRouter route, string routeKey, RouteValueDictionary values, RouteDirection routeDirection)
{
object value;
if (values.TryGetValue(routeKey, out value) && value != null)
{
return validOptions.Contains(value.ToString(), StringComparer.OrdinalIgnoreCase);
}
return false;
}
}
下面是一个基于正则表达式创建路由约束的例子:
public class RegexLangConstraint : RegexRouteConstraint
{
public RegexLangConstraint() : base(@"^([a-z]{2})-([A-Z]{2})$")
{
}
}
该约束将使用以下模式检查语言参数:[fr-FR]或[en-US]。
让我们看看如何将这个约束应用到路由定义中:
routes.MapRoute(
name: "fr_products",
template: "products/{lang}/{category:alpha}/{id:guid}",
defaults: new { controller = "Products", action = "Details" },
constraints: new { lang = new RegexLangConstrain() });
它是如何工作的…
下面是一个用于约定路由或属性路由的约束的非详尽列表:
int :{id:int}bool :{isActive:bool}datetime :{dateBirth:datetime}decimal :{price:decimal}guid :{id:guid}required :{userName:required}- 【t】【t】
minlength(value) : {userName:minlength(8)}maxlength(value) : {userName:maxlength(20)}length(min,max) :{userName:length(8,20)}min(value) :{age:min(18)}max(value) :{age:max(100)}range(min,max) :{age:range(18,100)}regex(expression) :{ssn:regex(^d{3}-d{3}-d{4}$)}
创建域路由
在这个食谱中,我们将学习如何创建一个域路由。
准备
在一些场景中,域路由可能非常有用和有趣:
- 在使用多种语言的 web 应用时,url 可能如下所示:
route like "www.{language}-{culture}.domain.com" or "www.{language}.domain.com"
- 当使用多租户 web 应用时,url 可以如下所示:
route like "www.{controller}.domain.com", "www.{area}.domain.com", "www.{subentity}.domain.com"
怎么做……
让我们添加新的路线到Startup.cs:
public void Configure(IApplicationBuilder app)
{
app.UseMvc(routes =>
{
routes.MapRoute(
name: "DomainRoute",
template: "home.domain.com/{action=Index}/{id?}");
routes.MapRoute(
name: "DomainRoute2",
template: "{controller=Home}.domain.com/{id?}");
routes.MapRoute(
name: "DomainRoute3",
template: "{controller=Home}-{action=Index}.domain.com/{id?}");
}
}
创建 seo 友好的路线
在这个食谱中,我们将学习如何创建搜索引擎友好的路线。 为此,我们可以使用 MVC 路由系统、IISUrlRewriting模块或两者都使用。
准备
在某些情况下,SEO 是必须考虑的。
我们正在创建一个新的版本的网站与 MVC。 但是我们有很多遗留 url 已经被使用,或者是在 MVC 版本的应用之前被记录的。 我们也花了很多钱购买关键字到摩托搜索谷歌抛出所有这些 url。
为了解决这个问题,我们将使用 IISUrlRewriting模块将遗留 url 作为一个操作重定向到相应的控制器。
怎么做……
- 首先,我们必须使用 Web 平台安装程序安装
UrlRewriting模块:
<assets/8a470c5d-48bf-4c9e-ad57-e3a0264c3589.png>
- 接下来,我们添加一条规则,将遗留 URL(如
http://localhost:1962/products.ASPX?lang=fr-FR&category=smartphone&subcategory=samsung&id=cadc5808-75a7-4428-b491-3cacfe37d9ce)重定向到现有的 MUV 路由:http://localhost:1962/fr-FR/products/smartphone/samsung/cadc5808-75a7-4428-b491-3cacfe37d9ce。
MVC 路由有以下模板模式:
http://localhost:1962/products/{lang}/{category}/{subcategory}/{id}。
十、ASP.NET MVC 核心
在本章中,我们将涵盖以下主题:
- 注入依赖关系并为控制器配置 IoC
- 使用 ActionResults
- 创建和处理区域
- 创建和使用 POCO 控制器
- 使用 MediatR 创建和使用控制器
- 管理异常
注入依赖关系并为控制器配置 IoC
在本教程中,您将学习如何在控制器中使用构造函数注入依赖项,以及如何配置依赖项的生存期。
准备
我们用 VS 2017 创建了一个空的 web 应用,然后添加了一个空的控制器。
让我们创建一个存储库,将硬编码的值注入到控制器中。
怎么做……
我们已经讨论了用 ASP 注入依赖关系.NET Core第五章,SOLID 原理,反转控制,依赖注入。 我们了解到 IoC 机制是 ASP 的内部机制.NET 的核心。 这是由构造函数完成的,它的生命周期必须在Startup.cs中的Configure方法中配置。 我们会做一些调整,一切都会自动工作。
- 首先,让我们看看要注入到控制器中的存储库:
public interface IProductRepository
{
int GetCountProducts();
}
public class ProductRepository : IProductRepository
{
public int GetCountProducts()
{
return 10;
}
}
正如我们所看到的,这个存储库只有一个方法来检索字符串列表。
- 接下来,让我们将这个存储库注入到控制器构造函数中。 在
ProductController.cs中,我们将开发以下代码:
public class ProductController : Controller
{
private readonly IProductRepository repo;
public ProductController(IProductRepository repo)
{
this.repo = repo;
}
public IActionResult Index()
{
var count = repo.GetCountProducts();
return View(new ValueViewModel(count));
}
}
- 现在我们将创建一个
ViewModel类和View文件:
public class ValueViewModel
{
public ValueViewModel(object val)
{
Value = val.ToString();
}
public string Value { get; set; }
}
Index.cshtml代码如下:
@model Ch10.R1.ValueViewModel
<h1>Products count @Model.Value</h1>
- 接下来,我们将在
Startup.cs的ConfigureServices方法中配置生命周期存储库:
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc();
services.AddTransient<IProductRepository, ProductRepository>();
}
-
最后,我们可以看到在操作方法中显示从存储库中检索的数据而没有错误的视图。 这是因为 ASP 的 IoC 内部机制.NET Core 自动为我们实例化回购,而不使用任何新的关键字,也不应用松耦合原则。
-
We can also inject this repository by inserting the
FromServicesattribute above the repository used as a parameter of an action method inProductController, after having configured it inConfigureServices.我们需要对
Startup.cs做如下更改:
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc();
services.AddScoped<IProductRepository, ProductRepository>();
}
让我们将ProductController.cs文件修改如下:
public class ProductController : Controller
{
public IActionResult Index([FromServices]IProductRepository repo)
{
var count = repo.GetCountProducts();
return View();
}
}
- 我们可以阅读下面的代码,并看到相同的 repo 已经通过第三方 IoC 容器注入到相同的控制器中,而不是通过内部 ASP.NET Core IoC 默认容器。
我们将创建一个IoConfig类来配置新的 IoC 容器。 当然,IoC 容器可以是 ASP 的任何现有 IoC 容器.NET Core 兼容 Unity、Ninject、Castle Windsor、StructureMap、SimpleInjector 等。 在这个例子中,我们将使用一个Autofac模块,根据 Autofac 文档:
A small class that can be used to bundle up a set of related components behind a facade to simplify configuration and deployment.
让我们按如下方式创建AutofacModule.cs文件:
public class AutofacModule : Module
{
protected override void Load(ContainerBuilder builder)
{
builder.Register(c => new ProductRepository())
.As<IProductRepository>()
.InstancePerLifetimeScope();
}
}
我们应该更改Startup.cs文件并注册Autofac模块:
using Autofac;
using Autofac.Extensions.DependencyInjection;
using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.DependencyInjection;
using System;
public class Startup
{
public IServiceProvider ConfigureServices
(IServiceCollection services)
{
services.AddMvc();
// Add Autofac
var containerBuilder = new ContainerBuilder();
containerBuilder.RegisterModule<AutofacModule>();
containerBuilder.Populate(services);
var container = containerBuilder.Build();
return new AutofacServiceProvider(container);
}
}
另外,我们应该修改ProductController.cs文件:
public class ProductController : Controller
{
private readonly IProductRepository repo;
public ProductController(IProductRepository repo)
{
this.repo = repo;
}
public IActionResult Index()
{
var count = repo.GetCountProducts();
return View();
}
}
In any class where we use IoC, we have to use only the constructor where we inject dependencies. Adding a default empty constructor will generate an exception, because the IoC container will wait for only one construction with the dependencies to inject.
- 现在让我们看看
Startup.cs中的ConfigureServices方法代码,它仍然带有Autofac,但没有模块:
public class Startup
{
public IServiceProvider ConfigureServices
(IServiceCollection services)
{
services.AddMvc();
// Add Autofac
var containerBuilder = new ContainerBuilder();
containerBuilder
.RegisterType<ProductRepository>()
.As<IProductRepository>();
containerBuilder.Populate(services);
var container = containerBuilder.Build();
return new AutofacServiceProvider(container);
}
}
- ASP.NET Core 自动将控制器注册并解析为服务; 但是,如果我们用 ASP 代替。 带有
Autofac的 NET Core DI 容器(或另一个 DI 容器),ConfigureServices中的代码应更改如下:
public class Startup
{
public IContainer AppContainer { get; private set; }
public IServiceProvider ConfigureServices
(IServiceCollection services)
{
services
.AddMvc()
.AddApplicationPart(typeof(ProductController).Assembly)
.AddControllersAsServices();
// Add Autofac
var containerBuilder = new ContainerBuilder();
containerBuilder
.RegisterType<ProductRepository>()
.As<IProductRepository>();
containerBuilder.Populate(services);
this.AppContainer = containerBuilder.Build();
return new AutofacServiceProvider(this.AppContainer);
}
}
- 我们可能想要处理在应用容器中已解析的资源。 要做到这一点,我们必须通过向
Configure方法添加以下代码来注册ApplicationStopped事件:
public void Configure(IApplicationBuilder app,
IApplicationLifetime appLifetime)
{
app.UseMvc(routes =>
{
routes.MapRoute(
name: "default",
template: "{controller=Home}/{action=Index}/{id?}");
});
appLifetime.ApplicationStopped.Register(() =>
this.ApplicationContainer.Dispose());
}
使用 ActionResults
在本食谱中,您将了解我们将在遵循 mvc 的应用中使用哪些ActionResults。
准备
我们用 VS 2017 创建了一个空的 web 应用,并添加了一个空的控制器。 我们将创建一个存储库,其中包含要注入控制器的硬编码值。
怎么做……
以下是所有类型,我们可以从控制器操作返回的特定的ActionResults:
public virtual JsonResult Json(object data)
public virtual ViewResult View()
public virtual ViewComponentResult ViewComponent(string componentName)
public virtual PartialViewResult PartialView()
public virtual ChallengeResult Challenge()
public virtual ForbidResult Forbid()
public virtual SignInResult SignIn(ClaimsPrincipal principal, string authenticationScheme)
public virtual SignOutResult SignOut(params string[] authenticationSchemes)
public virtual ContentResult Content(string content)
public virtual FileContentResult File(byte[] fileContents, string contentType)
public virtual FileStreamResult File(Stream fileStream, string contentType)
public virtual VirtualFileResult File(string virtualPath, string contentType)
public virtual FileStreamResult File(Stream fileStream, string contentType, string fileDownloadName)
public virtual LocalRedirectResult LocalRedirect(string localUrl);
public virtual PhysicalFileResult PhysicalFile(string physicalPath, string contentType)
public virtual RedirectResult Redirect(string url)
public virtual RedirectToActionResult RedirectToAction(string actionName)
public virtual RedirectToRouteResult RedirectToRoute(object routeValues)
创建和处理区域
在本食谱中,您将学习如何在 ASP 中管理区域.NET 的核心。 为此,我们将:
- 创建领域
- 创建区域路线
- 避免区域航线冲突
- 更改默认视图位置
- 为区域的动作控制器创建链接
准备
我们在 VS 2017 中创建了一个空的 web 应用。
怎么做……
在构建 MVC 应用时,有时我们需要功能分离。 我们开发的应用可能比看起来的要大,这样我们在一个应用中有几个应用。 区域使我们能够根据需要创建许多 MVC 结构,并使我们能够更轻松地管理复杂的应用。 例如,在电子商务应用中,我们可以为网站的管理部分需要不同的区域,这些区域对应于不同的角色(用户管理、营销、汽车搜索参考、订单跟踪、库存管理),当然还有网站本身。
每个区域都有自己的控制器、模型和视图文件夹,我们必须在Startup.cs中配置该区域的路由,以匹配区域结构的物理文件路径与传入 URL。
没有像以前版本的 MVC 那样的脚手架来创建区域。 所以,我们必须从零开始创建区域:
- 让我们创建一个空 ASP.NET Core Web 应用在 VS 2017。
- 接下来,我们将右键单击应用根目录,并创建该区域的结构,其中包含一个 MVC 结构文件夹,如下面的截图所示。 我们还可以添加一个经典的 MVC 结构来讨论与区域的潜在路由冲突:
<assets/add25ff9-4e15-466c-be9e-16ff773ac8dd.png>
- 接下来,让我们为区域和控制器创建路由,在
Startup.cs的Configure方法中添加如下代码:
app.UseMvc(routes =>
{
routes.MapRoute(
name: "area",
template: "{area=Products}/{controller=Home}/{action=Index}");
routes.MapRoute(
name: "default",
template: "{controller=Home}/{action=Index}/{id?}");
});
- 如果我们使用Ctrl+F5来启动应用,我们将看到以下页面:
<assets/f4ecad21-c300-4684-9583-b5888f68aa41.png>
- 对于旧版本的 ASP。 在 asp.net MVC 中,我们将在
MapRoute方法中为每个路由定义添加namespaces,以避免同名控制器之间的冲突。 ASP.NET Core MVC 中,我们更愿意使用每个控制器上面的 routing 属性来指定控制器来自某个区域,例如:
public class HomeController : Controller
{
[Area("Products")]
[Route("[area]/[controller]/[action]")]
public IActionResult Index()
{
return View();
}
}
- 为了访问
Product区域中的Home控制器,我们必须在浏览器的地址工具栏中显式地输入区域路由:
<assets/f387bda0-45d9-4466-aa97-14fc804b80e0.png>
- 如果我们想要更改
AreaViewLocation,我们必须将以下代码添加到Startup.cs的ConfiguresServices:
services.Configure<RazorViewEngineOptions>(options =>
{
options.AreaViewLocationFormats.Clear();
options.AreaViewLocationFormats.Add(
"/Products/{2}/Views/{1}/{0}.cshtml");
options.AreaViewLocationFormats.Add(
"/Products/{2}/Views/Shared/{0}.cshtml");
options.AreaViewLocationFormats.Add("/Views/Shared/{0}.cshtml");
});
We could also recreate the area’s location by overriding RazorViewEngine.
- 要生成基于区域的链接,我们可以使用
HtmlHelper或TagHelper语法:
@Html.ActionLink("Products Area Home Page",
"Index", "Home", new { area = "Products" })
<a asp-area="Products" asp-controller="Home" asp-action="Index">Products Area Home Page</a>
创建和使用 POCO 控制器
在本食谱中,您将了解什么是 POCO 控制器,以及我们为什么要使用它们。
准备
我们在 VS 2017 中创建了一个空的 web 应用。
怎么做……
POCO 控制器是带有两个属性的简单类:
[Controller]属性,用于类本身或其基类- 一个
routing属性,在应用中定义其路由
它们不继承自Controller类,因此它们将不能返回任何 MVC 或 WebAPI 结果作为视图、HttpStatus代码或任何继承自IActionResult的类型。
我们可以将它们放在应用的任何位置,项目的根目录,或者名为POCOController的文件夹中。 在这个练习中,我们将创建一个类,并通过在它上面添加Controller属性,按照给定的步骤创建一个控制器:
- 我们将添加
Microsoft.AspNetCore.MvcNuget 包到项目:
"Microsoft.AspNetCore.Mvc": "2.0.0"
- 我们将添加 MVC 服务,并在
Startup.cs中使用它:
public class Startup
{
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc();
}
public void Configure(IApplicationBuilder app)
{
app.UseMvc();
}
}
- 现在,让我们添加一个
POCO控制器:
[Controller]
[Route("api/[controller]")]
public class PocoCtrl
{
[HttpGet]
public string Get()
{
return "This is a POCO Controller";
}
}
- 让我们看看这个控制器是否工作:
<assets/cb41ac07-e458-4d9e-a1da-fd18565eb678.png>
- 现在,让我们创建另一个
POCO控制器,通过创建一个由controller装饰的基类和routing属性装饰的基类,作为一个继承自基类但没有装饰的简单类:
[Controller]
public class PocoCtrlBase { }
[Route("api/[controller]")]
public class PocoCtrlInherits : PocoCtrlBase
{
[HttpGet]
public string Get()
{
return "This is a POCO Controller inherited";
}
}
- 让我们看看控制器是否工作:
<assets/8510e783-1d90-439c-b544-735f049631e9.png>
The goal of the POCO controller is to create a lightweight controller which includes only action methods.
使用 MediatR 创建和使用控制器
在本菜谱中,您将学习在没有服务和存储库的情况下使用 MVC 控制器的另一种方法。
This recipe could be applied to the WebAPI controller. We could add that it’s logical to mix MVC and WebAPI practices in the same controller. Now, there’s no difference between them.
准备
我们在 VS 2017 中创建了一个空的 web 应用。
怎么做……
- 首先,让我们在
project.json中添加MediatR 依赖注入包。 它将包括 MediatR 4.0.0 包:
"MediatR.Extensions.Microsoft.DependencyInjection": "4.0.0"
我们还需要AutoMapper包将Business models映射到ViewModels。 我们将在第 13 章,Views,Models,and ViewModels中更详细地讨论AutoMapper:
"AutoMapper.Extensions.Microsoft.DependencyInjection": "3.2.0"
- 接下来,让我们在
Startup.cs中添加一些配置:
public class Startup
{
public void ConfigureServices(IServiceCollection services)
{
var connection = @"Data Source=MyServer;Initial
Catalog=CookBook;Integrated Security=True";
services.AddDbContext<CookBookContext>(
options => options.UseSqlServer(connection));
services.AddMvc();
services.AddAutoMapper(StartupAssembly());
services.AddMediatR(StartupAssembly());
}
public void Configure(IApplicationBuilder app)
{
app.UseDeveloperExceptionPage();
app.UseMvc(routes =>
{
routes.MapRoute(
name: "default",
template: "{controller=Book}/{action=Index}/{id?}");
});
}
private static Assembly StartupAssembly()
{
return typeof(Startup).GetTypeInfo().Assembly;
}
}
我们添加了 MVC、实体框架、Automapper 和 MediatR 作为 ASP 的服务.NET Core 应用。 我们现在可以使用它们。
- 接下来是
DbContext和Business对象:
public class CookBookContext : DbContext
{
public CookBookContext(DbContextOptions<CookBookContext> options)
: base(options) { }
public DbSet<Book> Book { get; set; }
}
public class Book
{
public int Id { get; set; }
public string Name { get; set; }
public decimal Price { get; set; }
}
- 现在,让我们创建用于读取数据的
Query对象。 我们将使用 MediatR 中的两个类来完成此工作:一个是继承自IAsyncRequest的Query类,另一个是继承自IAsyncRequestQueryHandler的Handler类。 这个Handler类实现了IAsyncRequestQueryHandler中的Handle方法,该方法接受Query类作为参数,并返回ViewModel以显示。 下面是这些类的代码:ViewModels:
public class BookIndexViewModel
{
public int Id { get; set; }
public string Name { get; set; }
public decimal Price { get; set; }
}
public class BookListIndexViewModel
{
public List<BookIndexViewModel> BookList { get; set; }
public string Message { get; set; }
}
public class BookListIndexQuery : IAsyncRequest<BookListIndexViewModel>{ }
public class BookListIndexQueryHandler : IAsyncRequestHandler<BookListIndexQuery, BookListIndexViewModel>
{
private readonly CookBookContext _context;
public BookListIndexQueryHandler(CookBookContext context)
{
_context = context;
}
public async Task<BookListIndexViewModel>
Handle(BookListIndexQuery query)
{
var books = await _context.Book.ToListAsync();
var model = new BookListIndexViewModel
{
BookList = await _context.Book
.ProjectTo<BookIndexViewModel>()
.ToListAsync()
};
return model;
}
}
- 现在,让我们来看看控制器的代码:
public class BookController : Controller
{
private readonly IMediator _mediator;
public BookController(IMediator mediator)
{
_mediator = mediator;
}
public async Task<IActionResult> Index(BookListIndexQuery query)
{
var model = await _mediator.SendAsync(query);
return View(model);
}
}
- 接下来,我们将在
Views | Book | Index.cshtml文件中创建视图:
@model Ch10.R5.MediatR.BookListIndexViewModel
@if (Model.BookList.Any())
{
<div>Books available</div>
foreach (var book in Model.BookList)
{
<div>Id : @book.Id</div>
<div>Name : @book.Name</div>
<div>Price : @book.Price</div>
}
}
- 让我们看看结果:
<assets/dddc4e1b-159c-4337-ac2c-9a48c2324044.png>
- 现在,我们创建
ViewModel、Command和CommandHandler类,对数据库进行插入操作。
public class BookAddViewModel
{
[Required(ErrorMessage = "A name is required for the book")]
[StringLength(50, ErrorMessage =
"The name of the book must not exceed 50 characters")]
public string Name { get; set; }
[Required(ErrorMessage = "A price is required for this book")]
[RegularExpression(@"^d+(.d{1,2})?$")]
[Range(0.1, 100)]
public decimal Price { get; set; }
}
-
Command:
public class BookAddCommand : IAsyncRequest<Result>
{
public string Name { get; }
public decimal Price { get; }
}
public class BookAddCommandHandler : IAsyncRequestHandler<BookAddCommand, Result>
{
private readonly CookBookContext _context;
public BookAddCommandHandler(CookBookContext context)
{
_context = context;
}
public async Task<Result> Handle(BookAddCommand command)
{
var book = new Book
{
Name = command.Name,
Price = command.Price
};
_context.Book.Add(book);
await _context.SaveChangesAsync();
var result = new Result { Success = true };
return result;
}
}
public class Result
{
public bool Success { get; set; }
public string ErrorMessage { get; set; }
}
- 让我们看看新控制器的代码:
public class BookController : Controller
{
private readonly IMediator _mediator;
private readonly IMapper _mapper;
public BookController(IMediator mediator, IMapper mapper)
{
_mediator = mediator;\
_mapper = mapper;
}
public async Task<IActionResult> Index(BookListIndexQuery query)
{
var model = await _mediator.SendAsync(query);
return View(model);
}
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Add(BookAddViewModel model)
{
if (ModelState.IsValid)
{
var command = new BookAddCommand();
command = _mapper
.Map<BookAddViewModel, BookAddCommand>(model);
var result = await _mediator.SendAsync(command);
if (result.Success)
{
return RedirectToAction("Index");
}
ModelState.AddModelError(string.Empty, result.ErrorMessage);
}
return View(model);
}
}
- 最后,我们可以在创建了
_ViewImports.cshtml文件后将这段代码添加到View中:
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
<div>
<form asp-controller="Book" asp-action="Add" method="post" role="form">
<label>Book Name</label><input type="text" name="Name" /><br />
<label>Book Price</label><input type="text" name="Price" /><br />
<input type="submit" value="Add Book" />
</form>
</div>
- 我们可以这样构造这个项目:
<assets/1be35190-bf8a-44e3-ae1f-8069af307f4f.png>
它是如何工作的…
MediatR是通过应用一些命令查询职责隔离(或分离)概念获得的中介模式的实现。 这个 NuGet 包是由 Jimmy Bogard 创建的,他也创建了 AutoMapper。
CQRS 将我们的代码分为两个不同的部分:命令和查询(读取代码和持久写入代码)。
以下是关于 CQRS 模式优势的概述:
- 分离读和写:我们可以将这个概念传播到我们的数据库配置中,以便有一个更快的只读(可能是非规范化的)数据库用于读,另一个用于写。 问题将是同步两者,但有几种方法可以做到这一点。
- 有了 Azure,在我们的软件架构中很容易将服务总线与消息系统结合起来。 在功能需求方面,您添加的责任越多,就越增加其可维护性; 然而,如果您决定不承担任何责任,那么您就会拥有一个更轻量的、可维护性更低的微服务架构。
- CQRS 模式使我们的代码更具可读性、可测试性和可维护性。
- 相反,它会降低生产力和可读性,并增加复杂性。
There is no good or bad design pattern (except anti-pattern). They have to resolve a problem, and match with our project requirements. So, we don’t have to follow a pattern or an architectural style by mode, but analyze seriously the pros and the cons of the pattern we apply, and ask ourselves the question: Are they really relevant for my project?
在 MediatR 中,称为处理程序的类管理查询和命令消息。 我们不使用 CQRS 中使用的事件溯源或领域驱动设计概念。 查询类用于读取,命令类用于插入、更新和删除操作。 命令和查询有它们自己的 viewmodel,也有它们自己的处理程序类,以便与数据库、服务、第三方服务等进行通信。
MediatR 模式给我们带来以下结果:
- 我们应用单一责任原则,让控制器专注于管理 HTTP 调用,而不是承担许多其他职责,比如查询数据库、在表中插入一行,等等。
- 不再需要存储库来抽象数据存储,也不再需要服务层来放入控制器的逻辑中。 关注的分离更好。
管理异常
在本菜谱中,您将学习如何以一种原始的方式管理异常。
准备
我们在 VS 2017 中创建了一个空的 web 应用。
怎么做……
- 首先,我们将创建一个
Result类。 这个类将被Service层返回。 它允许我们管理要记录的错误消息并显示到视图:
public class Result
{
public bool IsSuccess { get; }
public string SuccessMessageToLog { get; set; }
public string ErrorToLog { get; }
public string ErrorToDisplay { get; set; }
public ErrorType? ErrorType { get; }
public bool IsFailure => !IsSuccess;
protected Result(bool isSuccess, string error, ErrorType? errorType)
{
if ((isSuccess && error != string.Empty) ||
(isSuccess && !errorType.HasValue))
throw new InvalidOperationException();
if ((!isSuccess && error == string.Empty) ||
(isSuccess && errorType.HasValue))
throw new InvalidOperationException();
IsSuccess = isSuccess;
ErrorToLog = error;
}
public static Result Fail(string message)
{
return new Result(false, message, null);
}
public static Result Fail(ErrorType? errorType)
{
return new Result(false, string.Empty, errorType);
}
public static Result Fail(string message, ErrorType? errorType)
{
return new Result(false, message, errorType);
}
public static Result Ok()
{
return new Result(true, string.Empty, null);
}
}
public enum ErrorType
{
DatabaseIsOffline,
CustomerAlreadyExists
}
- 接下来,让我们创建
Product,ProductContext和ProductInputViewModel,我们将在后面使用:
public class Product
{
public int Id { get; set; }
public string Name { get; set; }
public decimal Price { get; set; }
}
public class ProductContext : DbContext
{
public ProductContext(DbContextOptions<ProductContext> options) : base(options){ }
public DbSet<Product> Product { get; set; }
}
public class ProductInputViewModel
{
public string Name { get; set; }
public decimal Price { get; set; }
}
- 接下来,我们将添加
ProductService类,其中我们将使用Result类来管理可能发生的异常,以及记录并显示到用户界面的消息:
public class ProductService : IProductService
{
private readonly ILogger _logger;
private readonly ProductContext _context;
public ProductService(ILogger logger, ProductContext context)
{
_logger = logger;
_context = context;
}
public Result CreateProduct(ProductInputViewModel productViewModel)
{
var product = new Product()
{
Name = productViewModel.Name,
Price = productViewModel.Price
};
Result result = SaveProduct(product);
if (result.IsFailure && result.ErrorType.HasValue)
{
switch (result.ErrorType.Value)
{
case ErrorType.DatabaseIsOffline:
Log(result);
result.ErrorToDisplay = "Unable to connect to the
database. Please try again later";
break;
case ErrorType.CustomerAlreadyExists:
Log(result);
result.ErrorToDisplay = "A product with the name " +
productViewModel.Name + " already exists";
break;
default:
throw new ArgumentException();
}
}
return result;
}
private Result SaveProduct(Product product)
{
try
{
_context.Product.Add(product);
_context.SaveChanges();
return Result.Ok();
}
catch (DbUpdateException ex)
{
if (ex.Message == "Unable to open the DB connection")
return Result.Fail(ex.Message,
ErrorType.DatabaseIsOffline);
if (ex.Message.Contains("IX_Customer_Name"))
return Result.Fail(ex.Message,
ErrorType.CustomerAlreadyExists);
throw;
}
}
private void Log(Result result)
{
if (result.IsFailure)
_logger.LogError(result.ErrorToLog);
else
_logger.LogInformation(result.SuccessMessageToLog);
}
}
- 最后添加
ProductController:
public class ProductController : Controller
{
private readonly IProductService _service;
public ProductController(IProductService service)
{
_service = service;
}
[HttpGet]
public ActionResult Index()
{
return View();
}
[HttpPost]
public ActionResult CreateProduct(ProductInputViewModel product)
{
Result productResult = _service.CreateProduct(product);
if (productResult.IsFailure)
{
ModelState.AddModelError(string.Empty,
productResult.ErrorToDisplay);
return View();
}
return RedirectToAction("Index");
}
}
十一、Web API
在本章中,我们将涵盖以下主题:
- 使用 ActionResults
- 配置内容协商
- 配置跨域起始请求
- 使用轻便
- 测试 Web api
- 管理异常
使用 ActionResult
在本食谱中,您将学习如何使用ActionResult返回 Web API。 ActionResult是一种核心类型的 MVC,用于从服务器返回结果到客户端。 ActionResult是一个基类和它的抽象类,所以我们可以使用它的派生类之一,例如JsonResult、ViewResult、RedirectResult或FileResult。
准备
我们将创建一个带有CRUD方法的 Web API 控制器,以理解每个 HTTP 谓词必须返回的ActionResult内容。
怎么做……
ASP.NET Core MVC 和 Web api 合并,基类现在是相同的。 ActionResults现在返回 HTTP 状态码的结果,就像ApiController基类在 ASP. xml 之前返回.NET 的核心。
我们将使用ActionResults来返回带有CRUDWeb API 控制器的 HTTP 状态代码:
-
首先,让我们通过创建一个空 Web 应用来创建 Web API 应用。
-
接下来,我们将添加 ASP.NET Core MVC 依赖于项目:
"Microsoft.AspNetCore.MVC": "2.0.0",
- 接下来,我们将以下代码添加到
Startup.cs中。 这段代码将允许我们使用 Web API 的控制器:
public void ConfigureServices(IServiceCollection services)
{
services.AddMVC();
}
public void Configure(IApplicationBuilder app)
{
app.UseMVC();
}
- 接下来,我们将创建一个在这个 Web API 控制器中使用的存储库:
public interface IProductRepository
{
IEnumerable<Product> GetAllProducts();
void Add(Product product);
Product Find(int id);
void Update(Product product);
void Remove(int id);
}
- 接下来,让我们在
Startup.cs中注册和配置存储库生命周期:
services.AddScoped<IProductRepository, ProductRepository>();
- 接下来,让我们创建 Web API 控制器:
[Route("api/[controller]")]
public class ProductApiController : Controller
{
private readonly IProductRepository _productRepository;
public ProductApiController(IProductRepository repository)
{
_productRepository = repository;
}
}
- 现在我们将添加
GET方法。 其路线为api/productapi:
[HttpGet]
public IActionResult Get()
{
var productsFromRepo = _productRepository.GetAllProducts();
if (productsFromRepo == null)
return NotFound();
// return HTTP response status code 404
return Ok(productsFromRepo);
// return HTTP status code 200
}
- 现在我们将添加不同路径的
GET方法api/productapi/{id}:
[HttpGet("{id:int}")]
public IActionResult Get(int id)
{
var productFromRepo = _productRepository.Find(id);
if (productFromRepo == null)
return NotFound();
// return HTTP response status code 404
return Ok(productFromRepo);
// return HTTP response status code 200
}
- 接下来,我们将添加
POST方法。 它的路由为api/productapi,在请求体中有一个Product对象:
[HttpPost]
public IActionResult Post([FromBody]Product product)
{
if (product == null)
return BadRequest();
// return HTTP response status code 400
_productRepository.Add(product);
return CreatedAtRoute("GetProduct",
new { id = product.Id }, product);
// return HTTP response status code 201
}
- 接下来,我们将添加
PUT方法。 它的路由为api/productapi/{id},在请求体中有一个Product对象:
[HttpPut("{id}")]
public IActionResult Put(int id, [FromBody]Product product)
{
if (product == null || product.Id != id)
return BadRequest();
// return HTTP response status code 401
var productFromRepo = _productRepository.Find(id);
if (productFromRepo == null)
return NotFound();
// return HTTP response status code 404
_productRepository.Update(product);
return new NoContentResult();
// return HTTP response status code 204
}
- 接下来,我们将添加
PATCH方法。 它的路由为api/productapi/{id},在请求体中有一个Product对象。 当我们进行部分更新时,使用PATCH动词:
[HttpPut("{id}")]
public IActionResult Patch(int id, [FromBody]Product product)
{
if (product == null)
return BadRequest();
// return HTTP response status code 400
var productFromRepo = _productRepository.Find(id);
if (productFromRepo == null)
return NotFound();
// return HTTP response status code 404
productFromRepo.Id = product.Id;
_productRepository.Update(product);
return new NoContentResult();
// return HTTP response status code 204
}
- 接下来,我们将添加
DELETE方法。 其路线为api/productapi/{id}:
[HttpDelete("{id}")]
public IActionResult Delete(int id)
{
var productFromRepo = _productRepository.Find(id);
if (productFromRepo == null)
return NotFound();
// return HTTP response status code 404
_productRepository.Remove(id);
return new NoContentResult();
// return HTTP response status code 204
}
它是如何工作的…
IActionResult由ActionResult抽象类实现。 ActionResult抽象类用于创建多个类,向调用方客户端返回预定义的 HTTP 状态码,如下所示:
OkResult:返回200HTTP 状态码CreatedResult:返回201HTTP 状态码CreatedAtActionResult:返回201HTTP 状态码CreatedAtRouteResult:返回201HTTP 状态码NoContentResult:返回204HTTP 状态码BadRequestResult:返回400HTTP 状态码UnauthorizedResult:返回401HTTP 状态码NotFoundResult:返回404HTTP 状态码
ControllerBase类具有针对每种结果类型的几个方法。 我们可以从 Web API 中调用其中一个来返回所需的状态代码,比如下面的代码:
return Ok(); // returns OkResult
return Created(); // returns CreatedResult
return NotFound(); // returns NotFoundResult
配置内容协商
在本食谱中,您将学习如何在 ASP 中管理内容协商.NET 的核心。 ASP。 由于ContentFormatters,NET Core 可以以任何格式(如 JSON、XML、PLIST 和 SOAP)返回数据。 更多信息请参见https://docs.microsoft.com/en-us/aspnet/core/mvc/advanced/custom-formatters。
任何客户机都可以发出一个带有头的请求,以告诉服务器它希望响应的格式。 服务器应用可以读取报头值,并在创建对请求的响应时使用它。 内容协商是整个流程的名称。
准备
默认情况下,ASP.NET Core 将从动作方法返回 JSON。
让我们从前面的配方中获得代码,并让我们看看从GetAllProducts得到的结果:
<assets/5ed31c92-5e33-4ac1-9e44-6cac475c136e.png>
我们得到 JSON 值,因为在默认情况下,ASP.NET Core 不考虑浏览器发送的Accept头。
怎么做……
- 首先,让我们在项目中添加以下依赖项:
"Microsoft.AspNetCore.MVC.Formatters.Xml": "2.0.0"
- 接下来,我们将以下代码添加到
Startup.cs的ConfigureServices方法中:
services.AddMVC(options =>
{
options.RespectBrowserAcceptHeader = true;
options.InputFormatters.Add(
new XmlDataContractSerializerInputFormatter());
options.OutputFormatters.Add(
new XmlDataContractSerializerOutputFormatter());
});
- 最后,我们可以看到 XML 格式的产品列表:
<assets/bc836487-3246-4ef9-a4f6-0eb238e362d1.png>
配置跨域起始请求
在本教程中,您将学习如何在 ASP 中配置和使用跨源资源共享(CORS).NET Core 应用。
准备
为了配置和使用 CORS,我们将创建两个应用:一个 Web API 应用公开配置了 CORS 约束的服务,另一个客户机应用试图通过 jQuery AJAX 调用使用 Web API 服务。
怎么做……
我们将创建两个独立的应用项目,并从其中一个向other发出请求。 other项目是一个 ASP.NET Core 项目,我们将在其中启用/配置 CORS:
- 首先,让我们创建 Web API 应用,通过创建一个空的 Web 应用:
dotnet new mvc -n Chapter11.R3.Server
- 接下来,我们将添加 ASP.NET Core MVC 依赖于项目:
"Microsoft.AspNetCore.MVC": "2.0.0"
- 接下来,我们将以下代码添加到
Startup.cs中。 这段代码允许我们使用 Web API 的控制器:
public void ConfigureServices(IServiceCollection services)
{
services.AddMVC();
}
public void Configure(IApplicationBuilder app)
{
app.UseMVC();
}
- 接下来,让我们将 CORS 中间件添加到 ASP.NET Core 管道,让我们通过将以下代码添加到
Startup.cs来配置它:
public void ConfigureServices(IServiceCollection services)
{
services.AddMVC();
services.AddCors(options => { options.AddPolicy("AllowMyClientOrigin", builder =>
builder.WithOrigins("http://localhost:63125")
.WithMethods("GET", "HEAD")
.WithHeaders("accept", "content-type", "origin"));
}); }
public void Configure(IApplicationBuilder app)
{
app.UseMVC();
app.UseCors("AllowMyClientOrigin"); }
要使用在ConfigureServices方法中创建的新的 CORS 策略,我们在Configure方法中调用该策略。 当前的 CORS 策略有三种方法:
如果我们想要允许所有客户端域名的 url 到 Web API 中,我们应该用ConfigureServices方法中的这段代码改变 CORS 策略:
services.AddCors(options =>options.AddPolicy("AllowMyClientOrigin",
p => p.AllowAnyOrigin()));
- 接下来,我们将用
GET方法创建一个 API 控制器:
[Route("api/[controller]")]
public class TestAPIController : Controller
{
[HttpGet]
public IActionResult Get()
{
var result = new { Success = "True", Message = "API Message" };
return Ok(result);
}
}
- 最后,为了允许来自其他域的应用使用
GET方法,我们还将在我们希望公开给域的 Web API 方法之上添加以下属性。EnableCors属性接受一个字符串参数,这是之前在Startup.cs中创建的 CORS 策略:
[HttpGet]
[EnableCors("AllowMyClientOrigin")] public IActionResult Get()
{
var result = new { Success = "True", Message = "API Message" };
return Ok(result);
}
- 为了允许控制器级的 CORS 配置对该控制器中的所有方法可用,我们可以在
controller之上添加EnableCors属性,而不是Action方法:
[Route("api/[controller]")]
[EnableCors("AllowMyClientOrigin")] public class TestAPIController : Controller
{
[HttpGet]
public IActionResult Get()
{
var result = new { Success = "True", Message = "API Message" };
return Ok(result);
}
}
- 为了允许应用级别的 CORS 配置可用于此应用中的所有控制器,我们可以将
CorsAuthorizationFilterFactory过滤器添加到全局过滤器集合中。 代码最终应该如下:
public void ConfigureServices(IServiceCollection services)
{
services.AddMVC();
services.AddCors(options =>
{
options.AddPolicy("AllowMyClientOrigin", builder =>
builder.WithOrigins("http://localhost:63125")
.WithMethods("GET", "HEAD")
.WithHeaders("accept", "content-type", "origin"));
});
services.Configure<MVCOptions>(options =>
{ options.Filters.Add(new
CorsAuthorizationFilterFactory("AllowMyClientOrigin"));
}); }
[Route("api/[controller]")]
public class TestAPIController : Controller
{
[HttpGet]
public IActionResult Get()
{
var result = new { Success = "True", Message = "API Message" };
return Ok(result);
}
}
- 接下来,让我们创建一个新的空 web 应用来测试 web API,并通过右键单击项目的根,添加一个
bower.js文件来添加前端依赖项。 然后,去添加|新项目| .NET Core|客户端| Bower 配置文件:
<assets/340bd063-2eb8-4024-9bd6-f9800452454c.png>
- 接下来,让我们通过右键单击项目的根目录,将 jQuery 添加到使用 Bower 包管理器的客户机应用中。 然后,选择 Manage Bower Packages 并安装 jQuery:
<assets/d0a1d161-b8d9-493c-8531-cf4f11fa314b.png>
- 接下来,让我们在
wwwroot文件夹中添加一个 HTML 页面,以添加 JavaScript 代码,它将调用 API 服务。wwwroot文件夹是我们添加所有静态文件(.html、.css、.js、.font、图像等等)的文件夹。 我们也可以将这个.js代码添加到视图中:
<assets/02878671-7d62-40b3-9674-2a9df4ddd766.png>
- 在 ASP 中允许静态文件.NET Core 应用,我们必须添加以下代码到
Configure方法:
public void Configure(IApplicationBuilder app)
{
app.UseStaticFiles();
}
- 接下来,我们将在刚刚创建的 HTML 页面中添加以下代码:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title></title>
</head>
<body>
<button id="bt1">Call API</button>
<script src="lib/jquery/dist/jquery.js"></script>
<script type="text/javascript">
$(document).ready(function () {
jQuery.support.cors = true;
$('#bt1').click(function () {
console.log('Clicked');
$.ajax({
url: 'http://localhost:64112/api/TestAPI',
type: 'GET',
dataType: 'json',
success: function (data) {
console.log(data.success);
console.log(data.message);
},
error: function (jqXHR, textStatus, errorThrown) {
console.log(textStatus, errorThrown);
}
});
});
});
</script>
</body>
</html>
- 最后,通过单击 call API 按钮,我们将启动 HTML 页面并使用
GETHTTP 方法调用TestAPI控制器。 为了确保没有发生错误,我们将通过测试应用的浏览器中的F12按钮启动。 让我们打开控制台,看看结果:
<assets/942b5017-58b4-465d-bbae-ff23f3601b84.png>
- 如果发生任何错误,控制台消息将是如下截图所示:
<assets/1097679a-daa0-4490-89bd-1cd31bb5dd10.png>
使用轻便
在本教程中,您将学习如何使用Swagger为我们的 REST api 创建帮助页面和文档。
准备
让我们用 VS 2017 创建一个空项目。
怎么做……
- 首先,我们将
Swashbuckle引用添加到项目中:
"Swashbuckle": "5.6.0"
- 接下来,让我们在
Startup.cs中添加Swagger作为中间件:
public void ConfigureServices(IServiceCollection services)
{
services.AddMVC();
services.AddSwaggerGen();
}
public void Configure(IApplicationBuilder app)
{
app.UseMVC();
app.UseSwagger();
app.UseSwaggerUi();
}
- 现在,让我们通过访问http://{UrlAPI}/swagger/ui来启动 API 文档。 我们现在可以看到生成的 API 文档:
<assets/ec26ba5f-8a71-487b-841b-c9aa443c952f.png>
- 当我们点击每个 HTTP 方法时,我们可以看到 Swagger 提供给我们的所有选项,比如轻松地测试 API:
<assets/d56f613a-76b6-4d76-a92a-fc622ed0dcc2.png>
- 接下来,让我们使用另一个特性:添加关于 API 的信息。 为此,我们将以下代码添加到
Startup.cs:
public class Startup
{
public Startup(IHostingEnvironment env)
{
var builder = new ConfigurationBuilder()
.SetBasePath(env.ContentRootPath);
Configuration = builder.Build();
}
public IConfigurationRoot Configuration
{
get;
}
public void ConfigureServices(IServiceCollection services)
{
services.AddMVC();
services.AddSwaggerGen();
services.ConfigureSwaggerGen(options => { options.SingleApiVersion(new Info
{ Version = "v1",
Title = "ECommerce API",
Description = "The Api to get all data from the SB Store",
TermsOfService = "None"
}); options.IncludeXmlComments( Path.ChangeExtension(Assembly.GetEntryAssembly().Location, "xml")
); options.DescribeAllEnumsAsStrings(); }); }
public void Configure(IApplicationBuilder app)
{
app.UseMVC();
app.UseSwagger();
app.UseSwaggerUi();
}
}
options.IncludeXmlComments()方法意味着我们可以考虑 API 方法上面的 XML 注释来生成 Swagger 文档:
<assets/6c31bf5f-a44e-49c0-9228-c4583f948e85.png>
- 让我们在 c#代码中添加一些注释,看看所有这些变化:
/// <summary>
/// This method insert a product in the database
/// </summary>
/// <param name="product"></param>
/// <returns>CreatedAtRouteResult</returns>
[HttpPost]
public IActionResult Post([FromBody]Product product)
- 让我们看看现在在 Swagger UI 中的变化:
<assets/1f918e54-dc5b-4beb-9b5f-6f4692dd0198.png>
- 最后,让我们看看 Swagger 更有趣的功能:Try it Out! 按钮测试 HTTP 方法,并允许我们查看响应和请求 URL 的更多相关信息:响应体、响应代码和响应头:
<assets/c3d4ad9d-9f6a-4b92-bdf1-fe99ccc2d8d3.png>
它是如何工作的…
Swagger 为我们提供了一种很好的、简单的方法来生成完整的 UI 文档。 该文档将由现有的 API 源代码生成,并且可以通过应用中的 XML 注释来完成。 它还允许我们测试每个 HTTP 方法(如Postman),通过生成的 UI 发送值,并选择不同的内容类型报头,如application/json或application/xml。
它还提供了一些客户端 api,允许 Swagger 生成的端点与 JavaScript、AngularJS 或 Xamarin 等客户端技术之间进行通信。
测试 Web API
在本教程中,您将学习如何使用 Moq 和 xUnit 测试 Web API 控制器。
准备
让我们在 VS 2017 中创建一个类库项目,并使用本章Using ActionResultsrecipe 中创建的ProductAPIController方法。 我们将为这个 API 创建一些测试用例。
怎么做……
- 首先,让我们在解决方案中创建一个类库项目:
<assets/24b485bd-db83-405b-ab6b-b3d4edc43f95.png>
-
接下来,我们将更改项目中生成的代码,以导入
xunit、dotnet-test-xunit和moq。 我们还必须在 Web API 项目中添加引用。 -
下面是一些测试方法:
public class ProductApiControllerTests
{
#region Tests for GET : api/productapi
[Fact]
public void GET
_Returns404NotFoundResultIfProductListHaveNoItemsInRepo()
{
// Arrange
var mockRepo = new Mock<IProductRepository>();
var emptyProductList = GetEmptyProductsList();
mockRepo.Setup(repo => repo.GetAllProducts())
.Returns(emptyProductList);
var controller = new ProductApiController(mockRepo.Object);
// Act
var result = controller.Get();
// Assert
Assert.IsType<NotFoundObjectResult>(result);
}
[Fact]
public void GET
_Returns200OkResultIfProductListHaveAtLeastOneItemInRepo()
{
// Arrange
var mockRepo = new Mock<IProductRepository>();
var productList = GetProductsList();
mockRepo.Setup(repo => repo.GetAllProducts())
.Returns(productList);
var controller = new ProductApiController(mockRepo.Object);
// Act
var result = controller.Get();
// Assert
Assert.IsType<OkObjectResult>(result);
}
#endregion
#region Tests for POST : api/productapi
public void POST
_Returns400BadRequestResultIfProductFromRequestParameterIsNull()
{
// Arrange
var mockRepo = new Mock<IProductRepository>();
var product = GetProduct();
var nullProduct = GetNullProduct();
mockRepo.Setup(repo => repo.Add(product));
var controller = new ProductApiController(mockRepo.Object);
// Act
var result = controller.Post(nullProduct);
// Assert
Assert.IsType<BadRequestObjectResult>(result);
}
public void POST
_Returns201CreatedAtRouteResultIfProductIsInsertedSuccesfully
InRepo()
{
// Arrange
var mockRepo = new Mock<IProductRepository>();
var product = GetProduct();
mockRepo.Setup(repo => repo.Add(product));
var controller = new ProductApiController(mockRepo.Object);
// Act
var result = controller.Post(product);
// Assert}
Assert.IsType<CreatedAtRouteResult>(result);
}
#endregion
#region private methods
private IEnumerable<Product> GetEmptyProductsList()
{
return new List<Product>();
}
private IEnumerable<Product> GetProductsList()
{
return new List<Product>
{
new Product { Id = 1, Name = "Phone" },
new Product { Id = 2, Name = "Laptop" },
new Product { Id = 3, Name = "Computer" },
new Product { Id = 4, Name = "Screen" },
new Product { Id = 5, Name = "Mouse" }
};
}
private Product GetEmptyProduct()
{
return new Product();
}
private Product GetNullProduct()
{
return null;
}
private Product GetProduct()
{
return new Product
{
Id = 6, Name = "Tablet"
};
}
private Product GetProductById(int id)
{
return GetProductsList().Where(p => p.Id == id).SingleOrDefault();
}
private Product GetProductAleadyExist()
{
return GetProductsList()
.Where(p => p.Id == 1).SingleOrDefault();
}
#endregion
}
Most of the test methods have been removed for brevity. The test project can be found on the GitHub repository, at https://github.com/polatengin/B05277.
有更多的…
我们可以使用一些工具来测试我们的 api,例如:
- WireShark(https://www.wireshark.org/)是一款很好的网络监控工具,可以监听来自网卡的所有收发报文。
- 邮差(https://www.getpostman.com/)和**【T5 提琴手】(【https://www.telerik.com/fiddler T6】**)也很有用的工具为测试 api 来创建与 HTTP 调用。 他们把自己安装成计算机的某种代理。 它们可以向特定的 URL 发送和显示请求和响应对。
管理异常
在本菜谱中,您将学习如何使用 Web API 管理异常。
通常,我们不想失去异常的原因。 因此,我们应该持久化并维护所有异常的日志。 就持久化异常日志所需的存储容量而言,异常日志可能是巨大的。
我们可以记录所有异常文件或数据库表(如该软件,甲骨文,或 MySql),或文档存储(如 MongoDb (https://www.mongodb.com/),或 Azure CosmosDb(https://azure.microsoft.com/en-us/services/cosmos-db/)。
记录的异常至少包含:
- 一个描述性的信息
- 异常消息
- 异常。net 类型
- 堆栈跟踪
我们通常只向客户端发送描述性消息,并记录关于异常的其他信息。
使用 Web API 2,在 ASP 之前。 在 asp.net Core 中,HttpError类向客户端发送了一个结构化错误。
HttpError传统上由 Web API 使用,以(某种)标准化的方式向客户端提供错误信息。 它有一些有趣的特性:
ExceptionMessageExceptionTypeInnerExceptionMessageModelStateMessageDetailStackTrace
准备
让我们打开 ASP.NET Core Web API 项目,我们在Using ActionResultsrecipe 中创建。
怎么做……
- 首先,让我们将
WebApiCompatShim库添加到project.json:
"Microsoft.AspNetCore.MVC.WebApiCompatShim": "1.0.0"
- 为了使用
WebApiCompatShim,我们需要修改ConfigureServices中的一些代码来添加一个服务:
services.AddMVC().AddWebApiConventions();
- 接下来,我们将更改
api/productapi/{id}的现有代码:
[HttpGet("{id:int}")]
public IActionResult Get(int id)
{
var productFromRepo = _productRepository.Find(id);
if (productFromRepo == null)
return NotFound();
return Ok(productFromRepo);
}
我们可以增加一个不同参数的Get()方法,如下:
[Route("{id:int}")]
public HttpResponseMessage Get(int id, HttpRequestMessage request)
{
var product = _productRepository.Find(id);
if (product == null)
{
Return request.CreateErrorResponse(HttpStatusCode.NotFound,
new HttpError
{
Message = "This product does not exist",
MessageDetail = string.Format("The product requested with
ID {0} does not exist in the repository", id)
});
}
return request.CreateResponse(product);
}
- 最后,如果我们发送一个产品 ID(在存储库中不存在)作为参数,我们将收到以下消息:
<assets/5b5c992c-48d1-457b-9121-24204abe179c.png>
有更多的…
我们可以通过创建一个Exception中间件来全局管理异常。
从 Web API 2.1 开始,我们使用IExceptionHandler来全局处理异常。 它帮助捕获控制器、操作、过滤器、路由,有时还捕获MessageHandlers和MediaTypeFormatters中的异常。
我们也可以考虑创建一个自定义全局Exception过滤器来记录异常,但是我们将在第 12 章、Filte**rs中讨论这个问题。
十二、过滤器
在本章中,我们将涵盖以下主题:
- 使用策略、需求和过滤器管理身份验证和授权
- 用过滤器管理依赖注入
- 创建和使用操作筛选器
- 创建和使用结果过滤器
- 创建和使用资源筛选器
- 创建并使用异常过滤器
- 全局使用过滤器和使用中间件
介绍
过滤器是注入到请求处理中的代码。 过滤器可以有很多类别:授权、缓存、日志记录、异常等等。 过滤器在 ActionInvocation 之后执行,在中间件在 ASP 上执行之后执行.NET Core 管道。
这里是官方 ASP 的过滤管道.NET Core 文档:
<assets/d4098cfe-d3d7-49ba-a943-45b368d1bc16.png>
它们可以作为属性在操作或控制器级别应用,也可以作为添加在Startup.cs中的全局过滤器列表中的应用级别的全局过滤器。
我们还可以控制在同一操作上执行过滤器的顺序。
我们可以创建同步和异步过滤器,但是我们不应该混合使用这两种过滤器以避免副作用。
有预定义的过滤器,也有我们创建的过滤器:
-
预定义的过滤器(它们用于动作和控制器级别):
[AllowAnonymous][Authorize][FormatFilter][TypeFilter(typeof(MyFilterAttribute))][ServiceFilter(typeof(MyFilterAttribute))]
-
我们创建的过滤器(全局或非全局):
- 动作过滤器(源自
IActionFilter,IAsyncActionFilter,或IActionFilterAttribute) - 结果过滤器(源自
IResultFilter,IAsyncResultFilter,或IResultFilterAttribute) - 资源过滤器(源自
IResourceFilter,IAsyncResourceFilter,或IResourceFilterAttribute) - 异常过滤器(派生自
IExceptionFilter,IAsyncExceptionFilter,或IExceptionFilterAttribute)
- 动作过滤器(源自
然而,全局过滤器将首先执行; 然后是控制器级别的过滤器,最后是操作级别的过滤器。
对于异常过滤器,顺序是相反的:
- 全球。
- 控制器。
- 行动。
使用策略、需求和过滤器管理身份验证和授权
在本食谱中,您将学习如何在全局、控制器和操作级别应用授权和身份验证。
准备
让我们用 VS 2017 创建一个空的 web 应用。
怎么做……
授权过滤器的目标是将操作方法单独或按控制器限制为特定的用户、角色或声明。 它总是在动作执行之前运行:
- 使用
Authorization筛选器的一个经典方法是在控制器级别添加这个筛选器,并在Action级别覆盖AllowAnonymous属性,如下所示:
[Authorize]
public class AccountController : Controller
{
[HttpGet]
[AllowAnonymous]
public IActionResult Login(string returnUrl = null)
{
ViewData["ReturnUrl"] = returnUrl;
return View();
}
[HttpGet]
[AllowAnonymous]
public IActionResult Register(string returnUrl = null)
{
ViewData["ReturnUrl"] = returnUrl;
return View();
}
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> LogOff()
{
await _signInManager.SignOutAsync();
_logger.LogInformation(4, "User logged out.");
return RedirectToAction(nameof(HomeController.Index), "Home");
}
[HttpGet]
[AllowAnonymous]
public IActionResult ForgotPassword()
{
return View();
}
}
如果未经授权的用户试图访问resource或action方法,Authorization筛选器将自动返回401状态码。
在 ASP.NET Core,可以通过继承IAuthorizationFilter或AuthorizeAttribute来创建我们自己的Authorization属性。 使用 Core,你不能编写自己的Authorization过滤器; 但是,可以通过策略和AuthorizationPolicyBuilder对象定制Authorization过滤器。
- 让我们覆盖
Authorize属性配置,并将其作为全局过滤器添加:
var authorizationPolicy = new AuthorizationPolicyBuilder()
.RequireAuthenticatedUser()
.Build();
config.Filters.Add(new AuthorizeFilter(authorizationPolicy));
- 我们还可以添加一些策略来应用于
Authorize过滤器,如下所示:
services.AddAuthorization(auth =>
auth.AddPolicy("AuthPolicyAdminRole", policy =>
policy.RequireRole("AdminRole")));
services.AddAuthorization(auth =>
auth.AddPolicy("AuthPolicyReviewerRole", policy =>
policy.RequireRole("ReviewerRole")));
services.AddAuthorization(auth =>
auth.AddPolicy("AuthPolicySpecificClaim", policy =>
policy.RequireClaim("SpecificClaim")));
- 这就是我们在控制器或动作级别应用这些策略的方式:
[Authorize(Policy = "AuthPolicyAdminRole")]
[Authorize(Policy = "AuthPolicyOtherRole")]
[Authorize(Policy = "AuthPolicySpecificClaim")]
public IActionResult AuthorizedRoleView()
Another great thing with policies is that they are testable. We can develop a Unit Test project, and test all the policies autonomously.
身份验证
通常的做法是同时使用Authentication和Authorization过滤器。 使用WebForms,我们可以在每个页面上以编程方式添加自定义Authentication,但这很费力。
MVC 为WebApplications认证带来了更大的粒度。 现在,我们可以根据我们在网站中操作的需要,在操作、控制器或全局级别上添加Authentication。 它首先在所有 MVC 过滤器之前执行。
Authentication过滤器在 ASP 中不再存在.NET MVC 6(可能还没有移植)。
在 ASP.NET Core 中,可以通过继承FilterAttribute和IAuthenticationFilter来创建我们自己的Authorization属性。
例如,在未经身份验证的用户试图访问方法或控制器中的操作方法时,我们使用它返回特定的 HTTP 状态代码、操作结果或路由结果。 检查身份验证的方法有:
- 我们总是可以通过编程方式使用以下代码来运行身份验证:
public IActionResult About()
{
if (User.Identity.IsAuthenticated)
RedirectToAction("Index");
return View();
}
- 我们可以合并
Authentication和Authorization,方法是通过应用于Authorization过滤器的策略添加之前的身份验证检查,如下所示:
services.AddAuthorization(auth =>
auth.AddPolicy("AuthPolicyAuthentication", policy =>
policy.RequireAuthenticatedUser()));
- 有了这个政策,我们将能够替换以下代码:
if (User.Identity.IsAuthenticated)
我们可以使用Authorize属性将认证策略应用到操作中,如下所示:
[Authorize(Policy = "AuthPolicyAuthentication")]
public IActionResult About()
{
if (User.Identity.IsAuthenticated)
RedirectToAction("Index");
return View();
}
Barry Dorran has one of the nicest implementation of basic authentication middleware on his GitHub repository at https://github.com/blowdart/idunno.Authentication.
有更多的…
我们可以跟随 Barry Dorrans 在 ASP 安全方面的工作.NET 的核心。 他创建了一个非常好的工作室,这是食谱的主要基础。 您可以通过https://github.com/blowdart/AspNetAuthorizationWorkshop访问。
用过滤器管理依赖注入
在本食谱中,您将学习如何以及何时对过滤器使用依赖注入。
准备
让我们用 VS 2017 创建一个空的 web 项目。
怎么做……
我们将通过在控制器中应用依赖注入来理解IActionFilter接口、TypeFilter和ServiceFilter属性。
- 首先,让我们通过派生
IActionFilter来创建一个ActionFilter类:
public class MyActionFilter : IActionFilter
{
public void OnActionExecuting(ActionExecutingContext context)
{
// do something before the action executes
}
public void OnActionExecuted(ActionExecutedContext context)
{
// do something after the action executes
}
}
- 要使用这个类作为属性,我们必须使用
TypeFilter属性,并将这个类作为参数赋予它。 我们不能直接使用MyActionFilter作为属性,因为它不能从ActionFilterAttribute继承:
[TypeFilter(typeof(MyActionFilter))]
public IActionResult Index()
{
return View();
}
- 如果我们测试,这个页面工作良好:
<assets/815da4a5-0681-4031-9c54-37e52bba1d5f.png>
- 现在让我们尝试使用
ServiceFilter属性:
[ServiceFilter(typeof(MyActionFilter))]
public IActionResult Index()
- 如果我们再次测试,我们将得到一个 HTTP 错误 500,并告诉我们过滤器没有注册:
<assets/29308b38-f554-4696-ab19-ec7ab4ca9f3c.png>
- 让我们用
Startup.cs的ConfigureServices方法注册过滤器:
services.AddScoped<MyActionFilter>();
- 现在我们可以看到它工作得很好:
<assets/e1242d16-122a-492d-b338-a59462c1562a.png>
当我们使用TypeFilter属性时,我们的过滤器不是由 DI 容器解析的,而是由ObjectFactory实例化的,所以它不需要有生命周期。
- 如果我们有由 ASP 解析的依赖关系,可以将
MyActionFilter作为子类添加到从TypeFilterAttribute继承的类中.NET Core DI 容器或过滤器构造函数中使用的另一个 DI 容器(即使TypeFilter属性没有被 DI 解析):
public class MyActionTypeFilterAttribute : TypeFilterAttribute
{
Public MyActionTypeFilterAttribute():
base(typeof(MyActionTypeFilterAttributeImpl))
{ }
private class MyActionTypeFilterAttributeImpl : IActionFilter
{
private readonly IRepository _repository;
public MyActionTypeFilterAttributeImpl(IRepository repository)
{
_repository = repository;
}
public void OnActionExecuting(ActionExecutingContext context)
{
// do something before the action executes
var id = context.ActionArguments["id"] as int?;
if (id.HasValue)
{
var product = _repository.GetProduct(id.Value);
if (product != null)
{
context.Result = new NotFoundObjectResult(id.Value);
}
}
}
public void OnActionExecuted(ActionExecutedContext context)
{
// do something after the action executes
}
}
}
- 我们还可以使用
MyActionTypeFilterAttribute,将其添加到动作之上,如下所示:
[MyActionTypeFilterAttribute]
public IActionResult Index()
instead of using the following code:
[TypeFilter(typeof(MyActionTypeFilterAttribute))]
public IActionResult Index()
- 另一件很棒的事情是有能力通过构造函数在过滤器中注入一些服务,就像一个存储库:
public class MyActionFilter : IActionFilter
{
private readonly IAppRepository _repository;
public MyActionFilter(IAppRepository repository)
{
_repository = repository ;
}
public void OnActionExecuting(ActionExecutingContext context)
{
// do something before the action executes
if (context.ActionArguments.ContainsKey("id"))
{
var id = context.ActionArguments["id"] as int?;
if (id.HasValue)
{
var product = _repository.GetProduct(id.Value);
if (product != null)
{
context.Result = new NotFoundObjectResult(id.Value);
return;
}
}
}
}
public void OnActionExecuted(ActionExecutedContext context)
{
// do something after the action executes
}
}
- 我们可以在动作过滤器中使用日志层而不是存储库层:
public class MyActionFilterLogger : IActionFilter
{
private readonly ILogger _logger;
public MyActionFilterLogger(ILoggerFactory loggerFactory)
{
_logger = loggerFactory.CreateLogger<MyActionFilterLogger>();
}
public void OnActionExecuted(ActionExecutedContext context)
{
_logger.LogInformation(
"OnActionExecuted" + DateTime.Now.ToLongDateString());
}
public void OnActionExecuting(ActionExecutingContext context)
{
_logger.LogInformation(
"OnActionExecuting" + DateTime.Now.ToLongDateString());
}
}
- 现在我们将在 ASP 中注册这两个过滤器.NET Core IoC 容器:
services.AddScoped<MyActionFilter>();
services.AddScoped<MyActionFilterLogger>();
- 我们可以使用它们作为全局过滤器:
config.Filters.Add(typeof(MyActionFilter));
config.Filters.Add(typeof(MyActionFilterLogger));
- 我们也可以使用它们作为控制器过滤器:
[ServiceFilter(typeof(MyActionFilter))]
[ServiceFilter(typeof(MyActionFilterLogger))]
public class HomeController : Controller
- 或者,我们可以使用它们作为动作过滤器:
[ServiceFilter(typeof(MyActionFilter))]
[ServiceFilter(typeof(MyActionFilterLogger))]
public IActionResult Index()
它是如何工作的…
ServiceFilterAttribute是 ASP 的方式.NET Core 注入在构造函数中有依赖项的过滤器。 如果我们不使用ServiceFilterAttribute来使用过滤器属性,它们将由 ASP 自动管理.NET 的核心。
TypeFilter属性与ServiceFilter属性之间存在差异。 使用TypeFilter,参数中的过滤器类型将由ObjectFactory类解析。 在ServiceFilter中,参数中的过滤器类型由内部 DI 容器解析。
创建和使用操作筛选器
在本食谱中,您将学习如何创建和使用操作过滤器。
准备
让我们用 VS 2017 创建一个空的 web 项目。
怎么做……
动作过滤器在动作过滤器执行之前和/或之后注入业务逻辑。 它通常检查和/或修改请求的元素; 例如,参数和头文件。 动作过滤器还可以检查模型绑定。 模型绑定通过一个类的实例设置 UI 元素的值(如textbox、combobox、checkbox等),并将 UI 元素的值获取到一个类的实例。 如果一个类用于获取/设置 UI 元素的值,则该类称为ViewModel。 模型绑定机制自动绑定ViewModel和 UI 层。
要创建我们自己的动作过滤器,我们必须创建一个派生于以下基类或接口的类:
IActionFilter或IAsyncActionFilter,这取决于我们是要创建同步过滤器还是异步过滤器ActionFilterAttribute
IActionFilter和IAsyncActionFilter继承自IFilterMetadata接口。 所有实现这些接口的类都不能作为控制器或操作方法上面的属性使用。 类应该实现IActionFilter、IAsyncActionFilter、TypeFilter或ServiceFilter属性,将其添加到控制器或操作方法之上。 但它们可以被用作全局过滤器。
ActionFilterAttribute继承自Attribute、IActionFilter、IFilterMetadata、IAsyncActionFilter、IResultFilter、IAsyncResultFilter、IOrderedFilter。
- 让我们创建一个
ActionFilterAttribute的实现:
public class MyActionAttributeFilter : ActionFilterAttribute
{
public override void OnActionExecuting(
ActionExecutingContext context) {}
public override void OnActionExecuted(
ActionExecutedContext context) {}
public override void OnResultExecuting(
ResultExecutingContext context) {}
public override void OnResultExecuted(
ResultExecutedContext context) {}
}
我们可以在全局、控制器或操作级别使用该类作为过滤器。
Note: We can be notified by ActionExecutedResult. Canceled property when another filter as a resource filter short circuits the current action filter.
- 在全局级别上,将过滤器添加到
Startup.cs的配置中:
services.AddMVC(
config =>
{
config.Filters.Add(new MyActionAttributeFilter());
});
- 在控制器或操作级别,它可以通过将其添加到操作或控制器之上来使用:
[MyActionAttributeFilter]
public IActionResult Index()
- 或者,我们可以添加一个
TypeFilter属性:
[TypeFilter(typeof(MyActionAttributeFilter))]
public IActionResult Index()
- 另一种选择是添加一个
ServiceFilter属性,但是在这种情况下,它的生命周期将由 ASP 管理.NET Core DI 容器。 我们应该给它一个生命周期:
services.AddScoped<MyActionAttributeFilter>();
我们可以在控制器中使用它:
[ServiceFilter(typeof(MyActionAttributeFilter))]
public IActionResult Index()
- 接下来,让我们实现一个
IActionFilter:
public class MyValidateModelStateActionFilter : IActionFilter
{
public void OnActionExecuting(ActionExecutingContext context)
{
// do something before the action executes
if (!context.ModelState.IsValid)
{
context.Result =
new BadRequestObjectResult(context.ModelState);
}
}
public void OnActionExecuted(ActionExecutedContext context)
{
// do something after the action executes
}
}
- 现在,我们将实现一个
IAsyncActionFilter:
public class myAsyncActionFilter : IAsyncActionFilter
{
public async Task OnActionExecutionAsync(
ActionExecutingContext context,
ActionExecutionDelegate next)
{
// do something before the action executes
await next();
// do something after the action executes
}
}
- 将它与
TypeFilter一起使用(这样它就不会被 ASP 管理.NET Core DI 容器),我们只需要将它添加到动作或控制器之上:
[TypeFilter(typeof(MyActionAttributeFilter))]
public IEnumerable<string> Get()
- 与
ServiceFilter一起使用,用 ASP 对其进行管理.NET Core DI 容器,我们必须给它一个生命周期:
services.AddScoped<MyActionAttributeFilter>();
我们将在控制器中使用它:
[ServiceFilter(typeof(MyActionAttributeFilter))]
public IEnumerable<string> Get()
我们也可以设置过滤器的顺序:
[ServiceFilter(typeof(MyActionFilter))]
[ServiceFilter(typeof(MyActionAttributeFilter))]
public IEnumerable<string> Get()
首先执行MyActionAttributeFilter,然后执行MyActionFilter。
但是我们可以改变顺序:
[ServiceFilter(typeof(MyActionFilter), Order = 2)]
[ServiceFilter(typeof(MyActionAttributeFilter), Order = 1)]
public IEnumerable<string> Get()
The filter with the highest Order property value is executed first.
In this example, MyActionFilter will be executed first, and MyActionAttributeFilter second.
创建和使用结果过滤器
在本食谱中,您将学习如何创建和使用结果过滤器。
准备
让我们用 VS 2017 创建一个空的 web 应用。
怎么做……
结果过滤器在执行结果之前或之后注入处理。 它可以检查和修改操作方法生成的结果。
一些常见的结果过滤器实现可能是添加或检查头,以及许多其他。
要创建我们自己的结果过滤器,我们必须创建一个派生于以下基类或接口的类:
IResultFilter或IAsyncResultFilter,这取决于我们是要创建同步过滤器还是异步过滤器ResultFilterAttribute
与IActionResult/IAsyncActionResult接口一样,IResultFilter和IResultActionFilter继承自IFilterMetadata。 在这些接口中实现的所有类都不能使用控制器或操作方法之上的属性。 一个类应该实现IResultFilter,或者IResultActionFilter和TypeFilter,或者ServiceFilter属性来添加到控制器或动作方法之上; 但是,它们可以用作全局过滤器。
ResultFilterAttribute继承自Attribute、IFilterMetadata、IResultFilter、IAsyncResultFilter、IOrderedFilter,其实现如下:
- 让我们创建一个空的同步资源过滤器:
public class MyResultFilter : IResultFilter
{
public void OnResultExecuted(ResultExecutedContext context)
{
// code executed before the filter executes
}
public void OnResultExecuting(ResultExecutingContext context)
{
// code executed after the filter executes
}
}
- 让我们创建一个空的异步资源过滤器:
public class MyAsyncResultFilter : IAsyncResultFilter
{
public async Task OnResultExecutionAsync(
ResultExecutingContext context, ResultExecutionDelegate next)
{
// code executed before the filter executes
await next();
// code executed after the filter executes
}
}
- 现在,我们将通过操作
ViewResult并向ViewData字典中添加一个条目来实现一个结果过滤器:
public class MyAsyncResultFilter : IAsyncResultFilter
{
public async Task OnResultExecutionAsync(
ResultExecutingContext context, ResultExecutionDelegate next)
{
// do something before the filter executes
ViewResult result = context.Result as ViewResult;}
if (result != null)}
{
result.ViewData["messageOnResult"] =
"This message comes from MyAsyncResultFilter" +
DateTime.Now.ToLongTimeString();
}
await next();
// do something after the filter executes
}
}
- 我们可以和
ServiceFilter一起使用。 在调用它之前,我们必须注册它:
services.AddScoped<MyAsyncResultFilter>();
我们将在控制器中使用它:
[ServiceFilter(typeof(MyAsyncResultFilter))]
public IActionResult Index()
- 我们可以看到结果显示如下:
<assets/3c681521-de9c-4b4a-a51f-c70a505511dd.png>
结果筛选器只在动作筛选器没有例外地被执行时才会被执行。
一个好的做法是只使用异步过滤器或同步过滤器,但是混合使用这两种过滤器会产生副作用。
创建和使用资源筛选器
在本食谱中,您将学习如何创建资源过滤器以及何时使用它。
准备
让我们用 VS 2017 创建一个空的 web 应用。
怎么做……
在授权过滤器之后,资源过滤器是过滤器管道上执行的第一个过滤器。 如果没有异常发生,此筛选器将在ActionFilter之后执行。 它也是最终执行的最后一个过滤器,离开过滤器管道。
一个常见的resource过滤器实现是缓存管理。 MVC 中的OutputCache属性就是一个资源筛选器的例子。
要创建我们自己的资源筛选器,我们必须创建一个派生自IResourceFilter或IAsyncResourceFilter的类,这取决于我们是要创建同步筛选器还是异步筛选器。
- 首先,让我们创建一个空的同步资源过滤器:
public class MyResourceFilter : IResourceFilter
{
public void OnResourceExecuted(ResourceExecutedContext context)
{
// code executed before the filter executes
}
public void OnResourceExecuting(ResourceExecutingContext context)
{
// code executed after the filter executes
}
}
- 让我们创建一个空的异步资源过滤器:
public class MyAsyncResourceFilter : IAsyncResourceFilter
{
public async Task OnResourceExecutionAsync(
ResourceExecutingContext context, ResourceExecutionDelegate next)
{
// code executed before the filter executes
await next();
// code executed after the filter executes
}
}
- 要在不使用 DI 的情况下使用它,我们将使用
TypeFilter属性调用它:
[TypeFilter(typeof(MyResourceFilter))]
public IEnumerable<string> Get()
- 现在,让我们创建一个资源缓存过滤器,看看我们如何实现一个资源过滤器:
public class MyResourceCacheFilter : IResourceFilter
{
private int _cacheDuration = 0;
private bool _cacheEnabled = true;
private readonly IConfigurationRoot _configuration;
private readonly IMemoryCache _cache;
private readonly ILogger _logger;
public MyResourceCacheFilter(IConfigurationRoot configuration,
IMemoryCache cache, ILoggerFactory loggerFactory)
{
_configuration = configuration;
_cache = cache;
_logger = loggerFactory.CreateLogger("MyResourceFilter");
InitFilter();
}
public void OnResourceExecuted(ResourceExecutedContext context)
{
if (_cacheEnabled)
{
string _cachekey = string.Join(":", new string[]
{
context.RouteData.Values["controller"].ToString(),
context.RouteData.Values["action"].ToString()
});
string cachedContent = string.Empty;
if (_cache.TryGetValue(_cachekey, out cachedContent))
{
if (!String.IsNullOrEmpty(cachedContent))
{
context.Result = new ContentResult()
{
Content = cachedContent
};
}
}
}
}
public void OnResourceExecuting(ResourceExecutingContext context)
{
try
{
if (_cacheEnabled)
{
if (_cache != null)
{
string _cachekey = string.Join(":", new string[]
{
context.RouteData.Values["controller"].ToString(),
context.RouteData.Values["action"].ToString()
});
var result = context.Result as ContentResult;
if (result != null)
{
string cachedContent = string.Empty;
_cache.Set(_cachekey, result.Content,
DateTime.Now.AddSeconds(_cacheDuration));
}
}
}
}
catch (Exception ex)
{
_logger.LogError("Error caching in MyResourceFilter", ex);
}
}
private void InitFilter()
{
if (!Boolean.TryParse(
_configuration.GetSection("CacheEnabled").Value,
out _cacheEnabled))
{
_cacheEnabled = false;
}
if (!Int32.TryParse(
_configuration.GetSection("CacheDuration").Value,
out _cacheDuration))
{
_cacheDuration = 21600; // 6 hours cache by default
}
}
}
- 我们可以和
ServiceFilter一起使用。 在调用它之前,我们必须注册它:
services.AddScoped<MyResourceCacheFilter>();
我们将在控制器中使用它:
[ServiceFilter(typeof(MyResourceCacheFilter))]
public IEnumerable<string> Get()
创建并使用异常过滤器
在本食谱中,您将学习如何在全局、控制器和操作级别创建和使用异常过滤器。
准备
让我们用 VS 2017 创建一个空的 web 应用。
怎么做……
ASP.NET Core MVC 与早期版本的 ASP 相比有很多优势。 asp.net MVC,如 asp.net.NET MVC 5:
- 在全局、控制器或操作级别上不再有
HandleError属性可用(因此,在web.config中不再有customErrors属性需要参数化) global.asax不再有Application_Error事件- 从基类
Controller中不再覆盖更多的OnException方法
当然,我们仍然可以使用try/catch块,但它被认为是对开发人员不友好的解决方案。 我们可以避免这种解决方案,并获得更清晰的代码。
现在的解决方案是:
- 使用
Exception过滤器(在操作方法中处理异常) - 使用
Exception中间件(在应用级别处理更多的通用错误)
异常筛选器将处理来自任何引发异常的操作或筛选器的异常。 要成为异常过滤器,类必须实现以下类或接口之一:
ExceptionFilterAttributeIExceptionFilter或IAsyncExceptionFilter为异步过滤
可以使用异常过滤器捕获特定的异常类型(并应用特定的处理,或重定向到特定的错误页面)。
现在,在 ASP.NET Core,默认情况下,如果发生异常,不显示错误消息。 因此,重定向到错误页面的需要不是那么相关,但它更优雅; 然而,我们总是需要捕获所有异常,至少要在全局日志中记录异常:
- 让我们创建一个
ExceptionFilterAttribute。 我们必须在控制器或动作级别的ServiceFilter或TypeFilter属性中使用它:
public class MyExceptionAttributeFilter : ExceptionFilterAttribute
{
public override void OnException(ExceptionContext context)
{
if (context.Exception is InvalidOperationException)
{
// Log the exception informations by ASP.NET Core
// or custom logger
context.Exception = null;
}
}
}
我们必须将 null 设置为context.Exception,以停止执行其他异常过滤器。 如果我们将context.Exception设置为 true,我们假设异常已经被处理了。
- 与
ServiceFilter一起使用,用 ASP 对其进行管理.NET Core DI 容器,我们必须给它一个生命周期:
services.AddScoped<MyExceptionAttributeFilter>();
- 我们将在控制器中使用它:
[ServiceFilter(typeof(MyExceptionAttributeFilter))]
public IEnumerable<string> Get() { }
- 接下来,让我们创建一些全局异常过滤器:
public class MyGlobalExceptionFilter : IExceptionFilter
{
public void OnException(ExceptionContext context)
{
//throw new NotImplementedException();
}
}
public class MyGlobalAsyncExceptionFilter : IAsyncExceptionFilter
{
public Task OnExceptionAsync(ExceptionContext context)
{
return Task.FromResult(0);
//throw new NotImplementedException();
}
}
- 要使用它作为全局过滤器,我们必须将它添加到配置中:
services.AddMVC(
config =>
{
config.Filters.Add(new MyGlobalExceptionFilter());
});
这个过滤器会自动应用到所有控制器中的所有动作过滤器; 但是,如果全局异常过滤器中的代码不是特定于动作方法的,同样,我们倾向于使用中间件来做这件事。 我们将在第 16 章、OWIN 和 Middleware中研究这个案例。
全局使用过滤器和使用中间件
在本菜谱中,您将学习如何全局使用过滤器,并了解全局使用过滤器和使用中间件之间的区别。
准备
让我们用 VS 2017 创建一个空的 web 应用。
怎么做……
- 首先,让我们通过派生
IResultFilter来创建一个ResultFilter类:
public class MyResultFilter : IResultFilter
{
public void OnResultExecuting(ActionExecutingContext context)
{
// do something before the action executes
}
public void OnResultExecuted(ActionExecutedContext context)
{
// do something after the action executes
ViewResult result = context.Result as ViewResult;
if (result != null)
{
result.ViewData["globalMessage"] =
"Comes from MyActionAttributeFilter at " +
DateTime.Now.ToLongTimeString();
}
}
}
- 接下来,让我们将该过滤器配置为
Startup.cs中的全局过滤器。 我们可以通过两种方式做到这一点; 按类型或按实例:
services.AddMVC(config =>
{
// FIRST WAY
config.Filters.Add(typeof(MyResultFilter)); // by type
// OR
config.Filters.Add(new TypeFilterAttribute(
typeof(MyResultFilter))); // by type
// SECOND WAY
config.Filters.Add(new MyResultFilter ()); // by instance
});
这个过滤器将应用于应用中的任何操作或控制器的任何地方,而不必将其用作控制器或操作之上的属性。
- 让我们在
index.cshtml中添加一些代码,以确保我们的全局过滤器能够正常工作:
<!DOCTYPE html>
<html>
<head>
<title>Index</title>
</head>
<body>
<div>
<h1>Message</h1>
Display message from Global Filter
</div>
<footer>
Message: @ViewData["message"]
</footer>
</body>
</html>
- 最后,我们可以看到结果:
<assets/d6cd4b18-e49f-4b10-9c7d-903f1a9192b9.png>
- 或者,我们可以通过派生
ActionFilterAttribute来创建一个类。 注意,当继承ActionFilterAttribute时,我们还实现了IActionFilter和IResultFilter接口:
public class MyActionAttributeFilter : ActionFilterAttribute
{
public MyActionAttributeFilter() {}
public override void OnActionExecuting(
ActionExecutingContext context)
{ }
public override void OnActionExecuted(ActionExecutedContext context)
{ }
public override void OnResultExecuting(
ResultExecutingContext context)
{ }
public override void OnResultExecuted(ResultExecutedContext context)
{
ViewResult result = context.Result as ViewResult;
if (result != null)
{
result.ViewData["globalMessage"] =
"Comes from MyActionAttributeFilter at " +
DateTime.Now.ToLongTimeString();
}
}
}
- 接下来,让我们将该过滤器配置为
Startup.cs中的全局过滤器。 我们可以通过两种方式做到这一点; 按类型或按实例:
services.AddMVC(config =>
{
// FIRST WAY
config.Filters.Add(typeof(MyActionAttributeFilter)); // by type
// OR
config.Filters.Add(new TypeFilterAttribute(
typeof(MyActionAttributeFilter))); // by type
// SECOND WAY
config.Filters.Add(new MyActionAttributeFilter()); // by instance
});
这个过滤器将应用于应用中的任何操作或控制器的任何地方,而不必将其用作控制器或操作之上的属性。
- 让我们在
index.cshtml中添加一些代码,以确保我们的全局过滤器能够正常工作:
<!DOCTYPE html>
<html>
<head>
<title>Index</title>
</head>
<body>
<div>
<h1>Message</h1>
Display message from Global Filter
</div>
<footer>
Message: @ViewData["message"]
</footer>
</body>
</html>
过滤器是了解 MVC 的代码,而中间件是管道上一种不了解 MVC 的更通用的代码。 我们更倾向于使用Exception中间件,而不是全局异常过滤器。
有更多的…
从这些接口派生,我们可以创建全局过滤器:
- 对于资源过滤器:
IResourceFilter和IAsyncResourceFilter - 动作过滤器:
IActionFilter和IAsyncActionFilter - 用于结果过滤器:
IResultFilter和IAsyncResultFilter - 对于异常过滤器:
IExceptionFilter和IAsyncExceptionFilter
4512

被折叠的 条评论
为什么被折叠?



