Fix for #1671 - Adding [FromHeader] attribute

This commit is contained in:
Ryan Nowak 2014-12-05 11:42:30 -08:00
parent d7094fd32d
commit 9468d741ee
7 changed files with 364 additions and 1 deletions

View File

@ -0,0 +1,19 @@
// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using Microsoft.AspNet.Mvc.ModelBinding;
namespace Microsoft.AspNet.Mvc
{
/// <summary>
/// <see cref="FromHeaderAttribute"/> can be placed on an action parameter or model property to indicate
/// that model binding should use a header value as the data source.
/// </summary>
[AttributeUsage(AttributeTargets.Parameter | AttributeTargets.Property, AllowMultiple = false, Inherited = true)]
public class FromHeaderAttribute : Attribute, IHeaderBinderMetadata, IModelNameProvider
{
/// <inheritdoc />
public string Name { get; set; }
}
}

View File

@ -0,0 +1,13 @@
// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
namespace Microsoft.AspNet.Mvc.ModelBinding
{
/// <summary>
/// Metadata interface that indicates model binding should use a header value for
/// the data source of a property or parameter.
/// </summary>
public interface IHeaderBinderMetadata : IBinderMetadata
{
}
}

View File

@ -0,0 +1,81 @@
// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Threading.Tasks;
namespace Microsoft.AspNet.Mvc.ModelBinding
{
public class HeaderModelBinder : MetadataAwareBinder<IHeaderBinderMetadata>
{
/// <inheritdoc />
protected override Task<bool> BindAsync(
[NotNull] ModelBindingContext bindingContext,
[NotNull] IHeaderBinderMetadata metadata)
{
var request = bindingContext.OperationBindingContext.HttpContext.Request;
if (bindingContext.ModelType == typeof(string))
{
var value = request.Headers.Get(bindingContext.ModelName);
bindingContext.Model = value;
return Task.FromResult(true);
}
else if (typeof(IEnumerable<string>).GetTypeInfo().IsAssignableFrom(
bindingContext.ModelType.GetTypeInfo()))
{
var values = request.Headers.GetCommaSeparatedValues(bindingContext.ModelName);
if (values != null)
{
bindingContext.Model = ConvertValuesToCollectionType(bindingContext.ModelType, values);
}
return Task.FromResult(true);
}
return Task.FromResult(false);
}
private object ConvertValuesToCollectionType(Type modelType, IList<string> values)
{
// There's a limited set of collection types we can support here.
//
// For the simple cases - choose a string[] or List<string> if the destination type supports
// it.
//
// For more complex cases, if the destination type is a class and implements ICollection<string>
// then activate it and add the values.
//
// Otherwise just give up.
if (typeof(List<string>).IsAssignableFrom(modelType))
{
return new List<string>(values);
}
else if (typeof(string[]).IsAssignableFrom(modelType))
{
return values.ToArray();
}
else if (
modelType.GetTypeInfo().IsClass &&
!modelType.GetTypeInfo().IsAbstract &&
typeof(ICollection<string>).IsAssignableFrom(modelType))
{
var result = (ICollection<string>)Activator.CreateInstance(modelType);
foreach (var value in values)
{
result.Add(value);
}
return result;
}
else
{
return null;
}
}
}
}

View File

@ -30,6 +30,7 @@ namespace Microsoft.AspNet.Mvc
options.ModelBinders.Add(typeof(BinderTypeBasedModelBinder));
options.ModelBinders.Add(typeof(ServicesModelBinder));
options.ModelBinders.Add(typeof(BodyModelBinder));
options.ModelBinders.Add(new HeaderModelBinder());
options.ModelBinders.Add(new TypeConverterModelBinder());
options.ModelBinders.Add(new TypeMatchModelBinder());
options.ModelBinders.Add(new CancellationTokenModelBinder());

View File

