netcore webapi action 同时支持 get 和 post 请求

news2024/11/17 1:43:38

最近在项目开发过程中,有个别接口需要同时支持GET和POST请求,经过一番测试,貌似NetCore只能接收指定的FromBody、FromQuery等参数,经过一番查找后发现文章:为ASP.NET Core实现一个自适应ModelBinder,让Action自适应前端参数传递

文章地址:https://masuit.org/1889?t=0HMUL0LVM3L9U

后续说明使用与原文不一致的代码,原文内容如下:

在以前.NET Framework写MVC5的时候,Action的参数前端传递的时候默认是可以自适应的,即:以queryString、表单或者json传递都能够被正确接收,而到了asp.net core中,action接收参数默认只有queryString,显式声明了FromForm或FromBody之后也只能被表单或json接受,即使是同时打上FromForm和FromBody,也只有FromForm生效,FromBody不会起作用的,比如下面的代码:

public ActionResult Test([FromBody]MyClass model) // 只能接受以application/json传递过来的参数 
{
return Ok(model);
}

public ActionResult Test([FromForm]MyClass model) // 只能接受以表单传递过来的参数 
{
return Ok(model);
}

public ActionResult Test([FromBody, FromForm]MyClass model) // 只能接受以application/json传递过来的参数,从表单来的无效 
{
return Ok(model);
}

这就很麻烦了,如果我想同一个接口同时支持queryString、表单和json请求类型的参数绑定到模型上,那只能写多个接口重载来适配,如果想一个action同时支持queryString、表单和json请求类型的参数绑定,我们的主要目的是替换掉FromBody的默认行为,那么只有写一个自定义的ModelBinder;
话不多说,直接上代码,再说原理:

public class BodyOrDefaultModelBinder : IModelBinder
{
private readonly IModelBinder _bodyBinder;
private readonly IModelBinder _complexBinder;

public BodyOrDefaultModelBinder(IModelBinder bodyBinder, IModelBinder complexBinder)
{
_bodyBinder = bodyBinder;
_complexBinder = complexBinder;
}

public async Task BindModelAsync(ModelBindingContext bindingContext)
{
var request = bindingContext.HttpContext.Request;
request.EnableBuffering();
var buffer = new byte[Convert.ToInt32(request.ContentLength)];
_ = await request.Body.ReadAsync(buffer, 0, buffer.Length);
var text = Encoding.UTF8.GetString(buffer);
request.Body.Position = 0;
if (bindingContext.ModelType.IsPrimitive || bindingContext.ModelType == typeof(string) || bindingContext.ModelType.IsEnum || bindingContext.ModelType == typeof(DateTime) || bindingContext.ModelType == typeof(Guid))
{
var parameter = bindingContext.ModelMetadata.ParameterName;
var value = "";
if (request.Query.ContainsKey(parameter))
{
value = request.Query[parameter] + "";
}
else if (request.ContentType.StartsWith("application/json"))
{
try
{
value = JObject.Parse(text)[parameter] + "";
}
catch
{
value = text;
}
}
else if (request.HasFormContentType)
{
value = request.Form[bindingContext.ModelMetadata.ParameterName] + "";
}

if (value.TryConvertTo(bindingContext.ModelType, out var result))
{
bindingContext.Result = ModelBindingResult.Success(result);
}

return;
}

if (request.HasFormContentType)
{
if (bindingContext.ModelType.IsClass)
{
await DefaultBindModel(bindingContext);
}
else
{
bindingContext.Result = ModelBindingResult.Success(request.Form[bindingContext.ModelMetadata.ParameterName].ToString().ConvertTo(bindingContext.ModelType));
}

return;
}

try
{
bindingContext.Result = ModelBindingResult.Success(JsonConvert.DeserializeObject(text, bindingContext.ModelType) ?? request.Query[bindingContext.ModelMetadata.ParameterName!].ToString().ConvertTo(bindingContext.ModelType));
}
catch
{
await DefaultBindModel(bindingContext);
}
}

private async Task DefaultBindModel(ModelBindingContext bindingContext)
{
await _bodyBinder.BindModelAsync(bindingContext);
if (bindingContext.Result.IsModelSet)
{
return;
}

bindingContext.ModelState.Clear();
await _complexBinder.BindModelAsync(bindingContext);
}
}

