ASP.NET Core: 二十一. 内容协商与自定义IActionResult和格式化类(二)

简介: 上一章的结尾留下了一个问题:同样是ObjectResult,在执行的时候又是如何被转换成string和JSON两种格式的呢?本章来解答这个问题,这里涉及到一个名词:“内容协商”。除了这个,本章将通过两个例子来介绍如何自定义IActionResult和格式化类。

JsonOutputFormatter没有重写CanWriteResult方法,采用的是OutputFormatter的CanWriteResult方法,代码如下:

public abstract class OutputFormatter : IOutputFormatter, IApiResponseTypeMetadataProvider
{
   //部分代码略
    protected virtual bool CanWriteType(Type type)
    {
        return true;
    }
    /// <inheritdoc />
    public virtual bool CanWriteResult(OutputFormatterCanWriteContext context)
    {
        if (SupportedMediaTypes.Count == 0)
        {
            var message = Resources.FormatFormatter_NoMediaTypes(
                GetType().FullName,
                nameof(SupportedMediaTypes));
             throw new InvalidOperationException(message);
        }
         if (!CanWriteType(context.ObjectType))
        {
            return false;
        }
        if (!context.ContentType.HasValue)
        {
            context.ContentType = new StringSegment(SupportedMediaTypes[0]);
            return true;
        }
        else
        {
            var parsedContentType = new MediaType(context.ContentType);
            for (var i = 0; i < SupportedMediaTypes.Count; i++)
            {
                var supportedMediaType = new MediaType(SupportedMediaTypes[i]);
                if (supportedMediaType.HasWildcard)
                {
                    if (context.ContentTypeIsServerDefined
                        && parsedContentType.IsSubsetOf(supportedMediaType))
                    {
                        return true;
                    }
                }
                else
                {
                    if (supportedMediaType.IsSubsetOf(parsedContentType))
                    {
                        context.ContentType = new StringSegment(SupportedMediaTypes[i]);
                        return true;
                    }
                }
            }
        }
         return false;
    }
}

通过代码可以看出它主要是利用SupportedMediaTypes和context.ContentType做一系列的判断,它们分别来自客户端和服务端:


SupportedMediaTypes:它是客户端在请求的时候给出的,标识客户端期望服务端按照什么样的格式返回请求结果。


context.ContentType:它来自ObjectResult.ContentTypes,是由服务端在Action执行后给出的。


二者的值都是类似“application/json”、“text/plain”这样的格式,当然也有可能为空,即客户端或服务端未对请求做数据格式的设定。通过上面的代码可以知道,如果这两个值均未做设置或者只有一方做了设置并且设置为JSON时,这个CanWriteResult方法的返回值都是true。所以这样的情况下除了前三种Formatter对应的特定类型外的ObjectResult都会交由JsonOutputFormatter处理。这也就是为什么同样是ObjectResult,但string类型的Action返回结果是String类型,而Book类型的Action返回的结果是JSON类型。这个JsonOutputFormatter有点像当其他的Formatter无法处理时用来“保底”的。


那么SupportedMediaTypes和context.ContentType这两个值又是在什么时候被设置的呢? 在讲请求的模型参数绑定的时候,可以通过在请求Request的Header中添加“content-type: application/json”这样的标识来说明请求中包含的数据的格式是JSON类型的。同样,在请求的时候也可以添加“accept:xxx”这样的标识,来表明期望服务端对本次请求返回的数据的格式。例如期望是JSON格式“accept:application/json”,文本格式“accept: text/plain”等。这个值就是SupportedMediaTypes。


在服务端,也可以对返回的数据格式做设置,例如下面的代码:

 [Produces("application/json")]
 public Book GetModel()
 {
     return new Book() { Code = "1001", Name = "ASP" };
 }

通过这个ProducesAttribute设置的值最终就会被赋值给ObjectResult.ContentTypes,最终传递给context.ContentType。ProducesAttribute实际是一个IResultFilter,代码如下:

