【问题标题】:Can we overload API with and without jsonbody我们可以在有和没有 jsonbody 的情况下重载 API
【发布时间】:2021-06-14 03:12:34
【问题描述】:

我有一个没有 json 正文参数的 Post API (https://localhost:443526/api/Home/notifications)。我想用 json 正文请求重载相同的端点(Post)。有没有可能。

[HttpPost("notifications")]
public async Task<IActionResult> NotificationAsync()
{
    return Ok();
}

[HttpPost("notifications")]
public async Task<IActionResult> NotificationCheckAsync([FromBody] NotificationRequest request)
{
    return Ok();
}

【问题讨论】:

  • 制作 NotificationAsync(), [HttpGet]
  • 我们能不能用同样的方法实现(发布)
  • 这两种不同的方法应该做什么?通常,正文将包含您想要发布的信息,即添加到数据库中。没有任何提供的信息,你想做什么?
  • 一个路由不能指向两种不同的动作方法。您可能想要编写一个逻辑来根据内容类型标头值以及request 是否为空来从数据中收集数据。
  • 不,你不能,每个端点都引用控制器中的特定操作。

标签: c# asp.net asp.net-web-api .net-core webapi


【解决方案1】:

你不能,因为这里重要的不是方法签名,而是端点路由。路由引擎应该以某种方式将请求映射到/api/Home/notifications 到控制器和方法。 POST body 不是路由的一部分,所以如果不可能,这个决定。您不能将一条路线映射到两种方法。

更可行的方法是可选的 POST 正文参数。通过设置MvcOptions.AllowEmptyInputInBodyModelBinding,这对于整个 API(即不适用于单个端点)都是可能的,这对您来说可能足够也可能不够。每个端点的支持计划在 .net 5 preview 7 中进行,这将允许:

public async Task<IActionResult> NotificationAsync([FromBody(EmptyBodyBehavior = EmptyBodyBehavior.Allow)] NotificationRequest request){
  ... null check logic ...
}

在此处跟踪该功能:https://github.com/dotnet/aspnetcore/issues/6878

这样您就可以省略 post 有效负载。但这也不是你想要的,我猜,因为你在评论中说你想从表单或正文参数中读取有效负载。

我个人认为这会让您的客户感到困惑,而且在大多数情况下也没有必要。

然而读取表单数据无论如何都需要模型绑定。您可以阅读它,例如此处:https://www.stevejgordon.co.uk/html-encode-string-aspnet-core-model-binding 或搜索 IModelBinderProvider。我可以想象模型提供者可以首先检查表单数据,如果没有找到,检查 POST 数据,因为您可以访问请求对象。

 public async Task BindModelAsync(ModelBindingContext bindingContext)
        {
            var notificationRequest = new NotificationRequest();
            // try to read form data
            var form = await bindingContext.HttpContext.Request.ReadFormAsync();
            // if did not succeed, read body
            var body = bindingContext.HttpContext.Request.Body;
            ...
         }
       

【讨论】:

    【解决方案2】:

    这里的简短回答是可以。为了实现这一点,您将需要使用鉴别器值并使用 ModelBinders。 在 API 上不使用版本的一种方法是通过 Content-Types。您可以随着时间的推移保持相同的端点并添加多个 Content-Type,从而允许您在不破坏现有功能的情况下扩展 API。想象一下,您的初始 api 仅具有一个使用 application.json 的端点,后来获得了接受新功能但保持旧集成到位的新要求。在这种情况下,您将使用一个 Content-Type: application.json 来访问您的方法,而无需来自正文的参数,而另一个 Content-Type: application+body.json 来访问另一个。对于额外的,您需要在 Startup 上注册它,以便请求可以从标头中读取它。然后在您的 ModelBinder 上,您将决定它绑定到哪个模型。最重要的是,您可以实现一个 ActionConstraint 以仅根据 Content-Type 访问您需要的端点。

    以下是创建 .Net Core API 时默认提供的 WeatherForecast 示例。

    正如您在下面的示例中看到的,我使用 2 个不同的 Dto 重载端点,模型绑定器考虑了 Content-Types 以将 Dto 绑定到正确的模型。 ActionContraint 限制请求命中端点 C# 允许您重载一个方法,但是使用 .NetCore 路由,如果它们保持相同的名称,您将没有使用相同的 ActionMethods 的那种灵活性,您将得到一个异常。这就是为什么您需要为您计划通过的每个 Dto 创建 2 种不同的方法。在您的情况下,您有一个为空的方法和另一个接收参数的方法,所以这是一个更简单的方案。

    Startup.cs

        public void ConfigureServices(IServiceCollection services)
        {
     var iMvcBuilder = services.AddControllers(mvcOptions =>
                    {
                        //mvcOptions.EnableEndpointRouting = false;
                        mvcOptions.ModelBinderProviders.Insert(0, new WeatherForecastForCreationModelBinderProvider());
    
                                            var jsonOutputFormatter = mvcOptions.OutputFormatters.OfType<SystemTextJsonOutputFormatter>()
                            .FirstOrDefault();
    
                        //TODO: test to remove this, no need to have this if you put the correct Produces or Consumes attributes
                        if (jsonOutputFormatter != null)
                        {
                            jsonOutputFormatter.SupportedMediaTypes.Add("application/vnd.weatherforecast.type1.json");
                            jsonOutputFormatter.SupportedMediaTypes.Add("application/vnd.weatherforecast.type2.json);
                        }
                    })
    

    模型绑定器WeatherForecastForCreationModelBinderProvider

    public class WeatherForecastForCreationModelBinderProvider : IModelBinderProvider
    {
        public IModelBinder GetBinder(ModelBinderProviderContext context)
        {
            if (context.Metadata.ModelType == typeof(WeatherForecastForCreationDto1) || context.Metadata.ModelType == typeof(WeatherForecastForCreationDto2))
            {
                return new WeatherForecastForCreationModelBinder();
            }
    
            return null;
        }
    }
    

    模型绑定器WeatherForecastForCreationModelBinder

     public class WeatherForecastForCreationModelBinder : IModelBinder
        {
            public Task BindModelAsync(ModelBindingContext bindingContext)
            {
                if (bindingContext == null)
                    throw new ArgumentNullException(nameof(bindingContext));
    
                string valueFromBody = string.Empty;
    
                using (var sr = new StreamReader(bindingContext.HttpContext.Request.Body))
                {
                    valueFromBody = sr.ReadToEndAsync().GetAwaiter().GetResult();
                }
    
                if (string.IsNullOrEmpty(valueFromBody))
                {
                    return Task.CompletedTask;
                }
    
                string contentType = bindingContext.HttpContext.Request.Headers.FirstOrDefault(x => x.Key == HeaderNames.ContentType).Value;
                dynamic model = null;
    
                var isDeserializingOk = true;
    
                switch (contentType)
                {
                    case "application/vnd.weatherforecast.type1.json":
                        try
                        {
                            model = JsonConvert.DeserializeObject<WeatherForecastForCreationDto1>(valueFromBody);
                        }
                        catch
                        {
                            isDeserializingOk = false;
                        }
                        break;
                    case "application/vnd.weatherforecast.type2.json":
                        try
                        {
                            model = JsonConvert.DeserializeObject<WeatherForecastForCreationDto2>(valueFromBody);
                        }
                        catch
                        {
                            isDeserializingOk = false;
                        }
                        break;
                }
    
                if (!isDeserializingOk)
                {
                    bindingContext.Result = ModelBindingResult.Failed();
    
                    return Task.CompletedTask;
                }
    
                bindingContext.Result = ModelBindingResult.Success(model);
    
                return Task.CompletedTask;
            }
        }
    

    控制器方法:

    /// <summary>
        /// Create Weather Forecast
        /// </summary>
        /// <param name="weatherForecast"></param>
        /// <returns></returns>
        [HttpPost(Name = "CreateWeatherForecast")]
        [ProducesResponseType(201)]
        [Consumes(HttpMediaTypes.WeatherForecastType1)]
        [RequestHeaderMatchesMediaType("Content-Type", new[] { HttpMediaTypes.WeatherForecastType1 })]
        public async Task<IActionResult> CreateWeatherForecast1(
            [FromBody] WeatherForecastForCreationDto1 weatherForecastForCreationDto1,
            [FromServices] IWeatherForcastService weatherForcastService,
            [FromServices] IMapper mapper)
        {
            if (!ModelState.IsValid)
            {
                return ValidationFailureRequest();
            }
    
            var entity = await weatherForcastService.CreateSummary1Async(weatherForecastForCreationDto1);
            var outputDto = mapper.Map<WeatherForecastDto1>(entity);
    
            return StatusCode(StatusCodes.Status201Created, outputDto);
        }
    
        [HttpPost]
        [ProducesResponseType(201)]
        [Consumes(HttpMediaTypes.WeatherForecastType2)]
        [RequestHeaderMatchesMediaType("Content-Type", new[] { HttpMediaTypes.WeatherForecastType2 })]
        public async Task<IActionResult> CreateWeatherForecast2(
            [FromBody] WeatherForecastForCreationDto2 weatherForecastForCreationDto2,
            [FromServices] IWeatherForcastService weatherForcastService,
            [FromServices] IMapper mapper)
        {
            if (!ModelState.IsValid)
            {
                return ValidationFailureRequest();
            }
    
            var entity = await weatherForcastService.CreateSummary2Async(weatherForecastForCreationDto2);
            var outputDto = mapper.Map<WeatherForecastDto2>(entity);
    
            return StatusCode(StatusCodes.Status201Created, outputDto);
        }
    

    DTO:WeatherForecastForCreationDto1WeatherForecastForCreationDto2

     public class WeatherForecastForCreationDto1
        {
            /// <summary>
            /// Date
            /// </summary>
            /// <example>
            /// <code>
            /// DateTime.Now;
            /// </code>
            /// </example>
            public DateTime Date { get; set; }
            /// <summary>
            /// Temperature C
            /// </summary>
            /// <example>
            /// 14
            /// </example>
            public int TemperatureC { get; set; }
            /// <summary>
            /// Summary
            /// </summary>
            /// <example>
            /// Scorching
            /// </example>
            public string  Summary { get; set; }
        }
    
    public class WeatherForecastForCreationDto2
        {
            /// <summary>
            /// Date
            /// </summary>
            /// <example>
            /// <code>
            /// DateTime.Now;
            /// </code>
            /// </example>
            public DateTime Date { get; set; }
            /// <summary>
            /// Temperature C
            /// </summary>
            /// <example>
            /// 35
            /// </example>
            public int TemperatureC { get; set; }
        }
    

    这是你的 ActionContraint RequestHeaderMatchesMediaTypeAttribute

    [AttributeUsage(AttributeTargets.All, Inherited = true, AllowMultiple = true)]
        public class RequestHeaderMatchesMediaTypeAttribute : Attribute, IActionConstraint
        {
            private readonly string[] _mediaTypes;
            private readonly string _requestHeaderToMatch;
    
            public RequestHeaderMatchesMediaTypeAttribute(string requestHeaderToMatch,
                string[] mediaTypes)
            {
                _requestHeaderToMatch = requestHeaderToMatch;
                _mediaTypes = mediaTypes;
            }
    
            public int Order => 0;
    
            public string RequestHeaderToMatch => _requestHeaderToMatch;
            public string[] MediaTypes => _mediaTypes;
    
            public bool Accept(ActionConstraintContext context)
            {
                var requestHeaders = context.RouteContext.HttpContext.Request.Headers;
    
                if (!requestHeaders.ContainsKey(_requestHeaderToMatch))
                {
                    return false;
                }
    
                // if one of the media types matches, return true
                foreach (var mediaType in _mediaTypes)
                {
                    var mediaTypeMatches = string.Equals(requestHeaders[_requestHeaderToMatch].ToString(),
                        mediaType, StringComparison.OrdinalIgnoreCase);
    
                    if (mediaTypeMatches)
                    {
                        return true;
                    }
                }
    
                return false;
            }
        }
    

    【讨论】:

      猜你喜欢
      • 2017-04-05
      • 2013-06-24
      • 2011-11-30
      • 1970-01-01
      • 2012-04-08
      • 2021-08-09
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多