一个BCD编码问题

背景

最近在接手某一个物联网项目,其中有一个需求是要获某个倾角传感器数据。该传感器使用RS485串口通讯,说明书写得相对简单,只描述了一个自定义的协议。其中关于读取所有角度数据的命令如下:

根据前文描述的数据协议格式,这里的角度数据分别是:

1
2
0x00 20 10     # x轴角度 20.10
0x10 05 20 # y轴角度 -5.20

显然,这里的报文使用的都是16进制表示,但是看它的备注,却仿佛是把16进制的数据直接当成文本来解读,然后解析成十进制数,最后除以100得到最终的角度数据。我很好奇,为什么不直接使用浮点数来表示,毕竟占用4个字节相对于占用3个字节显示不是什么大问题。

Read More

ASP.NET Core 依赖注入 — 如何避免手工构建 Service Provider 实例

依赖注入是个很好的理念。不过服务的注册都是在启动时(Startup-time)完成的。在Host完成启动之前的Startup::ConfigureServices()里,我们是没法拿到所注册服务的实例的。如果全盘采用依赖注入,这当然不成问题:服务和服务之间的依赖,可以在动态时刻自动解析。但是,有时候现实并不会那么完美,当配置一些选项的时候,我们可能需要一个具体的实例。有时候这会造成一定的困惑,尤其是当我们需要配合传统的手工new一个服务实例的时候(e.g.: new SomeServiceA(serviceB, serviceC,...))。在以前,网上随处可见有人推荐ServiceCollection.BuilServiceProvider()然后通过ServiceProvier.GetRequiredService<ServiceB>()获取所依赖的服务。

然而,自 ASP.NET Core 3.x 起,如果我们在应用层代码手工调用ServiceCollection.BuildServiceProvider(),很可能会得到一条警告消息:

Warning ASP0000 Calling ‘BuildServiceProvider’ from application code results in an additional copy of singleton services being created.

这是因为构建新的ServiceProvider会导致构建新的服务实例副本,而且这些新的服务实例副本完全独立于原来的服务容器。这往往会造成一些难以发现的Bug,例如这个Stack Overflow上的这个问题

那么如何避免手工创建ServiceCollection.BuildServiceProvider()呢?

Read More

ASP.NET Core MVC — IActionResult执行(2) 如何自定义一个OutputFormatter

在之前的IActionResult与执行的工作原理笔记中,我们直接新建了一个Utf8ForExcelCsvResult类来返回一个Csv文件。但是这种代码有个局限性——无法动态返回不同格式的数据。不妨考虑我们有一个Action方法,调用后返回一个列表:

1
2
3
4
5
6
7
8
public IActionResult Privacy()
{
var records = new List<Foo> {
new Foo { Id = 1, Name = "你好" },
new Foo { Id = 2, Name = "こんにちは" },
};
return new ObjectResult(records);
}

通常情况下,我们希望它能返回JSON。但是我们希望这个方法不只是简单地把结果转换成JSON,而是可以在不改代码的前提下,根据需要,有选择地生成CSV格式的数据。

这种需求可以通过新建一个CsvOutputFormatter(参见我的SO回答):

Read More

ASP.NET Core MVC — IActionResult执行(1) ObjectResultExecutor与OutputFormatter的工作原理

在命名空间Microsoft.AspNetCore.Mvc.Infrastructure下,有一个ObjectResultExecutor类,继承自IActionResultExecutor<ObjectResult>,负责执行ObjectResult。其构造函数会注入被两个参数:

  • OutputFormatterSelector: 用于选择当前输出格式
  • IHttpResponseStreamWriterFactory: 用于向 HttpResponse 写入Stream的Writer
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class ObjectResultExecutor : IActionResultExecutor<ObjectResult>
{
public ObjectResultExecutor( OutputFormatterSelector formatterSelector,IHttpResponseStreamWriterFactory writerFactory, ILoggerFactory loggerFactory)
{
if (formatterSelector == null) { /* throw ;*/ }
if (writerFactory == null) { /* throw ;*/ }
if (loggerFactory == null) { /* throw ;*/ }

FormatterSelector = formatterSelector;
WriterFactory = writerFactory.CreateWriter;
Logger = loggerFactory.CreateLogger<ObjectResultExecutor>();
}

...
}

这里的ObjectResultExecutor所依赖的服务是在AddMvcCore()时自动注册的

1
2
3
services.TryAddSingleton<IHttpResponseStreamWriterFactory, MemoryPoolHttpResponseStreamWriterFactory>();
...
services.TryAddSingleton<OutputFormatterSelector, DefaultOutputFormatterSelector>();

注意这里的MemoryPoolHttpResponseStreamWriterFactory会生成一个HttpResponseStreamWriter向 HttpResponse中写入文本数据:

