Asp.net 不同模型类型的单一控制器动作取决于路线数据

Asp.net single controller action with different model type depending on route data

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

例如,我有一个带有 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、验证等)。我想做的唯一 不同 的事情是选择它应该绑定到哪个模型类型。其余保持不变。

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

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

[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(); 
        });

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

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

然后我像这样在我的操作中使用活页夹属性使用活页夹

[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;
    }
}