public class ProducesAttribute : Attribute, IResultFilter, IOrderedFilter, IApiResponseMetadataProvider
    {
        //部分代码省略
        public virtual void OnResultExecuting(ResultExecutingContext context)
        {
            //部分代码省略
           SetContentTypes(objectResult.ContentTypes);
        }
        public void SetContentTypes(MediaTypeCollection contentTypes)
        {
            contentTypes.Clear();
            foreach (var contentType in ContentTypes)
            {
                contentTypes.Add(contentType);
            }
        }
        private MediaTypeCollection GetContentTypes(string firstArg, string[] args)
        {
            var completeArgs = new List<string>();
            completeArgs.Add(firstArg);
            completeArgs.AddRange(args);
            var contentTypes = new MediaTypeCollection();
            foreach (var arg in completeArgs)
            {
                var contentType = new MediaType(arg);
                if (contentType.HasWildcard)
                {
                    throw new InvalidOperationException(                     Resources.FormatMatchAllContentTypeIsNotAllowed(arg));
                }
                contentTypes.Add(arg);
            }
            return contentTypes;
        }
    }

在执行OnResultExecuting的时候,会将设置的“application/json”赋值给ObjectResult.ContentTypes。所以请求最终返回结果的数据格式是由二者“协商”决定的。下面回到Formatter的筛选方法FormatterSelector.SelectFormatter(),这个方法写在DefaultOutputFormatterSelector.cs中。精简后的代码如下:

public class DefaultOutputFormatterSelector : OutputFormatterSelector
{
    //部分代码略
    public override IOutputFormatter SelectFormatter(OutputFormatterCanWriteContext context, IList<IOutputFormatter> formatters, MediaTypeCollection contentTypes)
    {
        //部分代码略
        var request = context.HttpContext.Request;
        var acceptableMediaTypes = GetAcceptableMediaTypes(request);
        var selectFormatterWithoutRegardingAcceptHeader = false;
        IOutputFormatter selectedFormatter = null;
        if (acceptableMediaTypes.Count == 0)
        {
            //客户端未设置Accept标头的情况
            selectFormatterWithoutRegardingAcceptHeader = true;
        }
        else
        {
            if (contentTypes.Count == 0)
            {
                //服务端未指定数据格式的情况
                selectedFormatter = SelectFormatterUsingSortedAcceptHeaders(
                    context,
                    formatters,
                    acceptableMediaTypes);
            }
            else
            {
                //客户端和服务端均指定了数据格式的情况
                selectedFormatter = SelectFormatterUsingSortedAcceptHeadersAndContentTypes(
                    context,
                    formatters,
                    acceptableMediaTypes,
                    contentTypes);
            }
            if (selectedFormatter == null)
            {
                //如果未找到合适的,由系统参数ReturnHttpNotAcceptable决定直接返回错误
                //还是忽略客户端的Accept设置再筛选一次
                if (!_returnHttpNotAcceptable)
                {
                    selectFormatterWithoutRegardingAcceptHeader = true;
                }
            }
        }
        if (selectFormatterWithoutRegardingAcceptHeader)
        {
            //Accept标头未设置或者被忽略的情况
            if (contentTypes.Count == 0)
            {
                //服务端也未指定数据格式的情况
                selectedFormatter = SelectFormatterNotUsingContentType(
                    context,
                    formatters);
            }
            else
            {
                //服务端指定数据格式的情况
                selectedFormatter = SelectFormatterUsingAnyAcceptableContentType(
                    context,
                    formatters,
                    contentTypes);
            }
        }
        if (selectedFormatter == null)
        {
            // No formatter supports this.
            _logger.NoFormatter(context);
            return null;
        }
        _logger.FormatterSelected(selectedFormatter, context);
        return selectedFormatter;
    }
    // 4种情况对应的4个方法略
    // SelectFormatterNotUsingContentType
    // SelectFormatterUsingSortedAcceptHeaders
    // SelectFormatterUsingAnyAcceptableContentType
    // SelectFormatterUsingSortedAcceptHeadersAndContentTypes
}