这一大片代码,看懵了吧,接下来说下原理:

既然是要同时支持queryString、表单和json请求类型,那么肯定是在模型绑定的时候做各种的兼容处理,这里就优先从请求体里面获取传递的参数信息,如果请求体里面拿不到,则从queryString里面找,而从请求体获取又分为了表单和json;而action的参数又分为了基本类型的参数和复杂类型的参数,所以模型绑定的时候还需要检测被绑定的模型是基本类型还是复杂类型。


首先,我们不管有没有请求体参数过来,我们先从请求体里把内容解析成字符串出来留作之后的备用,然后检查被绑定模型的类型,如果是基本类型,比如int类型的id参数,那我们就可以先看queryString中有没有这个key,没有就从json或者表单里面去找,找到之后转换成对应的类型ConvertTo,其中的bindingContext.ModelMetadata.ParameterName拿到参数名字(id),bindingContext.ModelType拿到参数对应的类型是int。

如果是复杂类型的模型,那就检测是表单还是json,尝试从表单或反序列化json进行模型绑定,如果绑定失败,再调用框架自带的BodyBinder和ComplexBinder。

但是,就上面这段代码,也用不了啊,它还需要传入bodyBinder和complexBinder这两个框架的模型绑定器,也跟FromBody还没有任何关系啊,所以我们还需要实现一个ModelBinderProvider,让它跟FromBody产生关系:

public class BodyOrDefaultModelBinderProvider : IModelBinderProvider
{
private readonly BodyModelBinderProvider _bodyModelBinderProvider;
private readonly ComplexObjectModelBinderProvider _complexDataModelBinderProvider;

public BodyOrDefaultModelBinderProvider(BodyModelBinderProvider bodyModelBinderProvider, ComplexObjectModelBinderProvider complexDataModelBinderProvider)
{
_bodyModelBinderProvider = bodyModelBinderProvider;
_complexDataModelBinderProvider = complexDataModelBinderProvider;
}

public IModelBinder GetBinder(ModelBinderProviderContext context)
{
if (context.BindingInfo.BindingSource != null && context.BindingInfo.BindingSource.CanAcceptDataFrom(BindingSource.Body))
{
var bodyBinder = _bodyModelBinderProvider.GetBinder(context);
var complexBinder = _complexDataModelBinderProvider.GetBinder(context);
return new BodyOrDefaultModelBinder(bodyBinder, complexBinder);
}

return null;
}
}


在获取绑定器的时候,检测绑定器上下文的绑定源是否是FromBody;其中bodyBinder和complexBinder则由对应的provider提供,那么你的问题可能又来了:BodyModelBinderProvider和ComplexObjectModelBinderProvider又从哪儿来呢?

既然bodyBinder和complexBinder这两个框架的模型绑定器是框架自带的,那么BodyModelBinderProvider和ComplexObjectModelBinderProvider肯定也是框架自带的,它们就在services.AddControllers()或者services.AddMvc()的时候,ModelBinderProviders里面就已经有了。

而我们写自定义的模型绑定器,最终也是要注册到ModelBinderProviders中才会生效的,那怎么获取BodyModelBinderProvider和ComplexObjectModelBinderProvider呢?ModelBinderProviders是个抽象的IModelBinderProvider集合,我们在这个集合里面找到类型是BodyModelBinderProvider和ComplexObjectModelBinderProvider的ModelBinderProvider然后传递给我们自己的BodyOrDefaultModelBinderProvider即可,这样我们便能够注册BodyOrDefaultModelBinderProvider到ModelBinderProviders中,但是,注册的时候有个讲究,我们的目的是替换掉原始的FromBody行为,让其同时支持queryString、表单和json请求类型,所以我们直接粗暴的将BodyOrDefaultModelBinderProvider插到ModelBinderProviders的第一位即可:

builder.Services.AddControllers(options =>
{
options.ModelBinderProviders.Insert(0, new BodyOrDefaultModelBinderProvider(options.ModelBinderProviders.OfType<BodyModelBinderProvider>().Single(), options.ModelBinderProviders.OfType<ComplexObjectModelBinderProvider>().Single()));
});


这样看起来还是不够优雅,我们稍微再弄个扩展函数封装一下:

public static class BodyOrDefaultModelBinderProviderSetup
{
public static void InsertBodyOrDefaultBinding(this IList<IModelBinderProvider> providers)
{
var bodyProvider = providers.OfType<BodyModelBinderProvider>().Single();
var complexDataProvider = providers.OfType<ComplexDataModelBinderProvider>().Single();
providers.Insert(0, new BodyOrDefaultModelBinderProvider(bodyProvider, complexDataProvider));
}
}
builder.Services.AddControllers(options => options.ModelBinderProviders.InsertBodyOrDefaultBinding());


这样,是否优雅了许多,且只需要在程序启动的时候注册一下BodyOrDefaultModelBinderProvider,其他的没有任何代码侵入,即可实现全局的请求参数自适应绑定。

但是,还没有完
你以为这就完了?光是上面实现的这样,我们只能支持到单个参数的action自适应,多个参数的时候程序会报错的,比如下面这个action:

 [HttpPost("/test2")]    
    public IActionResult Test([FromBody] string name, [FromBody] int age)    
    {
            return Ok(new { name, age });    
    }

意思就是说FromBody只适用于单个参数的action,有多个参数的action它就不支持了。所以我们还需要实现一个自定义的attribute来支持这种多参数的action,那我们按照FromBody的源码抄一个吧:

[AttributeUsage(AttributeTargets.Parameter | AttributeTargets.Property)]
public class FromBodyOrDefaultAttribute : Attribute, IBindingSourceMetadata
{
}

根据VS的提示,它还需要一个BindingSource

而框架自带的BindingSource.Body肯定是不能用的了,所以我们还需要实现一个BindingSource,并重写CanAcceptDataFrom函数,判断传入的BindingSource是否和BindingSource.Body或者当前类型的BindingSource相同即可:

public class BodyOrDefaultBindingSource : BindingSource
{
public static readonly BindingSource BodyOrDefault = new BodyOrDefaultBindingSource("BodyOrDefault", "BodyOrDefault", true, true);

public BodyOrDefaultBindingSource(string id, string displayName, bool isGreedy, bool isFromRequest) : base(id, displayName, isGreedy, isFromRequest)
{
}

public override bool CanAcceptDataFrom(BindingSource bindingSource)
{
return bindingSource == Body || bindingSource == this;
}
}

然后将BodyOrDefaultBindingSource.BodyOrDefault传递给FromBodyOrDefaultAttribute的BindingSource属性:

[AttributeUsage(AttributeTargets.Parameter | AttributeTargets.Property)]
public class FromBodyOrDefaultAttribute : Attribute, IBindingSourceMetadata
{
public BindingSource BindingSource => BodyOrDefaultBindingSource.BodyOrDefault;
}


最后再改造一下BodyOrDefaultModelBinderProvider的代码,将GetBinder函数里的判断条件改成:

if (context.BindingInfo.BindingSource != null && (context.BindingInfo.BindingSource.CanAcceptDataFrom(BindingSource.Body) || context.BindingInfo.BindingSource.CanAcceptDataFrom(BodyOrDefaultBindingSource.BodyOrDefault)))

到此为止,才算是完整实现了action参数模型自适应绑定的功能。


跑起来演示一遍

你以为就这样让你抄代码用?
那也太不友好了,基于上面的示例代码已经完善成了一个nuget包:Masuit.Tools.AspNetCore,你直接安装这个nuget包即可使用。

完整的源代码也上传到了github:https://github.com/ldqk/Masuit.Tools/tree/master/Masuit.Tools.AspNetCore/ModelBinder

以上均为引用原文内容,感谢大佬开源分享。

不知道原文作者使用的是哪个版本,本文使用的是 Masuit.Tools.AspNetCore-1.2.7.4 版本,引用后按照原文尝试,其中:

services.AddControllers(options => options.ModelBinderProviders.InsertBodyOrDefaultBinding());

代码中 InsertBodyOrDefaultBinding 方法已不存在,于是开始阅读作者源代码,发现作者已封装为中间件:


于是直接在Startup.cs的Configure方法中加入 app.UseBodyOrDefaultModelBinder(); 即可:

 编写控制器方法:

/// <summary>
/// 
/// </summary>
/// <param name="data"></param>
/// <returns></returns>
[HttpPost,HttpGet]
public IActionResult RequestAction([FromBodyOrDefault] ParamEntity data)
{
    return Ok(data);
}

编写完成后,使用PostMan进行测试:

POST请求:

请求结果:

GET请求:

看上去调用比较顺利,希望本文对你有帮助。 

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1139203.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

HackTheBox-Starting Point--Tier 1---Sequel

文章目录 一 题目二 实验过程 一 题目 Tags Vulnerability Assessment、Databases、MySQL、SQL、Reconnaissance、Weak Credentials译文&#xff1a;漏洞评估、数据库、MYSQL、SQL、侦察、凭证薄弱Connect To attack the target machine, you must be on the same network.C…

QT 中 Graphics View 程序例子-Diagram Scene Example

一、 概况 本例演示如何使用图形视图框架。 “图表场景”示例是一个应用程序&#xff0c;您可以在其中创建流程图。可以添加流程图形状和文本&#xff0c;并通过箭头连接形状&#xff0c;如上图所示。形状、箭头和文本可以赋予不同的颜色&#xff0c;并且可以更改文本的字体、…

HackTheBox-Starting Point--Tier 1---Crocodile

文章目录 一 题目二 实验过程 一 题目 Tags Web、Network、Custom Applications、Protocols、Apache、FTP、Reconnaissance、Web Site Structure Discovery、Clear Text Credentials、Anonymous/Guest Access译文&#xff1a;Web、网络、定制应用程序、协议、Apache、FTP、侦…

C++项目——云备份-③-实用工具类设计与实现

文章目录 专栏导读1.文件实用工具类的设计2.文件实用工具类的实现2.1前置知识补充2.1.1struct stat 与 stat介绍2.1.2std::experimental::filesystem认识 2.2FileUtil实现 3.JSON实用工具类的设计4.JSON实用工具类的实现5.实用工具类整理 专栏导读 &#x1f338;作者简介&#…

ESP32智能小车+PS2无线遥控器+麦克纳姆轮+microPython

from machine import Pin,PWM from ps2 import PS2Controller import time import os# ############################################# # PS2 遥控器 # ############################################# ps2ctl PS2Controller(di_pin_no26, do_pin_no27, cs_pin_no14, clk_pin…

Unity中Shader的模型网格阴影

文章目录 前言一、网格阴影原理1、在世界空间下&#xff0c;把角色模型在Y轴上压缩成一个面片&#xff0c;把颜色修改成像影子的颜色2、把压缩后的面片&#xff0c;移动到合适的位置&#xff0c;把模型和阴影面片错开3、实现距离脚近的阴影偏移少&#xff0c;距离脚远的阴影偏移…

【已解决】AttributeError: module ‘cv2‘ has no attribute ‘bgsegm‘

问题 使用cv2.bgsegm.createBackgroundSubtractorMOG()去除背景的时候&#xff0c;遇到如下问题&#xff1a; AttributeError: module cv2 has no attribute bgsegm原因 报错原因&#xff1a;使用的python环境中没有安装扩展包contrib 解决方法 可以通过pip或者conda安装 …

QT中文乱码解决方案与乱码的原因

相信大家应该都遇到过中文乱码的问题&#xff0c;有时候改一改中文就不乱码了&#xff0c;但是有时候用同样的方式还是乱码&#xff0c;那么这个乱码到底是什么原因&#xff0c;又该如何彻底解决呢&#xff1f; 总结 先总结一下&#xff1a; Qt5中&#xff0c;将QString()的构…

Java实现Csv文件导入导出

Java实现Csv文件导入导出 什么是.csv文件&#xff1f; CSV&#xff08;Comma-Separated Values&#xff0c;逗号分隔的值&#xff09;是一种简单、实用的文件格式&#xff0c;用于存储和表示包括文本、数值等各种类型的数据。CSV 文件通常以 .csv 作为文件扩展名。这种文件格…

基于蜣螂优化算法DBO优化的VMD-KELM光伏发电短期功率预测MATLAB代码

微❤关注“电气仔推送”获得资料&#xff08;专享优惠&#xff09; VMD适用于处理非线性和非平稳信号&#xff0c;例如振动信号、生物信号、地震信号、图像信号等。它在信号处理、振动分析、图像处理等领域有广泛的应用&#xff0c;特别是在提取信号中的隐含信息和去除噪声方面…

字符串中的strcpy和strncpy区别

strcpy:函数原型是char *strcpy(char* dest, const char *src)&#xff0c;含义是将src中的字符串复制到dest中。 strncpy:函数原型是char *strncpy(char *dest const char *src,int n&#xff09;&#xff0c;表示把src所指向的字符串中以src地址开始的前n个字节复制到dest所…

香港施政报告人才引进政策2023全面解读,对优才计划申请是否有影响?

香港施政报告人才引进政策2023全面解读&#xff0c;对优才计划申请是否有影响&#xff1f; 香港第二份施政报告10月25日出来了&#xff01;这次真的是“走进民生”啊&#xff0c;什么路都帮你想好了&#xff01; 总结就是&#xff1a;继续抢人才、留人才&#xff01;在昨天的《…

103.linux5.15.198 编译 firefly-rk3399(2)

1. 平台&#xff1a; rk3399 firefly 2g16g 2. 内核&#xff1a;linux5.15.136 &#xff08;从内核镜像网站下载&#xff09; 3. 交叉编译工具 gcc version 7.5.0 (Ubuntu/Linaro 7.5.0-3ubuntu1~18.04) 4. 宿主机&#xff1a;ubuntu18.04 5. 需要的素材和资料&#xff…

MySQL 单表查询 多表设计

目录 数据库操作-DQL(单表查询)语法基本查询&#xff08;不带任何条件&#xff09;条件查询&#xff08;where&#xff09;聚合函数分组查询&#xff08;group by [having]&#xff09;&#xff08;重点&#xff09;排序查询&#xff08;order by&#xff09;&#xff08;重点&…

MySQL数据库基本操作2

文章目录 主要内容一.DQL1.语法格式代码如下&#xff08;示例&#xff09;: 2.数据准备代码如下&#xff08;示例&#xff09;: 3.简单查询代码如下&#xff08;示例&#xff09;: 4.运算符5.运算符操作-算术运算符代码如下&#xff08;示例&#xff09;: 6.运算符操作-条件查询…

Spring Cloud Sentinel整合Nacos实现配置持久化

sentinel配置相关配置后无法持久化&#xff0c;服务重启之后就没了&#xff0c;所以整合nacos&#xff0c;在nacos服务持久化&#xff0c;sentinel实时与nacos通信获取相关配置。 使用上一章节Feign消费者服务实现整合。 版本信息&#xff1a; nacos:1.4.1 Sentinel 控制台 …

【DRAM存储器十七】DDR2介绍-DDR2的新增技术-Post CAS、ODT、RDQS、OCD

&#x1f449;个人主页&#xff1a;highman110 &#x1f449;作者简介&#xff1a;一名硬件工程师&#xff0c;持续学习&#xff0c;不断记录&#xff0c;保持思考&#xff0c;输出干货内容 参考资料&#xff1a;《镁光DDR数据手册》 目录 Post CAS ODT RDQS OCD Post CA…

方舟生存进化ARK个人服务器搭建教程保姆级

方舟生存进化ARK个人服务器搭建教程保姆级 大家好我是艾西&#xff0c;在很久之前我有给大家分享过方舟生存进化的搭建架设教程&#xff0c;但时间久远且以前的教程我现在回头看去在某些地方说的并不是那么清楚。最近也是闲暇无事打算重新巩固下方舟生存进化的搭建架设教程&…

[计算机提升] Windows文件系统类型介绍

1.13 文件系统 在Windows系统中&#xff0c;文件系统是一种用于组织和管理计算机上存储的文件和目录的方法。它提供了一种结构化的方式来访问、存储和检索数据。 以下是Windows系统中常见的文件系统&#xff1a; FAT&#xff08;FAT16、FAT32&#xff09;&#xff1a;FAT&…

2023年中国研磨液需求量、市场规模及行业竞争格局分析[图]

研磨是半导体加工过程中的一项重要工艺&#xff0c;它主要是应用化学研磨液混配磨料的方式对半导体表面进行精密加工&#xff0c;研磨液是平坦化工艺中研磨材料和化学添加剂的混合物&#xff0c;研磨材料主要是石英、二氧化铝和氧化铈&#xff0c;研磨液是影响半导体表面质量的…