diff --git a/src/Microsoft.AspNet.Mvc.ModelBinding/BinderMetadata/FromHeaderAttribute.cs b/src/Microsoft.AspNet.Mvc.ModelBinding/BinderMetadata/FromHeaderAttribute.cs
new file mode 100644
index 0000000000..b77d402fad
--- /dev/null
+++ b/src/Microsoft.AspNet.Mvc.ModelBinding/BinderMetadata/FromHeaderAttribute.cs
@@ -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
+{
+ ///
+ /// can be placed on an action parameter or model property to indicate
+ /// that model binding should use a header value as the data source.
+ ///
+ [AttributeUsage(AttributeTargets.Parameter | AttributeTargets.Property, AllowMultiple = false, Inherited = true)]
+ public class FromHeaderAttribute : Attribute, IHeaderBinderMetadata, IModelNameProvider
+ {
+ ///
+ public string Name { get; set; }
+ }
+}
diff --git a/src/Microsoft.AspNet.Mvc.ModelBinding/BinderMetadata/IHeaderBinderMetadata.cs b/src/Microsoft.AspNet.Mvc.ModelBinding/BinderMetadata/IHeaderBinderMetadata.cs
new file mode 100644
index 0000000000..6954375006
--- /dev/null
+++ b/src/Microsoft.AspNet.Mvc.ModelBinding/BinderMetadata/IHeaderBinderMetadata.cs
@@ -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
+{
+ ///
+ /// Metadata interface that indicates model binding should use a header value for
+ /// the data source of a property or parameter.
+ ///
+ public interface IHeaderBinderMetadata : IBinderMetadata
+ {
+ }
+}
\ No newline at end of file
diff --git a/src/Microsoft.AspNet.Mvc.ModelBinding/Binders/HeaderModelBinder.cs b/src/Microsoft.AspNet.Mvc.ModelBinding/Binders/HeaderModelBinder.cs
new file mode 100644
index 0000000000..7c1ec4c745
--- /dev/null
+++ b/src/Microsoft.AspNet.Mvc.ModelBinding/Binders/HeaderModelBinder.cs
@@ -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
+ {
+ ///
+ protected override Task 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).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 values)
+ {
+ // There's a limited set of collection types we can support here.
+ //
+ // For the simple cases - choose a string[] or List if the destination type supports
+ // it.
+ //
+ // For more complex cases, if the destination type is a class and implements ICollection
+ // then activate it and add the values.
+ //
+ // Otherwise just give up.
+ if (typeof(List).IsAssignableFrom(modelType))
+ {
+ return new List(values);
+ }
+ else if (typeof(string[]).IsAssignableFrom(modelType))
+ {
+ return values.ToArray();
+ }
+ else if (
+ modelType.GetTypeInfo().IsClass &&
+ !modelType.GetTypeInfo().IsAbstract &&
+ typeof(ICollection).IsAssignableFrom(modelType))
+ {
+ var result = (ICollection)Activator.CreateInstance(modelType);
+ foreach (var value in values)
+ {
+ result.Add(value);
+ }
+
+ return result;
+ }
+ else
+ {
+ return null;
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Microsoft.AspNet.Mvc/MvcOptionsSetup.cs b/src/Microsoft.AspNet.Mvc/MvcOptionsSetup.cs
index dc5653e263..e9e1b1bf6e 100644
--- a/src/Microsoft.AspNet.Mvc/MvcOptionsSetup.cs
+++ b/src/Microsoft.AspNet.Mvc/MvcOptionsSetup.cs
@@ -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());
diff --git a/test/Microsoft.AspNet.Mvc.FunctionalTests/ModelBindingFromHeaderTest.cs b/test/Microsoft.AspNet.Mvc.FunctionalTests/ModelBindingFromHeaderTest.cs
new file mode 100644
index 0000000000..d578524d07
--- /dev/null
+++ b/test/Microsoft.AspNet.Mvc.FunctionalTests/ModelBindingFromHeaderTest.cs
@@ -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 _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(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(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(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(body);
+
+ Assert.Null(result.HeaderValue);
+ Assert.Equal(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(body);
+
+ Assert.Equal(title, result.HeaderValue);
+ Assert.Equal(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; }
+ }
+ }
+}
\ No newline at end of file
diff --git a/test/Microsoft.AspNet.Mvc.Test/MvcOptionSetupTest.cs b/test/Microsoft.AspNet.Mvc.Test/MvcOptionSetupTest.cs
index 694180aee6..29a7d58c29 100644
--- a/test/Microsoft.AspNet.Mvc.Test/MvcOptionSetupTest.cs
+++ b/test/Microsoft.AspNet.Mvc.Test/MvcOptionSetupTest.cs
@@ -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);
diff --git a/test/WebSites/ModelBindingWebSite/Controllers/FromHeader_BlogController.cs b/test/WebSites/ModelBindingWebSite/Controllers/FromHeader_BlogController.cs
new file mode 100644
index 0000000000..4eb67c6a15
--- /dev/null
+++ b/test/WebSites/ModelBindingWebSite/Controllers/FromHeader_BlogController.cs
@@ -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; }
+ }
+ }
+}
\ No newline at end of file