[Fix for #1929] HeaderModelBinder needs to honor explicit name.

This commit is contained in:
Harsh Gupta 2015-01-30 14:36:32 -08:00
parent 2c3148fd25
commit e805e67b4c
4 changed files with 187 additions and 11 deletions

View File

@ -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
{
/// <summary>
/// A <see cref="MetadataAwareBinder{IHeaderBinderMetadata}"/> which uses <see cref="Http.HttpRequest.Headers"/>
/// to bind the model.
/// </summary>
public class HeaderModelBinder : MetadataAwareBinder<IHeaderBinderMetadata>
{
/// <inheritdoc />
protected override Task<bool> 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<string>).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);
}
}
}

View File

@ -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<Result>(body);
Assert.Equal(title, result.HeaderValue);
Assert.Equal<string>(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<Result>(body);
Assert.Equal<string>(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()

View File

@ -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
{
}
}
}

View File

@ -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]