我们正在编写一些 API,它需要 header 中的 sessionId 和正文中的一些其他数据。是否可以只从标题和正文部分自动解析一个类
- 您的
GetPendingClockInMessages 带有[HttpGet("messages")] 注释。但是,HTTP GET 方法根本没有主体。此外,它不能消耗application/json。请改成HttpPost("messages")
- 通常,
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] 属性,这种方式更容易)。