DefaultOutputFormatterSelector根据客户端和服务端关于返回数据格式的设置的4种不同情况作了分别处理,优化了查找顺序,此处就不详细讲解了。

总结一下这个规则:


只有在Action返回类型为ObjectResult的时候才会进行“协商”。如果返回类型为JsonResult、ContentResult、ViewResult等特定ActionResult,无论请求是否设置了accept标识,都会被忽略,会固定返回 JSON、String,Html类型的结果。

当系统检测到请求是来自浏览器时,会忽略 其Header中Accept 的设置,所以会由服务器端设置的格式决定(未做特殊配置时,系统默认为JSON)。 这是为了在使用不同浏览器使用 API 时提供更一致的体验。系统提供了参数RespectBrowserAcceptHeader,即尊重浏览器在请求的Header中关于Accept的设置,默认值为false。将其设置为true的时候,浏览器请求中的Accept 标识才会生效。注意这只是使该Accept 标识生效,依然不能由其决定返回格式,会进入“协商”阶段。

若二者均未设置,采用默认的JSON格式。

若二者其中有一个被设置,采用该设置值。

若二者均设置且不一致,即二者值不相同且没有包含关系(有通配符的情况),会判断系统参数ReturnHttpNotAcceptable(返回不可接受,默认值为false),若ReturnHttpNotAcceptable值为false,则忽略客户端的Accept设置,按照无Accept设置的情况再次筛选一次Formatter。如果该值为true,则直接返回状态406。

涉及的两个系统参数RespectBrowserAcceptHeader和ReturnHttpNotAcceptable的设置方法是在 Startup.cs 中通过如下代码设置:

 services.AddMvc(
    options =>
    {
        options.RespectBrowserAcceptHeader = true;
        options.ReturnHttpNotAcceptable = true;
    }
 )

 最终,通过上述方法找到了合适的Formatter,接着就是通过该Formatter的WriteAsync方法将请求结果格式化后写入HttpContext.Response中。JsonOutputFormatter重写了OutputFormatter的WriteResponseBodyAsync方法(WriteAsync方法会调用WriteResponseBodyAsync方法),代码如下:

public override async Task WriteResponseBodyAsync(OutputFormatterWriteContext context, Encoding selectedEncoding)
{
    if (context == null)
    {
        throw new ArgumentNullException(nameof(context));
    }
    if (selectedEncoding == null)
    {
        throw new ArgumentNullException(nameof(selectedEncoding));
    }
    var response = context.HttpContext.Response;
    using (var writer = context.WriterFactory(response.Body, selectedEncoding))
    {
        WriteObject(writer, context.Object);
        // Perf: call FlushAsync to call WriteAsync on the stream with any content left in the TextWriter's
        // buffers. This is better than just letting dispose handle it (which would result in a synchronous
        // write).
        await writer.FlushAsync();
    }
}

这个方法的功能就是将结果数据转换为JSON并写入HttpContext.Response. Body中。至此,请求结果就按照JSON的格式返回给客户端了。


在实际项目中,如果上述的几种格式均不能满足需求,比如某种数据经常需要通过特殊的格式传输,想自定义一种格式,该如何实现呢?通过本节的介绍,可以想到两种方式,即自定义一种IActionResult或者自定义一种IOutputFormatter。


