This is MVC part of feature URL Extensions. It does following:

1. Creates a filter called FormatFilter. This will look at the format parameter if present
   in the route data or query data and sets the content type in ObjectResult
2. It adds new options called FormatterOptions, that contains the map of format to content tyepe
3. A method in MVC options to add the formatter mapping
This commit is contained in:
Mugdha Kulkarni 2014-12-31 12:30:42 -08:00
parent cf7d428abd
commit d91b7776b3
10 changed files with 491 additions and 53 deletions

View File

@ -11,11 +11,11 @@ using Microsoft.Framework.ConfigurationModel;
using Microsoft.Framework.DependencyInjection;
using MvcSample.Web.Filters;
using MvcSample.Web.Services;
using Microsoft.AspNet.Mvc.Core.Filters;
#if ASPNET50
using Autofac;
using Microsoft.Framework.DependencyInjection.Autofac;
using Microsoft.AspNet.Mvc.Core.Filters;
#endif
namespace MvcSample.Web
@ -64,8 +64,13 @@ namespace MvcSample.Web
services.Configure<MvcOptions>(options =>
{
options.Filters.Add(typeof(PassThroughAttribute), order: 17);
<<<<<<< HEAD
options.AddXmlDataContractSerializerFormatter();
=======
var formatFilter = new FormatFilter();
options.Filters.Add(formatFilter);
>>>>>>> This is MVC part of feature URL Extensions. It does following:
});
services.Configure<RazorViewEngineOptions>(options =>
{
@ -107,6 +112,9 @@ namespace MvcSample.Web
{
options.Filters.Add(typeof(PassThroughAttribute), order: 17);
options.AddXmlDataContractSerializerFormatter();
var formatFilter = new FormatFilter();
options.Filters.Add(formatFilter);
});
});
}

View File

@ -0,0 +1,90 @@
using System;
using Microsoft.Framework.OptionsModel;
using System.Threading.Tasks;
using System.Linq;
namespace Microsoft.AspNet.Mvc.Core.Filters
{
public class FormatFilter : IFormatFilter
{
public void OnResourceExecuting([NotNull] ResourceExecutingContext context)
{
var options = (IOptions<MvcOptions>)context.HttpContext.RequestServices.GetService(
typeof(IOptions<MvcOptions>));
string format = null;
if (context.RouteData.Values.ContainsKey("format"))
{
format = context.RouteData.Values["format"].ToString();
}
else if(context.HttpContext.Request.Query.ContainsKey("format"))
{
format = context.HttpContext.Request.Query.Get("format").ToString();
}
if (format != null)
{
var contentType = options.Options.OutputFormatterOptions.GetContentTypeForFormat(format);
if (contentType == null)
{
// no contentType exists for the foramt, return 404
context.Result = new HttpNotFoundResult();
}
else
{
if (context.Filters.Any(f => f is ProducesAttribute))
{
var produces = context.Filters.First(f => f is ProducesAttribute) as ProducesAttribute;
if(!produces.ContentTypes.Contains(contentType))
{
context.Result = new HttpNotFoundResult();
}
}
}
}
}
public void OnResourceExecuted([NotNull] ResourceExecutedContext context)
{
}
public void OnResultExecuting([NotNull]ResultExecutingContext context)
{
var options = (IOptions<MvcOptions>)context.HttpContext.RequestServices.GetService(
typeof(IOptions<MvcOptions>));
string format = null;
if (context.RouteData.Values.ContainsKey("format"))
{
format = context.RouteData.Values["format"].ToString();
}
else if (context.HttpContext.Request.Query.ContainsKey("format"))
{
format = context.HttpContext.Request.Query.Get("format").ToString();
}
if (format != null)
{
var contentType = options.Options.OutputFormatterOptions.GetContentTypeForFormat(format);
if (contentType != null)
{
var objectResult = context.Result as ObjectResult;
if (objectResult != null)
{
objectResult.ContentTypes.Clear();
objectResult.ContentTypes.Add(contentType);
}
}
else
{
context.Result = new HttpStatusCodeResult(404);
}
}
}
public void OnResultExecuted([NotNull]ResultExecutedContext context)
{
}
}
}

