欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

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

程序员文章站 2022-07-01 17:01:50
上一章的结尾留下了一个问题:同样是ObjectResult,在执行的时候又是如何被转换成string和JSON两种格式的呢? 本章来解答这个问题,这里涉及到一个名词:“内容协商”。除了这个,本章将通过两个例子来介绍如何自定义IActionResult和格式化类。(ASP.NET Core 系列目录) ......

上一章的结尾留下了一个问题:同样是objectresult,在执行的时候又是如何被转换成string和json两种格式的呢?

本章来解答这个问题,这里涉及到一个名词:“内容协商”。除了这个,本章将通过两个例子来介绍如何自定义iactionresult和格式化类。(asp.net core 系列目录)

一、内容协商

依然以返回book类型的action为例,看看它是怎么被转换为json类型的。

public book getmodel()
{
    return new book() { code = "1001", name = "asp" };
}

这个action执行后被封装为objectresult,接下来就是这个objectresult的执行过程。

objectresult的代码如下:

public class objectresult : actionresult, istatuscodeactionresult
{
     //部分代码略
     public override task executeresultasync(actioncontext context)
     {
          var executor = context.httpcontext.requestservices.getrequiredservice<iactionresultexecutor<objectresult>>();
          return executor.executeasync(context, this);
     }
}

它是如何被执行的呢?首先会通过依赖注入获取objectresult对应的执行者,获取到的是objectresultexecutor,然后调用objectresultexecutor的executeasync方法。代码如下:

public class objectresultexecutor : iactionresultexecutor<objectresult>
{
     //部分代码略
     public virtual task executeasync(actioncontext context, objectresult result)
     {
          //部分代码略
          var formattercontext = new outputformatterwritecontext(
               context.httpcontext,
               writerfactory,
               objecttype,
               result.value);

            var selectedformatter = formatterselector.selectformatter(
                formattercontext,
                (ilist<ioutputformatter>)result.formatters ?? array.empty<ioutputformatter>(),
                result.contenttypes);
            if (selectedformatter == null)
            {
                // no formatter supports this.
                logger.noformatter(formattercontext);
                context.httpcontext.response.statuscode = statuscodes.status406notacceptable;
                return task.completedtask;
            }

            result.onformatting(context);
            return selectedformatter.writeasync(formattercontext);
     }
}

核心代码就是formatterselector.selectformatter()方法,它的作用是选择一个合适的formatter。formatter顾名思义就是一个用于格式化数据的类。系统默认提供了4种formatter,如下图 1

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

图 1

它们都实现了ioutputformatter接口,继承关系如下图 2:

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

图 2

ioutputformatter代码如下:

public interface ioutputformatter
{
    bool canwriteresult(outputformattercanwritecontext context);
    task writeasync(outputformatterwritecontext context);
}

又是非常熟悉的方式,就像在众多xxxresultexecutor中筛选一个合适的action的执行者一样,首先将它们按照一定的顺序排列,然后开始遍历,逐一执行它们的canxxx方法,若其中一个的执行结果为true,则它就会被选出来。例如stringoutputformatter的代码如下:

public class stringoutputformatter : textoutputformatter
{
    public stringoutputformatter()
    {
        supportedencodings.add(encoding.utf8);
        supportedencodings.add(encoding.unicode);
        supportedmediatypes.add("text/plain");
    }

    public override bool canwriteresult(outputformattercanwritecontext context)
    {
        if (context == null)
        {
            throw new argumentnullexception(nameof(context));
        }

        if (context.objecttype == typeof(string) || context.object is string)
        {
            return base.canwriteresult(context);
        }

         return false;
    }
    //省略部分代码
}

从stringoutputformatter的canwriteresult方法中可以知道它能处理的是string类型的数据。它的构造方法中标识它可以处理的字符集为utf8和unicode。对应的数据格式标记为“text/plain”。同样查看httpnocontentoutputformatter和httpnocontentoutputformatter对应的是返回值为void或者task的,streamoutputformatter对应的是stream类型的。

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种不同情况作了分别处理,优化了查找顺序,此处就不详细讲解了。

