【问题标题】:Asp.net single controller action with different model type depending on route dataAsp.net 单控制器动作,根据路由数据具有不同的模型类型
【发布时间】:2021-03-23 21:27:50
【问题描述】:

我想在我的控制器中创建一个通用操作。我将从正文中读取的模型取决于其他路线数据。

例如,我有一个带有create 操作的resources 控制器。路由类似于/api/[controller]/[action]/{resource},其中resource 是路由参数。

所以,POST: /api/resources/create/book 应该在存储库中创建图书资源。每个资源都有自己的CreateModel。对于实例book 可以使用

class BookCreateModel
{
   [Required]
   public string Title {get; set;}

   [Required]
   public Guid AuthorId {get; set;}

   ... // etc
}

我希望我的操作具有如下签名

public Task<IActionResult> Create([FromRoute] resource, [FromBody] object model)
{
   if(!ModelState.IsValid)
      return BadRequest(ModelState);
   ...
}

实际的model 类型应取决于resource 参数和操作名称(本例中为create

我可能应该创建一个模型绑定器,但我希望拥有默认模型绑定器的所有功能(ModelState、验证等)。我想要做的唯一不同的事情是选择它应该绑定到哪个模型类型。其余部分应保持不变。

有没有办法做到这一点,还是我应该自己实现整个绑定逻辑?

【问题讨论】:

  • 您有需要这样设计的特殊用例吗?与更标准的基于资源(或 REST)的 API 设计相比,这似乎很违反直觉。

标签: c# asp.net-mvc asp.net-core asp.net-core-webapi .net-5


【解决方案1】:

如何设置如下路由并注册启动使用端点来映射控制器

[Produces("application/json")]
[Route("api/[controller]/[action]/{resource}")]
[ApiController]
public class ResourceController : ControllerBase
{ 
   
    [HttpPost]
    public Task<IActionResult> Create([FromRoute] string resource, [FromBody] object model)
    {
          if(!ModelState.IsValid)
              return BadRequest(ModelState);
    }
}

启动

 app.UseEndpoints(endpoints =>
        {
            endpoints.MapControllers(); 
        });

【讨论】:

  • 您的示例中模型的实际类型是什么?我已经有这样的设置了。
  • 嗯,这将始终是 json 对象。既然你有资源,我认为你可以使用.ToObject&lt;resourceclass&gt;(); .TryValidateModel 转换为对象并运行验证
  • 我明白你的意思。我只想在动作方法之外这样做。我想在活页夹代码中做到这一点,所以我现在尝试实现我自己的活页夹
  • 实际上我希望它是格式化程序不知道的。数据可能并不总是采用 JSON 格式。任何格式化程序都应该读取输入,我只想向格式化上下文提供我真正想要的最终数据类型
  • 我确信模型绑定能够检查和记录验证错误。当涉及到 api 端点时,您不担心它是什么类型的对象用于进一步的业务流程?
【解决方案2】:

经过大量的谷歌搜索,我能找到并解决我的问题的唯一解决方案是创建我自己的活页夹来模仿默认的BodyModelBinder

然后,在InputFormatterContext 被实例化的地方,我没有将bindingContext.ModelMetadata 作为第四个参数传递,而是传递bindingContext.ModelMetadata.GetMetadataForType([desired type here]),其中我传递的类型取决于其他上下文值(例如路由参数、控制器和动作上下文等)。

然后我像这样在我的操作中使用 binder 属性来使用 binder

[HttpPut("{resource}"), ActionName("Create")]
public async Task<IActionResult> CreateResource(
   [ModelBinder(BinderType = typeof(BodyResourceModelBinder))] object model
)
{
   ...
}

通过在模型实例中执行.GetType(),我得到了我在活页夹代码中设置的实际类型。

最终的活页夹代码是这样的:

public class BodyResourceModelBinder: IModelBinder
{
    private readonly IList<IInputFormatter> _formatters;
    private readonly Func<Stream, Encoding, TextReader> _readerFactory = (s, e) => new StreamReader(s, e);
    private readonly ILogger _logger;

    public BodyResourceModelBinder(IOptions<MvcOptions> mvcOptions, ILogger<BodyResourceModelBinder> logger = null)
    {
        if(mvcOptions == null || mvcOptions.Value == null)
            throw new ArgumentNullException(nameof(mvcOptions));
        _formatters = mvcOptions.Value.InputFormatters.ToList();
        _logger = logger;
    }

    internal bool AllowEmptyBody { get; set; }

    public async Task BindModelAsync(ModelBindingContext bindingContext)
    {
        if(bindingContext == null)
        {
            throw new ArgumentNullException(nameof(bindingContext));
        }
        string modelBindingKey;
        if(bindingContext.IsTopLevelObject)
        {
            modelBindingKey = bindingContext.BinderModelName ?? string.Empty;
        }
        else
        {
            modelBindingKey = bindingContext.ModelName;
        }
        var httpContext = bindingContext.HttpContext;
        var formatterContext = new InputFormatterContext(
             httpContext,
             modelBindingKey,
             bindingContext.ModelState,

             // THIS IS THE ACTUAL CHANGE. I CREATE NEW METADATA BASED ON THE TYPE I WANT TO BIND TO
             bindingContext.ModelMetadata.GetMetadataForType(typeof(<PUT YOUR TYPE HERE>)),

             _readerFactory,
             AllowEmptyBody);

        var formatter = (IInputFormatter)null;
        for(var i = 0; i < _formatters.Count; i++)
        {
            if(_formatters[i].CanRead(formatterContext))
            {
                formatter = _formatters[i];
                break;
            }
        }

        if(formatter == null)
        {
            var message = $"Unsupported content type: {httpContext.Request.ContentType}";
            var exception = new UnsupportedContentTypeException(message);
            bindingContext.ModelState.AddModelError(modelBindingKey, exception, bindingContext.ModelMetadata);
            return;
        }

        try
        {
            var result = await formatter.ReadAsync(formatterContext);

            if(result.HasError)
                return;

            if(result.IsModelSet)
            {
                // The actual type of result.Model here is the type you provided in the Metadata of the IInputFormatter above
                var model = result.Model;
                bindingContext.Result = ModelBindingResult.Success(model);
            }
            else
            {
                var message = bindingContext
                     .ModelMetadata
                     .ModelBindingMessageProvider
                     .MissingRequestBodyRequiredValueAccessor();
                bindingContext.ModelState.AddModelError(modelBindingKey, message);
            }
        }
        catch(Exception exception) when(exception is InputFormatterException || ShouldHandleException(formatter))
        {
            bindingContext.ModelState.AddModelError(modelBindingKey, exception, bindingContext.ModelMetadata);
        }
    }

    private bool ShouldHandleException(IInputFormatter formatter)
    {
        // Any explicit policy on the formatters overrides the default.
        var policy = (formatter as IInputFormatterExceptionPolicy)?.ExceptionPolicy ??
             InputFormatterExceptionPolicy.MalformedInputExceptions;
        return policy == InputFormatterExceptionPolicy.AllExceptions;
    }
}

【讨论】:

    猜你喜欢
    • 2019-07-10
    • 2013-06-02
    • 2014-05-30
    • 2011-01-29
    • 2015-02-08
    • 1970-01-01
    • 2020-08-02
    • 1970-01-01
    • 2013-03-14
    相关资源
    最近更新 更多