【问题标题】:Combine [FromBody] with [FromHeader] in WebAPI in .net Core 3.0在 .net Core 3.0 的 WebAPI 中将 [FromBody] 与 [FromHeader] 结合使用
【发布时间】:2019-11-25 18:31:35
【问题描述】:

我们正在编写一些 API,它需要 header 中的 sessionId 和正文中的一些其他数据。 是否可以只从标题和正文部分自动解析一个类?

类似:

[HttpGet("messages")]
[Produces("application/json")]
[Consumes("application/json")]
[Authorize(Policy = nameof(SessionHeaderKeyHandler))]
public async Task<ActionResult<MessageData>> GetPendingClockInMessages(PendingMessagesData pendingMessagesRequest)
{
    some body...
}

请求类如:

public class PendingMessagesData
{
    [FromHeader]
    public string SessionId { get; set; }
    [FromBody]
    public string OrderBy { get; set; }
}

我知道,可以这样做,但这意味着我必须将 SessionId 作为参数传递给其他方法,而不是只传递一个对象。我们必须在每个 API 调用中都这样做。

public async Task<ActionResult<MessageData>> GetPendingClockInMessages(
[FromHeader] string sessionId,
[FromBody] PendingMessagesData pendingMessagesRequest)
{
    some body...
}

谢谢你, 雅库布

【问题讨论】:

  • 您想从标头或查询字符串参数中获取SessionId 吗?如果要从标头中获取SessionId,可以直接在控制器中提取HttpContext.Request.Headers["SessionId"] 之类的值,然后不需要在PendingMessagesData 类中包含SessionId
  • 是的,我可以,但自动执行并将其传递给 PendingMessagesData 似乎更好。我更喜欢这个而不是单独的读取标题并将其作为参数传递给某个方法。我必须在每个 API 调用中读取此值,并将其作为额外参数传递给服务方法。

标签: .net-core asp.net-core-webapi


【解决方案1】:

我们正在编写一些 API,它需要 header 中的 sessionId 和正文中的一些其他数据。是否可以只从标题和正文部分自动解析一个类

  1. 您的GetPendingClockInMessages 带有[HttpGet("messages")] 注释。但是,HTTP GET 方法根本没有主体。此外,它不能消耗application/json。请改成HttpPost("messages")
  2. 通常,SessionId 不会像其他 HTTP 标头一样在 Session: {SessionId} 的标头中传递。会话通过 IDataProtector 加密。换句话说,你不能通过Request.Headers["SessionId"]得到它。

除了以上两个事实之外,您还可以创建一个自定义模型绑定器来执行此操作。

由于 Session 不直接来自标头,让我们创建一个自定义 [FromSession] 属性来替换您的 [FromHeader]

public class FromSessionAttribute : Attribute, IBindingSourceMetadata
{
    public static readonly BindingSource Instance = new BindingSource("FromSession", "FromSession Binding Source", true, true);
    public BindingSource BindingSource { get { return FromSessionAttribute.Instance; } }
}

既然你正在消费application/json,让我们创建一个活页夹,如下所示:

public class MyModelBinder : IModelBinder
{
    private readonly JsonOptions jsonOptions;

    public MyModelBinder(IOptions<JsonOptions> jsonOptions)
    {
        this.jsonOptions = jsonOptions.Value;
    }

    public async Task BindModelAsync(ModelBindingContext bindingContext)
    {
        var type = bindingContext.ModelType;
        var pis = type.GetProperties();
        var result= Activator.CreateInstance(type);

        var body= bindingContext.ActionContext.HttpContext.Request.Body;
        var stream = new System.IO.StreamReader(body);
        var json = await stream.ReadToEndAsync();
        try{
            result = JsonSerializer.Deserialize(json, type, this.jsonOptions.JsonSerializerOptions);
        } catch(Exception){
            // in case we want to pass string directly. if you don't need this feature, remove this branch
            if(pis.Count()==2){
                var prop = pis
                    .Where(pi => pi.PropertyType == typeof(string) )
                    .Where(pi => !pi.GetCustomAttributesData().Any(ca => ca.AttributeType == typeof(FromSessionAttribute)))
                    .FirstOrDefault();
                if(prop != null){
                    prop.SetValue( result ,json.Trim('"'));
                }
            } else{
                bindingContext.ModelState.AddModelError("", $"cannot deserialize from body");
                return;
            }
        }
        var sessionId = bindingContext.HttpContext.Session.Id;
        if (string.IsNullOrEmpty(sessionId)) {
            bindingContext.ModelState.AddModelError("sessionId", $"cannot get SessionId From Session");
            return;
        } else {
            var props = pis.Where(pi => {
                    var attributes = pi.GetCustomAttributesData();
                    return attributes.Any( ca => ca.AttributeType == typeof(FromSessionAttribute));
                });
            foreach(var prop in props) {
                prop.SetValue(result, sessionId);
            }
            bindingContext.Result = ModelBindingResult.Success(result);
        }
    }
}

如何使用

FromSession装饰属性,表示我们想通过HttpContext.Sessino.Id获取属性:

public class PendingMessagesData
{
    [FromBody]
    public string OrderBy { get; set; }  // or a complex model: `public MySub Sub{ get; set; }`
    [FromSession]
    public string SessionId { get; set; }
}

最后在action方法参数上添加modelbinder:

[HttpPost("messages")]
[Produces("application/json")]
[Consumes("application/json")]
public async Task<ActionResult> GetPendingClockInMessages([ModelBinder(typeof(MyModelBinder))]PendingMessagesData pendingMessagesRequest)
{
    return Json(pendingMessagesRequest);
}

就个人而言,我更喜欢另一种方式,即创建一个FromSessionBinderProvider,这样我就可以毫不费力地实现它。 :

public class FromSessionDataModelBinder : IModelBinder
{
    public Task BindModelAsync(ModelBindingContext bindingContext)
    {
        var sessionId = bindingContext.HttpContext.Session.Id;
        if (string.IsNullOrEmpty(sessionId)) {
            bindingContext.ModelState.AddModelError(sessionId, $"cannot get SessionId From Session");
        } else {
            bindingContext.Result = ModelBindingResult.Success(sessionId);
        }
        return Task.CompletedTask;
    }
}

public class FromSessionBinderProvider : IModelBinderProvider
{
    public IModelBinder GetBinder(ModelBinderProviderContext context)
    {
        if (context == null) { throw new ArgumentNullException(nameof(context)); }
        var hasFromSessionAttribute = context.BindingInfo?.BindingSource == FromSessionAttribute.Instance;
        return hasFromSessionAttribute ?
            new BinderTypeModelBinder(typeof(FromSessionDataModelBinder)) :
            null;
    }
}

(如果您能够移除 [ApiController] 属性,这种方式更容易)。

【讨论】:

    猜你喜欢
    • 2020-02-10
    • 2019-03-28
    • 2018-11-08
    • 2021-11-24
    • 2020-07-14
    • 2021-09-15
    • 1970-01-01
    • 2020-02-27
    • 1970-01-01
    相关资源
    最近更新 更多