如何:来自多个来源的参数绑定

时间:2017-09-14 11:11:01

标签: c# asp.net-core asp.net-core-mvc asp.net-core-2.0

目前我正在尝试基于asp.net core 2.0创建一个web api,我想创建一个嵌套路由。在put请求的情况下,它发送路由中的一部分信息和正文中的另一部分。

要求

要调用的网址是

https://localhost/api/v1/master/42/details

如果我们想在 master 42 下面创建一个新的细节,我希望在主体的id出来的时候发送正文中的详细数据。< / p>

curl -X POST --header 'Content-Type: application/json' \
--header 'Accept: application/json' \
-d '{ \ 
   "name": "My detail name", \ 
   "description": "Just some kind of information" \ 
 }' 'https://localhost/api/v1/master/42/details'

请求的响应将是

{
    "name": "My detail name",
    "description": "Just some kind of information",
    "masterId": 42,
    "id": 47
}

和响应标题中的位置网址,如

{
    "location": "https://localhost/api/v1/master/42/details/47
}

到目前为止完成的工作

为了实现这一点,我创建了这个控制器:

[Produces("application/json")]
[Route("api/v1/master/{masterId:int}/details")]
public class MasterController : Controller
{
    [HttpPost]
    [Produces(typeof(DetailsResponse))]
    public async Task<IActionResult> Post([FromBody, FromRoute]DetailCreateRequest request)
    {
        if(!ModelState.IsValid)
            return BadRequest(ModelState);

        var response = await Do.Process(request);

        return CreatedAtAction(nameof(Get), new { id = response.Id }, response);
    }
}

使用这些类:

public class DetailCreateRequest
{
    public int MasterId { get; set; }
    public string Name { get; set; }
    public string Description { get; set; }
}

public class DetailResponse
{
    public int Id { get; set; }
    public int MasterId { get; set; }
    public string Name { get; set; }
    public string Description { get; set; }
}

问题

到目前为止,大部分内容都按预期工作。唯一真正无效的是将MasterId从路径合并到来自身体的DetailCreateRequest

首先尝试:在参数

上使用两个属性

我试图通过这个动作调用结合这两件事:

public async Task<IActionResult> Post([FromBody, FromRoute]DetailCreateRequest request)

但是传入的对象只有MasterId为零。如果我改变了两个属性的顺序,那么只会获取路径中的id,并且忽略正文中的所有值(所以似乎是第一个属性获胜)。

第二次尝试:使用两个不同的参数

我尝试的另一种方法是此操作调用:

public async Task<IActionResult> Post([FromRoute]int masterId,

[FromBody] DetailCreateRequest request)

在第一个位置看起来没问题,因为现在我在控制器动作中都有两个值。但这种方法的主要问题是模型验证。正如您在上面的代码中看到的那样,我检查ModelState.IsValid,它是通过FluentValidation的一些检查填写的,但是这些检查不能真正完成,因为对象没有正确构建,因为缺少主人。

(不工作)想法:使用合并参数

创建自己的属性

试图实现这样的事情:

public async Task<IActionResult> Post([FromMultiple(Merge.FromBody, Merge.FromRoute)]DetailCreateRequest request)

如果我们已经有这样的东西,那就太好了。属性中参数的顺序将给出合并(以及可能的覆盖)发生的顺序。

我已经开始实现此属性并为所需的IValueProviderIValueProviderFactory创建框架。但这似乎是一项相当多的工作。特别是找到所有漂亮的细节,使其与我正在使用的asp.net核心和其他库的整个流程无缝协作(如swagger通过swashbuckle)。

所以我的问题是,如果asp.net核心中已经存在某种机制来实现这样的合并,或者如果有人知道已经存在的解决方案或关于如何实现这样的野兽的好例子。

到目前为止的解决方案:Custom ModelBinder

Merchezatter获得答案后,​​我会研究如何创建custom model binder并提出这个实现:

public class MergeBodyAndValueProviderBinder : IModelBinder
{
    public Task BindModelAsync(ModelBindingContext bindingContext)
    {
        if (bindingContext == null)
            throw new ArgumentNullException(nameof(bindingContext));

        var body = bindingContext.HttpContext.Request.Body;
        var type = bindingContext.ModelMetadata.ModelType;
        var instance = TryCreateInstanceFromBody(body, type, out bool instanceChanged);
        var bindingFlags = BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic;
        var setters = type.GetProperties(bindingFlags).Where(property => property.CanWrite);

        foreach (var setter in setters)
        {
            var result = bindingContext.ValueProvider.GetValue(setter.Name);

            if (result != ValueProviderResult.None)
            {
                try
                {
                    var value = Convert.ChangeType(result.FirstValue, setter.PropertyType);
                    setter.SetMethod.Invoke(instance, new[] { value });
                    instanceChanged = true;
                }
                catch
                { }
            }
        }

        if (instanceChanged)
            bindingContext.Result = ModelBindingResult.Success(instance);

        return Task.CompletedTask;
    }

    private static object TryCreateInstanceFromBody(Stream body, Type type, out bool instanceChanged)
    {
        try
        {
            using (var reader = new StreamReader(body, Encoding.UTF8, false, 1024, true))
            {
                var data = reader.ReadToEnd();
                var instance = JsonConvert.DeserializeObject(data, type);
                instanceChanged = true;
                return instance;
            }
        }
        catch
        {
            instanceChanged = false;
            return Activator.CreateInstance(type);
        }
    }
}

它尝试将正文反序列化为所需的对象类型,然后尝试从可用的值提供程序应用更多值。为了使这个模型绑定器工作,我不得不使用ModelBinderAttribute装饰目标类,并使MasterId内部,以便swagger不宣布它并且JsonConvert不反序列化它:

[ModelBinder(BinderType = typeof(MergeBodyAndValueProviderBinder))]
public class DetailCreateRequest
{
    internal int MasterId { get; set; }
    public string Name { get; set; }
    public string Description { get; set; }
}

在我的控制器中,操作方法参数仍然包含[FromBody]标志,因为swagger使用它来宣告如何调用该方法,但它永远不会被调用,因为我的模型绑定器有一个更高的优先级。

public async Task<IActionResult> Post([FromBody]DetailCreateRequest 

请求)

所以它并不完美,但到目前为止效果还不错。

1 个答案:

答案 0 :(得分:3)

这看起来是一个正确的选择:

[HttpPost]
[Produces(typeof(DetailsResponse))]
public async Task<IActionResult> Post([FromRoute]int masterId, [FromBody]DetailCreateRequest request) {
    //...
}

但是如果您在域模型验证方面遇到一些问题,请创建没有主ID的自定义Dto对象。 否则,您可以使用自定义模型绑定器,然后使用操作和绑定上下文中的参数。

相关问题