@ -0,0 +1,173 @@
// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Net;
using System.Net.Http;
using System.Threading.Tasks;
using Microsoft.AspNet.Builder;
using Microsoft.AspNet.TestHost;
using Xunit;
using Newtonsoft.Json;
namespace Microsoft.AspNet.Mvc.FunctionalTests
{
public class ModelBindingFromHeaderTest
{
private readonly IServiceProvider _services = TestHelper.CreateServices(nameof(ModelBindingWebSite));
private readonly Action<IApplicationBuilder> _app = new ModelBindingWebSite.Startup().Configure;
// The action that this test hits will echo back the model-bound value
[Theory]
[InlineData("transactionId", "1e331f25-0869-4c87-8a94-64e6e40cb5a0")]
[InlineData("TransaCtionId", "1e331f25-0869-4c87-8a94-64e6e40cb5a0")] // Case-Insensitive
[InlineData("TransaCtionId", "1e331f25-0869-4c87-8a94-64e6e40cb5a0,abcd")] // Binding to string doesn't split values
public async Task FromHeader_BindHeader_ToString_OnParameter(string headerName, string headerValue)
{
// Arrange
var expected = headerValue;
var server = TestServer.Create(_services, _app);
var client = server.CreateClient();
var request = new HttpRequestMessage(HttpMethod.Get, "http://localhost/Blog/BindToStringParameter");
request.Headers.TryAddWithoutValidation(headerName, headerValue);
// Act
var response = await client.SendAsync(request);
// Assert
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
var body = await response.Content.ReadAsStringAsync();
var result = JsonConvert.DeserializeObject<Result>(body);
Assert.Equal(expected, result.HeaderValue);
}
// The action that this test hits will echo back the model-bound value
[Fact]
public async Task FromHeader_BindHeader_ToString_OnParameter_CustomName()
{
// Arrange
var expected = "1e331f25-0869-4c87-8a94-64e6e40cb5a0";
var server = TestServer.Create(_services, _app);
var client = server.CreateClient();
var request = new HttpRequestMessage(HttpMethod.Get, "http://localhost/Blog/BindToStringParameter/CustomName");
request.Headers.TryAddWithoutValidation("tId", "1e331f25-0869-4c87-8a94-64e6e40cb5a0");
// Act
var response = await client.SendAsync(request);
// Assert
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
var body = await response.Content.ReadAsStringAsync();
var result = JsonConvert.DeserializeObject<Result>(body);
Assert.Equal(expected, result.HeaderValue);
Assert.Null(result.HeaderValues);
Assert.Empty(result.ModelStateErrors);
}
// The action that this test hits will echo back the model-state error
[Theory]
[InlineData("transactionId1234", "1e331f25-0869-4c87-8a94-64e6e40cb5a0")]
public async Task FromHeader_BindHeader_ToString_OnParameter_NoValues(string headerName, string headerValue)
{
// Arrange
var expected = headerValue;
var server = TestServer.Create(_services, _app);
var client = server.CreateClient();
var request = new HttpRequestMessage(HttpMethod.Get, "http://localhost/Blog/BindToStringParameter");
request.Headers.TryAddWithoutValidation(headerName, headerValue);
// Act
var response = await client.SendAsync(request);
// Assert
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
var body = await response.Content.ReadAsStringAsync();
var result = JsonConvert.DeserializeObject<Result>(body);
Assert.Null(result.HeaderValue);
Assert.Null(result.HeaderValues);
// This is a bug - the model state error key is wrong here.
var error = Assert.Single(result.ModelStateErrors);
Assert.Equal("transactionId.transactionId", error);
}
// The action that this test hits will echo back the model-bound values
[Theory]
[InlineData("transactionIds", "1e331f25-0869-4c87-8a94-64e6e40cb5a0")]
[InlineData("transactionIds", "1e331f25-0869-4c87-8a94-64e6e40cb5a0,abcd,efg")]
public async Task FromHeader_BindHeader_ToStringArray_OnParameter(string headerName, string headerValue)
{
// Arrange
var expected = headerValue.Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries);
var server = TestServer.Create(_services, _app);
var client = server.CreateClient();
var request = new HttpRequestMessage(HttpMethod.Get, "http://localhost/Blog/BindToStringArrayParameter");
request.Headers.TryAddWithoutValidation(headerName, headerValue);
// Act
var response = await client.SendAsync(request);
// Assert
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
var body = await response.Content.ReadAsStringAsync();
var result = JsonConvert.DeserializeObject<Result>(body);
Assert.Null(result.HeaderValue);
Assert.Equal<string>(expected, result.HeaderValues);
Assert.Empty(result.ModelStateErrors);
}
// The action that this test hits will echo back the model-bound values
[Fact]
public async Task FromHeader_BindHeader_ToModel()
{
// Arrange
var title = "How to make really really good soup.";
var tags = new string[] { "Cooking", "Recipes", "Awesome" };
var server = TestServer.Create(_services, _app);
var client = server.CreateClient();
var request = new HttpRequestMessage(HttpMethod.Get, "http://localhost/Blog/BindToModel?author=Marvin");
request.Headers.TryAddWithoutValidation("title", title);
request.Headers.TryAddWithoutValidation("tags", string.Join(", ", tags));
// Act
var response = await client.SendAsync(request);
// Assert
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
var body = await response.Content.ReadAsStringAsync();
var result = JsonConvert.DeserializeObject<Result>(body);
Assert.Equal(title, result.HeaderValue);
Assert.Equal<string>(tags, result.HeaderValues);
Assert.Empty(result.ModelStateErrors);
}
private class Result
{
public string HeaderValue { get; set; }
public string[] HeaderValues { get; set; }
public string[] ModelStateErrors { get; set; }
}
}
}