1
2
3
4
5
6
7
8
9
10
11
12
internal class MemoryPoolHttpResponseStreamWriterFactory : IHttpResponseStreamWriterFactory
{
...

public TextWriter CreateWriter(Stream stream, Encoding encoding)
{
if (stream == null) { /* throw ;*/ }
if (encoding == null){ /* throw ;*/ }

return new HttpResponseStreamWriter(stream, encoding, DefaultBufferSize, _bytePool, _charPool);
}
}

总之,借助于此工厂函数生成的TextWriter,我们可以把Text型数据(不可用于Binary数据)输出到响应流。

ObjectResultExecutor的关键方法实现如下:

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 virtual Task ExecuteAsync(ActionContext context, ObjectResult result)
{
if (context == null) { /* throw ;*/ }
if (result == null) { /* throw ;*/ }

InferContentTypes(context, result);

var objectType = result.DeclaredType;
if (objectType == null || objectType == typeof(object))
{
objectType = result.Value?.GetType();
}

// 构建格式器上下文
var formatterContext = new OutputFormatterWriteContext( context.HttpContext, WriterFactory, objectType, result.Value);
// 生成对应的Formatter
var selectedFormatter = FormatterSelector.SelectFormatter( formatterContext, (IList<IOutputFormatter>)result.Formatters ?? Array.Empty<IOutputFormatter>(), result.ContentTypes);

if (selectedFormatter == null) { /* sttauscode=406 */ }

Logger.ObjectResultExecuting(result.Value);
// change result.ContentTypes
result.OnFormatting(context);

// 使用指定的格式化器写数据
return selectedFormatter.WriteAsync(formatterContext);
}

总体逻辑是:

  1. 构建格式化上下文(OutputFormatterWriteContext)
  2. 选择对应的输出格式化器(IOutputFormatter)
  3. 执行IOutputFormatter::WriteAsync(formatterContext)

这里的FormatterSelector服务类似于一个Provider,会根据指定的FormatterContext等信息生成一个IOutputFormatter。从而根据运行时的外部请求和特定条件把ObjectResult转换成特定格式的输出。

ASP.NET Core MVC — IActionResult执行(0) IActionResult与执行的工作原理

IActionResult接口表达了Action执行完毕之后的结果,该接口只规定了一个ExecuteResultAsync(ctx)方法:

1
2
3
4
public interface IActionResult
{
Task ExecuteResultAsync(ActionContext context);
}

当某个Action返回IActionResult实例之后,此实例的该方法会被MVC调用,从而把相应的IActionResult对象写到响应中。例如,我们可以自定义一个IActionResult实现,用于生成带BOM头的CSV文件流,代码可能长这样(引自我的SO回答):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class Utf8ForExcelCsvResult : IActionResult
{
public string Content{get;set;}
public string ContentType{get;set;}
public string FileName {get;set;}
public Task ExecuteResultAsync(ActionContext context)
{
var Response =context.HttpContext.Response;
Response.Headers["Content-Type"] = this.ContentType;
Response.Headers["Content-Disposition"]=$"attachment; filename={this.FileName}; filename*=UTF-8''{this.FileName}";
using(var sw = new StreamWriter(Response.Body,System.Text.Encoding.UTF8)){
sw.Write(Content);
}
return Task.CompletedTask ;
}
}

对于一个简单的情形这种写法是没有太大问题的,但是对于更复杂的IActionResult,这种方式在代码解耦方面并不好。因为IActionResult更多是在描述结果,而这里参杂了如何把结果写入响应流。比如说我们要根据请求的Accept字段来返回不同的响应,就需要加入更多逻辑。这种逻辑可以纳入IActionResult的执行。

Read More

ASP.NET Core MVC — MVC执行概貌(3) ControllerActionInvker与模型绑定、模型校验

ControllerActionInvker中有一个字段_arguments,类型为Dictionary<string, object>,用于存放参数绑定的结构。该类提供了BinArgumentsAsync()方法用于绑定参数:

1
2
3
4
5
6
7
8
9
10
private Task BindArgumentsAsync()
{
var actionDescriptor = _controllerContext.ActionDescriptor;
if (actionDescriptor.BoundProperties.Count == 0 && actionDescriptor.Parameters.Count == 0) {
return Task.CompletedTask;
}

Debug.Assert(_cacheEntry.ControllerBinderDelegate != null);
return _cacheEntry.ControllerBinderDelegate(_controllerContext, _instance, _arguments);
}

该方法执行后,会为字段_arguments逐一添加以参数名为键名的键值。这里的ControllerBinderDelegate是一个委托类型,负责绑定参数:

1
internal delegate Task ControllerBinderDelegate(ControllerContext controllerContext, object controller, Dictionary<string, object> arguments);

