From d91b7776b3c7f1fb361ddce8deacdb0d47064af4 Mon Sep 17 00:00:00 2001 From: Mugdha Kulkarni Date: Wed, 31 Dec 2014 12:30:42 -0800 Subject: [PATCH] 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 --- samples/MvcSample.Web/Startup.cs | 10 +- .../Filters/FormatFilter.cs | 90 +++++++ .../Filters/IFormatFilter.cs | 9 + .../Filters/ProducesAttribute.cs | 9 +- .../Filters/UrlExtensionFilter.cs | 49 ---- .../OutputFormatterOptions.cs | 46 ++++ src/Microsoft.AspNet.Mvc.Core/project.json | 4 +- src/Microsoft.AspNet.Mvc/MvcOptionsSetup.cs | 2 +- .../Filters/FormatFilterTest.cs | 253 ++++++++++++++++++ .../OutputFormatterOptionsTest.cs | 72 +++++ 10 files changed, 491 insertions(+), 53 deletions(-) create mode 100644 src/Microsoft.AspNet.Mvc.Core/Filters/FormatFilter.cs create mode 100644 src/Microsoft.AspNet.Mvc.Core/Filters/IFormatFilter.cs delete mode 100644 src/Microsoft.AspNet.Mvc.Core/Filters/UrlExtensionFilter.cs create mode 100644 src/Microsoft.AspNet.Mvc.Core/OutputFormatterOptions.cs create mode 100644 test/Microsoft.AspNet.Mvc.Core.Test/Filters/FormatFilterTest.cs create mode 100644 test/Microsoft.AspNet.Mvc.Core.Test/OutputFormatterOptionsTest.cs diff --git a/samples/MvcSample.Web/Startup.cs b/samples/MvcSample.Web/Startup.cs index d656371647..0642cace1e 100644 --- a/samples/MvcSample.Web/Startup.cs +++ b/samples/MvcSample.Web/Startup.cs @@ -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(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(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); }); }); } diff --git a/src/Microsoft.AspNet.Mvc.Core/Filters/FormatFilter.cs b/src/Microsoft.AspNet.Mvc.Core/Filters/FormatFilter.cs new file mode 100644 index 0000000000..cc4505ac6d --- /dev/null +++ b/src/Microsoft.AspNet.Mvc.Core/Filters/FormatFilter.cs @@ -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)context.HttpContext.RequestServices.GetService( + typeof(IOptions)); + 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)context.HttpContext.RequestServices.GetService( + typeof(IOptions)); + 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) + { + + } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Mvc.Core/Filters/IFormatFilter.cs b/src/Microsoft.AspNet.Mvc.Core/Filters/IFormatFilter.cs new file mode 100644 index 0000000000..f25ceed50c --- /dev/null +++ b/src/Microsoft.AspNet.Mvc.Core/Filters/IFormatFilter.cs @@ -0,0 +1,9 @@ +using System; + +namespace Microsoft.AspNet.Mvc.Core.Filters +{ + public interface IFormatFilter : IResourceFilter, IResultFilter + { + + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Mvc.Core/Filters/ProducesAttribute.cs b/src/Microsoft.AspNet.Mvc.Core/Filters/ProducesAttribute.cs index b3ae09cd90..1e8dff1356 100644 --- a/src/Microsoft.AspNet.Mvc.Core/Filters/ProducesAttribute.cs +++ b/src/Microsoft.AspNet.Mvc.Core/Filters/ProducesAttribute.cs @@ -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 { /// @@ -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); + } } } diff --git a/src/Microsoft.AspNet.Mvc.Core/Filters/UrlExtensionFilter.cs b/src/Microsoft.AspNet.Mvc.Core/Filters/UrlExtensionFilter.cs deleted file mode 100644 index bf2faca5a0..0000000000 --- a/src/Microsoft.AspNet.Mvc.Core/Filters/UrlExtensionFilter.cs +++ /dev/null @@ -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 FormatContentTypeMap = - // new Dictionary(); - - //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)context.HttpContext.RequestServices.GetService(typeof(IOptions)); - - 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) - { - - } - } -} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Mvc.Core/OutputFormatterOptions.cs b/src/Microsoft.AspNet.Mvc.Core/OutputFormatterOptions.cs new file mode 100644 index 0000000000..f66453ad64 --- /dev/null +++ b/src/Microsoft.AspNet.Mvc.Core/OutputFormatterOptions.cs @@ -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 map = new Dictionary(); + + 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; + } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Mvc.Core/project.json b/src/Microsoft.AspNet.Mvc.Core/project.json index 4f5944b3cc..af6a915106 100644 --- a/src/Microsoft.AspNet.Mvc.Core/project.json +++ b/src/Microsoft.AspNet.Mvc.Core/project.json @@ -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": {}, diff --git a/src/Microsoft.AspNet.Mvc/MvcOptionsSetup.cs b/src/Microsoft.AspNet.Mvc/MvcOptionsSetup.cs index c8ee5d8f56..52f23a68f4 100644 --- a/src/Microsoft.AspNet.Mvc/MvcOptionsSetup.cs +++ b/src/Microsoft.AspNet.Mvc/MvcOptionsSetup.cs @@ -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 { diff --git a/test/Microsoft.AspNet.Mvc.Core.Test/Filters/FormatFilterTest.cs b/test/Microsoft.AspNet.Mvc.Core.Test/Filters/FormatFilterTest.cs new file mode 100644 index 0000000000..f5583860e2 --- /dev/null +++ b/test/Microsoft.AspNet.Mvc.Core.Test/Filters/FormatFilterTest.cs @@ -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)context.HttpContext.RequestServices.GetService( + typeof(IOptions)); + 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 CreateMockHttpContext() + { + MvcOptions options = new MvcOptions(); + MvcOptionsSetup.ConfigureMvc(options); + var mvcOptions = new Mock>(); + mvcOptions.Setup(o => o.Options).Returns(options); + + var serviceProvider = new Mock(); + serviceProvider + .Setup(s => s.GetService(It.Is(t => t == typeof(IOptions)))) + .Returns(mvcOptions.Object); + + var httpContext = new Mock(); + 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); + } + } + } +} \ No newline at end of file diff --git a/test/Microsoft.AspNet.Mvc.Core.Test/OutputFormatterOptionsTest.cs b/test/Microsoft.AspNet.Mvc.Core.Test/OutputFormatterOptionsTest.cs new file mode 100644 index 0000000000..c205d7151b --- /dev/null +++ b/test/Microsoft.AspNet.Mvc.Core.Test/OutputFormatterOptionsTest.cs @@ -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(() => options.GetContentTypeForFormat(format)); + } + } +} \ No newline at end of file