diff --git a/Mvc.sln b/Mvc.sln index b2d7a62568..2e42a3e926 100644 --- a/Mvc.sln +++ b/Mvc.sln @@ -130,6 +130,8 @@ Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "ValidationWebSite", "test\W EndProject Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Microsoft.AspNet.Mvc.Xml.Test", "test\Microsoft.AspNet.Mvc.Xml.Test\Microsoft.AspNet.Mvc.Xml.Test.kproj", "{22019146-BDFA-442E-8C8E-345FB9644578}" EndProject +Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "FormatFilterWebSite", "test\WebSites\FormatFilterWebSite\FormatFilterWebSite.kproj", "{AC9BE567-540E-4C70-90C2-AAF021307A80}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -740,6 +742,18 @@ Global {22019146-BDFA-442E-8C8E-345FB9644578}.Release|Mixed Platforms.Build.0 = Release|Any CPU {22019146-BDFA-442E-8C8E-345FB9644578}.Release|x86.ActiveCfg = Release|Any CPU {22019146-BDFA-442E-8C8E-345FB9644578}.Release|x86.Build.0 = Release|Any CPU + {AC9BE567-540E-4C70-90C2-AAF021307A80}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {AC9BE567-540E-4C70-90C2-AAF021307A80}.Debug|Any CPU.Build.0 = Debug|Any CPU + {AC9BE567-540E-4C70-90C2-AAF021307A80}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU + {AC9BE567-540E-4C70-90C2-AAF021307A80}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU + {AC9BE567-540E-4C70-90C2-AAF021307A80}.Debug|x86.ActiveCfg = Debug|Any CPU + {AC9BE567-540E-4C70-90C2-AAF021307A80}.Debug|x86.Build.0 = Debug|Any CPU + {AC9BE567-540E-4C70-90C2-AAF021307A80}.Release|Any CPU.ActiveCfg = Release|Any CPU + {AC9BE567-540E-4C70-90C2-AAF021307A80}.Release|Any CPU.Build.0 = Release|Any CPU + {AC9BE567-540E-4C70-90C2-AAF021307A80}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU + {AC9BE567-540E-4C70-90C2-AAF021307A80}.Release|Mixed Platforms.Build.0 = Release|Any CPU + {AC9BE567-540E-4C70-90C2-AAF021307A80}.Release|x86.ActiveCfg = Release|Any CPU + {AC9BE567-540E-4C70-90C2-AAF021307A80}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -803,5 +817,6 @@ Global {C3123A70-41C4-4122-AD1C-D35DF8958DD7} = {16703B76-C9F7-4C75-AE6C-53D92E308E3C} {87AB84B2-22C1-43C6-BB8A-1D327B446FB0} = {16703B76-C9F7-4C75-AE6C-53D92E308E3C} {22019146-BDFA-442E-8C8E-345FB9644578} = {3BA657BF-28B1-42DA-B5B0-1C4601FCF7B1} + {AC9BE567-540E-4C70-90C2-AAF021307A80} = {16703B76-C9F7-4C75-AE6C-53D92E308E3C} EndGlobalSection EndGlobal diff --git a/samples/MvcSample.Web/Controllers/FormatFilterController.cs b/samples/MvcSample.Web/Controllers/FormatFilterController.cs new file mode 100644 index 0000000000..3f1e29216e --- /dev/null +++ b/samples/MvcSample.Web/Controllers/FormatFilterController.cs @@ -0,0 +1,22 @@ +// 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 Microsoft.AspNet.Mvc; + +namespace MvcSample.Web.Controllers +{ + [FormatFilter] + public class FormatFilterController : Controller + { + public Product GetProduct(int id) + { + return new Product() { SampleInt = id }; + } + + [Produces("application/custom", "application/json", "text/json")] + public Product ProducesMethod(int id) + { + return new Product() { SampleInt = id }; ; + } + } +} \ No newline at end of file diff --git a/samples/MvcSample.Web/Models/Product.cs b/samples/MvcSample.Web/Models/Product.cs new file mode 100644 index 0000000000..b1eb2434fc --- /dev/null +++ b/samples/MvcSample.Web/Models/Product.cs @@ -0,0 +1,9 @@ +using System; + +namespace MvcSample.Web +{ + public class Product + { + public int SampleInt { get; set; } + } +} \ No newline at end of file diff --git a/samples/MvcSample.Web/Startup.cs b/samples/MvcSample.Web/Startup.cs index 0642cace1e..d2f766cc09 100644 --- a/samples/MvcSample.Web/Startup.cs +++ b/samples/MvcSample.Web/Startup.cs @@ -11,7 +11,6 @@ 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; @@ -64,13 +63,9 @@ namespace MvcSample.Web services.Configure(options => { options.Filters.Add(typeof(PassThroughAttribute), order: 17); -<<<<<<< HEAD - options.AddXmlDataContractSerializerFormatter(); -======= - var formatFilter = new FormatFilter(); + var formatFilter = new FormatFilterAttribute(); options.Filters.Add(formatFilter); ->>>>>>> This is MVC part of feature URL Extensions. It does following: }); services.Configure(options => { @@ -113,7 +108,7 @@ namespace MvcSample.Web options.Filters.Add(typeof(PassThroughAttribute), order: 17); options.AddXmlDataContractSerializerFormatter(); - var formatFilter = new FormatFilter(); + var formatFilter = new FormatFilterAttribute(); options.Filters.Add(formatFilter); }); }); @@ -121,10 +116,8 @@ namespace MvcSample.Web app.UseMvc(routes => { + routes.MapRoute("FormatRoute", "{controller}/{action}/{id}.{format?}"); routes.MapRoute("areaRoute", "{area:exists}/{controller}/{action}"); - - routes.MapRoute("formatRoute", "{controller}/{action}/{format}"); - routes.MapRoute( "controllerActionRoute", "{controller}/{action}", diff --git a/src/Microsoft.AspNet.Mvc.Core/Filters/FormatFilter.cs b/src/Microsoft.AspNet.Mvc.Core/Filters/FormatFilter.cs deleted file mode 100644 index cc4505ac6d..0000000000 --- a/src/Microsoft.AspNet.Mvc.Core/Filters/FormatFilter.cs +++ /dev/null @@ -1,90 +0,0 @@ -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/FormatFilterAttribute.cs b/src/Microsoft.AspNet.Mvc.Core/Filters/FormatFilterAttribute.cs new file mode 100644 index 0000000000..d657aaf312 --- /dev/null +++ b/src/Microsoft.AspNet.Mvc.Core/Filters/FormatFilterAttribute.cs @@ -0,0 +1,122 @@ +// 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.Diagnostics; +using System.Linq; +using Microsoft.AspNet.Mvc.Description; +using Microsoft.Framework.DependencyInjection; +using Microsoft.Framework.OptionsModel; +using Microsoft.Net.Http.Headers; +using System.Collections.Generic; + +namespace Microsoft.AspNet.Mvc +{ + /// + /// This will look at the format parameter if present in the route data or query data and + /// sets the content type in ObjectResult corresponding to the format value. + /// + [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false, Inherited = true)] + public class FormatFilterAttribute : Attribute, IFormatFilter, IResourceFilter, IResultFilter + { + /// + /// As a resourceFilter, this filter looks at the request and rejects it + /// before going ahead if + /// 1. The Format in the request doesnt match any format in the map. + /// 2. If there is a conflicting producesFilter. + /// + /// + public void OnResourceExecuting([NotNull] ResourceExecutingContext context) + { + var format = GetFormat(context); + + if (format != null && !string.IsNullOrEmpty(format.ToString())) + { + var formatContentType = GetContentType(format, context); + if (formatContentType == null) + { + // no contentType exists for the format, return 404 + context.Result = new HttpNotFoundResult(); + } + else + { + var responseTypeFilters = context.Filters.OfType(); + if (responseTypeFilters.Count() != 0) + { + var contentTypes = new List(); + foreach (var filter in responseTypeFilters) + { + filter.SetContentTypes(contentTypes); + } + + if (!contentTypes.Any(c => c.IsSubsetOf(formatContentType))) + { + context.Result = new HttpNotFoundResult(); + } + } + } + } + } + + public void OnResourceExecuted([NotNull] ResourceExecutedContext context) + { + + } + + public void OnResultExecuting([NotNull] ResultExecutingContext context) + { + var format = GetFormat(context); + if (format != null) + { + var contentType = GetContentType(format, context); + Debug.Assert(contentType != null); + + var objectResult = context.Result as ObjectResult; + if (objectResult != null) + { + objectResult.ContentTypes.Clear(); + objectResult.ContentTypes.Add(contentType); + } + } + } + + public void OnResultExecuted([NotNull] ResultExecutedContext context) + { + + } + + public MediaTypeHeaderValue GetContentTypeForCurrentRequest(FilterContext context) + { + var format = GetFormat(context); + if (format != null && !string.IsNullOrEmpty(format.ToString())) + { + return GetContentType(format, context); + } + + return null; + } + + private object GetFormat(FilterContext context) + { + object format = null; + + if (!context.RouteData.Values.TryGetValue("format", out format)) + { + if (context.HttpContext.Request.Query.ContainsKey("format")) + { + format = context.HttpContext.Request.Query.Get("format"); + } + } + + return format; + } + + private MediaTypeHeaderValue GetContentType(object format, FilterContext context) + { + Debug.Assert(format != null); + var options = context.HttpContext.RequestServices.GetService>(); + var contentType = options.Options.FormatterMappings.GetContentTypeForFormat(format.ToString()); + return contentType; + } + } +} \ 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 index f25ceed50c..3df67ed4f9 100644 --- a/src/Microsoft.AspNet.Mvc.Core/Filters/IFormatFilter.cs +++ b/src/Microsoft.AspNet.Mvc.Core/Filters/IFormatFilter.cs @@ -1,9 +1,16 @@ -using System; +// 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.Core.Filters +using Microsoft.Net.Http.Headers; +using System; + +namespace Microsoft.AspNet.Mvc { - public interface IFormatFilter : IResourceFilter, IResultFilter + /// + /// Implement this interface if you want to have your own implementation of FormatFilter + /// + public interface IFormatFilter : IFilter { - + MediaTypeHeaderValue GetContentTypeForCurrentRequest(FilterContext context); } } \ 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 1e8dff1356..53526cbc9d 100644 --- a/src/Microsoft.AspNet.Mvc.Core/Filters/ProducesAttribute.cs +++ b/src/Microsoft.AspNet.Mvc.Core/Filters/ProducesAttribute.cs @@ -9,7 +9,6 @@ using Microsoft.AspNet.Mvc.Core; using Microsoft.AspNet.Mvc.Description; using Microsoft.Net.Http.Headers; - namespace Microsoft.AspNet.Mvc { /// @@ -37,7 +36,8 @@ namespace Microsoft.AspNet.Mvc { // Check if FormatFilter has already set the content type // If it has, dont override it - if (objectResult.ContentTypes.Count == 0) + var formatFilter = context.Filters.OfType().LastOrDefault(); + if (formatFilter == null || formatFilter.GetContentTypeForCurrentRequest(context) == null) { SetContentTypes(objectResult.ContentTypes); } diff --git a/src/Microsoft.AspNet.Mvc.Core/FormatterMappings.cs b/src/Microsoft.AspNet.Mvc.Core/FormatterMappings.cs new file mode 100644 index 0000000000..59e5cf7eab --- /dev/null +++ b/src/Microsoft.AspNet.Mvc.Core/FormatterMappings.cs @@ -0,0 +1,54 @@ +// 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 Microsoft.Net.Http.Headers; +using System.Collections.ObjectModel; +using System.Globalization; +using Microsoft.AspNet.Mvc.Core; + +namespace Microsoft.AspNet.Mvc +{ + /// + /// These options are used to specify mapping between the Url Format and corresponding ContentType. + /// + public class FormatterMappings + { + private readonly Dictionary _map = + new Dictionary(StringComparer.OrdinalIgnoreCase); + + public void SetFormatMapping([NotNull] string format, [NotNull] MediaTypeHeaderValue contentType) + { + if (contentType == null) + { + throw new ArgumentException((Resources.ArgumentCannotBeNullOrEmpty), "contentType"); + } + + format = RemovePeriodIfPresent(format); + _map[format] = contentType; + } + + public MediaTypeHeaderValue GetContentTypeForFormat(string format) + { + format = RemovePeriodIfPresent(format); + MediaTypeHeaderValue value = null; + _map.TryGetValue(format, out value); + return value; + } + + private string RemovePeriodIfPresent(string format) + { + if (string.IsNullOrEmpty(format)) + { + throw new ArgumentException(Resources.ArgumentCannotBeNullOrEmpty, "format"); + } + if (format.StartsWith(".")) + { + format = format.Substring(1); + } + + return format; + } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Mvc.Core/MvcOptions.cs b/src/Microsoft.AspNet.Mvc.Core/MvcOptions.cs index 10fb86df8c..6fb389ca7d 100644 --- a/src/Microsoft.AspNet.Mvc.Core/MvcOptions.cs +++ b/src/Microsoft.AspNet.Mvc.Core/MvcOptions.cs @@ -18,7 +18,6 @@ namespace Microsoft.AspNet.Mvc { private AntiForgeryOptions _antiForgeryOptions = new AntiForgeryOptions(); private int _maxModelStateErrors = ModelStateDictionary.DefaultMaxAllowedErrors; - //private OutputFormatterOptions _outputFormatterOptions = new OutputFormatterOptions(); public MvcOptions() { @@ -29,7 +28,7 @@ namespace Microsoft.AspNet.Mvc OutputFormatters = new List(); InputFormatters = new List(); Filters = new List(); - OutputFormatterOptions = new OutputFormatterOptions(); + FormatterMappings = new FormatterMappings(); } /// @@ -55,7 +54,7 @@ namespace Microsoft.AspNet.Mvc } } - public OutputFormatterOptions OutputFormatterOptions { get; } + public FormatterMappings FormatterMappings { get; } /// /// Gets a list of which are used to construct filters that @@ -69,16 +68,6 @@ namespace Microsoft.AspNet.Mvc /// public List OutputFormatters { get; private set; } - /// - /// Sets the mapping for output format specified in URL (extension) and content type - /// - /// URL extension for output format - /// Content type mapping to the format - public void AddFormatMapping(string format, MediaTypeHeaderValue contentType) - { - OutputFormatterOptions.AddFormatMapping(format, contentType); - } - /// /// Gets a list of the which are used to construct /// a list of by . diff --git a/src/Microsoft.AspNet.Mvc.Core/OutputFormatterOptions.cs b/src/Microsoft.AspNet.Mvc.Core/OutputFormatterOptions.cs deleted file mode 100644 index f66453ad64..0000000000 --- a/src/Microsoft.AspNet.Mvc.Core/OutputFormatterOptions.cs +++ /dev/null @@ -1,46 +0,0 @@ -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 af6a915106..4f5944b3cc 100644 --- a/src/Microsoft.AspNet.Mvc.Core/project.json +++ b/src/Microsoft.AspNet.Mvc.Core/project.json @@ -13,9 +13,7 @@ "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.Net.Http": "2.2.13.0", -"Microsoft.Net.Http.Server": "1.0.0.0-rc1-11332" + "Microsoft.Framework.Runtime.Interfaces": { "version": "1.0.0-*", "type": "build" } }, "frameworks": { "aspnet50": {}, diff --git a/src/Microsoft.AspNet.Mvc/MvcOptionsSetup.cs b/src/Microsoft.AspNet.Mvc/MvcOptionsSetup.cs index 52f23a68f4..2a3538124e 100644 --- a/src/Microsoft.AspNet.Mvc/MvcOptionsSetup.cs +++ b/src/Microsoft.AspNet.Mvc/MvcOptionsSetup.cs @@ -46,9 +46,8 @@ namespace Microsoft.AspNet.Mvc options.OutputFormatters.Add(new StringOutputFormatter()); options.OutputFormatters.Add(new JsonOutputFormatter()); - // Set up default mapping for xml and json extensions to content type - options.AddFormatMapping("json", MediaTypeHeaderValue.Parse("application/json")); - options.AddFormatMapping("xml", MediaTypeHeaderValue.Parse("application/xml")); + // Set up default mapping for json extensions to content type + options.FormatterMappings.SetFormatMapping("json", MediaTypeHeaderValue.Parse("application/json")); // Set up default input formatters. options.InputFormatters.Add(new JsonInputFormatter()); diff --git a/test/Microsoft.AspNet.Mvc.Core.Test/Filters/FormatFilterTest.cs b/test/Microsoft.AspNet.Mvc.Core.Test/Filters/FormatFilterTest.cs index f5583860e2..7ab55493d2 100644 --- a/test/Microsoft.AspNet.Mvc.Core.Test/Filters/FormatFilterTest.cs +++ b/test/Microsoft.AspNet.Mvc.Core.Test/Filters/FormatFilterTest.cs @@ -1,223 +1,348 @@ -using System; -using System.Threading.Tasks; -using Microsoft.AspNet.Mvc.Core.Filters; -using Microsoft.AspNet.PipelineCore; +// 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.Http; +using Microsoft.AspNet.Mvc; +using Microsoft.AspNet.Http.Core; using Microsoft.AspNet.Routing; +using Microsoft.Framework.DependencyInjection; +using Microsoft.Framework.OptionsModel; using Microsoft.Net.Http.Headers; using Xunit; #if ASPNET50 using Moq; -using Microsoft.Framework.OptionsModel; -using Microsoft.AspNet.Http; +using System.Net; #endif -namespace Microsoft.AspNet.Mvc.Core.Test +namespace Microsoft.AspNet.Mvc { - public enum FormatPlace - { - RouteData, - QueryData, - RouteAndQueryData - } - public class FormatFilterTests { + public enum FormatSource + { + RouteData, + QueryData, + RouteAndQueryData + } + +#if ASPNET50 [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, + [InlineData("json", FormatSource.RouteData, "application/json")] + [InlineData("json", FormatSource.QueryData, "application/json")] + [InlineData("json", FormatSource.RouteAndQueryData, "application/json")] + public void FormatFilter_ContextContainsFormat_DefaultFormat( + string format, + FormatSource place, string contentType) { // Arrange - var mediaType = MediaTypeHeaderValue.Parse(contentType); - var context = CreateResultExecutingContext(format, place); - var filter = new FormatFilter(); + var mediaType = MediaTypeHeaderValue.Parse("application/json"); + var resultExecutingContext = CreateResultExecutingContext( + format, + FormatSource.RouteData); + var resourceExecutingContext = CreateResourceExecutingContext( + new IFilter[] { }, + format, + FormatSource.RouteData); + var filter = new FormatFilterAttribute(); // Act - filter.OnResultExecuting(context); + filter.OnResourceExecuting(resourceExecutingContext); + + // Assert + Assert.Null(resourceExecutingContext.Result); + + // Act + filter.OnResultExecuting(resultExecutingContext); // Assert - var objectResult = context.Result as ObjectResult; + var objectResult = Assert.IsType(resultExecutingContext.Result); Assert.Equal(1, objectResult.ContentTypes.Count); - ValidateMediaType(mediaType, objectResult.ContentTypes[0]); + AssertMediaTypesEqual(mediaType, objectResult.ContentTypes[0]); + } + + [Fact] + public void FormatFilter_ContextContainsFormat_InRouteAndQueryData() + { + // Arrange + var mediaType = MediaTypeHeaderValue.Parse("application/json"); + + var httpContext = CreateMockHttpContext(); + + // Routedata contains json + var data = new RouteData(); + data.Values.Add("format", "json"); + + // Query contains xml + httpContext.Setup(c => c.Request.Query.ContainsKey("format")).Returns(true); + httpContext.Setup(c => c.Request.Query.Get("format")).Returns("xml"); + var ac = new ActionContext(httpContext.Object, data, new ActionDescriptor()); + + var resultExecutingContext = new ResultExecutingContext( + ac, + new IFilter[] { }, + new ObjectResult("Hello!"), + controller: new object()); + + var resourceExecutingContext = new ResourceExecutingContext( + ac, + new IFilter[] { }); + + var filter = new FormatFilterAttribute(); + + // Act + filter.OnResourceExecuting(resourceExecutingContext); + filter.OnResultExecuting(resultExecutingContext); + + // Assert + var objectResult = Assert.IsType(resultExecutingContext.Result); + Assert.Equal(1, objectResult.ContentTypes.Count); + AssertMediaTypesEqual(mediaType, objectResult.ContentTypes[0]); } [Theory] - [InlineData("foo", FormatPlace.RouteData, "application/foo")] - [InlineData("foo", FormatPlace.QueryData, "application/foo")] - [InlineData("foo", FormatPlace.RouteAndQueryData, "application/foo")] + [InlineData("foo", FormatSource.RouteData, "application/foo")] + [InlineData("foo", FormatSource.QueryData, "application/foo")] + [InlineData("foo", FormatSource.RouteAndQueryData, "application/foo")] public void FormatFilter_ContextContainsFormat_Custom( string format, - FormatPlace place, + FormatSource 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(); + var resultExecutingContext = CreateResultExecutingContext(format, place); + var resourceExecutingContext = CreateResourceExecutingContext(new IFilter[] { }, format, place); + var options = resultExecutingContext.HttpContext.RequestServices.GetService>(); + options.Options.FormatterMappings.SetFormatMapping(format, MediaTypeHeaderValue.Parse(contentType)); + + var filter = new FormatFilterAttribute(); // Act - filter.OnResultExecuting(context); - + filter.OnResourceExecuting(resourceExecutingContext); + filter.OnResultExecuting(resultExecutingContext); + // Assert - var objectResult = context.Result as ObjectResult; + var objectResult = Assert.IsType(resultExecutingContext.Result); Assert.Equal(1, objectResult.ContentTypes.Count); - ValidateMediaType(mediaType, objectResult.ContentTypes[0]); + AssertMediaTypesEqual(mediaType, objectResult.ContentTypes[0]); } [Theory] - [InlineData("foo", FormatPlace.RouteData, "application/foo")] - public void FormatFilter_ContextContainsFormat_NonExisting( + [InlineData("foo", FormatSource.RouteData, "application/foo")] + [InlineData("foo", FormatSource.QueryData, "application/foo")] + public void FormatFilter_ContextContainsNonExistingFormat( string format, - FormatPlace place, + FormatSource place, string contentType) { // Arrange var mediaType = MediaTypeHeaderValue.Parse(contentType); var resourceExecutingContext = CreateResourceExecutingContext(new IFilter[] { }, format, place); - var filter = new FormatFilter(); + var filter = new FormatFilterAttribute(); // Act filter.OnResourceExecuting(resourceExecutingContext); // Assert var actionResult = resourceExecutingContext.Result; - Assert.True(actionResult is HttpNotFoundResult); + Assert.IsType(actionResult); } [Fact] public void FormatFilter_ContextDoesntContainFormat() { - // Arrange + // Arrange var resourceExecutingContext = CreateResourceExecutingContext(new IFilter[] { }); - var filter = new FormatFilter(); + var filter = new FormatFilterAttribute(); // Act filter.OnResourceExecuting(resourceExecutingContext); // Assert - var result = resourceExecutingContext.Result as IActionResult; - Assert.False(result is HttpNotFoundResult); + Assert.Null(resourceExecutingContext.Result); } [Theory] - [InlineData("json", FormatPlace.RouteData, "application/json")] - [InlineData("json", FormatPlace.QueryData, "application/json")] + [InlineData("json", FormatSource.RouteData, "application/json")] + [InlineData("json", FormatSource.QueryData, "application/json")] public void FormatFilter_ContextContainsFormat_ContainsProducesFilter_Matching( string format, - FormatPlace place, + FormatSource 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(); + var filter = new FormatFilterAttribute(); // Act filter.OnResourceExecuting(context); - // Assert - var result = context.Result as IActionResult; - Assert.False(result is HttpNotFoundResult); + // Assert + Assert.Null(context.Result); + } + + [Fact] + public void FormatFilter_ContextContainsFormat_ContainsProducesFilter_WildCardMatching() + { + // Arrange + var produces = new ProducesAttribute( + "application/baz", + new string[] { "application/foo", "text/bar" }); + var context = CreateResourceExecutingContext(new IFilter[] { produces }, "star", FormatSource.RouteData); + var options = context.HttpContext.RequestServices.GetService>(); + options.Options.FormatterMappings.SetFormatMapping("star", MediaTypeHeaderValue.Parse("application/*")); + + var filter = new FormatFilterAttribute(); + + // Act + filter.OnResourceExecuting(context); + + // Assert + Assert.Null(context.Result); } [Theory] - [InlineData("json", FormatPlace.RouteData, "application/json")] - [InlineData("json", FormatPlace.QueryData, "application/json")] + [InlineData("json", FormatSource.RouteData, "application/json")] + [InlineData("json", FormatSource.QueryData, "application/json")] public void FormatFilter_ContextContainsFormat_ContainsProducesFilter_Conflicting( string format, - FormatPlace place, + FormatSource 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(); + var filter = new FormatFilterAttribute(); // Act filter.OnResourceExecuting(context); // Assert - var result = context.Result as IActionResult; - Assert.True(result is HttpNotFoundResult); + var result = Assert.IsType(context.Result); + } + + [Theory] + [InlineData("", FormatSource.RouteData)] + [InlineData(null, FormatSource.QueryData)] + [InlineData("", FormatSource.RouteData)] + [InlineData(null, FormatSource.QueryData)] + public void FormatFilter_ContextContainsFormat_Invalid( + string format, + FormatSource place) + { + // Arrange + var resourceExecutingContext = CreateResourceExecutingContext( + new IFilter[] { }, + format, + FormatSource.RouteData); + var filter = new FormatFilterAttribute(); + + // Act + filter.OnResourceExecuting(resourceExecutingContext); + + // Assert + Assert.Null(resourceExecutingContext.Result); + } + + [Theory] + [InlineData("json", FormatSource.RouteData, "application/json")] + [InlineData("json", FormatSource.QueryData, "application/json")] + [InlineData("", FormatSource.RouteAndQueryData, null)] + [InlineData(null, FormatSource.RouteAndQueryData, null)] + public void FormatFilter_GetContentTypeForRequest( + string format, + FormatSource place, + string contentType) + { + // Arrange + var resourceExecutingContext = CreateResourceExecutingContext( + new IFilter[] { }, + format, + FormatSource.RouteData); + var filter = new FormatFilterAttribute(); + var returnContentType = filter.GetContentTypeForCurrentRequest(resourceExecutingContext); + + + MediaTypeHeaderValue mediaType = null; + if (returnContentType != null) + { + mediaType = MediaTypeHeaderValue.Parse("application/json"); + } + + Assert.Equal(mediaType, returnContentType); } private static ResourceExecutingContext CreateResourceExecutingContext( IFilter[] filters, string format = null, - FormatPlace? place = null) + FormatSource? place = null) { - if(format == null || 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) + FormatSource? place = null) { - if (format == null || place == null) + if (format == null && place == null) { return new ResultExecutingContext( new ActionContext(new DefaultHttpContext(), new RouteData(), new ActionDescriptor()), new IFilter[] { }, - new ObjectResult("Some Value")); + new ObjectResult("Some Value"), + controller: new object()); } return new ResultExecutingContext( CreateActionContext(format, place), new IFilter[] { }, - new ObjectResult("Some Value")); + new ObjectResult("Some Value"), + controller: new object()); } - private static ActionContext CreateActionContext(string format = null, FormatPlace? place = null) + private static ActionContext CreateActionContext(string format = null, FormatSource? place = null) { var httpContext = CreateMockHttpContext(); + var data = new RouteData(); - if (place == FormatPlace.RouteData || place == FormatPlace.RouteAndQueryData) - { - var data = new RouteData(); + if (place == FormatSource.RouteData || place == FormatSource.RouteAndQueryData) + { data.Values.Add("format", format); - httpContext.Setup(c => c.Request.Query.ContainsKey("format")).Returns(false); - return new ActionContext(httpContext.Object, data, new ActionDescriptor()); + httpContext.Setup(c => c.Request.Query.ContainsKey("format")).Returns(false); } - if (place == FormatPlace.QueryData || place == FormatPlace.RouteAndQueryData) + if (place == FormatSource.QueryData || place == FormatSource.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()); + httpContext.Setup(c => c.Request.Query.Get("format")).Returns(format); } - else if(place == null && format == null) + 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()); + httpContext.Setup(c => c.Request.Query.ContainsKey("format")).Returns(false); } - return null; + return new ActionContext(httpContext.Object, data, new ActionDescriptor()); } private static Mock CreateMockHttpContext() { - MvcOptions options = new MvcOptions(); + var options = new MvcOptions(); MvcOptionsSetup.ConfigureMvc(options); var mvcOptions = new Mock>(); mvcOptions.Setup(o => o.Options).Returns(options); @@ -236,7 +361,9 @@ namespace Microsoft.AspNet.Mvc.Core.Test return httpContext; } - private static void ValidateMediaType(MediaTypeHeaderValue expectedMediaType, MediaTypeHeaderValue actualMediaType) + private static void AssertMediaTypesEqual( + MediaTypeHeaderValue expectedMediaType, + MediaTypeHeaderValue actualMediaType) { Assert.Equal(expectedMediaType.MediaType, actualMediaType.MediaType); Assert.Equal(expectedMediaType.SubType, actualMediaType.SubType); @@ -249,5 +376,6 @@ namespace Microsoft.AspNet.Mvc.Core.Test Assert.Equal(item.Value, NameValueHeaderValue.Find(actualMediaType.Parameters, item.Name).Value); } } +#endif } } \ No newline at end of file diff --git a/test/Microsoft.AspNet.Mvc.Core.Test/Filters/ProducesAttributeTests.cs b/test/Microsoft.AspNet.Mvc.Core.Test/Filters/ProducesAttributeTests.cs index 718648b2f6..9f699627e9 100644 --- a/test/Microsoft.AspNet.Mvc.Core.Test/Filters/ProducesAttributeTests.cs +++ b/test/Microsoft.AspNet.Mvc.Core.Test/Filters/ProducesAttributeTests.cs @@ -9,6 +9,10 @@ using Microsoft.AspNet.Routing; using Microsoft.Net.Http.Headers; using Xunit; +#if ASPNET50 +using Moq; +#endif + namespace Microsoft.AspNet.Mvc.Test { public class ProducesAttributeTests @@ -20,7 +24,7 @@ namespace Microsoft.AspNet.Mvc.Test var mediaType1 = MediaTypeHeaderValue.Parse("application/json"); var mediaType2 = MediaTypeHeaderValue.Parse("text/json;charset=utf-8"); var producesContentAttribute = new ProducesAttribute("application/json", "text/json;charset=utf-8"); - var resultExecutingContext = CreateResultExecutingContext(producesContentAttribute); + var resultExecutingContext = CreateResultExecutingContext(new IFilter[] { producesContentAttribute }); var next = new ResultExecutionDelegate( () => Task.FromResult(CreateResultExecutedContext(resultExecutingContext))); @@ -33,6 +37,33 @@ namespace Microsoft.AspNet.Mvc.Test ValidateMediaType(mediaType1, objectResult.ContentTypes[0]); ValidateMediaType(mediaType2, objectResult.ContentTypes[1]); } + + [Fact] + public async Task ProducesContentAttribute_FormatFilterAttribute() + { + // Arrange + var mediaType1 = MediaTypeHeaderValue.Parse("application/xml"); + var mediaType2 = MediaTypeHeaderValue.Parse("application/json"); + var producesContentAttribute = new ProducesAttribute("application/xml"); + + var formatFilter = new Mock(); + formatFilter.Setup(f => f.GetContentTypeForCurrentRequest(It.IsAny())) + .Returns(mediaType2); + + var filters = new IFilter[] { producesContentAttribute, formatFilter.Object }; + var resultExecutingContext = CreateResultExecutingContext(filters); + + var next = new ResultExecutionDelegate( + () => Task.FromResult(CreateResultExecutedContext(resultExecutingContext))); + + // Act + await producesContentAttribute.OnResultExecutionAsync(resultExecutingContext, next); + + // Assert + var objectResult = Assert.IsType(resultExecutingContext.Result); + Assert.Equal(0, objectResult.ContentTypes.Count); + } + [Theory] [InlineData("", "")] @@ -95,13 +126,12 @@ namespace Microsoft.AspNet.Mvc.Test return new ResultExecutedContext(context, context.Filters, context.Result, context.Controller); } - private static ResultExecutingContext CreateResultExecutingContext(IFilter filter) + private static ResultExecutingContext CreateResultExecutingContext(IFilter[] filters) { return new ResultExecutingContext( CreateActionContext(), - new IFilter[] { filter, }, - new ObjectResult("Some Value"), - controller: new object()); + filters, + new ObjectResult("Some Value")); } private static ActionContext CreateActionContext() diff --git a/test/Microsoft.AspNet.Mvc.Core.Test/FormatterMappingsTest.cs b/test/Microsoft.AspNet.Mvc.Core.Test/FormatterMappingsTest.cs new file mode 100644 index 0000000000..38965622c7 --- /dev/null +++ b/test/Microsoft.AspNet.Mvc.Core.Test/FormatterMappingsTest.cs @@ -0,0 +1,53 @@ +// 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.Net.Http.Headers; +using Xunit; + + +namespace Microsoft.AspNet.Mvc +{ + public class FormatterMappingsTest + { + [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 FormatterMappings_SetFormatMapping_DiffSetGetFormat(string setFormat, string contentType, string getFormat) + { + // Arrange + var mediaType = MediaTypeHeaderValue.Parse(contentType); + var options = new FormatterMappings(); + options.SetFormatMapping(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 FormatterMappings_SetFormatMapping_Invalid(string format, string contentType) + { + // Arrange + MediaTypeHeaderValue mediaType = null; + if (!string.IsNullOrEmpty(contentType)) + { + mediaType = MediaTypeHeaderValue.Parse(contentType); + } + + var options = new FormatterMappings(); + + // Act and Assert + Assert.Throws(() => options.SetFormatMapping(format, mediaType)); + } + } +} \ 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 deleted file mode 100644 index c205d7151b..0000000000 --- a/test/Microsoft.AspNet.Mvc.Core.Test/OutputFormatterOptionsTest.cs +++ /dev/null @@ -1,72 +0,0 @@ -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 diff --git a/test/Microsoft.AspNet.Mvc.Core.Test/Routing/AttributeRouteTest.cs b/test/Microsoft.AspNet.Mvc.Core.Test/Routing/AttributeRouteTest.cs index e26f675015..5f44dcfa77 100644 --- a/test/Microsoft.AspNet.Mvc.Core.Test/Routing/AttributeRouteTest.cs +++ b/test/Microsoft.AspNet.Mvc.Core.Test/Routing/AttributeRouteTest.cs @@ -245,6 +245,119 @@ namespace Microsoft.AspNet.Mvc.Routing } } + [Theory] + [InlineData("moo/{p1}.{p2?}", "/moo/foo.bar", "foo", "bar", null)] + [InlineData("moo/{p1?}", "/moo/foo", "foo", null, null)] + [InlineData("moo/{p1?}", "/moo", null, null, null)] + [InlineData("moo/{p1}.{p2?}", "/moo/foo", "foo", null, null)] + [InlineData("moo/{p1}.{p2?}", "/moo/foo..bar", "foo.", "bar", null)] + [InlineData("moo/{p1}.{p2?}", "/moo/foo.moo.bar", "foo.moo", "bar", null)] + [InlineData("moo/{p1}.{p2}", "/moo/foo.bar", "foo", "bar", null)] + [InlineData("moo/foo.{p1}.{p2?}", "/moo/foo.moo.bar", "moo", "bar", null)] + [InlineData("moo/foo.{p1}.{p2?}", "/moo/foo.moo", "moo", null, null)] + [InlineData("moo/.{p2?}", "/moo/.foo", null, "foo", null)] + [InlineData("moo/{p1}.{p2?}", "/moo/....", "..", ".", null)] + [InlineData("moo/{p1}.{p2?}", "/moo/.bar", ".bar", null, null)] + [InlineData("moo/{p1}.{p2}.{p3?}", "/moo/foo.moo.bar", "foo", "moo", "bar")] + [InlineData("moo/{p1}.{p2}.{p3?}", "/moo/foo.moo", "foo", "moo", null)] + [InlineData("moo/{p1}.{p2}.{p3}.{p4?}", "/moo/foo.moo.bar", "foo", "moo", "bar")] + [InlineData("{p1}.{p2?}/{p3}", "/foo.moo/bar", "foo", "moo", "bar")] + [InlineData("{p1}.{p2?}/{p3}", "/foo/bar", "foo", null, "bar")] + [InlineData("{p1}.{p2?}/{p3}", "/.foo/bar", ".foo", null, "bar")] + public async Task AttributeRoute_WithOptionalCompositeParameter_Valid( + string template, + string request, + string p1, + string p2, + string p3) + { + // Arrange + var expectedRouteGroup = string.Format("{0}&&{1}", 0, template); + + // We need to force the creation of a closure in order to avoid an issue with Moq and Roslyn. + var numberOfCalls = 0; + Action callBack = ctx => { ctx.IsHandled = true; numberOfCalls++; }; + + var next = new Mock(); + next.Setup(r => r.RouteAsync(It.IsAny())) + .Callback(callBack) + .Returns(Task.FromResult(true)) + .Verifiable(); + + var firstRoute = CreateMatchingEntry(next.Object, template, order: 0); + + // We setup the route entries in reverse order of precedence to ensure that when we + // try to route the request, the route with a higher precedence gets tried first. + var matchingRoutes = new[] { firstRoute }; + var linkGenerationEntries = Enumerable.Empty(); + var route = new AttributeRoute(next.Object, matchingRoutes, linkGenerationEntries, NullLoggerFactory.Instance); + var context = CreateRouteContext(request); + + // Act + await route.RouteAsync(context); + + // Assert + Assert.True(context.IsHandled); + if (p1 != null) + { + Assert.Equal(p1, context.RouteData.Values["p1"]); + } + if (p2 != null) + { + Assert.Equal(p2, context.RouteData.Values["p2"]); + } + if (p3 != null) + { + Assert.Equal(p3, context.RouteData.Values["p3"]); + } + } + + [Theory] + [InlineData("moo/{p1}.{p2?}", "/moo/foo.")] + [InlineData("moo/{p1}.{p2?}", "/moo/.")] + [InlineData("moo/{p1}.{p2}", "/foo.")] + [InlineData("moo/{p1}.{p2}", "/foo")] + [InlineData("moo/{p1}.{p2}.{p3?}", "/moo/foo.moo.")] + [InlineData("moo/foo.{p2}.{p3?}", "/moo/bar.foo.moo")] + [InlineData("moo/foo.{p2}.{p3?}", "/moo/kungfoo.moo.bar")] + [InlineData("moo/foo.{p2}.{p3?}", "/moo/kungfoo.moo")] + [InlineData("moo/{p1}.{p2}.{p3?}", "/moo/foo")] + [InlineData("{p1}.{p2?}/{p3}", "/foo./bar")] + [InlineData("moo/.{p2?}", "/moo/.")] + [InlineData("{p1}.{p2}/{p3}", "/.foo/bar")] + public async Task AttributeRoute_WithOptionalCompositeParameter_Invalid( + string template, + string request) + { + // Arrange + var expectedRouteGroup = string.Format("{0}&&{1}", 0, template); + + // We need to force the creation of a closure in order to avoid an issue with Moq and Roslyn. + var numberOfCalls = 0; + Action callBack = ctx => { ctx.IsHandled = true; numberOfCalls++; }; + + var next = new Mock(); + next.Setup(r => r.RouteAsync(It.IsAny())) + .Callback(callBack) + .Returns(Task.FromResult(true)) + .Verifiable(); + + var firstRoute = CreateMatchingEntry(next.Object, template, order: 0); + + // We setup the route entries in reverse order of precedence to ensure that when we + // try to route the request, the route with a higher precedence gets tried first. + var matchingRoutes = new[] { firstRoute }; + var linkGenerationEntries = Enumerable.Empty(); + var route = new AttributeRoute(next.Object, matchingRoutes, linkGenerationEntries, NullLoggerFactory.Instance); + var context = CreateRouteContext(request); + + // Act + await route.RouteAsync(context); + + // Assert + Assert.False(context.IsHandled); + } + [Theory] [InlineData("template/5", "template/{parameter:int}")] [InlineData("template/5", "template/{parameter}")] @@ -1196,6 +1309,96 @@ namespace Microsoft.AspNet.Mvc.Routing Assert.Equal("Store", path); } + public static IEnumerable OptionalParamValues + { + get + { + return new object[][] + { + // defaults + // ambient values + // values + new object[] + { + "Test/{val1}/{val2}.{val3?}", + new {val1 = "someval1", val2 = "someval2", val3 = "someval3a"}, + new {val3 = "someval3v"}, + "Test/someval1/someval2.someval3v", + }, + new object[] + { + "Test/{val1}/{val2}.{val3?}", + new {val3 = "someval3a"}, + new {val1 = "someval1", val2 = "someval2", val3 = "someval3v" }, + "Test/someval1/someval2.someval3v", + }, + new object[] + { + "Test/{val1}/{val2}.{val3?}", + null, + new {val1 = "someval1", val2 = "someval2" }, + "Test/someval1/someval2", + }, + new object[] + { + "Test/{val1}.{val2}.{val3}.{val4?}", + new {val1 = "someval1", val2 = "someval2" }, + new {val4 = "someval4", val3 = "someval3" }, + "Test/someval1.someval2.someval3.someval4", + }, + new object[] + { + "Test/{val1}.{val2}.{val3}.{val4?}", + new {val1 = "someval1", val2 = "someval2" }, + new {val3 = "someval3" }, + "Test/someval1.someval2.someval3", + }, + new object[] + { + "Test/.{val2?}", + null, + new {val2 = "someval2" }, + "Test/.someval2", + }, + new object[] + { + "Test/.{val2?}", + null, + null, + "Test/", + }, + new object[] + { + "Test/{val1}.{val2}", + new {val1 = "someval1", val2 = "someval2" }, + new {val3 = "someval3" }, + "Test/someval1.someval2?val3=someval3", + }, + }; + } + } + + [Theory] + [MemberData("OptionalParamValues")] + public void AttributeRoute_GenerateLink_Match_WithOptionalParameters( + string template, + object ambientValues, + object values, + string expected) + { + // Arrange + var entry = CreateGenerationEntry(template, null); + var route = CreateAttributeRoute(entry); + + var context = CreateVirtualPathContext(values, ambientValues); + + // Act + var path = route.GetVirtualPath(context); + + // Assert + Assert.Equal(expected, path); + } + [Fact] public async Task AttributeRoute_CreatesNewRouteData() { diff --git a/test/Microsoft.AspNet.Mvc.FunctionalTests/ConnegTests.cs b/test/Microsoft.AspNet.Mvc.FunctionalTests/ConnegTests.cs index 6ceb500855..709dea295e 100644 --- a/test/Microsoft.AspNet.Mvc.FunctionalTests/ConnegTests.cs +++ b/test/Microsoft.AspNet.Mvc.FunctionalTests/ConnegTests.cs @@ -405,5 +405,35 @@ namespace Microsoft.AspNet.Mvc.FunctionalTests // Assert Assert.Equal(HttpStatusCode.NotAcceptable, response.StatusCode); } + + [Fact] + public async Task ProducesAttribute_And_FormatFilterAttribute_Conflicting() + { + // Arrange + var server = TestServer.Create(_provider, _app); + var client = server.CreateClient(); + var expectedContentType = MediaTypeHeaderValue.Parse("application/json"); + + // Act + var response = await client.GetAsync("http://localhost/FormatFilter/MethodWithFormatFilter.json"); + + // Assert + Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); + } + + [Fact] + public async Task ProducesAttribute_And_FormatFilterAttribute_Collaborating() + { + // Arrange + var server = TestServer.Create(_provider, _app); + var client = server.CreateClient(); + + // Act + var response = await client.GetAsync("http://localhost/FormatFilter/MethodWithFormatFilter"); + + // Assert + var type = response.Content.Headers.ContentType; + var body = await response.Content.ReadAsStringAsync(); + } } } \ No newline at end of file diff --git a/test/Microsoft.AspNet.Mvc.FunctionalTests/FormatFilterTest.cs b/test/Microsoft.AspNet.Mvc.FunctionalTests/FormatFilterTest.cs new file mode 100644 index 0000000000..e5aee15f4d --- /dev/null +++ b/test/Microsoft.AspNet.Mvc.FunctionalTests/FormatFilterTest.cs @@ -0,0 +1,120 @@ +// 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.Threading.Tasks; +using Microsoft.AspNet.Builder; +using Microsoft.AspNet.TestHost; +using Xunit; + +namespace Microsoft.AspNet.Mvc.FunctionalTests +{ + public class FormatFilterTest + { + private readonly IServiceProvider _services = TestHelper.CreateServices("FormatFilterWebSite"); + private readonly Action _app = new FormatFilterWebSite.Startup().Configure; + + [Fact] + public async Task FormatFilter_NoExtensionInRequest() + { + // Arrange + var server = TestServer.Create(_services, _app); + var client = server.CreateClient(); + + // Act + var response = await client.GetAsync("http://localhost/FormatFilter/GetProduct/5"); + + // Assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.Equal(@"{""SampleInt"":5}", await response.Content.ReadAsStringAsync()); + } + + [Fact] + public async Task FormatFilter_ExtensionInRequest_Default() + { + // Arrange + var server = TestServer.Create(_services, _app); + var client = server.CreateClient(); + + // Act + var response = await client.GetAsync("http://localhost/FormatFilter/GetProduct/5.json"); + + // Assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.Equal(@"{""SampleInt"":5}", await response.Content.ReadAsStringAsync()); + } + + [Fact] + public async Task FormatFilter_ExtensionInRequest_Custom() + { + // 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] + public async Task FormatFilter_ExtensionInRequest_NonExistant() + { + // Arrange + var server = TestServer.Create(_services, _app); + var client = server.CreateClient(); + + // Act + var response = await client.GetAsync("http://localhost/FormatFilter/GetProduct/5.xml"); + + // Assert + Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); + } + + [Fact] + public async Task FormatFilter_And_ProducesFilter_Match() + { + // Arrange + var server = TestServer.Create(_services, _app); + var client = server.CreateClient(); + + // Act + var response = await client.GetAsync("http://localhost/FormatFilter/ProducesMethod/5.json"); + + // Assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.Equal(@"{""SampleInt"":5}", await response.Content.ReadAsStringAsync()); + } + + [Fact] + public async Task FormatFilter_And_ProducesFilter_Conflict() + { + // Arrange + var server = TestServer.Create(_services, _app); + var client = server.CreateClient(); + + // Act + var response = await client.GetAsync("http://localhost/FormatFilter/ProducesMethod/5.xml"); + + // Assert + Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); + } + + [Fact] + public async Task FormatFilter_And_OverrideProducesFilter() + { + // Arrange + var server = TestServer.Create(_services, _app); + var client = server.CreateClient(); + + // Act + var response = await client.GetAsync("http://localhost/ProducesDerived/ReturnClassName.json"); + + // Assert + Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); + } + } +} \ No newline at end of file diff --git a/test/Microsoft.AspNet.Mvc.FunctionalTests/project.json b/test/Microsoft.AspNet.Mvc.FunctionalTests/project.json index dfeaeca892..9f5a1f2abc 100644 --- a/test/Microsoft.AspNet.Mvc.FunctionalTests/project.json +++ b/test/Microsoft.AspNet.Mvc.FunctionalTests/project.json @@ -44,7 +44,8 @@ "MvcTagHelpersWebSite": "1.0.0", "Microsoft.Framework.ConfigurationModel.Json": "1.0.0-*", "xunit.runner.kre": "1.0.0-*", - "Microsoft.AspNet.WebUtilities": "1.0.0-*" + "Microsoft.AspNet.WebUtilities": "1.0.0-*", +"FormatFilterWebSite": "1.0.0-*" }, "commands": { "test": "xunit.runner.kre" diff --git a/test/WebSites/ConnegWebSite/Controllers/FormatFilterController.cs b/test/WebSites/ConnegWebSite/Controllers/FormatFilterController.cs new file mode 100644 index 0000000000..75ee7711b2 --- /dev/null +++ b/test/WebSites/ConnegWebSite/Controllers/FormatFilterController.cs @@ -0,0 +1,18 @@ +// 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 ConnegWebSite; +using Microsoft.AspNet.Mvc; + +namespace ConnegWebsite +{ + [Produces("application/FormatFilterController")] + public class FormatFilterController : Controller + { + [FormatFilter] + public User MethodWithFormatFilter() + { + return new User() { Name = "Joe", Address = "1 abc way" }; + } + } +} \ No newline at end of file diff --git a/test/WebSites/ConnegWebSite/Controllers/ProducesContentBaseController.cs b/test/WebSites/ConnegWebSite/Controllers/ProducesContentBaseController.cs index 2e35e28a32..082d3c6630 100644 --- a/test/WebSites/ConnegWebSite/Controllers/ProducesContentBaseController.cs +++ b/test/WebSites/ConnegWebSite/Controllers/ProducesContentBaseController.cs @@ -37,6 +37,6 @@ namespace ConnegWebSite public virtual string ReturnClassNameContentTypeOnDerivedAction() { return "ProducesContentBaseController"; - } + } } } \ No newline at end of file diff --git a/test/WebSites/ConnegWebSite/Controllers/ProducesContentOnClassController.cs b/test/WebSites/ConnegWebSite/Controllers/ProducesContentOnClassController.cs index 9b0acbdd10..92e6fb4717 100644 --- a/test/WebSites/ConnegWebSite/Controllers/ProducesContentOnClassController.cs +++ b/test/WebSites/ConnegWebSite/Controllers/ProducesContentOnClassController.cs @@ -39,6 +39,6 @@ namespace ConnegWebSite { // should be written using the content defined at derived class's class. return "ProducesContentOnClassController"; - } + } } } \ No newline at end of file diff --git a/test/WebSites/ConnegWebSite/Startup.cs b/test/WebSites/ConnegWebSite/Startup.cs index 12821f00e5..99f766494c 100644 --- a/test/WebSites/ConnegWebSite/Startup.cs +++ b/test/WebSites/ConnegWebSite/Startup.cs @@ -31,7 +31,7 @@ namespace ConnegWebSite app.UseMvc(routes => { routes.MapRoute("ActionAsMethod", "{controller}/{action}", - defaults: new { controller = "Home", action = "Index" }); + defaults: new { controller = "Home", action = "Index" }); }); } } diff --git a/test/WebSites/FiltersWebSite/FiltersWebSite.kproj b/test/WebSites/FiltersWebSite/FiltersWebSite.kproj index b979cc3d87..b7fd6c388d 100644 --- a/test/WebSites/FiltersWebSite/FiltersWebSite.kproj +++ b/test/WebSites/FiltersWebSite/FiltersWebSite.kproj @@ -15,4 +15,9 @@ 49641 + + + + + \ No newline at end of file diff --git a/test/WebSites/FiltersWebSite/Startup.cs b/test/WebSites/FiltersWebSite/Startup.cs index 848e9ed791..13cdf43215 100644 --- a/test/WebSites/FiltersWebSite/Startup.cs +++ b/test/WebSites/FiltersWebSite/Startup.cs @@ -5,6 +5,7 @@ using System.Security.Claims; using Microsoft.AspNet.Builder; using Microsoft.AspNet.Mvc; using Microsoft.AspNet.Security; +using Microsoft.AspNet.Routing; using Microsoft.Framework.DependencyInjection; namespace FiltersWebSite diff --git a/test/WebSites/FormatFilterWebSite/Controllers/FormatFilterController.cs b/test/WebSites/FormatFilterWebSite/Controllers/FormatFilterController.cs new file mode 100644 index 0000000000..aafbaee7b7 --- /dev/null +++ b/test/WebSites/FormatFilterWebSite/Controllers/FormatFilterController.cs @@ -0,0 +1,22 @@ +// 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 Microsoft.AspNet.Mvc; + +namespace FormatFilterWebSite +{ + [FormatFilter] + public class FormatFilterController : Controller + { + public Product GetProduct(int id) + { + return new Product() { SampleInt = id }; + } + + [Produces("application/custom", "application/json", "text/json")] + public Product ProducesMethod(int id) + { + return new Product() { SampleInt = id }; + } + } +} \ No newline at end of file diff --git a/test/WebSites/FormatFilterWebSite/Controllers/ProducesBaseClass.cs b/test/WebSites/FormatFilterWebSite/Controllers/ProducesBaseClass.cs new file mode 100644 index 0000000000..1281a339bb --- /dev/null +++ b/test/WebSites/FormatFilterWebSite/Controllers/ProducesBaseClass.cs @@ -0,0 +1,17 @@ +// 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 Microsoft.AspNet.Mvc; + +namespace FormatFilterWebSite +{ + public class ProducesBaseController : Controller + { + [Produces("application/custom_ProducesBaseController_Action")] + public virtual string ReturnClassName() + { + // Should be written using the action's content type. Overriding the one at the class. + return "ProducesBaseController"; + } + } +} \ No newline at end of file diff --git a/test/WebSites/FormatFilterWebSite/Controllers/ProducesOverrideClass.cs b/test/WebSites/FormatFilterWebSite/Controllers/ProducesOverrideClass.cs new file mode 100644 index 0000000000..3a47305329 --- /dev/null +++ b/test/WebSites/FormatFilterWebSite/Controllers/ProducesOverrideClass.cs @@ -0,0 +1,18 @@ +// 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 Microsoft.AspNet.Mvc; + +namespace FormatFilterWebSite +{ + [Produces("application/custom_ProducesOverrideController")] + public class ProducesOverrideController : ProducesBaseController + { + [FormatFilter] + public override string ReturnClassName() + { + // should be written using the content defined at base class's action. + return "ProducesOverrideController"; + } + } +} \ No newline at end of file diff --git a/test/WebSites/FormatFilterWebSite/CustomFormatter.cs b/test/WebSites/FormatFilterWebSite/CustomFormatter.cs new file mode 100644 index 0000000000..08d0df4045 --- /dev/null +++ b/test/WebSites/FormatFilterWebSite/CustomFormatter.cs @@ -0,0 +1,43 @@ +// 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.Text; +using System.Threading.Tasks; +using Microsoft.AspNet.Http; +using Microsoft.AspNet.Mvc; +using Microsoft.Net.Http.Headers; + +namespace FormatFilterWebSite +{ + public class CustomFormatter : OutputFormatter + { + public string ContentType { get; private set; } + + public CustomFormatter(string contentType) + { + ContentType = contentType; + SupportedMediaTypes.Add(MediaTypeHeaderValue.Parse(contentType)); + SupportedEncodings.Add(Encoding.GetEncoding("utf-8")); + } + + public override bool CanWriteResult(OutputFormatterContext context, MediaTypeHeaderValue contentType) + { + if (base.CanWriteResult(context, contentType)) + { + var actionReturn = context.Object as Product; + if (actionReturn != null) + { + return true; + } + } + return false; + } + + public override async Task WriteResponseBodyAsync(OutputFormatterContext context) + { + var response = context.ActionContext.HttpContext.Response; + response.ContentType = ContentType + ";charset=utf-8"; + await response.WriteAsync(context.Object.ToString()); + } + } +} \ No newline at end of file diff --git a/test/WebSites/FormatFilterWebSite/FormatFilterWebSite.kproj b/test/WebSites/FormatFilterWebSite/FormatFilterWebSite.kproj new file mode 100644 index 0000000000..7cd6d33886 --- /dev/null +++ b/test/WebSites/FormatFilterWebSite/FormatFilterWebSite.kproj @@ -0,0 +1,24 @@ + + + + 14.0 + $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) + + + + ac9be567-540e-4c70-90c2-aaf021307a80 + FormatFilterWebSite + ..\..\..\artifacts\obj\$(MSBuildProjectName) + ..\..\..\artifacts\bin\$(MSBuildProjectName)\ + + + 2.0 + 51135 + + + + + + + + \ No newline at end of file diff --git a/test/WebSites/FormatFilterWebSite/Models/Product.cs b/test/WebSites/FormatFilterWebSite/Models/Product.cs new file mode 100644 index 0000000000..1342473416 --- /dev/null +++ b/test/WebSites/FormatFilterWebSite/Models/Product.cs @@ -0,0 +1,14 @@ +using System; + +namespace FormatFilterWebSite +{ + public class Product + { + public int SampleInt { get; set; } + + public override string ToString() + { + return "SampleInt:" + SampleInt; + } + } +} \ No newline at end of file diff --git a/test/WebSites/FormatFilterWebSite/Startup.cs b/test/WebSites/FormatFilterWebSite/Startup.cs new file mode 100644 index 0000000000..5486deb46e --- /dev/null +++ b/test/WebSites/FormatFilterWebSite/Startup.cs @@ -0,0 +1,43 @@ +// 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 Microsoft.AspNet.Builder; +using Microsoft.AspNet.Mvc; +using Microsoft.AspNet.Routing; +using Microsoft.Framework.DependencyInjection; +using Microsoft.Net.Http.Headers; + +namespace FormatFilterWebSite +{ + public class Startup + { + public void Configure(IApplicationBuilder app) + { + var configuration = app.GetTestConfiguration(); + + app.UseServices(services => + { + services.AddMvc(configuration); + services.Configure(options => + { + var formatFilter = new FormatFilterAttribute(); + options.Filters.Add(formatFilter); + + var customFormatter = new CustomFormatter("application/custom"); + options.OutputFormatters.Add(customFormatter); + + options.FormatterMappings.SetFormatMapping( + "custom", + MediaTypeHeaderValue.Parse("application/custom")); + }); + }); + + app.UseMvc(routes => + { + routes.MapRoute("formatroute", + "{controller}/{action}/{id}.{format?}", + new { controller = "Home", action = "Index" }); + }); + } + } +} \ No newline at end of file diff --git a/test/WebSites/FormatFilterWebSite/project.json b/test/WebSites/FormatFilterWebSite/project.json new file mode 100644 index 0000000000..464f3bbc61 --- /dev/null +++ b/test/WebSites/FormatFilterWebSite/project.json @@ -0,0 +1,19 @@ +{ + "commands": { + "web": "Microsoft.AspNet.Hosting server=Microsoft.AspNet.Server.WebListener server.urls=http://localhost:5001", + "kestrel": "Microsoft.AspNet.Hosting --server Kestrel --server.urls http://localhost:5000" + }, + "dependencies": { + "Kestrel": "1.0.0-*", + "Microsoft.AspNet.Mvc": "6.0.0-*", + "Microsoft.AspNet.Mvc.TestConfiguration": "1.0.0", + "Microsoft.AspNet.Server.IIS": "1.0.0-*", + "Microsoft.AspNet.Server.WebListener": "1.0.0-*", + "Microsoft.AspNet.StaticFiles": "1.0.0-*" + }, + "frameworks": { + "aspnet50": { }, + "aspnetcore50": { } + }, + "webroot": "wwwroot" +} \ No newline at end of file