Incorporating PR comments - 1/19

This commit is contained in:
Mugdha Kulkarni 2015-01-19 13:41:35 -08:00
parent c8b911b596
commit 7ee80020e1
15 changed files with 117 additions and 34 deletions

View File

@ -98,6 +98,7 @@ namespace MvcSample.Web
services.AddSingleton<PassThroughAttribute>(); services.AddSingleton<PassThroughAttribute>();
services.AddSingleton<UserNameService>(); services.AddSingleton<UserNameService>();
services.AddTransient<ITestService, TestService>(); services.AddTransient<ITestService, TestService>();
// Setup services with a test AssemblyProvider so that only the // Setup services with a test AssemblyProvider so that only the
// sample's assemblies are loaded. This prevents loading controllers from other assemblies // sample's assemblies are loaded. This prevents loading controllers from other assemblies
// when the sample is used in the Functional Tests. // when the sample is used in the Functional Tests.

View File

@ -13,8 +13,8 @@ using System.Collections.Generic;
namespace Microsoft.AspNet.Mvc namespace Microsoft.AspNet.Mvc
{ {
/// <summary> /// <summary>
/// This will look at the format parameter if present in the route data or query data and /// This will look at the format parameter if present in the route data or query data and sets the content type in
/// sets the content type in ObjectResult corresponding to the format value. /// ObjectResult corresponding to the format value.
/// </summary> /// </summary>
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false, Inherited = true)] [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false, Inherited = true)]
public class FormatFilterAttribute : Attribute, IFormatFilter, IResourceFilter, IResultFilter public class FormatFilterAttribute : Attribute, IFormatFilter, IResourceFilter, IResultFilter
@ -22,7 +22,7 @@ namespace Microsoft.AspNet.Mvc
/// <summary> /// <summary>
/// As a resourceFilter, this filter looks at the request and rejects it /// As a resourceFilter, this filter looks at the request and rejects it
/// before going ahead if /// before going ahead if
/// 1. The Format in the request doesnt match any format in the map. /// 1. The format in the request doesnt match any format in the map.
/// 2. If there is a conflicting producesFilter. /// 2. If there is a conflicting producesFilter.
/// </summary> /// </summary>
/// <param name="context"></param> /// <param name="context"></param>
@ -30,7 +30,7 @@ namespace Microsoft.AspNet.Mvc
{ {
var format = GetFormat(context); var format = GetFormat(context);
if (format != null && !string.IsNullOrEmpty(format.ToString())) if (!string.IsNullOrEmpty(format))
{ {
var formatContentType = GetContentType(format, context); var formatContentType = GetContentType(format, context);
if (formatContentType == null) if (formatContentType == null)
@ -44,6 +44,7 @@ namespace Microsoft.AspNet.Mvc
if (responseTypeFilters.Count() != 0) if (responseTypeFilters.Count() != 0)
{ {
var contentTypes = new List<MediaTypeHeaderValue>(); var contentTypes = new List<MediaTypeHeaderValue>();
foreach (var filter in responseTypeFilters) foreach (var filter in responseTypeFilters)
{ {
filter.SetContentTypes(contentTypes); filter.SetContentTypes(contentTypes);
@ -58,11 +59,15 @@ namespace Microsoft.AspNet.Mvc
} }
} }
/// <inheritdoc />
public void OnResourceExecuted([NotNull] ResourceExecutedContext context) public void OnResourceExecuted([NotNull] ResourceExecutedContext context)
{ {
} }
/// <summary>
/// This method looks at the format that request has and sets it in the context
/// </summary>
public void OnResultExecuting([NotNull] ResultExecutingContext context) public void OnResultExecuting([NotNull] ResultExecutingContext context)
{ {
var format = GetFormat(context); var format = GetFormat(context);
@ -80,11 +85,18 @@ namespace Microsoft.AspNet.Mvc
} }
} }
/// <inheritdoc />
public void OnResultExecuted([NotNull] ResultExecutedContext context) public void OnResultExecuted([NotNull] ResultExecutedContext context)
{ {
} }
/// <summary>
/// Looks at the current request for the format parameter. If it contains format, it returns the content type
/// for it.
/// </summary>
/// <param name="context"></param>
/// <returns></returns>
public MediaTypeHeaderValue GetContentTypeForCurrentRequest(FilterContext context) public MediaTypeHeaderValue GetContentTypeForCurrentRequest(FilterContext context)
{ {
var format = GetFormat(context); var format = GetFormat(context);
@ -96,7 +108,7 @@ namespace Microsoft.AspNet.Mvc
return null; return null;
} }
private object GetFormat(FilterContext context) private string GetFormat(FilterContext context)
{ {
object format = null; object format = null;
@ -105,17 +117,22 @@ namespace Microsoft.AspNet.Mvc
if (context.HttpContext.Request.Query.ContainsKey("format")) if (context.HttpContext.Request.Query.ContainsKey("format"))
{ {
format = context.HttpContext.Request.Query.Get("format"); format = context.HttpContext.Request.Query.Get("format");
return format.ToString();
} }
} }
else
{
return format.ToString();
}
return format; return null;
} }
private MediaTypeHeaderValue GetContentType(object format, FilterContext context) private MediaTypeHeaderValue GetContentType(object format, FilterContext context)
{ {
Debug.Assert(format != null); Debug.Assert(format != null);
var options = context.HttpContext.RequestServices.GetService<IOptions<MvcOptions>>(); var options = context.HttpContext.RequestServices.GetRequiredService<IOptions<MvcOptions>>();
var contentType = options.Options.FormatterMappings.GetContentTypeForFormat(format.ToString()); var contentType = options.Options.FormatterMappings.GetMediaTypeForFormat(format.ToString());
return contentType; return contentType;
} }
} }