目录
相关文章
|
开发框架 .NET C#
ASP.NET Core Blazor 路由配置和导航
大家好,我是码农刚子。本文系统介绍Blazor单页应用的路由机制,涵盖基础配置、路由参数、编程式导航及高级功能。通过@page指令定义路由,支持参数约束、可选参数与通配符捕获,结合NavigationManager实现页面跳转与参数传递,并演示用户管理、产品展示等典型场景,全面掌握Blazor路由从入门到实战的完整方案。
861 6
|
开发框架 .NET 开发者
简化 ASP.NET Core 依赖注入(DI)注册-Scrutor
Scrutor 是一个简化 ASP.NET Core 应用程序中依赖注入(DI)注册过程的开源库,支持自动扫描和注册服务。通过简单的配置,开发者可以轻松地从指定程序集中筛选、注册服务,并设置其生命周期,同时支持服务装饰等高级功能。适用于大型项目,提高代码的可维护性和简洁性。仓库地址:&lt;https://github.com/khellang/Scrutor&gt;
1017 5
|
开发框架 .NET C#
在 ASP.NET Core 中创建 gRPC 客户端和服务器
本文介绍了如何使用 gRPC 框架搭建一个简单的“Hello World”示例。首先创建了一个名为 GrpcDemo 的解决方案,其中包含一个 gRPC 服务端项目 GrpcServer 和一个客户端项目 GrpcClient。服务端通过定义 `greeter.proto` 文件中的服务和消息类型,实现了一个简单的问候服务 `GreeterService`。客户端则通过 gRPC 客户端库连接到服务端并调用其 `SayHello` 方法,展示了 gRPC 在 C# 中的基本使用方法。
692 5
在 ASP.NET Core 中创建 gRPC 客户端和服务器
|
JSON 安全 API
.net 自定义日志类
在.NET中,创建自定义日志类有助于更好地管理日志信息。示例展示了如何创建、配置和使用日志记录功能,包括写入日志文件、设置日志级别、格式化消息等。注意事项涵盖时间戳、日志级别、JSON序列化、线程安全、日志格式、文件处理及示例使用。请根据需求调整代码。
434 13
|
开发框架 算法 中间件
ASP.NET Core 中的速率限制中间件
在ASP.NET Core中,速率限制中间件用于控制客户端请求速率,防止服务器过载并提高安全性。通过`AddRateLimiter`注册服务,并配置不同策略如固定窗口、滑动窗口、令牌桶和并发限制。这些策略可在全局、控制器或动作级别应用,支持自定义响应处理。使用中间件`UseRateLimiter`启用限流功能,并可通过属性禁用特定控制器或动作的限流。这有助于有效保护API免受滥用和过载。 欢迎关注我的公众号:Net分享 (239字符)
617 1
|
开发框架 缓存 .NET
GraphQL 与 ASP.NET Core 集成:从入门到精通
本文详细介绍了如何在ASP.NET Core中集成GraphQL,包括安装必要的NuGet包、创建GraphQL Schema、配置GraphQL服务等步骤。同时,文章还探讨了常见问题及其解决方法,如处理复杂查询、错误处理、性能优化和实现认证授权等,旨在帮助开发者构建灵活且高效的API。
655 3
|
Windows
.NET 隐藏/自定义windows系统光标
【10月更文挑战第20天】在.NET中,可以使用`Cursor`类来控制光标。要隐藏光标,可将光标设置为`Cursors.None`。此外,还可以通过从文件或资源加载自定义光标来更改光标的样式。例如,在表单加载时设置`this.Cursor = Cursors.None`隐藏光标,或使用`Cursor.FromFile`方法加载自定义光标文件,也可以将光标文件添加到项目资源中并通过资源管理器加载。这些方法适用于整个表单或特定控件。
469 0
|
开发框架 JavaScript 前端开发
一个适用于 ASP.NET Core 的轻量级插件框架
一个适用于 ASP.NET Core 的轻量级插件框架
617 0
|
监控 前端开发 API
一款基于 .NET MVC 框架开发、功能全面的MES系统
一款基于 .NET MVC 框架开发、功能全面的MES系统
1060 5
|
开发框架 前端开发 JavaScript
ASP.NET MVC 教程
ASP.NET 是一个使用 HTML、CSS、JavaScript 和服务器脚本创建网页和网站的开发框架。
608 7