View File

@ -39,10 +39,11 @@ namespace Microsoft.AspNet.Mvc
// Assert
var i = 0;
Assert.Equal(10, mvcOptions.ModelBinders.Count);
Assert.Equal(11, mvcOptions.ModelBinders.Count);
Assert.Equal(typeof(BinderTypeBasedModelBinder), mvcOptions.ModelBinders[i++].OptionType);
Assert.Equal(typeof(ServicesModelBinder), mvcOptions.ModelBinders[i++].OptionType);
Assert.Equal(typeof(BodyModelBinder), mvcOptions.ModelBinders[i++].OptionType);
Assert.Equal(typeof(HeaderModelBinder), mvcOptions.ModelBinders[i++].OptionType);
Assert.Equal(typeof(TypeConverterModelBinder), mvcOptions.ModelBinders[i++].OptionType);
Assert.Equal(typeof(TypeMatchModelBinder), mvcOptions.ModelBinders[i++].OptionType);
Assert.Equal(typeof(CancellationTokenModelBinder), mvcOptions.ModelBinders[i++].OptionType);

View File

@ -0,0 +1,75 @@
// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System.Linq;
using Microsoft.AspNet.Mvc;
namespace ModelBindingWebSite.Controllers
{
[Route("Blog")]
public class FromHeader_BlogController : Controller
{
// Echo back the header value
[HttpGet("BindToStringParameter")]
public object BindToStringParameter([FromHeader] string transactionId)
{
return new Result()
{
HeaderValue = transactionId,
ModelStateErrors = ModelState.Where(kvp => kvp.Value.Errors.Count > 0).Select(kvp => kvp.Key).ToArray(),
};
}
// Echo back the header values
[HttpGet("BindToStringArrayParameter")]
public object BindToStringArrayParameter([FromHeader] string[] transactionIds)
{
return new Result()
{
HeaderValues = transactionIds,
ModelStateErrors = ModelState.Where(kvp => kvp.Value.Errors.Count > 0).Select(kvp => kvp.Key).ToArray(),
};
}
[HttpGet("BindToStringParameter/CustomName")]
public object BindToStringParameterWithCustomName([FromHeader(Name = "tId")] string transactionId)
{
return new Result()
{
HeaderValue = transactionId,
ModelStateErrors = ModelState.Where(kvp => kvp.Value.Errors.Count > 0).Select(kvp => kvp.Key).ToArray(),
};
}
[HttpGet("BindToModel")]
public object BindToModel(BlogPost blogPost)
{
return new Result()
{
HeaderValue = blogPost.Title,
HeaderValues = blogPost.Tags,
ModelStateErrors = ModelState.Where(kvp => kvp.Value.Errors.Count > 0).Select(kvp => kvp.Key).ToArray(),
};
}
private class Result
{
public string HeaderValue { get; set; }
public string[] HeaderValues { get; set; }
public string[] ModelStateErrors { get; set; }
}
public class BlogPost
{
[FromHeader]
public string Title { get; set; }
[FromHeader]
public string[] Tags { get; set; }
public string Author { get; set; }
}
}
}