Home > OS >  Is it possible to extract ampersand-separated keyless url parameters into action method parameters i
Is it possible to extract ampersand-separated keyless url parameters into action method parameters i

Time:11-02

An external service is calling my .NET Core API with a URL in the format mysite.com/date/2021&4&12. I cannot control how it calls my API, so I must accommodate that URL format.

I can solve this in my controller action method with

[HttpGet("date/{dateString}")]
public Thing GetThingByDate(string dateString)
{
  //...string manipulation on dateString
}

But is it possible to have .NET Core do the work or even to write something myself so I can get

[HttpGet("date/((some kind of formatting here))")]
public Thing GetThingByDate(int year, int month, int day)
{
}

CodePudding user response:

Yes this is possible. One approach to make this reusable is to create a custom model and a model binder for this.

First here's my naive controller implementation:

[Route("api/[controller]")]
[ApiController]
public class ValuesController : ControllerBase
{

    [HttpGet("{**slug}")]
    public IActionResult Get(MyDateModel slug)
    {
        return Ok(slug);
    }

}

[ModelBinder(BinderType = typeof(MyDateModelBinder))]
public class MyDateModel
{
    public int Year { get; set; }
    public int Month { get; set; }
    public int Day { get; set; }
}

The {**slug} is a catch-all attribute, so everything, that has not yet been bound is thrown into a variable named 'slug'. In our case "2021&4&12"

We also create a new view model with our, year, month and day properties and bind it to our yet-to-create model binder.

Now where all the magic happens. Microsoft provides a good example as a starting point at https://docs.microsoft.com/en-us/aspnet/core/mvc/advanced/custom-model-binding?view=aspnetcore-5.0. We are going to base our work an this:

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

        var modelName = bindingContext.ModelName;

        // Try to fetch the value of the argument by name
        var valueProviderResult = bindingContext.ValueProvider.GetValue(modelName);

        if (valueProviderResult == ValueProviderResult.None)
        {
            return Task.CompletedTask;
        }

        bindingContext.ModelState.SetModelValue(modelName, valueProviderResult);

        var rawValue = valueProviderResult.FirstValue;

        // Check if the argument value is null or empty
        if (string.IsNullOrEmpty(rawValue))
        {
            return Task.CompletedTask;
        }

        // parse the string to the target value
        try
        {
            var values = rawValue.Split("&");
            var model = new MyDateModel
            {
                Year = Int32.Parse(values[0]),
                Month = Int32.Parse(values[1]),
                Day = Int32.Parse(values[2]),
            };

            bindingContext.Result = ModelBindingResult.Success(model);
        }
        catch (Exception ex)
        {
            bindingContext.ModelState.TryAddModelError(
                modelName, ex.Message);

        }
        return Task.CompletedTask;
    }
}

I'm not going to go into too much detail, at to what happens at which line, as you can set a breakpoint and get a good Eureka! - moment your self. Please be aware, that this rather naive implementation is missing quiet a lot of robustness and error handling - so if your string is in a wrong format or has a wrong delimiter, it wont work and your error messages might not be suitable for clients.

In our example now, I've issed the request https://localhost:5001/api/values/2021&4&12 and it returned the following values as JSON:

{
    "year": 2021,
    "month": 4,
    "day": 12
}
  • Related