View File

@ -7,10 +7,14 @@ using System;
namespace Microsoft.AspNet.Mvc namespace Microsoft.AspNet.Mvc
{ {
/// <summary> /// <summary>
/// Implement this interface if you want to have your own implementation of FormatFilter /// Implement this interface if you want to have your own implementation of FormatFilter. A FormatFilter decides
/// what content type to use if the format is present in the Url.
/// </summary> /// </summary>
public interface IFormatFilter : IFilter public interface IFormatFilter : IFilter
{ {
/// <summary>
/// Get the <see cref="MediaTypeHeaderValue"/> registered fot the format in the request.
/// </summary>
MediaTypeHeaderValue GetContentTypeForCurrentRequest(FilterContext context); MediaTypeHeaderValue GetContentTypeForCurrentRequest(FilterContext context);
} }
} }

View File

@ -11,15 +11,24 @@ using Microsoft.AspNet.Mvc.Core;
namespace Microsoft.AspNet.Mvc namespace Microsoft.AspNet.Mvc
{ {
/// <summary> /// <summary>
/// These options are used to specify mapping between the Url Format and corresponding ContentType. /// Used to specify mapping between the Url Format and corresponding <see cref="MediaTypeHeaderValue"/>.
/// </summary> /// </summary>
public class FormatterMappings public class FormatterMappings
{ {
private readonly Dictionary<string, MediaTypeHeaderValue> _map = private readonly Dictionary<string, MediaTypeHeaderValue> _map =
new Dictionary<string, MediaTypeHeaderValue>(StringComparer.OrdinalIgnoreCase); new Dictionary<string, MediaTypeHeaderValue>(StringComparer.OrdinalIgnoreCase);
public void SetFormatMapping([NotNull] string format, [NotNull] MediaTypeHeaderValue contentType) /// <summary>
/// This will set mapping for the format to specified <see cref="MediaTypeHeaderValue"/>.
/// If the format already exists, the <see cref="MediaTypeHeaderValue"/> will be overwritten with the new value.
/// </summary>
public void SetMediaTypeMappingForFormat([NotNull] string format, [NotNull] MediaTypeHeaderValue contentType)
{ {
if (string.IsNullOrEmpty(format))
{
throw new ArgumentException(Resources.ArgumentCannotBeNullOrEmpty, "format");
}
if (contentType == null) if (contentType == null)
{ {
throw new ArgumentException((Resources.ArgumentCannotBeNullOrEmpty), "contentType"); throw new ArgumentException((Resources.ArgumentCannotBeNullOrEmpty), "contentType");
@ -29,7 +38,10 @@ namespace Microsoft.AspNet.Mvc
_map[format] = contentType; _map[format] = contentType;
} }
public MediaTypeHeaderValue GetContentTypeForFormat(string format) /// <summary>
/// Gets <see cref="MediaTypeHeaderValue"/> for the specified format.
/// </summary>
public MediaTypeHeaderValue GetMediaTypeForFormat(string format)
{ {
format = RemovePeriodIfPresent(format); format = RemovePeriodIfPresent(format);
MediaTypeHeaderValue value = null; MediaTypeHeaderValue value = null;
@ -39,10 +51,6 @@ namespace Microsoft.AspNet.Mvc
private string RemovePeriodIfPresent(string format) private string RemovePeriodIfPresent(string format)
{ {
if (string.IsNullOrEmpty(format))
{
throw new ArgumentException(Resources.ArgumentCannotBeNullOrEmpty, "format");
}
if (format.StartsWith(".")) if (format.StartsWith("."))
{ {
format = format.Substring(1); format = format.Substring(1);

View File

@ -54,6 +54,9 @@ namespace Microsoft.AspNet.Mvc
} }
} }
/// <summary>
/// Used to specify mapping between the Url Format and corresponding <see cref="MediaTypeHeaderValue"/>.
/// </summary>
public FormatterMappings FormatterMappings { get; } public FormatterMappings FormatterMappings { get; }
/// <summary> /// <summary>

View File

@ -47,7 +47,7 @@ namespace Microsoft.AspNet.Mvc
options.OutputFormatters.Add(new JsonOutputFormatter()); options.OutputFormatters.Add(new JsonOutputFormatter());
// Set up default mapping for json extensions to content type // Set up default mapping for json extensions to content type
options.FormatterMappings.SetFormatMapping("json", MediaTypeHeaderValue.Parse("application/json")); options.FormatterMappings.SetMediaTypeMappingForFormat("json", MediaTypeHeaderValue.Parse("application/json"));
// Set up default input formatters. // Set up default input formatters.
options.InputFormatters.Add(new JsonInputFormatter()); options.InputFormatters.Add(new JsonInputFormatter());

View File

@ -66,6 +66,8 @@ namespace Microsoft.AspNet.Mvc
[Fact] [Fact]
public void FormatFilter_ContextContainsFormat_InRouteAndQueryData() public void FormatFilter_ContextContainsFormat_InRouteAndQueryData()
{ {
// If the format is present in both route and query data, the one in route data wins
// Arrange // Arrange
var mediaType = MediaTypeHeaderValue.Parse("application/json"); var mediaType = MediaTypeHeaderValue.Parse("application/json");
@ -116,7 +118,7 @@ namespace Microsoft.AspNet.Mvc
var resultExecutingContext = CreateResultExecutingContext(format, place); var resultExecutingContext = CreateResultExecutingContext(format, place);
var resourceExecutingContext = CreateResourceExecutingContext(new IFilter[] { }, format, place); var resourceExecutingContext = CreateResourceExecutingContext(new IFilter[] { }, format, place);
var options = resultExecutingContext.HttpContext.RequestServices.GetService<IOptions<MvcOptions>>(); var options = resultExecutingContext.HttpContext.RequestServices.GetService<IOptions<MvcOptions>>();
options.Options.FormatterMappings.SetFormatMapping(format, MediaTypeHeaderValue.Parse(contentType)); options.Options.FormatterMappings.SetMediaTypeMappingForFormat(format, MediaTypeHeaderValue.Parse(contentType));
var filter = new FormatFilterAttribute(); var filter = new FormatFilterAttribute();
@ -194,7 +196,7 @@ namespace Microsoft.AspNet.Mvc
new string[] { "application/foo", "text/bar" }); new string[] { "application/foo", "text/bar" });
var context = CreateResourceExecutingContext(new IFilter[] { produces }, "star", FormatSource.RouteData); var context = CreateResourceExecutingContext(new IFilter[] { produces }, "star", FormatSource.RouteData);
var options = context.HttpContext.RequestServices.GetService<IOptions<MvcOptions>>(); var options = context.HttpContext.RequestServices.GetService<IOptions<MvcOptions>>();
options.Options.FormatterMappings.SetFormatMapping("star", MediaTypeHeaderValue.Parse("application/*")); options.Options.FormatterMappings.SetMediaTypeMappingForFormat("star", MediaTypeHeaderValue.Parse("application/*"));
var filter = new FormatFilterAttribute(); var filter = new FormatFilterAttribute();

View File

@ -131,7 +131,8 @@ namespace Microsoft.AspNet.Mvc.Test
return new ResultExecutingContext( return new ResultExecutingContext(
CreateActionContext(), CreateActionContext(),
filters, filters,
new ObjectResult("Some Value")); new ObjectResult("Some Value"),
controller: new object());
} }
private static ActionContext CreateActionContext() private static ActionContext CreateActionContext()

View File

@ -21,13 +21,13 @@ namespace Microsoft.AspNet.Mvc
// Arrange // Arrange
var mediaType = MediaTypeHeaderValue.Parse(contentType); var mediaType = MediaTypeHeaderValue.Parse(contentType);
var options = new FormatterMappings(); var options = new FormatterMappings();
options.SetFormatMapping(setFormat, mediaType); options.SetMediaTypeMappingForFormat(setFormat, mediaType);
// Act // Act
var returnmediaType = options.GetContentTypeForFormat(getFormat); var returnMediaType = options.GetMediaTypeForFormat(getFormat);
// Assert // Assert
Assert.Equal(mediaType, returnmediaType); Assert.Equal(mediaType, returnMediaType);
} }
[Theory] [Theory]
@ -45,9 +45,10 @@ namespace Microsoft.AspNet.Mvc
} }
var options = new FormatterMappings(); var options = new FormatterMappings();
var expectedError = "Value cannot be null or empty." + Environment.NewLine + "Parameter name: format";
// Act and Assert // Act and Assert
Assert.Throws<ArgumentException>(() => options.SetFormatMapping(format, mediaType)); Assert.Throws<ArgumentException>(() => options.SetMediaTypeMappingForFormat(format, mediaType));
} }
} }
} }

