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