Reputation: 668
I want to bind a Guid parameter to my ASP.NET MVC Core API:
[FromHeader] Guid id
but it's always null. If I change the parameter to a string and parse the Guid from the string manually it works, so I think it's not detecting Guid as a convertable type.
In the documentation it says
In MVC simple types are any .NET primitive type or type with a string type converter.
There is a type converter for Guids (GuidConverter) but maybe ASP.NET MVC Core doesn't know about it.
Does anyone know how to bind a Guid parameter with ASP.NET MVC Core or how to tell it to use GuidConverter?
Upvotes: 14
Views: 21252
Reputation: 2125
The easiest way to do this is to remove the attribute before your parameter of type Guid in your controller action like this:
public async Task<IActionResult> UpdateAsync(Guid ApplicantId, [FromBody]UpdateApplicantRequest request)
{}
Plain and simple, hope this helps.
Upvotes: 0
Reputation: 645
[UPDATE]
This has been improved in 2.1.0-preview2. Your code actually works now. You can bind a non-string type from header to your parameters. You only need to set compatibility version in your startup class.
Controller
[HttpGet]
public Task<JsonResult> Get([FromHeader] Guid id)
{
return new JsonResult(new {id});
}
Startup
Services
.AddMvc
.SetCompatibilityVersion(CompatibilityVersion.Version_2_1);
Look at the same Github discussion mentioned above: https://github.com/aspnet/Mvc/issues/5859
Upvotes: 3
Reputation: 498
I did it this way, which does not require an additional attribute on the controller action.
Model Binder
public class GuidHeaderModelBinder : IModelBinder
{
public async Task BindModelAsync(ModelBindingContext BindingContext)
{
// Read HTTP header.
string headerName = BindingContext.FieldName;
if (BindingContext.HttpContext.Request.Headers.ContainsKey(headerName))
{
StringValues headerValues = BindingContext.HttpContext.Request.Headers[headerName];
if (headerValues == StringValues.Empty)
{
// Value not found in HTTP header. Substitute empty GUID.
BindingContext.ModelState.SetModelValue(BindingContext.FieldName, headerValues, Guid.Empty.ToString());
BindingContext.Result = ModelBindingResult.Success(Guid.Empty);
}
else
{
// Value found in HTTP header.
string correlationIdText = headerValues[0];
BindingContext.ModelState.SetModelValue(BindingContext.FieldName, headerValues, correlationIdText);
// Parse GUID.
BindingContext.Result = Guid.TryParse(correlationIdText, out Guid correlationId)
? ModelBindingResult.Success(correlationId)
: ModelBindingResult.Failed();
}
}
else
{
// HTTP header not found.
BindingContext.Result = ModelBindingResult.Failed();
}
await Task.FromResult(default(object));
}
}
Model Binder Provider (verifies conditions where model binding will succeed)
public class GuidHeaderModelBinderProvider : IModelBinderProvider
{
public IModelBinder GetBinder(ModelBinderProviderContext Context)
{
if (Context.Metadata.ModelType == typeof(Guid))
{
if (Context.BindingInfo.BindingSource == BindingSource.Header)
{
return new BinderTypeModelBinder(typeof(GuidHeaderModelBinder));
}
}
return null;
}
}
FooBar Controller Action
[HttpGet("getbars")]
public async Task<string> GetBarsAsync([FromHeader] Guid CorrelationId, int Count)
{
Logger.Log(CorrelationId, $"Creating {Count} foo bars.");
StringBuilder stringBuilder = new StringBuilder();
for (int count = 0; count < Count; count++)
{
stringBuilder.Append("Bar! ");
}
return await Task.FromResult(stringBuilder.ToString());
}
Startup
// Add MVC and configure model binding.
Services.AddMvc(Options =>
{
Options.ModelBinderProviders.Insert(0, new GuidHeaderModelBinderProvider());
});
Upvotes: 0
Reputation: 34992
I have just found out that basically ASP Core only supports binding header values to strings and collections of strings! (whereas binding from route values, query string and body supports any complex type)
You can check the HeaderModelBinderProvider
source in Github and see for yourself:
public IModelBinder GetBinder(ModelBinderProviderContext context)
{
if (context == null)
{
throw new ArgumentNullException(nameof(context));
}
if (context.BindingInfo.BindingSource != null &&
context.BindingInfo.BindingSource.CanAcceptDataFrom(BindingSource.Header))
{
// We only support strings and collections of strings. Some cases can fail
// at runtime due to collections we can't modify.
if (context.Metadata.ModelType == typeof(string) ||
context.Metadata.ElementType == typeof(string))
{
return new HeaderModelBinder();
}
}
return null;
}
I have submitted a new issue, but in the meantime I would suggest you either bind to a string or create your own specific model binder (something that combines [FromHeader]
and [ModelBinder]
into your own binder)
Edit
The sample model binder could look like this:
public class GuidHeaderModelBinder : IModelBinder
{
public Task BindModelAsync(ModelBindingContext bindingContext)
{
if (bindingContext.ModelType != typeof(Guid)) return Task.CompletedTask;
if (!bindingContext.BindingSource.CanAcceptDataFrom(BindingSource.Header)) return Task.CompletedTask;
var headerName = bindingContext.ModelName;
var stringValue = bindingContext.HttpContext.Request.Headers[headerName];
bindingContext.ModelState.SetModelValue(bindingContext.ModelName, stringValue, stringValue);
// Attempt to parse the guid
if (Guid.TryParse(stringValue, out var valueAsGuid))
{
bindingContext.Result = ModelBindingResult.Success(valueAsGuid);
}
return Task.CompletedTask;
}
}
And this would be an example using it:
public IActionResult SampleAction(
[FromHeader(Name = "my-guid")][ModelBinder(BinderType = typeof(GuidHeaderModelBinder))]Guid foo)
{
return Json(new { foo });
}
Which you can try, for example with jquery in the browser:
$.ajax({
method: 'GET',
headers: { 'my-guid': '70e9dfda-4982-4b88-96f9-d7d284a10cb4' },
url: '/home/sampleaction'
});
Upvotes: 16