From e805e67b4c96dd619daab4ff57f5bea2d0b5c3f9 Mon Sep 17 00:00:00 2001 From: Harsh Gupta Date: Fri, 30 Jan 2015 14:36:32 -0800 Subject: [PATCH] [Fix for #1929] HeaderModelBinder needs to honor explicit name. --- .../Binders/HeaderModelBinder.cs | 25 ++--- .../ModelBindingFromHeaderTest.cs | 51 ++++++++++ .../Binders/HeaderModelBinderTests.cs | 98 +++++++++++++++++++ .../Controllers/FromHeader_BlogController.cs | 24 +++++ 4 files changed, 187 insertions(+), 11 deletions(-) create mode 100644 test/Microsoft.AspNet.Mvc.ModelBinding.Test/Binders/HeaderModelBinderTests.cs diff --git a/src/Microsoft.AspNet.Mvc.ModelBinding/Binders/HeaderModelBinder.cs b/src/Microsoft.AspNet.Mvc.ModelBinding/Binders/HeaderModelBinder.cs index 60725aeaf7..24c8a6dff5 100644 --- a/src/Microsoft.AspNet.Mvc.ModelBinding/Binders/HeaderModelBinder.cs +++ b/src/Microsoft.AspNet.Mvc.ModelBinding/Binders/HeaderModelBinder.cs @@ -1,47 +1,50 @@ // 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; using Microsoft.AspNet.Mvc.ModelBinding.Internal; namespace Microsoft.AspNet.Mvc.ModelBinding { + /// + /// A which uses + /// to bind the model. + /// public class HeaderModelBinder : MetadataAwareBinder { /// protected override Task BindAsync( - [NotNull] ModelBindingContext bindingContext, + [NotNull] ModelBindingContext bindingContext, [NotNull] IHeaderBinderMetadata metadata) { var request = bindingContext.OperationBindingContext.HttpContext.Request; + var modelMetadata = bindingContext.ModelMetadata; + // Property name can be null if the model metadata represents a type (rahter than a property or parameter). + var headerName = modelMetadata.BinderModelName ?? modelMetadata.PropertyName ?? bindingContext.ModelName; if (bindingContext.ModelType == typeof(string)) { - var value = request.Headers.Get(bindingContext.ModelName); + var value = request.Headers.Get(headerName); if (value != null) { bindingContext.Model = value; } - - return Task.FromResult(true); } else if (typeof(IEnumerable).GetTypeInfo().IsAssignableFrom( bindingContext.ModelType.GetTypeInfo())) { - var values = request.Headers.GetCommaSeparatedValues(bindingContext.ModelName); + var values = request.Headers.GetCommaSeparatedValues(headerName); if (values != null) { - bindingContext.Model = ModelBindingHelper.ConvertValuesToCollectionType(bindingContext.ModelType, values); + bindingContext.Model = + ModelBindingHelper.ConvertValuesToCollectionType(bindingContext.ModelType, values); } - - return Task.FromResult(true); } - return Task.FromResult(false); + // Always return true as header model binder is supposed to always handle IHeaderBinderMetadata. + return Task.FromResult(true); } } } \ No newline at end of file diff --git a/test/Microsoft.AspNet.Mvc.FunctionalTests/ModelBindingFromHeaderTest.cs b/test/Microsoft.AspNet.Mvc.FunctionalTests/ModelBindingFromHeaderTest.cs index 6c2102562c..d68e1ddfcc 100644 --- a/test/Microsoft.AspNet.Mvc.FunctionalTests/ModelBindingFromHeaderTest.cs +++ b/test/Microsoft.AspNet.Mvc.FunctionalTests/ModelBindingFromHeaderTest.cs @@ -44,6 +44,57 @@ namespace Microsoft.AspNet.Mvc.FunctionalTests Assert.Equal(expected, result.HeaderValue); } + [Fact] + public async Task FromHeader_BindHeader_ToString_OnProperty_CustomName() + { + // 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/BindToProperty/CustomName"); + request.Headers.TryAddWithoutValidation("BlogTitle", title); + request.Headers.TryAddWithoutValidation("BlogTags", 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); + } + + [Fact] + public async Task FromHeader_NonExistingHeaderAddsValidationErrors_OnProperty_CustomName() + { + // Arrange + 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/BindToProperty/CustomName"); + request.Headers.TryAddWithoutValidation("BlogTags", 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(tags, result.HeaderValues); + var error = Assert.Single(result.ModelStateErrors); + Assert.Equal("Title", error); + } // The action that this test hits will echo back the model-bound value [Fact] public async Task FromHeader_BindHeader_ToString_OnParameter_CustomName() diff --git a/test/Microsoft.AspNet.Mvc.ModelBinding.Test/Binders/HeaderModelBinderTests.cs b/test/Microsoft.AspNet.Mvc.ModelBinding.Test/Binders/HeaderModelBinderTests.cs new file mode 100644 index 0000000000..baa92719bf --- /dev/null +++ b/test/Microsoft.AspNet.Mvc.ModelBinding.Test/Binders/HeaderModelBinderTests.cs @@ -0,0 +1,98 @@ +// 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.Threading.Tasks; +using Microsoft.AspNet.Http.Core; +using Xunit; + +namespace Microsoft.AspNet.Mvc.ModelBinding.Test +{ + public class HeaderModelBinderTests + { + [Theory] + [InlineData(typeof(string))] + [InlineData(typeof(string[]))] + [InlineData(typeof(object))] + [InlineData(typeof(int))] + [InlineData(typeof(int[]))] + [InlineData(typeof(TestFromHeader))] + public async Task BindModelAsync_ReturnsTrue_ForAllTypes(Type type) + { + // Arrange + var binder = new HeaderModelBinder(); + var modelBindingContext = GetBindingContext(type); + + // Act + var result = await binder.BindModelAsync(modelBindingContext); + + // Assert + Assert.True(result); + } + + [Fact] + public async Task HeaderBinder_BindsHeaders_ToStringCollection() + { + // Arrange + var type = typeof(string[]); + var header = "Accept"; + var headerValue = "application/json,text/json"; + var binder = new HeaderModelBinder(); + var modelBindingContext = GetBindingContext(type); + + modelBindingContext.ModelName = header; + modelBindingContext.OperationBindingContext.HttpContext.Request.Headers.Add(header, new[] { headerValue }); + + // Act + var result = await binder.BindModelAsync(modelBindingContext); + + // Assert + Assert.True(result); + Assert.Equal(headerValue.Split(','), modelBindingContext.Model); + } + + [Fact] + public async Task HeaderBinder_BindsHeaders_ToStringType() + { + // Arrange + var type = typeof(string); + var header = "User-Agent"; + var headerValue = "UnitTest"; + var binder = new HeaderModelBinder(); + var modelBindingContext = GetBindingContext(type); + + modelBindingContext.ModelName = header; + modelBindingContext.OperationBindingContext.HttpContext.Request.Headers.Add(header, new[] { headerValue }); + + // Act + var result = await binder.BindModelAsync(modelBindingContext); + + // Assert + Assert.True(result); + Assert.Equal(headerValue, modelBindingContext.Model); + } + + private static ModelBindingContext GetBindingContext(Type modelType) + { + var metadataProvider = new EmptyModelMetadataProvider(); + var bindingContext = new ModelBindingContext + { + ModelMetadata = metadataProvider.GetMetadataForType(null, modelType), + ModelName = "modelName", + OperationBindingContext = new OperationBindingContext + { + ModelBinder = new HeaderModelBinder(), + MetadataProvider = metadataProvider, + HttpContext = new DefaultHttpContext() + } + }; + + bindingContext.ModelMetadata.BinderMetadata = new TestFromHeader(); + return bindingContext; + } + + public class TestFromHeader : IHeaderBinderMetadata + { + } + } +} \ No newline at end of file diff --git a/test/WebSites/ModelBindingWebSite/Controllers/FromHeader_BlogController.cs b/test/WebSites/ModelBindingWebSite/Controllers/FromHeader_BlogController.cs index 3a5bdae409..ac89820650 100644 --- a/test/WebSites/ModelBindingWebSite/Controllers/FromHeader_BlogController.cs +++ b/test/WebSites/ModelBindingWebSite/Controllers/FromHeader_BlogController.cs @@ -1,6 +1,7 @@ // 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.Collections.Generic; using System.ComponentModel; using System.ComponentModel.DataAnnotations; using System.Linq; @@ -11,6 +12,17 @@ namespace ModelBindingWebSite.Controllers [Route("Blog")] public class FromHeader_BlogController : Controller { + [HttpGet("BindToProperty/CustomName")] + public object BindToProperty(BlogWithHeaderOnProperty blogWithHeader) + { + return new Result() + { + HeaderValue = blogWithHeader.Title, + HeaderValues = blogWithHeader.Tags, + ModelStateErrors = ModelState.Where(kvp => kvp.Value.Errors.Count > 0).Select(kvp => kvp.Key).ToArray(), + }; + } + // Echo back the header value [HttpGet("BindToStringParameter")] public object BindToStringParameter([FromHeader] string transactionId) @@ -109,6 +121,18 @@ namespace ModelBindingWebSite.Controllers public string Author { get; set; } } + public class BlogWithHeaderOnProperty + { + [FromHeader(Name = "BlogTitle")] + [Required] + public string Title { get; set; } + + [FromHeader(Name = "BlogTags")] + public string[] Tags { get; set; } + + public string Author { get; set; } + } + public class BlogPostWithInitializedValue { [Required]