总结一下这个规则:

  1. 只有在action返回类型为objectresult的时候才会进行“协商”。如果返回类型为jsonresult、contentresult、viewresult等特定actionresult,无论请求是否设置了accept标识,都会被忽略,会固定返回 json、string,html类型的结果。
  2. 当系统检测到请求是来自浏览器时,会忽略 其header中accept 的设置,所以会由服务器端设置的格式决定(未做特殊配置时,系统默认为json)。 这是为了在使用不同浏览器使用 api 时提供更一致的体验。系统提供了参数respectbrowseracceptheader,即尊重浏览器在请求的header中关于accept的设置,默认值为false。将其设置为true的时候,浏览器请求中的accept 标识才会生效。注意这只是使该accept 标识生效,依然不能由其决定返回格式,会进入“协商”阶段。
  3. 若二者均未设置,采用默认的json格式。
  4. 若二者其中有一个被设置,采用该设置值。
  5. 若二者均设置且不一致,即二者值不相同且没有包含关系(有通配符的情况),会判断系统参数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。

二、自定义iactionresult

举个简单的例子,以第一节的第3个例子为例,该例通过 “return new jsonresult(new book() { code = "1001", name = "asp" })”返回了一个jsonresult。

返回的json值为:

{"code":"1001","name":"asp"}

假如对于book这种类型,希望用特殊的格式返回,例如这样的格式:

book code:[1001]|book name:<asp>

可以通过自定义一个类似jsonresult的类来实现。代码如下:

public class bookresult : actionresult
{
        public bookresult(book content)
        {
            content = content;
        }
        public book content { get; set; }
        public string contenttype { get; set; }
        public int? statuscode { get; set; }

        public override async task executeresultasync(actioncontext context)
        {
            if (context == null)
            {
                throw new argumentnullexception(nameof(context));
            }

             var executor = context.httpcontext.requestservices.getrequiredservice<iactionresultexecutor<bookresult>>();
            await executor.executeasync(context, this);
        }
    }

定义了一个名为bookresult的类,为了方便继承了actionresult。由于是为了处理book类型,在构造函数中添加了book类型的参数,并将该参数赋值给属性content。重写executeresultasync方法,对应jsonresultexecutor,还需要自定义一个bookresultexecutor。代码如下:

public class bookresultexecutor : iactionresultexecutor<bookresult>
{
    private const string defaultcontenttype = "text/plain; charset=utf-8";
    private readonly ihttpresponsestreamwriterfactory _httpresponsestreamwriterfactory;

    public bookresultexecutor(ihttpresponsestreamwriterfactory httpresponsestreamwriterfactory)
    {
        _httpresponsestreamwriterfactory = httpresponsestreamwriterfactory;
    }

    private static string formattostring(book book)
    {
        return string.format("book code:[{0}]|book name:<{1}>", book.code, book.name);
    }

     /// <inheritdoc />
    public virtual async task executeasync(actioncontext context, bookresult result)
    {
        if (context == null)
        {
            throw new argumentnullexception(nameof(context));
        }

        if (result == null)
        {
            throw new argumentnullexception(nameof(result));
        }

        var response = context.httpcontext.response;
        string resolvedcontenttype;
        encoding resolvedcontenttypeencoding;
     responsecontenttypehelper.resolvecontenttypeandencoding(
            result.contenttype,
            response.contenttype,
            defaultcontenttype,
            out resolvedcontenttype,
            out resolvedcontenttypeencoding);
        response.contenttype = resolvedcontenttype;

        if (result.statuscode != null)
        {
            response.statuscode = result.statuscode.value;
        }

        string content = formattostring(result.content);
        if (result.content != null)
        {
            response.contentlength = resolvedcontenttypeencoding.getbytecount(content);
            using (var textwriter = _httpresponsestreamwriterfactory.createwriter(response.body, resolvedcontenttypeencoding))
            {
                await textwriter.writeasync(content);
                await textwriter.flushasync();
            }
        }
    }
}

这里定义了默认的contenttype 类型,采用了文本格式,即"text/plain; charset=utf-8",这会在请求结果的header中出现。为了特殊说明这个格式,也可以自定义一个特殊类型,例如"text/book; charset=utf-8",这需要项目中提前约定好。定义了一个formattostring方法用于将book类型的数据格式化。最终将格式化的数据写入response.body中。

这个bookresultexecutor定义之后,需要在依赖注入中(startup文件中的configureservices方法)注册:

public void configureservices(iservicecollection services)
{
    //省略部分代码
    services.tryaddsingleton<iactionresultexecutor<bookresult>, bookresultexecutor>();
}

至此,这个自定义的bookresult就可以被使用了,例如下面代码所示的action:

