ghosttie
ghosttie

Reputation: 668

binding a Guid parameter in asp.net mvc core

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

Answers (4)

Zeeshan Adil
Zeeshan Adil

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

Ben
Ben

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

Erik Madsen
Erik Madsen

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

Daniel J.G.
Daniel J.G.

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

Related Questions