【ASP.NET Core】从向 Web API 提交纯文本内容谈起

前些时日,老周在升级“华南闲肾回收登记平台”时,为了扩展业务,尤为是容许其余开发人员在其余平台向本系统提交有关肾的介绍资料,因而就为该系统增长了几个 Web API。api

其中,有关肾的介绍采用纯文本方式提交,大概的代码是这样的。服务器

    [Route("api/[controller]/[action]")]
    public class PigController : Controller
    {
        [HttpPost]
        public string KidneyRemarks([FromBody]string remarks)
        {
            return $"根据你的描述,贵肾的当前状态为:{remarks}";
        }
    }

这个 Action 很简单(主要为了方便别人看懂),参数接受一个字符串实例,返回的也是字符串。哦,重点要记住,对参数要加上 FromBody 特性。嗯,为啥呢。由于咱们要获得的数据是从客户端发来的 HTTP 正文提取的,应用这个特性就是说参数的值来自于提交的正文,而不是 Header,也不是 url 参数。async

随后老周兴高采烈地用 Postman 进行测试。ide

幻想老是很美丽的,现实老是很骨感的。结果……函数

 

没成功,这时候,按照常规思路,会产生各类怀疑。怀疑地址错了吗?哪一个配置没写上?是否是路由不正确?……测试

别急,看看服务器返回的状态码:415 Unsupported Media Type。啥意思呢,其实,这就是问题所在了。咱们提交纯文本类型的数据,用的 Content-Type 是 text/plain,但是,不受支持!编码

不信?如今把提交的内容改成 JSON 看看。url

看看,我没说错吧。spa

这就很明了啦,JSON 默认是被支持的,可是纯文本不行。有办法让它支持 text / plain 类型的数据吗?答案是:能的。code

在 Startup 中使用 ConfigureServices 方法配置服务时,咱们通常就是简单地写上。

   services.AddMvc();

而后,各个 MVC 选项保持默认。

在 MVC 选项中,能够控制输入和输出的格式,分别由两个属性来管理:

InputFormatters 属性:是一个集合,里面的每一个对象都要实现 IInputFormatter 接口,默认提供对 JSON 和 XML 的支持。

OutputFormatters 属性:也是一个集合,里面的元素都要实现 IOutputFormatter 接口,默认支持 JSON 和 XML,也支持文本类型。

也就是说,输出是支持纯文本的,因此 Action 能够返回 string 类型的值,但输入是不支持文本格式的,因此,用 text / plain 格式提交,就会获得 415 代码了。

 

明白了这个原理,解决起问题来就好办了,我们本身实现一个支持纯文本格式的 InputFormatter 就好了。不过呢,咱们也没必要直接实现 IInputFormatter 接口,由于,有个抽象类挺好使的—— TextInputFormatter,处理文本直接实现它就行了。

因而乎,老周就写了这个类。

    public sealed class PlainTextInputFormatter : TextInputFormatter
    {
        public override async Task<InputFormatterResult> ReadRequestBodyAsync(InputFormatterContext context, Encoding encoding)
        {
            string content;
            using(var reader = context.ReaderFactory(context.HttpContext.Request.Body, encoding))
            {
                content = await reader.ReadToEndAsync();
            }
            // 最后一步别忘了
            return await InputFormatterResult.SuccessAsync(content);
        }
    }

TextInputFormatter 类只有 ReadRequestBodyAsync 方法是抽象的,因此,若是没其余活要干的话,只实现这个方法就够了。这个方法的功能就是读取 HTTP 请求的正文,而后把你读取到的内容填充给 InputFormatterResult 对象。

InputFormatterResult 类颇有意思的,没有公共构造函数,你没法 new 对象,只能靠媒人介绍对象,经过 Failure、Success 这些方法直接返回对象实例。这些方法你看名字就知道什么用途了,不用多解释。

在上面代码中,ReaderFactory 属性实际上是个委托,经过构造函数建立的,不过,这个委托实例在传进 ReadRequestBodyAsync 方法时已经建立,你只须要像调用方法同样调用它就好了,第一个参数是一个流,哪里的流?固然是 HTTP 请求的正文了,这里能够透过 HttpContext 的 Request 的 Body 来得到;第二个参数嘛,呵呵,是文本编码,这个好办,直接把传进 ReadRequestBodyAsync 方法的 encoding 传过去就好了。