View File

@ -0,0 +1,9 @@
using System;
namespace Microsoft.AspNet.Mvc.Core.Filters
{
public interface IFormatFilter : IResourceFilter, IResultFilter
{
}
}

View File

@ -2,12 +2,14 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Linq;
using System.Collections.Generic;
using System.Linq;
using Microsoft.AspNet.Mvc.Core;
using Microsoft.AspNet.Mvc.Description;
using Microsoft.Net.Http.Headers;
namespace Microsoft.AspNet.Mvc
{
/// <summary>
@ -33,7 +35,12 @@ namespace Microsoft.AspNet.Mvc
if (objectResult != null)
{
SetContentTypes(objectResult.ContentTypes);
// Check if FormatFilter has already set the content type
// If it has, dont override it
if (objectResult.ContentTypes.Count == 0)
{
SetContentTypes(objectResult.ContentTypes);
}
}
}

View File

@ -1,49 +0,0 @@
using Microsoft.AspNet.Mvc.HeaderValueAbstractions;
using Microsoft.Framework.OptionsModel;
using System;
using System.Collections.Generic;
namespace Microsoft.AspNet.Mvc.Core.Filters
{
public class UrlExtensionFilter : IResultFilter
{
//private Dictionary<string, MediaTypeHeaderValue> FormatContentTypeMap =
// new Dictionary<string, MediaTypeHeaderValue>();
//public void AddFormatMapping(string format, MediaTypeHeaderValue contentType)
//{
// if(FormatContentTypeMap.ContainsKey(format))
// {
// FormatContentTypeMap.Remove(format);
// }
// FormatContentTypeMap.Add(format, contentType);
//}
public void OnResultExecuting([NotNull] ResultExecutingContext context)
{
var options = (IOptions<MvcOptions>)context.HttpContext.RequestServices.GetService(typeof(IOptions<MvcOptions>));
if (context.RouteData.Values.ContainsKey("format"))
{
var format = context.RouteData.Values["format"].ToString();
var contentType = options.Options.OutputFormatterOptions.GetContentTypeForFormat(format);
if (contentType != null)
{
var objectResult = context.Result as ObjectResult;
objectResult.ContentTypes.Clear();
objectResult.ContentTypes.Add(contentType);
}
else
{
throw new InvalidOperationException("No formatter exists for format:" + format);
}
}
}
public void OnResultExecuted([NotNull] ResultExecutedContext context)
{
}
}
}

View File

@ -0,0 +1,46 @@
using System;
using System.Collections.Generic;
using Microsoft.Net.Http.Headers;
namespace Microsoft.AspNet.Mvc.Core
{
public class OutputFormatterOptions
{
private Dictionary<string, MediaTypeHeaderValue> map = new Dictionary<string, MediaTypeHeaderValue>();
public void AddFormatMapping(string format, MediaTypeHeaderValue contentType)
{
if (!string.IsNullOrEmpty(format) && contentType != null)
{
if(format.StartsWith("."))
{
format = format.TrimStart('.');
}
map[format.ToLower()] = contentType;
}
}
public MediaTypeHeaderValue GetContentTypeForFormat(string format)
{
if (!string.IsNullOrEmpty(format))
{
if (format.StartsWith("."))
{
format = format.TrimStart('.');
}
if (map.ContainsKey(format.ToLower()))
{
return map[format.ToLower()];
}
else
{
return null;
}
}
return null;
}
}
}

View File

