0

I have a Post API with no json body parameters(https://localhost:443526/api/Home/notifications). I want to Overload the same endpoint(Post)with json body request.Is it possible any way.

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

[HttpPost("notifications")]
public async Task<IActionResult> NotificationCheckAsync([FromBody] NotificationRequest request)
{
    return Ok();
}
jamesnet214
  • 1,044
  • 13
  • 21
Ashwin Kumar
  • 61
  • 1
  • 11
  • 2
    Make the NotificationAsync(), [HttpGet] – juanvan Jun 14 '21 at 03:16
  • Can we make anyway possible with same method(Post) – Ashwin Kumar Jun 14 '21 at 03:19
  • What are the two different methods suppose to do? Generally the body would contain information you want to POST something with, i.e. add to a database. Without any supplied info, what do you want to do? – Timothy G. Jun 14 '21 at 03:22
  • I want to pass form-data request for one API and another with a json body request. – Ashwin Kumar Jun 14 '21 at 03:26
  • 1
    One route can not point to two different action methods. You might want to write a logic to collect data from from-data based on the content-type header value and whether `request` is null or not. – Chetan Jun 14 '21 at 04:12
  • 1
    No you can not, every endpoint refers to a specific action in your controller. – Saeed Aghdam Jun 14 '21 at 07:40
  • Does this answer your question? [Can you overload controller methods in ASP.NET MVC?](https://stackoverflow.com/questions/436866/can-you-overload-controller-methods-in-asp-net-mvc) – Rafael Biz Jun 14 '21 at 20:01

2 Answers2

0

You cannot, because what counts here is not the method signature, but the endpoint route. The routing engine should somehow map the request to /api/Home/notifications to a controller and method. POST body is not part of the route, so this decision if not possible. You cannot map one route to two methods.

More feasible approach would be an optional POST body parameter. This is possible either for the whole API (i.e. not for individual endpoints) by setting MvcOptions.AllowEmptyInputInBodyModelBinding, which may or may not be sufficient for you. Per endpoint support is planned in .net 5 preview 7, which will allow:

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

The feature is tracked here: https://github.com/dotnet/aspnetcore/issues/6878

With this you would be able to omit the post payload. But this is also not what you want, I guess, because you said in the comment you want to read the payload either from form or body parameter.

I personnaly think this is confusing for your clients and also not necessary in most cases.

However reading form data will anyway require model binding. You can read on it e.g. here: https://www.stevejgordon.co.uk/html-encode-string-aspnet-core-model-binding or search for IModelBinderProvider. I could imagine that model provider could first check for form data, and, if not found, check the POST data, because you have access to the request object.

 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;
            ...
         }
       
Maxim Zabolotskikh
  • 3,091
  • 20
  • 21
0

The short answer here is yes you can. In order to achieve this, you will need to use a discriminator value and play with ModelBinders. One way for not using versions on your API is through Content-Types. You can keep the same endpoint over time and add multiple Content-Types that allow you to extend your API without breaking the existing functionality already in place. Imagine your initial api only with one endpoint using application.json for example and later getting a new requirement to accept new functionality but keeping old integrations in place. In this case, you will use one Content-Type: application.json to access your method without parameters from the body and another Content-Type: application+body.json to access the other one. For the extra one, you will need to register it on the Startup so the request can read it from the headers. Then on your ModelBinder, you will decide to which model it binds. On top of that, you can implement an ActionConstraint to only hit the endpoint you need based on the Content-Type.

Below is an example with the WeatherForecast that is coming by default when you create a .Net Core API.

As you can see in the example below I am overloading the endpoint with 2 different Dtos, the model binder takes into consideration the Content-Types to bind the Dto to the correct model. ActionContraint restrict the request to hit the endpoint C# allows you to overload a method, but with the.NetCore routes you don't have that kind of flexibility using the same ActionMethods if they keep the same names you will get an exception. That's why you need to create 2 different methods for each of the Dtos that you are planning to pass. In your case, you have one method that is empty and the other one that receives a parameter, so that is an easier scenario.

On 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);
                    }
                })

Model Binder 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;
    }
}

Model Binder 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;
        }
    }

Controller methods:

/// <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);
    }

DTOs: WeatherForecastForCreationDto1 and WeatherForecastForCreationDto2

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

And here is your 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;
        }
    }
Zinov
  • 3,817
  • 5
  • 36
  • 70