Fix for #1671 - Adding [FromHeader] attribute
This commit is contained in:
parent
d7094fd32d
commit
9468d741ee
|
|
@ -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; }
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
{
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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());
|
||||
|
|
|
|||
|
|
@ -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; }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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; }
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue