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

news/2024/5/19 21:30:44 标签: .netcore

最近在项目开发过程中,有个别接口需要同时支持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.niftyadmin.cn/n/5128353.html

相关文章

Go RESTful API 接口开发

文章目录 什么是 RESTful APIGo 流行 Web 框架-GinGo HelloWorldGin 路由和控制器Gin 处理请求参数生成 HTTP 请求响应Gin 的学习内容实战用 Gin 框架开发 RESTful APIOAuth 2.0接口了解用 Go 开发 OAuth2.0 接口示例 编程有一个准则——Don‘t Repeat Yourself&#xff08;不要…

LVS集群-DR模式

概念&#xff1a; LVS-DR模式&#xff0c;也是最常用的lVS负载方式&#xff0c;DR DIRECT ROUTING 直接路由模式 负载均衡器lVS调度器&#xff0c;只负责请求和转发到后端的真实服务器&#xff0c;但是影响结果&#xff0c;由后端服务器直接转发给客户端&#xff0c;不需要经…

刀片式服务器介绍

大家都知道服务器分为机架式服务器、刀片式服务器、塔式服务器三类&#xff0c;今天小编就分别讲一讲这三种服务器&#xff0c;第二篇先来讲一讲刀片式服务器的介绍。 刀片式服务器定义&#xff1a;是一种高密度的服务器架构&#xff0c;通过多个独立服务器单元组成&#xff0c…

免费的MySQL连接工具

1.DBeaver 简单好用&#xff0c;支持数据库多。 Download | DBeaver Community CSDN资源路径&#xff1a; https://download.csdn.net/download/qq_34253002/88475845?spm1001.2014.3001.5501 2.MySQL Workbench 需要使用到Oracle账号下载。 MySQL :: Download MySQL Wor…

创建一个具有背景轮播和3D卡片翻转效果的个人名片网页

目录 项目展示 图片展示 前言 项目目标 项目目标 步骤 3&#xff1a;CSS 样式 步骤 4&#xff1a;JavaScript 动画 项目源码 知识点介绍 &#xff08;大佬请绕道&#xff09; HTML 结构的构建 2. CSS 样式的设计 3. JavaScript 动画的实现 4. 背景图轮播的逻辑 5…

JS递归(含多个练习)

递归是什么&#xff1f; 调用自身函数称为递归函数 function fn(){fn()}fn()递归的作用和循环是基本一样的 编写递归函数&#xff0c;一定要包含两个条件 1.基线条件 2.递归条件 接下来我用几个实例为大家带来递归的用法 1.使用递归让延迟器有定时器的效果 function timer() …

【代码随想录】算法训练计划03

1、203. 移除链表元素 题目&#xff1a; 给你一个链表的头节点 head 和一个整数 val &#xff0c;请你删除链表中所有满足 Node.val val 的节点&#xff0c;并返回 新的头节点 。 输入&#xff1a;head [1,2,6,3,4,5,6], val 6 输出&#xff1a;[1,2,3,4,5] 思路&#xf…

时间、空间复杂度的例题详解

文章前言 上篇文章带大家认识了数据结构和算法的含义&#xff0c;以及理解了时间、空间复杂度&#xff0c;那么接下来来深入理解一下时间、空间复杂度。 时间复杂度实例 实例1 // 计算Func2的时间复杂度&#xff1f; void Func2(int N) {int count 0;for (int k 0; k <…