public bookresult getbookresult()
{
    return new bookresult(new book() { code = "1001", name = "asp" });
}

用fiddler访问这个action测试一下,返回结果如下:

book code:[1001]|book name:<asp>

header值:

content-length: 32
content-type: text/book; charset=utf-8

这是自定义了content-type的结果。

三、 自定义格式化类

对于上一节的例子,也可以对照jsonoutputformatter来自定义一个格式化类来实现。将新定义一个名为bookoutputformatter的类,也如同jsonoutputformatter一样继承textoutputformatter。代码如下:

public class bookoutputformatter : textoutputformatter
    {
        public bookoutputformatter()
        {
            supportedencodings.add(encoding.utf8);
            supportedencodings.add(encoding.unicode);
            supportedmediatypes.add("text/book");
        }

        public override bool canwriteresult(outputformattercanwritecontext context)
        {
            if (context == null)
            {
                throw new argumentnullexception(nameof(context));
            }

            if (context.objecttype == typeof(book) || context.object is book)
            {
                return base.canwriteresult(context);
            }

             return false;
        }

         private static string formattostring(book book)
        {
            return string.format("book code:[{0}]|book name:<{1}>",book.code,book.name);
        }

        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 valueasstring = formattostring(context.object as book);
            if (string.isnullorempty(valueasstring))
            {
                await task.completedtask;
            }

            var response = context.httpcontext.response;
            await response.writeasync(valueasstring, selectedencoding);
        }
    }

首先在构造函数中定义它所支持的字符集和content-type类型。重写canwriteresult方法,这是用于确定它是否能处理对应的请求返回结果。可以在此方法中做多种判断,最终返回bool类型的结果。本例比较简单,仅是判断返回的结果是否为book类型。同样定义了formattostring方法用于请求结果的格式化。最后重写writeresponsebodyasync方法,将格式化后的结果写入response.body中。

bookoutputformatter定义之后也需要注册到系统中去,例如如下代码:

 services.addmvc(
    options =>
    {
        options.outputformatters.insert(0,new bookoutputformatter());
    }
 )

这里采用了insert方法,也就是将其插入了outputformatters集合的第一个。所以在筛选outputformatters的时候,它也是第一个。此时的outputformatters如下图 3

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

图 3

通过fiddler测试一下,以第一节返回book类型的第4个例子为例:

public book getmodel()
{
    return new book() { code = "1001", name = "asp" };
}

当设定accept: text/book或者未设定accept的时候,采用了自定义的bookoutputformatter,返回结果为:

book code:[1001]|book name:<asp>

content-type值是:content-type: text/book; charset=utf-8。

当设定accept: application/json的时候,返回json,值为:

{"code":"1001","name":"asp"}

content-type值是:content-type: application/json; charset=utf-8。

这是由于bookoutputformatter类型排在了jsonoutputformatter的前面,所以对于book类型会首先采用bookoutputformatter,当客户端通过accept方式要求返回结果为json的时候,才采用了json类型。测试一下服务端的要求。将这个action添加produces设置,代码如下:

 [produces("application/json")]
 public book getmodel()
 {
     return new book() { code = "1001", name = "asp" };
 }

此时无论设定accept: text/book或者未设定accept的情况,都会按照json的方式返回结果。这也验证了第二节关于服务端和客户端“协商”的规则。

四、添加xml类型支持

第三、四节通过自定义的方式实现了特殊格式的处理,在项目中常见的格式还有xml,这在asp.net core中没有做默认支持。如果需要xml格式的支持,可以通过nuget添加相应的包。

在nuget中搜索并安装microsoft.aspnetcore.mvc.formatters.xml,如下图 4

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

图 4

不需要像bookoutputformatter那样都注册方式,系统提供了注册方法:

services.addmvc().addxmlserializerformatters();

或者

services.addmvc().addxmldatacontractserializerformatters();

分别对应了两种格式化程序:

system.xml.serialization.xmlserializer;
system.runtime.serialization.datacontractserializer;

二者的区别就不在这里描述了。注册之后,就可以通过在请求的header中通过设置“accept: application/xml”来获取xml类型的结果了。访问上一节的返回结果类型为book的例子,返回的结果如下:

<book xmlns:xsi="http://www.w3.org/2001/xmlschema-instance" xmlns:xsd="http://www.w3.org/2001/xmlschema">
  <code>1001</code>
  <name>asp</name>
</book>

content-type值是:content-type: application/xml; charset=utf-8。