View File

@ -432,8 +432,9 @@ namespace Microsoft.AspNet.Mvc.FunctionalTests
var response = await client.GetAsync("http://localhost/FormatFilter/MethodWithFormatFilter"); var response = await client.GetAsync("http://localhost/FormatFilter/MethodWithFormatFilter");
// Assert // Assert
var type = response.Content.Headers.ContentType; Assert.Equal(HttpStatusCode.OK, response.StatusCode);
var body = await response.Content.ReadAsStringAsync(); var body = await response.Content.ReadAsStringAsync();
Assert.Equal(body, "MethodWithFormatFilter");
} }
} }
} }

View File

@ -12,7 +12,7 @@ namespace Microsoft.AspNet.Mvc.FunctionalTests
{ {
public class FormatFilterTest public class FormatFilterTest
{ {
private readonly IServiceProvider _services = TestHelper.CreateServices("FormatFilterWebSite"); private readonly IServiceProvider _services = TestHelper.CreateServices(nameof(FormatFilterWebSite));
private readonly Action<IApplicationBuilder> _app = new FormatFilterWebSite.Startup().Configure; private readonly Action<IApplicationBuilder> _app = new FormatFilterWebSite.Startup().Configure;
[Fact] [Fact]
@ -45,6 +45,21 @@ namespace Microsoft.AspNet.Mvc.FunctionalTests
Assert.Equal(@"{""SampleInt"":5}", await response.Content.ReadAsStringAsync()); Assert.Equal(@"{""SampleInt"":5}", await response.Content.ReadAsStringAsync());
} }
[Fact]
public async Task FormatFilter_ExtensionInRequest_Optional()
{
// Arrange
var server = TestServer.Create(_services, _app);
var client = server.CreateClient();
// Act
var response = await client.GetAsync("http://localhost/FormatFilter/GetProduct.json");
// Assert
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
Assert.Equal(@"{""SampleInt"":0}", await response.Content.ReadAsStringAsync());
}
[Fact] [Fact]
public async Task FormatFilter_ExtensionInRequest_Custom() public async Task FormatFilter_ExtensionInRequest_Custom()
{ {
@ -60,6 +75,21 @@ namespace Microsoft.AspNet.Mvc.FunctionalTests
Assert.Equal(@"SampleInt:5", await response.Content.ReadAsStringAsync()); Assert.Equal(@"SampleInt:5", await response.Content.ReadAsStringAsync());
} }
[Fact]
public async Task FormatFilter_ExtensionInRequest_CaseInsensitivity()
{
// Arrange
var server = TestServer.Create(_services, _app);
var client = server.CreateClient();
// Act
var response = await client.GetAsync("http://localhost/FormatFilter/GetProduct/5.Custom");
// Assert
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
Assert.Equal(@"SampleInt:5", await response.Content.ReadAsStringAsync());
}
[Fact] [Fact]
public async Task FormatFilter_ExtensionInRequest_NonExistant() public async Task FormatFilter_ExtensionInRequest_NonExistant()
{ {

View File

@ -9,10 +9,21 @@ namespace ConnegWebsite
[Produces("application/FormatFilterController")] [Produces("application/FormatFilterController")]
public class FormatFilterController : Controller public class FormatFilterController : Controller
{ {
[FormatFilter] public override void OnActionExecuted(ActionExecutedContext context)
public User MethodWithFormatFilter()
{ {
return new User() { Name = "Joe", Address = "1 abc way" }; var result = context.Result as ObjectResult;
if (result != null)
{
result.Formatters.Add(new CustomFormatter("application/FormatFilterController"));
}
base.OnActionExecuted(context);
}
[FormatFilter]
public string MethodWithFormatFilter()
{
return "MethodWithFormatFilter";
} }
} }
} }

View File

@ -8,7 +8,7 @@ namespace FormatFilterWebSite
[FormatFilter] [FormatFilter]
public class FormatFilterController : Controller public class FormatFilterController : Controller
{ {
public Product GetProduct(int id) public Product GetProduct(int id = 0)
{ {
return new Product() { SampleInt = id }; return new Product() { SampleInt = id };
} }

View File

@ -26,7 +26,7 @@ namespace FormatFilterWebSite
var customFormatter = new CustomFormatter("application/custom"); var customFormatter = new CustomFormatter("application/custom");
options.OutputFormatters.Add(customFormatter); options.OutputFormatters.Add(customFormatter);
options.FormatterMappings.SetFormatMapping( options.FormatterMappings.SetMediaTypeMappingForFormat(
"custom", "custom",
MediaTypeHeaderValue.Parse("application/custom")); MediaTypeHeaderValue.Parse("application/custom"));
}); });
@ -37,6 +37,10 @@ namespace FormatFilterWebSite
routes.MapRoute("formatroute", routes.MapRoute("formatroute",
"{controller}/{action}/{id}.{format?}", "{controller}/{action}/{id}.{format?}",
new { controller = "Home", action = "Index" }); new { controller = "Home", action = "Index" });
routes.MapRoute("optionalroute",
"{controller}/{action}.{format?}",
new { controller = "Home", action = "Index" });
}); });
} }
} }

Binary file not shown.