NET Core 3.0中WebAPI中的[FromBody]与[FromHeader]组合

时间:2019-11-22 14:14:41

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

我们正在编写一些API,该API在标头中需要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...
}

谢谢你, 雅各布

1 个答案:

答案 0 :(得分:0)

  

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

  1. 您的GetPendingClockInMessages[HttpGet("messages")]注释。但是,HTTP GET方法根本没有正文。另外,它不能消耗application/json。请更改为HttpPost("messages")
  2. 通常,SessionId不会像其他HTTP头一样在Session: {SessionId}的头中传递。会话通过IDataProtector加密。换句话说,您无法通过Request.Headers["SessionId"]来获取它。

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

由于会话不是直接来自标头,因此我们创建一个自定义[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]属性,则这种方式会更容易)。