该委托的实例由ControllerBinderDelegateProvider的静态方法CreateBinderDelegate()提供:

Read More

ASP.NET Core MVC — MVC执行概貌(2) ControllerActionInvoker与Filters管道

从实现上说,ControllerActionInvoker其实是ResourceInvoker的子类。其InvokeAsync()方法的主要流程就是调用InvokeFilterPipelineAsync()

ResourceInvoker

ResourceInvoker中的Resource含义和Resource filters中的含义相同。但是这里的ResourceInvoker会负责调用整个Filter管道,主要包括Authorization Filters(2.x)、Exception FiltersModel BindingAction Filters、及Result Filters的全部过程:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public virtual async Task InvokeAsync()
{
try{
//.. diagnose;

using (_logger.ActionScope(_actionContext.ActionDescriptor)){
// ... log
try {
await InvokeFilterPipelineAsync();
} finally{
ReleaseResources();
// ... log
}
}
} finally {
//.. diagnose;
}
}

显然,这里的InvokeFilterPipelineAsync()是整个方法的核心,该方法会不停地尝试调用自身的一个Next方法,直至结束:

Read More

ASP.NET Core MVC — MVC执行概貌(1) IActionInvoker的构建

IActionInvoker是通过IActionInvokerFactory工厂构建的。

当构建IActionInvoker时,IActionInvokerFactory会遍历自身的IActionInvokerProvider列表,依次调用provider.OnProvidersExecuting(context);然后再按逆序遍历该列表,依次调用provider.OnProvidersExecuted(context);,从而形成如下的层级式调用:

1
2
3
4
5
6
7
8
9
10
provider0.OnProvidersExecuting(context);
provider1.OnProvidersExecuting(context);
provider2.OnProvidersExecuting(context);
...
...
...
...
provider2.OnProvidersExecuted(context);
provider1.OnProvidersExecuted(context);
provider0.OnProvidersExecuted(context);

最后,当所有这些都处理完成之后,返回ActionInvokerProviderContextResult属性。

Read More

ASP.NET Core MVC — MVC执行概貌(0) 路由与MVC的执行流程

许多中间件都会为IApplicationBuilder添加一个扩展方法以简化调用,比如在2.x中常见的UseRoute()之类,其实就是简单调用UseMiddleware<Xxx>()。在ASP.NET CoreMVC也同样提供了一个UseMvc()扩展方法,但是和会转换为UseMiddleware<XMiddleware>()的那些方法不同,并没有一个与UseMvc()对应的、名称为MvcMiddleware的中间件存在。

其实,所谓的UseMvc()其实不过是对UseRouter()的封装:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// project: 
// aspnet/Mvc
// file:
// src/Microsoft.AspNetCore.Mvc.Core/Builder/MvcApplicationBuilderExtensions.cs

public static IApplicationBuilder UseMvc( this IApplicationBuilder app, Action<IRouteBuilder> configureRoutes)
{
if (app == null){ throw new ArgumentNullException(nameof(app)); }
if (configureRoutes == null){ throw new ArgumentNullException(nameof(configureRoutes)); }

// throw an exception if there's no MvcMarkerService
VerifyMvcIsRegistered(app);

var routes = new RouteBuilder(app)
{
DefaultHandler = app.ApplicationServices.GetRequiredService<MvcRouteHandler>(),
};

configureRoutes(routes);

routes.Routes.Insert(0, AttributeRouting.CreateAttributeMegaRoute(app.ApplicationServices));

return app.UseRouter(routes.Build());
}

可以看到,执行app.UseMvc(rb=>{ /**/ }) 之类的操作,其实是在配置一个路由中间件,其默认处理器是MvcRouterHandler的实例。

Read More

Authorization — (5) 授权与认证的实现对比

概念对比

Authentication Authorization
全局选项 AuthenticationOptions AuthorizationOptions
模式/策略 AuthenticationScheme:
   - Name,
   - HandlerType
AuthorizationPolicy:
   - AuthenticationSchemes,
   - Requirements
模式/策略Provider IAuthenticationSchemeProvider
AuthenticationSchemeProvider
IAuthorizationPolicyProvider
DefaultAuthorizationPolicyProvider
具体模式/策略细节 AuthenticationSchemeOptions IRequirement
具体模式/策略Handler IAuthenticationHandler
AuthenticationHandler<SomeSchemeOptions>
IAuthorizationHandler
Authorization<SomeRequirement>
HandlerProvider IAuthenticationHandlerProvider
AuthenticationHandlerProvider
IAuthorizationHandlerProvider
DefaultAuthorizationHandlerProvider
服务 IAuthenticationService
AuthenticationService
IAuthorizationService
DefaultAuthorizationService

Read More