ReaderFactory 委托调用后返回一个 TextReader,是了,咱们就是用它来读取请求正文的。最后把读出来的字符串填充给 InputFormatterResult 就好了。

不过呢,这个类如今还不能用,由于默认状况下,SupportedMediaTypes 集合是空的,你得添加一下,它支持哪些 Content-Type,咱们这里只要 text / plain 就好了。

        public PlainTextInputFormatter()
        {
            SupportedMediaTypes.Add("text/plain");
            SupportedEncodings.Add(System.Text.Encoding.UTF8);
        }

这些写在构造函数里就 OK 了。注意 SupportedEncodings 集合,是配置字符编码,通常嘛,UTF-8 最合适了。你也能够从 TextInputFormatter 类的两个只读的静态字段中获取。

protected static readonly Encoding UTF8EncodingWithoutBOM;
protected static readonly Encoding UTF16EncodingLittleEndian;

如今基本能够用了。由于咱们上面写的那个 Action 是带字符串类型参数的,若是你以为不放心,能够覆写一下 CanReadType 方法,这个方法有个 type 参数,指的是 Model Type,说白了就是 Action 要接收的参数的类型,我们这里是 string,因此,实现这个方法很简单,若是是字符串类型就返回 true,表示能读取,不然返回 false,表示不能读。

 

回到 Startup 类,找到 ConfigureServices 方法,咱们在 AddMvc 的时候要对 Options 配置一下,把我们刚刚写好的 InputFormatter 加进去。

        public void ConfigureServices(IServiceCollection services)
        {
            services.AddMvc(opt =>
            {
                opt.InputFormatters.Add(new PlainTextInputFormatter());             });
        }

好了,如今再请 Postman 大叔,从新测试一下。

 

嗯,皆大欢喜,又解决一个问题了。

 

咱们不妨继续扩展一下,若是提交的是 text / plain 数据内容,而 Action 想让其赋值给 DateTime 或者 int 类型的参数呢。其实也同样,就是本身实现一下输入格式。这一次咱们不继承 TextInputFormatter 类了,而是继承抽象程度更高的 InputFormatter 类。

    public sealed class CustInputFormatter : InputFormatter
    {
        public CustInputFormatter()
        {
            SupportedMediaTypes.Add("text/plain");
        }

        protected override bool CanReadType(Type type)
        {
            return (type == typeof(DateTime)) || (type == typeof(int));
        }

        public override async Task<InputFormatterResult> ReadRequestBodyAsync(InputFormatterContext context)
        {
            string val;
            using (var reader = context.ReaderFactory(context.HttpContext.Request.Body, Encoding.UTF8))
            {
                val = await reader.ReadToEndAsync();
            }
            InputFormatterResult result = null;
            if(context.ModelType == typeof(DateTime))
            {
                result = InputFormatterResult.Success(DateTime.Parse(val));
            }
            else
            {
                result = InputFormatterResult.Success(int.Parse(val));
            }
            return result;
        }
    }

这一次应该不用我解释,你都能看懂了。不过注意一点,由于要应用的目标参数多是 int 和 DateTime 类型,因此,在填充 InputFormatterResult 对象时,你要先检查一下 ModelType 属性。

            if(context.ModelType == typeof(DateTime))
            {
                result = InputFormatterResult.Success(DateTime.Parse(val));
            }
            else
            {
                result = InputFormatterResult.Success(int.Parse(val));
            }

如今应用一下这个输入格式类。

        public void ConfigureServices(IServiceCollection services)
        {
            services.AddMvc(o =>
            {
                o.InputFormatters.Add(new CustInputFormatter());
            });
        }

 

下面来试试吧,建一个 Controller,而后定义两个 Action,一个接收 int 类型的参数,一个接收 DateTime 类型的参数。

    [Route("[controller]/[action]")]
    public class TestController : Controller
    {
        [HttpPost]
        public string Upload([FromBody]DateTime dt)
        {
            return $"你提交的时间是:{dt}";
        }

        [HttpPost]
        public string UploadInt([FromBody]int val)
        {
            return $"你提交的整数值是:{val}";
        }
    }

FromBody 特性千万要记得用上,否则待会读不了你又要处处 Debug 了。

 

好,测试开始了,首先试一下 DateTime 类型的。

 

再试一下 int 类型的。

 

感受如何,好刺激吧。好啦,今天的高大上技巧就分享到这儿了。

示例源代码下载:请用洪荒之力猛点这里

相关文章
相关标签/搜索