0%

ASP.NET Core中使用GraphQL


上一章中,我们介绍了如何在GraphQL中处理一对多关系,这一章,我们来介绍一下GraphQL中如何处理多对多关系。

我们继续延伸上一章的需求,上一章中我们引入了客户和订单,但是我们没有涉及订单中的物品。在实际需求中,一个订单可以包含多个物品,一个物品也可以属于多个订单,所以订单和物品之间是一个多对多关系。

为了创建订单和物品之间的关系,这里我们首先创建一个订单物品实体。

OrderItem
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
[Table("OrderItems")]
public class OrderItem
{
public int Id { get; set; }

public string Barcode { get; set; }

[ForeignKey("Barcode")]
public virtual Item Item { get; set; }

public int Quantity { get; set; }

public int OrderId { get; set; }

[ForeignKey("OrderId")]
public virtual Order Order { get; set; }
}

创建完成之后,我们还需要修改OrderItem实体, 添加他们与OrderItem之间的关系

Order
1
2
3
4
5
6
7
8
9
10
11
public class Order
{
public int OrderId { get; set; }
public string Tag { get; set; }
public DateTime CreatedAt { get; set; }

public Customer Customer { get; set; }
public int CustomerId { get; set; }

public virtual ICollection<OrderItem> OrderItems { get; set; }
}
Item
1
2
3
4
5
6
7
8
9
10
11
12
[Table("Items")]
public class Item
{
[Key]
public string Barcode { get; set; }

public string Title { get; set; }

public decimal SellingPrice { get; set; }

public virtual ICollection<OrderItem> OrderItems { get; set; }
}

修改完成之后,我们使用如下命令创建数据库迁移脚本,并更新数据库

1
2
dotnet ef migrations add AddOrderItemTable
dotnet ef database update

迁移成功之后,我们可以添加一个新的GraphQL节点,使用这个新节点,我们可以向订单中添加物品。为了实现这个功能,我们首先需要为OrderItem实体添加它在GraphQL中对应的类型OrderItemType

OrderItemType
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class OrderItemType : ObjectGraphType<OrderItem>  
{
public OrderItemType(IDataStore dateStore)
{
Field(i => i.ItemId);

Field<ItemType, Item>().Name("Item").ResolveAsync(ctx =>
{
return dateStore.GetItemByIdAsync(ctx.Source.ItemId);
});

Field(i => i.Quantity);

Field(i => i.OrderId);

Field<OrderType, Order>().Name("Order").ResolveAsync(ctx =>
{
return dateStore.GetOrderByIdAsync(ctx.Source.OrderId);
});

}
}

第二步,我们还需要创建一个OrderItemInputType来定义添加OrderItem需要哪些字段。

OrderItemInputType
1
2
3
4
5
6
7
8
9
10
public class OrderItemInputType : InputObjectGraphType  
{
public OrderItemInputType()
{
Name = "OrderItemInput";
Field<NonNullGraphType<IntGraphType>>("quantity");
Field<NonNullGraphType<IntGraphType>>("itemId");
Field<NonNullGraphType<IntGraphType>>("orderId");
}
}

第三步,我们需要在InventoryMutation类中针对OrderItem添加新的mutation

InventoryMutation
1
2
3
4
5
6
7
8
Field<OrderItemType, OrderItem>()  
.Name("addOrderItem")
.Argument<NonNullGraphType<OrderItemInputType>>("orderitem", "orderitem input")
.ResolveAsync(ctx =>
{
var orderItem = ctx.GetArgument<OrderItem>("orderitem");
return dataStore.AddOrderItemAsync(orderItem);
});

第四步,我们需要在IDataStore接口中定义几个新的方法,并在DataStore类中实现他们

IDataStore
1
2
3
4
5
Task<OrderItem> AddOrderItemAsync(OrderItem orderItem);

Task<Order> GetOrderByIdAsync(int orderId);

Task<IEnumerable<OrderItem>> GetOrderItemByOrderIdAsync(int orderId);
DataStore
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public async Task<OrderItem> AddOrderItemAsync(OrderItem orderItem)
{
var addedOrderItem = await _context.OrderItems.AddAsync(orderItem);
await _context.SaveChangesAsync();
return addedOrderItem.Entity;
}

public async Task<Order> GetOrderByIdAsync(int orderId)
{
return await _context.Orders.FindAsync(orderId);
}

public async Task<IEnumerable<OrderItem>> GetOrderItemByOrderIdAsync(int orderId)
{
return await _context.OrderItems
.Where(o => o.OrderId == orderId)
.ToListAsync();
}

第五步,我们来修改OrderType类,我们希望查询订单的时候,可以返回订单中的所有物品

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class OrderType : ObjectGraphType<Order>
{
public OrderType(IDataStore dataStore)
{
Field(o => o.Tag);
Field(o => o.CreatedAt);
Field<CustomerType, Customer>()
.Name("Customer")
.ResolveAsync(ctx =>
{
return dataStore.GetCustomerByIdAsync(ctx.Source.CustomerId);
});

Field<OrderItemType, OrderItem>()
.Name("Items")
.ResolveAsync(ctx =>
{
return dataStore.GetOrderItemByOrderIdAsync(ctx.Source.OrderId);
});
}
}
}

最后我们还需要在Startup类中注册我们刚定义的2个新类型

1
2
services.AddScoped<OrderItemType>();  
services.AddScoped<OrderItemInputType>();

以上就是所有的代码修改。现在我们启动项目

首先我们先为之前添加的订单1, 添加两个物品

然后我们来调用查询Order的query, 结果中订单中物品正确显示了。

本文源代码: https://github.com/lamondlu/GraphQL_Blogs/tree/master/Part%20IX

ASP.NET Core 中间件(Middleware)详解 - 晓晨Master - 博客园

Excerpt

本文为官方文档译文,官方文档现已非机器翻译 https://docs.microsoft.com/zh cn/aspnet/core/fundamentals/middleware/?view=aspnetcore 2.1 什么是中间件(Middleware)? 中间件是组装到应用程序管道中以处理请


本文为官方文档译文,官方文档现已非机器翻译 https://docs.microsoft.com/zh-cn/aspnet/core/fundamentals/middleware/?view=aspnetcore-2.1

什么是中间件(Middleware)?

中间件是组装到应用程序管道中以处理请求和响应的软件。 每个组件:

  • 选择是否将请求传递给管道中的下一个组件。
  • 可以在调用管道中的下一个组件之前和之后执行工作。

请求委托(Request delegates)用于构建请求管道,处理每个HTTP请求。

请求委托使用RunMapUse扩展方法进行配置。单独的请求委托可以以内联匿名方法(称为内联中间件)指定,或者可以在可重用的类中定义它。这些可重用的类和内联匿名方法是中间件或中间件组件。请求流程中的每个中间件组件都负责调用流水线中的下一个组件,如果适当,则负责链接短路。

将HTTP模块迁移到中间件解释了ASP.NET Core和以前版本(ASP.NET)中的请求管道之间的区别,并提供了更多的中间件示例。

使用 IApplicationBuilder 创建中间件管道

ASP.NET Core请求流程由一系列请求委托组成,如下图所示(执行流程遵循黑色箭头):

每个委托可以在下一个委托之前和之后执行操作。委托还可以决定不将请求传递给下一个委托,这称为请求管道的短路。短路通常是可取的,因为它避免了不必要的工作。例如,静态文件中间件可以返回一个静态文件的请求,并使管道的其余部分短路。需要在管道早期调用异常处理委托,因此它们可以捕获后面管道的异常。

最简单的可能是ASP.NET Core应用程序建立一个请求的委托,处理所有的请求。此案例不包含实际的请求管道。相反,针对每个HTTP请求都调用一个匿名方法。

1
using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Http; public class Startup { public void Configure(IApplicationBuilder app) { app.Run(async context => { await context.Response.WriteAsync("Hello, World!"); }); } }

第一个 app.Run 委托终止管道。

有如下代码:

通过浏览器访问,发现确实在第一个app.Run终止了管道。

您可以将多个请求委托与app.Use连接在一起。 next参数表示管道中的下一个委托。 (请记住,您可以通过不调用下一个参数来结束流水线。)通常可以在下一个委托之前和之后执行操作,如下例所示:

