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

最近在项目开发过程中,有个别接口需要同时支持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请求:

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

.NET Core 中,如果你正在创建一个 Web API 应用,并希望获取当前 HTTP 请求的唯一标识(如 ID),你可以通过多种方式实现。通常,你会选择使用一些自定义中间件、全局属性或者依赖注入来存储和检索这个ID。 1. **全局属性**:可以在 `Controller` 类上添加一个特性,然后在每个 API 操作方法内部访问这个属性。例如: ```csharp [ApiController] public class YourController : ControllerBase { [RequestId] public string RequestId { get; private set; } [HttpGet] public IActionResult Get() { // 在这里可以使用 RequestId 变量 return Ok(RequestId); } } ``` 在这个例子中,你需要定义一个名为 `[RequestId]` 的特性,并在其中设置属性值。通常,中间件会自动将请求ID附加到上下文中。 2. **中间件**:可以通过编写一个自定义中间件,在每个请求处理之前记录并提供ID。下面是一个简单的示例: ```csharp public class RequestIdMiddleware { private readonly RequestDelegate _next; public RequestIdMiddleware(RequestDelegate next) { _next = next; } public async Task Invoke(HttpContext context) { var requestId = Guid.NewGuid().ToString(); // 创建一个随机ID context.RequestServices.GetRequiredService<IHttpContextAccessor>(). HttpContext.Items["RequestId"] = requestId; await _next(context); } } // 注册中间件 app.UseMiddleware<RequestIdMiddleware>(); ``` 3. **依赖注入**:如果你的API需要频繁使用请求ID,可以考虑将其注入到依赖的服务中: ```csharp public interface IRequestIdService { string GetCurrentRequestId(); } public class RequestIdService : IRequestIdService { private readonly IHttpContextAccessor _httpContextAccessor; public RequestIdService(IHttpContextAccessor httpContextAccessor) { _httpContextAccessor = httpContextAccessor; } public string GetCurrentRequestId() => (string)_httpContextAccessor.HttpContext.Items["RequestId"]; } services.AddSingleton<IRequestIdService, RequestIdService>(); ``` 在控制器中,你可以通过依赖注入来获取 `IRequestIdService` 并调用其方法获取ID。
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值