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
}