1
public class Startup { public void Configure(IApplicationBuilder app) { app.Use(async (context, next) => { await context.Response.WriteAsync("进入第一个委托 执行下一个委托之前\r\n"); //调用管道中的下一个委托 await next.Invoke(); await context.Response.WriteAsync("结束第一个委托 执行下一个委托之后\r\n"); }); app.Run(async context => { await context.Response.WriteAsync("进入第二个委托\r\n"); await context.Response.WriteAsync("Hello from 2nd delegate.\r\n"); await context.Response.WriteAsync("结束第二个委托\r\n"); }); } }

使用浏览器访问有如下结果:

可以看出请求委托的执行顺序是遵循上面的流程图的。

注意:
响应发送到客户端后,请勿调用next.Invoke。 响应开始之后,对HttpResponse的更改将抛出异常。 例如,设置响应头,状态代码等更改将会引发异常。在调用next之后写入响应体。

  • 可能导致协议违规。 例如,写入超过content-length所述内容长度。

  • 可能会破坏响应内容格式。 例如,将HTML页脚写入CSS文件。

HttpResponse.HasStarted是一个有用的提示,指示是否已发送响应头和/或正文已写入。

顺序

Startup。Configure方法中添加中间件组件的顺序定义了在请求上调用它们的顺序,以及响应的相反顺序。 此排序对于安全性,性能和功能至关重要。

Startup.Configure方法(如下所示)添加了以下中间件组件:

  1. 异常/错误处理
  2. 静态文件服务
  3. 身份认证
  4. MVC
1
public void Configure(IApplicationBuilder app) { app.UseExceptionHandler("/Home/Error"); // Call first to catch exceptions // thrown in the following middleware. app.UseStaticFiles(); // Return static files and end pipeline. app.UseAuthentication(); // Authenticate before you access // secure resources. app.UseMvcWithDefaultRoute(); // Add MVC to the request pipeline. }

上面的代码,UseExceptionHandler是添加到管道中的第一个中间件组件,因此它捕获以后调用中发生的任何异常。

静态文件中间件在管道中提前调用,因此可以处理请求和短路,而无需通过剩余的组件。 静态文件中间件不提供授权检查。 由其提供的任何文件,包括wwwroot下的文件都是公开的。

如果请求没有被静态文件中间件处理,它将被传递给执行身份验证的Identity中间件(app.UseAuthentication)。 身份不会使未经身份验证的请求发生短路。 虽然身份认证请求,但授权(和拒绝)仅在MVC选择特定的Razor页面或控制器和操作之后才会发生。

授权(和拒绝)仅在MVC选择特定的Razor页面或Controller和Action之后才会发生。

以下示例演示了中间件顺序,其中静态文件的请求在响应压缩中间件之前由静态文件中间件处理。 静态文件不会按照中间件的顺序进行压缩。 来自UseMvcWithDefaultRoute的MVC响应可以被压缩。

1
public void Configure(IApplicationBuilder app) { app.UseStaticFiles(); // Static files not compressed app.UseResponseCompression(); app.UseMvcWithDefaultRoute(); }

Use, Run, 和 Map#

你可以使用UseRunMap配置HTTP管道。Use方法可以使管道短路(即,可以不调用下一个请求委托)。Run方法是一个约定, 并且一些中间件组件可能暴露在管道末端运行的Run [Middleware]方法。Map*扩展用作分支管道的约定。映射根据给定的请求路径的匹配来分支请求流水线,如果请求路径以给定路径开始,则执行分支。

1
public class Startup { private static void HandleMapTest1(IApplicationBuilder app) { app.Run(async context => { await context.Response.WriteAsync("Map Test 1"); }); } private static void HandleMapTest2(IApplicationBuilder app) { app.Run(async context => { await context.Response.WriteAsync("Map Test 2"); }); } public void Configure(IApplicationBuilder app) { app.Map("/map1", HandleMapTest1); app.Map("/map2", HandleMapTest2); app.Run(async context => { await context.Response.WriteAsync("Hello from non-Map delegate. <p>"); }); } }

下表显示了使用以前代码的 http://localhost:19219 的请求和响应:

请求 响应
localhost:1234 Hello from non-Map delegate.
localhost:1234/map1 Map Test 1
localhost:1234/map2 Map Test 2
localhost:1234/map3 Hello from non-Map delegate.

当使用Map时,匹配的路径段将从HttpRequest.Path中删除,并为每个请求追加到Http Request.PathBase

MapWhen根据给定谓词的结果分支请求流水线。 任何类型为Func<HttpContext,bool>的谓词都可用于将请求映射到管道的新分支。 在以下示例中,谓词用于检测查询字符串变量分支的存在:

1
public class Startup { private static void HandleBranch(IApplicationBuilder app) { app.Run(async context => { var branchVer = context.Request.Query["branch"]; await context.Response.WriteAsync($"Branch used = {branchVer}"); }); } public void Configure(IApplicationBuilder app) { app.MapWhen(context => context.Request.Query.ContainsKey("branch"), HandleBranch); app.Run(async context => { await context.Response.WriteAsync("Hello from non-Map delegate. <p>"); }); } }

以下下表显示了使用上面代码 http://localhost:19219 的请求和响应:

请求 响应
localhost:1234 Hello from non-Map delegate.
localhost:1234/?branch=1 Branch used = master

Map支持嵌套,例如:

1
app.Map("/level1", level1App => { level1App.Map("/level2a", level2AApp => { // "/level1/level2a" //... }); level1App.Map("/level2b", level2BApp => { // "/level1/level2b" //... }); });

Map也可以一次匹配多个片段,例如:

1
app.Map("/level1/level2", HandleMultiSeg);

内置中间件#

ASP.NET Core附带以下中间件组件:

中间件 描述
Authentication 提供身份验证支持
CORS 配置跨域资源共享
Response Caching 提供缓存响应支持
Response Compression 提供响应压缩支持
Routing 定义和约束请求路由
Session 提供用户会话管理
Static Files 为静态文件和目录浏览提供服务提供支持
URL Rewriting Middleware 用于重写 Url,并将请求重定向的支持

编写中间件

中间件通常封装在一个类中,并使用扩展方法进行暴露。 查看以下中间件,它从查询字符串设置当前请求的Culture:

1
public class Startup { public void Configure(IApplicationBuilder app) { app.Use((context, next) => { var cultureQuery = context.Request.Query["culture"]; if (!string.IsNullOrWhiteSpace(cultureQuery)) { var culture = new CultureInfo(cultureQuery); CultureInfo.CurrentCulture = culture; CultureInfo.CurrentUICulture = culture; } // Call the next delegate/middleware in the pipeline return next(); }); app.Run(async (context) => { await context.Response.WriteAsync( $"Hello {CultureInfo.CurrentCulture.DisplayName}"); }); } }

您可以通过传递Culture来测试中间件,例如 http://localhost:19219/?culture=zh-CN

以下代码将中间件委托移动到一个类:

1
using Microsoft.AspNetCore.Http; using System.Globalization; using System.Threading.Tasks; namespace Culture { public class RequestCultureMiddleware { private readonly RequestDelegate _next; public RequestCultureMiddleware(RequestDelegate next) { _next = next; } public Task Invoke(HttpContext context) { var cultureQuery = context.Request.Query["culture"]; if (!string.IsNullOrWhiteSpace(cultureQuery)) { var culture = new CultureInfo(cultureQuery); CultureInfo.CurrentCulture = culture; CultureInfo.CurrentUICulture = culture; } // Call the next delegate/middleware in the pipeline return this._next(context); } } }

以下通过IApplicationBuilder的扩展方法暴露中间件:

1
using Microsoft.AspNetCore.Builder; namespace Culture { public static class RequestCultureMiddlewareExtensions { public static IApplicationBuilder UseRequestCulture( this IApplicationBuilder builder) { return builder.UseMiddleware<RequestCultureMiddleware>(); } } }

以下代码从Configure调用中间件:

1
public class Startup { public void Configure(IApplicationBuilder app) { app.UseRequestCulture(); app.Run(async (context) => { await context.Response.WriteAsync( $"Hello {CultureInfo.CurrentCulture.DisplayName}"); }); } }

中间件应该遵循显式依赖原则,通过在其构造函数中暴露其依赖关系。 中间件在应用程序生命周期构建一次。 如果您需要在请求中与中间件共享服务,请参阅以下请求相关性。

中间件组件可以通过构造方法参数来解析依赖注入的依赖关系。 UseMiddleware也可以直接接受其他参数。

每个请求的依赖关系

因为中间件是在应用程序启动时构建的,而不是每个请求,所以在每个请求期间,中间件构造函数使用的作用域生命周期服务不会与其他依赖注入类型共享。 如果您必须在中间件和其他类型之间共享作用域服务,请将这些服务添加到Invoke方法的签名中。 Invoke方法可以接受由依赖注入填充的其他参数。 例如:

1
public class MyMiddleware { private readonly RequestDelegate _next; public MyMiddleware(RequestDelegate next) { _next = next; } public async Task Invoke(HttpContext httpContext, IMyScopedService svc) { svc.MyProperty = 1000; await _next(httpContext); } }

业务场景:

业务需求要求,需要对 WebApi 接口服务统一返回参数,也就是把实际的结果用一定的格式包裹起来,比如下面格式:

1
2
3
4
5
6
7
{
"response":{
"code":200,
"msg":"Remote service error",
"result":""
}
}

具体实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Filters;

public class WebApiResultMiddleware : ActionFilterAttribute
{
public override void OnResultExecuting(ResultExecutingContext context)
{

if (context.Result is ObjectResult)
{
var objectResult = context.Result as ObjectResult;
if (objectResult.Value == null)
{
context.Result = new ObjectResult(new { code = 404, sub_msg = "未找到资源", msg = "" });
}
else
{
context.Result = new ObjectResult(new { code = 200, msg = "", result = objectResult.Value });
}
}
else if (context.Result is EmptyResult)
{
context.Result = new ObjectResult(new { code = 404, sub_msg = "未找到资源", msg = "" });
}
else if (context.Result is ContentResult)
{
context.Result = new ObjectResult(new { code = 200, msg = "", result= (context.Result as ContentResult).Content });
}
else if (context.Result is StatusCodeResult)
{
context.Result = new ObjectResult(new { code = (context.Result as StatusCodeResult).StatusCode, sub_msg = "", msg = "" });
}
}
}

Startup添加对应配置:

1
2
3
4
5
6
7
8
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc(options =>
{
options.Filters.Add(typeof(WebApiResultMiddleware));
options.RespectBrowserAcceptHeader = true;
});
}

ASP.NET Core WebApi使用Swagger生成api说明文档看这篇就够了 - 依乐祝 - 博客园

Excerpt

引言 在使用asp.net core 进行api开发完成后,书写api说明文档对于程序员来说想必是件很痛苦的事情吧,但文档又必须写,而且文档的格式如果没有具体要求的话,最终完成的文档则完全取决于开发者的心情。或者详细点,或者简单点。那么有没有一种快速有效的方法来构建api说明文档呢?答案是肯定的,


引言

在使用asp.net core 进行api开发完成后,书写api说明文档对于程序员来说想必是件很痛苦的事情吧,但文档又必须写,而且文档的格式如果没有具体要求的话,最终完成的文档则完全取决于开发者的心情。或者详细点,或者简单点。那么有没有一种快速有效的方法来构建api说明文档呢?答案是肯定的, Swagger就是最受欢迎的REST APIs文档生成工具之一!

为什么使用Swagger作为REST APIs文档生成工具

  1. Swagger 可以生成一个具有互动性的API控制台,开发者可以用来快速学习和尝试API。
  2. Swagger 可以生成客户端SDK代码用于各种不同的平台上的实现。
  3. Swagger 文件可以在许多不同的平台上从代码注释中自动生成。
  4. Swagger 有一个强大的社区,里面有许多强悍的贡献者。

asp.net core中如何使用Swagger生成api说明文档呢

  1. Swashbuckle.AspNetCore 是一个开源项目,用于生成 ASP.NET Core Web API 的 Swagger 文档。
  2. NSwag 是另一个用于将 Swagger UI 或 ReDoc 集成到 ASP.NET Core Web API 中的开源项目。 它提供了为 API 生成 C# 和 TypeScript 客户端代码的方法。

下面以Swashbuckle.AspNetCore为例为大家进行展示

Swashbuckle由哪些组成部分呢?

  • Swashbuckle.AspNetCore.Swagger:将 SwaggerDocument 对象公开为 JSON 终结点的 Swagger 对象模型和中间件。
  • Swashbuckle.AspNetCore.SwaggerGen:从路由、控制器和模型直接生成 SwaggerDocument 对象的 Swagger 生成器。 它通常与 Swagger 终结点中间件结合,以自动公开 Swagger JSON。
  • Swashbuckle.AspNetCore.SwaggerUI:Swagger UI 工具的嵌入式版本。 它解释 Swagger JSON 以构建描述 Web API 功能的可自定义的丰富体验。 它包括针对公共方法的内置测试工具。

如何使用vs2017安装Swashbuckle呢?

添加并配置 Swagger 中间件

首先引入命名空间:

1
using Swashbuckle.AspNetCore.Swagger;

将 Swagger 生成器添加到 Startup.ConfigureServices 方法中的服务集合中:

1
//注册Swagger生成器,定义一个和多个Swagger 文档 services.AddSwaggerGen(c => { c.SwaggerDoc("v1", new Info { Title = "My API", Version = "v1" }); });

在 Startup.Configure 方法中,启用中间件为生成的 JSON 文档和 Swagger UI 提供服务:

1
//启用中间件服务生成Swagger作为JSON终结点 app.UseSwagger(); //启用中间件服务对swagger-ui,指定Swagger JSON终结点 app.UseSwaggerUI(c => { c.SwaggerEndpoint("/swagger/v1/swagger.json", "My API V1"); });

启动应用,并导航到 http://localhost:<port>/swagger/v1/swagger.json。 生成的描述终结点的文档显示如下json格式。

1530193531880

可在 http://localhost:<port>/swagger 找到 Swagger UI。 通过 Swagger UI 浏览 API文档,如下所示。

1530193586713

要在应用的根 (http://localhost:<port>/) 处提供 Swagger UI,请将 RoutePrefix 属性设置为空字符串:

1
app.UseSwaggerUI(c => { c.SwaggerEndpoint("/swagger/v1/swagger.json", "My API V1"); c.RoutePrefix = string.Empty; });

Swagger的高级用法(自定义以及扩展)

使用Swagger为API文档增加说明信息

在 AddSwaggerGen 方法的进行如下的配置操作会添加诸如作者、许可证和说明信息等:

1
//注册Swagger生成器,定义一个和多个Swagger 文档 services.AddSwaggerGen(c => { c.SwaggerDoc("v1", new Info { Version = "v1", Title = "yilezhu's API", Description = "A simple example ASP.NET Core Web API", TermsOfService = "None", Contact = new Contact { Name = "依乐祝", Email = string.Empty, Url = "http://www.cnblogs.com/yilezhu/" }, License = new License { Name = "许可证名字", Url = "http://www.cnblogs.com/yilezhu/" } }); });

wagger UI 显示版本的信息如下图所示:

1530194050313

为了防止博客被转载后,不保留本文的链接,特意在此加入本文的链接:https://www.cnblogs.com/yilezhu/p/9241261.html

为接口方法添加注释

大家先点击下api,展开如下图所示,可以没有注释啊,怎么来添加注释呢?

1530194181832

按照下图所示用三个/添加文档注释,如下所示

1
/// <summary> /// 这是一个api方法的注释 /// </summary> /// <returns></returns> [HttpGet] public ActionResult<IEnumerable<string>> Get() { return new string[] { "value1", "value2" }; }

然后运行项目,回到swaggerUI中去查看注释是否出现了呢

1530194413243

还是没有出现,别急,往下看!

启用XML 注释

可使用以下方法启用 XML 注释:

  • 右键单击“解决方案资源管理器”中的项目,然后选择“属性”
  • 查看“生成”选项卡的“输出”部分下的“XML 文档文件”框
  • 1530194540621

启用 XML 注释后会为未记录的公共类型和成员提供调试信息。如果出现很多警告信息  例如,以下消息指示违反警告代码 1591:

1
warning CS1591: Missing XML comment for publicly visible type or member 'TodoController.GetAll()'

如果你有强迫症,想取消警告怎么办呢?可以按照下图所示进行取消

1530194772758

注意上面生成的xml文档文件的路径,

注意:

1.对于 Linux 或非 Windows 操作系统,文件名和路径区分大小写。 例如,“SwaggerDemo.xml”文件在 Windows 上有效,但在 CentOS 上无效。

2.获取应用程序路径,建议采用Path.GetDirectoryName(typeof(Program).Assembly.Location)这种方式或者·AppContext.BaseDirectory这样来获取

1
//注册Swagger生成器,定义一个和多个Swagger 文档 services.AddSwaggerGen(c => { c.SwaggerDoc("v1", new Info { Version = "v1", Title = "yilezhu's API", Description = "A simple example ASP.NET Core Web API", TermsOfService = "None", Contact = new Contact { Name = "依乐祝", Email = string.Empty, Url = "http://www.cnblogs.com/yilezhu/" }, License = new License { Name = "许可证名字", Url = "http://www.cnblogs.com/yilezhu/" } }); // 为 Swagger JSON and UI设置xml文档注释路径 var basePath = Path.GetDirectoryName(typeof(Program).Assembly.Location);//获取应用程序所在目录(绝对,不受工作目录影响,建议采用此方法获取路径) var xmlPath = Path.Combine(basePath, "SwaggerDemo.xml"); c.IncludeXmlComments(xmlPath); });

重新生成并运行项目查看一下注释出现了没有

1530195392840

通过上面的操作可以总结出,Swagger UI 显示上述注释代码的 <summary> 元素的内部文本作为api大的注释!

当然你还可以将 remarks 元素添加到 Get 操作方法文档。 它可以补充 <summary> 元素中指定的信息,并提供更可靠的 Swagger UI。 <remarks> 元素内容可包含文本、JSON 或 XML。 代码如下:

1
/// <summary> /// 这是一个带参数的get请求 /// </summary> /// <remarks> /// 例子: /// Get api/Values/1 /// </remarks> /// <param name="id">主键</param> /// <returns>测试字符串</returns> [HttpGet("{id}")] public ActionResult<string> Get(int id) { return $"你请求的 id 是 {id}"; }

重新生成下项目,当好到SwaggerUI看到如下所示:

1530196170696

描述响应类型

摘录自:https://www.cnblogs.com/yanbigfeg/p/9232844.html

接口使用者最关心的就是接口的返回内容和响应类型啦。下面展示一下201和400状态码的一个简单例子:

我们需要在我们的方法上添加:[ProducesResponseType(201)][ProducesResponseType(400)]

然后添加相应的状态说明:返回value字符串如果id为空

最终代码应该是这个样子:

1
/// <summary> /// 这是一个带参数的get请求 /// </summary> /// <remarks> /// 例子: /// Get api/Values/1 /// </remarks> /// <param name="id">主键</param> /// <returns>测试字符串</returns> /// <response code="201">返回value字符串</response> /// <response code="400">如果id为空</response> // GET api/values/2 [HttpGet("{id}")] [ProducesResponseType(201)] [ProducesResponseType(400)] public ActionResult<string> Get(int id) { return $"你请求的 id 是 {id}"; }

效果如下所示
状态相应效果

使用SwaggerUI测试api接口

下面我们通过一个小例子通过SwaggerUI调试下接口吧

  1. 点击一个需要测试的API接口,然后点击Parameters左右边的“Try it out ” 按钮
  2. 在出现的参数文本框中输入参数,如下图所示的,输入参数2
  3. 点击执行按钮,会出现下面所示的格式化后的Response,如下图所示

1530196606406

好了,今天的在ASP.NET Core WebApi使用Swagger生成api说明文档看这篇就够了的教程就到这里了。希望能够对大家学习在ASP.NET Core中使用Swagger生成api文档有所帮助!

总结

本文从手工书写api文档的痛处说起,进而引出Swagger这款自动生成api说明文档的工具!然后通过通俗易懂的文字结合图片为大家演示了如何在一个ASP.NET Core WebApi中使用SwaggerUI生成api说明文档。最后又为大家介绍了一些ASP.NET Core 中Swagger的一些高级用法!希望对大家在ASP.NET Core中使用Swagger有所帮助!

ASP.NET Core 2.0 使用支付宝PC网站支付 - 晓晨Master - 博客园

Excerpt

前言 == 最近在使用ASP.NET Core来进行开发,刚好有个接入支付宝支付的需求,百度了一下没找到相关的资料,看了官方的SDK以及Demo都还是.NET Framework的,所以就先根据官方SDK的源码,用.NET Standard 2.0 实现了支付宝服务端SDK,Alipay.AopSd


前言

最近在使用ASP.NET Core来进行开发,刚好有个接入支付宝支付的需求,百度了一下没找到相关的资料,看了官方的SDK以及Demo都还是.NET Framework的,所以就先根据官方SDK的源码,用.NET Standard 2.0 实现了支付宝服务端SDK,Alipay.AopSdk.Core(github:https://github.com/stulzq/Alipay.AopSdk.Core) ,支持.NET CORE 2.0。为了使用方便,已上传至Nuget可以直接使用。

支付宝有比较多的支付产品,比如当面付、APP支付、手机网站支付、电脑网站支付等,本次讲的是电脑网站支付。

如果你没有时间阅读文章,可以直接从github获取Demo原来进行查看,非常简单。github: https://github.com/stulzq/Alipay.Demo.PCPayment

创建项目

新建一个ASP.NET Core 2.0 MVC项目

配置

由于我在开发的时候支付接口并没有申请下来,所以使用的是支付宝沙箱环境来进行开发的。

支付宝沙箱环境介绍:蚂蚁沙箱环境(Beta)是协助开发者进行接口功能开发及主要功能联调的辅助环境。沙箱环境模拟了开放平台部分产品的主要功能和主要逻辑,在开发者应用上线审核前,开发者可以根据自身需求,先在沙箱环境中了解、组合和调试各种开放接口,进行开发调通工作,从而帮助开发者在应用上线审核完成后,能更快速、更顺利的进行线上调试和验收工作。
如果在签约或创建应用前想要进行集成测试,可以使用沙箱环境。
沙箱环境支持使用个人账号或企业账号登陆。

沙箱环境地址:https://openhome.alipay.com/platform/appDaily.htm?tab=info

1.生成密钥#

这里所使用的RSA密钥标准为PKCS1,需要特别注意。

可以下载我写的密钥生成器:https://github.com/dotnetcore/Alipay.AopSdk.Core/tree/dev/tool

运行可以直接生成长度为2048标准为PKCS1的公钥和私钥。

或者使用下载支付宝官方提供的密钥生成工具来进行生,详细介绍:https://doc.open.alipay.com/docs/doc.htm?treeId=291&articleId=105971&docType=1

2.设置应用公钥#

我们生成密钥之后,需要到支付宝后台设置应用公钥,就是我们生成的公钥。

设置之后,支付宝会给我们一个支付宝公钥,保存这个支付宝公钥

这个支付宝公钥和我们自己生成的公钥是不一样的,我们在配置SDK时用的公钥就是支付宝公钥

3.配置SDK#

新建一个Config类,在里面存储我们的配置。

1
public class Config { // 应用ID,您的APPID public static string AppId = ""; // 支付宝网关 public static string Gatewayurl = ""; // 商户私钥,您的原始格式RSA私钥 public static string PrivateKey = ""; // 支付宝公钥,查看地址:https://openhome.alipay.com/platform/keyManage.htm 对应APPID下的支付宝公钥。 public static string AlipayPublicKey = ""; // 签名方式 public static string SignType = "RSA2"; // 编码格式 public static string CharSet = "UTF-8"; }
  • 应用ID和支付宝网关都可以在支付宝后台查看。

  • 商户私钥即我们自己生成的私钥,公钥就是支付宝公钥这里一定要注意,别用错了。这里的公钥私钥直接填写字符串即可。

  • 签名方式推荐使用RSA2,使用RSA2,支付宝会用SHA256withRsa算法进行接口调用时的验签(不限制密钥长度)。

  • 编码格式,如果我们是直接配置的字符串(公钥、私钥),那么就是我们代码的编码,如果使用的是文件(公钥、私钥),那么就是文件的编码。

  • 完成配置如下:

添加SDK

官方SDK的源码(.NET Framework),用.NET Standard 2.0 实现的支付宝服务端SDK,Alipay.AopSdk.Core(github:https://github.com/stulzq/Alipay.AopSdk.Core) ,支持.NET Core 2.0。
通过Nuget安装:Install-Package Alipay.AopSdk.Core

支付

添加一个控制器 PayController

1
/// 发起支付请求 /// </summary> /// <param name="tradeno">外部订单号,商户网站订单系统中唯一的订单号</param> /// <param name="subject">订单名称</param> /// <param name="totalAmout">付款金额</param> /// <param name="itemBody">商品描述</param> /// <returns></returns> [HttpPost] public void PayRequest(string tradeno,string subject,string totalAmout,string itemBody) { DefaultAopClient client = new DefaultAopClient(Config.Gatewayurl, Config.AppId, Config.PrivateKey, "json", "2.0", Config.SignType, Config.AlipayPublicKey, Config.CharSet, false); // 组装业务参数model AlipayTradePagePayModel model = new AlipayTradePagePayModel(); model.Body = itemBody; model.Subject = subject; model.TotalAmount = totalAmout; model.OutTradeNo = tradeno; model.ProductCode = "FAST_INSTANT_TRADE_PAY"; AlipayTradePagePayRequest request = new AlipayTradePagePayRequest(); // 设置同步回调地址 request.SetReturnUrl("http://localhost:5000/Pay/Callback"); // 设置异步通知接收地址 request.SetNotifyUrl(""); // 将业务model载入到request request.SetBizModel(model); var response = client.SdkExecute(request); Console.WriteLine($"订单支付发起成功,订单号:{tradeno}"); //跳转支付宝支付 Response.Redirect(Config.Gatewayurl + "?" + response.Body); }

运行:

  • 图1

  • 图2

  • 图3

支付异步回调通知

支付宝同步回调通知(支付成功后跳转到商户网站),是不可靠的,所以这里必须使用异步通知来获取支付结果,异步通知即支付宝主动请求我们提供的地址,我们根据请求数据来校验,获取支付结果。

1
/// <summary> /// 支付异步回调通知 需配置域名 因为是支付宝主动post请求这个action 所以要通过域名访问或者公网ip /// </summary> public async void Notify() { /* 实际验证过程建议商户添加以下校验。 1、商户需要验证该通知数据中的out_trade_no是否为商户系统中创建的订单号, 2、判断total_amount是否确实为该订单的实际金额(即商户订单创建时的金额), 3、校验通知中的seller_id(或者seller_email) 是否为out_trade_no这笔单据的对应的操作方(有的时候,一个商户可能有多个seller_id/seller_email) 4、验证app_id是否为该商户本身。 */ Dictionary<string, string> sArray = GetRequestPost(); if (sArray.Count != 0) { bool flag = AlipaySignature.RSACheckV1(sArray, Config.AlipayPublicKey,Config.CharSet, Config.SignType, false); if (flag) { //交易状态 //判断该笔订单是否在商户网站中已经做过处理 //如果没有做过处理,根据订单号(out_trade_no)在商户网站的订单系统中查到该笔订单的详细,并执行商户的业务程序 //请务必判断请求时的total_amount与通知时获取的total_fee为一致的 //如果有做过处理,不执行商户的业务程序 //注意: //退款日期超过可退款期限后(如三个月可退款),支付宝系统发送该交易状态通知 Console.WriteLine(Request.Form["trade_status"]); await Response.WriteAsync("success"); } else { await Response.WriteAsync("fail"); } } }

同步回调

同步回调即支付成功跳转回商户网站

运行:

1
/// <summary> /// 支付同步回调 /// </summary> [HttpGet] public IActionResult Callback() { /* 实际验证过程建议商户添加以下校验。 1、商户需要验证该通知数据中的out_trade_no是否为商户系统中创建的订单号, 2、判断total_amount是否确实为该订单的实际金额(即商户订单创建时的金额), 3、校验通知中的seller_id(或者seller_email) 是否为out_trade_no这笔单据的对应的操作方(有的时候,一个商户可能有多个seller_id/seller_email) 4、验证app_id是否为该商户本身。 */ Dictionary<string, string> sArray = GetRequestGet(); if (sArray.Count != 0) { bool flag = AlipaySignature.RSACheckV1(sArray, Config.AlipayPublicKey, Config.CharSet, Config.SignType, false); if (flag) { Console.WriteLine($"同步验证通过,订单号:{sArray["out_trade_no"]}"); ViewData["PayResult"] = "同步验证通过"; } else { Console.WriteLine($"同步验证失败,订单号:{sArray["out_trade_no"]}"); ViewData["PayResult"] = "同步验证失败"; } } return View(); }

订单查询

查询订单当前状态:已付款、未付款等等。

运行:

1
[HttpPost] public JsonResult Query(string tradeno, string alipayTradeNo) { DefaultAopClient client = new DefaultAopClient(Config.Gatewayurl, Config.AppId, Config.PrivateKey, "json", "2.0", Config.SignType, Config.AlipayPublicKey, Config.CharSet, false); AlipayTradeQueryModel model = new AlipayTradeQueryModel(); model.OutTradeNo = tradeno; model.TradeNo = alipayTradeNo; AlipayTradeQueryRequest request = new AlipayTradeQueryRequest(); request.SetBizModel(model); var response = client.Execute(request); return Json(response.Body); }

订单退款

退回该订单金额。

运行:

1
/// <summary> /// 订单退款 /// </summary> /// <param name="tradeno">商户订单号</param> /// <param name="alipayTradeNo">支付宝交易号</param> /// <param name="refundAmount">退款金额</param> /// <param name="refundReason">退款原因</param> /// <param name="refundNo">退款单号</param> /// <returns></returns> [HttpPost] public JsonResult Refund(string tradeno,string alipayTradeNo,string refundAmount,string refundReason,string refundNo) { DefaultAopClient client = new DefaultAopClient(Config.Gatewayurl, Config.AppId, Config.PrivateKey, "json", "2.0", Config.SignType, Config.AlipayPublicKey, Config.CharSet, false); AlipayTradeRefundModel model = new AlipayTradeRefundModel(); model.OutTradeNo = tradeno; model.TradeNo = alipayTradeNo; model.RefundAmount = refundAmount; model.RefundReason = refundReason; model.OutRequestNo = refundNo; AlipayTradeRefundRequest request = new AlipayTradeRefundRequest(); request.SetBizModel(model); var response = client.Execute(request); return Json(response.Body); }

退款查询

查询退款信息。

运行:

1
/// <summary> /// 退款查询 /// </summary> /// <param name="tradeno">商户订单号</param> /// <param name="alipayTradeNo">支付宝交易号</param> /// <param name="refundNo">退款单号</param> /// <returns></returns> [HttpPost] public JsonResult RefundQuery(string tradeno,string alipayTradeNo,string refundNo) { DefaultAopClient client = new DefaultAopClient(Config.Gatewayurl, Config.AppId, Config.PrivateKey, "json", "2.0", Config.SignType, Config.AlipayPublicKey, Config.CharSet, false); if (string.IsNullOrEmpty(refundNo)) { refundNo = tradeno; } AlipayTradeFastpayRefundQueryModel model = new AlipayTradeFastpayRefundQueryModel(); model.OutTradeNo = tradeno; model.TradeNo = alipayTradeNo; model.OutRequestNo = refundNo; AlipayTradeFastpayRefundQueryRequest request = new AlipayTradeFastpayRefundQueryRequest(); request.SetBizModel(model); var response = client.Execute(request); return Json(response.Body); }

订单关闭

对一定时间以后没有进行付款的订单进行关闭,订单状态需为:待付款,已完成支付的订单无法关闭。

运行:

1
/// <summary> /// 关闭订单 /// </summary> /// <param name="tradeno">商户订单号</param> /// <param name="alipayTradeNo">支付宝交易号</param> /// <returns></returns> [HttpPost] public JsonResult OrderClose(string tradeno, string alipayTradeNo) { DefaultAopClient client = new DefaultAopClient(Config.Gatewayurl, Config.AppId, Config.PrivateKey, "json", "2.0", Config.SignType, Config.AlipayPublicKey, Config.CharSet, false); AlipayTradeCloseModel model = new AlipayTradeCloseModel(); model.OutTradeNo = tradeno; model.TradeNo = alipayTradeNo; AlipayTradeCloseRequest request = new AlipayTradeCloseRequest(); request.SetBizModel(model); var response = client.Execute(request); return Json(response.Body); }

地址集合

最重要的:

本文Demo:https://github.com/stulzq/Alipay.Demo.PCPayment

如果有问题欢迎提出!

目录

一.简介

gRPC 是一个由Google开源的,跨语言的,高性能的远程过程调用(RPC)框架。 gRPC使客户端和服务端应用程序可以透明地进行通信,并简化了连接系统的构建。它使用HTTP/2作为通信协议,使用 Protocol Buffers 作为序列化协议。

它的主要优点:

  • 现代高性能轻量级 RPC 框架。
  • 约定优先的 API 开发,默认使用 Protocol Buffers 作为描述语言,允许与语言无关的实现。
  • 可用于多种语言的工具,以生成强类型的服务器和客户端。
  • 支持客户端,服务器双向流调用。
  • 通过Protocol Buffers二进制序列化减少网络使用。
  • 使用 HTTP/2 进行传输

这些优点使gRPC非常适合:

  • 高性能轻量级微服务 - gRPC设计为低延迟和高吞吐量通信,非常适合需要高性能的轻量级微服务。
  • 多语言混合开发 - gRPC工具支持所有流行的开发语言,使gRPC成为多语言开发环境的理想选择。
  • 点对点实时通信 - gRPC对双向流调用提供出色的支持。gRPC服务可以实时推送消息而无需轮询。
  • 网络受限环境 - 使用 Protocol Buffers二进制序列化消息,该序列化始终小于等效的JSON消息,对网络带宽需求比JSON小。

不建议使用gRPC的场景:

  • 浏览器可访问的API - 浏览器不完全支持gRPC。虽然gRPC-Web可以提供浏览器支持,但是它有局限性,引入了服务器代理
  • 广播实时通信 - gRPC支持通过流进行实时通信,但不存在向已注册连接广播消息的概念
  • 进程间通信 - 进程必须承载HTTP/2才能接受传入的gRPC调用,对于Windows,进程间通信管道是一种更快速的方法。

摘自微软官方文档

支持的语言如下:

1569301484094

二.gRPC on .NET Core

gRPC 现在可以非常简单的在 .NET Core 和 ASP.NET Core 中使用,在 .NET Core 上的实现的开源地址:https://github.com/grpc/grpc-dotnet ,它目前由微软官方 ASP.NET 项目的人员进行维护,良好的接入 .NET Core 生态。

.NET Core 的 gRPC 功能如下:

  • Grpc.AspNetCore 一个用于在ASP.NET Core承载gRPC服务的框架,将 gRPC和ASP.NET Core 功能集成在一起,如:日志、依赖注入、身份认证和授权。
  • Grpc.Net.Client 基于HttpClient (HttpClient现已支持HTTP/2)的 gRPC客户端
  • Grpc.Net.ClientFactory 与gRPC客户端集成的HttpClientFactory,允许对gRPC客户端进行集中配置,并使用DI注入到应用程序中

三.使用 ASP.NET Core 创建 gRPC 服务

  1. 通过 Visual Studio 2019 (16.3.0)提供的模板,可以快速创建 gRPC 服务。

1569332979179

扒拉一下默认源码包含了什么东东。

① 配置文件 appsettings.json ,多了Kestrel 启用 HTTP/2 的配置,因为 gRPC 是基于 HTTP/2 来通信的

1569333539435

② PB协议文件 greet.proto 用于自动生成服务、客户端和消息(表示传递的数据)的C# Class

1569333899754

③ 服务类 GreeterService ,服务类集成的 Greeter.GreeterBase 来自于根据proto文件自动生成的,生成的类在 obj\Debug\netcoreapp3.0目录下

1569334077321

自动生成的类:

1569334149194

Startup.cs类,将 gRPC服务添加到了终结点路由中

1569334239963

⑤ csproj 项目文件,包含了 proto 文件引用

1569334307823

2.运行

第一次运行会提示是否信任证书,点击“是”

1569334375312

1569334392704

这是因为HTTP/2需要HTTPS,尽管HTTP/2协议没有明确规定需要HTTPS,但是为了安全在浏览器实现上都要求了HTTPS,所以现在的HTTP/2和HTTPS基本都是一对。

1569334575324

四. 创建 gRPC 客户端

1.添加一个.NET Core 控制台应用程序

2.通过nuget添加包:Grpc.Net.Client、Google.Protobuf、Grpc.Tools

1569335021283

3.将服务的 proto 文件复制到客户端

1569335104139

4.编辑客户端项目文件,添加关于proto文件的描述

1
2
3
<ItemGroup>
<Protobuf Include="Protos\greet.proto" GrpcServices="Client" />
</ItemGroup>

注意 GrpcServices="Client" 这里是Client和服务是不一样的

5.生成客户端项目可以通过proto文件生成类

6.添加客户端调用代码

1
2
3
4
5
6
7
8
9
static async Task Main(string[] args)
{
var channel = GrpcChannel.ForAddress("https://localhost:5001");
var client = new Greeter.GreeterClient(channel);
var reply = await client.SayHelloAsync(
new HelloRequest { Name = "晓晨" });
Console.WriteLine("Greeter 服务返回数据: " + reply.Message);
Console.ReadKey();
}

7.先启动服务,然后运行客户端

1569335521902

这里可以看到,客户端成功调用了服务,收到了返回的消息。

五.自己动手写一个服务

前面我们使用的 Greeter 服务是由模板自动给我们创建的,现在我们来自己动手写一个服务。

编写一个“撸猫服务”

1.定义 proto 文件 LuCat.proto,并在csproj项目文件中添加描述

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
syntax = "proto3";

option csharp_namespace = "AspNetCoregRpcService";

import "google/protobuf/empty.proto";
package LuCat; //定义包名

//定义服务
service LuCat{
//定义吸猫方法
rpc SuckingCat(google.protobuf.Empty) returns(SuckingCatResult);
}

message SuckingCatResult{
string message=1;
}


2.实现服务 LuCatService.cs

1
2
3
4
5
6
7
8
9
10
11
12
public class LuCatService:LuCat.LuCatBase
{
private static readonly List<string> Cats=new List<string>(){"英短银渐层","英短金渐层","美短","蓝猫","狸花猫","橘猫"};
private static readonly Random Rand=new Random(DateTime.Now.Millisecond);
public override Task<SuckingCatResult> SuckingCat(Empty request, ServerCallContext context)
{
return Task.FromResult(new SuckingCatResult()
{
Message = $"您吸了一只{Cats[Rand.Next(0, Cats.Count)]}"
});
}
}

3.在 Startup终结点路由中注册

1
endpoints.MapGrpcService<LuCatService>();

4.添加客户端调用

1
2
3
var catClient = new LuCat.LuCatClient(channel);
var catReply = await catClient.SuckingCatAsync(new Empty());
Console.WriteLine("调用撸猫服务:"+ catReply.Message);

5.运行测试

1569338919789

六.实际使用中的技巧

技巧1

上面章节的操作步骤中,我们需要在服务和客户端之间复制proto,这是一个可以省略掉的步骤。

1.复制 Protos 文件夹到解决方案根目录(sln文件所在目录)

1569335816218

2.删除客户端和服务项目中的 Protos 文件夹

3.在客户端项目文件csproj中添加关于proto文件的描述

1
2
3
<ItemGroup>
<Protobuf Include="..\..\Protos\greet.proto" GrpcServices="Client" Link="Protos\greet.proto" />
</ItemGroup>

4.在服务项目文件csproj中添加关于proto文件的描述

1
2
3
<ItemGroup>
<Protobuf Include="..\..\Protos\greet.proto" GrpcServices="Server" Link="Protos\greet.proto" />
</ItemGroup>

在实际项目中,请自己计算相对路径

5.这样两个项目都是使用的一个proto文件,只用维护这一个文件即可

1569336339344

技巧2

我们在实际项目中使用,肯定有多个 proto 文件,难道我们每添加一个 proto 文件都要去更新 csproj文件?

我们可以使用MSBuild变量来帮我们完成,我们将 csproj 项目文件中引入proto文件信息进行修改。

服务端:

1
2
3
<ItemGroup>
<Protobuf Include="..\..\Protos\*.proto" GrpcServices="Server" Link="Protos\%(RecursiveDir)%(Filename)%(Extension)" />
</ItemGroup>

客户端:

1
2
3
<ItemGroup>
<Protobuf Include="..\..\Protos\*.proto" GrpcServices="Client" Link="Protos\%(RecursiveDir)%(Filename)%(Extension)" />
</ItemGroup>

示例:

1569339140058

七.总结

gRPC 现目前是一款非常成熟的高性能RPC框架,当前的生态是非常好的,很多公司的产品或者开源项目都有在使用gRPC,有了它,相信可以让我们更容易的构建.NET Core 微服务,可以让 .NET Core 更好的接入 gRPC 生态。不得不说这是 .NET Core 3.0 带来的最令人振奋的特性之一。

参考资料:

如果大家无法访问proto3说明文档,这里提供一个离线网页版(请另存为下载后用Chrome打开)

最近把一个Asp .net core 2.0的项目迁移到Asp .net core 3.1,项目启动的时候直接报错:

1
2
3
InvalidOperationException: Endpoint CoreAuthorization.Controllers.HomeController.Index (CoreAuthorization) contains authorization metadata, but a middleware was not found that supports authorization.
Configure your application startup by adding app.UseAuthorization() inside the call to Configure(..) in the application startup code. The call to app.UseAuthorization() must appear between app.UseRouting() and app.UseEndpoints(...).
Microsoft.AspNetCore.Routing.EndpointMiddleware.ThrowMissingAuthMiddlewareException(Endpoint endpoint)

看意思是缺少了一个authorization的中间件,这个项目在Asp.net core 2.0上是没问题的。
startup是这样注册的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}

public IConfiguration Configuration { get; }


public void ConfigureServices(IServiceCollection services)
{
services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme).AddCookie(options =>
{
options.LoginPath = "/account/Login";
});

services.AddControllersWithViews();
}


public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
app.UseExceptionHandler("/Home/Error");

app.UseHsts();
}

app.UseStaticFiles();

app.UseRouting();

app.UseAuthentication();

app.UseEndpoints(endpoints =>
{
endpoints.MapControllerRoute(
name: "default",
pattern: "{controller=Home}/{action=Index}/{id?}");
});
}
}

查了文档后发现3.0的示例代码多了一个UseAuthorization,改成这样就可以了:

1
2
3
4
5
6
7
8
9
10
11
app.UseRouting();
app.UseAuthentication();

app.UseAuthorization();

app.UseEndpoints(endpoints =>
{
endpoints.MapControllerRoute(
name: "default",
pattern: "{controller=Home}/{action=Index}/{id?}");
});

看来Asp .net Core 3.1的认证跟授权又不太一样了,只能继续看文档学习了。

先说一下Authentication跟Authorization的区别。这两个单词长的十分相似,而且还经常一起出现,很多时候容易搞混了。

  1. Authentication是认证,明确是你谁,确认是不是合法用户。常用的认证方式有用户名密码认证。
  2. Authorization是授权,明确你是否有某个权限。当用户需要使用某个功能的时候,系统需要校验用户是否需要这个功能的权限。
    所以这两个单词是不同的概念,不同层次的东西。UseAuthorization在asp.net core 2.0中是没有的。在3.0之后微软明确的把授权功能提取到了Authorization中间件里,所以我们需要在UseAuthentication之后再次UseAuthorization。否则,当你使用授权功能比如使用[Authorize]属性的时候系统就会报错。

Authentication(认证)

认证的方案有很多,最常用的就是用户名密码认证,下面演示下基于用户名密码的认证。新建一个MVC项目,添加AccountController:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
[HttpPost]
public async Task<IActionResult> Login(
[FromForm]string userName, [FromForm]string password
)
{

...
var claims = new List<Claim>
{
new Claim(ClaimTypes.Name, userName),
new Claim(ClaimTypes.Role, "老师")
};

var claimsIdentity = new ClaimsIdentity(
claims, CookieAuthenticationDefaults.AuthenticationScheme);

await HttpContext.SignInAsync(
CookieAuthenticationDefaults.AuthenticationScheme,
new ClaimsPrincipal(claimsIdentity));

return Redirect("/");
}
public async Task<IActionResult> Logoff()
{
await HttpContext.SignOutAsync();

return Redirect("Login");
}

public IActionResult AccessDenied()
{
return Content("AccessDenied");
}

修改login.cshtml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@{
ViewData["Title"] = "Login Page";
}

<h1>
Login Page
</h1>

<form method="post">
<p>
用户名: <input name="userName" value="administrator" />
</p>
<p>
密码: <input name="password" value="123" />
</p>

<p>
<button>登录</button>
</p>
</form>

从前台传入用户名密码后进行用户名密码校验(示例代码省略了密码校验)。如果合法,则把用户的基本信息存到一个claim list里,并且指定cookie-base的认证存储方案。最后调用SignInAsync把认证信息写到cookie中。根据cookie的特性,接来下所有的http请求都会携带cookie,所以系统可以对接来下用户发起的所有请求进行认证校验。Claim有很多翻译,个人觉得叫“声明”比较好。一单认证成功,用户的认证信息里就会携带一串Claim,其实就是用户的一些信息,你可以存任何你觉得跟用户相关的东西,比如用户名,角色等,当然是常用的信息,不常用的信息建议在需要的时候查库。调用HttpContext.SignOutAsync()方法清除用户登认证信息。
Claims信息我们可以方便的获取到:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
@{
ViewData["Title"] = "Home Page";
}

<h2>
CoreAuthorization
</h2>

<p>
@Context.User.FindFirst(System.Security.Claims.ClaimTypes.Name)?.Value
</p>
<p>
角色:
@foreach (var claims in Context.User.Claims.Where(c => c.Type == System.Security.Claims.ClaimTypes.Role))
{
<span> @claims.Value </span>
}
</p>
<p>
<a href="/Student/index">/Student/index</a>
</p>
<p>
<a href="/Teacher/index">/Teacher/Index</a>
</p>
<p>
<a href="/Teacher/Edit">/Student/Edit</a>
</p>

<p>
<a href="/Account/Logoff">退出</a>
</p>

改一下home/Index页面的html,把这些claim信息展示出来。

以上就是一个基于用户名密码以及cookie的认证方案。

Authorization(授权)

有了认证我们还需要授权。刚才我们实现了用户名密码登录认证,但是系统还是没有任何管控,用户可以随意查库任意页面。现实中的系统往往都是某些页面可以随意查看,有些页面则需要认证授权后才可以访问。

AuthorizeAttribute

当我们希望一个页面只有认证后才可以访问,我们可以在相应的Controller或者Action上打上AuthorizeAttribute这个属性。修改HomeController:

1
2
3
4
5
6
7
8
9
[Authorize]
public class HomeController : Controller
{
public IActionResult Index()
{
return View();
}

}

重新启动网站,如果没有登录,访问home/index的时候网站会跳转到/account/AccessDenied。如果登录后则可以正常访问。AuthorizeAttribute默认授权校验其实是把认证跟授权合为一体了,只要认证过,就认为有授权,这是也是最最简单的授权模式。

基于角色的授权策略

显然上面默认的授权并不能满足我们开发系统的需要。AuthorizeAttribute还内置了基于Role(角色)的授权策略。
登录的时候给认证信息加上角色的声明:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
[HttpPost]
public async Task<IActionResult> Login(
[FromForm]string userName,
[FromForm]string password
)
{


var claims = new List<Claim>
{
new Claim(ClaimTypes.Name, userName),
new Claim(ClaimTypes.Role, "老师"),
};

var claimsIdentity = new ClaimsIdentity(
claims, CookieAuthenticationDefaults.AuthenticationScheme);

await HttpContext.SignInAsync(
CookieAuthenticationDefaults.AuthenticationScheme,
new ClaimsPrincipal(claimsIdentity));

return Redirect("/");
}

新建一个TeacherController:

1
2
3
4
5
6
7
8
[Authorize(Roles = "老师")]
public class TeacherController : Controller
{
public IActionResult Index()
{
return Content("Teacher index");
}
}

给AuthorizeAttribute的属性设置Roles=老师,表示只有老师角色的用户才可以访问。如果某个功能可以给多个角色访问那么可以给Roles设置多个角色,使用逗号进行分割。

1
2
3
4
5
6
7
8
9
[Authorize(Roles = "老师,校长")]
public class TeacherController : Controller
{
public IActionResult Index()
{
return Content("Teacher index");
}

}

这样认证的用户只要具有老师或者校长其中一个角色就可以访问。

基于策略的授权

上面介绍了内置的基于角色的授权策略。如果现实中需要更复杂的授权方案,我们还可以自定义策略来支持。比如我们下面定义一个策略:编辑功能只能姓王的老师可以访问。
定义一个要求:

1
2
3
4
public class LastNamRequirement : IAuthorizationRequirement
{
public string LastName { get; set; }
}

IAuthorizationRequirement其实是一个空接口,仅仅用来标记,继承这个接口就是一个要求。这是空接口,所以要求的定义比较宽松,想怎么定义都可以,一般都是根据具体的需求设置一些属性。比如上面的需求,本质上是根据老师的姓来决定是否授权通过,所以把姓作为一个属性暴露出去,以便可以配置不同的姓。
除了要求,我们还需要实现一个AuthorizationHandler:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
public class LastNameHandler : AuthorizationHandler<IAuthorizationRequirement>
{
protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, IAuthorizationRequirement requirement)
{
var lastNameRequirement = requirement as LastNamRequirement;
if (lastNameRequirement == null)
{
return Task.CompletedTask;
}

var isTeacher = context.User.HasClaim((c) =>
{
return c.Type == System.Security.Claims.ClaimTypes.Role && c.Value == "老师";
});
var isWang = context.User.HasClaim((c) =>
{
return c.Type == "LastName" && c.Value == lastNameRequirement.LastName;
});

if (isTeacher && isWang)
{
context.Succeed(requirement);
}

return Task.CompletedTask;
}
}

AuthorizationHandler是一个抽象类,继承它后需要重写其中的HandleRequirementAsync方法。这里才是真正判断是否授权成功的地方。要求(Requirement)跟用户的声明(Claim)信息会被传到这方法里,然后我们根据这些信息进行判断,如果符合授权就调用context.Succeed方法。这里注意如果不符合请谨慎调用context.Failed方法,因为策略之间一般是OR的关系,这个策略不通过,可能有其他策略通过
在ConfigureServices方法中添加策略跟注册AuthorizationHandler到DI容器中:

1
2
3
4
5
6
7
services.AddSingleton<IAuthorizationHandler, LastNameHandler>();
services.AddAuthorization(options =>
{
options.AddPolicy("王老师", policy =>
policy.AddRequirements(new LastNamRequirement { LastName = "王" })
);
});

使用AddSingleton生命周期来注册LastNameHandler,这个生命周期并不一定要单例,看情况而定。在AddAuthorization中添加一个策略叫”王老师”。这里有个个人认为比较怪的地方,为什么AuthorizationHandler不是在AddAuthorization方法中配置?而是仅仅注册到容器中就可以开始工作了。如果有一个需求,仅仅是需要自己调用一下自定义的AuthorizationHandler,而并不想它真正参与授权。这样的话就不能使用DI的方式来获取实例了,因为一注册进去就会参与授权的校验了。
在TeacherController下添加一个 Edit Action:

1
2
3
4
5
6
  [Authorize(Policy="王老师")]
public IActionResult Edit()
{
return Content("Edit success");
}

给AuthorizeAttribute的Policy设置为“王老师”。
修改Login方法添加一个姓的声明:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
[HttpPost]
public async Task<IActionResult> Login(
[FromForm]string userName,
[FromForm]string password
)
{


var claims = new List<Claim>
{
new Claim(ClaimTypes.Name, userName),
new Claim(ClaimTypes.Role, "老师"),
new Claim("LastName", "王"),
};

var claimsIdentity = new ClaimsIdentity(
claims, CookieAuthenticationDefaults.AuthenticationScheme);

await HttpContext.SignInAsync(
CookieAuthenticationDefaults.AuthenticationScheme,
new ClaimsPrincipal(claimsIdentity));

return Redirect("/");
}

运行一下程序,访问一下/teacher/edit,可以看到访问成功了。如果修改Login方法,修改LastName的声明为其他值,则访问会拒绝。

使用泛型Func方法配置策略

如果你的策略比较简单,其实还有个更简单的方法来配置,就是在AddAuthorization方法内直接使用一个Func来配置策略。
使用Func来配置一个女老师的策略:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
 options.AddPolicy("女老师", policy =>
policy.RequireAssertion((context) =>
{
var isTeacher = context.User.HasClaim((c) =>
{
return c.Type == System.Security.Claims.ClaimTypes.Role && c.Value == "老师";
});
var isFemale = context.User.HasClaim((c) =>
{
return c.Type == "Sex" && c.Value == "女";
});

return isTeacher && isFemale;
}
)
);

总结

  1. Authentication跟Authorization是两个不同的概念。Authentication是指认证,认证用户的身份;Authorization是授权,判断是否有某个功能的权限。
  2. Authorization内置了基于角色的授权策略。
  3. 可以使用自定义AuthorizationHandler跟Func的方式来实现自定义策略。

吐槽

关于认证跟授权微软为我们考虑了很多很多,包括identityserver,基本上能想到的都有了,什么oauth,openid,jwt等等。其实本人是不太喜欢用的。虽然微软都给你写好了,考虑很周到,但是学习跟Trouble shooting都是要成本的。其实使用中间件、过滤器再配合redis等组件,很容易自己实现一套授权认证方案,自由度也更高,有问题修起来也更快。自己实现一下也可以更深入的了解某项的技术,比如jwt是如果工作的,oauth是如何工作的,这样其实更有意义。

关注我的公众号一起玩转技术

前言

上篇文章中介绍了如何在 Docker 容器中部署我们的 asp.net core 应用程序,本篇主要是怎么样为我们在 Linux 或者 macOs 中部署的 dotnet 程序创建一个守护进程,来保证我们的程序在异常或者是电脑重启的时候仍然能够正常访问。

如果你以后用准备使用 asp.net core来开发项目的话,程序并且部署到 Linux 上的话,那么此篇文章你值得收藏。
如果你觉得对你有帮助的话,不妨点个【推荐】。

目录

  • 什么是守护进程
  • Supervisor 介绍
  • Supervisor 安装
  • Supervisor 配置,常用命令
  • Supervisor UI管理台

什么是守护进程

在linux或者unix操作系统中,守护进程(Daemon)是一种运行在后台的特殊进程,它独立于控制终端并且周期性的执行某种任务或等待处理某些发生的事件。由于在linux中,每个系统与用户进行交流的界面称为终端,每一个从此终端开始运行的进程都会依附于这个终端,这个终端被称为这些进程的控制终端,当控制终端被关闭的时候,相应的进程都会自动关闭。但是守护进程却能突破这种限制,它脱离于终端并且在后台运行,并且它脱离终端的目的是为了避免进程在运行的过程中的信息在任何终端中显示并且进程也不会被任何终端所产生的终端信息所打断。它从被执行的时候开始运转,直到整个系统关闭才退出。

此处的创建守护进程,是指发布在Linux上 asp.net core 程序的dotnet xxx.dll命令的宿主进程创建一个守护进程。

在 Linux 上有很多可以管理进程的工具,我们使用 Supervisor 来做这个事情。
原因有两点:
1、它是微软官方文档推荐的,降低学习成本。
2、它并不一定是最好的,但一定是文档最全的。

Supervisor 介绍

Supervisor是采用 Python(2.4+) 开发的,它是一个允许用户管理 基于 Unix 系统进程的 Client/Server 系统,提供了大量功能来实现对进程的管理。

官方文档:http://supervisord.org/

Supervisor 安装

在 masOS 中直接使用brew工具进行安装即可:
brew install supervisor

在 linux 中使用以下命令进行安装:

ubuntu
sudo apt-get install supervisor

centos
yum install supervisor

python
pip install supervosor
easy_install supervisor

安装完成之后:

image

1
2
3
mac:~ yangxiaodong$ brew install supervisor
Warning: supervisor-3.2.1 already installed

Supervisor 配置,常用命令

安装完成之后,在 /ect/supervisor/conf.d/ 目录下新建一个配置文件(touch HelloWebApp.conf),取名为 HelloWebApp.conf

打开HelloWebApp.conf (vim HelloWebApp.conf),写入如下命令:

1
2
3
4
5
6
7
8
9
10
11
[program:HelloWebApp]
command=dotnet HelloWebApp.dll
directory=/home/yxd/Workspace/publish
environment=ASPNETCORE__ENVIRONMENT=Production
user=www-data
stopsignal=INT
autostart=true
autorestart=true
startsecs=1
stderr_logfile=/var/log/HelloWebApp.err.log
stdout_logfile=/var/log/HelloWebApp.out.log

配置好以后 (:wq保存退出),需要重新加载一下配置

sudo supervisorctl shutdown && sudo supervisord -c /etc/supervisor/supervisord.conf

或者你可以直接重启 Supervisor:

1
2
sudo service supervisor stop
sudo service supervisor start

如果启动的时候报错,可以打开位于/etc/log/supervisor/supervisord.log文件来查看具体的日志。

其中dotnet 命令输出的日志文件分别为位于

1
2
/var/log/HelloWebApp.err.log
/var/log/HelloWebApp.out.log

在这些文件里面你可以查看程序中的异常信息或者是运行信息。

打开浏览器,输入 http://localhost:5000 发现已经可以浏览了。

Supervisor 常用命令

1
2
3
4
5
supervisorctl shutdown 

supervisorctl stop|start program_name

supervisorctl status

Supervisor UI 管理台

Supervisor 默认给我们提供了一个图形界面来供我们管理进程和任务,在 macOS 中默认配置的有,但是在 Linux 中我们需要手动开启一下。

打开位于/etc/supervisor/supervisord.conf文件,添加inet_http_server 节点

image

然后就可以通过界面来查看运行的进程了:

image

测试一下

最后,我们测试一下是否会自动重启,开机自动运行?

1、进程管理中干掉dot net ,发现可以重新启动。以下是日志:

1
2
3
4
5
2016-07-09 12:24:18,626 INFO spawned: 'HelloWebApp' with pid 1774
2016-07-09 12:24:19,766 INFO success: HelloWebApp entered RUNNING state, process has stayed up for > than 1 seconds (startsecs)
2016-07-09 12:27:43,208 INFO exited: HelloWebApp (exit status 0; expected)
2016-07-09 12:27:44,223 INFO spawned: 'HelloWebApp' with pid 3687
2016-07-09 12:27:45,243 INFO success: HelloWebApp entered RUNNING state, process has stayed up for > than 1 seconds (startsecs)

2、重启机器,发现可以自动运行。


本文地址:http://www.cnblogs.com/savorboard/p/dotnetcore-supervisor.html
作者博客:Savorboard
欢迎转载,请在明显位置给出出处及链接

一、概述

ASP.NET Core MVC 提供了基于角色( Role )、声明( Chaim ) 和策略 ( Policy ) 等的授权方式。在实际应用中,可能采用部门( Department , 本文采用用户组 Group )、职位 ( 可继续沿用 Role )、权限( Permission )的方式进行授权。要达到这个目的,仅仅通过自定义 IAuthorizationPolicyProvider 是不行的。本文通过自定义 IApplicationModelProvide 进行扩展。

AuthorizeAttribute 类实现了 IAuthorizeData 接口:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

namespace Microsoft.AspNetCore.Authorization
{
///


/// Defines the set of data required to apply authorization rules to a resource.
///

public interface IAuthorizeData
{
///
/// Gets or sets the policy name that determines access to the resource.
///

string Policy { get; set; }
///
/// Gets or sets a comma delimited list of roles that are allowed to access the resource.
///

string Roles { get; set; }
///
/// Gets or sets a comma delimited list of schemes from which user information is constructed.
///

string AuthenticationSchemes { get; set; }
}
}

使用 AuthorizeAttribute 不外乎如下几种形式:

1
2
3
4

[Authorize]
[Authorize(“SomePolicy”)]
[Authorize(Roles = “角色1,角色2”)]
[Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)]

当然,参数还可以组合起来。另外,Roles 和 AuthenticationSchemes 的值以半角逗号分隔,是 Or 的关系;多个 Authorize 是 And 的关系;Policy 、Roles 和 AuthenticationSchemes 如果同时使用,也是 And 的关系。

如果要扩展 AuthorizeAttribute,先扩展 IAuthorizeData 增加新的属性:

1
2
3
4
5

public interface IPermissionAuthorizeData : IAuthorizeData
{
string Groups { get; set; }
string Permissions { get; set; }
}

然后定义 AuthorizeAttribute:

1
2
3
4
5
6
7
8
9

[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = true, Inherited = true)]
public class PermissionAuthorizeAttribute : Attribute, IPermissionAuthorizeData
{
public string Policy { get; set; }
public string Roles { get; set; }
public string AuthenticationSchemes { get; set; }
public string Groups { get; set; }
public string Permissions { get; set; }
}

现在,在 Controller 或 Action 上就可以这样使用了:

1
2
3

[PermissionAuthorize(Roles = “经理,副经理”)] // 经理或部门经理
[PermissionAuthorize(Groups = “研发部,生产部”, Roles = “经理”] // 研发部经理或生成部经理。Groups 和 Roles 是 `And` 的关系。
[PermissionAuthorize(Groups = “研发部,生产部”, Roles = “经理”, Permissions = “请假审批”] // 研发部经理或生成部经理,并且有请假审批的权限。Groups 、Roles 和 Permission 是 `And` 的关系。

数据已经准备好,下一步就是怎么提取出来。通过扩展 AuthorizationApplicationModelProvider 来实现。

三、PermissionAuthorizationApplicationModelProvider : IApplicationModelProvider

AuthorizationApplicationModelProvider 类的作用是构造 AuthorizeFilter 对象放入 ControllerModel 或 ActionModel 的 Filters 属性中。具体过程是先提取 Controller 和 Action 实现了 IAuthorizeData 接口的 Attribute,如果使用的是默认的DefaultAuthorizationPolicyProvider,则会先创建一个 AuthorizationPolicy 对象作为 AuthorizeFilter 构造函数的参数。
创建 AuthorizationPolicy 对象是由 AuthorizationPolicy 的静态方法 public static async Task<AuthorizationPolicy> CombineAsync(IAuthorizationPolicyProvider policyProvider, IEnumerable<IAuthorizeData> authorizeData) 来完成的。该静态方法会解析 IAuthorizeData 的数据,但不懂解析 IPermissionAuthorizeData

因为 AuthorizationApplicationModelProvider 类对 AuthorizationPolicy.CombineAsync 静态方法有依赖,这里不得不做一个类似的 PermissionAuthorizationApplicationModelProvider 类,在本类实现 CombineAsync 方法。暂且不论该方法放在本类是否合适的问题。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83

public static AuthorizeFilter GetFilter(IAuthorizationPolicyProvider policyProvider, IEnumerable authData)
{
// The default policy provider will make the same policy for given input, so make it only once.
// This will always execute synchronously.
if (policyProvider.GetType() == typeof(DefaultAuthorizationPolicyProvider))
{
var policy = CombineAsync(policyProvider, authData).GetAwaiter().GetResult();
return new AuthorizeFilter(policy);
}
else
{
return new AuthorizeFilter(policyProvider, authData);
}
}
private static async Task CombineAsync(IAuthorizationPolicyProvider policyProvider, IEnumerable authorizeData)
{
if (policyProvider == null)
{
throw new ArgumentNullException(nameof(policyProvider));
}
if (authorizeData == null)
{
throw new ArgumentNullException(nameof(authorizeData));
}
var policyBuilder = new AuthorizationPolicyBuilder();
var any = false;
foreach (var authorizeDatum in authorizeData)
{
any = true;
var useDefaultPolicy = true;
if (!string.IsNullOrWhiteSpace(authorizeDatum.Policy))
{
var policy = await policyProvider.GetPolicyAsync(authorizeDatum.Policy);
if (policy == null)
{
//throw new InvalidOperationException(Resources.FormatException_AuthorizationPolicyNotFound(authorizeDatum.Policy));
throw new InvalidOperationException(nameof(authorizeDatum.Policy));
}
policyBuilder.Combine(policy);
useDefaultPolicy = false;
}
var rolesSplit = authorizeDatum.Roles?.Split(‘,’);
if (rolesSplit != null && rolesSplit.Any())
{
var trimmedRolesSplit = rolesSplit.Where(r => !string.IsNullOrWhiteSpace(r)).Select(r => r.Trim());
policyBuilder.RequireRole(trimmedRolesSplit);
useDefaultPolicy = false;
}
if(authorizeDatum is IPermissionAuthorizeData permissionAuthorizeDatum )
{
var groupsSplit = permissionAuthorizeDatum.Groups?.Split(‘,’);
if (groupsSplit != null && groupsSplit.Any())
{
var trimmedGroupsSplit = groupsSplit.Where(r => !string.IsNullOrWhiteSpace(r)).Select(r => r.Trim());
policyBuilder.RequireClaim(“Group”, trimmedGroupsSplit); // TODO: 注意硬编码
useDefaultPolicy = false;
}
var permissionsSplit = permissionAuthorizeDatum.Permissions?.Split(‘,’);
if (permissionsSplit != null && permissionsSplit.Any())
{
var trimmedPermissionsSplit = permissionsSplit.Where(r => !string.IsNullOrWhiteSpace(r)).Select(r => r.Trim());
policyBuilder.RequireClaim(“Permission”, trimmedPermissionsSplit);// TODO: 注意硬编码
useDefaultPolicy = false;
}
}
var authTypesSplit = authorizeDatum.AuthenticationSchemes?.Split(‘,’);
if (authTypesSplit != null && authTypesSplit.Any())
{
foreach (var authType in authTypesSplit)
{
if (!string.IsNullOrWhiteSpace(authType))
{
policyBuilder.AuthenticationSchemes.Add(authType.Trim());
}
}
}
if (useDefaultPolicy)
{
policyBuilder.Combine(await policyProvider.GetDefaultPolicyAsync());
}
}
return any ? policyBuilder.Build() : null;
}

if(authorizeDatum is IPermissionAuthorizeData permissionAuthorizeDatum ) 为扩展部分。

四、Startup

注册 PermissionAuthorizationApplicationModelProvider 服务,需要在 AddMvc 之后替换掉 AuthorizationApplicationModelProvider 服务。

1
2

services.AddMvc();
services.Replac(ServiceDescriptor.Transient<IApplicationModelProvider,PermissionAuthorizationApplicationModelProvider>());

五、Jwt 示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38

[Route(“api/[controller]“)]
[ApiController]
public class ValuesController : ControllerBase
{
private readonly JwtSecurityTokenHandler _tokenHandler = new JwtSecurityTokenHandler();
[HttpGet]
[Route(“SignIn”)]
public async Task<ActionResult> SignIn()
{
var user = new ClaimsPrincipal(new ClaimsIdentity(new[]
{
// 备注:Claim Type: Group 和 Permission 这里使用的是硬编码,应该定义为类似于 ClaimTypes.Role 的常量;另外,下列模拟数据不一定合逻辑。
new Claim(ClaimTypes.Name, “Bob”),
new Claim(ClaimTypes.Role, “经理”), // 注意:不能使用逗号分隔来达到多个角色的目的,下同。
new Claim(ClaimTypes.Role, “副经理”),
new Claim(“Group”, “研发部”),
new Claim(“Group”, “生产部”),
new Claim(“Permission”, “请假审批”),
new Claim(“Permission”, “权限1”),
new Claim(“Permission”, “权限2”),
}, JwtBearerDefaults.AuthenticationScheme));
var token = new JwtSecurityToken(
“SignalRAuthenticationSample”,
“SignalRAuthenticationSample”,
user.Claims,
expires: DateTime.UtcNow.AddDays(30),
signingCredentials: SignatureHelper.GenerateSigningCredentials(“1234567890123456”));
return _tokenHandler.WriteToken(token);
}
[HttpGet]
[Route(“Test”)]
[PermissionAuthorize(Groups = “研发部,生产部”, Roles = “经理”, Permissions = “请假审批”] // 研发部经理或生成部经理,并且有请假审批的权限。Groups 、Roles 和 Permission 是 `And` 的关系。
public async Task<ActionResult<IEnumerable>> Test()
{
var user = HttpContext.User;
return new string[] { “value1”, “value2” };
}
}

六、问题

AuthorizeFilter 类显示实现了 IFilterFactory 接口的 CreateInstance 方法:

1
2
3
4
5
6
7
8
9
10
11
12

IFilterMetadata IFilterFactory.CreateInstance(IServiceProvider serviceProvider)
{
if (Policy != null || PolicyProvider != null)
{
// The filter is fully constructed. Use the current instance to authorize.
return this;
}
Debug.Assert(AuthorizeData != null);
var policyProvider = serviceProvider.GetRequiredService();
return AuthorizationApplicationModelProvider.GetFilter(policyProvider, AuthorizeData);
}

竟然对 AuthorizationApplicationModelProvider.GetFilter 静态方法产生了依赖。庆幸的是,如果通过 AuthorizeFilter(IAuthorizationPolicyProvider policyProvider, IEnumerable<IAuthorizeData> authorizeData) 或 AuthorizeFilter(AuthorizationPolicy policy) 创建 AuthorizeFilter 对象不会产生什么不良影响。

七、下一步

[PermissionAuthorize(Groups = "研发部,生产部", Roles = "经理", Permissions = "请假审批"] 这种形式还是不够灵活,哪怕用多个 Attribute, And 和 Or 的逻辑组合不一定能满足需求。可以在 IPermissionAuthorizeData 新增一个 Rule 属性,实现类似的效果:

1

[PermissionAuthorize(Rule = “(Groups:研发部,生产部)&&(Roles:请假审批||Permissions:超级权限)”]

通过 Rule 计算复杂的授权。

八、如果通过自定义 IAuthorizationPolicyProvider 实现?

另一种方式是自定义 IAuthorizationPolicyProvider ,不过还需要自定义 AuthorizeFilter。因为当不是使用 DefaultAuthorizationPolicyProvider 而是自定义 IAuthorizationPolicyProvider 时,AuthorizationApplicationModelProvider(或前文定义的 PermissionAuthorizationApplicationModelProvider)会使用 AuthorizeFilter(IAuthorizationPolicyProvider policyProvider, IEnumerable<IAuthorizeData> authorizeData) 创建 AuthorizeFilter 对象,而不是 AuthorizeFilter(AuthorizationPolicy policy)。这会造成 AuthorizeFilter 对象在 OnAuthorizationAsync 时会间接调用 AuthorizationPolicy.CombineAsync 静态方法。

这可以说是一个设计上的缺陷,不应该让 AuthorizationPolicy.CombineAsync 静态方法存在,哪怕提供个 IAuthorizationPolicyCombiner 也好。另外,上文提到的 AuthorizationApplicationModelProvider.GetFilter 静态方法同样不是一种好的设计。等微软想通吧。

参考资料

https://docs.microsoft.com/zh-cn/aspnet/core/security/authorization/iauthorizationpolicyprovider?view=aspnetcore-2.1

排版问题:http://blog.tubumu.com/2018/11/28/aspnetcore-mvc-extend-authorization/

很长一段时间以来,我都在思考如何在ASP.NET Core的框架下,实现一套完整的事件驱动型架构。这个问题看上去有点大,其实主要目标是为了实现一个基于ASP.NET Core的微服务,它能够非常简单地订阅来自于某个渠道的事件消息,并对接收到的消息进行处理,于此同时,它还能够向该渠道发送事件消息,以便订阅该事件消息的消费者能够对消息数据做进一步处理。让我们回顾一下微服务之间通信的几种方式,分为同步和异步两种。同步通信最常见的就是RESTful API,而且非常简单轻量,一个Request/Response回环就结束了;异步通信最常见的就是通过消息渠道,将载有特殊意义的数据的事件消息发送到消息渠道,而对某种类型消息感兴趣的消费者,就可以获取消息中所带信息并执行相应操作,这也是我们比较熟知的事件驱动架构的一种表现形式。虽然事件驱动型架构看起来非常复杂,从微服务的实现来看显得有些繁重,但它的应用范围确实很广,也为服务间通信提供了新的思路。了解DDD的朋友相信一定知道CQRS体系结构模式,它就是一种事件驱动型架构。事实上,实现一套完整的、安全的、稳定的、正确的事件驱动架构并不简单,由于异步特性带来的一致性问题会非常棘手,甚至需要借助一些基础结构层工具(比如关系型数据库,不错!只能是关系型数据库)来解决一些特殊问题。本文就打算带领大家一起探探路,基于ASP.NET Core Web API实现一个相对比较简单的事件驱动架构,然后引出一些有待深入思考的问题,留在今后的文章中继续讨论。或许,本文所引入的源代码无法直接用于生产环境,但我希望本文介绍的内容能够给到读者一些启发,并能够帮助解决实际中遇到的问题。

本文会涉及一些相关的专业术语,在此先作约定:

  • 事件:在某一特定时刻发生在某件事物上的一件事情,例如:在我撰写本文的时候,电话铃响了
  • 消息:承载事件数据的实体。事件的序列化/反序列化和传输都以消息的形式进行
  • 消息通信渠道:一种带有消息路由功能的数据传输机制,用以在消息的派发器和订阅器之间进行数据传输

注意:为了迎合描述的需要,在下文中可能会混用事件和消息两个概念。

先从简单的设计开始,基本上事件驱动型架构会有事件消息(Events)、事件订阅器(Event Subscriber)、事件派发器(Event Publisher)、事件处理器(Event Handler)以及事件总线(Event Bus)等主要组件,它们之间的关系大致如下:

class_diagram

首先,IEvent接口定义了事件消息(更确切地说,数据)的基本结构,几乎所有的事件都会有一个唯一标识符(Id)和一个事件发生的时间(Timestamp),这个时间通常使用UTC时间作为标准。IEventHandler定义了事件处理器接口,显而易见,它包含两个方法:CanHandle方法,用以确定传入的事件对象是否可被当前处理器所处理,以及Handle方法,它定义了事件的处理过程。IEvent和IEventHandler构成了事件处理的基本元素。

然后就是IEventSubscriber与IEventPublisher接口。前者表示实现该接口的类型为事件订阅器,它负责事件处理器的注册,并侦听来自事件通信渠道上的消息,一旦所获得的消息能够被某个处理器处理,它就会指派该处理器对接收到的消息进行处理。因此,IEventSubscriber会保持着对事件处理器的引用;而对于实现了IEventPublisher接口的事件派发器而言,它的主要任务就是将事件消息发送到消息通信渠道,以便订阅端能够获得消息并进行处理。

IEventBus接口表示消息通信渠道,也就是大家所熟知的消息总线的概念。它不仅具有消息订阅的功能,而且还具有消息派发的能力,因此,它会同时继承于IEventSubscriber和IEventPublisher接口。在上面的设计中,通过接口分离消息总线的订阅器和派发器的角色是很有必要的,因为两种角色的各自职责不一样,这样的设计同时满足SOLID中的SRP和ISP两个准则。

基于以上基础模型,我们可以很快地将这个对象关系模型转换为C#代码:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

public interface IEvent

{

Guid Id { get``; }

DateTime Timestamp { get``; }

}

public interface IEventHandler

{

Task<``bool``> HandleAsync(IEvent @``event``, CancellationToken cancellationToken = default``);

bool CanHandle(IEvent @``event``);

}

public interface IEventHandler<``in T> : IEventHandler

where T : IEvent

{

Task<``bool``> HandleAsync(T @``event``, CancellationToken cancellationToken = default``);

}

public interface IEventPublisher : IDisposable

{

Task PublishAsync<TEvent>(TEvent @``event``, CancellationToken cancellationToken = default``)

where TEvent : IEvent;

}

public interface IEventSubscriber : IDisposable

{

void Subscribe();

}

public interface IEventBus : IEventPublisher, IEventSubscriber { }

短短30行代码,就把我们的基本对象关系描述清楚了。对于上面的代码我们需要注意以下几点:

  1. 这段代码使用了C# 7.1的新特性(default关键字)
  2. Publish以及Handle方法被替换为支持异步调用的PublishAsync和HandleAsync方法,它们会返回Task对象,这样可以方便使用C#中async/await的编程模型
  3. 由于我们的这个模型可以作为实现消息系统的通用模型,并且会需要用到ASP.NET Core的项目中,因此,建议将这些接口的定义放在一个独立的NetStandard的Class Library中,方便今后重用和扩展

OK,接口定义好了。实现呢?下面,我们实现一个非常简单的消息总线:PassThroughEventBus。在今后的文章中,我还会介绍如何基于RabbitMQ和Azure Service Bus实现不一样的消息总线。

顾名思义,PassThroughEventBus表示当有消息被派发到消息总线时,消息总线将不做任何处理与路由,而是直接将消息推送到订阅方。在订阅方的事件监听函数中,会通过已经注册的事件处理器对接收到的消息进行处理。整个过程并不会依赖于任何外部组件,不需要引用额外的开发库,只是利用现有的.NET数据结构来模拟消息的派发和订阅过程。因此,PassThroughEventBus不具备容错和消息重发功能,不具备消息存储和路由功能,我们先实现这样一个简单的消息总线,来体验事件驱动型架构的设计过程。

我们可以使用.NET中的Queue或者ConcurrentQueue等基本数据结构来作为消息队列的实现,与这些基本的数据结构相比,消息队列本身有它自己的职责,它需要在消息被推送进队列的同时通知调用方。当然,PassThroughEventBus不需要依赖于Queue或者ConcurrentQueue,它所要做的事情就是模拟一个消息队列,当消息推送进来的时候,立刻通知订阅方进行处理。同样,为了分离职责,我们可以引入一个EventQueue的实现(如下),从而将消息推送和路由的职责(基础结构层的职责)从消息总线中分离出来。

1

2

3

4

5

6

7

8

9

10

11

12

13

internal sealed class EventQueue

{

public event System.EventHandler<EventProcessedEventArgs> EventPushed;

public EventQueue() { }

public void Push(IEvent @``event``)

{

OnMessagePushed(``new EventProcessedEventArgs(@``event``));

}

private void OnMessagePushed(EventProcessedEventArgs e) => this``.EventPushed?.Invoke(``this``, e);

}

EventQueue中最主要的方法就是Push方法,从上面的代码可以看到,当EventQueue的Push方法被调用时,它将立刻触发EventPushed事件,它是一个.NET事件,用以通知EventQueue对象的订阅者,消息已经被派发。整个EventQueue的实现非常简单,我们仅专注于事件的路由,完全没有考虑任何额外的事情。

接下来,就是利用EventQueue来实现PassThroughEventBus。毫无悬念,PassThroughEventBus需要实现IEventBus接口,它的两个基本操作分别是Publish和Subscribe。在Publish方法中,会将传入的事件消息转发到EventQueue上,而Subscribe方法则会订阅EventQueue.EventPushed事件(.NET事件),而在EventPushed事件处理过程中,会从所有已注册的事件处理器(Event Handlers)中找到能够处理所接收到的事件,并对其进行处理。整个流程还是非常清晰的。以下便是PassThroughEventBus的实现代码:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

public sealed class PassThroughEventBus : IEventBus

{

private readonly EventQueue eventQueue = new EventQueue();

private readonly IEnumerable<IEventHandler> eventHandlers;

public PassThroughEventBus(IEnumerable<IEventHandler> eventHandlers)

{

this``.eventHandlers = eventHandlers;

}

private void EventQueue_EventPushed(``object sender, EventProcessedEventArgs e)

=> (``from eh in this``.eventHandlers

where eh.CanHandle(e.Event)

select eh).ToList().ForEach(async eh => await eh.HandleAsync(e.Event));

public Task PublishAsync<TEvent>(TEvent @``event``, CancellationToken cancellationToken = default``)

where TEvent : IEvent

=> Task.Factory.StartNew(() => eventQueue.Push(@``event``));

public void Subscribe()

=> eventQueue.EventPushed += EventQueue_EventPushed;

#region IDisposable Support

private bool disposedValue = false``;

void Dispose(``bool disposing)

{

if (!disposedValue)

{

if (disposing)

{

this``.eventQueue.EventPushed -= EventQueue_EventPushed;

}

disposedValue = true``;

}

}

public void Dispose() => Dispose(``true``);

#endregion

}

实现过程非常简单,当然,从这些代码也可以更清楚地了解到,PassThroughEventBus不做任何路由处理,更不会依赖于一个基础结构设施(比如实现了AMQP的消息队列),因此,不要指望能够在生产环境中使用它。不过,目前来看,它对于我们接下来要讨论的事情还是会很有帮助的,至少在我们引入基于RabbitMQ等实现的消息总线之前。

同样地,请将PassThroughEventBus实现在另一个NetStandard的Class Library中,虽然它不需要额外的依赖,但它毕竟是众多消息总线中的一种,将它从接口定义的程序集中剥离开来,好处有两点:第一,保证了定义接口的程序集的纯净度,使得该程序集不需要依赖任何外部组件,并确保了该程序集的职责单一性,即为消息系统的实现提供基础类库;第二,将PassThroughEventBus置于独立的程序集中,有利于调用方针对IEventBus进行技术选择,比如,如果开发者选择使用基于RabbitMQ的实现,那么,只需要引用基于RabbitMQ实现IEventBus接口的程序集就可以了,而无需引用包含了PassThroughEventBus的程序集。这一点我觉得可以归纳为框架设计中“隔离依赖关系(Dependency Segregation)”的准则。

好了,基本组件都定义好了,接下来,让我们一起基于ASP.NET Core Web API来做一个RESTful服务,并接入上面的消息总线机制,实现消息的派发和订阅。

我们仍然以客户管理的RESTful API为例子,不过,我们不会过多地讨论如何去实现管理客户信息的RESTful服务,那并不是本文的重点。作为一个案例,我使用ASP.NET Core 2.0 Web API建立了这个服务,使用Visual Studio 2017 15.5做开发,并在CustomersController中使用Dapper来对客户信息CRUD。后台基于SQL Server 2017 Express Edition,使用SQL Server Management Studio能够让我方便地查看数据库操作的结果。

RESTful API的实现

假设我们的客户信息只包含客户ID和名称,下面的CustomersController代码展示了我们的RESTful服务是如何保存并读取客户信息的。当然,我已经将本文的代码通过Github开源,开源协议为MIT,虽然商业友好,但毕竟是案例代码没有经过测试,所以请谨慎使用。本文源代码的使用我会在文末介绍。

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

[Route(``"api/[controller]"``)]

public class CustomersController : Controller

{

private readonly IConfiguration configuration;

private readonly string connectionString;

public CustomersController(IConfiguration configuration)

{

this``.configuration = configuration;

this``.connectionString = configuration[``"mssql:connectionString"``];

}

[HttpGet(``"{id}"``)]

public async Task<IActionResult> Get(Guid id)

{

const string sql = "SELECT [CustomerId] AS Id, [CustomerName] AS Name FROM [dbo].[Customers] WHERE [CustomerId]=@id"``;

using (``var connection = new SqlConnection(connectionString))

{

var customer = await connection.QueryFirstOrDefaultAsync<Model.Customer>(sql, new { id });

if (customer == null``)

{

return NotFound();

}

return Ok(customer);

}

}

[HttpPost]

public async Task<IActionResult> Create([FromBody] dynamic model)

{

var name = (``string``)model.Name;

if (``string``.IsNullOrEmpty(name))

{

return BadRequest();

}

const string sql = "INSERT INTO [dbo].[Customers] ([CustomerId], [CustomerName]) VALUES (@Id, @Name)"``;

using (``var connection = new SqlConnection(connectionString))

{

var customer = new Model.Customer(name);

await connection.ExecuteAsync(sql, customer);

return Created(Url.Action(``"Get"``, new { id = customer.Id }), customer.Id);

}

}

}

代码一如既往的简单,Web API控制器通过Dapper简单地实现了客户信息的创建和返回。我们不妨测试一下,使用下面的Invoke-RestMethod PowerShell指令,发送Post请求,通过上面的Create方法创建一个用户:

image

可以看到,response中已经返回了新建客户的ID号。接下来,继续使用Invoke-RestMethod来获取新建客户的详细信息:

image

OK,API调试完全没有问题。下面,我们将这个案例再扩充一下,我们希望这个API在完成客户信息创建的同时,向外界发送一条“客户信息已创建”的事件,并设置一个事件处理器,负责将该事件的详细内容保存到数据库中。

加入事件总线和消息处理机制

首先,我们在ASP.NET Core Web API项目上,添加对以上两个程序集的引用,然后,按常规做法,在ConfigureServices方法中,将PassThroughEventBus添加到IoC容器中:

1

2

3

4

5

public void ConfigureServices(IServiceCollection services)

{

services.AddMvc();

services.AddSingleton<IEventBus, PassThroughEventBus>();

}

在此,将事件总线注册为单例(Singleton)服务,是因为它不保存状态。理论上讲,使用单例服务时,需要特别注意服务实例对象的生命周期管理,因为它的生命周期是整个应用程序级别,在程序运行的过程中,由其引用的对象资源将无法释放,因此,当程序结束运行时,需要合理地将这些资源dispose掉。好在ASP.NET Core的依赖注入框架中已经帮我们处理过了,因此,对于上面的PassThroughEventBus单例注册,我们不需要过多担心,程序执行结束并正常退出时,依赖注入框架会自动帮我们dispose掉PassThroughEventBus的单例实例。那么对于单例实例来说,我们是否只需要通过AddSingleton方法进行注册就可以了,而无需关注它是否真的被dispose了呢?答案是否定的,有兴趣的读者可以参考微软的官方文档,在下一篇文章中我会对这部分内容做些介绍。

接下来,我们需要定义一个CustomerCreatedEvent对象,表示“客户信息已经创建”这一事件信息,同时,再定义一个CustomerCreatedEventHandler事件处理器,用来处理从PassThroughEventBus接收到的事件消息。代码如下,当然也很简单:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

public class CustomerCreatedEvent : IEvent

{

public CustomerCreatedEvent(``string customerName)

{

this``.Id = Guid.NewGuid();

this``.Timestamp = DateTime.UtcNow;

this``.CustomerName = customerName;

}

public Guid Id { get``; }

public DateTime Timestamp { get``; }

public string CustomerName { get``; }

}

public class CustomerCreatedEventHandler : IEventHandler<CustomerCreatedEvent>

{

public bool CanHandle(IEvent @``event``)

=> @``event``.GetType().Equals(``typeof``(CustomerCreatedEvent));

public Task<``bool``> HandleAsync(CustomerCreatedEvent @``event``, CancellationToken cancellationToken = default``)

{

return Task.FromResult(``true``);

public Task<``bool``> HandleAsync(IEvent @``event``, CancellationToken cancellationToken = default``)

=> CanHandle(@``event``) ? HandleAsync((CustomerCreatedEvent)@``event``, cancellationToken) : Task.FromResult(``false``);

}

两者分别实现了我们最开始定义好的IEvent和IEventHandler接口。在CustomerCreatedEventHandler类的第一个HandleAsync重载方法中,我们暂且让它简单地返回一个true值,表示事件处理成功。下面要做的事情就是,在客户信息创建成功后,向事件总线发送CustomerCreatedEvent事件,以及在ASP.NET Core Web API程序启动的时候,注册CustomerCreatedEventHandler实例,并调用事件总线的Subscribe方法,使其开始侦听事件的派发行为。

于是,CustomerController需要依赖IEventBus,并且在CustomerController.Create方法中,需要通过调用IEventBus的Publish方法将事件发送出去。现对CustomerController的实现做一些调整,调整后代码如下:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

[Route(``"api/[controller]"``)]

public class CustomersController : Controller

{

private readonly IConfiguration configuration;

private readonly string connectionString;

private readonly IEventBus eventBus;

public CustomersController(IConfiguration configuration,

IEventBus eventBus)

{

this``.configuration = configuration;

this``.connectionString = configuration[``"mssql:connectionString"``];

this``.eventBus = eventBus;

}

[HttpPost]

public async Task<IActionResult> Create([FromBody] dynamic model)

{

var name = (``string``)model.Name;

if (``string``.IsNullOrEmpty(name))

{

return BadRequest();

}

const string sql = "INSERT INTO [dbo].[Customers] ([CustomerId], [CustomerName]) VALUES (@Id, @Name)"``;

using (``var connection = new SqlConnection(connectionString))

{

var customer = new Model.Customer(name);

await connection.ExecuteAsync(sql, customer);

await this``.eventBus.PublishAsync(``new CustomerCreatedEvent(name));

return Created(Url.Action(``"Get"``, new { id = customer.Id }), customer.Id);

}

}

}

然后,修改Startup.cs中的ConfigureServices方法,将CustomerCreatedEventHandler注册进来:

1

2

3

4

5

6

7

public void ConfigureServices(IServiceCollection services)

{

services.AddMvc();

services.AddTransient<IEventHandler, CustomerCreatedEventHandler>();

services.AddSingleton<IEventBus, PassThroughEventBus>();

}

并且调用Subscribe方法,开始侦听消息总线:

1

2

3

4

5

6

7

8

9

10

11

12

public void Configure(IApplicationBuilder app, IHostingEnvironment env)

{

var eventBus = app.ApplicationServices.GetRequiredService<IEventBus>();

eventBus.Subscribe();

if (env.IsDevelopment())

{

app.UseDeveloperExceptionPage();

}

app.UseMvc();

}

OK,现在让我们在CustomerCreatedEventHandler的HandleAsync方法上设置个断点,按下F5启用Visual Studio 2017调试,然后重新使用Invoke-RestMethod命令发送一个Post请求,可以看到,HandleAsync方法上的断点被命中,同时事件已被正确派发:

image

数据库中的数据也被正确更新:

image

目前还差最后一小步,就是在HandleAsync中,将CustomerCreatedEvent对象的数据序列化并保存到数据库中。当然这也不难,同样可以考虑使用Dapper,或者直接使用ADO.NET,甚至使用比较重量级的Entity Framework Core,都可以实现。那就在此将这个问题留给感兴趣的读者朋友自己搞定啦。

到这里基本上本文的内容也就告一段落了,回顾一下,本文一开始就提出了一种相对简单的消息系统和事件驱动型架构的设计模型,并实现了一个最简单的事件总线:PassThroughEventBus。随后,结合一个实际的ASP.NET Core Web API案例,了解了在RESTful API中实现事件消息派发和订阅的过程,并实现了在事件处理器中,对获得的事件消息进行处理。

然而,我们还有很多问题需要更深入地思考,比如:

  • 如果事件处理器需要依赖基础结构层组件,依赖关系如何管理?组件生命周期如何管理?
  • 如何实现基于RabbitMQ或者Azure Service Bus的事件总线?
  • 如果在数据库更新成功后,事件发送失败怎么办?
  • 如何保证事件处理的顺序?

等等。。。在接下来的文章中,我会尽力做更详细的介绍。

本系列文章的源代码在https://github.com/daxnet/edasample这个Github Repo里,通过不同的release tag来区分针对不同章节的源代码。本文的源代码请参考chapter_1这个tag,如下:

image

接下来还将会有chapter_2、chapter_3等这些tag,对应本系列文章的第二部分、第三部分等等。敬请期待。