@ -13,7 +13,9 @@
"Microsoft.AspNet.Routing": "1.0.0-*",
"Microsoft.AspNet.Security": "1.0.0-*",
"Microsoft.AspNet.Security.DataProtection": "1.0.0-*",
"Microsoft.Framework.Runtime.Interfaces": { "version": "1.0.0-*", "type": "build" }
"Microsoft.Framework.Runtime.Interfaces": { "version": "1.0.0-*", "type": "build" },
"Microsoft.Net.Http": "2.2.13.0",
"Microsoft.Net.Http.Server": "1.0.0.0-rc1-11332"
},
"frameworks": {
"aspnet50": {},

View File

@ -7,7 +7,7 @@ using Microsoft.AspNet.Mvc.ModelBinding;
using Microsoft.AspNet.Mvc.Razor;
using Microsoft.Framework.OptionsModel;
using Newtonsoft.Json.Linq;
using Microsoft.AspNet.Mvc.HeaderValueAbstractions;
using Microsoft.Net.Http.Headers;
namespace Microsoft.AspNet.Mvc
{

View File

@ -0,0 +1,253 @@
using System;
using System.Threading.Tasks;
using Microsoft.AspNet.Mvc.Core.Filters;
using Microsoft.AspNet.PipelineCore;
using Microsoft.AspNet.Routing;
using Microsoft.Net.Http.Headers;
using Xunit;
#if ASPNET50
using Moq;
using Microsoft.Framework.OptionsModel;
using Microsoft.AspNet.Http;
#endif
namespace Microsoft.AspNet.Mvc.Core.Test
{
public enum FormatPlace
{
RouteData,
QueryData,
RouteAndQueryData
}
public class FormatFilterTests
{
[Theory]
[InlineData("json", FormatPlace.RouteData, "application/json")]
[InlineData("json", FormatPlace.QueryData, "application/json")]
[InlineData("json", FormatPlace.RouteAndQueryData, "application/json")]
public void FormatFilter_ContextContainsFormat_DefaultFormat(string format,
FormatPlace place,
string contentType)
{
// Arrange
var mediaType = MediaTypeHeaderValue.Parse(contentType);
var context = CreateResultExecutingContext(format, place);
var filter = new FormatFilter();
// Act
filter.OnResultExecuting(context);
// Assert
var objectResult = context.Result as ObjectResult;
Assert.Equal(1, objectResult.ContentTypes.Count);
ValidateMediaType(mediaType, objectResult.ContentTypes[0]);
}
[Theory]
[InlineData("foo", FormatPlace.RouteData, "application/foo")]
[InlineData("foo", FormatPlace.QueryData, "application/foo")]
[InlineData("foo", FormatPlace.RouteAndQueryData, "application/foo")]
public void FormatFilter_ContextContainsFormat_Custom(
string format,
FormatPlace place,
string contentType)
{
// Arrange
var mediaType = MediaTypeHeaderValue.Parse(contentType);
var context = CreateResultExecutingContext(format, place);
var options = (IOptions<MvcOptions>)context.HttpContext.RequestServices.GetService(
typeof(IOptions<MvcOptions>));
options.Options.AddFormatMapping(format, MediaTypeHeaderValue.Parse(contentType));
var filter = new FormatFilter();
// Act
filter.OnResultExecuting(context);
// Assert
var objectResult = context.Result as ObjectResult;
Assert.Equal(1, objectResult.ContentTypes.Count);
ValidateMediaType(mediaType, objectResult.ContentTypes[0]);
}
[Theory]
[InlineData("foo", FormatPlace.RouteData, "application/foo")]
public void FormatFilter_ContextContainsFormat_NonExisting(
string format,
FormatPlace place,
string contentType)
{
// Arrange
var mediaType = MediaTypeHeaderValue.Parse(contentType);
var resourceExecutingContext = CreateResourceExecutingContext(new IFilter[] { }, format, place);
var filter = new FormatFilter();
// Act
filter.OnResourceExecuting(resourceExecutingContext);
// Assert
var actionResult = resourceExecutingContext.Result;
Assert.True(actionResult is HttpNotFoundResult);
}
[Fact]
public void FormatFilter_ContextDoesntContainFormat()
{
// Arrange
var resourceExecutingContext = CreateResourceExecutingContext(new IFilter[] { });
var filter = new FormatFilter();
// Act
filter.OnResourceExecuting(resourceExecutingContext);
// Assert
var result = resourceExecutingContext.Result as IActionResult;
Assert.False(result is HttpNotFoundResult);
}
[Theory]
[InlineData("json", FormatPlace.RouteData, "application/json")]
[InlineData("json", FormatPlace.QueryData, "application/json")]
public void FormatFilter_ContextContainsFormat_ContainsProducesFilter_Matching(
string format,
FormatPlace place,
string contentType)
{
// Arrange
var produces = new ProducesAttribute(contentType, new string[] { "application/foo", "text/bar" });
var context = CreateResourceExecutingContext(new IFilter[] { produces }, format, place);
var filter = new FormatFilter();
// Act
filter.OnResourceExecuting(context);
// Assert
var result = context.Result as IActionResult;
Assert.False(result is HttpNotFoundResult);
}
[Theory]
[InlineData("json", FormatPlace.RouteData, "application/json")]
[InlineData("json", FormatPlace.QueryData, "application/json")]
public void FormatFilter_ContextContainsFormat_ContainsProducesFilter_Conflicting(
string format,
FormatPlace place,
string contentType)
{
// Arrange
var mediaType = MediaTypeHeaderValue.Parse(contentType);
var produces = new ProducesAttribute("application/xml", new string[] { "application/foo", "text/bar" });
var context = CreateResourceExecutingContext(new IFilter[] { produces }, format, place);
var filter = new FormatFilter();
// Act
filter.OnResourceExecuting(context);
// Assert
var result = context.Result as IActionResult;
Assert.True(result is HttpNotFoundResult);
}
private static ResourceExecutingContext CreateResourceExecutingContext(
IFilter[] filters,
string format = null,
FormatPlace? place = null)
{
if(format == null || place == null)
{
var context = new ResourceExecutingContext(
CreateActionContext(),
filters);
context.Result = new HttpStatusCodeResult(200);
return context;
}
var context1 = new ResourceExecutingContext(
CreateActionContext(format, place),
filters);
context1.Result = new HttpStatusCodeResult(200);
return context1;
}
private static ResultExecutingContext CreateResultExecutingContext(
string format = null,
FormatPlace? place = null)
{
if (format == null || place == null)
{
return new ResultExecutingContext(
new ActionContext(new DefaultHttpContext(), new RouteData(), new ActionDescriptor()),
new IFilter[] { },
new ObjectResult("Some Value"));
}
return new ResultExecutingContext(
CreateActionContext(format, place),
new IFilter[] { },
new ObjectResult("Some Value"));
}
private static ActionContext CreateActionContext(string format = null, FormatPlace? place = null)
{
var httpContext = CreateMockHttpContext();
if (place == FormatPlace.RouteData || place == FormatPlace.RouteAndQueryData)
{
var data = new RouteData();
data.Values.Add("format", format);
httpContext.Setup(c => c.Request.Query.ContainsKey("format")).Returns(false);
return new ActionContext(httpContext.Object, data, new ActionDescriptor());
}
if (place == FormatPlace.QueryData || place == FormatPlace.RouteAndQueryData)
{
httpContext.Setup(c => c.Request.Query.ContainsKey("format")).Returns(true);
httpContext.Setup(c => c.Request.Query.Get("format")).Returns(format);
return new ActionContext(httpContext.Object, new RouteData(), new ActionDescriptor());
}
else if(place == null && format == null)
{
httpContext.Setup(c => c.Request.Query.ContainsKey("format")).Returns(false);
return new ActionContext(httpContext.Object, new RouteData(), new ActionDescriptor());
}
return null;
}
private static Mock<HttpContext> CreateMockHttpContext()
{
MvcOptions options = new MvcOptions();
MvcOptionsSetup.ConfigureMvc(options);
var mvcOptions = new Mock<IOptions<MvcOptions>>();
mvcOptions.Setup(o => o.Options).Returns(options);
var serviceProvider = new Mock<IServiceProvider>();
serviceProvider
.Setup(s => s.GetService(It.Is<Type>(t => t == typeof(IOptions<MvcOptions>))))
.Returns(mvcOptions.Object);
var httpContext = new Mock<HttpContext>();
httpContext
.Setup(c => c.RequestServices)
.Returns(serviceProvider.Object);
httpContext.Setup(c => c.Request.Query.ContainsKey("format")).Returns(false);
return httpContext;
}
private static void ValidateMediaType(MediaTypeHeaderValue expectedMediaType, MediaTypeHeaderValue actualMediaType)
{
Assert.Equal(expectedMediaType.MediaType, actualMediaType.MediaType);
Assert.Equal(expectedMediaType.SubType, actualMediaType.SubType);
Assert.Equal(expectedMediaType.Charset, actualMediaType.Charset);
Assert.Equal(expectedMediaType.MatchesAllTypes, actualMediaType.MatchesAllTypes);
Assert.Equal(expectedMediaType.MatchesAllSubTypes, actualMediaType.MatchesAllSubTypes);
Assert.Equal(expectedMediaType.Parameters.Count, actualMediaType.Parameters.Count);
foreach (var item in expectedMediaType.Parameters)
{
Assert.Equal(item.Value, NameValueHeaderValue.Find(actualMediaType.Parameters, item.Name).Value);
}
}
}
}

View File

@ -0,0 +1,72 @@
using System;
using Microsoft.AspNet.Mvc.Core;
using Microsoft.Net.Http.Headers;
using Xunit;
namespace Microsoft.AspNet.Mvc.Core.Test
{
public class OutputFormatterOptionsTest
{
[Theory]
[InlineData("xml", "application/xml")]
[InlineData("json", "application/json")]
[InlineData("foo", "text/foo")]
[InlineData(".json", "application/json")]
[InlineData(".foo", "text/foo")]
public void OutputFormatterOptions_AddFormatMapping_Valid(string format, string contentType)
{
// Arrange
var mediaType = MediaTypeHeaderValue.Parse(contentType);
OutputFormatterOptions options = new OutputFormatterOptions();
options.AddFormatMapping(format, mediaType);
// Act
var returnmediaType = options.GetContentTypeForFormat(format);
// Assert
Assert.Equal(mediaType, returnmediaType);
}
[Theory]
[InlineData(".xml", "application/xml", "xml")]
[InlineData("json", "application/json", "JSON")]
[InlineData(".foo", "text/foo", "Foo")]
[InlineData(".Json", "application/json", "json")]
[InlineData("FOo", "text/foo", "FOO")]
public void OutputFormatterOptions_AddFormatMapping_DiffSetGetFormat(string setFormat, string contentType, string getFormat)
{
// Arrange
var mediaType = MediaTypeHeaderValue.Parse(contentType);
OutputFormatterOptions options = new OutputFormatterOptions();
options.AddFormatMapping(setFormat, mediaType);
// Act
var returnmediaType = options.GetContentTypeForFormat(getFormat);
// Assert
Assert.Equal(mediaType, returnmediaType);
}
[Theory]
[InlineData("xml", null)]
[InlineData(".json", null)]
[InlineData(null, "application/json")]
[InlineData("", "text/foo")]
public void OutputFormatterOptions_AddFormatMapping_Invalid(string format, string contentType)
{
// Arrange
MediaTypeHeaderValue mediaType = null;
if (!string.IsNullOrEmpty(contentType))
{
mediaType = MediaTypeHeaderValue.Parse(contentType);
}
OutputFormatterOptions options = new OutputFormatterOptions();
options.AddFormatMapping(format, mediaType);
// Act and Assert
Assert.Throws<ArgumentException>(() => options.GetContentTypeForFormat(format));
}
}
}