From ad208442d8c27f3c25481fc553603398dd5706ef Mon Sep 17 00:00:00 2001 From: Pranav K Date: Wed, 8 Oct 2014 12:08:55 -0700 Subject: [PATCH 01/39] Updating functional tests to restore CallContextServiceLocator.Locator.Service on test finish --- .../CustomUrlHelperTests.cs | 69 ++++----- .../MvcSampleTests.cs | 139 ++++++++++-------- .../TestHelper.cs | 33 ++++- test/WebSites/UrlHelperWebSite/Config.json | 5 - test/WebSites/UrlHelperWebSite/Startup.cs | 7 +- 5 files changed, 144 insertions(+), 109 deletions(-) delete mode 100644 test/WebSites/UrlHelperWebSite/Config.json diff --git a/test/Microsoft.AspNet.Mvc.FunctionalTests/CustomUrlHelperTests.cs b/test/Microsoft.AspNet.Mvc.FunctionalTests/CustomUrlHelperTests.cs index 6d0c4fefaa..b94d625bda 100644 --- a/test/Microsoft.AspNet.Mvc.FunctionalTests/CustomUrlHelperTests.cs +++ b/test/Microsoft.AspNet.Mvc.FunctionalTests/CustomUrlHelperTests.cs @@ -1,8 +1,5 @@ using System; -using System.IO; using System.Net; -using System.Net.Http; -using System.Text; using System.Threading.Tasks; using Microsoft.AspNet.Builder; using Microsoft.AspNet.TestHost; @@ -26,35 +23,39 @@ public class CustomUrlHelperTests [Fact] public async Task CustomUrlHelper_GeneratesUrlFromController() { - // Arrange - var server = TestServer.Create(_services, _app); - var client = server.CreateClient(); + using (TestHelper.ReplaceCallContextServiceLocationService(_services)) + { + // Arrange + var server = TestServer.Create(_services, _app); + var client = server.CreateClient(); - // Act - var response = await client.GetAsync("http://localhost/Home/UrlContent"); + // Act + var response = await client.GetAsync("http://localhost/Home/UrlContent"); + var responseData = await response.Content.ReadAsStringAsync(); - string responseData = await response.Content.ReadAsStringAsync(); - - //Assert - Assert.Equal(HttpStatusCode.OK, response.StatusCode); - Assert.Equal(_cdnServerBaseUrl + "/bootstrap.min.css", responseData); + //Assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.Equal(_cdnServerBaseUrl + "/bootstrap.min.css", responseData); + } } [Fact] public async Task CustomUrlHelper_GeneratesUrlFromView() { - // Arrange - var server = TestServer.Create(_services, _app); - var client = server.CreateClient(); + using (TestHelper.ReplaceCallContextServiceLocationService(_services)) + { + // Arrange + var server = TestServer.Create(_services, _app); + var client = server.CreateClient(); - // Act - var response = await client.GetAsync("http://localhost/Home/Index"); + // Act + var response = await client.GetAsync("http://localhost/Home/Index"); + var responseData = await response.Content.ReadAsStringAsync(); - string responseData = await response.Content.ReadAsStringAsync(); - - //Assert - Assert.Equal(HttpStatusCode.OK, response.StatusCode); - Assert.Contains(_cdnServerBaseUrl + "/bootstrap.min.css", responseData); + //Assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.Contains(_cdnServerBaseUrl + "/bootstrap.min.css", responseData); + } } [Theory] @@ -62,18 +63,20 @@ public async Task CustomUrlHelper_GeneratesUrlFromView() [InlineData("http://localhost/Home/LinkByUrlAction", "/home/urlcontent")] public async Task LowercaseUrls_LinkGeneration(string url, string expectedLink) { - // Arrange - var server = TestServer.Create(_services, _app); - var client = server.CreateClient(); - - // Act - var response = await client.GetAsync(url); + using (TestHelper.ReplaceCallContextServiceLocationService(_services)) + { + // Arrange + var server = TestServer.Create(_services, _app); + var client = server.CreateClient(); - string responseData = await response.Content.ReadAsStringAsync(); + // Act + var response = await client.GetAsync(url); + var responseData = await response.Content.ReadAsStringAsync(); - //Assert - Assert.Equal(HttpStatusCode.OK, response.StatusCode); - Assert.Equal(expectedLink, responseData, ignoreCase: false); + //Assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.Equal(expectedLink, responseData, ignoreCase: false); + } } } } \ No newline at end of file diff --git a/test/Microsoft.AspNet.Mvc.FunctionalTests/MvcSampleTests.cs b/test/Microsoft.AspNet.Mvc.FunctionalTests/MvcSampleTests.cs index b31ba85ee1..01176a3865 100644 --- a/test/Microsoft.AspNet.Mvc.FunctionalTests/MvcSampleTests.cs +++ b/test/Microsoft.AspNet.Mvc.FunctionalTests/MvcSampleTests.cs @@ -6,7 +6,6 @@ using System.Net; using System.Net.Http; using System.Net.Http.Headers; -using System.Text; using System.Threading.Tasks; using Microsoft.AspNet.Builder; using Microsoft.AspNet.TestHost; @@ -24,56 +23,65 @@ public class MvcSampleTests [Fact] public async Task Home_Index_ReturnsSuccess() { - // Arrange - var server = TestServer.Create(_services, _app); - var client = server.CreateClient(); - - // Act - var response = await client.GetAsync("http://localhost/Home/Index"); - - // Assert - Assert.NotNull(response); - Assert.Equal(HttpStatusCode.OK, response.StatusCode); + using (TestHelper.ReplaceCallContextServiceLocationService(_services)) + { + // Arrange + var server = TestServer.Create(_services, _app); + var client = server.CreateClient(); + + // Act + var response = await client.GetAsync("http://localhost/Home/Index"); + + // Assert + Assert.NotNull(response); + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + } } [Fact] public async Task Home_NotFoundAction_Returns404() { - // Arrange - var server = TestServer.Create(_services, _app); - var client = server.CreateClient(); - - // Act - var response = await client.GetAsync("http://localhost/Home/NotFound"); - - // Assert - Assert.NotNull(response); - Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); + using (TestHelper.ReplaceCallContextServiceLocationService(_services)) + { + // Arrange + var server = TestServer.Create(_services, _app); + var client = server.CreateClient(); + + // Act + var response = await client.GetAsync("http://localhost/Home/NotFound"); + + // Assert + Assert.NotNull(response); + Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); + } } [Fact] public async Task Home_CreateUser_ReturnsXmlBasedOnAcceptHeader() { - // Arrange - var server = TestServer.Create(_services, _app); - var client = server.CreateClient(); - var request = new HttpRequestMessage(HttpMethod.Get, "http://localhost/Home/ReturnUser"); - request.Headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse("application/xml;charset=utf-8")); - - // Act - var response = await client.SendAsync(request); - - // Assert - Assert.NotNull(response); - Assert.Equal(HttpStatusCode.OK, response.StatusCode); - Assert.Equal("I like playing Football" + - "
My address
13true" + - "
Dependents address
0false" + - "0Dependents name" + - "13.37" + - "My nameSecure stringSoftware Engineer
", - new StreamReader(await response.Content.ReadAsStreamAsync(), Encoding.UTF8).ReadToEnd()); + using (TestHelper.ReplaceCallContextServiceLocationService(_services)) + { + // Arrange + var server = TestServer.Create(_services, _app); + var client = server.CreateClient(); + var request = new HttpRequestMessage(HttpMethod.Get, "http://localhost/Home/ReturnUser"); + request.Headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse("application/xml;charset=utf-8")); + + // Act + var response = await client.SendAsync(request); + + // Assert + Assert.NotNull(response); + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.Equal("I like playing Football" + + "
My address
13true" + + "
Dependents address
0false" + + "0Dependents name" + + "13.37" + + "My nameSecure stringSoftware Engineer
", + await response.Content.ReadAsStringAsync()); + } } [Theory] @@ -82,33 +90,38 @@ public async Task Home_CreateUser_ReturnsXmlBasedOnAcceptHeader() [InlineData("http://localhost/Filters/NotGrantedClaim", HttpStatusCode.Unauthorized)] public async Task FiltersController_Tests(string url, HttpStatusCode statusCode) { - // Arrange - var server = TestServer.Create(_services, _app); - var client = server.CreateClient(); - - // Act - var response = await client.GetAsync(url); - - // Assert - Assert.NotNull(response); - Assert.Equal(statusCode, response.StatusCode); + using (TestHelper.ReplaceCallContextServiceLocationService(_services)) + { + // Arrange + var server = TestServer.Create(_services, _app); + var client = server.CreateClient(); + + // Act + var response = await client.GetAsync(url); + + // Assert + Assert.NotNull(response); + Assert.Equal(statusCode, response.StatusCode); + } } [Fact] public async Task FiltersController_Crash_ThrowsException() { - // Arrange - var server = TestServer.Create(_services, _app); - var client = server.CreateClient(); - - // Act - var response = await client.GetAsync("http://localhost/Filters/Crash?message=HelloWorld"); - - // Assert - Assert.NotNull(response); - Assert.Equal(HttpStatusCode.OK, response.StatusCode); - Assert.Equal("Boom HelloWorld", - new StreamReader(await response.Content.ReadAsStreamAsync(), Encoding.UTF8).ReadToEnd()); + using (TestHelper.ReplaceCallContextServiceLocationService(_services)) + { + // Arrange + var server = TestServer.Create(_services, _app); + var client = server.CreateClient(); + + // Act + var response = await client.GetAsync("http://localhost/Filters/Crash?message=HelloWorld"); + + // Assert + Assert.NotNull(response); + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.Equal("Boom HelloWorld", await response.Content.ReadAsStringAsync()); + } } } } \ No newline at end of file diff --git a/test/Microsoft.AspNet.Mvc.FunctionalTests/TestHelper.cs b/test/Microsoft.AspNet.Mvc.FunctionalTests/TestHelper.cs index ec59194d90..eb23816a37 100644 --- a/test/Microsoft.AspNet.Mvc.FunctionalTests/TestHelper.cs +++ b/test/Microsoft.AspNet.Mvc.FunctionalTests/TestHelper.cs @@ -15,7 +15,7 @@ namespace Microsoft.AspNet.Mvc.FunctionalTests public static class TestHelper { // Path from Mvc\\test\\Microsoft.AspNet.Mvc.FunctionalTests - private static readonly string WebsitesDirectoryPath = Path.Combine("..", "websites"); + private static readonly string WebsitesDirectoryPath = Path.Combine("..", "WebSites"); public static IServiceProvider CreateServices(string applicationWebSiteName) { @@ -58,9 +58,7 @@ public static IServiceProvider CreateServices(string applicationWebSiteName, str typeof(ILoggerFactory), NullLoggerFactory.Instance); - var tempServiceProvider = services.BuildServiceProvider(originalProvider); - CallContextServiceLocator.Locator.ServiceProvider = tempServiceProvider; - return tempServiceProvider; + return services.BuildServiceProvider(originalProvider); } // Calculate the path relative to the application base path. @@ -72,6 +70,17 @@ public static string CalculateApplicationBasePath(IApplicationEnvironment appEnv Path.Combine(appEnvironment.ApplicationBasePath, websitePath, applicationWebSiteName)); } + /// + /// Creates a disposable action that replaces the service provider + /// with the passed in service that is switched back on . + /// + /// This is required for config since it uses the static property to get to + /// . + public static IDisposable ReplaceCallContextServiceLocationService(IServiceProvider serviceProvider) + { + return new CallContextProviderAction(serviceProvider); + } + private static Type CreateAssemblyProviderType(string siteName) { // Creates a service type that will limit MVC to only the controllers in the test site. @@ -81,5 +90,21 @@ private static Type CreateAssemblyProviderType(string siteName) var providerType = typeof(TestAssemblyProvider<>).MakeGenericType(assembly.GetExportedTypes()[0]); return providerType; } + + private sealed class CallContextProviderAction : IDisposable + { + private readonly IServiceProvider _originalProvider; + + public CallContextProviderAction(IServiceProvider provider) + { + _originalProvider = CallContextServiceLocator.Locator.ServiceProvider; + CallContextServiceLocator.Locator.ServiceProvider = provider; + } + + public void Dispose() + { + CallContextServiceLocator.Locator.ServiceProvider = _originalProvider; + } + } } } \ No newline at end of file diff --git a/test/WebSites/UrlHelperWebSite/Config.json b/test/WebSites/UrlHelperWebSite/Config.json deleted file mode 100644 index 8b9227c91e..0000000000 --- a/test/WebSites/UrlHelperWebSite/Config.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "ServeCDNContent": "true", - "CDNServerBaseUrl" : "http://cdn.contoso.com", - "GenerateLowercaseUrls": "true" -} \ No newline at end of file diff --git a/test/WebSites/UrlHelperWebSite/Startup.cs b/test/WebSites/UrlHelperWebSite/Startup.cs index 79c1565342..74843c00de 100644 --- a/test/WebSites/UrlHelperWebSite/Startup.cs +++ b/test/WebSites/UrlHelperWebSite/Startup.cs @@ -12,16 +12,15 @@ public class Startup public void Configure(IApplicationBuilder app) { var configuration = app.GetTestConfiguration(); - configuration.AddJsonFile("config.json"); // Set up application services app.UsePerRequestServices(services => { services.ConfigureOptions(optionsSetup => { - optionsSetup.ServeCDNContent = Convert.ToBoolean(configuration.Get("ServeCDNContent")); - optionsSetup.CDNServerBaseUrl = configuration.Get("CDNServerBaseUrl"); - optionsSetup.GenerateLowercaseUrls = Convert.ToBoolean(configuration.Get("GenerateLowercaseUrls")); + optionsSetup.ServeCDNContent = true; + optionsSetup.CDNServerBaseUrl = "http://cdn.contoso.com"; + optionsSetup.GenerateLowercaseUrls = true; }); // Add MVC services to the services container From e319fef5cb39ba1a375545c86f8d8bc2f174ae9a Mon Sep 17 00:00:00 2001 From: Harsh Gupta Date: Thu, 9 Oct 2014 12:41:03 -0700 Subject: [PATCH 02/39] Adding CancellationTokenModelBinder. --- .../Binders/CancellationTokenModelBinder.cs | 26 ++++++++ src/Microsoft.AspNet.Mvc/MvcOptionsSetup.cs | 1 + .../ModelBindingTests.cs | 31 +++++++++ .../CancellationTokenModelBinderTests.cs | 63 +++++++++++++++++++ .../MvcOptionSetupTest.cs | 11 ++-- .../Controllers/HomeController.cs | 18 +++++- 6 files changed, 144 insertions(+), 6 deletions(-) create mode 100644 src/Microsoft.AspNet.Mvc.ModelBinding/Binders/CancellationTokenModelBinder.cs create mode 100644 test/Microsoft.AspNet.Mvc.ModelBinding.Test/Binders/CancellationTokenModelBinderTests.cs diff --git a/src/Microsoft.AspNet.Mvc.ModelBinding/Binders/CancellationTokenModelBinder.cs b/src/Microsoft.AspNet.Mvc.ModelBinding/Binders/CancellationTokenModelBinder.cs new file mode 100644 index 0000000000..a9671a9e9b --- /dev/null +++ b/src/Microsoft.AspNet.Mvc.ModelBinding/Binders/CancellationTokenModelBinder.cs @@ -0,0 +1,26 @@ +// 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.Threading; +using System.Threading.Tasks; + +namespace Microsoft.AspNet.Mvc.ModelBinding +{ + /// + /// Represents a model binder which can bind models of type . + /// + public class CancellationTokenModelBinder : IModelBinder + { + /// + public Task BindModelAsync(ModelBindingContext bindingContext) + { + if (bindingContext.ModelType == typeof(CancellationToken)) + { + bindingContext.Model = bindingContext.HttpContext.RequestAborted; + return Task.FromResult(true); + } + + return Task.FromResult(false); + } + } +} diff --git a/src/Microsoft.AspNet.Mvc/MvcOptionsSetup.cs b/src/Microsoft.AspNet.Mvc/MvcOptionsSetup.cs index 3959a57f92..9c449a294b 100644 --- a/src/Microsoft.AspNet.Mvc/MvcOptionsSetup.cs +++ b/src/Microsoft.AspNet.Mvc/MvcOptionsSetup.cs @@ -27,6 +27,7 @@ public static void ConfigureMvc(MvcOptions options) // Set up ModelBinding options.ModelBinders.Add(new TypeConverterModelBinder()); options.ModelBinders.Add(new TypeMatchModelBinder()); + options.ModelBinders.Add(new CancellationTokenModelBinder()); options.ModelBinders.Add(new ByteArrayModelBinder()); options.ModelBinders.Add(typeof(GenericModelBinder)); options.ModelBinders.Add(new MutableObjectModelBinder()); diff --git a/test/Microsoft.AspNet.Mvc.FunctionalTests/ModelBindingTests.cs b/test/Microsoft.AspNet.Mvc.FunctionalTests/ModelBindingTests.cs index 6e8d99f9c5..f120d7e3d5 100644 --- a/test/Microsoft.AspNet.Mvc.FunctionalTests/ModelBindingTests.cs +++ b/test/Microsoft.AspNet.Mvc.FunctionalTests/ModelBindingTests.cs @@ -18,6 +18,37 @@ public class ModelBindingTests private readonly IServiceProvider _services = TestHelper.CreateServices("ModelBindingWebSite"); private readonly Action _app = new ModelBindingWebSite.Startup().Configure; + [Fact] + public async Task ModelBindCancellationTokenParameteres() + { + // Arrange + var server = TestServer.Create(_services, _app); + var client = server.CreateClient(); + + // Act + var response = await client.GetAsync("http://localhost/Home/ActionWithCancellationToken"); + + //Assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.Equal("true", await response.Content.ReadAsStringAsync()); + } + + [Fact] + public async Task ModelBindCancellationToken_ForProperties() + { + // Arrange + var server = TestServer.Create(_services, _app); + var client = server.CreateClient(); + + // Act + var response = await client.GetAsync( + "http://localhost/Home/ActionWithCancellationTokenModel?wrapper=bogusValue"); + + //Assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.Equal("true", await response.Content.ReadAsStringAsync()); + } + [Fact] public async Task ModelBindingBindsBase64StringsToByteArrays() { diff --git a/test/Microsoft.AspNet.Mvc.ModelBinding.Test/Binders/CancellationTokenModelBinderTests.cs b/test/Microsoft.AspNet.Mvc.ModelBinding.Test/Binders/CancellationTokenModelBinderTests.cs new file mode 100644 index 0000000000..b2322de320 --- /dev/null +++ b/test/Microsoft.AspNet.Mvc.ModelBinding.Test/Binders/CancellationTokenModelBinderTests.cs @@ -0,0 +1,63 @@ +// 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.Threading; +using System.Threading.Tasks; +using Microsoft.AspNet.PipelineCore; +using Xunit; + +namespace Microsoft.AspNet.Mvc.ModelBinding.Test +{ + public class CancellationTokenModelBinderTests + { + [Fact] + public async Task CancellationTokenModelBinder_ReturnsTrue_ForCancellationTokenType() + { + // Arrange + var bindingContext = GetBindingContext(typeof(CancellationToken)); + var binder = new CancellationTokenModelBinder(); + + // Act + var bound = await binder.BindModelAsync(bindingContext); + + // Assert + Assert.True(bound); + Assert.Equal(bindingContext.HttpContext.RequestAborted, bindingContext.Model); + } + + [Theory] + [InlineData(typeof(int))] + [InlineData(typeof(object))] + [InlineData(typeof(CancellationTokenModelBinderTests))] + public async Task CancellationTokenModelBinder_ReturnsFalse_ForNonCancellationTokenType(Type t) + { + // Arrange + var bindingContext = GetBindingContext(t); + var binder = new CancellationTokenModelBinder(); + + // Act + var bound = await binder.BindModelAsync(bindingContext); + + // Assert + Assert.False(bound); + Assert.Null(bindingContext.Model); + } + + private static ModelBindingContext GetBindingContext(Type modelType) + { + var metadataProvider = new EmptyModelMetadataProvider(); + ModelBindingContext bindingContext = new ModelBindingContext + { + ModelMetadata = metadataProvider.GetMetadataForType(null, modelType), + ModelName = "someName", + ValueProvider = new SimpleHttpValueProvider(), + ModelBinder = new CancellationTokenModelBinder(), + MetadataProvider = metadataProvider, + HttpContext = new DefaultHttpContext(), + }; + + return bindingContext; + } + } +} diff --git a/test/Microsoft.AspNet.Mvc.Test/MvcOptionSetupTest.cs b/test/Microsoft.AspNet.Mvc.Test/MvcOptionSetupTest.cs index c95f0a8244..5ce47d0865 100644 --- a/test/Microsoft.AspNet.Mvc.Test/MvcOptionSetupTest.cs +++ b/test/Microsoft.AspNet.Mvc.Test/MvcOptionSetupTest.cs @@ -35,13 +35,14 @@ public void Setup_SetsUpModelBinders() setup.Invoke(mvcOptions); // Assert - Assert.Equal(6, mvcOptions.ModelBinders.Count); + Assert.Equal(7, mvcOptions.ModelBinders.Count); Assert.Equal(typeof(TypeConverterModelBinder), mvcOptions.ModelBinders[0].OptionType); Assert.Equal(typeof(TypeMatchModelBinder), mvcOptions.ModelBinders[1].OptionType); - Assert.Equal(typeof(ByteArrayModelBinder), mvcOptions.ModelBinders[2].OptionType); - Assert.Equal(typeof(GenericModelBinder), mvcOptions.ModelBinders[3].OptionType); - Assert.Equal(typeof(MutableObjectModelBinder), mvcOptions.ModelBinders[4].OptionType); - Assert.Equal(typeof(ComplexModelDtoModelBinder), mvcOptions.ModelBinders[5].OptionType); + Assert.Equal(typeof(CancellationTokenModelBinder), mvcOptions.ModelBinders[2].OptionType); + Assert.Equal(typeof(ByteArrayModelBinder), mvcOptions.ModelBinders[3].OptionType); + Assert.Equal(typeof(GenericModelBinder), mvcOptions.ModelBinders[4].OptionType); + Assert.Equal(typeof(MutableObjectModelBinder), mvcOptions.ModelBinders[5].OptionType); + Assert.Equal(typeof(ComplexModelDtoModelBinder), mvcOptions.ModelBinders[6].OptionType); } [Fact] diff --git a/test/WebSites/ModelBindingWebSite/Controllers/HomeController.cs b/test/WebSites/ModelBindingWebSite/Controllers/HomeController.cs index 5284020409..a6ff4da7f7 100644 --- a/test/WebSites/ModelBindingWebSite/Controllers/HomeController.cs +++ b/test/WebSites/ModelBindingWebSite/Controllers/HomeController.cs @@ -2,8 +2,9 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System.Collections.Generic; -using Microsoft.AspNet.Mvc; using System.Linq; +using System.Threading; +using Microsoft.AspNet.Mvc; using ModelBindingWebSite.Models; namespace ModelBindingWebSite.Controllers @@ -26,6 +27,16 @@ public object ModelWithFewValidationErrors(ModelWithValidation model) return CreateValidationDictionary(); } + public bool ActionWithCancellationToken(CancellationToken token) + { + return token == ActionContext.HttpContext.RequestAborted; + } + + public bool ActionWithCancellationTokenModel(CancellationTokenModel wrapper) + { + return wrapper.CancellationToken == ActionContext.HttpContext.RequestAborted; + } + private Dictionary CreateValidationDictionary() { var result = new Dictionary(); @@ -42,5 +53,10 @@ private Dictionary CreateValidationDictionary() return result; } + + public class CancellationTokenModel + { + public CancellationToken CancellationToken { get; set; } + } } } \ No newline at end of file From f66345263d9135f0aa80bf2f1766a22d39230bce Mon Sep 17 00:00:00 2001 From: Ryan Nowak Date: Wed, 8 Oct 2014 09:04:49 -0700 Subject: [PATCH 03/39] Add WebApiCompatShim project structure --- Mvc.NoFun.sln | 28 ++++++++++++- Mvc.sln | 39 +++++++++++++++++++ ...icrosoft.AspNet.Mvc.WebApiCompatShim.kproj | 26 +++++++++++++ .../project.json | 18 +++++++++ .../project.json | 4 +- ...soft.AspNet.Mvc.WebApiCompatShimTest.kproj | 26 +++++++++++++ .../project.json | 16 ++++++++ .../WebApiCompatShimWebSite/Startup.cs | 23 +++++++++++ .../WebApiCompatShimWebSite.kproj | 24 ++++++++++++ .../WebApiCompatShimWebSite/project.json | 11 ++++++ 10 files changed, 213 insertions(+), 2 deletions(-) create mode 100644 src/Microsoft.AspNet.Mvc.WebApiCompatShim/Microsoft.AspNet.Mvc.WebApiCompatShim.kproj create mode 100644 src/Microsoft.AspNet.Mvc.WebApiCompatShim/project.json create mode 100644 test/Microsoft.AspNet.Mvc.WebApiCompatShimTest/Microsoft.AspNet.Mvc.WebApiCompatShimTest.kproj create mode 100644 test/Microsoft.AspNet.Mvc.WebApiCompatShimTest/project.json create mode 100644 test/WebSites/WebApiCompatShimWebSite/Startup.cs create mode 100644 test/WebSites/WebApiCompatShimWebSite/WebApiCompatShimWebSite.kproj create mode 100644 test/WebSites/WebApiCompatShimWebSite/project.json diff --git a/Mvc.NoFun.sln b/Mvc.NoFun.sln index b23f7460b1..0f306eb7d7 100644 --- a/Mvc.NoFun.sln +++ b/Mvc.NoFun.sln @@ -1,7 +1,7 @@  Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio 14 -VisualStudioVersion = 14.0.22130.0 +VisualStudioVersion = 14.0.22013.1 MinimumVisualStudioVersion = 10.0.40219.1 Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "samples", "samples", "{DAAE4C74-D06F-4874-A166-33305D2643CE}" EndProject @@ -42,6 +42,10 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution global.json = global.json EndProjectSection EndProject +Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Microsoft.AspNet.Mvc.WebApiCompatShim", "src\Microsoft.AspNet.Mvc.WebApiCompatShim\Microsoft.AspNet.Mvc.WebApiCompatShim.kproj", "{23D30B8C-04B1-4577-A604-ED27EA1E4A0E}" +EndProject +Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Microsoft.AspNet.Mvc.WebApiCompatShimTest", "test\Microsoft.AspNet.Mvc.WebApiCompatShimTest\Microsoft.AspNet.Mvc.WebApiCompatShimTest.kproj", "{5DE8E4D9-AACD-4B5F-819F-F091383FB996}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -192,6 +196,26 @@ Global {E69FD235-2042-43A4-9970-59CB29955B4E}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU {E69FD235-2042-43A4-9970-59CB29955B4E}.Release|Mixed Platforms.Build.0 = Release|Any CPU {E69FD235-2042-43A4-9970-59CB29955B4E}.Release|x86.ActiveCfg = Release|Any CPU + {23D30B8C-04B1-4577-A604-ED27EA1E4A0E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {23D30B8C-04B1-4577-A604-ED27EA1E4A0E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {23D30B8C-04B1-4577-A604-ED27EA1E4A0E}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU + {23D30B8C-04B1-4577-A604-ED27EA1E4A0E}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU + {23D30B8C-04B1-4577-A604-ED27EA1E4A0E}.Debug|x86.ActiveCfg = Debug|Any CPU + {23D30B8C-04B1-4577-A604-ED27EA1E4A0E}.Release|Any CPU.ActiveCfg = Release|Any CPU + {23D30B8C-04B1-4577-A604-ED27EA1E4A0E}.Release|Any CPU.Build.0 = Release|Any CPU + {23D30B8C-04B1-4577-A604-ED27EA1E4A0E}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU + {23D30B8C-04B1-4577-A604-ED27EA1E4A0E}.Release|Mixed Platforms.Build.0 = Release|Any CPU + {23D30B8C-04B1-4577-A604-ED27EA1E4A0E}.Release|x86.ActiveCfg = Release|Any CPU + {5DE8E4D9-AACD-4B5F-819F-F091383FB996}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {5DE8E4D9-AACD-4B5F-819F-F091383FB996}.Debug|Any CPU.Build.0 = Debug|Any CPU + {5DE8E4D9-AACD-4B5F-819F-F091383FB996}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU + {5DE8E4D9-AACD-4B5F-819F-F091383FB996}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU + {5DE8E4D9-AACD-4B5F-819F-F091383FB996}.Debug|x86.ActiveCfg = Debug|Any CPU + {5DE8E4D9-AACD-4B5F-819F-F091383FB996}.Release|Any CPU.ActiveCfg = Release|Any CPU + {5DE8E4D9-AACD-4B5F-819F-F091383FB996}.Release|Any CPU.Build.0 = Release|Any CPU + {5DE8E4D9-AACD-4B5F-819F-F091383FB996}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU + {5DE8E4D9-AACD-4B5F-819F-F091383FB996}.Release|Mixed Platforms.Build.0 = Release|Any CPU + {5DE8E4D9-AACD-4B5F-819F-F091383FB996}.Release|x86.ActiveCfg = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -211,5 +235,7 @@ Global {5F945B82-FE5F-425C-956C-8BC2F2020254} = {3BA657BF-28B1-42DA-B5B0-1C4601FCF7B1} {98335B23-E4B9-4CAD-9749-0DED32A659A1} = {32285FA4-6B46-4D6B-A840-2B13E4C8B58E} {E69FD235-2042-43A4-9970-59CB29955B4E} = {3BA657BF-28B1-42DA-B5B0-1C4601FCF7B1} + {23D30B8C-04B1-4577-A604-ED27EA1E4A0E} = {32285FA4-6B46-4D6B-A840-2B13E4C8B58E} + {5DE8E4D9-AACD-4B5F-819F-F091383FB996} = {3BA657BF-28B1-42DA-B5B0-1C4601FCF7B1} EndGlobalSection EndGlobal diff --git a/Mvc.sln b/Mvc.sln index b47670f815..7750fe6354 100644 --- a/Mvc.sln +++ b/Mvc.sln @@ -92,6 +92,12 @@ Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "RazorInstrumentationWebSite EndProject Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "ApplicationModelWebSite", "test\WebSites\ApplicationModelWebSite\ApplicationModelWebSite.kproj", "{CAE52CB7-0FAC-4B5B-8251-B0FF837DB657}" EndProject +Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Microsoft.AspNet.Mvc.WebApiCompatShim", "src\Microsoft.AspNet.Mvc.WebApiCompatShim\Microsoft.AspNet.Mvc.WebApiCompatShim.kproj", "{23D30B8C-04B1-4577-A604-ED27EA1E4A0E}" +EndProject +Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "WebApiCompatShimWebSite", "test\WebSites\WebApiCompatShimWebSite\WebApiCompatShimWebSite.kproj", "{B2B7BC91-688E-4C1E-A71F-CE948D958DDF}" +EndProject +Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Microsoft.AspNet.Mvc.WebApiCompatShimTest", "test\Microsoft.AspNet.Mvc.WebApiCompatShimTest\Microsoft.AspNet.Mvc.WebApiCompatShimTest.kproj", "{5DE8E4D9-AACD-4B5F-819F-F091383FB996}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -482,6 +488,36 @@ Global {CAE52CB7-0FAC-4B5B-8251-B0FF837DB657}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU {CAE52CB7-0FAC-4B5B-8251-B0FF837DB657}.Release|Mixed Platforms.Build.0 = Release|Any CPU {CAE52CB7-0FAC-4B5B-8251-B0FF837DB657}.Release|x86.ActiveCfg = Release|Any CPU + {23D30B8C-04B1-4577-A604-ED27EA1E4A0E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {23D30B8C-04B1-4577-A604-ED27EA1E4A0E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {23D30B8C-04B1-4577-A604-ED27EA1E4A0E}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU + {23D30B8C-04B1-4577-A604-ED27EA1E4A0E}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU + {23D30B8C-04B1-4577-A604-ED27EA1E4A0E}.Debug|x86.ActiveCfg = Debug|Any CPU + {23D30B8C-04B1-4577-A604-ED27EA1E4A0E}.Release|Any CPU.ActiveCfg = Release|Any CPU + {23D30B8C-04B1-4577-A604-ED27EA1E4A0E}.Release|Any CPU.Build.0 = Release|Any CPU + {23D30B8C-04B1-4577-A604-ED27EA1E4A0E}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU + {23D30B8C-04B1-4577-A604-ED27EA1E4A0E}.Release|Mixed Platforms.Build.0 = Release|Any CPU + {23D30B8C-04B1-4577-A604-ED27EA1E4A0E}.Release|x86.ActiveCfg = Release|Any CPU + {B2B7BC91-688E-4C1E-A71F-CE948D958DDF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B2B7BC91-688E-4C1E-A71F-CE948D958DDF}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B2B7BC91-688E-4C1E-A71F-CE948D958DDF}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU + {B2B7BC91-688E-4C1E-A71F-CE948D958DDF}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU + {B2B7BC91-688E-4C1E-A71F-CE948D958DDF}.Debug|x86.ActiveCfg = Debug|Any CPU + {B2B7BC91-688E-4C1E-A71F-CE948D958DDF}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B2B7BC91-688E-4C1E-A71F-CE948D958DDF}.Release|Any CPU.Build.0 = Release|Any CPU + {B2B7BC91-688E-4C1E-A71F-CE948D958DDF}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU + {B2B7BC91-688E-4C1E-A71F-CE948D958DDF}.Release|Mixed Platforms.Build.0 = Release|Any CPU + {B2B7BC91-688E-4C1E-A71F-CE948D958DDF}.Release|x86.ActiveCfg = Release|Any CPU + {5DE8E4D9-AACD-4B5F-819F-F091383FB996}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {5DE8E4D9-AACD-4B5F-819F-F091383FB996}.Debug|Any CPU.Build.0 = Debug|Any CPU + {5DE8E4D9-AACD-4B5F-819F-F091383FB996}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU + {5DE8E4D9-AACD-4B5F-819F-F091383FB996}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU + {5DE8E4D9-AACD-4B5F-819F-F091383FB996}.Debug|x86.ActiveCfg = Debug|Any CPU + {5DE8E4D9-AACD-4B5F-819F-F091383FB996}.Release|Any CPU.ActiveCfg = Release|Any CPU + {5DE8E4D9-AACD-4B5F-819F-F091383FB996}.Release|Any CPU.Build.0 = Release|Any CPU + {5DE8E4D9-AACD-4B5F-819F-F091383FB996}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU + {5DE8E4D9-AACD-4B5F-819F-F091383FB996}.Release|Mixed Platforms.Build.0 = Release|Any CPU + {5DE8E4D9-AACD-4B5F-819F-F091383FB996}.Release|x86.ActiveCfg = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -526,5 +562,8 @@ Global {0EF9860B-10D7-452F-B0F4-A405B88BEBB3} = {16703B76-C9F7-4C75-AE6C-53D92E308E3C} {2B2B9876-903C-4065-8D62-2EE832BBA106} = {16703B76-C9F7-4C75-AE6C-53D92E308E3C} {CAE52CB7-0FAC-4B5B-8251-B0FF837DB657} = {16703B76-C9F7-4C75-AE6C-53D92E308E3C} + {23D30B8C-04B1-4577-A604-ED27EA1E4A0E} = {32285FA4-6B46-4D6B-A840-2B13E4C8B58E} + {B2B7BC91-688E-4C1E-A71F-CE948D958DDF} = {16703B76-C9F7-4C75-AE6C-53D92E308E3C} + {5DE8E4D9-AACD-4B5F-819F-F091383FB996} = {3BA657BF-28B1-42DA-B5B0-1C4601FCF7B1} EndGlobalSection EndGlobal diff --git a/src/Microsoft.AspNet.Mvc.WebApiCompatShim/Microsoft.AspNet.Mvc.WebApiCompatShim.kproj b/src/Microsoft.AspNet.Mvc.WebApiCompatShim/Microsoft.AspNet.Mvc.WebApiCompatShim.kproj new file mode 100644 index 0000000000..d198c9868f --- /dev/null +++ b/src/Microsoft.AspNet.Mvc.WebApiCompatShim/Microsoft.AspNet.Mvc.WebApiCompatShim.kproj @@ -0,0 +1,26 @@ + + + + 12.0 + $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) + + + + + 23d30b8c-04b1-4577-a604-ed27ea1e4a0e + Library + Microsoft.AspNet.Mvc.WebApiCompatShim + + + + ConsoleDebugger + + + WebDebugger + + + + 2.0 + + + \ No newline at end of file diff --git a/src/Microsoft.AspNet.Mvc.WebApiCompatShim/project.json b/src/Microsoft.AspNet.Mvc.WebApiCompatShim/project.json new file mode 100644 index 0000000000..12a0f20de6 --- /dev/null +++ b/src/Microsoft.AspNet.Mvc.WebApiCompatShim/project.json @@ -0,0 +1,18 @@ +{ + "version": "6.0.0-*", + "compilationOptions": { + "warningsAsErrors": true + }, + "dependencies": { + "Microsoft.AspNet.Mvc.Common": { "version": "6.0.0-*", "type": "build" }, + "Microsoft.AspNet.Mvc.Core": "6.0.0-*", + "Microsoft.AspNet.WebApi.Client": "5.2.2" + }, + "frameworks": { + "aspnet50": { + "frameworkAssemblies": { + "System.Net.Http": "4.0.0.0" + } + } + } +} \ 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 0de07c0da9..72b46b1642 100644 --- a/test/Microsoft.AspNet.Mvc.FunctionalTests/project.json +++ b/test/Microsoft.AspNet.Mvc.FunctionalTests/project.json @@ -24,6 +24,7 @@ "ValueProvidersSite": "1.0.0", "XmlSerializerWebSite": "1.0.0", "UrlHelperWebSite": "1.0.0", + "Microsoft.AspNet.TestHost": "1.0.0-*", "Microsoft.AspNet.PipelineCore": "1.0.0-*", "Microsoft.AspNet.Mvc.TestConfiguration": "1.0.0", @@ -42,7 +43,8 @@ "frameworks": { "aspnet50": { "dependencies": { - "AutofacWebSite": "1.0.0" + "AutofacWebSite": "1.0.0", + "WebApiCompatShimWebSite": "1.0.0" } }, "aspnetcore50": { diff --git a/test/Microsoft.AspNet.Mvc.WebApiCompatShimTest/Microsoft.AspNet.Mvc.WebApiCompatShimTest.kproj b/test/Microsoft.AspNet.Mvc.WebApiCompatShimTest/Microsoft.AspNet.Mvc.WebApiCompatShimTest.kproj new file mode 100644 index 0000000000..3b41ac20ef --- /dev/null +++ b/test/Microsoft.AspNet.Mvc.WebApiCompatShimTest/Microsoft.AspNet.Mvc.WebApiCompatShimTest.kproj @@ -0,0 +1,26 @@ + + + + 12.0 + $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) + + + + + 5de8e4d9-aacd-4b5f-819f-f091383fb996 + Library + Microsoft.AspNet.Mvc.WebApiCompatShimTest + + + + ConsoleDebugger + + + WebDebugger + + + + 2.0 + + + \ No newline at end of file diff --git a/test/Microsoft.AspNet.Mvc.WebApiCompatShimTest/project.json b/test/Microsoft.AspNet.Mvc.WebApiCompatShimTest/project.json new file mode 100644 index 0000000000..ceb4dec9b7 --- /dev/null +++ b/test/Microsoft.AspNet.Mvc.WebApiCompatShimTest/project.json @@ -0,0 +1,16 @@ +{ + "compilationOptions": { + "warningsAsErrors": "true" + }, + "dependencies": { + "Microsoft.AspNet.Mvc": "6.0.0-*", + "Microsoft.AspNet.Mvc.WebApiCompatShim": "6.0.0-*", + "Xunit.KRunner": "1.0.0-*" + }, + "commands": { + "test": "Xunit.KRunner" + }, + "frameworks": { + "aspnet50": { } + } +} diff --git a/test/WebSites/WebApiCompatShimWebSite/Startup.cs b/test/WebSites/WebApiCompatShimWebSite/Startup.cs new file mode 100644 index 0000000000..284d184502 --- /dev/null +++ b/test/WebSites/WebApiCompatShimWebSite/Startup.cs @@ -0,0 +1,23 @@ +// 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.Framework.DependencyInjection; + +namespace WebApiCompatShimWebSite +{ + public class Startup + { + public void Configure(IApplicationBuilder app) + { + var configuration = app.GetTestConfiguration(); + + app.UsePerRequestServices(services => + { + services.AddMvc(configuration); + }); + + app.UseMvc(); + } + } +} diff --git a/test/WebSites/WebApiCompatShimWebSite/WebApiCompatShimWebSite.kproj b/test/WebSites/WebApiCompatShimWebSite/WebApiCompatShimWebSite.kproj new file mode 100644 index 0000000000..9e6a59bc0c --- /dev/null +++ b/test/WebSites/WebApiCompatShimWebSite/WebApiCompatShimWebSite.kproj @@ -0,0 +1,24 @@ + + + + 12.0 + $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) + + + + b2b7bc91-688e-4c1e-a71f-ce948d958ddf + Web + WebApiCompatShimWebSite + + + ConsoleDebugger + + + WebDebugger + + + 2.0 + 9748 + + + \ No newline at end of file diff --git a/test/WebSites/WebApiCompatShimWebSite/project.json b/test/WebSites/WebApiCompatShimWebSite/project.json new file mode 100644 index 0000000000..2f16c0d99c --- /dev/null +++ b/test/WebSites/WebApiCompatShimWebSite/project.json @@ -0,0 +1,11 @@ +{ + "dependencies": { + "Microsoft.AspNet.Mvc": "6.0.0-*", + "Microsoft.AspNet.Mvc.TestConfiguration": "1.0.0", + "Microsoft.AspNet.Mvc.WebApiCompatShim": "6.0.0-*", + "Microsoft.AspNet.Server.IIS": "1.0.0-*" + }, + "frameworks": { + "aspnet50": { } + } +} From d9fe3058027e8edd951b4e5a6c6c86797d450edb Mon Sep 17 00:00:00 2001 From: Ryan Nowak Date: Wed, 8 Oct 2014 09:19:42 -0700 Subject: [PATCH 04/39] Fix for #1275 - Adding ApiController This change includes the basic properties that we're providing for compatability as well as some functional tests and unit tests that verify that ApiController can be a controller class. --- .../ApiController.cs | 65 +++++++++ .../WebApiCompatShimBasicTest.cs | 56 ++++++++ .../ApiControllerActionDiscoveryTest.cs | 133 ++++++++++++++++++ .../ApiControllerTest.cs | 51 +++++++ .../project.json | 29 ++-- .../Controllers/BasicApiController.cs | 36 +++++ 6 files changed, 356 insertions(+), 14 deletions(-) create mode 100644 src/Microsoft.AspNet.Mvc.WebApiCompatShim/ApiController.cs create mode 100644 test/Microsoft.AspNet.Mvc.FunctionalTests/WebApiCompatShimBasicTest.cs create mode 100644 test/Microsoft.AspNet.Mvc.WebApiCompatShimTest/ApiControllerActionDiscoveryTest.cs create mode 100644 test/Microsoft.AspNet.Mvc.WebApiCompatShimTest/ApiControllerTest.cs create mode 100644 test/WebSites/WebApiCompatShimWebSite/Controllers/BasicApiController.cs diff --git a/src/Microsoft.AspNet.Mvc.WebApiCompatShim/ApiController.cs b/src/Microsoft.AspNet.Mvc.WebApiCompatShim/ApiController.cs new file mode 100644 index 0000000000..c9f8d3fe4c --- /dev/null +++ b/src/Microsoft.AspNet.Mvc.WebApiCompatShim/ApiController.cs @@ -0,0 +1,65 @@ +// 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.Security.Principal; +using Microsoft.AspNet.Http; +using Microsoft.AspNet.Mvc; +using Microsoft.AspNet.Mvc.ModelBinding; + +namespace System.Web.Http +{ + public abstract class ApiController : IDisposable + { + /// Gets the action context. + /// The setter is intended for unit testing purposes only. + [Activate] + public ActionContext ActionContext { get; set; } + + /// + /// Gets the http context. + /// + public HttpContext Context + { + get + { + return ActionContext?.HttpContext; + } + } + + /// + /// Gets model state after the model binding process. This ModelState will be empty before model binding happens. + /// + public ModelStateDictionary ModelState + { + get + { + return ActionContext?.ModelState; + } + } + + /// Gets a factory used to generate URLs to other APIs. + /// The setter is intended for unit testing purposes only. + [Activate] + public IUrlHelper Url { get; set; } + + /// Gets or sets the current principal associated with this request. + public IPrincipal User + { + get + { + return Context?.User; + } + } + + [NonAction] + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + protected virtual void Dispose(bool disposing) + { + } + } +} diff --git a/test/Microsoft.AspNet.Mvc.FunctionalTests/WebApiCompatShimBasicTest.cs b/test/Microsoft.AspNet.Mvc.FunctionalTests/WebApiCompatShimBasicTest.cs new file mode 100644 index 0000000000..ebc4129d2c --- /dev/null +++ b/test/Microsoft.AspNet.Mvc.FunctionalTests/WebApiCompatShimBasicTest.cs @@ -0,0 +1,56 @@ +// 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. + +#if ASPNET50 +using System; +using System.Threading.Tasks; +using Microsoft.AspNet.Builder; +using Microsoft.AspNet.TestHost; +using Xunit; +using System.Net; + +namespace Microsoft.AspNet.Mvc.FunctionalTests +{ + public class WebApiCompatShimBasicTest + { + private readonly IServiceProvider _provider = TestHelper.CreateServices(nameof(WebApiCompatShimWebSite)); + private readonly Action _app = new WebApiCompatShimWebSite.Startup().Configure; + + [Fact] + public async Task ApiController_Activates_HttpContextAndUser() + { + // Arrange + var server = TestServer.Create(_provider, _app); + var client = server.CreateClient(); + + // Act + var response = await client.GetAsync("http://localhost/BasicApi/WriteToHttpContext"); + var content = await response.Content.ReadAsStringAsync(); + + // Assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.Equal( + "Hello, Anonymous User from WebApiCompatShimWebSite.BasicApiController.WriteToHttpContext", + content); + } + + [Fact] + public async Task ApiController_Activates_UrlHelper() + { + // Arrange + var server = TestServer.Create(_provider, _app); + var client = server.CreateClient(); + + // Act + var response = await client.GetAsync("http://localhost/BasicApi/GenerateUrl"); + var content = await response.Content.ReadAsStringAsync(); + + // Assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.Equal( + "Visited: /BasicApi/GenerateUrl", + content); + } + } +} +#endif \ No newline at end of file diff --git a/test/Microsoft.AspNet.Mvc.WebApiCompatShimTest/ApiControllerActionDiscoveryTest.cs b/test/Microsoft.AspNet.Mvc.WebApiCompatShimTest/ApiControllerActionDiscoveryTest.cs new file mode 100644 index 0000000000..f6759bba9c --- /dev/null +++ b/test/Microsoft.AspNet.Mvc.WebApiCompatShimTest/ApiControllerActionDiscoveryTest.cs @@ -0,0 +1,133 @@ +// 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.Collections.Generic; +using System.Linq; +using System.Reflection; +using Microsoft.AspNet.Mvc; +using Microsoft.AspNet.Mvc.Filters; +using Microsoft.Framework.DependencyInjection; +using Microsoft.Framework.DependencyInjection.NestedProviders; +using Microsoft.Framework.OptionsModel; +using Moq; +using Xunit; + +namespace System.Web.Http +{ + public class ApiControllerActionDiscoveryTest + { + // For now we just want to verify that an ApiController is-a controller and produces + // actions. When we implement the conventions for action discovery, this test will be revised. + [Fact] + public void GetActions_ApiControllerWithControllerSuffix_IsController() + { + // Arrange + var provider = CreateProvider(); + + // Act + var context = new ActionDescriptorProviderContext(); + provider.Invoke(context); + + var results = context.Results.Cast(); + + // Assert + var controllerType = typeof(TestControllers.ProductsController).GetTypeInfo(); + var filtered = results.Where(ad => ad.ControllerDescriptor.ControllerTypeInfo == controllerType).ToArray(); + + Assert.Equal(3, filtered.Length); + } + + [Fact] + public void GetActions_ApiControllerWithoutControllerSuffix_IsNotController() + { + // Arrange + var provider = CreateProvider(); + + // Act + var context = new ActionDescriptorProviderContext(); + provider.Invoke(context); + + var results = context.Results.Cast(); + + // Assert + var controllerType = typeof(TestControllers.Blog).GetTypeInfo(); + var filtered = results.Where(ad => ad.ControllerDescriptor.ControllerTypeInfo == controllerType).ToArray(); + + Assert.Empty(filtered); + } + + private INestedProviderManager CreateProvider() + { + var assemblyProvider = new Mock(); + assemblyProvider + .SetupGet(ap => ap.CandidateAssemblies) + .Returns(new Assembly[] { typeof(ApiControllerActionDiscoveryTest).Assembly }); + + var filterProvider = new Mock(); + filterProvider + .SetupGet(fp => fp.Filters) + .Returns(new List()); + + var conventions = new NamespaceLimitedActionDiscoveryConventions(); + + var optionsAccessor = new Mock>(); + optionsAccessor + .SetupGet(o => o.Options) + .Returns(new MvcOptions()); + + var provider = new ControllerActionDescriptorProvider( + assemblyProvider.Object, + conventions, + filterProvider.Object, + optionsAccessor.Object); + + return new NestedProviderManager( + new INestedProvider[] + { + provider + }); + } + + private class NamespaceLimitedActionDiscoveryConventions : DefaultActionDiscoveryConventions + { + public override bool IsController(TypeInfo typeInfo) + { + return + typeInfo.Namespace == "System.Web.Http.TestControllers" && + base.IsController(typeInfo); + } + } + } +} + +// These need to be public top-level classes to test discovery end-to-end. Don't reuse +// these outside of this test. +namespace System.Web.Http.TestControllers +{ + public class ProductsController : ApiController + { + public IActionResult GetAll() + { + return null; + } + + public IActionResult Get(int id) + { + return null; + } + + public IActionResult Edit(int id) + { + return null; + } + } + + // Not a controller, because there's no controller suffix + public class Blog : ApiController + { + public IActionResult GetBlogPosts() + { + return null; + } + } +} \ No newline at end of file diff --git a/test/Microsoft.AspNet.Mvc.WebApiCompatShimTest/ApiControllerTest.cs b/test/Microsoft.AspNet.Mvc.WebApiCompatShimTest/ApiControllerTest.cs new file mode 100644 index 0000000000..1aa96e5cac --- /dev/null +++ b/test/Microsoft.AspNet.Mvc.WebApiCompatShimTest/ApiControllerTest.cs @@ -0,0 +1,51 @@ +// 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.Security.Claims; +using Microsoft.AspNet.Mvc; +using Microsoft.AspNet.PipelineCore; +using Microsoft.AspNet.Routing; +using Xunit; + +namespace System.Web.Http +{ + public class ApiControllerTest + { + [Fact] + public void AccessDependentProperties() + { + // Arrange + var controller = new ConcreteApiController(); + + var httpContext = new DefaultHttpContext(); + httpContext.User = new ClaimsPrincipal(); + + var routeContext = new RouteContext(httpContext); + var actionContext = new ActionContext(routeContext, new ActionDescriptor()); + + // Act + controller.ActionContext = actionContext; + + // Assert + Assert.Same(httpContext, controller.Context); + Assert.Same(actionContext.ModelState, controller.ModelState); + Assert.Same(httpContext.User, controller.User); + } + + [Fact] + public void AccessDependentProperties_UnsetContext() + { + // Arrange + var controller = new ConcreteApiController(); + + // Act & Assert + Assert.Null(controller.Context); + Assert.Null(controller.ModelState); + Assert.Null(controller.User); + } + + private class ConcreteApiController : ApiController + { + } + } +} \ No newline at end of file diff --git a/test/Microsoft.AspNet.Mvc.WebApiCompatShimTest/project.json b/test/Microsoft.AspNet.Mvc.WebApiCompatShimTest/project.json index ceb4dec9b7..1ca380fda6 100644 --- a/test/Microsoft.AspNet.Mvc.WebApiCompatShimTest/project.json +++ b/test/Microsoft.AspNet.Mvc.WebApiCompatShimTest/project.json @@ -1,16 +1,17 @@ { - "compilationOptions": { - "warningsAsErrors": "true" - }, - "dependencies": { - "Microsoft.AspNet.Mvc": "6.0.0-*", - "Microsoft.AspNet.Mvc.WebApiCompatShim": "6.0.0-*", - "Xunit.KRunner": "1.0.0-*" - }, - "commands": { - "test": "Xunit.KRunner" - }, - "frameworks": { - "aspnet50": { } - } + "compilationOptions": { + "warningsAsErrors": "true" + }, + "dependencies": { + "Microsoft.AspNet.Mvc": "6.0.0-*", + "Microsoft.AspNet.Mvc.WebApiCompatShim": "6.0.0-*", + "Moq": "4.2.1312.1622", + "Xunit.KRunner": "1.0.0-*" + }, + "commands": { + "test": "Xunit.KRunner" + }, + "frameworks": { + "aspnet50": { } + } } diff --git a/test/WebSites/WebApiCompatShimWebSite/Controllers/BasicApiController.cs b/test/WebSites/WebApiCompatShimWebSite/Controllers/BasicApiController.cs new file mode 100644 index 0000000000..9dc05e5467 --- /dev/null +++ b/test/WebSites/WebApiCompatShimWebSite/Controllers/BasicApiController.cs @@ -0,0 +1,36 @@ +// 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.Threading.Tasks; +using System.Web.Http; +using Microsoft.AspNet.Http; +using Microsoft.AspNet.Mvc; + +namespace WebApiCompatShimWebSite +{ + public class BasicApiController : ApiController + { + // Verifies property activation + [HttpGet] + public async Task WriteToHttpContext() + { + var message = string.Format( + "Hello, {0} from {1}", + User.Identity?.Name ?? "Anonymous User", + ActionContext.ActionDescriptor.DisplayName); + + await Context.Response.WriteAsync(message); + return new EmptyResult(); + } + + // Verifies property activation + [HttpGet] + public async Task GenerateUrl() + { + var message = string.Format("Visited: {0}", Url.Action()); + + await Context.Response.WriteAsync(message); + return new EmptyResult(); + } + } +} \ No newline at end of file From 2578b8107f56d2715b32e96b5c7f1c1fb8225f6d Mon Sep 17 00:00:00 2001 From: Ryan Nowak Date: Wed, 8 Oct 2014 13:50:13 -0700 Subject: [PATCH 05/39] Fix for #1277 - Add Options/Startup API for WebAPI shim Adds an options class, as well as a default options setup that will configure the default set of formatters. Currently most of what options needs to do is a placeholder, but it later do things like add ApplicationModelConventions, filters, formatters, model binders, etc. Those will be added in follow up items. --- .../WebApiCompatShimOptions.cs | 19 ++++++++++++ .../WebApiCompatShimOptionsSetup.cs | 30 +++++++++++++++++++ ...piCompatShimServiceCollectionExtensions.cs | 16 ++++++++++ .../WebApiCompatShimBasicTest.cs | 29 +++++++++++++++++- .../Controllers/BasicApiController.cs | 13 ++++++++ .../WebApiCompatShimWebSite/Startup.cs | 4 ++- 6 files changed, 109 insertions(+), 2 deletions(-) create mode 100644 src/Microsoft.AspNet.Mvc.WebApiCompatShim/WebApiCompatShimOptions.cs create mode 100644 src/Microsoft.AspNet.Mvc.WebApiCompatShim/WebApiCompatShimOptionsSetup.cs create mode 100644 src/Microsoft.AspNet.Mvc.WebApiCompatShim/WebApiCompatShimServiceCollectionExtensions.cs diff --git a/src/Microsoft.AspNet.Mvc.WebApiCompatShim/WebApiCompatShimOptions.cs b/src/Microsoft.AspNet.Mvc.WebApiCompatShim/WebApiCompatShimOptions.cs new file mode 100644 index 0000000000..e498c22864 --- /dev/null +++ b/src/Microsoft.AspNet.Mvc.WebApiCompatShim/WebApiCompatShimOptions.cs @@ -0,0 +1,19 @@ +// 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.Linq; +using System.Net.Http.Formatting; + +namespace Microsoft.AspNet.Mvc.WebApiCompatShim +{ + public class WebApiCompatShimOptions + { + public WebApiCompatShimOptions() + { + // Start with an empty collection, our options setup will add the default formatters. + Formatters = new MediaTypeFormatterCollection(Enumerable.Empty()); + } + + public MediaTypeFormatterCollection Formatters { get; set; } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Mvc.WebApiCompatShim/WebApiCompatShimOptionsSetup.cs b/src/Microsoft.AspNet.Mvc.WebApiCompatShim/WebApiCompatShimOptionsSetup.cs new file mode 100644 index 0000000000..e2f837828b --- /dev/null +++ b/src/Microsoft.AspNet.Mvc.WebApiCompatShim/WebApiCompatShimOptionsSetup.cs @@ -0,0 +1,30 @@ +// 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.Net.Http.Formatting; +using Microsoft.Framework.OptionsModel; + +namespace Microsoft.AspNet.Mvc.WebApiCompatShim +{ + public class WebApiCompatShimOptionsSetup : IOptionsAction, IOptionsAction + { + public int Order + { + // We want to run after the default MvcOptionsSetup. + get { return DefaultOrder.DefaultFrameworkSortOrder + 100; } + } + + public string Name { get; set; } + + public void Invoke(MvcOptions options) + { + // Placeholder + } + + public void Invoke(WebApiCompatShimOptions options) + { + // Add the default formatters + options.Formatters.AddRange(new MediaTypeFormatterCollection()); + } + } +} diff --git a/src/Microsoft.AspNet.Mvc.WebApiCompatShim/WebApiCompatShimServiceCollectionExtensions.cs b/src/Microsoft.AspNet.Mvc.WebApiCompatShim/WebApiCompatShimServiceCollectionExtensions.cs new file mode 100644 index 0000000000..8ff52fb81a --- /dev/null +++ b/src/Microsoft.AspNet.Mvc.WebApiCompatShim/WebApiCompatShimServiceCollectionExtensions.cs @@ -0,0 +1,16 @@ +// 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.WebApiCompatShim; + +namespace Microsoft.Framework.DependencyInjection +{ + public static class WebApiCompatShimServiceCollectionExtensions + { + public static IServiceCollection AddWebApiConventions(this IServiceCollection services) + { + services.AddOptionsAction(); + return services; + } + } +} diff --git a/test/Microsoft.AspNet.Mvc.FunctionalTests/WebApiCompatShimBasicTest.cs b/test/Microsoft.AspNet.Mvc.FunctionalTests/WebApiCompatShimBasicTest.cs index ebc4129d2c..9069278dd4 100644 --- a/test/Microsoft.AspNet.Mvc.FunctionalTests/WebApiCompatShimBasicTest.cs +++ b/test/Microsoft.AspNet.Mvc.FunctionalTests/WebApiCompatShimBasicTest.cs @@ -3,11 +3,13 @@ #if ASPNET50 using System; +using System.Net; +using System.Net.Http.Formatting; using System.Threading.Tasks; using Microsoft.AspNet.Builder; using Microsoft.AspNet.TestHost; +using Newtonsoft.Json; using Xunit; -using System.Net; namespace Microsoft.AspNet.Mvc.FunctionalTests { @@ -51,6 +53,31 @@ public async Task ApiController_Activates_UrlHelper() "Visited: /BasicApi/GenerateUrl", content); } + + [Fact] + public async Task Options_SetsDefaultFormatters() + { + // Arrange + var server = TestServer.Create(_provider, _app); + var client = server.CreateClient(); + + var expected = new string[] + { + typeof(JsonMediaTypeFormatter).FullName, + typeof(XmlMediaTypeFormatter).FullName, + typeof(FormUrlEncodedMediaTypeFormatter).FullName, + }; + + // Act + var response = await client.GetAsync("http://localhost/BasicApi/GetFormatters"); + var content = await response.Content.ReadAsStringAsync(); + + var formatters = JsonConvert.DeserializeObject(content); + + // Assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.Equal(expected, formatters); + } } } #endif \ No newline at end of file diff --git a/test/WebSites/WebApiCompatShimWebSite/Controllers/BasicApiController.cs b/test/WebSites/WebApiCompatShimWebSite/Controllers/BasicApiController.cs index 9dc05e5467..09478361be 100644 --- a/test/WebSites/WebApiCompatShimWebSite/Controllers/BasicApiController.cs +++ b/test/WebSites/WebApiCompatShimWebSite/Controllers/BasicApiController.cs @@ -1,15 +1,21 @@ // 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.Linq; using System.Threading.Tasks; using System.Web.Http; using Microsoft.AspNet.Http; using Microsoft.AspNet.Mvc; +using Microsoft.AspNet.Mvc.WebApiCompatShim; +using Microsoft.Framework.OptionsModel; namespace WebApiCompatShimWebSite { public class BasicApiController : ApiController { + [Activate] + public IOptionsAccessor OptionsAccessor { get; set; } + // Verifies property activation [HttpGet] public async Task WriteToHttpContext() @@ -32,5 +38,12 @@ public async Task GenerateUrl() await Context.Response.WriteAsync(message); return new EmptyResult(); } + + // Verifies the default options configure formatters correctly. + [HttpGet] + public string[] GetFormatters() + { + return OptionsAccessor.Options.Formatters.Select(f => f.GetType().FullName).ToArray(); + } } } \ No newline at end of file diff --git a/test/WebSites/WebApiCompatShimWebSite/Startup.cs b/test/WebSites/WebApiCompatShimWebSite/Startup.cs index 284d184502..daa7f3fd7f 100644 --- a/test/WebSites/WebApiCompatShimWebSite/Startup.cs +++ b/test/WebSites/WebApiCompatShimWebSite/Startup.cs @@ -15,8 +15,10 @@ public void Configure(IApplicationBuilder app) app.UsePerRequestServices(services => { services.AddMvc(configuration); - }); + services.AddWebApiConventions(); + }); + app.UseMvc(); } } From 9b11c1d90f824abded6fc9a4a161407adb3948ce Mon Sep 17 00:00:00 2001 From: Ryan Nowak Date: Wed, 8 Oct 2014 14:31:36 -0700 Subject: [PATCH 06/39] Fix #1276 - Implement WebAPI action conventions and overloading This change adds ApplicationModel conventions that can enable WebAPI action conventions (verb mapping) and WebAPI overloading. The conventions activate when a controller has a marker attribute. ApiController has this attribute, so any ported code will automatically opt-in. Also ported some old tests for action selection to our new functional test framework. --- .../ApiController.cs | 3 + .../IUseWebApiActionConventions.cs | 9 + .../Conventions/IUseWebApiOverloading.cs | 9 + .../UseWebApiActionConventionsAttribute.cs | 12 + .../UseWebApiOverloadingAttribute.cs | 12 + ...iActionConventionsGlobalModelConvention.cs | 95 +++ .../WebApiOverloadingGlobalModelConvention.cs | 35 + .../FormDataCollectionExtensions.cs | 109 +++ .../FromUriAttribute.cs | 22 + .../OverloadActionConstraint.cs | 125 ++++ .../Properties/Resources.Designer.cs | 62 ++ .../Resources.resx | 126 ++++ .../WebApiCompatShimOptionsSetup.cs | 4 +- .../WebApiCompatShimActionSelectionTest.cs | 627 ++++++++++++++++++ .../WebApiCompatShimBasicTest.cs | 8 +- .../ApiControllerActionDiscoveryTest.cs | 218 +++++- .../FormDataCollectionExtensionsTest.cs | 43 ++ .../OverloadActionConstraintTest.cs | 444 +++++++++++++ .../ActionSelectionFilter.cs | 20 + .../EnumParameterOverloadsController.cs | 35 + .../Legacy/ParameterAttributeController.cs | 22 + .../ActionSelection/Legacy/TestController.cs | 39 ++ ...PIActionConventionsActionNameController.cs | 18 + .../WebAPIActionConventionsController.cs | 41 ++ ...IActionConventionsDefaultPostController.cs | 16 + ...ActionConventionsVerbOverrideController.cs | 18 + .../WebApiCompatShimWebSite/Models/User.cs | 11 + .../Models/UserAddress.cs | 11 + .../Models/UserKind.cs | 12 + .../WebApiCompatShimWebSite/Startup.cs | 12 +- 30 files changed, 2197 insertions(+), 21 deletions(-) create mode 100644 src/Microsoft.AspNet.Mvc.WebApiCompatShim/Conventions/IUseWebApiActionConventions.cs create mode 100644 src/Microsoft.AspNet.Mvc.WebApiCompatShim/Conventions/IUseWebApiOverloading.cs create mode 100644 src/Microsoft.AspNet.Mvc.WebApiCompatShim/Conventions/UseWebApiActionConventionsAttribute.cs create mode 100644 src/Microsoft.AspNet.Mvc.WebApiCompatShim/Conventions/UseWebApiOverloadingAttribute.cs create mode 100644 src/Microsoft.AspNet.Mvc.WebApiCompatShim/Conventions/WebApiActionConventionsGlobalModelConvention.cs create mode 100644 src/Microsoft.AspNet.Mvc.WebApiCompatShim/Conventions/WebApiOverloadingGlobalModelConvention.cs create mode 100644 src/Microsoft.AspNet.Mvc.WebApiCompatShim/FormDataCollectionExtensions.cs create mode 100644 src/Microsoft.AspNet.Mvc.WebApiCompatShim/FromUriAttribute.cs create mode 100644 src/Microsoft.AspNet.Mvc.WebApiCompatShim/OverloadActionConstraint.cs create mode 100644 src/Microsoft.AspNet.Mvc.WebApiCompatShim/Properties/Resources.Designer.cs create mode 100644 src/Microsoft.AspNet.Mvc.WebApiCompatShim/Resources.resx create mode 100644 test/Microsoft.AspNet.Mvc.FunctionalTests/WebApiCompatShimActionSelectionTest.cs create mode 100644 test/Microsoft.AspNet.Mvc.WebApiCompatShimTest/FormDataCollectionExtensionsTest.cs create mode 100644 test/Microsoft.AspNet.Mvc.WebApiCompatShimTest/OverloadActionConstraintTest.cs create mode 100644 test/WebSites/WebApiCompatShimWebSite/ActionSelectionFilter.cs create mode 100644 test/WebSites/WebApiCompatShimWebSite/Controllers/ActionSelection/Legacy/EnumParameterOverloadsController.cs create mode 100644 test/WebSites/WebApiCompatShimWebSite/Controllers/ActionSelection/Legacy/ParameterAttributeController.cs create mode 100644 test/WebSites/WebApiCompatShimWebSite/Controllers/ActionSelection/Legacy/TestController.cs create mode 100644 test/WebSites/WebApiCompatShimWebSite/Controllers/ActionSelection/WebAPIActionConventionsActionNameController.cs create mode 100644 test/WebSites/WebApiCompatShimWebSite/Controllers/ActionSelection/WebAPIActionConventionsController.cs create mode 100644 test/WebSites/WebApiCompatShimWebSite/Controllers/ActionSelection/WebAPIActionConventionsDefaultPostController.cs create mode 100644 test/WebSites/WebApiCompatShimWebSite/Controllers/ActionSelection/WebAPIActionConventionsVerbOverrideController.cs create mode 100644 test/WebSites/WebApiCompatShimWebSite/Models/User.cs create mode 100644 test/WebSites/WebApiCompatShimWebSite/Models/UserAddress.cs create mode 100644 test/WebSites/WebApiCompatShimWebSite/Models/UserKind.cs diff --git a/src/Microsoft.AspNet.Mvc.WebApiCompatShim/ApiController.cs b/src/Microsoft.AspNet.Mvc.WebApiCompatShim/ApiController.cs index c9f8d3fe4c..45b8c0a1f1 100644 --- a/src/Microsoft.AspNet.Mvc.WebApiCompatShim/ApiController.cs +++ b/src/Microsoft.AspNet.Mvc.WebApiCompatShim/ApiController.cs @@ -5,9 +5,12 @@ using Microsoft.AspNet.Http; using Microsoft.AspNet.Mvc; using Microsoft.AspNet.Mvc.ModelBinding; +using Microsoft.AspNet.Mvc.WebApiCompatShim; namespace System.Web.Http { + [UseWebApiActionConventions] + [UseWebApiOverloading] public abstract class ApiController : IDisposable { /// Gets the action context. diff --git a/src/Microsoft.AspNet.Mvc.WebApiCompatShim/Conventions/IUseWebApiActionConventions.cs b/src/Microsoft.AspNet.Mvc.WebApiCompatShim/Conventions/IUseWebApiActionConventions.cs new file mode 100644 index 0000000000..52ec587231 --- /dev/null +++ b/src/Microsoft.AspNet.Mvc.WebApiCompatShim/Conventions/IUseWebApiActionConventions.cs @@ -0,0 +1,9 @@ +// 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.WebApiCompatShim +{ + public interface IUseWebApiActionConventions + { + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Mvc.WebApiCompatShim/Conventions/IUseWebApiOverloading.cs b/src/Microsoft.AspNet.Mvc.WebApiCompatShim/Conventions/IUseWebApiOverloading.cs new file mode 100644 index 0000000000..04e0ef4e13 --- /dev/null +++ b/src/Microsoft.AspNet.Mvc.WebApiCompatShim/Conventions/IUseWebApiOverloading.cs @@ -0,0 +1,9 @@ +// 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.WebApiCompatShim +{ + public interface IUseWebApiOverloading + { + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Mvc.WebApiCompatShim/Conventions/UseWebApiActionConventionsAttribute.cs b/src/Microsoft.AspNet.Mvc.WebApiCompatShim/Conventions/UseWebApiActionConventionsAttribute.cs new file mode 100644 index 0000000000..6a055894f3 --- /dev/null +++ b/src/Microsoft.AspNet.Mvc.WebApiCompatShim/Conventions/UseWebApiActionConventionsAttribute.cs @@ -0,0 +1,12 @@ +// 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; + +namespace Microsoft.AspNet.Mvc.WebApiCompatShim +{ + [AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = true)] + public class UseWebApiActionConventionsAttribute : Attribute, IUseWebApiActionConventions + { + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Mvc.WebApiCompatShim/Conventions/UseWebApiOverloadingAttribute.cs b/src/Microsoft.AspNet.Mvc.WebApiCompatShim/Conventions/UseWebApiOverloadingAttribute.cs new file mode 100644 index 0000000000..84565cce3d --- /dev/null +++ b/src/Microsoft.AspNet.Mvc.WebApiCompatShim/Conventions/UseWebApiOverloadingAttribute.cs @@ -0,0 +1,12 @@ +// 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; + +namespace Microsoft.AspNet.Mvc.WebApiCompatShim +{ + [AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = true)] + public class UseWebApiOverloadingAttribute : Attribute, IUseWebApiOverloading + { + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Mvc.WebApiCompatShim/Conventions/WebApiActionConventionsGlobalModelConvention.cs b/src/Microsoft.AspNet.Mvc.WebApiCompatShim/Conventions/WebApiActionConventionsGlobalModelConvention.cs new file mode 100644 index 0000000000..c52d92b6b7 --- /dev/null +++ b/src/Microsoft.AspNet.Mvc.WebApiCompatShim/Conventions/WebApiActionConventionsGlobalModelConvention.cs @@ -0,0 +1,95 @@ +// 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 System.Linq; +using Microsoft.AspNet.Mvc.ApplicationModel; + +namespace Microsoft.AspNet.Mvc.WebApiCompatShim +{ + public class WebApiActionConventionsGlobalModelConvention : IGlobalModelConvention + { + private static readonly string[] SupportedHttpMethodConventions = new string[] + { + "GET", + "PUT", + "POST", + "DELETE", + "PATCH", + "HEAD", + "OPTIONS", + }; + + public void Apply(GlobalModel model) + { + foreach (var controller in model.Controllers) + { + if (IsConventionApplicable(controller)) + { + Apply(controller); + } + } + } + + private bool IsConventionApplicable(ControllerModel controller) + { + return controller.Attributes.OfType().Any(); + } + + private void Apply(ControllerModel model) + { + var newActions = new List(); + + foreach (var action in model.Actions) + { + SetHttpMethodFromConvention(action); + + // Action Name doesn't really come into play with attribute routed actions. However for a + // non-attribute-routed action we need to create a 'named' version and an 'unnamed' version. + if (!IsActionAttributeRouted(action)) + { + var namedAction = action; + + var unnamedAction = new ActionModel(namedAction); + unnamedAction.IsActionNameMatchRequired = false; + newActions.Add(unnamedAction); + } + } + + model.Actions.AddRange(newActions); + } + + private bool IsActionAttributeRouted(ActionModel action) + { + if (action.Controller.AttributeRoutes.Count > 0) + { + return true; + } + + return action.AttributeRouteModel?.Template != null; + } + + private void SetHttpMethodFromConvention(ActionModel action) + { + if (action.HttpMethods.Count > 0) + { + // If the HttpMethods are set from attributes, don't override it with the convention + return; + } + + // The Method name is used to infer verb constraints. Changing the action name has not impact. + foreach (var verb in SupportedHttpMethodConventions) + { + if (action.ActionMethod.Name.StartsWith(verb, StringComparison.OrdinalIgnoreCase)) + { + action.HttpMethods.Add(verb); + return; + } + } + + // If no convention matches, then assume POST + action.HttpMethods.Add("POST"); + } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Mvc.WebApiCompatShim/Conventions/WebApiOverloadingGlobalModelConvention.cs b/src/Microsoft.AspNet.Mvc.WebApiCompatShim/Conventions/WebApiOverloadingGlobalModelConvention.cs new file mode 100644 index 0000000000..8158788989 --- /dev/null +++ b/src/Microsoft.AspNet.Mvc.WebApiCompatShim/Conventions/WebApiOverloadingGlobalModelConvention.cs @@ -0,0 +1,35 @@ +// 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.Linq; +using Microsoft.AspNet.Mvc.ApplicationModel; + +namespace Microsoft.AspNet.Mvc.WebApiCompatShim +{ + public class WebApiOverloadingGlobalModelConvention : IGlobalModelConvention + { + public void Apply(GlobalModel model) + { + foreach (var controller in model.Controllers) + { + if (IsConventionApplicable(controller)) + { + Apply(controller); + } + } + } + + private bool IsConventionApplicable(ControllerModel controller) + { + return controller.Attributes.OfType().Any(); + } + + private void Apply(ControllerModel model) + { + foreach (var action in model.Actions) + { + action.ActionConstraints.Add(new OverloadActionConstraint()); + } + } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Mvc.WebApiCompatShim/FormDataCollectionExtensions.cs b/src/Microsoft.AspNet.Mvc.WebApiCompatShim/FormDataCollectionExtensions.cs new file mode 100644 index 0000000000..5a5616da3e --- /dev/null +++ b/src/Microsoft.AspNet.Mvc.WebApiCompatShim/FormDataCollectionExtensions.cs @@ -0,0 +1,109 @@ +// 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 System.Net.Http.Formatting; +using System.Text; + +namespace Microsoft.AspNet.Mvc.WebApiCompatShim +{ + public static class FormDataCollectionExtensions + { + // This is a helper method to use Model Binding over a JQuery syntax. + // Normalize from JQuery to MVC keys. The model binding infrastructure uses MVC keys + // x[] --> x + // [] --> "" + // x[field] --> x.field, where field is not a number + public static string NormalizeJQueryToMvc(string key) + { + if (key == null) + { + return string.Empty; + } + + StringBuilder sb = null; + var i = 0; + while (true) + { + int indexOpen = key.IndexOf('[', i); + if (indexOpen < 0) + { + // Fast path, no normalization needed. + // This skips the string conversion and allocating the string builder. + if (i == 0) + { + return key; + } + sb = sb ?? new StringBuilder(); + sb.Append(key, i, key.Length - i); + break; // no more brackets + } + + sb = sb ?? new StringBuilder(); + sb.Append(key, i, indexOpen - i); // everything up to "[" + + // Find closing bracket. + var indexClose = key.IndexOf(']', indexOpen); + if (indexClose == -1) + { + throw new ArgumentException(Resources.JQuerySyntaxMissingClosingBracket, "key"); + } + + if (indexClose == indexOpen + 1) + { + // Empty bracket. Signifies array. Just remove. + } + else + { + if (char.IsDigit(key[indexOpen + 1])) + { + // array index. Leave unchanged. + sb.Append(key, indexOpen, indexClose - indexOpen + 1); + } + else + { + // Field name. Convert to dot notation. + sb.Append('.'); + sb.Append(key, indexOpen + 1, indexClose - indexOpen - 1); + } + } + + i = indexClose + 1; + if (i >= key.Length) + { + break; // end of string + } + } + return sb.ToString(); + } + + public static IEnumerable> GetJQueryNameValuePairs( + [NotNull] this FormDataCollection formData) + { + var count = 0; + + foreach (var kv in formData) + { + ThrowIfMaxHttpCollectionKeysExceeded(count); + + var key = NormalizeJQueryToMvc(kv.Key); + var value = kv.Value ?? string.Empty; + yield return new KeyValuePair(key, value); + + count++; + } + } + + private static void ThrowIfMaxHttpCollectionKeysExceeded(int count) + { + if (count >= MediaTypeFormatter.MaxHttpCollectionKeys) + { + var message = Resources.FormatMaxHttpCollectionKeyLimitReached( + MediaTypeFormatter.MaxHttpCollectionKeys, + typeof(MediaTypeFormatter)); + throw new InvalidOperationException(message); + } + } + } +} diff --git a/src/Microsoft.AspNet.Mvc.WebApiCompatShim/FromUriAttribute.cs b/src/Microsoft.AspNet.Mvc.WebApiCompatShim/FromUriAttribute.cs new file mode 100644 index 0000000000..df8c80c42e --- /dev/null +++ b/src/Microsoft.AspNet.Mvc.WebApiCompatShim/FromUriAttribute.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 System; +using Microsoft.AspNet.Mvc.ApplicationModel; + +namespace Microsoft.AspNet.Mvc.WebApiCompatShim +{ + [AttributeUsage(AttributeTargets.Parameter, AllowMultiple = false, Inherited = true)] + public class FromUriAttribute : Attribute, IParameterModelConvention + { + public string Name { get; set; } + + public void Apply(ParameterModel model) + { + if (Name != null) + { + model.ParameterName = Name; + } + } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Mvc.WebApiCompatShim/OverloadActionConstraint.cs b/src/Microsoft.AspNet.Mvc.WebApiCompatShim/OverloadActionConstraint.cs new file mode 100644 index 0000000000..6abed0240d --- /dev/null +++ b/src/Microsoft.AspNet.Mvc.WebApiCompatShim/OverloadActionConstraint.cs @@ -0,0 +1,125 @@ +// 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 System.Linq; +using Microsoft.AspNet.Mvc.ModelBinding; +using Microsoft.AspNet.Routing; +using System.Net.Http.Formatting; + +namespace Microsoft.AspNet.Mvc.WebApiCompatShim +{ + public class OverloadActionConstraint : IActionConstraint + { + public int Order { get; } = Int32.MaxValue; + + public bool Accept(ActionConstraintContext context) + { + var candidates = context.Candidates.Select(c => new + { + Action = c, + Parameters = GetOverloadableParameters(c), + }); + + // Combined route value keys and query string keys. These are the values available for overload selection. + var requestKeys = GetCombinedKeys(context.RouteContext); + + // Group candidates by the highest number of keys, and then process them until we find an action + // with all parameters satisfied. + foreach (var group in candidates.GroupBy(c => c.Parameters?.Count ?? 0).OrderByDescending(g => g.Key)) + { + var foundMatch = false; + foreach (var candidate in group) + { + var allFound = true; + if (candidate.Parameters != null) + { + foreach (var parameter in candidate.Parameters) + { + if (!requestKeys.Contains(parameter.ParameterBindingInfo.Prefix)) + { + if (candidate.Action.Action == context.CurrentCandidate.Action) + { + return false; + } + + allFound = false; + break; + } + } + } + + if (allFound) + { + foundMatch = true; + } + } + + if (foundMatch) + { + return group.Any(c => c.Action.Action == context.CurrentCandidate.Action); + } + } + + return false; + } + + private List GetOverloadableParameters(ActionSelectorCandidate candidate) + { + if (candidate.Action.Parameters == null) + { + return null; + } + + var isOverloaded = false; + foreach (var constraint in candidate.Constraints) + { + if (constraint is OverloadActionConstraint) + { + isOverloaded = true; + } + } + + if (!isOverloaded) + { + return null; + } + + // We only consider parameters that are bound from the URL. + return candidate.Action.Parameters.Where( + p => + p.ParameterBindingInfo != null && + !p.IsOptional && + ValueProviderResult.CanConvertFromString(p.ParameterBindingInfo.ParameterType)) + .ToList(); + } + + private static ISet GetCombinedKeys(RouteContext routeContext) + { + var keys = new HashSet(routeContext.RouteData.Values.Keys, StringComparer.OrdinalIgnoreCase); + keys.Remove("controller"); + keys.Remove("action"); + + var queryString = routeContext.HttpContext.Request.QueryString.ToUriComponent(); + + if (queryString.Length > 0) + { + // We need to chop off the leading '?' + var queryData = new FormDataCollection(queryString.Substring(1)); + + var queryNameValuePairs = queryData.GetJQueryNameValuePairs(); + + if (queryNameValuePairs != null) + { + foreach (var queryNameValuePair in queryNameValuePairs) + { + keys.Add(queryNameValuePair.Key); + } + } + } + + return keys; + } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Mvc.WebApiCompatShim/Properties/Resources.Designer.cs b/src/Microsoft.AspNet.Mvc.WebApiCompatShim/Properties/Resources.Designer.cs new file mode 100644 index 0000000000..ee2edffaaf --- /dev/null +++ b/src/Microsoft.AspNet.Mvc.WebApiCompatShim/Properties/Resources.Designer.cs @@ -0,0 +1,62 @@ +// +namespace Microsoft.AspNet.Mvc.WebApiCompatShim +{ + using System.Globalization; + using System.Reflection; + using System.Resources; + + internal static class Resources + { + private static readonly ResourceManager _resourceManager + = new ResourceManager("Microsoft.AspNet.Mvc.WebApiCompatShim.Resources", typeof(Resources).GetTypeInfo().Assembly); + + /// + /// The key is invalid JQuery syntax because it is missing a closing bracket. + /// + internal static string JQuerySyntaxMissingClosingBracket + { + get { return GetString("JQuerySyntaxMissingClosingBracket"); } + } + + /// + /// The key is invalid JQuery syntax because it is missing a closing bracket. + /// + internal static string FormatJQuerySyntaxMissingClosingBracket() + { + return GetString("JQuerySyntaxMissingClosingBracket"); + } + + /// + /// The number of keys in a NameValueCollection has exceeded the limit of '{0}'. You can adjust it by modifying the MaxHttpCollectionKeys property on the '{1}' class. + /// + internal static string MaxHttpCollectionKeyLimitReached + { + get { return GetString("MaxHttpCollectionKeyLimitReached"); } + } + + /// + /// The number of keys in a NameValueCollection has exceeded the limit of '{0}'. You can adjust it by modifying the MaxHttpCollectionKeys property on the '{1}' class. + /// + internal static string FormatMaxHttpCollectionKeyLimitReached(object p0, object p1) + { + return string.Format(CultureInfo.CurrentCulture, GetString("MaxHttpCollectionKeyLimitReached"), p0, p1); + } + + private static string GetString(string name, params string[] formatterNames) + { + var value = _resourceManager.GetString(name); + + System.Diagnostics.Debug.Assert(value != null); + + if (formatterNames != null) + { + for (var i = 0; i < formatterNames.Length; i++) + { + value = value.Replace("{" + formatterNames[i] + "}", "{" + i + "}"); + } + } + + return value; + } + } +} diff --git a/src/Microsoft.AspNet.Mvc.WebApiCompatShim/Resources.resx b/src/Microsoft.AspNet.Mvc.WebApiCompatShim/Resources.resx new file mode 100644 index 0000000000..00afe5899e --- /dev/null +++ b/src/Microsoft.AspNet.Mvc.WebApiCompatShim/Resources.resx @@ -0,0 +1,126 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + The key is invalid JQuery syntax because it is missing a closing bracket. + + + The number of keys in a NameValueCollection has exceeded the limit of '{0}'. You can adjust it by modifying the MaxHttpCollectionKeys property on the '{1}' class. + + \ No newline at end of file diff --git a/src/Microsoft.AspNet.Mvc.WebApiCompatShim/WebApiCompatShimOptionsSetup.cs b/src/Microsoft.AspNet.Mvc.WebApiCompatShim/WebApiCompatShimOptionsSetup.cs index e2f837828b..3a1e0adfdc 100644 --- a/src/Microsoft.AspNet.Mvc.WebApiCompatShim/WebApiCompatShimOptionsSetup.cs +++ b/src/Microsoft.AspNet.Mvc.WebApiCompatShim/WebApiCompatShimOptionsSetup.cs @@ -18,7 +18,9 @@ public int Order public void Invoke(MvcOptions options) { - // Placeholder + // Add webapi behaviors to controllers with the appropriate attributes + options.ApplicationModelConventions.Add(new WebApiActionConventionsGlobalModelConvention()); + options.ApplicationModelConventions.Add(new WebApiOverloadingGlobalModelConvention()); } public void Invoke(WebApiCompatShimOptions options) diff --git a/test/Microsoft.AspNet.Mvc.FunctionalTests/WebApiCompatShimActionSelectionTest.cs b/test/Microsoft.AspNet.Mvc.FunctionalTests/WebApiCompatShimActionSelectionTest.cs new file mode 100644 index 0000000000..93072ae413 --- /dev/null +++ b/test/Microsoft.AspNet.Mvc.FunctionalTests/WebApiCompatShimActionSelectionTest.cs @@ -0,0 +1,627 @@ +// 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. + +#if ASPNET50 +using System; +using System.Net; +using System.Net.Http; +using System.Threading.Tasks; +using Microsoft.AspNet.Builder; +using Microsoft.AspNet.TestHost; +using Newtonsoft.Json; +using Xunit; + +namespace Microsoft.AspNet.Mvc.FunctionalTests +{ + public class WebApiCompatShimActionSelectionTest + { + private readonly IServiceProvider _services = TestHelper.CreateServices(nameof(WebApiCompatShimWebSite)); + private readonly Action _app = new WebApiCompatShimWebSite.Startup().Configure; + + [Theory] + [InlineData("GET", "GetItems")] + [InlineData("PUT", "PutItems")] + [InlineData("POST", "PostItems")] + [InlineData("DELETE", "DeleteItems")] + [InlineData("PATCH", "PatchItems")] + [InlineData("HEAD", "HeadItems")] + [InlineData("OPTIONS", "OptionsItems")] + public async Task WebAPIConvention_TakesHttpMethodFromPrefix_UnnamedAction(string httpMethod, string actionName) + { + // Arrange + var server = TestServer.Create(_services, _app); + var client = server.CreateClient(); + + var request = new HttpRequestMessage( + new HttpMethod(httpMethod), + "http://localhost/api/Admin/WebAPIActionConventions"); + + // Act + var response = await client.SendAsync(request); + var body = await response.Content.ReadAsStringAsync(); + + var result = JsonConvert.DeserializeObject(body); + + //Assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.Equal(actionName, result.ActionName); + } + + [Theory] + [InlineData("GET", "GetItems")] + [InlineData("PUT", "PutItems")] + [InlineData("POST", "PostItems")] + [InlineData("DELETE", "DeleteItems")] + [InlineData("PATCH", "PatchItems")] + [InlineData("HEAD", "HeadItems")] + [InlineData("OPTIONS", "OptionsItems")] + public async Task WebAPIConvention_TakesHttpMethodFromPrefix_NamedAction(string httpMethod, string actionName) + { + // Arrange + var server = TestServer.Create(_services, _app); + var client = server.CreateClient(); + + var request = new HttpRequestMessage( + new HttpMethod(httpMethod), + "http://localhost/api/Blog/WebAPIActionConventions/" + actionName); + + // Act + var response = await client.SendAsync(request); + var body = await response.Content.ReadAsStringAsync(); + + var result = JsonConvert.DeserializeObject(body); + + //Assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.Equal(actionName, result.ActionName); + } + + [Fact] + public async Task WebAPIConvention_TakesHttpMethodFromPrefix_NamedAction_MismatchedVerb() + { + // Arrange + var server = TestServer.Create(_services, _app); + var client = server.CreateClient(); + + var request = new HttpRequestMessage( + new HttpMethod("POST"), + "http://localhost/api/Blog/WebAPIActionConventions/GetItems"); + + // Act + var response = await client.SendAsync(request); + + //Assert + Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); + } + + [Fact] + public async Task WebAPIConvention_TakesHttpMethodFromPrefix_UnnamedAction_DefaultVerbIsPost_Success() + { + // Arrange + var server = TestServer.Create(_services, _app); + var client = server.CreateClient(); + + var request = new HttpRequestMessage( + new HttpMethod("POST"), + "http://localhost/api/Admin/WebApiActionConventionsDefaultPost"); + + // Act + var response = await client.SendAsync(request); + var body = await response.Content.ReadAsStringAsync(); + + var result = JsonConvert.DeserializeObject(body); + + //Assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.Equal("DefaultVerbIsPost", result.ActionName); + } + + [Fact] + public async Task WebAPIConvention_TakesHttpMethodFromPrefix_NamedAction_DefaultVerbIsPost_Success() + { + // Arrange + var server = TestServer.Create(_services, _app); + var client = server.CreateClient(); + + var request = new HttpRequestMessage( + new HttpMethod("POST"), + "http://localhost/api/Blog/WebAPIActionConventionsDefaultPost/DefaultVerbIsPost"); + + // Act + var response = await client.SendAsync(request); + var body = await response.Content.ReadAsStringAsync(); + + var result = JsonConvert.DeserializeObject(body); + + //Assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.Equal("DefaultVerbIsPost", result.ActionName); + } + + [Fact] + public async Task WebAPIConvention_TakesHttpMethodFromPrefix_UnnamedAction_DefaultVerbIsPost_VerbMismatch() + { + // Arrange + var server = TestServer.Create(_services, _app); + var client = server.CreateClient(); + + var request = new HttpRequestMessage( + new HttpMethod("GET"), + "http://localhost/api/Admin/WebApiActionConventionsDefaultPost"); + + // Act + var response = await client.SendAsync(request); + + //Assert + Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); + } + + [Fact] + public async Task WebAPIConvention_TakesHttpMethodFromPrefix_NamedAction_DefaultVerbIsPost_VerbMismatch() + { + // Arrange + var server = TestServer.Create(_services, _app); + var client = server.CreateClient(); + + var request = new HttpRequestMessage( + new HttpMethod("PUT"), + "http://localhost/api/Blog/WebApiActionConventionsDefaultPost/DefaultVerbIsPost"); + + // Act + var response = await client.SendAsync(request); + + //Assert + Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); + } + + [Fact] + public async Task WebAPIConvention_TakesHttpMethodFromMethodName_NotActionName_UnnamedAction_Success() + { + // Arrange + var server = TestServer.Create(_services, _app); + var client = server.CreateClient(); + + var request = new HttpRequestMessage( + new HttpMethod("POST"), + "http://localhost/api/Admin/WebAPIActionConventionsActionName"); + + // Act + var response = await client.SendAsync(request); + var body = await response.Content.ReadAsStringAsync(); + + var result = JsonConvert.DeserializeObject(body); + + //Assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.Equal("GetItems", result.ActionName); + } + + [Fact] + public async Task WebAPIConvention_TakesHttpMethodFromMethodName_NotActionName_NamedAction_Success() + { + // Arrange + var server = TestServer.Create(_services, _app); + var client = server.CreateClient(); + + var request = new HttpRequestMessage( + new HttpMethod("POST"), + "http://localhost/api/Blog/WebAPIActionConventionsActionName/GetItems"); + + // Act + var response = await client.SendAsync(request); + var body = await response.Content.ReadAsStringAsync(); + + var result = JsonConvert.DeserializeObject(body); + + //Assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.Equal("GetItems", result.ActionName); + } + + [Fact] + public async Task WebAPIConvention_TakesHttpMethodFromMethodName_NotActionName_UnnamedAction_VerbMismatch() + { + // Arrange + var server = TestServer.Create(_services, _app); + var client = server.CreateClient(); + + var request = new HttpRequestMessage( + new HttpMethod("Get"), + "http://localhost/api/Admin/WebAPIActionConventionsActionName"); + + // Act + var response = await client.SendAsync(request); + + //Assert + Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); + } + + [Fact] + public async Task WebAPIConvention_TakesHttpMethodFromMethodName_NotActionName_NamedAction_VerbMismatch() + { + // Arrange + var server = TestServer.Create(_services, _app); + var client = server.CreateClient(); + + var request = new HttpRequestMessage( + new HttpMethod("GET"), + "http://localhost/api/Blog/WebAPIActionConventionsActionName/GetItems"); + + // Act + var response = await client.SendAsync(request); + + //Assert + Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); + } + + [Fact] + public async Task WebAPIConvention_HttpMethodOverride_UnnamedAction_Success() + { + // Arrange + var server = TestServer.Create(_services, _app); + var client = server.CreateClient(); + + var request = new HttpRequestMessage( + new HttpMethod("GET"), + "http://localhost/api/Admin/WebAPIActionConventionsVerbOverride"); + + // Act + var response = await client.SendAsync(request); + var body = await response.Content.ReadAsStringAsync(); + + var result = JsonConvert.DeserializeObject(body); + + //Assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.Equal("PostItems", result.ActionName); + } + + [Fact] + public async Task WebAPIConvention_HttpMethodOverride_NamedAction_Success() + { + // Arrange + var server = TestServer.Create(_services, _app); + var client = server.CreateClient(); + + var request = new HttpRequestMessage( + new HttpMethod("GET"), + "http://localhost/api/Blog/WebAPIActionConventionsVerbOverride/PostItems"); + + // Act + var response = await client.SendAsync(request); + var body = await response.Content.ReadAsStringAsync(); + + var result = JsonConvert.DeserializeObject(body); + + //Assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.Equal("PostItems", result.ActionName); + } + + [Fact] + public async Task WebAPIConvention_HttpMethodOverride_UnnamedAction_VerbMismatch() + { + // Arrange + var server = TestServer.Create(_services, _app); + var client = server.CreateClient(); + + var request = new HttpRequestMessage( + new HttpMethod("POST"), + "http://localhost/api/Admin/WebAPIActionConventionsVerbOverride"); + + // Act + var response = await client.SendAsync(request); + + //Assert + Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); + } + + [Fact] + public async Task WebAPIConvention_HttpMethodOverride_NamedAction_VerbMismatch() + { + // Arrange + var server = TestServer.Create(_services, _app); + var client = server.CreateClient(); + + var request = new HttpRequestMessage( + new HttpMethod("POST"), + "http://localhost/api/Blog/WebAPIActionConventionsVerbOverride/PostItems"); + + // Act + var response = await client.SendAsync(request); + + //Assert + Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); + } + + // This was ported from the WebAPI 5.2 codebase. Kept the same intentionally for compatability. + [Theory] + [InlineData("GET", "api/Admin/Test", "GetUsers")] + [InlineData("GET", "api/Admin/Test/2", "GetUser")] + [InlineData("GET", "api/Admin/Test/3?name=mario", "GetUserByNameAndId")] + [InlineData("GET", "api/Admin/Test/3?name=mario&ssn=123456", "GetUserByNameIdAndSsn")] + [InlineData("GET", "api/Admin/Test?name=mario&ssn=123456", "GetUserByNameAndSsn")] + [InlineData("GET", "api/Admin/Test?name=mario&ssn=123456&age=3", "GetUserByNameAgeAndSsn")] + [InlineData("GET", "api/Admin/Test/5?random=9", "GetUser")] + [InlineData("POST", "api/Admin/Test", "PostUser")] + [InlineData("POST", "api/Admin/Test?name=mario&age=10", "PostUserByNameAndAge")] + + // Note: Normally the following would not match DeleteUserByIdAndOptName because it has 'id' and 'age' as parameters while the DeleteUserByIdAndOptName action has 'id' and 'name'. + // However, because the default value is provided on action parameter 'name', having the 'id' in the request was enough to match the action. + [InlineData("DELETE", "api/Admin/Test/6?age=10", "DeleteUserByIdAndOptName")] + [InlineData("DELETE", "api/Admin/Test", "DeleteUserByOptName")] + [InlineData("DELETE", "api/Admin/Test?name=user", "DeleteUserByOptName")] + [InlineData("DELETE", "api/Admin/Test/6?email=user@test.com", "DeleteUserById_Email_OptName_OptPhone")] + [InlineData("DELETE", "api/Admin/Test/6?email=user@test.com&name=user", "DeleteUserById_Email_OptName_OptPhone")] + [InlineData("DELETE", "api/Admin/Test/6?email=user@test.com&name=user&phone=123456789", "DeleteUserById_Email_OptName_OptPhone")] + [InlineData("DELETE", "api/Admin/Test/6?email=user@test.com&height=1.8", "DeleteUserById_Email_Height_OptName_OptPhone")] + [InlineData("DELETE", "api/Admin/Test/6?email=user@test.com&height=1.8&name=user", "DeleteUserById_Email_Height_OptName_OptPhone")] + [InlineData("DELETE", "api/Admin/Test/6?email=user@test.com&height=1.8&name=user&phone=12345678", "DeleteUserById_Email_Height_OptName_OptPhone")] + [InlineData("HEAD", "api/Admin/Test/6", "Head_Id_OptSize_OptIndex")] + [InlineData("HEAD", "api/Admin/Test/6?size=2", "Head_Id_OptSize_OptIndex")] + [InlineData("HEAD", "api/Admin/Test/6?index=2", "Head_Id_OptSize_OptIndex")] + [InlineData("HEAD", "api/Admin/Test/6?index=2&size=10", "Head_Id_OptSize_OptIndex")] + [InlineData("HEAD", "api/Admin/Test/6?index=2&otherParameter=10", "Head_Id_OptSize_OptIndex")] + [InlineData("HEAD", "api/Admin/Test/6?otherQueryParameter=1234", "Head_Id_OptSize_OptIndex")] + [InlineData("HEAD", "api/Admin/Test", "Head")] + [InlineData("HEAD", "api/Admin/Test?otherParam=2", "Head")] + [InlineData("HEAD", "api/Admin/Test?index=2&size=10", "Head")] + public async Task LegacyActionSelection_OverloadedAction_WithUnnamedAction(string httpMethod, string requestUrl, string expectedActionName) + { + // Arrange + var server = TestServer.Create(_services, _app); + var client = server.CreateClient(); + + var request = new HttpRequestMessage(new HttpMethod(httpMethod), "http://localhost/" + requestUrl); + + // Act + var response = await client.SendAsync(request); + var body = await response.Content.ReadAsStringAsync(); + + var result = JsonConvert.DeserializeObject(body); + + //Assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.Equal(expectedActionName, result.ActionName); + } + + [Theory] + [InlineData("GET", "api/Store/Test", "GetUsers")] + [InlineData("GET", "api/Store/Test/2", "GetUsersByName")] + [InlineData("GET", "api/Store/Test/luigi?ssn=123456", "GetUserByNameAndSsn")] + [InlineData("GET", "api/Store/Test/luigi?ssn=123456&id=2&ssn=12345", "GetUserByNameIdAndSsn")] + [InlineData("GET", "api/Store/Test?age=10&ssn=123456", "GetUsers")] + [InlineData("GET", "api/Store/Test?id=3&ssn=123456&name=luigi", "GetUserByNameIdAndSsn")] + [InlineData("POST", "api/Store/Test/luigi?age=20", "PostUserByNameAndAge")] + public async Task LegacyActionSelection_OverloadedAction_NonIdRouteParameter(string httpMethod, string requestUrl, string expectedActionName) + { + // Arrange + var server = TestServer.Create(_services, _app); + var client = server.CreateClient(); + + var request = new HttpRequestMessage(new HttpMethod(httpMethod), "http://localhost/" + requestUrl); + + // Act + var response = await client.SendAsync(request); + var body = await response.Content.ReadAsStringAsync(); + + var result = JsonConvert.DeserializeObject(body); + + //Assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.Equal(expectedActionName, result.ActionName); + } + + [Theory] + [InlineData("GET", "api/Admin/Test/3?NAME=mario", "GetUserByNameAndId")] + [InlineData("GET", "api/Admin/Test/3?name=mario&SSN=123456", "GetUserByNameIdAndSsn")] + [InlineData("GET", "api/Admin/Test?nAmE=mario&ssn=123456&AgE=3", "GetUserByNameAgeAndSsn")] + [InlineData("DELETE", "api/Admin/Test/6?AGe=10", "DeleteUserByIdAndOptName")] + public async Task LegacyActionSelection_OverloadedAction_Parameter_Casing(string httpMethod, string requestUrl, string expectedActionName) + { + // Arrange + var server = TestServer.Create(_services, _app); + var client = server.CreateClient(); + + var request = new HttpRequestMessage(new HttpMethod(httpMethod), "http://localhost/" + requestUrl); + + // Act + var response = await client.SendAsync(request); + var body = await response.Content.ReadAsStringAsync(); + + var result = JsonConvert.DeserializeObject(body); + + //Assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.Equal(expectedActionName, result.ActionName); + } + + [Theory] + [InlineData("GET", "api/Blog/Test/GetUsers", "GetUsers")] + [InlineData("GET", "api/Blog/Test/GetUser/7", "GetUser")] + [InlineData("GET", "api/Blog/Test/GetUser?id=3", "GetUser")] + [InlineData("GET", "api/Blog/Test/GetUser/4?id=3", "GetUser")] + [InlineData("GET", "api/Blog/Test/GetUserByNameAgeAndSsn?name=user&age=90&ssn=123456789", "GetUserByNameAgeAndSsn")] + [InlineData("GET", "api/Blog/Test/GetUserByNameAndSsn?name=user&ssn=123456789", "GetUserByNameAndSsn")] + [InlineData("POST", "api/Blog/Test/PostUserByNameAndAddress?name=user", "PostUserByNameAndAddress")] + public async Task LegacyActionSelection_RouteWithActionName(string httpMethod, string requestUrl, string expectedActionName) + { + // Arrange + var server = TestServer.Create(_services, _app); + var client = server.CreateClient(); + + var request = new HttpRequestMessage(new HttpMethod(httpMethod), "http://localhost/" + requestUrl); + + // Act + var response = await client.SendAsync(request); + var body = await response.Content.ReadAsStringAsync(); + + var result = JsonConvert.DeserializeObject(body); + + //Assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.Equal(expectedActionName, result.ActionName); + } + + [Theory] + [InlineData("GET", "api/Blog/Test/getusers", "GetUsers")] + [InlineData("GET", "api/Blog/Test/getuseR/1", "GetUser")] + [InlineData("GET", "api/Blog/Test/Getuser?iD=3", "GetUser")] + [InlineData("GET", "api/Blog/Test/GetUser/4?Id=3", "GetUser")] + [InlineData("GET", "api/Blog/Test/GetUserByNameAgeandSsn?name=user&age=90&ssn=123456789", "GetUserByNameAgeAndSsn")] + [InlineData("GET", "api/Blog/Test/getUserByNameAndSsn?name=user&ssn=123456789", "GetUserByNameAndSsn")] + [InlineData("POST", "api/Blog/Test/PostUserByNameAndAddress?name=user", "PostUserByNameAndAddress")] + public async Task LegacyActionSelection_RouteWithActionName_Casing(string httpMethod, string requestUrl, string expectedActionName) + { + // Arrange + var server = TestServer.Create(_services, _app); + var client = server.CreateClient(); + + var request = new HttpRequestMessage(new HttpMethod(httpMethod), "http://localhost/" + requestUrl); + + // Act + var response = await client.SendAsync(request); + var body = await response.Content.ReadAsStringAsync(); + + var result = JsonConvert.DeserializeObject(body); + + //Assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.Equal(expectedActionName, result.ActionName); + } + + [Theory] + [InlineData("GET", "api/Admin/Test", "GetUsers")] + [InlineData("GET", "api/Admin/Test/?name=peach", "GetUsersByName")] + [InlineData("GET", "api/Admin/Test?name=peach", "GetUsersByName")] + [InlineData("GET", "api/Admin/Test?name=peach&ssn=123456", "GetUserByNameAndSsn")] + [InlineData("GET", "api/Admin/Test?name=peach&ssn=123456&age=3", "GetUserByNameAgeAndSsn")] + public async Task LegacyActionSelection_RouteWithoutActionName(string httpMethod, string requestUrl, string expectedActionName) + { + // Arrange + var server = TestServer.Create(_services, _app); + var client = server.CreateClient(); + + var request = new HttpRequestMessage(new HttpMethod(httpMethod), "http://localhost/" + requestUrl); + + // Act + var response = await client.SendAsync(request); + var body = await response.Content.ReadAsStringAsync(); + + var result = JsonConvert.DeserializeObject(body); + + //Assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.Equal(expectedActionName, result.ActionName); + } + + + [Theory] + [InlineData("GET", "api/Admin/ParameterAttribute/2", "GetUser")] + [InlineData("GET", "api/Admin/ParameterAttribute?id=2", "GetUser")] + [InlineData("GET", "api/Admin/ParameterAttribute?myId=2", "GetUserByMyId")] + [InlineData("POST", "api/Admin/ParameterAttribute/3?name=user", "PostUserNameFromUri")] + [InlineData("POST", "api/Admin/ParameterAttribute/3", "PostUserNameFromBody")] + [InlineData("DELETE", "api/Admin/ParameterAttribute/3?name=user", "DeleteUserWithNullableIdAndName")] + [InlineData("DELETE", "api/Admin/ParameterAttribute?address=userStreet", "DeleteUser")] + public async Task LegacyActionSelection_ModelBindingParameterAttribute_AreAppliedWhenSelectingActions(string httpMethod, string requestUrl, string expectedActionName) + { + // Arrange + var server = TestServer.Create(_services, _app); + var client = server.CreateClient(); + + var request = new HttpRequestMessage(new HttpMethod(httpMethod), "http://localhost/" + requestUrl); + + // Act + var response = await client.SendAsync(request); + var body = await response.Content.ReadAsStringAsync(); + + var result = JsonConvert.DeserializeObject(body); + + //Assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.Equal(expectedActionName, result.ActionName); + } + + [Theory] + [InlineData("GET", "api/Support/notActionParameterValue1/Test", "GetUsers")] + [InlineData("GET", "api/Support/notActionParameterValue2/Test/2", "GetUser")] + [InlineData("GET", "api/Support/notActionParameterValue1/Test?randomQueryVariable=val1", "GetUsers")] + [InlineData("GET", "api/Support/notActionParameterValue2/Test/2?randomQueryVariable=val2", "GetUser")] + public async Task LegacyActionSelection_ActionsThatHaveSubsetOfRouteParameters_AreConsideredForSelection(string httpMethod, string requestUrl, string expectedActionName) + { + // Arrange + var server = TestServer.Create(_services, _app); + var client = server.CreateClient(); + + var request = new HttpRequestMessage(new HttpMethod(httpMethod), "http://localhost/" + requestUrl); + + // Act + var response = await client.SendAsync(request); + var body = await response.Content.ReadAsStringAsync(); + + var result = JsonConvert.DeserializeObject(body); + + //Assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.Equal(expectedActionName, result.ActionName); + } + + // This would result in ambiguous match because complex parameter is not considered for matching. + // Therefore, PostUserByNameAndAddress(string name, Address address) would conflicts with PostUserByName(string name) + [Fact] + public async Task LegacyActionSelection_RequestToAmbiguousAction_OnDefaultRoute() + { + // Arrange + var server = TestServer.Create(_services, _app); + var client = server.CreateClient(); + + var request = new HttpRequestMessage(new HttpMethod("POST"), "http://localhost/api/Admin/Test?name=mario"); + + // Act & Assert + await Assert.ThrowsAsync(async () => await client.SendAsync(request)); + } + + [Theory] + [InlineData("GET", "api/Admin/EnumParameterOverloads", "Get")] + [InlineData("GET", "api/Admin/EnumParameterOverloads?scope=global", "GetWithEnumParameter")] + [InlineData("GET", "api/Admin/EnumParameterOverloads?level=off&kind=trace", "GetWithTwoEnumParameters")] + [InlineData("GET", "api/Admin/EnumParameterOverloads?level=", "GetWithNullableEnumParameter")] + public async Task LegacyActionSelection_SelectAction_ReturnsActionDescriptor_ForEnumParameterOverloads(string httpMethod, string requestUrl, string expectedActionName) + { + // Arrange + var server = TestServer.Create(_services, _app); + var client = server.CreateClient(); + + var request = new HttpRequestMessage(new HttpMethod(httpMethod), "http://localhost/" + requestUrl); + + // Act + var response = await client.SendAsync(request); + var body = await response.Content.ReadAsStringAsync(); + + var result = JsonConvert.DeserializeObject(body); + + //Assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.Equal(expectedActionName, result.ActionName); + } + + // Verify response has all the methods in its Allow header. values are unsorted. + private void AssertAllowedHeaders(HttpResponseMessage response, params HttpMethod[] allowedMethods) + { + foreach (var method in allowedMethods) + { + Assert.Contains(method.ToString(), response.Content.Headers.Allow); + } + Assert.Equal(allowedMethods.Length, response.Content.Headers.Allow.Count); + } + + private class ActionSelectionResult + { + public string ActionName { get; set; } + + public string ControllerName { get; set; } + } + } +} + #endif \ No newline at end of file diff --git a/test/Microsoft.AspNet.Mvc.FunctionalTests/WebApiCompatShimBasicTest.cs b/test/Microsoft.AspNet.Mvc.FunctionalTests/WebApiCompatShimBasicTest.cs index 9069278dd4..c15ebb13f7 100644 --- a/test/Microsoft.AspNet.Mvc.FunctionalTests/WebApiCompatShimBasicTest.cs +++ b/test/Microsoft.AspNet.Mvc.FunctionalTests/WebApiCompatShimBasicTest.cs @@ -26,7 +26,7 @@ public async Task ApiController_Activates_HttpContextAndUser() var client = server.CreateClient(); // Act - var response = await client.GetAsync("http://localhost/BasicApi/WriteToHttpContext"); + var response = await client.GetAsync("http://localhost/api/Blog/BasicApi/WriteToHttpContext"); var content = await response.Content.ReadAsStringAsync(); // Assert @@ -44,13 +44,13 @@ public async Task ApiController_Activates_UrlHelper() var client = server.CreateClient(); // Act - var response = await client.GetAsync("http://localhost/BasicApi/GenerateUrl"); + var response = await client.GetAsync("http://localhost/api/Blog/BasicApi/GenerateUrl"); var content = await response.Content.ReadAsStringAsync(); // Assert Assert.Equal(HttpStatusCode.OK, response.StatusCode); Assert.Equal( - "Visited: /BasicApi/GenerateUrl", + "Visited: /api/Blog/BasicApi/GenerateUrl", content); } @@ -69,7 +69,7 @@ public async Task Options_SetsDefaultFormatters() }; // Act - var response = await client.GetAsync("http://localhost/BasicApi/GetFormatters"); + var response = await client.GetAsync("http://localhost/api/Blog/BasicApi/GetFormatters"); var content = await response.Content.ReadAsStringAsync(); var formatters = JsonConvert.DeserializeObject(content); diff --git a/test/Microsoft.AspNet.Mvc.WebApiCompatShimTest/ApiControllerActionDiscoveryTest.cs b/test/Microsoft.AspNet.Mvc.WebApiCompatShimTest/ApiControllerActionDiscoveryTest.cs index f6759bba9c..73a7f9b17c 100644 --- a/test/Microsoft.AspNet.Mvc.WebApiCompatShimTest/ApiControllerActionDiscoveryTest.cs +++ b/test/Microsoft.AspNet.Mvc.WebApiCompatShimTest/ApiControllerActionDiscoveryTest.cs @@ -6,6 +6,7 @@ using System.Reflection; using Microsoft.AspNet.Mvc; using Microsoft.AspNet.Mvc.Filters; +using Microsoft.AspNet.Mvc.WebApiCompatShim; using Microsoft.Framework.DependencyInjection; using Microsoft.Framework.DependencyInjection.NestedProviders; using Microsoft.Framework.OptionsModel; @@ -16,8 +17,6 @@ namespace System.Web.Http { public class ApiControllerActionDiscoveryTest { - // For now we just want to verify that an ApiController is-a controller and produces - // actions. When we implement the conventions for action discovery, this test will be revised. [Fact] public void GetActions_ApiControllerWithControllerSuffix_IsController() { @@ -32,9 +31,9 @@ public void GetActions_ApiControllerWithControllerSuffix_IsController() // Assert var controllerType = typeof(TestControllers.ProductsController).GetTypeInfo(); - var filtered = results.Where(ad => ad.ControllerDescriptor.ControllerTypeInfo == controllerType).ToArray(); + var actions = results.Where(ad => ad.ControllerDescriptor.ControllerTypeInfo == controllerType).ToArray(); - Assert.Equal(3, filtered.Length); + Assert.NotEmpty(actions); } [Fact] @@ -51,9 +50,179 @@ public void GetActions_ApiControllerWithoutControllerSuffix_IsNotController() // Assert var controllerType = typeof(TestControllers.Blog).GetTypeInfo(); - var filtered = results.Where(ad => ad.ControllerDescriptor.ControllerTypeInfo == controllerType).ToArray(); + var actions = results.Where(ad => ad.ControllerDescriptor.ControllerTypeInfo == controllerType).ToArray(); - Assert.Empty(filtered); + Assert.Empty(actions); + } + + [Fact] + public void GetActions_CreatesNamedAndUnnamedAction() + { + // Arrange + var provider = CreateProvider(); + + // Act + var context = new ActionDescriptorProviderContext(); + provider.Invoke(context); + + var results = context.Results.Cast(); + + // Assert + var controllerType = typeof(TestControllers.StoreController).GetTypeInfo(); + var actions = results + .Where(ad => ad.ControllerDescriptor.ControllerTypeInfo == controllerType) + .Where(ad => ad.MethodInfo.Name == "GetAll") + .ToArray(); + + Assert.Equal(2, actions.Length); + + var action = Assert.Single( + actions, + a => a.RouteConstraints.Any(rc => rc.RouteKey == "action" && rc.RouteValue == "GetAll")); + Assert.Equal( + new string[] { "GET" }, + Assert.Single(action.ActionConstraints.OfType()).HttpMethods); + + action = Assert.Single( + actions, + a => a.RouteConstraints.Any(rc => rc.RouteKey == "action" && rc.RouteValue == "")); + Assert.Equal( + new string[] { "GET" }, + Assert.Single(action.ActionConstraints.OfType()).HttpMethods); + } + + [Fact] + public void GetActions_CreatesNamedAndUnnamedAction_DefaultVerbIsPost() + { + // Arrange + var provider = CreateProvider(); + + // Act + var context = new ActionDescriptorProviderContext(); + provider.Invoke(context); + + var results = context.Results.Cast(); + + // Assert + var controllerType = typeof(TestControllers.StoreController).GetTypeInfo(); + var actions = results + .Where(ad => ad.ControllerDescriptor.ControllerTypeInfo == controllerType) + .Where(ad => ad.MethodInfo.Name == "Edit") + .ToArray(); + + Assert.Equal(2, actions.Length); + + var action = Assert.Single( + actions, + a => a.RouteConstraints.Any(rc => rc.RouteKey == "action" && rc.RouteValue == "Edit")); + Assert.Equal( + new string[] { "POST" }, + Assert.Single(action.ActionConstraints.OfType()).HttpMethods); + + action = Assert.Single( + actions, + a => a.RouteConstraints.Any(rc => rc.RouteKey == "action" && rc.RouteValue == "")); + Assert.Equal( + new string[] { "POST" }, + Assert.Single(action.ActionConstraints.OfType()).HttpMethods); + } + + [Fact] + public void GetActions_CreatesNamedAndUnnamedAction_RespectsVerbAttribute() + { + // Arrange + var provider = CreateProvider(); + + // Act + var context = new ActionDescriptorProviderContext(); + provider.Invoke(context); + + var results = context.Results.Cast(); + + // Assert + var controllerType = typeof(TestControllers.StoreController).GetTypeInfo(); + var actions = results + .Where(ad => ad.ControllerDescriptor.ControllerTypeInfo == controllerType) + .Where(ad => ad.MethodInfo.Name == "Delete") + .ToArray(); + + Assert.Equal(2, actions.Length); + + var action = Assert.Single( + actions, + a => a.RouteConstraints.Any(rc => rc.RouteKey == "action" && rc.RouteValue == "Delete")); + Assert.Equal( + new string[] { "PUT" }, + Assert.Single(action.ActionConstraints.OfType()).HttpMethods); + + action = Assert.Single( + actions, + a => a.RouteConstraints.Any(rc => rc.RouteKey == "action" && rc.RouteValue == "")); + Assert.Equal( + new string[] { "PUT" }, + Assert.Single(action.ActionConstraints.OfType()).HttpMethods); + } + + // The method name is used to infer a verb, not the action name + [Fact] + public void GetActions_CreatesNamedAndUnnamedAction_VerbBasedOnMethodName() + { + // Arrange + var provider = CreateProvider(); + + // Act + var context = new ActionDescriptorProviderContext(); + provider.Invoke(context); + + var results = context.Results.Cast(); + + // Assert + var controllerType = typeof(TestControllers.StoreController).GetTypeInfo(); + var actions = results + .Where(ad => ad.ControllerDescriptor.ControllerTypeInfo == controllerType) + .Where(ad => ad.MethodInfo.Name == "Options") + .ToArray(); + + Assert.Equal(2, actions.Length); + + var action = Assert.Single( + actions, + a => a.RouteConstraints.Any(rc => rc.RouteKey == "action" && rc.RouteValue == "GetOptions")); + Assert.Equal( + new string[] { "OPTIONS" }, + Assert.Single(action.ActionConstraints.OfType()).HttpMethods); + + action = Assert.Single( + actions, + a => a.RouteConstraints.Any(rc => rc.RouteKey == "action" && rc.RouteValue == "")); + Assert.Equal( + new string[] { "OPTIONS" }, + Assert.Single(action.ActionConstraints.OfType()).HttpMethods); + } + + [Fact] + public void GetActions_AllWebApiActionsAreOverloaded() + { + // Arrange + var provider = CreateProvider(); + + // Act + var context = new ActionDescriptorProviderContext(); + provider.Invoke(context); + + var results = context.Results.Cast(); + + // Assert + var controllerType = typeof(TestControllers.StoreController).GetTypeInfo(); + var actions = results + .Where(ad => ad.ControllerDescriptor.ControllerTypeInfo == controllerType) + .ToArray(); + + Assert.NotEmpty(actions); + foreach (var action in actions) + { + Assert.Single(action.ActionConstraints, c => c is OverloadActionConstraint); + } } private INestedProviderManager CreateProvider() @@ -70,13 +239,17 @@ private INestedProviderManager CreateProvider() var conventions = new NamespaceLimitedActionDiscoveryConventions(); + var options = new MvcOptions(); + options.ApplicationModelConventions.Add(new WebApiActionConventionsGlobalModelConvention()); + options.ApplicationModelConventions.Add(new WebApiOverloadingGlobalModelConvention()); + var optionsAccessor = new Mock>(); optionsAccessor .SetupGet(o => o.Options) - .Returns(new MvcOptions()); + .Returns(options); var provider = new ControllerActionDescriptorProvider( - assemblyProvider.Object, + assemblyProvider.Object, conventions, filterProvider.Object, optionsAccessor.Object); @@ -92,7 +265,7 @@ private class NamespaceLimitedActionDiscoveryConventions : DefaultActionDiscover { public override bool IsController(TypeInfo typeInfo) { - return + return typeInfo.Namespace == "System.Web.Http.TestControllers" && base.IsController(typeInfo); } @@ -110,8 +283,20 @@ public IActionResult GetAll() { return null; } + } + + // Not a controller, because there's no controller suffix + public class Blog : ApiController + { + public IActionResult GetBlogPosts() + { + return null; + } + } - public IActionResult Get(int id) + public class StoreController : ApiController + { + public IActionResult GetAll() { return null; } @@ -120,12 +305,15 @@ public IActionResult Edit(int id) { return null; } - } - // Not a controller, because there's no controller suffix - public class Blog : ApiController - { - public IActionResult GetBlogPosts() + [HttpPut] + public IActionResult Delete(int id) + { + return null; + } + + [ActionName("GetOptions")] + public IActionResult Options() { return null; } diff --git a/test/Microsoft.AspNet.Mvc.WebApiCompatShimTest/FormDataCollectionExtensionsTest.cs b/test/Microsoft.AspNet.Mvc.WebApiCompatShimTest/FormDataCollectionExtensionsTest.cs new file mode 100644 index 0000000000..5daf365ac8 --- /dev/null +++ b/test/Microsoft.AspNet.Mvc.WebApiCompatShimTest/FormDataCollectionExtensionsTest.cs @@ -0,0 +1,43 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information. + +using System.Linq; +using System.Net.Http.Formatting; +using Xunit; + +namespace Microsoft.AspNet.Mvc.WebApiCompatShim +{ + public class FormDataCollectionExtensionsTest + { + [Theory] + [InlineData("", null)] + [InlineData("", "")] // empty + [InlineData("x", "x")] // normal key + [InlineData("", "[]")] // trim [] + [InlineData("x", "x[]")] // trim [] + [InlineData("x[234]", "x[234]")] // array index + [InlineData("x.y", "x[y]")] // field lookup + [InlineData("x.y.z", "x[y][z]")] // nested field lookup + [InlineData("x.y[234].x", "x[y][234][x]")] // compound + public void TestNormalize(string expectedMvc, string jqueryString) + { + Assert.Equal(expectedMvc, FormDataCollectionExtensions.NormalizeJQueryToMvc(jqueryString)); + } + + [Fact] + public void TestGetJQueryNameValuePairs() + { + // Arrange + var formData = new FormDataCollection("x.y=30&x[y]=70&x[z][20]=cool"); + + // Act + var actual = FormDataCollectionExtensions.GetJQueryNameValuePairs(formData).ToArray(); + + // Assert + var arraySetter = Assert.Single(actual, kvp => kvp.Key == "x.z[20]"); + Assert.Equal("cool", arraySetter.Value); + + Assert.Single(actual, kvp => kvp.Key == "x.y" && kvp.Value == "30"); + Assert.Single(actual, kvp => kvp.Key == "x.y" && kvp.Value == "70"); + } + } +} diff --git a/test/Microsoft.AspNet.Mvc.WebApiCompatShimTest/OverloadActionConstraintTest.cs b/test/Microsoft.AspNet.Mvc.WebApiCompatShimTest/OverloadActionConstraintTest.cs new file mode 100644 index 0000000000..8069650d9a --- /dev/null +++ b/test/Microsoft.AspNet.Mvc.WebApiCompatShimTest/OverloadActionConstraintTest.cs @@ -0,0 +1,444 @@ +// 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.Collections.Generic; +using Microsoft.AspNet.Http; +using Microsoft.AspNet.PipelineCore; +using Microsoft.AspNet.Routing; +using Xunit; + +namespace Microsoft.AspNet.Mvc.WebApiCompatShim +{ + public class OverloadActionConstraintTest + { + [Fact] + public void Accept_RejectsActionMatchWithMissingParameter() + { + // Arrange + var action = new ActionDescriptor(); + action.Parameters = new List() + { + new ParameterDescriptor() + { + Name = "id", + ParameterBindingInfo = new ParameterBindingInfo("id", typeof(int)), + }, + }; + + var constraint = new OverloadActionConstraint(); + + var context = new ActionConstraintContext(); + context.Candidates = new List() + { + new ActionSelectorCandidate(action, new [] { constraint }), + }; + + context.CurrentCandidate = context.Candidates[0]; + context.RouteContext = CreateRouteContext(); + + // Act & Assert + Assert.False(constraint.Accept(context)); + } + + [Fact] + public void Accept_AcceptsActionWithSatisfiedParameters() + { + // Arrange + var action = new ActionDescriptor(); + action.Parameters = new List() + { + new ParameterDescriptor() + { + Name = "id", + ParameterBindingInfo = new ParameterBindingInfo("id", typeof(int)), + }, + new ParameterDescriptor() + { + Name = "quantity", + ParameterBindingInfo = new ParameterBindingInfo("quantity", typeof(int)), + }, + }; + + var constraint = new OverloadActionConstraint(); + + var context = new ActionConstraintContext(); + context.Candidates = new List() + { + new ActionSelectorCandidate(action, new [] { constraint }), + }; + + context.CurrentCandidate = context.Candidates[0]; + context.RouteContext = CreateRouteContext("?quantity=5", new { id = 17}); + + // Act & Assert + Assert.True(constraint.Accept(context)); + } + + [Fact] + public void Accept_AcceptsActionWithSatisfiedParameters_QueryStringOnly() + { + // Arrange + var action = new ActionDescriptor(); + action.Parameters = new List() + { + new ParameterDescriptor() + { + Name = "id", + ParameterBindingInfo = new ParameterBindingInfo("id", typeof(int)), + }, + new ParameterDescriptor() + { + Name = "quantity", + ParameterBindingInfo = new ParameterBindingInfo("quantity", typeof(int)), + }, + }; + + var constraint = new OverloadActionConstraint(); + + var context = new ActionConstraintContext(); + context.Candidates = new List() + { + new ActionSelectorCandidate(action, new [] { constraint }), + }; + + context.CurrentCandidate = context.Candidates[0]; + context.RouteContext = CreateRouteContext("?quantity=5&id=7", new { }); + + // Act & Assert + Assert.True(constraint.Accept(context)); + } + + [Fact] + public void Accept_AcceptsActionWithSatisfiedParameters_RouteDataOnly() + { + // Arrange + var action = new ActionDescriptor(); + action.Parameters = new List() + { + new ParameterDescriptor() + { + Name = "id", + ParameterBindingInfo = new ParameterBindingInfo("id", typeof(int)), + }, + new ParameterDescriptor() + { + Name = "quantity", + ParameterBindingInfo = new ParameterBindingInfo("quantity", typeof(int)), + }, + }; + + var constraint = new OverloadActionConstraint(); + + var context = new ActionConstraintContext(); + context.Candidates = new List() + { + new ActionSelectorCandidate(action, new [] { constraint }), + }; + + context.CurrentCandidate = context.Candidates[0]; + context.RouteContext = CreateRouteContext("?", new { quantity = 9, id = 17 }); + + // Act & Assert + Assert.True(constraint.Accept(context)); + } + + [Fact] + public void Accept_AcceptsActionWithUnsatisfiedOptionalParameter() + { + // Arrange + var action = new ActionDescriptor(); + action.Parameters = new List() + { + new ParameterDescriptor() + { + Name = "id", + ParameterBindingInfo = new ParameterBindingInfo("id", typeof(int)), + }, + new ParameterDescriptor() + { + Name = "quantity", + IsOptional = true, + ParameterBindingInfo = new ParameterBindingInfo("quantity", typeof(int)), + }, + }; + + var constraint = new OverloadActionConstraint(); + + var context = new ActionConstraintContext(); + context.Candidates = new List() + { + new ActionSelectorCandidate(action, new [] { constraint }), + }; + + context.CurrentCandidate = context.Candidates[0]; + context.RouteContext = CreateRouteContext("?store=5", new { id = 17 }); + + // Act & Assert + Assert.True(constraint.Accept(context)); + } + + [Fact] + public void Accept_AcceptsOneAndRejectsAnother() + { + // Arrange + var action1 = new ActionDescriptor(); + action1.Parameters = new List() + { + new ParameterDescriptor() + { + Name = "id", + ParameterBindingInfo = new ParameterBindingInfo("id", typeof(int)), + }, + new ParameterDescriptor() + { + Name = "quantity", + ParameterBindingInfo = new ParameterBindingInfo("quantity", typeof(int)), + }, + }; + + var action2 = new ActionDescriptor(); + action2.Parameters = new List() + { + new ParameterDescriptor() + { + Name = "id", + ParameterBindingInfo = new ParameterBindingInfo("id", typeof(int)), + }, + new ParameterDescriptor() + { + Name = "quantity_ordered", + ParameterBindingInfo = new ParameterBindingInfo("quantity_ordered", typeof(int)), + }, + }; + + var constraint = new OverloadActionConstraint(); + + var context = new ActionConstraintContext(); + context.Candidates = new List() + { + new ActionSelectorCandidate(action1, new [] { constraint }), + new ActionSelectorCandidate(action2, new [] { constraint }), + }; + + context.CurrentCandidate = context.Candidates[0]; + context.RouteContext = CreateRouteContext("?quantity=5", new { id = 17 }); + + // Act & Assert + Assert.True(constraint.Accept(context)); + + context.CurrentCandidate = context.Candidates[1]; + Assert.False(constraint.Accept(context)); + } + + [Fact] + public void Accept_RejectsWorseMatch() + { + // Arrange + var action1 = new ActionDescriptor(); + action1.Parameters = new List() + { + new ParameterDescriptor() + { + Name = "id", + ParameterBindingInfo = new ParameterBindingInfo("id", typeof(int)), + }, + }; + + var action2 = new ActionDescriptor(); + action2.Parameters = new List() + { + new ParameterDescriptor() + { + Name = "id", + ParameterBindingInfo = new ParameterBindingInfo("id", typeof(int)), + }, + new ParameterDescriptor() + { + Name = "quantity", + ParameterBindingInfo = new ParameterBindingInfo("quantity", typeof(int)), + }, + }; + + var constraint = new OverloadActionConstraint(); + + var context = new ActionConstraintContext(); + context.Candidates = new List() + { + new ActionSelectorCandidate(action1, new [] { constraint }), + new ActionSelectorCandidate(action2, new [] { constraint }), + }; + + context.CurrentCandidate = context.Candidates[0]; + context.RouteContext = CreateRouteContext("?quantity=5", new { id = 17 }); + + // Act & Assert + Assert.False(constraint.Accept(context)); + } + + [Fact] + public void Accept_RejectsWorseMatch_OptionalParameter() + { + // Arrange + var action1 = new ActionDescriptor(); + action1.Parameters = new List() + { + new ParameterDescriptor() + { + Name = "id", + ParameterBindingInfo = new ParameterBindingInfo("id", typeof(int)), + }, + new ParameterDescriptor() + { + Name = "quantity", + IsOptional = true, + ParameterBindingInfo = new ParameterBindingInfo("quantity", typeof(int)), + }, + }; + + var action2 = new ActionDescriptor(); + action2.Parameters = new List() + { + new ParameterDescriptor() + { + Name = "id", + ParameterBindingInfo = new ParameterBindingInfo("id", typeof(int)), + }, + new ParameterDescriptor() + { + Name = "quantity", + ParameterBindingInfo = new ParameterBindingInfo("quantity", typeof(int)), + }, + }; + + var constraint = new OverloadActionConstraint(); + + var context = new ActionConstraintContext(); + context.Candidates = new List() + { + new ActionSelectorCandidate(action1, new [] { constraint }), + new ActionSelectorCandidate(action2, new [] { constraint }), + }; + + context.CurrentCandidate = context.Candidates[0]; + context.RouteContext = CreateRouteContext("?quantity=5", new { id = 17 }); + + // Act & Assert + Assert.False(constraint.Accept(context)); + } + + [Fact] + public void Accept_AcceptsActionsOnSameTier() + { + // Arrange + var action1 = new ActionDescriptor(); + action1.Parameters = new List() + { + new ParameterDescriptor() + { + Name = "id", + ParameterBindingInfo = new ParameterBindingInfo("id", typeof(int)), + }, + new ParameterDescriptor() + { + Name = "quantity", + ParameterBindingInfo = new ParameterBindingInfo("quantity", typeof(int)), + }, + }; + + var action2 = new ActionDescriptor(); + action2.Parameters = new List() + { + new ParameterDescriptor() + { + Name = "id", + ParameterBindingInfo = new ParameterBindingInfo("id", typeof(int)), + }, + new ParameterDescriptor() + { + Name = "price", + ParameterBindingInfo = new ParameterBindingInfo("price", typeof(decimal)), + }, + }; + + var constraint = new OverloadActionConstraint(); + + var context = new ActionConstraintContext(); + context.Candidates = new List() + { + new ActionSelectorCandidate(action1, new [] { constraint }), + new ActionSelectorCandidate(action2, new [] { constraint }), + }; + + context.CurrentCandidate = context.Candidates[0]; + context.RouteContext = CreateRouteContext("?quantity=5&price=5.99", new { id = 17 }); + + // Act & Assert + Assert.True(constraint.Accept(context)); + + context.CurrentCandidate = context.Candidates[1]; + Assert.True(constraint.Accept(context)); + } + + [Fact] + public void Accept_AcceptsAction_WithFewerParameters_WhenOtherIsNotOverloaded() + { + // Arrange + var action1 = new ActionDescriptor(); + action1.Parameters = new List() + { + new ParameterDescriptor() + { + Name = "id", + ParameterBindingInfo = new ParameterBindingInfo("id", typeof(int)), + }, + }; + + var action2 = new ActionDescriptor(); + action2.Parameters = new List() + { + new ParameterDescriptor() + { + Name = "id", + ParameterBindingInfo = new ParameterBindingInfo("id", typeof(int)), + }, + new ParameterDescriptor() + { + Name = "quantity", + ParameterBindingInfo = new ParameterBindingInfo("quantity", typeof(int)), + }, + }; + + var constraint = new OverloadActionConstraint(); + + var context = new ActionConstraintContext(); + context.Candidates = new List() + { + new ActionSelectorCandidate(action1, new [] { constraint }), + new ActionSelectorCandidate(action2, new IActionConstraint[] { }), + }; + + context.CurrentCandidate = context.Candidates[0]; + context.RouteContext = CreateRouteContext("?quantity=5", new { id = 17 }); + + // Act & Assert + Assert.True(constraint.Accept(context)); + } + + private static RouteContext CreateRouteContext(string queryString = null, object routeValues = null) + { + var httpContext = new DefaultHttpContext(); + if (queryString != null) + { + httpContext.Request.QueryString = new QueryString(queryString); + } + + var routeContext = new RouteContext(httpContext); + routeContext.RouteData = new RouteData() + { + Values = new RouteValueDictionary(routeValues), + }; + + return routeContext; + } + } +} \ No newline at end of file diff --git a/test/WebSites/WebApiCompatShimWebSite/ActionSelectionFilter.cs b/test/WebSites/WebApiCompatShimWebSite/ActionSelectionFilter.cs new file mode 100644 index 0000000000..875794108b --- /dev/null +++ b/test/WebSites/WebApiCompatShimWebSite/ActionSelectionFilter.cs @@ -0,0 +1,20 @@ +// 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 WebApiCompatShimWebSite +{ + public class ActionSelectionFilterAttribute : ActionFilterAttribute + { + public override void OnActionExecuted(ActionExecutedContext context) + { + var action = (ControllerActionDescriptor)context.ActionDescriptor; + context.Result = new JsonResult(new + { + ActionName = action.Name, + ControllerName = action.ControllerName + }); + } + } +} \ No newline at end of file diff --git a/test/WebSites/WebApiCompatShimWebSite/Controllers/ActionSelection/Legacy/EnumParameterOverloadsController.cs b/test/WebSites/WebApiCompatShimWebSite/Controllers/ActionSelection/Legacy/EnumParameterOverloadsController.cs new file mode 100644 index 0000000000..8ffcc07054 --- /dev/null +++ b/test/WebSites/WebApiCompatShimWebSite/Controllers/ActionSelection/Legacy/EnumParameterOverloadsController.cs @@ -0,0 +1,35 @@ +// 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.Collections.Generic; +using System.Diagnostics; +using System.Web.Http; +using Microsoft.AspNet.Mvc.WebApiCompatShim; + +namespace WebApiCompatShimWebSite +{ + // This was ported from the WebAPI 5.2 codebase. Kept the same intentionally for compatability. + [ActionSelectionFilter] + public class EnumParameterOverloadsController : ApiController + { + public IEnumerable Get() + { + return new string[] { "get" }; + } + + public string GetWithEnumParameter(UserKind scope) + { + return scope.ToString(); + } + + public string GetWithTwoEnumParameters([FromUri]UserKind level, UserKind kind) + { + return level.ToString() + kind.ToString(); + } + + public string GetWithNullableEnumParameter(TraceLevel? level) + { + return level.ToString(); + } + } +} \ No newline at end of file diff --git a/test/WebSites/WebApiCompatShimWebSite/Controllers/ActionSelection/Legacy/ParameterAttributeController.cs b/test/WebSites/WebApiCompatShimWebSite/Controllers/ActionSelection/Legacy/ParameterAttributeController.cs new file mode 100644 index 0000000000..22b6834a82 --- /dev/null +++ b/test/WebSites/WebApiCompatShimWebSite/Controllers/ActionSelection/Legacy/ParameterAttributeController.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 System.Collections.Generic; +using System.Web.Http; +using Microsoft.AspNet.Mvc; +using Microsoft.AspNet.Mvc.WebApiCompatShim; + +namespace WebApiCompatShimWebSite +{ + // This was ported from the WebAPI 5.2 codebase. Kept the same intentionally for compatability. + [ActionSelectionFilter] + public class ParameterAttributeController : ApiController + { + public User GetUserByMyId(int myId) { return null; } + public User GetUser([FromUri(Name = "id")] int myId) { return null; } + public List PostUserNameFromUri(int id, [FromUri]string name) { return null; } + public List PostUserNameFromBody(int id, [FromBody] string name) { return null; } + public void DeleteUserWithNullableIdAndName(int? id, string name) { } + public void DeleteUser(string address) { } + } +} \ No newline at end of file diff --git a/test/WebSites/WebApiCompatShimWebSite/Controllers/ActionSelection/Legacy/TestController.cs b/test/WebSites/WebApiCompatShimWebSite/Controllers/ActionSelection/Legacy/TestController.cs new file mode 100644 index 0000000000..bfd4de8d5e --- /dev/null +++ b/test/WebSites/WebApiCompatShimWebSite/Controllers/ActionSelection/Legacy/TestController.cs @@ -0,0 +1,39 @@ +// 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.Collections.Generic; +using System.Web.Http; +using Microsoft.AspNet.Mvc; + +namespace WebApiCompatShimWebSite +{ + // This was ported from the WebAPI 5.2 codebase. Kept the same intentionally for compatability. + [ActionSelectionFilter] + public class TestController : ApiController + { + public User GetUser(int id) { return null; } + public List GetUsers() { return null; } + + public List GetUsersByName(string name) { return null; } + + [AcceptVerbs("PATCH")] + public void PutUser(User user) { } + + public User GetUserByNameAndId(string name, int id) { return null; } + public User GetUserByNameAndAge(string name, int age) { return null; } + public User GetUserByNameAgeAndSsn(string name, int age, int ssn) { return null; } + public User GetUserByNameIdAndSsn(string name, int id, int ssn) { return null; } + public User GetUserByNameAndSsn(string name, int ssn) { return null; } + public User PostUser(User user) { return null; } + public User PostUserByNameAndAge(string name, int age) { return null; } + public User PostUserByName(string name) { return null; } + public User PostUserByNameAndAddress(string name, UserAddress address) { return null; } + public User DeleteUserByOptName(string name = null) { return null; } + public User DeleteUserByIdAndOptName(int id, string name = "DefaultName") { return null; } + public User DeleteUserByIdNameAndAge(int id, string name, int age) { return null; } + public User DeleteUserById_Email_OptName_OptPhone(int id, string email, string name = null, int phone = 0) { return null; } + public User DeleteUserById_Email_Height_OptName_OptPhone(int id, string email, double height, string name = "DefaultName", int? phone = null) { return null; } + public void Head_Id_OptSize_OptIndex(int id, int size = 10, int index = 0) { } + public void Head() { } + } +} \ No newline at end of file diff --git a/test/WebSites/WebApiCompatShimWebSite/Controllers/ActionSelection/WebAPIActionConventionsActionNameController.cs b/test/WebSites/WebApiCompatShimWebSite/Controllers/ActionSelection/WebAPIActionConventionsActionNameController.cs new file mode 100644 index 0000000000..78c5eed691 --- /dev/null +++ b/test/WebSites/WebApiCompatShimWebSite/Controllers/ActionSelection/WebAPIActionConventionsActionNameController.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 System.Web.Http; +using Microsoft.AspNet.Mvc; + +namespace WebApiCompatShimWebSite +{ + // The verb is still inferred by the METHOD NAME not the action name. + [ActionSelectionFilter] + public class WebAPIActionConventionsActionNameController : ApiController + { + [ActionName("GetItems")] + public void PostItems() + { + } + } +} \ No newline at end of file diff --git a/test/WebSites/WebApiCompatShimWebSite/Controllers/ActionSelection/WebAPIActionConventionsController.cs b/test/WebSites/WebApiCompatShimWebSite/Controllers/ActionSelection/WebAPIActionConventionsController.cs new file mode 100644 index 0000000000..b45a24e36b --- /dev/null +++ b/test/WebSites/WebApiCompatShimWebSite/Controllers/ActionSelection/WebAPIActionConventionsController.cs @@ -0,0 +1,41 @@ +// 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.Web.Http; + +namespace WebApiCompatShimWebSite +{ + // Each of these is mapped to an unnamed action with the corresponding http verb, and also + // a named action with the corresponding http verb. + [ActionSelectionFilter] + public class WebAPIActionConventionsController : ApiController + { + public void GetItems() + { + } + + public void PostItems() + { + } + + public void PutItems() + { + } + + public void DeleteItems() + { + } + + public void PatchItems() + { + } + + public void HeadItems() + { + } + + public void OptionsItems() + { + } + } +} \ No newline at end of file diff --git a/test/WebSites/WebApiCompatShimWebSite/Controllers/ActionSelection/WebAPIActionConventionsDefaultPostController.cs b/test/WebSites/WebApiCompatShimWebSite/Controllers/ActionSelection/WebAPIActionConventionsDefaultPostController.cs new file mode 100644 index 0000000000..9f98c1c077 --- /dev/null +++ b/test/WebSites/WebApiCompatShimWebSite/Controllers/ActionSelection/WebAPIActionConventionsDefaultPostController.cs @@ -0,0 +1,16 @@ +// 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.Web.Http; + +namespace WebApiCompatShimWebSite +{ + // This action only accepts POST by default + [ActionSelectionFilter] + public class WebAPIActionConventionsDefaultPostController : ApiController + { + public void DefaultVerbIsPost() + { + } + } +} \ No newline at end of file diff --git a/test/WebSites/WebApiCompatShimWebSite/Controllers/ActionSelection/WebAPIActionConventionsVerbOverrideController.cs b/test/WebSites/WebApiCompatShimWebSite/Controllers/ActionSelection/WebAPIActionConventionsVerbOverrideController.cs new file mode 100644 index 0000000000..563595e433 --- /dev/null +++ b/test/WebSites/WebApiCompatShimWebSite/Controllers/ActionSelection/WebAPIActionConventionsVerbOverrideController.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 System.Web.Http; +using Microsoft.AspNet.Mvc; + +namespace WebApiCompatShimWebSite +{ + // The verb is overridden by the attribute + [ActionSelectionFilter] + public class WebAPIActionConventionsVerbOverrideController : ApiController + { + [HttpGet] + public void PostItems() + { + } + } +} \ No newline at end of file diff --git a/test/WebSites/WebApiCompatShimWebSite/Models/User.cs b/test/WebSites/WebApiCompatShimWebSite/Models/User.cs new file mode 100644 index 0000000000..50df5ddc90 --- /dev/null +++ b/test/WebSites/WebApiCompatShimWebSite/Models/User.cs @@ -0,0 +1,11 @@ +// 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; + +namespace WebApiCompatShimWebSite +{ + public class User + { + } +} \ No newline at end of file diff --git a/test/WebSites/WebApiCompatShimWebSite/Models/UserAddress.cs b/test/WebSites/WebApiCompatShimWebSite/Models/UserAddress.cs new file mode 100644 index 0000000000..6c79945ee2 --- /dev/null +++ b/test/WebSites/WebApiCompatShimWebSite/Models/UserAddress.cs @@ -0,0 +1,11 @@ +// 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; + +namespace WebApiCompatShimWebSite +{ + public class UserAddress + { + } +} \ No newline at end of file diff --git a/test/WebSites/WebApiCompatShimWebSite/Models/UserKind.cs b/test/WebSites/WebApiCompatShimWebSite/Models/UserKind.cs new file mode 100644 index 0000000000..2b96be4a19 --- /dev/null +++ b/test/WebSites/WebApiCompatShimWebSite/Models/UserKind.cs @@ -0,0 +1,12 @@ +// 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 WebApiCompatShimWebSite +{ + public enum UserKind + { + Normal, + Admin, + SuperAdmin, + } +} \ No newline at end of file diff --git a/test/WebSites/WebApiCompatShimWebSite/Startup.cs b/test/WebSites/WebApiCompatShimWebSite/Startup.cs index daa7f3fd7f..c4dcf3d447 100644 --- a/test/WebSites/WebApiCompatShimWebSite/Startup.cs +++ b/test/WebSites/WebApiCompatShimWebSite/Startup.cs @@ -2,6 +2,8 @@ // 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; namespace WebApiCompatShimWebSite @@ -19,7 +21,15 @@ public void Configure(IApplicationBuilder app) services.AddWebApiConventions(); }); - app.UseMvc(); + app.UseMvc(routes => + { + // Tests include different styles of WebAPI conventional routing and action selection - the prefix keeps + // them from matching too eagerly. + routes.MapRoute("named-action", "api/Blog/{controller}/{action}/{id?}"); + routes.MapRoute("unnamed-action", "api/Admin/{controller}/{id?}"); + routes.MapRoute("name-as-parameter", "api/Store/{controller}/{name?}"); + routes.MapRoute("extra-parameter", "api/Support/{extra}/{controller}/{id?}"); + }); } } } From e51e0e1d52c06fd5f03363f98cf26c5c51bce728 Mon Sep 17 00:00:00 2001 From: Ryan Nowak Date: Wed, 8 Oct 2014 14:52:04 -0700 Subject: [PATCH 07/39] Fix for issue #1279 - Add .Request property to ApiController This change adds a .Request property to the ApiController class that can be used to access an HttpRequestMessage wrapping the HttpContext. The HttpRequestMessage is stored in an http feature to make it accessible to model binders and other infrastructure. --- .../ApiController.cs | 22 +++ .../HttpRequestMessageFeature.cs | 70 +++++++++ ...HttpRequestMessageHttpContextExtensions.cs | 23 +++ .../IHttpRequestMessageFeature.cs | 12 ++ .../WebApiCompatShimBasicTest.cs | 23 +++ .../HttpRequestMessageFeatureTest.cs | 134 ++++++++++++++++++ .../HttpRequestMessageController.cs | 29 ++++ 7 files changed, 313 insertions(+) create mode 100644 src/Microsoft.AspNet.Mvc.WebApiCompatShim/HttpRequestMessage/HttpRequestMessageFeature.cs create mode 100644 src/Microsoft.AspNet.Mvc.WebApiCompatShim/HttpRequestMessage/HttpRequestMessageHttpContextExtensions.cs create mode 100644 src/Microsoft.AspNet.Mvc.WebApiCompatShim/HttpRequestMessage/IHttpRequestMessageFeature.cs create mode 100644 test/Microsoft.AspNet.Mvc.WebApiCompatShimTest/HttpRequestMessage/HttpRequestMessageFeatureTest.cs create mode 100644 test/WebSites/WebApiCompatShimWebSite/Controllers/HttpRequestMessage/HttpRequestMessageController.cs diff --git a/src/Microsoft.AspNet.Mvc.WebApiCompatShim/ApiController.cs b/src/Microsoft.AspNet.Mvc.WebApiCompatShim/ApiController.cs index 45b8c0a1f1..3d9c2bfd0f 100644 --- a/src/Microsoft.AspNet.Mvc.WebApiCompatShim/ApiController.cs +++ b/src/Microsoft.AspNet.Mvc.WebApiCompatShim/ApiController.cs @@ -1,6 +1,7 @@ // 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.Net.Http; using System.Security.Principal; using Microsoft.AspNet.Http; using Microsoft.AspNet.Mvc; @@ -13,6 +14,8 @@ namespace System.Web.Http [UseWebApiOverloading] public abstract class ApiController : IDisposable { + private HttpRequestMessage _request; + /// Gets the action context. /// The setter is intended for unit testing purposes only. [Activate] @@ -40,6 +43,25 @@ public ModelStateDictionary ModelState } } + /// Gets or sets the HTTP request message. + /// The setter is intended for unit testing purposes only. + public HttpRequestMessage Request + { + get + { + if (_request == null && ActionContext != null) + { + _request = ActionContext.HttpContext.GetHttpRequestMessage(); + } + + return _request; + } + set + { + _request = value; + } + } + /// Gets a factory used to generate URLs to other APIs. /// The setter is intended for unit testing purposes only. [Activate] diff --git a/src/Microsoft.AspNet.Mvc.WebApiCompatShim/HttpRequestMessage/HttpRequestMessageFeature.cs b/src/Microsoft.AspNet.Mvc.WebApiCompatShim/HttpRequestMessage/HttpRequestMessageFeature.cs new file mode 100644 index 0000000000..19f7d90591 --- /dev/null +++ b/src/Microsoft.AspNet.Mvc.WebApiCompatShim/HttpRequestMessage/HttpRequestMessageFeature.cs @@ -0,0 +1,70 @@ +// 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.Diagnostics.Contracts; +using System.Net.Http; +using Microsoft.AspNet.Http; + +namespace Microsoft.AspNet.Mvc.WebApiCompatShim +{ + public class HttpRequestMessageFeature : IHttpRequestMessageFeature + { + private readonly HttpContext _httpContext; + private HttpRequestMessage _httpRequestMessage; + + public HttpRequestMessageFeature([NotNull] HttpContext httpContext) + { + _httpContext = httpContext; + } + + public HttpRequestMessage HttpRequestMessage + { + get + { + if (_httpRequestMessage == null) + { + _httpRequestMessage = CreateHttpRequestMessage(_httpContext); + } + + return _httpRequestMessage; + } + + set + { + _httpRequestMessage = value; + } + } + + private static HttpRequestMessage CreateHttpRequestMessage(HttpContext httpContext) + { + var httpRequest = httpContext.Request; + var uriString = + httpRequest.Scheme + "://" + + httpRequest.Host + + httpRequest.PathBase + + httpRequest.Path + + httpRequest.QueryString; + + var message = new HttpRequestMessage(new HttpMethod(httpRequest.Method), uriString); + + // This allows us to pass the message through APIs defined in legacy code and then + // operate on the HttpContext inside. + message.Properties[nameof(HttpContext)] = httpContext; + + message.Content = new StreamContent(httpRequest.Body); + + foreach (var header in httpRequest.Headers) + { + // Every header should be able to fit into one of the two header collections. + // Try message.Headers first since that accepts more of them. + if (!message.Headers.TryAddWithoutValidation(header.Key, header.Value)) + { + var added = message.Content.Headers.TryAddWithoutValidation(header.Key, header.Value); + Contract.Assert(added); + } + } + + return message; + } + } +} diff --git a/src/Microsoft.AspNet.Mvc.WebApiCompatShim/HttpRequestMessage/HttpRequestMessageHttpContextExtensions.cs b/src/Microsoft.AspNet.Mvc.WebApiCompatShim/HttpRequestMessage/HttpRequestMessageHttpContextExtensions.cs new file mode 100644 index 0000000000..4a96fd3859 --- /dev/null +++ b/src/Microsoft.AspNet.Mvc.WebApiCompatShim/HttpRequestMessage/HttpRequestMessageHttpContextExtensions.cs @@ -0,0 +1,23 @@ +// 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.Net.Http; +using Microsoft.AspNet.Http; + +namespace Microsoft.AspNet.Mvc.WebApiCompatShim +{ + public static class HttpRequestMessageHttpContextExtensions + { + public static HttpRequestMessage GetHttpRequestMessage(this HttpContext httpContext) + { + var feature = httpContext.GetFeature(); + if (feature == null) + { + feature = new HttpRequestMessageFeature(httpContext); + httpContext.SetFeature(feature); + } + + return feature.HttpRequestMessage; + } + } +} diff --git a/src/Microsoft.AspNet.Mvc.WebApiCompatShim/HttpRequestMessage/IHttpRequestMessageFeature.cs b/src/Microsoft.AspNet.Mvc.WebApiCompatShim/HttpRequestMessage/IHttpRequestMessageFeature.cs new file mode 100644 index 0000000000..9f6b19dc3b --- /dev/null +++ b/src/Microsoft.AspNet.Mvc.WebApiCompatShim/HttpRequestMessage/IHttpRequestMessageFeature.cs @@ -0,0 +1,12 @@ +// 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.Net.Http; + +namespace Microsoft.AspNet.Mvc.WebApiCompatShim +{ + public interface IHttpRequestMessageFeature + { + HttpRequestMessage HttpRequestMessage { get; set; } + } +} diff --git a/test/Microsoft.AspNet.Mvc.FunctionalTests/WebApiCompatShimBasicTest.cs b/test/Microsoft.AspNet.Mvc.FunctionalTests/WebApiCompatShimBasicTest.cs index c15ebb13f7..a53c0b695b 100644 --- a/test/Microsoft.AspNet.Mvc.FunctionalTests/WebApiCompatShimBasicTest.cs +++ b/test/Microsoft.AspNet.Mvc.FunctionalTests/WebApiCompatShimBasicTest.cs @@ -4,6 +4,7 @@ #if ASPNET50 using System; using System.Net; +using System.Net.Http; using System.Net.Http.Formatting; using System.Threading.Tasks; using Microsoft.AspNet.Builder; @@ -78,6 +79,28 @@ public async Task Options_SetsDefaultFormatters() Assert.Equal(HttpStatusCode.OK, response.StatusCode); Assert.Equal(expected, formatters); } + + [Fact] + public async Task ApiController_RequestProperty() + { + // Arrange + var server = TestServer.Create(_provider, _app); + var client = server.CreateClient(); + + var expected = + "POST http://localhost/api/Blog/HttpRequestMessage/EchoProperty localhost " + + "13 Hello, world!"; + + // Act + var response = await client.PostAsync( + "http://localhost/api/Blog/HttpRequestMessage/EchoProperty", + new StringContent("Hello, world!")); + var content = await response.Content.ReadAsStringAsync(); + + // Assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.Equal(expected, content); + } } } #endif \ No newline at end of file diff --git a/test/Microsoft.AspNet.Mvc.WebApiCompatShimTest/HttpRequestMessage/HttpRequestMessageFeatureTest.cs b/test/Microsoft.AspNet.Mvc.WebApiCompatShimTest/HttpRequestMessage/HttpRequestMessageFeatureTest.cs new file mode 100644 index 0000000000..09912e7504 --- /dev/null +++ b/test/Microsoft.AspNet.Mvc.WebApiCompatShimTest/HttpRequestMessage/HttpRequestMessageFeatureTest.cs @@ -0,0 +1,134 @@ +// 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.IO; +using System.Net.Http; +using System.Text; +using System.Threading.Tasks; +using Microsoft.AspNet.Http; +using Microsoft.AspNet.PipelineCore; +using Xunit; + +namespace Microsoft.AspNet.Mvc.WebApiCompatShim +{ + public class HttpRequestMessageFeatureTest + { + [Fact] + public void HttpRequestMessage_CombinesUri() + { + // Arrange + var context = new DefaultHttpContext(); + var feature = new HttpRequestMessageFeature(context); + + context.Request.Method = "GET"; + + context.Request.Scheme = "http"; + context.Request.Host = new HostString("contoso.com"); + context.Request.PathBase = new PathString("/app"); + context.Request.Path = new PathString("/api/Products"); + context.Request.QueryString = new QueryString("?orderId=3"); + + // Act + var request = feature.HttpRequestMessage; + + // Assert + Assert.Equal("http://contoso.com/app/api/Products?orderId=3", request.RequestUri.AbsoluteUri); + } + + [Fact] + public void HttpRequestMessage_CopiesRequestMethod() + { + // Arrange + var context = new DefaultHttpContext(); + var feature = new HttpRequestMessageFeature(context); + + context.Request.Method = "OPTIONS"; + + // Act + var request = feature.HttpRequestMessage; + + // Assert + Assert.Equal(new HttpMethod("OPTIONS"), request.Method); + } + + [Fact] + public void HttpRequestMessage_CopiesHeader() + { + // Arrange + var context = new DefaultHttpContext(); + var feature = new HttpRequestMessageFeature(context); + + context.Request.Method = "OPTIONS"; + + context.Request.Headers.Add("Host", new string[] { "contoso.com" }); + + // Act + var request = feature.HttpRequestMessage; + + // Assert + Assert.Equal("contoso.com", request.Headers.Host); + } + + [Fact] + public void HttpRequestMessage_CopiesContentHeader() + { + // Arrange + var context = new DefaultHttpContext(); + var feature = new HttpRequestMessageFeature(context); + + context.Request.Method = "OPTIONS"; + + context.Request.Headers.Add("Content-Type", new string[] { "text/plain" }); + + // Act + var request = feature.HttpRequestMessage; + + // Assert + Assert.Equal("text/plain", request.Content.Headers.ContentType.ToString()); + } + + [Fact] + public async Task HttpRequestMessage_WrapsBodyContent() + { + // Arrange + var context = new DefaultHttpContext(); + var feature = new HttpRequestMessageFeature(context); + + context.Request.Method = "OPTIONS"; + + var bytes = Encoding.UTF8.GetBytes("Hello, world!"); + context.Request.Body = new MemoryStream(bytes); + context.Request.Body.Seek(0, SeekOrigin.Begin); + + // Act + var request = feature.HttpRequestMessage; + + // Assert + var streamContent = Assert.IsType(request.Content); + var content = await request.Content.ReadAsStringAsync(); + Assert.Equal("Hello, world!", content); + } + + [Fact] + public void HttpRequestMessage_CachesMessage() + { + // Arrange + var context = new DefaultHttpContext(); + var feature = new HttpRequestMessageFeature(context); + + context.Request.Method = "GET"; + context.Request.Scheme = "http"; + context.Request.Host = new HostString("contoso.com"); + + // Act + var request1 = feature.HttpRequestMessage; + + context.Request.Path = new PathString("/api/Products"); + var request2 = feature.HttpRequestMessage; + + // Assert + Assert.Same(request1, request2); + Assert.Equal("/", request2.RequestUri.AbsolutePath); + } + } +} \ No newline at end of file diff --git a/test/WebSites/WebApiCompatShimWebSite/Controllers/HttpRequestMessage/HttpRequestMessageController.cs b/test/WebSites/WebApiCompatShimWebSite/Controllers/HttpRequestMessage/HttpRequestMessageController.cs new file mode 100644 index 0000000000..174b0d7fc6 --- /dev/null +++ b/test/WebSites/WebApiCompatShimWebSite/Controllers/HttpRequestMessage/HttpRequestMessageController.cs @@ -0,0 +1,29 @@ +// 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.Threading.Tasks; +using System.Web.Http; +using Microsoft.AspNet.Http; +using Microsoft.AspNet.Mvc; + +namespace WebApiCompatShimWebSite +{ + public class HttpRequestMessageController : ApiController + { + public async Task EchoProperty() + { + var request = Request; + + var message = string.Format( + "{0} {1} {2} {3} {4}", + request.Method, + request.RequestUri.AbsoluteUri, + request.Headers.Host, + request.Content.Headers.ContentLength, + await request.Content.ReadAsStringAsync()); + + await Context.Response.WriteAsync(message); + return new EmptyResult(); + } + } +} \ No newline at end of file From aad3ae42ca2ba731592ee99d216de433f07d1ef3 Mon Sep 17 00:00:00 2001 From: Ryan Nowak Date: Wed, 8 Oct 2014 14:56:16 -0700 Subject: [PATCH 08/39] Fix for issue #1281 - Add ModelBinder for HttpRequestMessage This change adds a ModelBinder that can bind an HttpRequestMessage to an action parameter. This builds on an earlier change to construct and store the request message in the HttpContext via an http feature. --- .../HttpRequestMessageModelBinder.cs | 23 +++++++++++++++++++ .../WebApiCompatShimOptionsSetup.cs | 3 +++ .../WebApiCompatShimBasicTest.cs | 22 ++++++++++++++++++ .../HttpRequestMessageController.cs | 22 +++++++++++++++--- 4 files changed, 67 insertions(+), 3 deletions(-) create mode 100644 src/Microsoft.AspNet.Mvc.WebApiCompatShim/HttpRequestMessage/HttpRequestMessageModelBinder.cs diff --git a/src/Microsoft.AspNet.Mvc.WebApiCompatShim/HttpRequestMessage/HttpRequestMessageModelBinder.cs b/src/Microsoft.AspNet.Mvc.WebApiCompatShim/HttpRequestMessage/HttpRequestMessageModelBinder.cs new file mode 100644 index 0000000000..7da8febdbf --- /dev/null +++ b/src/Microsoft.AspNet.Mvc.WebApiCompatShim/HttpRequestMessage/HttpRequestMessageModelBinder.cs @@ -0,0 +1,23 @@ +using Microsoft.AspNet.Mvc.ModelBinding; +// 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.Net.Http; +using System.Threading.Tasks; + +namespace Microsoft.AspNet.Mvc.WebApiCompatShim +{ + public class HttpRequestMessageModelBinder : IModelBinder + { + public Task BindModelAsync(ModelBindingContext bindingContext) + { + if (bindingContext.ModelType == typeof(HttpRequestMessage)) + { + bindingContext.Model = bindingContext.HttpContext.GetHttpRequestMessage(); + return Task.FromResult(true); + } + + return Task.FromResult(false); + } + } +} diff --git a/src/Microsoft.AspNet.Mvc.WebApiCompatShim/WebApiCompatShimOptionsSetup.cs b/src/Microsoft.AspNet.Mvc.WebApiCompatShim/WebApiCompatShimOptionsSetup.cs index 3a1e0adfdc..22d6a3f230 100644 --- a/src/Microsoft.AspNet.Mvc.WebApiCompatShim/WebApiCompatShimOptionsSetup.cs +++ b/src/Microsoft.AspNet.Mvc.WebApiCompatShim/WebApiCompatShimOptionsSetup.cs @@ -21,6 +21,9 @@ public void Invoke(MvcOptions options) // Add webapi behaviors to controllers with the appropriate attributes options.ApplicationModelConventions.Add(new WebApiActionConventionsGlobalModelConvention()); options.ApplicationModelConventions.Add(new WebApiOverloadingGlobalModelConvention()); + + // Add a model binder to be able to bind HttpRequestMessage + options.ModelBinders.Insert(0, new HttpRequestMessageModelBinder()); } public void Invoke(WebApiCompatShimOptions options) diff --git a/test/Microsoft.AspNet.Mvc.FunctionalTests/WebApiCompatShimBasicTest.cs b/test/Microsoft.AspNet.Mvc.FunctionalTests/WebApiCompatShimBasicTest.cs index a53c0b695b..31985a71e0 100644 --- a/test/Microsoft.AspNet.Mvc.FunctionalTests/WebApiCompatShimBasicTest.cs +++ b/test/Microsoft.AspNet.Mvc.FunctionalTests/WebApiCompatShimBasicTest.cs @@ -101,6 +101,28 @@ public async Task ApiController_RequestProperty() Assert.Equal(HttpStatusCode.OK, response.StatusCode); Assert.Equal(expected, content); } + + [Fact] + public async Task ApiController_RequestParameter() + { + // Arrange + var server = TestServer.Create(_provider, _app); + var client = server.CreateClient(); + + var expected = + "POST http://localhost/api/Blog/HttpRequestMessage/EchoParameter localhost " + + "17 Hello, the world!"; + + // Act + var response = await client.PostAsync( + "http://localhost/api/Blog/HttpRequestMessage/EchoParameter", + new StringContent("Hello, the world!")); + var content = await response.Content.ReadAsStringAsync(); + + // Assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.Equal(expected, content); + } } } #endif \ No newline at end of file diff --git a/test/WebSites/WebApiCompatShimWebSite/Controllers/HttpRequestMessage/HttpRequestMessageController.cs b/test/WebSites/WebApiCompatShimWebSite/Controllers/HttpRequestMessage/HttpRequestMessageController.cs index 174b0d7fc6..7a28eb21e6 100644 --- a/test/WebSites/WebApiCompatShimWebSite/Controllers/HttpRequestMessage/HttpRequestMessageController.cs +++ b/test/WebSites/WebApiCompatShimWebSite/Controllers/HttpRequestMessage/HttpRequestMessageController.cs @@ -1,6 +1,8 @@ // 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.Http; using System.Threading.Tasks; using System.Web.Http; using Microsoft.AspNet.Http; @@ -12,18 +14,32 @@ public class HttpRequestMessageController : ApiController { public async Task EchoProperty() { - var request = Request; + await Echo(Request); + return new EmptyResult(); + } + + public async Task EchoParameter(HttpRequestMessage request) + { + if (!object.ReferenceEquals(request, Request)) + { + throw new InvalidOperationException(); + } + + await Echo(request); + return new EmptyResult(); + } + private async Task Echo(HttpRequestMessage request) + { var message = string.Format( "{0} {1} {2} {3} {4}", - request.Method, + request.Method, request.RequestUri.AbsoluteUri, request.Headers.Host, request.Content.Headers.ContentLength, await request.Content.ReadAsStringAsync()); await Context.Response.WriteAsync(message); - return new EmptyResult(); } } } \ No newline at end of file From 22869b41c020940948d8949b3025c402cd1f9558 Mon Sep 17 00:00:00 2001 From: Ryan Nowak Date: Wed, 8 Oct 2014 14:59:28 -0700 Subject: [PATCH 09/39] Fix issue #1280 - Add HttpResponseMessageFormatter Adds a formatter that can convert an HttpResponseMessage returned from an action into HttpContext.Response output. --- .../HttpResponseMessageOutputFormatter.cs | 69 +++++++++++++++++++ .../Properties/Resources.Designer.cs | 16 +++++ .../Resources.resx | 3 + .../WebApiCompatShimOptionsSetup.cs | 3 + .../WebApiCompatShimBasicTest.cs | 53 ++++++++++++++ .../HttpRequestMessageController.cs | 28 ++++++++ 6 files changed, 172 insertions(+) create mode 100644 src/Microsoft.AspNet.Mvc.WebApiCompatShim/Formatters/HttpResponseMessageOutputFormatter.cs diff --git a/src/Microsoft.AspNet.Mvc.WebApiCompatShim/Formatters/HttpResponseMessageOutputFormatter.cs b/src/Microsoft.AspNet.Mvc.WebApiCompatShim/Formatters/HttpResponseMessageOutputFormatter.cs new file mode 100644 index 0000000000..3dd743b842 --- /dev/null +++ b/src/Microsoft.AspNet.Mvc.WebApiCompatShim/Formatters/HttpResponseMessageOutputFormatter.cs @@ -0,0 +1,69 @@ +// 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 System.Linq; +using System.Net.Http; +using System.Threading.Tasks; +using Microsoft.AspNet.Mvc.HeaderValueAbstractions; +using Microsoft.AspNet.HttpFeature; + +namespace Microsoft.AspNet.Mvc.WebApiCompatShim +{ + public class HttpResponseMessageOutputFormatter : IOutputFormatter + { + public bool CanWriteResult(OutputFormatterContext context, MediaTypeHeaderValue contentType) + { + return context.Object is HttpResponseMessage; + } + + public IReadOnlyList GetSupportedContentTypes( + Type declaredType, + Type runtimeType, + MediaTypeHeaderValue contentType) + { + return null; + } + + public async Task WriteAsync(OutputFormatterContext context) + { + var response = context.ActionContext.HttpContext.Response; + + var responseMessage = context.Object as HttpResponseMessage; + if (responseMessage == null) + { + var message = Resources.FormatHttpResponseMessageFormatter_UnsupportedType( + nameof(HttpResponseMessageOutputFormatter), + nameof(HttpResponseMessage)); + + throw new InvalidOperationException(message); + } + + response.StatusCode = (int)responseMessage.StatusCode; + + var responseFeature = context.ActionContext.HttpContext.GetFeature(); + if (responseFeature != null) + { + responseFeature.ReasonPhrase = responseMessage.ReasonPhrase; + } + + var responseHeaders = responseMessage.Headers; + foreach (var header in responseHeaders) + { + response.Headers.AppendValues(header.Key, header.Value.ToArray()); + } + + if (responseMessage.Content != null) + { + var contentHeaders = responseMessage.Content.Headers; + foreach (var header in contentHeaders) + { + response.Headers.AppendValues(header.Key, header.Value.ToArray()); + } + + await responseMessage.Content.CopyToAsync(response.Body); + } + } + } +} diff --git a/src/Microsoft.AspNet.Mvc.WebApiCompatShim/Properties/Resources.Designer.cs b/src/Microsoft.AspNet.Mvc.WebApiCompatShim/Properties/Resources.Designer.cs index ee2edffaaf..cb10994d90 100644 --- a/src/Microsoft.AspNet.Mvc.WebApiCompatShim/Properties/Resources.Designer.cs +++ b/src/Microsoft.AspNet.Mvc.WebApiCompatShim/Properties/Resources.Designer.cs @@ -10,6 +10,22 @@ internal static class Resources private static readonly ResourceManager _resourceManager = new ResourceManager("Microsoft.AspNet.Mvc.WebApiCompatShim.Resources", typeof(Resources).GetTypeInfo().Assembly); + /// + /// The {0} only supports writing objects of type {1}. + /// + internal static string HttpResponseMessageFormatter_UnsupportedType + { + get { return GetString("HttpResponseMessageFormatter_UnsupportedType"); } + } + + /// + /// The {0} only supports writing objects of type {1}. + /// + internal static string FormatHttpResponseMessageFormatter_UnsupportedType(object p0, object p1) + { + return string.Format(CultureInfo.CurrentCulture, GetString("HttpResponseMessageFormatter_UnsupportedType"), p0, p1); + } + /// /// The key is invalid JQuery syntax because it is missing a closing bracket. /// diff --git a/src/Microsoft.AspNet.Mvc.WebApiCompatShim/Resources.resx b/src/Microsoft.AspNet.Mvc.WebApiCompatShim/Resources.resx index 00afe5899e..a86ebf7b9a 100644 --- a/src/Microsoft.AspNet.Mvc.WebApiCompatShim/Resources.resx +++ b/src/Microsoft.AspNet.Mvc.WebApiCompatShim/Resources.resx @@ -117,6 +117,9 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + The {0} only supports writing objects of type {1}. + The key is invalid JQuery syntax because it is missing a closing bracket. diff --git a/src/Microsoft.AspNet.Mvc.WebApiCompatShim/WebApiCompatShimOptionsSetup.cs b/src/Microsoft.AspNet.Mvc.WebApiCompatShim/WebApiCompatShimOptionsSetup.cs index 22d6a3f230..142a66d2ad 100644 --- a/src/Microsoft.AspNet.Mvc.WebApiCompatShim/WebApiCompatShimOptionsSetup.cs +++ b/src/Microsoft.AspNet.Mvc.WebApiCompatShim/WebApiCompatShimOptionsSetup.cs @@ -24,6 +24,9 @@ public void Invoke(MvcOptions options) // Add a model binder to be able to bind HttpRequestMessage options.ModelBinders.Insert(0, new HttpRequestMessageModelBinder()); + + // Add a formatter to write out an HttpResponseMessage to the response + options.OutputFormatters.Insert(0, new HttpResponseMessageOutputFormatter()); } public void Invoke(WebApiCompatShimOptions options) diff --git a/test/Microsoft.AspNet.Mvc.FunctionalTests/WebApiCompatShimBasicTest.cs b/test/Microsoft.AspNet.Mvc.FunctionalTests/WebApiCompatShimBasicTest.cs index 31985a71e0..ae897181bd 100644 --- a/test/Microsoft.AspNet.Mvc.FunctionalTests/WebApiCompatShimBasicTest.cs +++ b/test/Microsoft.AspNet.Mvc.FunctionalTests/WebApiCompatShimBasicTest.cs @@ -3,6 +3,7 @@ #if ASPNET50 using System; +using System.Collections.Generic; using System.Net; using System.Net.Http; using System.Net.Http.Formatting; @@ -123,6 +124,58 @@ public async Task ApiController_RequestParameter() Assert.Equal(HttpStatusCode.OK, response.StatusCode); Assert.Equal(expected, content); } + + [Fact] + public async Task ApiController_ResponseReturned() + { + // Arrange + var server = TestServer.Create(_provider, _app); + var client = server.CreateClient(); + + var expected = + "POST Hello, HttpResponseMessage world!"; + + // Act + var response = await client.PostAsync( + "http://localhost/api/Blog/HttpRequestMessage/EchoWithResponseMessage", + new StringContent("Hello, HttpResponseMessage world!")); + var content = await response.Content.ReadAsStringAsync(); + + // Assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.Equal(expected, content); + + IEnumerable values; + Assert.True(response.Headers.TryGetValues("X-Test", out values)); + Assert.Equal(new string[] { "Hello!" }, values); + Assert.Equal(38, response.Content.Headers.ContentLength); + } + + [Fact] + public async Task ApiController_ResponseReturned_Chunked() + { + // Arrange + var server = TestServer.Create(_provider, _app); + var client = server.CreateClient(); + + var expected = + "POST Hello, HttpResponseMessage world!"; + + // Act + var response = await client.PostAsync( + "http://localhost/api/Blog/HttpRequestMessage/EchoWithResponseMessageChunked", + new StringContent("Hello, HttpResponseMessage world!")); + var content = await response.Content.ReadAsStringAsync(); + + // Assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.Equal(expected, content); + + IEnumerable values; + Assert.True(response.Headers.TryGetValues("X-Test", out values)); + Assert.Equal(new string[] { "Hello!" }, values); + Assert.Equal(true, response.Headers.TransferEncodingChunked); + } } } #endif \ No newline at end of file diff --git a/test/WebSites/WebApiCompatShimWebSite/Controllers/HttpRequestMessage/HttpRequestMessageController.cs b/test/WebSites/WebApiCompatShimWebSite/Controllers/HttpRequestMessage/HttpRequestMessageController.cs index 7a28eb21e6..c4b654c5ee 100644 --- a/test/WebSites/WebApiCompatShimWebSite/Controllers/HttpRequestMessage/HttpRequestMessageController.cs +++ b/test/WebSites/WebApiCompatShimWebSite/Controllers/HttpRequestMessage/HttpRequestMessageController.cs @@ -7,6 +7,7 @@ using System.Web.Http; using Microsoft.AspNet.Http; using Microsoft.AspNet.Mvc; +using System.Net; namespace WebApiCompatShimWebSite { @@ -29,6 +30,33 @@ public async Task EchoParameter(HttpRequestMessage request) return new EmptyResult(); } + public async Task EchoWithResponseMessage(HttpRequestMessage request) + { + var message = string.Format( + "{0} {1}", + request.Method.ToString(), + await request.Content.ReadAsStringAsync()); + + var response = request.CreateResponse(HttpStatusCode.OK); + response.Content = new StringContent(message); + response.Headers.TryAddWithoutValidation("X-Test", "Hello!"); + return response; + } + + public async Task EchoWithResponseMessageChunked(HttpRequestMessage request) + { + var message = string.Format( + "{0} {1}", + request.Method.ToString(), + await request.Content.ReadAsStringAsync()); + + var response = request.CreateResponse(HttpStatusCode.OK); + response.Content = new StringContent(message); + response.Headers.TransferEncodingChunked = true; + response.Headers.TryAddWithoutValidation("X-Test", "Hello!"); + return response; + } + private async Task Echo(HttpRequestMessage request) { var message = string.Format( From 5a83383179abcfad5ebddc7736549fa096c1df9c Mon Sep 17 00:00:00 2001 From: Harsh Gupta Date: Thu, 9 Oct 2014 16:42:52 -0700 Subject: [PATCH 10/39] Adding ApiController.Validate : Fixes #1286 --- .../ApiController.cs | 35 +++++++++++++ .../WebApiCompatShimBasicTest.cs | 47 ++++++++++++++++++ .../Controllers/BasicApiController.cs | 49 +++++++++++++++++++ 3 files changed, 131 insertions(+) diff --git a/src/Microsoft.AspNet.Mvc.WebApiCompatShim/ApiController.cs b/src/Microsoft.AspNet.Mvc.WebApiCompatShim/ApiController.cs index 3d9c2bfd0f..efcefa8f32 100644 --- a/src/Microsoft.AspNet.Mvc.WebApiCompatShim/ApiController.cs +++ b/src/Microsoft.AspNet.Mvc.WebApiCompatShim/ApiController.cs @@ -7,6 +7,7 @@ using Microsoft.AspNet.Mvc; using Microsoft.AspNet.Mvc.ModelBinding; using Microsoft.AspNet.Mvc.WebApiCompatShim; +using Microsoft.Framework.DependencyInjection; namespace System.Web.Http { @@ -83,6 +84,40 @@ public void Dispose() GC.SuppressFinalize(this); } + /// + /// Validates the given entity and adds the validation errors to the + /// under an empty prefix. + /// + /// The type of the entity to be validated. + /// The entity being validated. + public void Validate(TEntity entity) + { + Validate(entity, keyPrefix: string.Empty); + } + + /// + /// Validates the given entity and adds the validation errors to the . + /// + /// The type of the entity to be validated. + /// The entity being validated. + /// + /// The key prefix under which the model state errors would be added in the + /// . + /// + public void Validate(TEntity entity, string keyPrefix) + { + var validator = Context.RequestServices.GetService(); + var metadataProvider = Context.RequestServices.GetService(); + var modelMetadata = metadataProvider.GetMetadataForType(() => entity, typeof(TEntity)); + var validatorProvider = Context.RequestServices.GetService(); + var modelValidationContext = new ModelValidationContext(metadataProvider, + validatorProvider, + ModelState, + modelMetadata, + containerMetadata: null); + validator.Validate(modelValidationContext, keyPrefix); + } + protected virtual void Dispose(bool disposing) { } diff --git a/test/Microsoft.AspNet.Mvc.FunctionalTests/WebApiCompatShimBasicTest.cs b/test/Microsoft.AspNet.Mvc.FunctionalTests/WebApiCompatShimBasicTest.cs index ae897181bd..b37a95c9c1 100644 --- a/test/Microsoft.AspNet.Mvc.FunctionalTests/WebApiCompatShimBasicTest.cs +++ b/test/Microsoft.AspNet.Mvc.FunctionalTests/WebApiCompatShimBasicTest.cs @@ -81,6 +81,53 @@ public async Task Options_SetsDefaultFormatters() Assert.Equal(expected, formatters); } + [Fact] + public async Task ApiController_CanValidateCustomObjectWithPrefix_Fails() + { + // Arrange + var server = TestServer.Create(_provider, _app); + var client = server.CreateClient(); + + // Act + var response = await client.GetStringAsync( + "http://localhost/api/Blog/BasicApi/ValidateObjectWithPrefixFails?prefix=prefix"); + + // Assert + var json = JsonConvert.DeserializeObject>(response); + Assert.Equal(1, json.Count); + Assert.Equal("The field ID must be between 0 and 100.", json["prefix.ID"]); + } + + [Fact] + public async Task ApiController_CanValidateCustomObject_IsSuccessFul() + { + // Arrange + var server = TestServer.Create(_provider, _app); + var client = server.CreateClient(); + + // Act + var response = await client.GetStringAsync("http://localhost/api/Blog/BasicApi/ValidateObject_Passes"); + + // Assert + Assert.Equal("true", response); + } + + [Fact] + public async Task ApiController_CanValidateCustomObject_Fails() + { + // Arrange + var server = TestServer.Create(_provider, _app); + var client = server.CreateClient(); + + // Act + var response = await client.GetStringAsync("http://localhost/api/Blog/BasicApi/ValidateObjectFails"); + + // Assert + var json = JsonConvert.DeserializeObject>(response); + Assert.Equal(1, json.Count); + Assert.Equal("The field ID must be between 0 and 100.", json["ID"]); + } + [Fact] public async Task ApiController_RequestProperty() { diff --git a/test/WebSites/WebApiCompatShimWebSite/Controllers/BasicApiController.cs b/test/WebSites/WebApiCompatShimWebSite/Controllers/BasicApiController.cs index 09478361be..36cda533c4 100644 --- a/test/WebSites/WebApiCompatShimWebSite/Controllers/BasicApiController.cs +++ b/test/WebSites/WebApiCompatShimWebSite/Controllers/BasicApiController.cs @@ -1,6 +1,8 @@ // 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.Collections.Generic; +using System.ComponentModel.DataAnnotations; using System.Linq; using System.Threading.Tasks; using System.Web.Http; @@ -45,5 +47,52 @@ public string[] GetFormatters() { return OptionsAccessor.Options.Formatters.Select(f => f.GetType().FullName).ToArray(); } + + [HttpGet] + public bool ValidateObject_Passes() + { + var entity = new TestEntity { ID = 42 }; + Validate(entity); + return ModelState.IsValid; + } + + [HttpGet] + public object ValidateObjectFails() + { + var entity = new TestEntity { ID = -1 }; + Validate(entity); + return CreateValidationDictionary(); + } + + [HttpGet] + public object ValidateObjectWithPrefixFails(string prefix) + { + var entity = new TestEntity { ID = -1 }; + Validate(entity, prefix); + return CreateValidationDictionary(); + } + + private class TestEntity + { + [Range(0, 100)] + public int ID { get; set; } + } + + private Dictionary CreateValidationDictionary() + { + var result = new Dictionary(); + foreach (var item in ModelState) + { + var error = item.Value.Errors.SingleOrDefault(); + if (error != null) + { + var value = error.Exception != null ? error.Exception.Message : + error.ErrorMessage; + result.Add(item.Key, value); + } + } + + return result; + } } } \ No newline at end of file From 3968df90e45deddaaf61e9831da3347a36714bf1 Mon Sep 17 00:00:00 2001 From: Ryan Nowak Date: Wed, 8 Oct 2014 15:01:02 -0700 Subject: [PATCH 11/39] Fix issue #1282 - Add Request.CreateResponse extension methods Adds the set of CreateResponse/CreateErrorResponse extension methods that return an HttpResponseMessage. For the overloads that perform content negotiation they will access the collection of MediaTypeFormatters through the shim options. Note that CreateResponse and friends use the OLD formatters. Also, HttpError and CreateErrorResponse assume ErrorDetail == false. Using the shim you will not get detailed error messages unless you construct the HttpError instance yourself. --- .../HttpError.cs | 265 ++++++++++++ .../HttpErrorKeys.cs | 56 +++ .../HttpRequestMessageExtensions.cs | 382 ++++++++++++++++++ .../Properties/Resources.Designer.cs | 80 ++++ .../Resources.resx | 15 + ...piCompatShimServiceCollectionExtensions.cs | 6 + .../WebApiCompatShimBasicTest.cs | 104 +++++ .../HttpErrorTest.cs | 285 +++++++++++++ .../HttpRequestMessageExtensionsTest.cs | 347 ++++++++++++++++ .../HttpRequestMessageController.cs | 43 +- .../WebApiCompatShimWebSite/Models/User.cs | 1 + 11 files changed, 1581 insertions(+), 3 deletions(-) create mode 100644 src/Microsoft.AspNet.Mvc.WebApiCompatShim/HttpError.cs create mode 100644 src/Microsoft.AspNet.Mvc.WebApiCompatShim/HttpErrorKeys.cs create mode 100644 src/Microsoft.AspNet.Mvc.WebApiCompatShim/HttpRequestMessage/HttpRequestMessageExtensions.cs create mode 100644 test/Microsoft.AspNet.Mvc.WebApiCompatShimTest/HttpErrorTest.cs create mode 100644 test/Microsoft.AspNet.Mvc.WebApiCompatShimTest/HttpRequestMessage/HttpRequestMessageExtensionsTest.cs diff --git a/src/Microsoft.AspNet.Mvc.WebApiCompatShim/HttpError.cs b/src/Microsoft.AspNet.Mvc.WebApiCompatShim/HttpError.cs new file mode 100644 index 0000000000..438facb22b --- /dev/null +++ b/src/Microsoft.AspNet.Mvc.WebApiCompatShim/HttpError.cs @@ -0,0 +1,265 @@ +// 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.Collections.Generic; +using System.Linq; +using System.Xml; +using System.Xml.Schema; +using System.Xml.Serialization; +using Microsoft.AspNet.Mvc; +using Microsoft.AspNet.Mvc.ModelBinding; +using ShimResources = Microsoft.AspNet.Mvc.WebApiCompatShim.Resources; + +namespace System.Web.Http +{ + /// + /// Defines a serializable container for storing error information. This information is stored + /// as key/value pairs. The dictionary keys to look up standard error information are available + /// on the type. + /// + [XmlRoot("Error")] + public sealed class HttpError : Dictionary, IXmlSerializable + { + /// + /// Initializes a new instance of the class. + /// + public HttpError() + : base(StringComparer.OrdinalIgnoreCase) + { + } + + /// + /// Initializes a new instance of the class containing error message . + /// + /// The error message to associate with this instance. + public HttpError([NotNull] string message) + : this() + { + Message = message; + } + + /// + /// Initializes a new instance of the class for . + /// + /// The exception to use for error information. + /// true to include the exception information in the error; false otherwise + public HttpError([NotNull] Exception exception, bool includeErrorDetail) + : this() + { + Message = ShimResources.HttpError_GenericError; + + if (includeErrorDetail) + { + Add(HttpErrorKeys.ExceptionMessageKey, exception.Message); + Add(HttpErrorKeys.ExceptionTypeKey, exception.GetType().FullName); + Add(HttpErrorKeys.StackTraceKey, exception.StackTrace); + if (exception.InnerException != null) + { + Add(HttpErrorKeys.InnerExceptionKey, new HttpError(exception.InnerException, includeErrorDetail)); + } + } + } + + /// + /// Initializes a new instance of the class for . + /// + /// The invalid model state to use for error information. + /// true to include exception messages in the error; false otherwise + public HttpError([NotNull] ModelStateDictionary modelState, bool includeErrorDetail) + : this() + { + if (modelState.IsValid) + { + throw new ArgumentException(ShimResources.HttpError_ValidModelState, nameof(modelState)); + } + + Message = ShimResources.HttpError_BadRequest; + + var modelStateError = new HttpError(); + foreach (KeyValuePair keyModelStatePair in modelState) + { + var key = keyModelStatePair.Key; + var errors = keyModelStatePair.Value.Errors; + if (errors != null && errors.Count > 0) + { + var errorMessages = errors.Select(error => + { + if (includeErrorDetail && error.Exception != null) + { + return error.Exception.Message; + } + else + { + return + string.IsNullOrEmpty(error.ErrorMessage) ? + ShimResources.HttpError_GenericError : + error.ErrorMessage; + } + }).ToArray(); + modelStateError.Add(key, errorMessages); + } + } + + Add(HttpErrorKeys.ModelStateKey, modelStateError); + } + + /// + /// The high-level, user-visible message explaining the cause of the error. Information carried in this field + /// should be considered public in that it will go over the wire regardless of the value of error detail policy. + /// As a result care should be taken not to disclose sensitive information about the server or the application. + /// + public string Message + { + get { return GetPropertyValue(HttpErrorKeys.MessageKey); } + set { this[HttpErrorKeys.MessageKey] = value; } + } + + /// + /// The containing information about the errors that occurred during model binding. + /// + /// + /// The inclusion of information carried in the is + /// controlled by the error detail policy. All other information in the + /// should be considered public in that it will go over the wire. As a result care should be taken not to + /// disclose sensitive information about the server or the application. + /// + public HttpError ModelState + { + get { return GetPropertyValue(HttpErrorKeys.ModelStateKey); } + } + + /// + /// A detailed description of the error intended for the developer to understand exactly what failed. + /// + /// + /// The inclusion of this field is controlled by the error detail policy. The + /// field is expected to contain information about the server or the application that should not + /// be disclosed broadly. + /// + public string MessageDetail + { + get { return GetPropertyValue(HttpErrorKeys.MessageDetailKey); } + set { this[HttpErrorKeys.MessageDetailKey] = value; } + } + + /// + /// The message of the if available. + /// + /// + /// The inclusion of this field is controlled by the error detail policy. The + /// field is expected to contain information about the server or the application that should not + /// be disclosed broadly. + /// + public string ExceptionMessage + { + get { return GetPropertyValue(HttpErrorKeys.ExceptionMessageKey); } + set { this[HttpErrorKeys.ExceptionMessageKey] = value; } + } + + /// + /// The type of the if available. + /// + /// + /// The inclusion of this field is controlled by the error detail policy. The + /// field is expected to contain information about the server or the application that should not + /// be disclosed broadly. + /// + public string ExceptionType + { + get { return GetPropertyValue(HttpErrorKeys.ExceptionTypeKey); } + set { this[HttpErrorKeys.ExceptionTypeKey] = value; } + } + + /// + /// The stack trace information associated with this instance if available. + /// + /// + /// The inclusion of this field is controlled by the error detail policy. The + /// field is expected to contain information about the server or the application that should not + /// be disclosed broadly. + /// + public string StackTrace + { + get { return GetPropertyValue(HttpErrorKeys.StackTraceKey); } + set { this[HttpErrorKeys.StackTraceKey] = value; } + } + + /// + /// The inner associated with this instance if available. + /// + /// + /// The inclusion of this field is controlled by the error detail policy. The + /// field is expected to contain information about the server or the application that should not + /// be disclosed broadly. + /// + public HttpError InnerException + { + get { return GetPropertyValue(HttpErrorKeys.InnerExceptionKey); } + } + + /// + /// Gets a particular property value from this error instance. + /// + /// The type of the property. + /// The name of the error property. + /// The value of the error property. + public TValue GetPropertyValue(string key) + { + object value; + if (TryGetValue(key, out value) && value is TValue) + { + return (TValue)value; + } + + return default(TValue); + } + + XmlSchema IXmlSerializable.GetSchema() + { + return null; + } + + void IXmlSerializable.ReadXml(XmlReader reader) + { + if (reader.IsEmptyElement) + { + reader.Read(); + return; + } + + reader.ReadStartElement(); + while (reader.NodeType != System.Xml.XmlNodeType.EndElement) + { + var key = XmlConvert.DecodeName(reader.LocalName); + var value = reader.ReadInnerXml(); + + Add(key, value); + reader.MoveToContent(); + } + reader.ReadEndElement(); + } + + void IXmlSerializable.WriteXml(XmlWriter writer) + { + foreach (var keyValuePair in this) + { + var key = keyValuePair.Key; + var value = keyValuePair.Value; + writer.WriteStartElement(XmlConvert.EncodeLocalName(key)); + if (value != null) + { + var innerError = value as HttpError; + if (innerError == null) + { + writer.WriteValue(value); + } + else + { + ((IXmlSerializable)innerError).WriteXml(writer); + } + } + writer.WriteEndElement(); + } + } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Mvc.WebApiCompatShim/HttpErrorKeys.cs b/src/Microsoft.AspNet.Mvc.WebApiCompatShim/HttpErrorKeys.cs new file mode 100644 index 0000000000..7d079b94ba --- /dev/null +++ b/src/Microsoft.AspNet.Mvc.WebApiCompatShim/HttpErrorKeys.cs @@ -0,0 +1,56 @@ +// 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 System.Web.Http +{ + /// + /// Provides keys to look up error information stored in the dictionary. + /// + public static class HttpErrorKeys + { + /// + /// Provides a key for the Message. + /// + public static readonly string MessageKey = "Message"; + + /// + /// Provides a key for the MessageDetail. + /// + public static readonly string MessageDetailKey = "MessageDetail"; + + /// + /// Provides a key for the ModelState. + /// + public static readonly string ModelStateKey = "ModelState"; + + /// + /// Provides a key for the ExceptionMessage. + /// + public static readonly string ExceptionMessageKey = "ExceptionMessage"; + + /// + /// Provides a key for the ExceptionType. + /// + public static readonly string ExceptionTypeKey = "ExceptionType"; + + /// + /// Provides a key for the StackTrace. + /// + public static readonly string StackTraceKey = "StackTrace"; + + /// + /// Provides a key for the InnerException. + /// + public static readonly string InnerExceptionKey = "InnerException"; + + /// + /// Provides a key for the MessageLanguage. + /// + public static readonly string MessageLanguageKey = "MessageLanguage"; + + /// + /// Provides a key for the ErrorCode. + /// + public static readonly string ErrorCodeKey = "ErrorCode"; + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Mvc.WebApiCompatShim/HttpRequestMessage/HttpRequestMessageExtensions.cs b/src/Microsoft.AspNet.Mvc.WebApiCompatShim/HttpRequestMessage/HttpRequestMessageExtensions.cs new file mode 100644 index 0000000000..a2555f9c75 --- /dev/null +++ b/src/Microsoft.AspNet.Mvc.WebApiCompatShim/HttpRequestMessage/HttpRequestMessageExtensions.cs @@ -0,0 +1,382 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information. + +using System.Collections.Generic; +using System.Net.Http.Headers; +using System.Web.Http; +using System.Net.Http.Formatting; +using Microsoft.AspNet.Http; +using Microsoft.AspNet.Mvc; +using Microsoft.AspNet.Mvc.ModelBinding; +using Microsoft.AspNet.Mvc.WebApiCompatShim; +using Microsoft.Framework.DependencyInjection; + +using ShimResources = Microsoft.AspNet.Mvc.WebApiCompatShim.Resources; +using Microsoft.Framework.OptionsModel; + +namespace System.Net.Http +{ + /// + /// Provides extension methods for the class. + /// + public static class HttpRequestMessageExtensions + { + /// + /// Helper method for creating an message with a "416 (Requested Range Not Satisfiable)" status code. + /// This response can be used in combination with the to indicate that the requested range or + /// ranges do not overlap with the current resource. The response contains a "Content-Range" header indicating the valid upper and lower + /// bounds for requested ranges. + /// + /// The request. + /// An instance, typically thrown by a + /// instance. + /// An 416 (Requested Range Not Satisfiable) error response with a Content-Range header indicating the valid range. + public static HttpResponseMessage CreateErrorResponse( + [NotNull] this HttpRequestMessage request, + [NotNull] InvalidByteRangeException invalidByteRangeException) + { + var rangeNotSatisfiableResponse = request.CreateErrorResponse( + HttpStatusCode.RequestedRangeNotSatisfiable, + invalidByteRangeException); + rangeNotSatisfiableResponse.Content.Headers.ContentRange = invalidByteRangeException.ContentRange; + return rangeNotSatisfiableResponse; + } + + /// + /// Helper method that performs content negotiation and creates a representing an error + /// with an instance of wrapping an with message . + /// If no formatter is found, this method returns a response with status 406 NotAcceptable. + /// + /// + /// This method requires that has been associated with an instance of + /// . + /// + /// The request. + /// The status code of the created response. + /// The error message. + /// An error response with error message and status code . + public static HttpResponseMessage CreateErrorResponse( + [NotNull] this HttpRequestMessage request, + HttpStatusCode statusCode, + [NotNull] string message) + { + return request.CreateErrorResponse(statusCode, new HttpError(message)); + } + + /// + /// Helper method that performs content negotiation and creates a representing an error + /// with an instance of wrapping an with error message + /// for exception . If no formatter is found, this method returns a response with status 406 NotAcceptable. + /// + /// + /// This method requires that has been associated with an instance of + /// . + /// + /// The request. + /// The status code of the created response. + /// The error message. + /// The exception. + /// An error response for with error message + /// and status code . + public static HttpResponseMessage CreateErrorResponse( + [NotNull] this HttpRequestMessage request, + HttpStatusCode statusCode, + [NotNull] string message, + [NotNull] Exception exception) + { + var error = new HttpError(exception, includeErrorDetail: false) { Message = message }; + return request.CreateErrorResponse(statusCode, error); + } + + /// + /// Helper method that performs content negotiation and creates a representing an error + /// with an instance of wrapping an for exception . + /// If no formatter is found, this method returns a response with status 406 NotAcceptable. + /// + /// + /// This method requires that has been associated with an instance of + /// . + /// + /// The request. + /// The status code of the created response. + /// The exception. + /// An error response for with status code . + public static HttpResponseMessage CreateErrorResponse( + [NotNull] this HttpRequestMessage request, + HttpStatusCode statusCode, + [NotNull] Exception exception) + { + return request.CreateErrorResponse(statusCode, new HttpError(exception, includeErrorDetail: false)); + } + + /// + /// Helper method that performs content negotiation and creates a representing an error + /// with an instance of wrapping an for model state . + /// If no formatter is found, this method returns a response with status 406 NotAcceptable. + /// + /// + /// This method requires that has been associated with an instance of + /// . + /// + /// The request. + /// The status code of the created response. + /// The model state. + /// An error response for with status code . + public static HttpResponseMessage CreateErrorResponse( + [NotNull] this HttpRequestMessage request, + HttpStatusCode statusCode, + [NotNull] ModelStateDictionary modelState) + { + return request.CreateErrorResponse(statusCode, new HttpError(modelState, includeErrorDetail: false)); + } + + /// + /// Helper method that performs content negotiation and creates a representing an error + /// with an instance of wrapping as the content. If no formatter + /// is found, this method returns a response with status 406 NotAcceptable. + /// + /// + /// This method requires that has been associated with an instance of + /// . + /// + /// The request. + /// The status code of the created response. + /// The error to wrap. + /// An error response wrapping with status code . + public static HttpResponseMessage CreateErrorResponse( + [NotNull] this HttpRequestMessage request, + HttpStatusCode statusCode, + [NotNull] HttpError error) + { + return request.CreateResponse(statusCode, error); + } + + /// + /// Helper method that performs content negotiation and creates a with an instance + /// of as the content and as the status code + /// if a formatter can be found. If no formatter is found, this method returns a response with status 406 NotAcceptable. + /// + /// + /// This method requires that has been associated with an instance of + /// . + /// + /// The type of the value. + /// The request. + /// The value to wrap. Can be null. + /// A response wrapping with status code. + public static HttpResponseMessage CreateResponse([NotNull] this HttpRequestMessage request, T value) + { + return request.CreateResponse(HttpStatusCode.OK, value, formatters: null); + } + + /// + /// Helper method that performs content negotiation and creates a with an instance + /// of as the content if a formatter can be found. If no formatter is found, this + /// method returns a response with status 406 NotAcceptable. + /// configuration. + /// + /// + /// This method requires that has been associated with an instance of + /// . + /// + /// The type of the value. + /// The request. + /// The status code of the created response. + /// The value to wrap. Can be null. + /// A response wrapping with . + public static HttpResponseMessage CreateResponse(this HttpRequestMessage request, HttpStatusCode statusCode, T value) + { + return request.CreateResponse(statusCode, value, formatters: null); + } + + /// + /// Helper method that performs content negotiation and creates a with an instance + /// of as the content if a formatter can be found. If no formatter is found, this + /// method returns a response with status 406 NotAcceptable. + /// + /// + /// This method will use the provided or it will get the + /// instance associated with . + /// + /// The type of the value. + /// The request. + /// The status code of the created response. + /// The value to wrap. Can be null. + /// The configuration to use. Can be null. + /// A response wrapping with . + public static HttpResponseMessage CreateResponse( + [NotNull] this HttpRequestMessage request, + HttpStatusCode statusCode, + T value, + IEnumerable formatters) + { + var context = GetHttpContext(request); + + if (formatters == null) + { + // Get the default formatters from options + var options = context.RequestServices.GetService>(); + formatters = options.Options.Formatters; + } + + var contentNegotiator = context.RequestServices.GetService(); + + var result = contentNegotiator.Negotiate(typeof(T), request, formatters); + if (result?.Formatter == null) + { + // Return a 406 when we're actually performing conneg and it fails to find a formatter. + return request.CreateResponse(HttpStatusCode.NotAcceptable); + } + else + { + return request.CreateResponse(statusCode, value, result.Formatter, result.MediaType); + } + } + + /// + /// Helper method that creates a with an instance containing the provided + /// . The given is used to find an instance of . + /// + /// The type of the value. + /// The request. + /// The status code of the created response. + /// The value to wrap. Can be null. + /// The media type used to look up an instance of . + /// A response wrapping with . + public static HttpResponseMessage CreateResponse(this HttpRequestMessage request, HttpStatusCode statusCode, T value, string mediaType) + { + return request.CreateResponse(statusCode, value, new MediaTypeHeaderValue(mediaType)); + } + + /// + /// Helper method that creates a with an instance containing the provided + /// . The given is used to find an instance of . + /// + /// The type of the value. + /// The request. + /// The status code of the created response. + /// The value to wrap. Can be null. + /// The media type used to look up an instance of . + /// A response wrapping with . + public static HttpResponseMessage CreateResponse( + [NotNull] this HttpRequestMessage request, + HttpStatusCode statusCode, + [NotNull] T value, + [NotNull] MediaTypeHeaderValue mediaType) + { + var context = GetHttpContext(request); + + // Get the default formatters from options + var options = context.RequestServices.GetService>(); + var formatters = options.Options.Formatters; + + var formatter = formatters.FindWriter(typeof(T), mediaType); + if (formatter == null) + { + var message = ShimResources.FormatHttpRequestMessage_CouldNotFindMatchingFormatter( + mediaType.ToString(), + value.GetType()); + throw new InvalidOperationException(message); + } + + return request.CreateResponse(statusCode, value, formatter, mediaType); + } + + /// + /// Helper method that creates a with an instance containing the provided + /// and the given . + /// + /// The type of the value. + /// The request. + /// The status code of the created response. + /// The value to wrap. Can be null. + /// The formatter to use. + /// A response wrapping with . + public static HttpResponseMessage CreateResponse( + [NotNull] this HttpRequestMessage request, + HttpStatusCode statusCode, + [NotNull] T value, + [NotNull] MediaTypeFormatter formatter) + { + return request.CreateResponse(statusCode, value, formatter, (MediaTypeHeaderValue)null); + } + + /// + /// Helper method that creates a with an instance containing the provided + /// and the given . + /// + /// The type of the value. + /// The request. + /// The status code of the created response. + /// The value to wrap. Can be null. + /// The formatter to use. + /// The media type override to set on the response's content. Can be null. + /// A response wrapping with . + public static HttpResponseMessage CreateResponse( + [NotNull] this HttpRequestMessage request, + HttpStatusCode statusCode, + [NotNull] T value, + [NotNull] MediaTypeFormatter formatter, + string mediaType) + { + var mediaTypeHeader = mediaType != null ? new MediaTypeHeaderValue(mediaType) : null; + return request.CreateResponse(statusCode, value, formatter, mediaTypeHeader); + } + + /// + /// Helper method that creates a with an instance containing the provided + /// and the given . + /// + /// The type of the value. + /// The request. + /// The status code of the created response. + /// The value to wrap. Can be null. + /// The formatter to use. + /// The media type override to set on the response's content. Can be null. + /// A response wrapping with . + public static HttpResponseMessage CreateResponse( + [NotNull] this HttpRequestMessage request, + HttpStatusCode statusCode, + T value, + [NotNull] MediaTypeFormatter formatter, + MediaTypeHeaderValue mediaType) + { + var response = new HttpResponseMessage(statusCode) + { + RequestMessage = request, + }; + + response.Content = new ObjectContent(value, formatter, mediaType); + + return response; + } + + private static HttpContext GetHttpContext(HttpRequestMessage request) + { + var context = request.GetProperty(nameof(HttpContext)); + if (context == null) + { + var message = ShimResources.FormatHttpRequestMessage_MustHaveHttpContext( + nameof(HttpRequestMessage), + "HttpRequestMessageHttpContextExtensions.GetHttpRequestMessage"); + throw new InvalidOperationException(message); + } + + return context; + } + + private static T GetProperty(this HttpRequestMessage request, string key) + { + object value; + request.Properties.TryGetValue(key, out value); + + if (value is T) + { + return (T)value; + } + else + { + return default(T); + } + } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Mvc.WebApiCompatShim/Properties/Resources.Designer.cs b/src/Microsoft.AspNet.Mvc.WebApiCompatShim/Properties/Resources.Designer.cs index cb10994d90..480fda8e65 100644 --- a/src/Microsoft.AspNet.Mvc.WebApiCompatShim/Properties/Resources.Designer.cs +++ b/src/Microsoft.AspNet.Mvc.WebApiCompatShim/Properties/Resources.Designer.cs @@ -10,6 +10,86 @@ internal static class Resources private static readonly ResourceManager _resourceManager = new ResourceManager("Microsoft.AspNet.Mvc.WebApiCompatShim.Resources", typeof(Resources).GetTypeInfo().Assembly); + /// + /// The request is invalid. + /// + internal static string HttpError_BadRequest + { + get { return GetString("HttpError_BadRequest"); } + } + + /// + /// The request is invalid. + /// + internal static string FormatHttpError_BadRequest() + { + return GetString("HttpError_BadRequest"); + } + + /// + /// An error has occurred. + /// + internal static string HttpError_GenericError + { + get { return GetString("HttpError_GenericError"); } + } + + /// + /// An error has occurred. + /// + internal static string FormatHttpError_GenericError() + { + return GetString("HttpError_GenericError"); + } + + /// + /// The model state is valid. + /// + internal static string HttpError_ValidModelState + { + get { return GetString("HttpError_ValidModelState"); } + } + + /// + /// The model state is valid. + /// + internal static string FormatHttpError_ValidModelState() + { + return GetString("HttpError_ValidModelState"); + } + + /// + /// Could not find a formatter matching the media type '{0}' that can write an instance of '{1}'. + /// + internal static string HttpRequestMessage_CouldNotFindMatchingFormatter + { + get { return GetString("HttpRequestMessage_CouldNotFindMatchingFormatter"); } + } + + /// + /// Could not find a formatter matching the media type '{0}' that can write an instance of '{1}'. + /// + internal static string FormatHttpRequestMessage_CouldNotFindMatchingFormatter(object p0, object p1) + { + return string.Format(CultureInfo.CurrentCulture, GetString("HttpRequestMessage_CouldNotFindMatchingFormatter"), p0, p1); + } + + /// + /// The {0} instance is not properly initialized. Use {1} to create an {0} for the current request. + /// + internal static string HttpRequestMessage_MustHaveHttpContext + { + get { return GetString("HttpRequestMessage_MustHaveHttpContext"); } + } + + /// + /// The {0} instance is not properly initialized. Use {1} to create an {0} for the current request. + /// + internal static string FormatHttpRequestMessage_MustHaveHttpContext(object p0, object p1) + { + return string.Format(CultureInfo.CurrentCulture, GetString("HttpRequestMessage_MustHaveHttpContext"), p0, p1); + } + /// /// The {0} only supports writing objects of type {1}. /// diff --git a/src/Microsoft.AspNet.Mvc.WebApiCompatShim/Resources.resx b/src/Microsoft.AspNet.Mvc.WebApiCompatShim/Resources.resx index a86ebf7b9a..e62964b509 100644 --- a/src/Microsoft.AspNet.Mvc.WebApiCompatShim/Resources.resx +++ b/src/Microsoft.AspNet.Mvc.WebApiCompatShim/Resources.resx @@ -117,6 +117,21 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + The request is invalid. + + + An error has occurred. + + + The model state is valid. + + + Could not find a formatter matching the media type '{0}' that can write an instance of '{1}'. + + + The {0} instance is not properly initialized. Use {1} to create an {0} for the current request. + The {0} only supports writing objects of type {1}. diff --git a/src/Microsoft.AspNet.Mvc.WebApiCompatShim/WebApiCompatShimServiceCollectionExtensions.cs b/src/Microsoft.AspNet.Mvc.WebApiCompatShim/WebApiCompatShimServiceCollectionExtensions.cs index 8ff52fb81a..c821107215 100644 --- a/src/Microsoft.AspNet.Mvc.WebApiCompatShim/WebApiCompatShimServiceCollectionExtensions.cs +++ b/src/Microsoft.AspNet.Mvc.WebApiCompatShim/WebApiCompatShimServiceCollectionExtensions.cs @@ -1,6 +1,7 @@ // 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.Net.Http.Formatting; using Microsoft.AspNet.Mvc.WebApiCompatShim; namespace Microsoft.Framework.DependencyInjection @@ -10,6 +11,11 @@ public static class WebApiCompatShimServiceCollectionExtensions public static IServiceCollection AddWebApiConventions(this IServiceCollection services) { services.AddOptionsAction(); + + // The constructors on DefaultContentNegotiator aren't DI friendly, so just + // new it up. + services.AddInstance(new DefaultContentNegotiator()); + return services; } } diff --git a/test/Microsoft.AspNet.Mvc.FunctionalTests/WebApiCompatShimBasicTest.cs b/test/Microsoft.AspNet.Mvc.FunctionalTests/WebApiCompatShimBasicTest.cs index b37a95c9c1..c9e2ff955b 100644 --- a/test/Microsoft.AspNet.Mvc.FunctionalTests/WebApiCompatShimBasicTest.cs +++ b/test/Microsoft.AspNet.Mvc.FunctionalTests/WebApiCompatShimBasicTest.cs @@ -7,7 +7,9 @@ using System.Net; using System.Net.Http; using System.Net.Http.Formatting; +using System.Net.Http.Headers; using System.Threading.Tasks; +using System.Web.Http; using Microsoft.AspNet.Builder; using Microsoft.AspNet.TestHost; using Newtonsoft.Json; @@ -223,6 +225,108 @@ public async Task ApiController_ResponseReturned_Chunked() Assert.Equal(new string[] { "Hello!" }, values); Assert.Equal(true, response.Headers.TransferEncodingChunked); } + + [Theory] + [InlineData("application/json", "application/json")] + [InlineData("text/xml", "text/xml")] + [InlineData("text/plain, text/xml; q=0.5", "text/xml")] + [InlineData("application/*", "application/json")] + public async Task ApiController_CreateResponse_Conneg(string accept, string mediaType) + { + // Arrange + var server = TestServer.Create(_provider, _app); + var client = server.CreateClient(); + + var request = new HttpRequestMessage( + HttpMethod.Get, + "http://localhost/api/Blog/HttpRequestMessage/GetUser"); + + request.Headers.Accept.ParseAdd(accept); + + // Act + var response = await client.SendAsync(request); + var user = await response.Content.ReadAsAsync(); + + // Assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.Equal("Test User", user.Name); + Assert.Equal(mediaType, response.Content.Headers.ContentType.MediaType); + } + + [Theory] + [InlineData("application/json")] + [InlineData("text/xml")] + public async Task ApiController_CreateResponse_HardcodedMediaType(string mediaType) + { + // Arrange + var server = TestServer.Create(_provider, _app); + var client = server.CreateClient(); + + var request = new HttpRequestMessage( + HttpMethod.Get, + "http://localhost/api/Blog/HttpRequestMessage/GetUser?mediaType=" + mediaType); + + // Act + var response = await client.SendAsync(request); + var user = await response.Content.ReadAsAsync(); + + // Assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.Equal("Test User", user.Name); + Assert.Equal(mediaType, response.Content.Headers.ContentType.MediaType); + } + + [Theory] + [InlineData("application/json", "application/json")] + [InlineData("text/xml", "text/xml")] + [InlineData("text/plain, text/xml; q=0.5", "text/xml")] + [InlineData("application/*", "application/json")] + public async Task ApiController_CreateResponse_Conneg_Error(string accept, string mediaType) + { + // Arrange + var server = TestServer.Create(_provider, _app); + var client = server.CreateClient(); + + var request = new HttpRequestMessage( + HttpMethod.Get, + "http://localhost/api/Blog/HttpRequestMessage/Fail"); + + request.Headers.Accept.ParseAdd(accept); + + // Act + var response = await client.SendAsync(request); + var error = await response.Content.ReadAsAsync(); + + // Assert + Assert.Equal(HttpStatusCode.InternalServerError, response.StatusCode); + Assert.Equal("It failed.", error.Message); + Assert.Equal(mediaType, response.Content.Headers.ContentType.MediaType); + } + + + [Fact] + public async Task ApiController_CreateResponse_HardcodedFormatter() + { + // Arrange + var server = TestServer.Create(_provider, _app); + var client = server.CreateClient(); + + var request = new HttpRequestMessage( + HttpMethod.Get, + "http://localhost/api/Blog/HttpRequestMessage/GetUserJson"); + + // Accept header will be ignored + request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("text/xml")); + + // Act + var response = await client.SendAsync(request); + var user = await response.Content.ReadAsAsync(); + + // Assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.Equal("Test User", user.Name); + Assert.Equal("text/json", response.Content.Headers.ContentType.MediaType); + } } } #endif \ No newline at end of file diff --git a/test/Microsoft.AspNet.Mvc.WebApiCompatShimTest/HttpErrorTest.cs b/test/Microsoft.AspNet.Mvc.WebApiCompatShimTest/HttpErrorTest.cs new file mode 100644 index 0000000000..3a30640193 --- /dev/null +++ b/test/Microsoft.AspNet.Mvc.WebApiCompatShimTest/HttpErrorTest.cs @@ -0,0 +1,285 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information. + +using System.Collections.Generic; +using System.IO; +using System.Net.Http.Formatting; +using Newtonsoft.Json.Linq; +using Xunit; +using Microsoft.AspNet.Mvc.ModelBinding; + +namespace System.Web.Http.Dispatcher +{ + public class HttpErrorTest + { + public static IEnumerable ErrorKeyValue + { + get + { + var httpError = new HttpError(); + yield return new object[] { httpError, (Func)(() => httpError.Message), "Message", "Message_Value" }; + yield return new object[] { httpError, (Func)(() => httpError.MessageDetail), "MessageDetail", "MessageDetail_Value" }; + yield return new object[] { httpError, (Func)(() => httpError.ExceptionMessage), "ExceptionMessage", "ExceptionMessage_Value" }; + yield return new object[] { httpError, (Func)(() => httpError.ExceptionType), "ExceptionType", "ExceptionType_Value" }; + yield return new object[] { httpError, (Func)(() => httpError.StackTrace), "StackTrace", "StackTrace_Value" }; + } + } + + public static IEnumerable HttpErrors + { + get + { + yield return new[] { new HttpError() }; + yield return new[] { new HttpError("error") }; + yield return new[] { new HttpError(new NotImplementedException(), true) }; + yield return new[] { new HttpError(new ModelStateDictionary() { { "key", new ModelState() { Errors = { new ModelError("error") } } } }, true) }; + } + } + + [Fact] + public void StringConstructor_AddsCorrectDictionaryItems() + { + HttpError error = new HttpError("something bad happened"); + + Assert.Contains(new KeyValuePair("Message", "something bad happened"), error); + } + + [Fact] + public void ExceptionConstructorWithDetail_AddsCorrectDictionaryItems() + { + HttpError error = new HttpError(new ArgumentException("error", new Exception()), true); + + Assert.Contains(new KeyValuePair("Message", "An error has occurred."), error); + Assert.Contains(new KeyValuePair("ExceptionMessage", "error"), error); + Assert.Contains(new KeyValuePair("ExceptionType", "System.ArgumentException"), error); + Assert.True(error.ContainsKey("StackTrace")); + Assert.True(error.ContainsKey("InnerException")); + Assert.IsType(error["InnerException"]); + } + + [Fact] + public void ModelStateConstructorWithDetail_AddsCorrectDictionaryItems() + { + ModelStateDictionary modelState = new ModelStateDictionary(); + modelState.AddModelError("[0].Name", "error1"); + modelState.AddModelError("[0].Name", "error2"); + modelState.AddModelError("[0].Address", "error"); + modelState.AddModelError("[2].Name", new Exception("OH NO")); + + HttpError error = new HttpError(modelState, true); + HttpError modelStateError = error["ModelState"] as HttpError; + + Assert.Contains(new KeyValuePair("Message", "The request is invalid."), error); + Assert.Contains("error1", modelStateError["[0].Name"] as IEnumerable); + Assert.Contains("error2", modelStateError["[0].Name"] as IEnumerable); + Assert.Contains("error", modelStateError["[0].Address"] as IEnumerable); + Assert.True(modelStateError.ContainsKey("[2].Name")); + Assert.Contains("OH NO", modelStateError["[2].Name"] as IEnumerable); + } + + [Fact] + public void ExceptionConstructorWithoutDetail_AddsCorrectDictionaryItems() + { + HttpError error = new HttpError(new ArgumentException("error", new Exception()), false); + + Assert.Contains(new KeyValuePair("Message", "An error has occurred."), error); + Assert.False(error.ContainsKey("ExceptionMessage")); + Assert.False(error.ContainsKey("ExceptionType")); + Assert.False(error.ContainsKey("StackTrace")); + Assert.False(error.ContainsKey("InnerException")); + } + + [Fact] + public void ModelStateConstructorWithoutDetail_AddsCorrectDictionaryItems() + { + ModelStateDictionary modelState = new ModelStateDictionary(); + modelState.AddModelError("[0].Name", "error1"); + modelState.AddModelError("[0].Name", "error2"); + modelState.AddModelError("[0].Address", "error"); + modelState.AddModelError("[2].Name", new Exception("OH NO")); + + HttpError error = new HttpError(modelState, false); + HttpError modelStateError = error["ModelState"] as HttpError; + + Assert.Contains(new KeyValuePair("Message", "The request is invalid."), error); + Assert.Contains("error1", modelStateError["[0].Name"] as IEnumerable); + Assert.Contains("error2", modelStateError["[0].Name"] as IEnumerable); + Assert.Contains("error", modelStateError["[0].Address"] as IEnumerable); + Assert.True(modelStateError.ContainsKey("[2].Name")); + Assert.DoesNotContain("OH NO", modelStateError["[2].Name"] as IEnumerable); + } + + [Fact] + public void HttpError_Roundtrips_WithJsonFormatter() + { + HttpError error = new HttpError("error") { { "ErrorCode", 42 }, { "Data", new[] { "a", "b", "c" } } }; + MediaTypeFormatter formatter = new JsonMediaTypeFormatter(); + MemoryStream stream = new MemoryStream(); + + formatter.WriteToStreamAsync(typeof(HttpError), error, stream, content: null, transportContext: null).Wait(); + stream.Position = 0; + HttpError roundtrippedError = formatter.ReadFromStreamAsync(typeof(HttpError), stream, content: null, formatterLogger: null).Result as HttpError; + + Assert.NotNull(roundtrippedError); + Assert.Equal("error", roundtrippedError.Message); + Assert.Equal(42L, roundtrippedError["ErrorCode"]); + JArray data = roundtrippedError["Data"] as JArray; + Assert.Equal(3, data.Count); + Assert.Contains("a", data); + Assert.Contains("b", data); + Assert.Contains("c", data); + } + + [Fact] + public void HttpError_Roundtrips_WithXmlFormatter() + { + HttpError error = new HttpError("error") { { "ErrorCode", 42 }, { "Data", new[] { "a", "b", "c" } } }; + MediaTypeFormatter formatter = new XmlMediaTypeFormatter(); + MemoryStream stream = new MemoryStream(); + + formatter.WriteToStreamAsync(typeof(HttpError), error, stream, content: null, transportContext: null).Wait(); + stream.Position = 0; + HttpError roundtrippedError = formatter.ReadFromStreamAsync(typeof(HttpError), stream, content: null, formatterLogger: null).Result as HttpError; + + Assert.NotNull(roundtrippedError); + Assert.Equal("error", roundtrippedError.Message); + Assert.Equal("42", roundtrippedError["ErrorCode"]); + Assert.Equal("a b c", roundtrippedError["Data"]); + } + + [Fact] + public void HttpErrorWithWhitespace_Roundtrips_WithXmlFormatter() + { + string message = " foo\n bar \n "; + HttpError error = new HttpError(message); + MediaTypeFormatter formatter = new XmlMediaTypeFormatter(); + MemoryStream stream = new MemoryStream(); + + formatter.WriteToStreamAsync(typeof(HttpError), error, stream, content: null, transportContext: null).Wait(); + stream.Position = 0; + HttpError roundtrippedError = formatter.ReadFromStreamAsync(typeof(HttpError), stream, content: null, formatterLogger: null).Result as HttpError; + + Assert.NotNull(roundtrippedError); + Assert.Equal(message, roundtrippedError.Message); + } + + [Fact] + public void HttpError_Roundtrips_WithXmlSerializer() + { + HttpError error = new HttpError("error") { { "ErrorCode", 42 }, { "Data", new[] { "a", "b", "c" } } }; + MediaTypeFormatter formatter = new XmlMediaTypeFormatter() { UseXmlSerializer = true }; + MemoryStream stream = new MemoryStream(); + + formatter.WriteToStreamAsync(typeof(HttpError), error, stream, content: null, transportContext: null).Wait(); + stream.Position = 0; + HttpError roundtrippedError = formatter.ReadFromStreamAsync(typeof(HttpError), stream, content: null, formatterLogger: null).Result as HttpError; + + Assert.NotNull(roundtrippedError); + Assert.Equal("error", roundtrippedError.Message); + Assert.Equal("42", roundtrippedError["ErrorCode"]); + Assert.Equal("a b c", roundtrippedError["Data"]); + } + + [Fact] + public void HttpErrorForInnerException_Serializes_WithXmlSerializer() + { + HttpError error = new HttpError(new ArgumentException("error", new Exception("innerError")), includeErrorDetail: true); + MediaTypeFormatter formatter = new XmlMediaTypeFormatter() { UseXmlSerializer = true }; + MemoryStream stream = new MemoryStream(); + + formatter.WriteToStreamAsync(typeof(HttpError), error, stream, content: null, transportContext: null).Wait(); + stream.Position = 0; + string serializedError = new StreamReader(stream).ReadToEnd(); + + Assert.NotNull(serializedError); + Assert.Equal( + "An error has occurred.errorSystem.ArgumentExceptionAn error has occurred.innerErrorSystem.Exception", + serializedError); + } + + [Fact] + public void GetPropertyValue_GetsValue_IfTypeMatches() + { + HttpError error = new HttpError(); + error["key"] = "x"; + + Assert.Equal("x", error.GetPropertyValue("key")); + Assert.Equal("x", error.GetPropertyValue("key")); + } + + [Fact] + public void GetPropertyValue_GetsDefault_IfTypeDoesNotMatch() + { + HttpError error = new HttpError(); + error["key"] = "x"; + + Assert.Null(error.GetPropertyValue("key")); + Assert.Equal(0, error.GetPropertyValue("key")); + } + + [Fact] + public void GetPropertyValue_GetsDefault_IfPropertyMissing() + { + HttpError error = new HttpError(); + + Assert.Null(error.GetPropertyValue("key")); + Assert.Equal(0, error.GetPropertyValue("key")); + } + + [Theory] + [MemberData("ErrorKeyValue")] + public void HttpErrorStringProperties_UseCorrectHttpErrorKey(HttpError httpError, Func productUnderTest, string key, string actualValue) + { + // Arrange + httpError[key] = actualValue; + + // Act + string expectedValue = productUnderTest.Invoke(); + + // Assert + Assert.Equal(expectedValue, actualValue); + } + + [Fact] + public void HttpErrorProperty_InnerException_UsesCorrectHttpErrorKey() + { + // Arrange + HttpError error = new HttpError(new ArgumentException("error", new Exception()), true); + + // Act + HttpError innerException = error.InnerException; + + // Assert + Assert.Same(error["InnerException"], innerException); + } + + [Fact] + public void HttpErrorProperty_ModelState_UsesCorrectHttpErrorKey() + { + // Arrange + ModelStateDictionary modelState = new ModelStateDictionary(); + modelState.AddModelError("[0].Name", "error1"); + HttpError error = new HttpError(modelState, true); + + // Act + HttpError actualModelStateError = error.ModelState; + + // Assert + Assert.Same(error["ModelState"], actualModelStateError); + } + + [Theory] + [MemberData("HttpErrors")] + public void HttpErrors_UseCaseInsensitiveComparer(HttpError httpError) + { + // Arrange + var lowercaseKey = "abcd"; + var uppercaseKey = "ABCD"; + + httpError[lowercaseKey] = "error"; + + // Act & Assert + Assert.True(httpError.ContainsKey(lowercaseKey)); + Assert.True(httpError.ContainsKey(uppercaseKey)); + } + } +} diff --git a/test/Microsoft.AspNet.Mvc.WebApiCompatShimTest/HttpRequestMessage/HttpRequestMessageExtensionsTest.cs b/test/Microsoft.AspNet.Mvc.WebApiCompatShimTest/HttpRequestMessage/HttpRequestMessageExtensionsTest.cs new file mode 100644 index 0000000000..6996b3245a --- /dev/null +++ b/test/Microsoft.AspNet.Mvc.WebApiCompatShimTest/HttpRequestMessage/HttpRequestMessageExtensionsTest.cs @@ -0,0 +1,347 @@ +// 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.Collections.Generic; +using System.Net.Http.Formatting; +using System.Net.Http.Headers; +using Microsoft.AspNet.Http; +using Microsoft.AspNet.Mvc.WebApiCompatShim; +using Microsoft.AspNet.PipelineCore; +using Microsoft.Framework.OptionsModel; +using Moq; +using Xunit; + +namespace System.Net.Http +{ + public class HttpRequestMessageExtensionsTest + { + [Fact] + public void CreateResponse_DoingConneg_OnlyContent_RetrievesContentNegotiatorFromServices() + { + // Arrange + var context = new DefaultHttpContext(); + + var services = new Mock(); + services + .Setup(s => s.GetService(typeof(IContentNegotiator))) + .Returns(Mock.Of()) + .Verifiable(); + + var options = new WebApiCompatShimOptions(); + options.Formatters.AddRange(new MediaTypeFormatterCollection()); + + var optionsAccessor = new Mock>(); + optionsAccessor.SetupGet(o => o.Options).Returns(options); + + services + .Setup(s => s.GetService(typeof(IOptionsAccessor))) + .Returns(optionsAccessor.Object); + + context.RequestServices = services.Object; + + var request = CreateRequest(context); + + // Act + request.CreateResponse(CreateValue()); + + // Assert + services.Verify(); + } + + [Fact] + public void CreateResponse_DoingConneg_RetrievesContentNegotiatorFromServices() + { + // Arrange + var context = new DefaultHttpContext(); + + var services = new Mock(); + services + .Setup(s => s.GetService(typeof(IContentNegotiator))) + .Returns(Mock.Of()) + .Verifiable(); + + var options = new WebApiCompatShimOptions(); + options.Formatters.AddRange(new MediaTypeFormatterCollection()); + + var optionsAccessor = new Mock>(); + optionsAccessor.SetupGet(o => o.Options).Returns(options); + + services + .Setup(s => s.GetService(typeof(IOptionsAccessor))) + .Returns(optionsAccessor.Object); + + context.RequestServices = services.Object; + + var request = CreateRequest(context); + + // Act + request.CreateResponse(HttpStatusCode.OK, CreateValue()); + + // Assert + services.Verify(); + } + + [Fact] + public void CreateResponse_DoingConneg_PerformsContentNegotiationAndCreatesContentUsingResults() + { + // Arrange + var context = new DefaultHttpContext(); + + var formatter = new XmlMediaTypeFormatter(); + + var contentNegotiator = new Mock(); + contentNegotiator + .Setup(c => c.Negotiate(It.IsAny(), It.IsAny(), It.IsAny>())) + .Returns(new ContentNegotiationResult(formatter, mediaType: null)); + + context.RequestServices = CreateServices(contentNegotiator.Object, formatter); + + var request = CreateRequest(context); + + // Act + var response = request.CreateResponse(HttpStatusCode.NoContent, "42"); + + // Assert + Assert.Equal(HttpStatusCode.NoContent, response.StatusCode); + Assert.Same(request, response.RequestMessage); + + var objectContent = Assert.IsType>(response.Content); + Assert.Equal("42", objectContent.Value); + Assert.Same(formatter, objectContent.Formatter); + } + + + [Fact] + public void CreateResponse_MatchingMediaType_WhenMediaTypeStringIsInvalidFormat_Throws() + { + HttpRequestMessage request = CreateRequest(new DefaultHttpContext()); + + var ex = Assert.Throws( + () => request.CreateResponse(HttpStatusCode.OK, CreateValue(), "foo/bar; param=value")); + + Assert.Equal("The format of value 'foo/bar; param=value' is invalid.", ex.Message); + } + + [Fact] + public void CreateResponse_MatchingMediaType_WhenRequestDoesNotHaveHttpContextThrows() + { + HttpRequestMessage request = CreateRequest(null); + + // Arrange + + // Act + var ex = Assert.Throws( + () => request.CreateResponse(HttpStatusCode.OK, CreateValue(), mediaType: "foo/bar")); + + Assert.Equal( + "The HttpRequestMessage instance is not properly initialized. " + + "Use HttpRequestMessageHttpContextExtensions.GetHttpRequestMessage to create an HttpRequestMessage " + + "for the current request.", + ex.Message); + } + + [Fact] + public void CreateResponse_MatchingMediaType_WhenMediaTypeDoesNotMatch_Throws() + { + // Arrange + var context = new DefaultHttpContext(); + context.RequestServices = CreateServices(new DefaultContentNegotiator()); + + var request = CreateRequest(context); + + // Act + var ex = Assert.Throws( + () => request.CreateResponse(HttpStatusCode.OK, CreateValue(), mediaType: "foo/bar")); + Assert.Equal( + "Could not find a formatter matching the media type 'foo/bar' that can write an instance of 'System.Object'.", + ex.Message); + } + + [Fact] + public void CreateResponse_MatchingMediaType_FindsMatchingFormatterAndCreatesResponse() + { + // Arrange + var context = new DefaultHttpContext(); + + var formatter = new Mock { CallBase = true }; + formatter.Setup(f => f.CanWriteType(typeof(object))).Returns(true).Verifiable(); + formatter.Object.SupportedMediaTypes.Add(new MediaTypeHeaderValue("foo/bar")); + + context.RequestServices = CreateServices(new DefaultContentNegotiator(), formatter.Object); + + var expectedValue = CreateValue(); + + var request = CreateRequest(context); + + // Act + var response = request.CreateResponse(HttpStatusCode.Gone, expectedValue, mediaType: "foo/bar"); + + // Assert + Assert.Equal(HttpStatusCode.Gone, response.StatusCode); + var content = Assert.IsType>(response.Content); + Assert.Same(expectedValue, content.Value); + Assert.Same(formatter.Object, content.Formatter); + Assert.Equal("foo/bar", content.Headers.ContentType.MediaType); + formatter.Verify(); + } + + [Fact] + public void CreateResponse_AcceptingFormatter_CreatesResponseWithDefaultMediaType() + { + // Arrange + var context = new DefaultHttpContext(); + + var formatter = new Mock() { CallBase = true }; + formatter + .Setup(f => f.CanWriteType(typeof(object))) + .Returns(true) + .Verifiable(); + formatter + .Setup(f => f.SetDefaultContentHeaders(typeof(object), It.IsAny(), It.IsAny())) + .Callback(SetMediaType) + .Verifiable(); + + formatter.Object.SupportedMediaTypes.Add(new MediaTypeHeaderValue("foo/bar")); + + var expectedValue = CreateValue(); + + var request = CreateRequest(context); + + // Act + var response = request.CreateResponse( + HttpStatusCode.MultipleChoices, + expectedValue, + formatter.Object, + mediaType: (string)null); + + // Assert + Assert.Equal(HttpStatusCode.MultipleChoices, response.StatusCode); + var content = Assert.IsType>(response.Content); + Assert.Same(expectedValue, content.Value); + Assert.Same(formatter.Object, content.Formatter); + Assert.Equal("foo/bar", content.Headers.ContentType.MediaType); + + formatter.Verify(); + } + + private static void SetMediaType(Type type, HttpContentHeaders headers, MediaTypeHeaderValue value) + { + headers.ContentType = new MediaTypeHeaderValue("foo/bar"); + } + + [Fact] + public void CreateResponse_AcceptingFormatter_WithOverridenMediaTypeString_CreatesResponse() + { + // Arrange + var context = new DefaultHttpContext(); + + var formatter = new Mock { CallBase = true }; + formatter.Setup(f => f.CanWriteType(typeof(object))).Returns(true).Verifiable(); + formatter.Object.SupportedMediaTypes.Add(new MediaTypeHeaderValue("foo/bar")); + + var expectedValue = CreateValue(); + + var request = CreateRequest(context); + + // Act + var response = request.CreateResponse( + HttpStatusCode.MultipleChoices, + CreateValue(), + formatter.Object, + mediaType: "bin/baz"); + + // Assert + Assert.Equal("bin/baz", response.Content.Headers.ContentType.MediaType); + } + + [Fact] + public void CreateResponse_AcceptingFormatter_WithOverridenMediaTypeHeader_CreatesResponse() + { + // Arrange + var context = new DefaultHttpContext(); + + var formatter = new Mock { CallBase = true }; + formatter.Setup(f => f.CanWriteType(typeof(object))).Returns(true).Verifiable(); + formatter.Object.SupportedMediaTypes.Add(new MediaTypeHeaderValue("foo/bar")); + + var expectedValue = CreateValue(); + + var request = CreateRequest(context); + + // Act + var response = request.CreateResponse( + HttpStatusCode.MultipleChoices, + CreateValue(), + formatter.Object, + mediaType: new MediaTypeHeaderValue("bin/baz")); + + // Assert + Assert.Equal("bin/baz", response.Content.Headers.ContentType.MediaType); + } + + [Fact] + public void CreateErrorResponseRangeNotSatisfiable_SetsCorrectStatusCodeAndContentRangeHeader() + { + // Arrange + var context = new DefaultHttpContext(); + context.RequestServices = CreateServices(new DefaultContentNegotiator()); + + var request = CreateRequest(context); + + var expectedContentRange = new ContentRangeHeaderValue(length: 128); + var invalidByteRangeException = new InvalidByteRangeException(expectedContentRange); + + // Act + var response = request.CreateErrorResponse(invalidByteRangeException); + + // Assert + Assert.Equal(HttpStatusCode.RequestedRangeNotSatisfiable, response.StatusCode); + Assert.Same(expectedContentRange, response.Content.Headers.ContentRange); + } + + private static HttpRequestMessage CreateRequest(HttpContext context) + { + var request = new HttpRequestMessage(); + request.Properties.Add(nameof(HttpContext), context); + return request; + } + + private static object CreateValue() + { + return new object(); + } + + private static IServiceProvider CreateServices( + IContentNegotiator contentNegotiator = null, + MediaTypeFormatter formatter = null) + { + var options = new WebApiCompatShimOptions(); + + if (formatter == null) + { + options.Formatters.AddRange(new MediaTypeFormatterCollection()); + } + else + { + options.Formatters.Add(formatter); + } + + var optionsAccessor = new Mock>(); + optionsAccessor.SetupGet(o => o.Options).Returns(options); + + var services = new Mock(MockBehavior.Strict); + services + .Setup(s => s.GetService(typeof(IOptionsAccessor))) + .Returns(optionsAccessor.Object); + + if (contentNegotiator != null) + { + services + .Setup(s => s.GetService(typeof(IContentNegotiator))) + .Returns(contentNegotiator); + } + + return services.Object; + } + } +} diff --git a/test/WebSites/WebApiCompatShimWebSite/Controllers/HttpRequestMessage/HttpRequestMessageController.cs b/test/WebSites/WebApiCompatShimWebSite/Controllers/HttpRequestMessage/HttpRequestMessageController.cs index c4b654c5ee..a46eca249c 100644 --- a/test/WebSites/WebApiCompatShimWebSite/Controllers/HttpRequestMessage/HttpRequestMessageController.cs +++ b/test/WebSites/WebApiCompatShimWebSite/Controllers/HttpRequestMessage/HttpRequestMessageController.cs @@ -8,6 +8,7 @@ using Microsoft.AspNet.Http; using Microsoft.AspNet.Mvc; using System.Net; +using System.Net.Http.Formatting; namespace WebApiCompatShimWebSite { @@ -16,7 +17,7 @@ public class HttpRequestMessageController : ApiController public async Task EchoProperty() { await Echo(Request); - return new EmptyResult(); + return new EmptyResult(); } public async Task EchoParameter(HttpRequestMessage request) @@ -33,8 +34,8 @@ public async Task EchoParameter(HttpRequestMessage request) public async Task EchoWithResponseMessage(HttpRequestMessage request) { var message = string.Format( - "{0} {1}", - request.Method.ToString(), + "{0} {1}", + request.Method.ToString(), await request.Content.ReadAsStringAsync()); var response = request.CreateResponse(HttpStatusCode.OK); @@ -57,6 +58,42 @@ public async Task EchoWithResponseMessageChunked(HttpReques return response; } + public HttpResponseMessage GetUser(string mediaType = null) + { + var user = new User() + { + Name = "Test User", + }; + + if (mediaType == null) + { + // This will perform content negotation + return Request.CreateResponse(HttpStatusCode.OK, user); + } + else + { + // This will use the provided media type + return Request.CreateResponse(HttpStatusCode.OK, user, mediaType); + } + } + + public HttpResponseMessage GetUserJson() + { + var user = new User() + { + Name = "Test User", + }; + + return Request.CreateResponse(HttpStatusCode.OK, user, new JsonMediaTypeFormatter(), "text/json"); + } + + [HttpGet] + public HttpResponseMessage Fail() + { + // This will perform content negotation + return Request.CreateErrorResponse(HttpStatusCode.InternalServerError, "It failed."); + } + private async Task Echo(HttpRequestMessage request) { var message = string.Format( diff --git a/test/WebSites/WebApiCompatShimWebSite/Models/User.cs b/test/WebSites/WebApiCompatShimWebSite/Models/User.cs index 50df5ddc90..7e6db5217d 100644 --- a/test/WebSites/WebApiCompatShimWebSite/Models/User.cs +++ b/test/WebSites/WebApiCompatShimWebSite/Models/User.cs @@ -7,5 +7,6 @@ namespace WebApiCompatShimWebSite { public class User { + public string Name { get; set; } } } \ No newline at end of file From 9ad3d5e68fed5f13cca4d7407a388612207f2041 Mon Sep 17 00:00:00 2001 From: Ryan Nowak Date: Fri, 10 Oct 2014 12:57:06 -0700 Subject: [PATCH 12/39] Separate ApiControllers from MVC Controllers in routing --- .../ApiController.cs | 1 + .../Conventions/IUseWebApiRoutes.cs | 9 +++ .../Conventions/UseWebApiRoutesAttribute.cs | 12 ++++ .../WebApiRoutesGlobalModelConvention.cs | 39 +++++++++++ .../Routing/RouteBuilderExtensions.cs | 69 +++++++++++++++++++ .../WebApiCompatShimOptionsSetup.cs | 3 + .../WebApiCompatShimBasicTest.cs | 39 ++++++++++- .../ApiControllerActionDiscoveryTest.cs | 30 +++++++- .../Controllers/MvcController.cs | 16 +++++ .../WebApiCompatShimWebSite/Startup.cs | 12 ++-- 10 files changed, 221 insertions(+), 9 deletions(-) create mode 100644 src/Microsoft.AspNet.Mvc.WebApiCompatShim/Conventions/IUseWebApiRoutes.cs create mode 100644 src/Microsoft.AspNet.Mvc.WebApiCompatShim/Conventions/UseWebApiRoutesAttribute.cs create mode 100644 src/Microsoft.AspNet.Mvc.WebApiCompatShim/Conventions/WebApiRoutesGlobalModelConvention.cs create mode 100644 src/Microsoft.AspNet.Mvc.WebApiCompatShim/Routing/RouteBuilderExtensions.cs create mode 100644 test/WebSites/WebApiCompatShimWebSite/Controllers/MvcController.cs diff --git a/src/Microsoft.AspNet.Mvc.WebApiCompatShim/ApiController.cs b/src/Microsoft.AspNet.Mvc.WebApiCompatShim/ApiController.cs index efcefa8f32..7a503c84bf 100644 --- a/src/Microsoft.AspNet.Mvc.WebApiCompatShim/ApiController.cs +++ b/src/Microsoft.AspNet.Mvc.WebApiCompatShim/ApiController.cs @@ -11,6 +11,7 @@ namespace System.Web.Http { + [UseWebApiRoutes] [UseWebApiActionConventions] [UseWebApiOverloading] public abstract class ApiController : IDisposable diff --git a/src/Microsoft.AspNet.Mvc.WebApiCompatShim/Conventions/IUseWebApiRoutes.cs b/src/Microsoft.AspNet.Mvc.WebApiCompatShim/Conventions/IUseWebApiRoutes.cs new file mode 100644 index 0000000000..51dae1638c --- /dev/null +++ b/src/Microsoft.AspNet.Mvc.WebApiCompatShim/Conventions/IUseWebApiRoutes.cs @@ -0,0 +1,9 @@ +// 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.WebApiCompatShim +{ + public interface IUseWebApiRoutes + { + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Mvc.WebApiCompatShim/Conventions/UseWebApiRoutesAttribute.cs b/src/Microsoft.AspNet.Mvc.WebApiCompatShim/Conventions/UseWebApiRoutesAttribute.cs new file mode 100644 index 0000000000..cfdf5f7fe0 --- /dev/null +++ b/src/Microsoft.AspNet.Mvc.WebApiCompatShim/Conventions/UseWebApiRoutesAttribute.cs @@ -0,0 +1,12 @@ +// 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; + +namespace Microsoft.AspNet.Mvc.WebApiCompatShim +{ + [AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = true)] + public class UseWebApiRoutesAttribute : Attribute, IUseWebApiRoutes + { + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Mvc.WebApiCompatShim/Conventions/WebApiRoutesGlobalModelConvention.cs b/src/Microsoft.AspNet.Mvc.WebApiCompatShim/Conventions/WebApiRoutesGlobalModelConvention.cs new file mode 100644 index 0000000000..e9dff1f49f --- /dev/null +++ b/src/Microsoft.AspNet.Mvc.WebApiCompatShim/Conventions/WebApiRoutesGlobalModelConvention.cs @@ -0,0 +1,39 @@ +// 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.Linq; +using Microsoft.AspNet.Mvc.ApplicationModel; + +namespace Microsoft.AspNet.Mvc.WebApiCompatShim +{ + public class WebApiRoutesGlobalModelConvention : IGlobalModelConvention + { + private readonly string _area; + + public WebApiRoutesGlobalModelConvention(string area) + { + _area = area; + } + + public void Apply(GlobalModel model) + { + foreach (var controller in model.Controllers) + { + if (IsConventionApplicable(controller)) + { + Apply(controller); + } + } + } + + private bool IsConventionApplicable(ControllerModel controller) + { + return controller.Attributes.OfType().Any(); + } + + private void Apply(ControllerModel model) + { + model.RouteConstraints.Add(new AreaAttribute(_area)); + } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Mvc.WebApiCompatShim/Routing/RouteBuilderExtensions.cs b/src/Microsoft.AspNet.Mvc.WebApiCompatShim/Routing/RouteBuilderExtensions.cs new file mode 100644 index 0000000000..7cf2cadbd2 --- /dev/null +++ b/src/Microsoft.AspNet.Mvc.WebApiCompatShim/Routing/RouteBuilderExtensions.cs @@ -0,0 +1,69 @@ +// 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.Collections.Generic; +using Microsoft.AspNet.Mvc.WebApiCompatShim; +using Microsoft.AspNet.Routing.Constraints; + +namespace Microsoft.AspNet.Routing +{ + public static class RouteBuilderExtensions + { + public static IRouteBuilder MapWebApiRoute( + this IRouteBuilder routeCollectionBuilder, + string name, + string template) + { + return MapWebApiRoute(routeCollectionBuilder, name, template, defaults: null); + } + + public static IRouteBuilder MapWebApiRoute( + this IRouteBuilder routeCollectionBuilder, + string name, + string template, + object defaults) + { + return MapWebApiRoute(routeCollectionBuilder, name, template, defaults, constraints: null); + } + + public static IRouteBuilder MapWebApiRoute( + this IRouteBuilder routeCollectionBuilder, + string name, + string template, + object defaults, + object constraints) + { + return MapWebApiRoute(routeCollectionBuilder, name, template, defaults, constraints, dataTokens: null); + } + + public static IRouteBuilder MapWebApiRoute( + this IRouteBuilder routeCollectionBuilder, + string name, + string template, + object defaults, + object constraints, + object dataTokens) + { + var mutableDefaults = ObjectToDictionary(defaults); + mutableDefaults.Add("area", WebApiCompatShimOptionsSetup.DefaultAreaName); + + var mutableConstraints = ObjectToDictionary(constraints); + mutableConstraints.Add("area", new RequiredRouteConstraint()); + + return routeCollectionBuilder.MapRoute(name, template, mutableDefaults, mutableConstraints, dataTokens); + } + + private static IDictionary ObjectToDictionary(object value) + { + var dictionary = value as IDictionary; + if (dictionary != null) + { + return new RouteValueDictionary(dictionary); + } + else + { + return new RouteValueDictionary(value); + } + } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Mvc.WebApiCompatShim/WebApiCompatShimOptionsSetup.cs b/src/Microsoft.AspNet.Mvc.WebApiCompatShim/WebApiCompatShimOptionsSetup.cs index 142a66d2ad..6d3f576bb5 100644 --- a/src/Microsoft.AspNet.Mvc.WebApiCompatShim/WebApiCompatShimOptionsSetup.cs +++ b/src/Microsoft.AspNet.Mvc.WebApiCompatShim/WebApiCompatShimOptionsSetup.cs @@ -8,6 +8,8 @@ namespace Microsoft.AspNet.Mvc.WebApiCompatShim { public class WebApiCompatShimOptionsSetup : IOptionsAction, IOptionsAction { + public readonly static string DefaultAreaName = "api"; + public int Order { // We want to run after the default MvcOptionsSetup. @@ -21,6 +23,7 @@ public void Invoke(MvcOptions options) // Add webapi behaviors to controllers with the appropriate attributes options.ApplicationModelConventions.Add(new WebApiActionConventionsGlobalModelConvention()); options.ApplicationModelConventions.Add(new WebApiOverloadingGlobalModelConvention()); + options.ApplicationModelConventions.Add(new WebApiRoutesGlobalModelConvention(area: DefaultAreaName)); // Add a model binder to be able to bind HttpRequestMessage options.ModelBinders.Insert(0, new HttpRequestMessageModelBinder()); diff --git a/test/Microsoft.AspNet.Mvc.FunctionalTests/WebApiCompatShimBasicTest.cs b/test/Microsoft.AspNet.Mvc.FunctionalTests/WebApiCompatShimBasicTest.cs index c9e2ff955b..77b819b3b4 100644 --- a/test/Microsoft.AspNet.Mvc.FunctionalTests/WebApiCompatShimBasicTest.cs +++ b/test/Microsoft.AspNet.Mvc.FunctionalTests/WebApiCompatShimBasicTest.cs @@ -303,7 +303,6 @@ public async Task ApiController_CreateResponse_Conneg_Error(string accept, strin Assert.Equal(mediaType, response.Content.Headers.ContentType.MediaType); } - [Fact] public async Task ApiController_CreateResponse_HardcodedFormatter() { @@ -327,6 +326,42 @@ public async Task ApiController_CreateResponse_HardcodedFormatter() Assert.Equal("Test User", user.Name); Assert.Equal("text/json", response.Content.Headers.ContentType.MediaType); } + + [Theory] + [InlineData("http://localhost/Mvc/Index", HttpStatusCode.OK)] + [InlineData("http://localhost/api/Blog/Mvc/Index", HttpStatusCode.NotFound)] + public async Task WebApiRouting_AccessMvcController(string url, HttpStatusCode expected) + { + // Arrange + var server = TestServer.Create(_provider, _app); + var client = server.CreateClient(); + + var request = new HttpRequestMessage(HttpMethod.Get, url); + + // Act + var response = await client.SendAsync(request); + + // Assert + Assert.Equal(expected, response.StatusCode); + } + + [Theory] + [InlineData("http://localhost/BasicApi/GenerateUrl", HttpStatusCode.NotFound)] + [InlineData("http://localhost/api/Blog/BasicApi/GenerateUrl", HttpStatusCode.OK)] + public async Task WebApiRouting_AccessWebApiController(string url, HttpStatusCode expected) + { + // Arrange + var server = TestServer.Create(_provider, _app); + var client = server.CreateClient(); + + var request = new HttpRequestMessage(HttpMethod.Get, url); + + // Act + var response = await client.SendAsync(request); + + // Assert + Assert.Equal(expected, response.StatusCode); + } } } -#endif \ No newline at end of file +#endif diff --git a/test/Microsoft.AspNet.Mvc.WebApiCompatShimTest/ApiControllerActionDiscoveryTest.cs b/test/Microsoft.AspNet.Mvc.WebApiCompatShimTest/ApiControllerActionDiscoveryTest.cs index 73a7f9b17c..7fbd35ed9c 100644 --- a/test/Microsoft.AspNet.Mvc.WebApiCompatShimTest/ApiControllerActionDiscoveryTest.cs +++ b/test/Microsoft.AspNet.Mvc.WebApiCompatShimTest/ApiControllerActionDiscoveryTest.cs @@ -225,6 +225,31 @@ public void GetActions_AllWebApiActionsAreOverloaded() } } + [Fact] + public void GetActions_AllWebApiActionsAreInWebApiArea() + { + // Arrange + var provider = CreateProvider(); + + // Act + var context = new ActionDescriptorProviderContext(); + provider.Invoke(context); + + var results = context.Results.Cast(); + + // Assert + var controllerType = typeof(TestControllers.StoreController).GetTypeInfo(); + var actions = results + .Where(ad => ad.ControllerDescriptor.ControllerTypeInfo == controllerType) + .ToArray(); + + Assert.NotEmpty(actions); + foreach (var action in actions) + { + Assert.Single(action.RouteConstraints, c => c.RouteKey == "area" && c.RouteValue == "api"); + } + } + private INestedProviderManager CreateProvider() { var assemblyProvider = new Mock(); @@ -240,8 +265,9 @@ private INestedProviderManager CreateProvider() var conventions = new NamespaceLimitedActionDiscoveryConventions(); var options = new MvcOptions(); - options.ApplicationModelConventions.Add(new WebApiActionConventionsGlobalModelConvention()); - options.ApplicationModelConventions.Add(new WebApiOverloadingGlobalModelConvention()); + + var setup = new WebApiCompatShimOptionsSetup(); + setup.Invoke(options); var optionsAccessor = new Mock>(); optionsAccessor diff --git a/test/WebSites/WebApiCompatShimWebSite/Controllers/MvcController.cs b/test/WebSites/WebApiCompatShimWebSite/Controllers/MvcController.cs new file mode 100644 index 0000000000..aa3b75d4ec --- /dev/null +++ b/test/WebSites/WebApiCompatShimWebSite/Controllers/MvcController.cs @@ -0,0 +1,16 @@ +// 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 WebApiCompatShimWebSite +{ + // This is reachable via our MVC routes, but not webapi routes + public class MvcController : Controller + { + public string Index() + { + return "Hello, World!"; + } + } +} \ No newline at end of file diff --git a/test/WebSites/WebApiCompatShimWebSite/Startup.cs b/test/WebSites/WebApiCompatShimWebSite/Startup.cs index c4dcf3d447..2bc4fd8915 100644 --- a/test/WebSites/WebApiCompatShimWebSite/Startup.cs +++ b/test/WebSites/WebApiCompatShimWebSite/Startup.cs @@ -2,7 +2,6 @@ // 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; @@ -23,12 +22,15 @@ public void Configure(IApplicationBuilder app) app.UseMvc(routes => { + // This route can't access any of our webapi controllers + routes.MapRoute("default", "{controller}/{action}/{id?}"); + // Tests include different styles of WebAPI conventional routing and action selection - the prefix keeps // them from matching too eagerly. - routes.MapRoute("named-action", "api/Blog/{controller}/{action}/{id?}"); - routes.MapRoute("unnamed-action", "api/Admin/{controller}/{id?}"); - routes.MapRoute("name-as-parameter", "api/Store/{controller}/{name?}"); - routes.MapRoute("extra-parameter", "api/Support/{extra}/{controller}/{id?}"); + routes.MapWebApiRoute("named-action", "api/Blog/{controller}/{action}/{id?}"); + routes.MapWebApiRoute("unnamed-action", "api/Admin/{controller}/{id?}"); + routes.MapWebApiRoute("name-as-parameter", "api/Store/{controller}/{name?}"); + routes.MapWebApiRoute("extra-parameter", "api/Support/{extra}/{controller}/{id?}"); }); } } From 7fabb8099192dd3c4422b2cc2afbbbfb35612564 Mon Sep 17 00:00:00 2001 From: Pranav K Date: Fri, 10 Oct 2014 15:13:49 -0700 Subject: [PATCH 13/39] Reacting to CoreCLR version changes --- src/Microsoft.AspNet.Mvc.Core/project.json | 4 ++-- src/Microsoft.AspNet.Mvc.ModelBinding/project.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Microsoft.AspNet.Mvc.Core/project.json b/src/Microsoft.AspNet.Mvc.Core/project.json index 6302a229c3..7920738b2f 100644 --- a/src/Microsoft.AspNet.Mvc.Core/project.json +++ b/src/Microsoft.AspNet.Mvc.Core/project.json @@ -26,7 +26,7 @@ }, "aspnetcore50": { "dependencies": { - "Microsoft.CSharp": "4.0.0.0", + "Microsoft.CSharp": "4.0.0-beta-*", "System.Collections": "4.0.10-beta-*", "System.Collections.Concurrent": "4.0.0-beta-*", "System.ComponentModel": "4.0.0-beta-*", @@ -46,7 +46,7 @@ "System.Runtime": "4.0.20-beta-*", "System.Runtime.Extensions": "4.0.10-beta-*", "System.Runtime.InteropServices": "4.0.20-beta-*", - "System.Runtime.Serialization.Xml": "4.0.0-beta-*", + "System.Runtime.Serialization.Xml": "4.0.10-beta-*", "System.Security.Claims": "1.0.0-*", "System.Security.Cryptography.Encryption": "4.0.0-beta-*", "System.Security.Cryptography.Hashing.Algorithms": "4.0.0-beta-*", diff --git a/src/Microsoft.AspNet.Mvc.ModelBinding/project.json b/src/Microsoft.AspNet.Mvc.ModelBinding/project.json index 7ed2c75fc1..f2f1271d2a 100644 --- a/src/Microsoft.AspNet.Mvc.ModelBinding/project.json +++ b/src/Microsoft.AspNet.Mvc.ModelBinding/project.json @@ -42,7 +42,7 @@ "System.Runtime.Extensions": "4.0.10-beta-*", "System.Runtime.InteropServices": "4.0.20-beta-*", "System.Runtime.Serialization.Primitives": "4.0.0-beta-*", - "System.Runtime.Serialization.Xml": "4.0.0-beta-*", + "System.Runtime.Serialization.Xml": "4.0.10-beta-*", "System.Text.Encoding": "4.0.10-beta-*", "System.Text.Encoding.Extensions": "4.0.10-beta-*", "System.Threading": "4.0.0-beta-*", From 13ee27c92cdb2a011812caf12f408ebc9d6f6704 Mon Sep 17 00:00:00 2001 From: YishaiGalatzer Date: Sat, 11 Oct 2014 13:26:07 -0700 Subject: [PATCH 14/39] Rename IControllerAssemblyProvider to IAssemblyProvider and follow through resultant renames --- samples/MvcSample.Web/Startup.cs | 4 ++-- samples/MvcSample.Web/TestAssemblyProvider.cs | 2 +- .../ControllerActionDescriptorProvider.cs | 8 ++++---- ...llerAssemblyProvider.cs => DefaultAssemblyProvider.cs} | 4 ++-- ...ControllerAssemblyProvider.cs => IAssemblyProvider.cs} | 2 +- .../ViewComponents/DefaultViewComponentSelector.cs | 4 ++-- .../Razor/RazorCompilationService.cs | 4 ++-- src/Microsoft.AspNet.Mvc/MvcServices.cs | 2 +- .../ActionAttributeTests.cs | 4 ++-- .../ControllerActionDescriptorProviderTests.cs | 8 ++++---- ...faultActionDiscoveryConventionsActionSelectionTests.cs | 6 +++--- ...lyProviderTests.cs => DefaultAssemblyProviderTests.cs} | 6 +++--- ...ollerAssemblyProvider.cs => StaticAssemblyProvider.cs} | 4 ++-- .../TestAssemblyProvider.cs | 2 +- test/Microsoft.AspNet.Mvc.FunctionalTests/TestHelper.cs | 2 +- .../RazorCompilationServiceTest.cs | 4 ++-- .../ApiControllerActionDiscoveryTest.cs | 2 +- 17 files changed, 34 insertions(+), 34 deletions(-) rename src/Microsoft.AspNet.Mvc.Core/{DefaultControllerAssemblyProvider.cs => DefaultAssemblyProvider.cs} (92%) rename src/Microsoft.AspNet.Mvc.Core/{IControllerAssemblyProvider.cs => IAssemblyProvider.cs} (87%) rename test/Microsoft.AspNet.Mvc.Core.Test/{DefaultControllerAssemblyProviderTests.cs => DefaultAssemblyProviderTests.cs} (91%) rename test/Microsoft.AspNet.Mvc.Core.Test/{StaticControllerAssemblyProvider.cs => StaticAssemblyProvider.cs} (76%) diff --git a/samples/MvcSample.Web/Startup.cs b/samples/MvcSample.Web/Startup.cs index 9e40c3b1ed..21bc2a2d35 100644 --- a/samples/MvcSample.Web/Startup.cs +++ b/samples/MvcSample.Web/Startup.cs @@ -41,7 +41,7 @@ public void Configure(IApplicationBuilder app) // Setup services with a test AssemblyProvider so that only the // sample's assemblies are loaded. This prevents loading controllers from other assemblies // when the sample is used in the Functional Tests. - services.AddTransient>(); + services.AddTransient>(); services.ConfigureOptions(options => { options.Filters.Add(typeof(PassThroughAttribute), order: 17); @@ -81,7 +81,7 @@ public void Configure(IApplicationBuilder app) // Setup services with a test AssemblyProvider so that only the // sample's assemblies are loaded. This prevents loading controllers from other assemblies // when the sample is used in the Functional Tests. - services.AddTransient>(); + services.AddTransient>(); services.ConfigureOptions(options => { diff --git a/samples/MvcSample.Web/TestAssemblyProvider.cs b/samples/MvcSample.Web/TestAssemblyProvider.cs index b1db4925f5..98b5fd9f7c 100644 --- a/samples/MvcSample.Web/TestAssemblyProvider.cs +++ b/samples/MvcSample.Web/TestAssemblyProvider.cs @@ -17,7 +17,7 @@ namespace MvcSample.Web /// This is a generic type because it needs to instantiated by a service provider to replace /// a built-in MVC service. /// - public class TestAssemblyProvider : IControllerAssemblyProvider + public class TestAssemblyProvider : IAssemblyProvider { public TestAssemblyProvider() { diff --git a/src/Microsoft.AspNet.Mvc.Core/ControllerActionDescriptorProvider.cs b/src/Microsoft.AspNet.Mvc.Core/ControllerActionDescriptorProvider.cs index 82b589e303..6b3c8fb610 100644 --- a/src/Microsoft.AspNet.Mvc.Core/ControllerActionDescriptorProvider.cs +++ b/src/Microsoft.AspNet.Mvc.Core/ControllerActionDescriptorProvider.cs @@ -21,17 +21,17 @@ public class ControllerActionDescriptorProvider : IActionDescriptorProvider // the reflected model is null. private const int DefaultAttributeRouteOrder = 0; - private readonly IControllerAssemblyProvider _controllerAssemblyProvider; + private readonly IAssemblyProvider _assemblyProvider; private readonly IActionDiscoveryConventions _conventions; private readonly IReadOnlyList _globalFilters; private readonly IEnumerable _modelConventions; - public ControllerActionDescriptorProvider(IControllerAssemblyProvider controllerAssemblyProvider, + public ControllerActionDescriptorProvider(IAssemblyProvider assemblyProvider, IActionDiscoveryConventions conventions, IGlobalFilterProvider globalFilters, IOptionsAccessor optionsAccessor) { - _controllerAssemblyProvider = controllerAssemblyProvider; + _assemblyProvider = assemblyProvider; _conventions = conventions; _globalFilters = globalFilters.Filters; _modelConventions = optionsAccessor.Options.ApplicationModelConventions; @@ -60,7 +60,7 @@ public GlobalModel BuildModel() var applicationModel = new GlobalModel(); applicationModel.Filters.AddRange(_globalFilters); - var assemblies = _controllerAssemblyProvider.CandidateAssemblies; + var assemblies = _assemblyProvider.CandidateAssemblies; var types = assemblies.SelectMany(a => a.DefinedTypes); var controllerTypes = types.Where(_conventions.IsController); diff --git a/src/Microsoft.AspNet.Mvc.Core/DefaultControllerAssemblyProvider.cs b/src/Microsoft.AspNet.Mvc.Core/DefaultAssemblyProvider.cs similarity index 92% rename from src/Microsoft.AspNet.Mvc.Core/DefaultControllerAssemblyProvider.cs rename to src/Microsoft.AspNet.Mvc.Core/DefaultAssemblyProvider.cs index 5c2459d555..1a7abf2c4f 100644 --- a/src/Microsoft.AspNet.Mvc.Core/DefaultControllerAssemblyProvider.cs +++ b/src/Microsoft.AspNet.Mvc.Core/DefaultAssemblyProvider.cs @@ -9,7 +9,7 @@ namespace Microsoft.AspNet.Mvc { - public class DefaultControllerAssemblyProvider : IControllerAssemblyProvider + public class DefaultAssemblyProvider : IAssemblyProvider { // List of Mvc assemblies that we'll use as roots for controller discovery. private static readonly HashSet _mvcAssemblyList = new HashSet(StringComparer.Ordinal) @@ -24,7 +24,7 @@ public class DefaultControllerAssemblyProvider : IControllerAssemblyProvider private readonly ILibraryManager _libraryManager; - public DefaultControllerAssemblyProvider(ILibraryManager libraryManager) + public DefaultAssemblyProvider(ILibraryManager libraryManager) { _libraryManager = libraryManager; } diff --git a/src/Microsoft.AspNet.Mvc.Core/IControllerAssemblyProvider.cs b/src/Microsoft.AspNet.Mvc.Core/IAssemblyProvider.cs similarity index 87% rename from src/Microsoft.AspNet.Mvc.Core/IControllerAssemblyProvider.cs rename to src/Microsoft.AspNet.Mvc.Core/IAssemblyProvider.cs index f8034ebf8c..a5a9cdfbb3 100644 --- a/src/Microsoft.AspNet.Mvc.Core/IControllerAssemblyProvider.cs +++ b/src/Microsoft.AspNet.Mvc.Core/IAssemblyProvider.cs @@ -6,7 +6,7 @@ namespace Microsoft.AspNet.Mvc { - public interface IControllerAssemblyProvider + public interface IAssemblyProvider { IEnumerable CandidateAssemblies { get; } } diff --git a/src/Microsoft.AspNet.Mvc.Core/ViewComponents/DefaultViewComponentSelector.cs b/src/Microsoft.AspNet.Mvc.Core/ViewComponents/DefaultViewComponentSelector.cs index 1ffa129f16..87338f5bec 100644 --- a/src/Microsoft.AspNet.Mvc.Core/ViewComponents/DefaultViewComponentSelector.cs +++ b/src/Microsoft.AspNet.Mvc.Core/ViewComponents/DefaultViewComponentSelector.cs @@ -9,9 +9,9 @@ namespace Microsoft.AspNet.Mvc { public class DefaultViewComponentSelector : IViewComponentSelector { - private readonly IControllerAssemblyProvider _assemblyProvider; + private readonly IAssemblyProvider _assemblyProvider; - public DefaultViewComponentSelector(IControllerAssemblyProvider assemblyProvider) + public DefaultViewComponentSelector(IAssemblyProvider assemblyProvider) { _assemblyProvider = assemblyProvider; } diff --git a/src/Microsoft.AspNet.Mvc.Razor/Razor/RazorCompilationService.cs b/src/Microsoft.AspNet.Mvc.Razor/Razor/RazorCompilationService.cs index 223c35cbc9..757512fbc2 100644 --- a/src/Microsoft.AspNet.Mvc.Razor/Razor/RazorCompilationService.cs +++ b/src/Microsoft.AspNet.Mvc.Razor/Razor/RazorCompilationService.cs @@ -37,12 +37,12 @@ private ICompilationService CompilationService private readonly IMvcRazorHost _razorHost; public RazorCompilationService(IServiceProvider serviceProvider, - IControllerAssemblyProvider _controllerAssemblyProvider, + IAssemblyProvider _assemblyProvider, IMvcRazorHost razorHost) { _serviceProvider = serviceProvider; _razorHost = razorHost; - _cache = new CompilerCache(_controllerAssemblyProvider.CandidateAssemblies); + _cache = new CompilerCache(_assemblyProvider.CandidateAssemblies); } /// diff --git a/src/Microsoft.AspNet.Mvc/MvcServices.cs b/src/Microsoft.AspNet.Mvc/MvcServices.cs index 802650cb8d..bc7e5816e5 100644 --- a/src/Microsoft.AspNet.Mvc/MvcServices.cs +++ b/src/Microsoft.AspNet.Mvc/MvcServices.cs @@ -45,7 +45,7 @@ public static IEnumerable GetDefaultServices(IConfiguration DefaultActionConstraintProvider>(); yield return describe.Transient(); - yield return describe.Transient(); + yield return describe.Transient(); yield return describe.Transient(); // The host is designed to be discarded after consumption and is very inexpensive to initialize. diff --git a/test/Microsoft.AspNet.Mvc.Core.Test/ActionAttributeTests.cs b/test/Microsoft.AspNet.Mvc.Core.Test/ActionAttributeTests.cs index d9822ca681..316d0ed481 100644 --- a/test/Microsoft.AspNet.Mvc.Core.Test/ActionAttributeTests.cs +++ b/test/Microsoft.AspNet.Mvc.Core.Test/ActionAttributeTests.cs @@ -196,7 +196,7 @@ private async Task InvokeActionSelector( private ControllerActionDescriptorProvider GetActionDescriptorProvider( IActionDiscoveryConventions actionDiscoveryConventions = null) { - var controllerAssemblyProvider = new StaticControllerAssemblyProvider(); + var assemblyProvider = new StaticAssemblyProvider(); if (actionDiscoveryConventions == null) { @@ -208,7 +208,7 @@ private ControllerActionDescriptorProvider GetActionDescriptorProvider( } return new ControllerActionDescriptorProvider( - controllerAssemblyProvider, + assemblyProvider, actionDiscoveryConventions, new TestGlobalFilterProvider(), new MockMvcOptionsAccessor()); diff --git a/test/Microsoft.AspNet.Mvc.Core.Test/ControllerActionDescriptorProviderTests.cs b/test/Microsoft.AspNet.Mvc.Core.Test/ControllerActionDescriptorProviderTests.cs index 3686f7e1f5..9ceb03e447 100644 --- a/test/Microsoft.AspNet.Mvc.Core.Test/ControllerActionDescriptorProviderTests.cs +++ b/test/Microsoft.AspNet.Mvc.Core.Test/ControllerActionDescriptorProviderTests.cs @@ -1334,7 +1334,7 @@ private ControllerActionDescriptorProvider GetProvider( { var conventions = new StaticActionDiscoveryConventions(controllerTypeInfo); - var assemblyProvider = new Mock(); + var assemblyProvider = new Mock(); assemblyProvider .SetupGet(ap => ap.CandidateAssemblies) .Returns(new Assembly[] { controllerTypeInfo.Assembly }); @@ -1353,7 +1353,7 @@ private ControllerActionDescriptorProvider GetProvider( { var conventions = new StaticActionDiscoveryConventions(controllerTypeInfo); - var assemblyProvider = new Mock(); + var assemblyProvider = new Mock(); assemblyProvider .SetupGet(ap => ap.CandidateAssemblies) .Returns(new Assembly[] { controllerTypeInfo.First().Assembly }); @@ -1373,7 +1373,7 @@ private ControllerActionDescriptorProvider GetProvider( { var conventions = new StaticActionDiscoveryConventions(type); - var assemblyProvider = new Mock(); + var assemblyProvider = new Mock(); assemblyProvider .SetupGet(ap => ap.CandidateAssemblies) .Returns(new Assembly[] { type.Assembly }); @@ -1389,7 +1389,7 @@ private IEnumerable GetDescriptors(params TypeInfo[] controlle { var conventions = new StaticActionDiscoveryConventions(controllerTypeInfos); - var assemblyProvider = new Mock(); + var assemblyProvider = new Mock(); assemblyProvider .SetupGet(ap => ap.CandidateAssemblies) .Returns(controllerTypeInfos.Select(cti => cti.Assembly).Distinct()); diff --git a/test/Microsoft.AspNet.Mvc.Core.Test/DefaultActionDiscoveryConventionsActionSelectionTests.cs b/test/Microsoft.AspNet.Mvc.Core.Test/DefaultActionDiscoveryConventionsActionSelectionTests.cs index ca3f70af72..de2e369f1c 100644 --- a/test/Microsoft.AspNet.Mvc.Core.Test/DefaultActionDiscoveryConventionsActionSelectionTests.cs +++ b/test/Microsoft.AspNet.Mvc.Core.Test/DefaultActionDiscoveryConventionsActionSelectionTests.cs @@ -100,10 +100,10 @@ private async Task InvokeActionSelector(RouteContext context, private ControllerActionDescriptorProvider GetActionDescriptorProvider(DefaultActionDiscoveryConventions actionDiscoveryConventions) { var assemblies = new Assembly[] { typeof(DefaultActionDiscoveryConventionsActionSelectionTests).GetTypeInfo().Assembly, }; - var controllerAssemblyProvider = new Mock(); - controllerAssemblyProvider.SetupGet(x => x.CandidateAssemblies).Returns(assemblies); + var AssemblyProvider = new Mock(); + AssemblyProvider.SetupGet(x => x.CandidateAssemblies).Returns(assemblies); return new ControllerActionDescriptorProvider( - controllerAssemblyProvider.Object, + AssemblyProvider.Object, actionDiscoveryConventions, new TestGlobalFilterProvider(), new MockMvcOptionsAccessor()); diff --git a/test/Microsoft.AspNet.Mvc.Core.Test/DefaultControllerAssemblyProviderTests.cs b/test/Microsoft.AspNet.Mvc.Core.Test/DefaultAssemblyProviderTests.cs similarity index 91% rename from test/Microsoft.AspNet.Mvc.Core.Test/DefaultControllerAssemblyProviderTests.cs rename to test/Microsoft.AspNet.Mvc.Core.Test/DefaultAssemblyProviderTests.cs index b478745a75..d04c9f82e0 100644 --- a/test/Microsoft.AspNet.Mvc.Core.Test/DefaultControllerAssemblyProviderTests.cs +++ b/test/Microsoft.AspNet.Mvc.Core.Test/DefaultAssemblyProviderTests.cs @@ -11,7 +11,7 @@ namespace Microsoft.AspNet.Mvc.Core { - public class DefaultControllerAssemblyProviderTests + public class DefaultAssemblyProviderTests { [Fact] public void CandidateAssemblies_IgnoresMvcAssemblies() @@ -27,7 +27,7 @@ public void CandidateAssemblies_IgnoresMvcAssemblies() CreateLibraryInfo("SomeRandomAssembly"), }) .Verifiable(); - var provider = new DefaultControllerAssemblyProvider(manager.Object); + var provider = new DefaultAssemblyProvider(manager.Object); // Act var candidates = provider.GetCandidateLibraries(); @@ -49,7 +49,7 @@ public void CandidateAssemblies_ReturnsLibrariesReferencingAnyMvcAssembly() .Returns(new[] { CreateLibraryInfo("Bar") }); manager.Setup(f => f.GetReferencingLibraries("Microsoft.AspNet.Mvc")) .Returns(new[] { CreateLibraryInfo("Baz") }); - var provider = new DefaultControllerAssemblyProvider(manager.Object); + var provider = new DefaultAssemblyProvider(manager.Object); // Act var candidates = provider.GetCandidateLibraries(); diff --git a/test/Microsoft.AspNet.Mvc.Core.Test/StaticControllerAssemblyProvider.cs b/test/Microsoft.AspNet.Mvc.Core.Test/StaticAssemblyProvider.cs similarity index 76% rename from test/Microsoft.AspNet.Mvc.Core.Test/StaticControllerAssemblyProvider.cs rename to test/Microsoft.AspNet.Mvc.Core.Test/StaticAssemblyProvider.cs index 1a00b99bec..fd2f685acf 100644 --- a/test/Microsoft.AspNet.Mvc.Core.Test/StaticControllerAssemblyProvider.cs +++ b/test/Microsoft.AspNet.Mvc.Core.Test/StaticAssemblyProvider.cs @@ -7,9 +7,9 @@ namespace Microsoft.AspNet.Mvc { /// - /// An implementation of IControllerAssemblyProvider that provides just this assembly. + /// An implementation of IAssemblyProvider that provides just this assembly. /// - public class StaticControllerAssemblyProvider : IControllerAssemblyProvider + public class StaticAssemblyProvider : IAssemblyProvider { public IEnumerable CandidateAssemblies { diff --git a/test/Microsoft.AspNet.Mvc.FunctionalTests/TestAssemblyProvider.cs b/test/Microsoft.AspNet.Mvc.FunctionalTests/TestAssemblyProvider.cs index 8ed5e855c6..1ef1bec151 100644 --- a/test/Microsoft.AspNet.Mvc.FunctionalTests/TestAssemblyProvider.cs +++ b/test/Microsoft.AspNet.Mvc.FunctionalTests/TestAssemblyProvider.cs @@ -13,7 +13,7 @@ namespace Microsoft.AspNet.Mvc.FunctionalTests /// This is a generic type because it needs to instantiated by a service provider to replace /// a built-in MVC service. /// - public class TestAssemblyProvider : IControllerAssemblyProvider + public class TestAssemblyProvider : IAssemblyProvider { public TestAssemblyProvider() { diff --git a/test/Microsoft.AspNet.Mvc.FunctionalTests/TestHelper.cs b/test/Microsoft.AspNet.Mvc.FunctionalTests/TestHelper.cs index eb23816a37..1973ecab12 100644 --- a/test/Microsoft.AspNet.Mvc.FunctionalTests/TestHelper.cs +++ b/test/Microsoft.AspNet.Mvc.FunctionalTests/TestHelper.cs @@ -47,7 +47,7 @@ public static IServiceProvider CreateServices(string applicationWebSiteName, str var configuration = new TestConfigurationProvider(); configuration.Configuration.Set( - typeof(IControllerAssemblyProvider).FullName, + typeof(IAssemblyProvider).FullName, providerType.AssemblyQualifiedName); services.AddInstance( diff --git a/test/Microsoft.AspNet.Mvc.Razor.Test/RazorCompilationServiceTest.cs b/test/Microsoft.AspNet.Mvc.Razor.Test/RazorCompilationServiceTest.cs index ab3100ea7c..20a36b3d50 100644 --- a/test/Microsoft.AspNet.Mvc.Razor.Test/RazorCompilationServiceTest.cs +++ b/test/Microsoft.AspNet.Mvc.Razor.Test/RazorCompilationServiceTest.cs @@ -27,7 +27,7 @@ public void CompileCoreCalculatesRootRelativePath(string appPath, string viewPat .Returns(GetGeneratorResult()) .Verifiable(); - var ap = new Mock(); + var ap = new Mock(); ap.SetupGet(e => e.CandidateAssemblies) .Returns(Enumerable.Empty()) .Verifiable(); @@ -68,7 +68,7 @@ public void CompileCoreSetsEnableInstrumentationOnHost(bool enableInstrumentatio var host = new Mock(); host.SetupAllProperties(); host.Setup(h => h.GenerateCode(It.IsAny(), It.IsAny())) - .Returns(GetGeneratorResult()); var assemblyProvider = new Mock(); + .Returns(GetGeneratorResult()); var assemblyProvider = new Mock(); assemblyProvider.SetupGet(e => e.CandidateAssemblies) .Returns(Enumerable.Empty()); diff --git a/test/Microsoft.AspNet.Mvc.WebApiCompatShimTest/ApiControllerActionDiscoveryTest.cs b/test/Microsoft.AspNet.Mvc.WebApiCompatShimTest/ApiControllerActionDiscoveryTest.cs index 7fbd35ed9c..cf43ba53a2 100644 --- a/test/Microsoft.AspNet.Mvc.WebApiCompatShimTest/ApiControllerActionDiscoveryTest.cs +++ b/test/Microsoft.AspNet.Mvc.WebApiCompatShimTest/ApiControllerActionDiscoveryTest.cs @@ -252,7 +252,7 @@ public void GetActions_AllWebApiActionsAreInWebApiArea() private INestedProviderManager CreateProvider() { - var assemblyProvider = new Mock(); + var assemblyProvider = new Mock(); assemblyProvider .SetupGet(ap => ap.CandidateAssemblies) .Returns(new Assembly[] { typeof(ApiControllerActionDiscoveryTest).Assembly }); From 75084ba0cd6e849cebbf4bac7d574fa0d1750b23 Mon Sep 17 00:00:00 2001 From: YishaiGalatzer Date: Sat, 11 Oct 2014 15:35:11 -0700 Subject: [PATCH 15/39] Move caching of compilation results to its own layer. This will allow only creating the razor compilation when really needed, with the right lifetime. --- .../Compilation/CompilerCache.cs | 6 ++-- .../Compilation/CompilerCacheEntry.cs | 2 +- .../Compilation/ICompilerCache.cs | 15 +++++++++ .../Compilation/UncachedCompilationResult.cs | 2 +- .../Razor/RazorCompilationService.cs | 32 +++---------------- .../VirtualPathRazorPageFactory.cs | 28 ++++++++++------ src/Microsoft.AspNet.Mvc/MvcServices.cs | 1 + .../RazorCompilationServiceTest.cs | 28 ++++------------ 8 files changed, 50 insertions(+), 64 deletions(-) create mode 100644 src/Microsoft.AspNet.Mvc.Razor/Compilation/ICompilerCache.cs diff --git a/src/Microsoft.AspNet.Mvc.Razor/Compilation/CompilerCache.cs b/src/Microsoft.AspNet.Mvc.Razor/Compilation/CompilerCache.cs index 0e8bccc48f..298d45c7b5 100644 --- a/src/Microsoft.AspNet.Mvc.Razor/Compilation/CompilerCache.cs +++ b/src/Microsoft.AspNet.Mvc.Razor/Compilation/CompilerCache.cs @@ -9,13 +9,13 @@ namespace Microsoft.AspNet.Mvc.Razor { - public class CompilerCache + public class CompilerCache : ICompilerCache { private readonly ConcurrentDictionary _cache; private static readonly Type[] EmptyType = new Type[0]; - public CompilerCache([NotNull] IEnumerable assemblies) - : this(GetFileInfos(assemblies)) + public CompilerCache([NotNull] IAssemblyProvider provider) + : this(GetFileInfos(provider.CandidateAssemblies)) { } diff --git a/src/Microsoft.AspNet.Mvc.Razor/Compilation/CompilerCacheEntry.cs b/src/Microsoft.AspNet.Mvc.Razor/Compilation/CompilerCacheEntry.cs index 0a395e73ee..d6c1028153 100644 --- a/src/Microsoft.AspNet.Mvc.Razor/Compilation/CompilerCacheEntry.cs +++ b/src/Microsoft.AspNet.Mvc.Razor/Compilation/CompilerCacheEntry.cs @@ -6,7 +6,7 @@ namespace Microsoft.AspNet.Mvc.Razor { /// - /// An entry in that contain metadata about precompiled and dynamically compiled file. + /// An entry in that contain metadata about precompiled and dynamically compiled file. /// public class CompilerCacheEntry { diff --git a/src/Microsoft.AspNet.Mvc.Razor/Compilation/ICompilerCache.cs b/src/Microsoft.AspNet.Mvc.Razor/Compilation/ICompilerCache.cs new file mode 100644 index 0000000000..132908856d --- /dev/null +++ b/src/Microsoft.AspNet.Mvc.Razor/Compilation/ICompilerCache.cs @@ -0,0 +1,15 @@ +// 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.FileSystems; + +namespace Microsoft.AspNet.Mvc.Razor +{ + public interface ICompilerCache + { + CompilationResult GetOrAdd([NotNull] RelativeFileInfo fileInfo, + bool enableInstrumentation, + [NotNull] Func compile); + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Mvc.Razor/Compilation/UncachedCompilationResult.cs b/src/Microsoft.AspNet.Mvc.Razor/Compilation/UncachedCompilationResult.cs index 493643dca0..d190563f83 100644 --- a/src/Microsoft.AspNet.Mvc.Razor/Compilation/UncachedCompilationResult.cs +++ b/src/Microsoft.AspNet.Mvc.Razor/Compilation/UncachedCompilationResult.cs @@ -6,7 +6,7 @@ namespace Microsoft.AspNet.Mvc.Razor { /// - /// Represents the result of compilation that does not come from the . + /// Represents the result of compilation that does not come from the . /// public class UncachedCompilationResult : CompilationResult { diff --git a/src/Microsoft.AspNet.Mvc.Razor/Razor/RazorCompilationService.cs b/src/Microsoft.AspNet.Mvc.Razor/Razor/RazorCompilationService.cs index 757512fbc2..2258be0038 100644 --- a/src/Microsoft.AspNet.Mvc.Razor/Razor/RazorCompilationService.cs +++ b/src/Microsoft.AspNet.Mvc.Razor/Razor/RazorCompilationService.cs @@ -16,42 +16,18 @@ namespace Microsoft.AspNet.Mvc.Razor /// public class RazorCompilationService : IRazorCompilationService { - private readonly IServiceProvider _serviceProvider; - - private readonly CompilerCache _cache; - private ICompilationService _compilationService; - - private ICompilationService CompilationService - { - get - { - if (_compilationService == null) - { - _compilationService = _serviceProvider.GetService(); - } - - return _compilationService; - } - } - + private readonly ICompilationService _compilationService; private readonly IMvcRazorHost _razorHost; - public RazorCompilationService(IServiceProvider serviceProvider, - IAssemblyProvider _assemblyProvider, + public RazorCompilationService(ICompilationService compilationService, IMvcRazorHost razorHost) { - _serviceProvider = serviceProvider; + _compilationService = compilationService; _razorHost = razorHost; - _cache = new CompilerCache(_assemblyProvider.CandidateAssemblies); } /// public CompilationResult Compile([NotNull] RelativeFileInfo file, bool isInstrumented) - { - return _cache.GetOrAdd(file, isInstrumented, () => CompileCore(file, isInstrumented)); - } - - internal CompilationResult CompileCore(RelativeFileInfo file, bool isInstrumented) { _razorHost.EnableInstrumentation = isInstrumented; @@ -68,7 +44,7 @@ internal CompilationResult CompileCore(RelativeFileInfo file, bool isInstrumente return CompilationResult.Failed(file.FileInfo, results.GeneratedCode, messages); } - return CompilationService.Compile(file.FileInfo, results.GeneratedCode); + return _compilationService.Compile(file.FileInfo, results.GeneratedCode); } } } diff --git a/src/Microsoft.AspNet.Mvc.Razor/VirtualPathRazorPageFactory.cs b/src/Microsoft.AspNet.Mvc.Razor/VirtualPathRazorPageFactory.cs index 4b7fb7ae93..8c6d8732d2 100644 --- a/src/Microsoft.AspNet.Mvc.Razor/VirtualPathRazorPageFactory.cs +++ b/src/Microsoft.AspNet.Mvc.Razor/VirtualPathRazorPageFactory.cs @@ -12,30 +12,36 @@ namespace Microsoft.AspNet.Mvc.Razor /// public class VirtualPathRazorPageFactory : IRazorPageFactory { - private IRazorCompilationService _compilationService; - private IRazorCompilationService CompilationService + private readonly ITypeActivator _activator; + private readonly IServiceProvider _serviceProvider; + private readonly IFileInfoCache _fileInfoCache; + private readonly ICompilerCache _compilerCache; + + private IRazorCompilationService _razorcompilationService; + + private IRazorCompilationService RazorCompilationService { get { - if (_compilationService == null) + if (_razorcompilationService == null) { - _compilationService = _serviceProvider.GetService(); + // it is ok to use the cached service provider because this service has + // a lifetime of Scoped. + _razorcompilationService = _serviceProvider.GetService(); } - return _compilationService; + return _razorcompilationService; } } - private readonly ITypeActivator _activator; - private readonly IServiceProvider _serviceProvider; - private readonly IFileInfoCache _fileInfoCache; - public VirtualPathRazorPageFactory(ITypeActivator typeActivator, IServiceProvider serviceProvider, + ICompilerCache compilerCache, IFileInfoCache fileInfoCache) { _activator = typeActivator; _serviceProvider = serviceProvider; + _compilerCache = compilerCache; _fileInfoCache = fileInfoCache; } @@ -58,7 +64,9 @@ public IRazorPage CreateInstance([NotNull] string relativePath, bool enableInstr RelativePath = relativePath, }; - var result = CompilationService.Compile(relativeFileInfo, enableInstrumentation); + var result = _compilerCache.GetOrAdd(relativeFileInfo, enableInstrumentation, () => + RazorCompilationService.Compile(relativeFileInfo, enableInstrumentation)); + var page = (IRazorPage)_activator.CreateInstance(_serviceProvider, result.CompiledType); page.Path = relativePath; diff --git a/src/Microsoft.AspNet.Mvc/MvcServices.cs b/src/Microsoft.AspNet.Mvc/MvcServices.cs index bc7e5816e5..fdd3c2d64b 100644 --- a/src/Microsoft.AspNet.Mvc/MvcServices.cs +++ b/src/Microsoft.AspNet.Mvc/MvcServices.cs @@ -51,6 +51,7 @@ public static IEnumerable GetDefaultServices(IConfiguration // The host is designed to be discarded after consumption and is very inexpensive to initialize. yield return describe.Transient(); + yield return describe.Singleton(); yield return describe.Singleton(); yield return describe.Singleton(); diff --git a/test/Microsoft.AspNet.Mvc.Razor.Test/RazorCompilationServiceTest.cs b/test/Microsoft.AspNet.Mvc.Razor.Test/RazorCompilationServiceTest.cs index 20a36b3d50..9272dc74f1 100644 --- a/test/Microsoft.AspNet.Mvc.Razor.Test/RazorCompilationServiceTest.cs +++ b/test/Microsoft.AspNet.Mvc.Razor.Test/RazorCompilationServiceTest.cs @@ -19,7 +19,7 @@ public class RazorCompilationServiceTest [Theory] [InlineData(@"src\work\myapp", @"src\work\myapp\views\index\home.cshtml")] [InlineData(@"src\work\myapp\", @"src\work\myapp\views\index\home.cshtml")] - public void CompileCoreCalculatesRootRelativePath(string appPath, string viewPath) + public void CompileCalculatesRootRelativePath(string appPath, string viewPath) { // Arrange var host = new Mock(); @@ -27,11 +27,6 @@ public void CompileCoreCalculatesRootRelativePath(string appPath, string viewPat .Returns(GetGeneratorResult()) .Verifiable(); - var ap = new Mock(); - ap.SetupGet(e => e.CandidateAssemblies) - .Returns(Enumerable.Empty()) - .Verifiable(); - var fileInfo = new Mock(); fileInfo.Setup(f => f.PhysicalPath).Returns(viewPath); fileInfo.Setup(f => f.CreateReadStream()).Returns(Stream.Null); @@ -40,11 +35,7 @@ public void CompileCoreCalculatesRootRelativePath(string appPath, string viewPat compiler.Setup(c => c.Compile(fileInfo.Object, It.IsAny())) .Returns(CompilationResult.Successful(typeof(RazorCompilationServiceTest))); - var serviceProvider = new Mock(); - serviceProvider.Setup(sp => sp.GetService(It.Is(t => t == typeof(ICompilationService)))) - .Returns(compiler.Object); - - var razorService = new RazorCompilationService(serviceProvider.Object, ap.Object, host.Object); + var razorService = new RazorCompilationService(compiler.Object, host.Object); var relativeFileInfo = new RelativeFileInfo() { @@ -53,7 +44,7 @@ public void CompileCoreCalculatesRootRelativePath(string appPath, string viewPat }; // Act - razorService.CompileCore(relativeFileInfo, isInstrumented: false); + razorService.Compile(relativeFileInfo, isInstrumented: false); // Assert host.Verify(); @@ -62,25 +53,20 @@ public void CompileCoreCalculatesRootRelativePath(string appPath, string viewPat [Theory] [InlineData(false)] [InlineData(true)] - public void CompileCoreSetsEnableInstrumentationOnHost(bool enableInstrumentation) + public void CompileSetsEnableInstrumentationOnHost(bool enableInstrumentation) { // Arrange var host = new Mock(); host.SetupAllProperties(); host.Setup(h => h.GenerateCode(It.IsAny(), It.IsAny())) - .Returns(GetGeneratorResult()); var assemblyProvider = new Mock(); - assemblyProvider.SetupGet(e => e.CandidateAssemblies) - .Returns(Enumerable.Empty()); + .Returns(GetGeneratorResult()); var compiler = new Mock(); compiler.Setup(c => c.Compile(It.IsAny(), It.IsAny())) .Returns(CompilationResult.Successful(GetType())); - var serviceProvider = new Mock(); - serviceProvider.Setup(sp => sp.GetService(It.Is(t => t == typeof(ICompilationService)))) - .Returns(compiler.Object); + var razorService = new RazorCompilationService(compiler.Object, host.Object); - var razorService = new RazorCompilationService(serviceProvider.Object, assemblyProvider.Object, host.Object); var relativeFileInfo = new RelativeFileInfo() { FileInfo = Mock.Of(), @@ -88,7 +74,7 @@ public void CompileCoreSetsEnableInstrumentationOnHost(bool enableInstrumentatio }; // Act - razorService.CompileCore(relativeFileInfo, isInstrumented: enableInstrumentation); + razorService.Compile(relativeFileInfo, isInstrumented: enableInstrumentation); // Assert Assert.Equal(enableInstrumentation, host.Object.EnableInstrumentation); From 20eadb94ee296bac26801df89e560804d26e0c17 Mon Sep 17 00:00:00 2001 From: Pranav K Date: Wed, 8 Oct 2014 19:50:40 -0700 Subject: [PATCH 16/39] Remove instrumentation from public API surface Fixes #1262 --- .../Compilation/ICompilerCache.cs | 6 +- .../IRazorPageFactory.cs | 3 +- src/Microsoft.AspNet.Mvc.Razor/IRazorView.cs | 5 +- .../IViewStartProvider.cs | 3 +- src/Microsoft.AspNet.Mvc.Razor/RazorView.cs | 10 +-- .../RazorViewEngine.cs | 26 ++------ .../ViewStartProvider.cs | 4 +- .../VirtualPathRazorPageFactory.cs | 43 +++++++----- src/Microsoft.AspNet.Mvc/MvcServices.cs | 3 +- .../RazorViewEngineTest.cs | 18 ++--- .../RazorViewTest.cs | 66 ++++++++++--------- 11 files changed, 89 insertions(+), 98 deletions(-) diff --git a/src/Microsoft.AspNet.Mvc.Razor/Compilation/ICompilerCache.cs b/src/Microsoft.AspNet.Mvc.Razor/Compilation/ICompilerCache.cs index 132908856d..d11d4a4a30 100644 --- a/src/Microsoft.AspNet.Mvc.Razor/Compilation/ICompilerCache.cs +++ b/src/Microsoft.AspNet.Mvc.Razor/Compilation/ICompilerCache.cs @@ -6,10 +6,10 @@ namespace Microsoft.AspNet.Mvc.Razor { - public interface ICompilerCache - { + public interface ICompilerCache + { CompilationResult GetOrAdd([NotNull] RelativeFileInfo fileInfo, bool enableInstrumentation, [NotNull] Func compile); - } + } } \ No newline at end of file diff --git a/src/Microsoft.AspNet.Mvc.Razor/IRazorPageFactory.cs b/src/Microsoft.AspNet.Mvc.Razor/IRazorPageFactory.cs index b84b180625..daaf28222e 100644 --- a/src/Microsoft.AspNet.Mvc.Razor/IRazorPageFactory.cs +++ b/src/Microsoft.AspNet.Mvc.Razor/IRazorPageFactory.cs @@ -12,8 +12,7 @@ public interface IRazorPageFactory /// Creates a for the specified path. /// /// The path to locate the page. - /// Indicates that execution of the page should be instrumented. /// The IRazorPage instance if it exists, null otherwise. - IRazorPage CreateInstance(string relativePath, bool enableInstrumentation); + IRazorPage CreateInstance(string relativePath); } } diff --git a/src/Microsoft.AspNet.Mvc.Razor/IRazorView.cs b/src/Microsoft.AspNet.Mvc.Razor/IRazorView.cs index a5e261d9c9..7f8f411a7e 100644 --- a/src/Microsoft.AspNet.Mvc.Razor/IRazorView.cs +++ b/src/Microsoft.AspNet.Mvc.Razor/IRazorView.cs @@ -2,7 +2,6 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using Microsoft.AspNet.Mvc.Rendering; -using Microsoft.AspNet.PageExecutionInstrumentation; namespace Microsoft.AspNet.Mvc.Razor { @@ -18,8 +17,6 @@ public interface IRazorView : IView /// /// The instance to execute. /// Determines if the view is to be executed as a partial. - void Contextualize(IRazorPage razorPage, - bool isPartial, - IPageExecutionListenerFeature pageExecutionListenerFeature); + void Contextualize(IRazorPage razorPage, bool isPartial); } } \ No newline at end of file diff --git a/src/Microsoft.AspNet.Mvc.Razor/IViewStartProvider.cs b/src/Microsoft.AspNet.Mvc.Razor/IViewStartProvider.cs index 2076af472f..47a445d04b 100644 --- a/src/Microsoft.AspNet.Mvc.Razor/IViewStartProvider.cs +++ b/src/Microsoft.AspNet.Mvc.Razor/IViewStartProvider.cs @@ -15,8 +15,7 @@ public interface IViewStartProvider /// that are applicable to the specified view. /// /// The path of the page to locate ViewStart files for. - /// Indicates that execution of the page should be instrumented. /// A sequence of that represent ViewStart. - IEnumerable GetViewStartPages(string path, bool enableInstrumentation); + IEnumerable GetViewStartPages(string path); } } \ No newline at end of file diff --git a/src/Microsoft.AspNet.Mvc.Razor/RazorView.cs b/src/Microsoft.AspNet.Mvc.Razor/RazorView.cs index a27e9f2a18..6064bea6fd 100644 --- a/src/Microsoft.AspNet.Mvc.Razor/RazorView.cs +++ b/src/Microsoft.AspNet.Mvc.Razor/RazorView.cs @@ -44,12 +44,10 @@ private bool EnableInstrumentation /// public virtual void Contextualize([NotNull] IRazorPage razorPage, - bool isPartial, - IPageExecutionListenerFeature pageExecutionListener) + bool isPartial) { _razorPage = razorPage; _isPartial = isPartial; - _pageExecutionFeature = pageExecutionListener; } /// @@ -61,6 +59,8 @@ public virtual async Task RenderAsync([NotNull] ViewContext context) throw new InvalidOperationException(message); } + _pageExecutionFeature = context.HttpContext.GetFeature(); + if (!_isPartial) { var bodyWriter = await RenderPageAsync(_razorPage, context, executeViewStart: true); @@ -131,7 +131,7 @@ private async Task RenderPageCoreAsync(IRazorPage page, ViewContext context) private async Task RenderViewStartAsync(ViewContext context) { - var viewStarts = _viewStartProvider.GetViewStartPages(_razorPage.Path, EnableInstrumentation); + var viewStarts = _viewStartProvider.GetViewStartPages(_razorPage.Path); foreach (var viewStart in viewStarts) { @@ -161,7 +161,7 @@ private async Task RenderLayoutAsync(ViewContext context, throw new InvalidOperationException(message); } - var layoutPage = _pageFactory.CreateInstance(previousPage.Layout, EnableInstrumentation); + var layoutPage = _pageFactory.CreateInstance(previousPage.Layout); if (layoutPage == null) { var message = Resources.FormatLayoutCannotBeLocated(previousPage.Layout); diff --git a/src/Microsoft.AspNet.Mvc.Razor/RazorViewEngine.cs b/src/Microsoft.AspNet.Mvc.Razor/RazorViewEngine.cs index b91790bbea..a6a5f2ab10 100644 --- a/src/Microsoft.AspNet.Mvc.Razor/RazorViewEngine.cs +++ b/src/Microsoft.AspNet.Mvc.Razor/RazorViewEngine.cs @@ -3,7 +3,6 @@ using System; using System.Collections.Generic; -using System.Diagnostics; using System.Globalization; using Microsoft.AspNet.Mvc.Razor.OptionDescriptors; using Microsoft.AspNet.Mvc.Rendering; @@ -37,9 +36,6 @@ public class RazorViewEngine : IViewEngine private readonly IRazorPageFactory _pageFactory; private readonly IReadOnlyList _viewLocationExpanders; private readonly IViewLocationCache _viewLocationCache; - // The RazorViewEngine is Request scoped which allows us to cache these value for the lifetime of a Request. - private bool _isPageExecutionFeatureInitialized; - private IPageExecutionListenerFeature _pageExecutionListenerFeature; /// /// Initializes a new instance of the class. @@ -95,7 +91,7 @@ private ViewEngineResult CreateViewEngineResult(ActionContext context, { if (viewName.EndsWith(ViewExtension, StringComparison.OrdinalIgnoreCase)) { - var page = _pageFactory.CreateInstance(viewName, IsInstrumentationEnabled(context)); + var page = _pageFactory.CreateInstance(viewName); if (page != null) { return CreateFoundResult(context, page, viewName, partial); @@ -137,7 +133,7 @@ private ViewEngineResult LocateViewFromViewLocations(ActionContext context, var viewLocation = _viewLocationCache.Get(expanderContext); if (!string.IsNullOrEmpty(viewLocation)) { - var page = _pageFactory.CreateInstance(viewLocation, IsInstrumentationEnabled(context)); + var page = _pageFactory.CreateInstance(viewLocation); if (page != null) { @@ -163,9 +159,7 @@ private ViewEngineResult LocateViewFromViewLocations(ActionContext context, viewName, controllerName, areaName); - - var isInstrumentated = IsInstrumentationEnabled(context); - var page = _pageFactory.CreateInstance(transformedPath, isInstrumentated); + var page = _pageFactory.CreateInstance(transformedPath); if (page != null) { // 3a. We found a page. Cache the set of values that produced it and return a found result. @@ -190,9 +184,8 @@ private ViewEngineResult CreateFoundResult(ActionContext actionContext, var services = actionContext.HttpContext.RequestServices; var view = services.GetService(); - Debug.Assert(_isPageExecutionFeatureInitialized, "IsInstrumentationEnabled must be called prior to this."); - view.Contextualize(page, partial, _pageExecutionListenerFeature); + view.Contextualize(page, partial); return ViewEngineResult.Found(viewName, view); } @@ -200,16 +193,5 @@ private static bool IsSpecificPath(string name) { return name[0] == '~' || name[0] == '/'; } - - private bool IsInstrumentationEnabled(ActionContext context) - { - if (!_isPageExecutionFeatureInitialized) - { - _isPageExecutionFeatureInitialized = true; - _pageExecutionListenerFeature = context.HttpContext.GetFeature(); - } - - return _pageExecutionListenerFeature != null; - } } } diff --git a/src/Microsoft.AspNet.Mvc.Razor/ViewStartProvider.cs b/src/Microsoft.AspNet.Mvc.Razor/ViewStartProvider.cs index a22e73d996..f99d1a61d7 100644 --- a/src/Microsoft.AspNet.Mvc.Razor/ViewStartProvider.cs +++ b/src/Microsoft.AspNet.Mvc.Razor/ViewStartProvider.cs @@ -23,10 +23,10 @@ public ViewStartProvider(IApplicationEnvironment appEnv, } /// - public IEnumerable GetViewStartPages([NotNull] string path, bool enableInstrumentation) + public IEnumerable GetViewStartPages([NotNull] string path) { var viewStartLocations = ViewStartUtility.GetViewStartLocations(_fileSystem, path); - var viewStarts = viewStartLocations.Select(p => _pageFactory.CreateInstance(p, enableInstrumentation)) + var viewStarts = viewStartLocations.Select(_pageFactory.CreateInstance) .Where(p => p != null) .ToArray(); diff --git a/src/Microsoft.AspNet.Mvc.Razor/VirtualPathRazorPageFactory.cs b/src/Microsoft.AspNet.Mvc.Razor/VirtualPathRazorPageFactory.cs index 8c6d8732d2..3fd41f329a 100644 --- a/src/Microsoft.AspNet.Mvc.Razor/VirtualPathRazorPageFactory.cs +++ b/src/Microsoft.AspNet.Mvc.Razor/VirtualPathRazorPageFactory.cs @@ -2,6 +2,8 @@ // 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.PageExecutionInstrumentation; using Microsoft.Framework.DependencyInjection; namespace Microsoft.AspNet.Mvc.Razor @@ -16,9 +18,22 @@ public class VirtualPathRazorPageFactory : IRazorPageFactory private readonly IServiceProvider _serviceProvider; private readonly IFileInfoCache _fileInfoCache; private readonly ICompilerCache _compilerCache; - + private readonly bool _isInstrumentationEnabled; private IRazorCompilationService _razorcompilationService; + public VirtualPathRazorPageFactory(ITypeActivator typeActivator, + IServiceProvider serviceProvider, + ICompilerCache compilerCache, + IFileInfoCache fileInfoCache, + IContextAccessor contextAccessor) + { + _activator = typeActivator; + _serviceProvider = serviceProvider; + _compilerCache = compilerCache; + _fileInfoCache = fileInfoCache; + _isInstrumentationEnabled = IsInstrumentationEnabled(contextAccessor.Value); + } + private IRazorCompilationService RazorCompilationService { get @@ -27,26 +42,15 @@ private IRazorCompilationService RazorCompilationService { // it is ok to use the cached service provider because this service has // a lifetime of Scoped. - _razorcompilationService = _serviceProvider.GetService(); + _razorcompilationService = _serviceProvider.GetService(); } return _razorcompilationService; } } - public VirtualPathRazorPageFactory(ITypeActivator typeActivator, - IServiceProvider serviceProvider, - ICompilerCache compilerCache, - IFileInfoCache fileInfoCache) - { - _activator = typeActivator; - _serviceProvider = serviceProvider; - _compilerCache = compilerCache; - _fileInfoCache = fileInfoCache; - } - /// - public IRazorPage CreateInstance([NotNull] string relativePath, bool enableInstrumentation) + public IRazorPage CreateInstance([NotNull] string relativePath) { if (relativePath.StartsWith("~/", StringComparison.Ordinal)) { @@ -64,8 +68,10 @@ public IRazorPage CreateInstance([NotNull] string relativePath, bool enableInstr RelativePath = relativePath, }; - var result = _compilerCache.GetOrAdd(relativeFileInfo, enableInstrumentation, () => - RazorCompilationService.Compile(relativeFileInfo, enableInstrumentation)); + var result = _compilerCache.GetOrAdd( + relativeFileInfo, + _isInstrumentationEnabled, + () => RazorCompilationService.Compile(relativeFileInfo, _isInstrumentationEnabled)); var page = (IRazorPage)_activator.CreateInstance(_serviceProvider, result.CompiledType); page.Path = relativePath; @@ -75,5 +81,10 @@ public IRazorPage CreateInstance([NotNull] string relativePath, bool enableInstr return null; } + + private static bool IsInstrumentationEnabled(HttpContext context) + { + return context.GetFeature() != null; + } } } diff --git a/src/Microsoft.AspNet.Mvc/MvcServices.cs b/src/Microsoft.AspNet.Mvc/MvcServices.cs index fdd3c2d64b..9154842654 100644 --- a/src/Microsoft.AspNet.Mvc/MvcServices.cs +++ b/src/Microsoft.AspNet.Mvc/MvcServices.cs @@ -59,7 +59,8 @@ public static IEnumerable GetDefaultServices(IConfiguration // specific services. yield return describe.Transient(); yield return describe.Scoped(); - yield return describe.Singleton(); + // The ViewStartProvider needs to be able to consume scoped instances of IRazorPageFactory + yield return describe.Scoped(); yield return describe.Transient(); // Transient since the IViewLocationExpanders returned by the instance is cached by view engines. diff --git a/test/Microsoft.AspNet.Mvc.Razor.Test/RazorViewEngineTest.cs b/test/Microsoft.AspNet.Mvc.Razor.Test/RazorViewEngineTest.cs index e927d08de2..9dea394ec9 100644 --- a/test/Microsoft.AspNet.Mvc.Razor.Test/RazorViewEngineTest.cs +++ b/test/Microsoft.AspNet.Mvc.Razor.Test/RazorViewEngineTest.cs @@ -183,7 +183,7 @@ public void FindView_ReturnsRazorView_IfLookupWasSuccessful() // Arrange var pageFactory = new Mock(); var page = Mock.Of(); - pageFactory.Setup(p => p.CreateInstance(It.IsAny(), false)) + pageFactory.Setup(p => p.CreateInstance(It.IsAny())) .Returns(Mock.Of()); var viewEngine = CreateViewEngine(pageFactory.Object); var context = GetActionContext(_controllerTestContext); @@ -203,7 +203,7 @@ public void FindView_UsesViewLocationFormat_IfRouteDoesNotContainArea() // Arrange var pageFactory = new Mock(); var page = Mock.Of(); - pageFactory.Setup(p => p.CreateInstance("fake-path1/bar/test-view.rzr", false)) + pageFactory.Setup(p => p.CreateInstance("fake-path1/bar/test-view.rzr")) .Returns(Mock.Of()) .Verifiable(); var viewEngine = new OverloadedLocationViewEngine(pageFactory.Object, @@ -224,7 +224,7 @@ public void FindView_UsesAreaViewLocationFormat_IfRouteContainsArea() // Arrange var pageFactory = new Mock(); var page = Mock.Of(); - pageFactory.Setup(p => p.CreateInstance("fake-area-path/foo/bar/test-view2.rzr", false)) + pageFactory.Setup(p => p.CreateInstance("fake-area-path/foo/bar/test-view2.rzr")) .Returns(Mock.Of()) .Verifiable(); var viewEngine = new OverloadedLocationViewEngine(pageFactory.Object, @@ -273,7 +273,7 @@ public void FindView_UsesViewLocationExpandersToLocateViews(IDictionary(); - pageFactory.Setup(p => p.CreateInstance("test-string/bar.cshtml", false)) + pageFactory.Setup(p => p.CreateInstance("test-string/bar.cshtml")) .Returns(Mock.Of()) .Verifiable(); var expander1Result = new[] { "some-seed" }; @@ -325,9 +325,9 @@ public void FindView_CachesValuesIfViewWasFound() { // Arrange var pageFactory = new Mock(); - pageFactory.Setup(p => p.CreateInstance("/Views/bar/baz.cshtml", false)) + pageFactory.Setup(p => p.CreateInstance("/Views/bar/baz.cshtml")) .Verifiable(); - pageFactory.Setup(p => p.CreateInstance("/Views/Shared/baz.cshtml", false)) + pageFactory.Setup(p => p.CreateInstance("/Views/Shared/baz.cshtml")) .Returns(Mock.Of()) .Verifiable(); var cache = GetViewLocationCache(); @@ -353,7 +353,7 @@ public void FindView_UsesCachedValueIfViewWasFound() { // Arrange var pageFactory = new Mock(MockBehavior.Strict); - pageFactory.Setup(p => p.CreateInstance("some-view-location", false)) + pageFactory.Setup(p => p.CreateInstance("some-view-location")) .Returns(Mock.Of()) .Verifiable(); var expander = new Mock(MockBehavior.Strict); @@ -384,10 +384,10 @@ public void FindView_LooksForViewsIfCachedViewDoesNotExist() { // Arrange var pageFactory = new Mock(); - pageFactory.Setup(p => p.CreateInstance("expired-location", false)) + pageFactory.Setup(p => p.CreateInstance("expired-location")) .Returns((IRazorPage)null) .Verifiable(); - pageFactory.Setup(p => p.CreateInstance("some-view-location", false)) + pageFactory.Setup(p => p.CreateInstance("some-view-location")) .Returns(Mock.Of()) .Verifiable(); var cacheMock = new Mock(); diff --git a/test/Microsoft.AspNet.Mvc.Razor.Test/RazorViewTest.cs b/test/Microsoft.AspNet.Mvc.Razor.Test/RazorViewTest.cs index 009273a94c..5c4ac4bfc7 100644 --- a/test/Microsoft.AspNet.Mvc.Razor.Test/RazorViewTest.cs +++ b/test/Microsoft.AspNet.Mvc.Razor.Test/RazorViewTest.cs @@ -47,7 +47,7 @@ public async Task RenderAsync_AsPartial_DoesNotCreateOutputBuffer() var view = new RazorView(Mock.Of(), Mock.Of(), CreateViewStartProvider()); - view.Contextualize(page, isPartial: true, pageExecutionListener: null); + view.Contextualize(page, isPartial: true); var viewContext = CreateViewContext(view); var expected = viewContext.Writer; @@ -74,7 +74,7 @@ public async Task RenderAsync_AsPartial_ActivatesViews_WithThePassedInViewContex var view = new RazorView(Mock.Of(), activator.Object, CreateViewStartProvider()); - view.Contextualize(page, isPartial: true, pageExecutionListener: null); + view.Contextualize(page, isPartial: true); var viewContext = CreateViewContext(view); var expectedWriter = viewContext.Writer; activator.Setup(a => a.Activate(page, It.IsAny())) @@ -104,7 +104,7 @@ public async Task RenderAsync_AsPartial_ActivatesViews() var view = new RazorView(Mock.Of(), activator.Object, CreateViewStartProvider()); - view.Contextualize(page, isPartial: true, pageExecutionListener: null); + view.Contextualize(page, isPartial: true); var viewContext = CreateViewContext(view); // Act @@ -126,16 +126,16 @@ public async Task RenderAsync_AsPartial_DoesNotExecuteLayoutOrViewStartPages() var view = new RazorView(pageFactory.Object, Mock.Of(), viewStartProvider); - view.Contextualize(page, isPartial: true, pageExecutionListener: null); + view.Contextualize(page, isPartial: true); var viewContext = CreateViewContext(view); // Act await view.RenderAsync(viewContext); // Assert - pageFactory.Verify(v => v.CreateInstance(It.IsAny(), It.IsAny()), Times.Never()); + pageFactory.Verify(v => v.CreateInstance(It.IsAny()), Times.Never()); Mock.Get(viewStartProvider) - .Verify(v => v.GetViewStartPages(It.IsAny(), It.IsAny()), Times.Never()); + .Verify(v => v.GetViewStartPages(It.IsAny()), Times.Never()); } [Fact] @@ -150,7 +150,7 @@ public async Task RenderAsync_CreatesOutputBuffer() var view = new RazorView(Mock.Of(), Mock.Of(), CreateViewStartProvider()); - view.Contextualize(page, isPartial: false, pageExecutionListener: null); + view.Contextualize(page, isPartial: false); var viewContext = CreateViewContext(view); var original = viewContext.Writer; @@ -173,7 +173,7 @@ public async Task RenderAsync_CopiesBufferedContentToOutput() var view = new RazorView(Mock.Of(), Mock.Of(), CreateViewStartProvider()); - view.Contextualize(page, isPartial: false, pageExecutionListener: null); + view.Contextualize(page, isPartial: false); var viewContext = CreateViewContext(view); var original = viewContext.Writer; @@ -198,7 +198,7 @@ public async Task RenderAsync_ActivatesPages() var view = new RazorView(Mock.Of(), activator.Object, CreateViewStartProvider()); - view.Contextualize(page, isPartial: false, pageExecutionListener: null); + view.Contextualize(page, isPartial: false); var viewContext = CreateViewContext(view); @@ -240,7 +240,7 @@ public async Task RenderAsync_ExecutesViewStart() var view = new RazorView(Mock.Of(), activator.Object, CreateViewStartProvider(viewStart1, viewStart2)); - view.Contextualize(page, isPartial: false, pageExecutionListener: null); + view.Contextualize(page, isPartial: false); var viewContext = CreateViewContext(view); // Act @@ -288,13 +288,13 @@ public async Task RenderAsync_ExecutesLayoutPages() activator.Setup(a => a.Activate(layout, It.IsAny())) .Verifiable(); var pageFactory = new Mock(); - pageFactory.Setup(p => p.CreateInstance(LayoutPath, false)) + pageFactory.Setup(p => p.CreateInstance(LayoutPath)) .Returns(layout); var view = new RazorView(pageFactory.Object, activator.Object, CreateViewStartProvider()); - view.Contextualize(page, isPartial: false, pageExecutionListener: null); + view.Contextualize(page, isPartial: false); var viewContext = CreateViewContext(view); // Act @@ -321,13 +321,13 @@ public async Task RenderAsync_ThrowsIfSectionsWereDefinedButNotRendered() v.RenderBodyPublic(); }); var pageFactory = new Mock(); - pageFactory.Setup(p => p.CreateInstance(LayoutPath, false)) + pageFactory.Setup(p => p.CreateInstance(LayoutPath)) .Returns(layout); var view = new RazorView(pageFactory.Object, Mock.Of(), CreateViewStartProvider()); - view.Contextualize(page, isPartial: false, pageExecutionListener: null); + view.Contextualize(page, isPartial: false); var viewContext = CreateViewContext(view); // Act and Assert @@ -347,13 +347,13 @@ public async Task RenderAsync_ThrowsIfBodyWasNotRendered() { }); var pageFactory = new Mock(); - pageFactory.Setup(p => p.CreateInstance(LayoutPath, false)) + pageFactory.Setup(p => p.CreateInstance(LayoutPath)) .Returns(layout); var view = new RazorView(pageFactory.Object, Mock.Of(), CreateViewStartProvider()); - view.Contextualize(page, isPartial: false, pageExecutionListener: null); + view.Contextualize(page, isPartial: false); var viewContext = CreateViewContext(view); // Act and Assert @@ -399,15 +399,15 @@ public async Task RenderAsync_ExecutesNestedLayoutPages() v.RenderBodyPublic(); }); var pageFactory = new Mock(); - pageFactory.Setup(p => p.CreateInstance("~/Shared/Layout1.cshtml", false)) + pageFactory.Setup(p => p.CreateInstance("~/Shared/Layout1.cshtml")) .Returns(layout1); - pageFactory.Setup(p => p.CreateInstance("~/Shared/Layout2.cshtml", false)) + pageFactory.Setup(p => p.CreateInstance("~/Shared/Layout2.cshtml")) .Returns(layout2); var view = new RazorView(pageFactory.Object, Mock.Of(), CreateViewStartProvider()); - view.Contextualize(page, isPartial: false, pageExecutionListener: null); + view.Contextualize(page, isPartial: false); var viewContext = CreateViewContext(view); // Act @@ -447,13 +447,13 @@ body content }); var pageFactory = new Mock(); - pageFactory.Setup(p => p.CreateInstance("layout-1", false)) + pageFactory.Setup(p => p.CreateInstance("layout-1")) .Returns(layout1); var view = new RazorView(pageFactory.Object, Mock.Of(), CreateViewStartProvider()); - view.Contextualize(page, isPartial: false, pageExecutionListener: null); + view.Contextualize(page, isPartial: false); var viewContext = CreateViewContext(view); // Act @@ -491,13 +491,13 @@ public async Task FlushAsync_DoesNotThrowWhenInvokedInsideOfASection() }); var pageFactory = new Mock(); - pageFactory.Setup(p => p.CreateInstance("layout-1", false)) + pageFactory.Setup(p => p.CreateInstance("layout-1")) .Returns(layout1); var view = new RazorView(pageFactory.Object, Mock.Of(), CreateViewStartProvider()); - view.Contextualize(page, isPartial: false, pageExecutionListener: null); + view.Contextualize(page, isPartial: false); var viewContext = CreateViewContext(view); // Act @@ -523,7 +523,7 @@ public async Task RenderAsync_ThrowsIfLayoutIsSpecifiedWhenNotBuffered() var view = new RazorView(Mock.Of(), Mock.Of(), CreateViewStartProvider()); - view.Contextualize(page, isPartial: false, pageExecutionListener: null); + view.Contextualize(page, isPartial: false); var viewContext = CreateViewContext(view); // Act and Assert @@ -558,13 +558,13 @@ public async Task RenderAsync_ThrowsIfFlushWasInvokedInsideRenderedSectionAndLay v.Layout = "~/Shared/Layout2.cshtml"; }); var pageFactory = new Mock(); - pageFactory.Setup(p => p.CreateInstance("~/Shared/Layout1.cshtml", false)) + pageFactory.Setup(p => p.CreateInstance("~/Shared/Layout1.cshtml")) .Returns(layout1); var view = new RazorView(pageFactory.Object, Mock.Of(), CreateViewStartProvider()); - view.Contextualize(page, isPartial: false, pageExecutionListener: null); + view.Contextualize(page, isPartial: false); var viewContext = CreateViewContext(view); // Act and Assert @@ -627,17 +627,18 @@ public async Task RenderAsync_UsesPageExecutionFeatureFromRequest_ToWrapWriter() layout.Path = "/Layout.cshtml"; var pageFactory = new Mock(); - pageFactory.Setup(p => p.CreateInstance("/Layout.cshtml", true)) + pageFactory.Setup(p => p.CreateInstance("/Layout.cshtml")) .Returns(layout); var viewStartProvider = new Mock(); - viewStartProvider.Setup(v => v.GetViewStartPages(It.IsAny(), true)) + viewStartProvider.Setup(v => v.GetViewStartPages(It.IsAny())) .Returns(Enumerable.Empty()) .Verifiable(); var view = new RazorView(pageFactory.Object, Mock.Of(), viewStartProvider.Object); - view.Contextualize(page, isPartial: false, pageExecutionListener: feature.Object); + view.Contextualize(page, isPartial: false); var viewContext = CreateViewContext(view); + viewContext.HttpContext.SetFeature(feature.Object); // Act await view.RenderAsync(viewContext); @@ -672,9 +673,10 @@ public async Task RenderAsync_UsesPageExecutionFeatureFromRequest_ToGetExecution var view = new RazorView(Mock.Of(), Mock.Of(), Mock.Of()); - view.Contextualize(page, isPartial: true, pageExecutionListener: feature.Object); + view.Contextualize(page, isPartial: true); var viewContext = CreateViewContext(view); viewContext.Writer = writer; + viewContext.HttpContext.SetFeature(feature.Object); // Act await view.RenderAsync(viewContext); @@ -700,7 +702,7 @@ public async Task RenderAsync_DoesNotSetExecutionContextWhenListenerIsNotRegiste var view = new RazorView(Mock.Of(), Mock.Of(), Mock.Of()); - view.Contextualize(page, isPartial, pageExecutionListener: null); + view.Contextualize(page, isPartial); var viewContext = CreateViewContext(view); // Act @@ -734,7 +736,7 @@ private static IViewStartProvider CreateViewStartProvider(params IRazorPage[] vi { viewStartPages = viewStartPages ?? new IRazorPage[0]; var viewStartProvider = new Mock(); - viewStartProvider.Setup(v => v.GetViewStartPages(It.IsAny(), false)) + viewStartProvider.Setup(v => v.GetViewStartPages(It.IsAny())) .Returns(viewStartPages); return viewStartProvider.Object; From 18e11f546d76c949350b2c796a16f8abcfbae1cc Mon Sep 17 00:00:00 2001 From: Pranav K Date: Fri, 10 Oct 2014 10:53:12 -0700 Subject: [PATCH 17/39] DecorateWriter does not get called for partial views rendered via Html.PartialAsync * Introducing StringCollectionTextWriter to buffer the contents of PartialAsync * Ensure DecorateWriter is called for partial views Fixes #1266 --- .../Rendering}/BufferEntryCollection.cs | 2 +- .../Rendering/Html/HtmlHelper.cs | 4 +- .../Rendering/HtmlString.cs | 34 +++ .../Rendering/StringCollectionTextWriter.cs | 190 +++++++++++++++ src/Microsoft.AspNet.Mvc.Razor/RazorPage.cs | 2 +- .../RazorTextWriter.cs | 224 +++++------------- src/Microsoft.AspNet.Mvc.Razor/RazorView.cs | 19 +- .../Rendering}/BufferEntryCollectionTest.cs | 2 +- .../StringCollectionTextWriterTest.cs | 160 +++++++++++++ .../RazorInstrumentationTests.cs | 149 ++++++++---- .../RazorPageTest.cs | 27 +++ .../RazorTextWriterTest.cs | 38 +-- .../RazorViewTest.cs | 17 +- .../HomeController.cs | 5 + .../Views/Home/VIewWithPartial.cshtml | 5 + .../Views/Home/_PartialView.cshtml | 4 + 16 files changed, 647 insertions(+), 235 deletions(-) rename src/{Microsoft.AspNet.Mvc.Razor => Microsoft.AspNet.Mvc.Core/Rendering}/BufferEntryCollection.cs (99%) create mode 100644 src/Microsoft.AspNet.Mvc.Core/Rendering/StringCollectionTextWriter.cs rename test/{Microsoft.AspNet.Mvc.Razor.Test => Microsoft.AspNet.Mvc.Core.Test/Rendering}/BufferEntryCollectionTest.cs (99%) create mode 100644 test/Microsoft.AspNet.Mvc.Core.Test/Rendering/StringCollectionTextWriterTest.cs create mode 100644 test/WebSites/RazorInstrumentationWebsite/Views/Home/VIewWithPartial.cshtml create mode 100644 test/WebSites/RazorInstrumentationWebsite/Views/Home/_PartialView.cshtml diff --git a/src/Microsoft.AspNet.Mvc.Razor/BufferEntryCollection.cs b/src/Microsoft.AspNet.Mvc.Core/Rendering/BufferEntryCollection.cs similarity index 99% rename from src/Microsoft.AspNet.Mvc.Razor/BufferEntryCollection.cs rename to src/Microsoft.AspNet.Mvc.Core/Rendering/BufferEntryCollection.cs index a4e5d28bdd..0a97d95169 100644 --- a/src/Microsoft.AspNet.Mvc.Razor/BufferEntryCollection.cs +++ b/src/Microsoft.AspNet.Mvc.Core/Rendering/BufferEntryCollection.cs @@ -6,7 +6,7 @@ using System.Collections.Generic; using System.Diagnostics; -namespace Microsoft.AspNet.Mvc.Razor +namespace Microsoft.AspNet.Mvc.Rendering { /// /// Represents a hierarchy of strings and provides an enumerator that iterates over it as a sequence. diff --git a/src/Microsoft.AspNet.Mvc.Core/Rendering/Html/HtmlHelper.cs b/src/Microsoft.AspNet.Mvc.Core/Rendering/Html/HtmlHelper.cs index ae2858ba32..8063dfeeb1 100644 --- a/src/Microsoft.AspNet.Mvc.Core/Rendering/Html/HtmlHelper.cs +++ b/src/Microsoft.AspNet.Mvc.Core/Rendering/Html/HtmlHelper.cs @@ -330,11 +330,11 @@ public string Name(string name) public async Task PartialAsync([NotNull] string partialViewName, object model, ViewDataDictionary viewData) { - using (var writer = new StringWriter(CultureInfo.CurrentCulture)) + using (var writer = new StringCollectionTextWriter(Encoding.UTF8)) { await RenderPartialCoreAsync(partialViewName, model, viewData, writer); - return new HtmlString(writer.ToString()); + return new HtmlString(writer); } } diff --git a/src/Microsoft.AspNet.Mvc.Core/Rendering/HtmlString.cs b/src/Microsoft.AspNet.Mvc.Core/Rendering/HtmlString.cs index a51265511e..c791e66719 100644 --- a/src/Microsoft.AspNet.Mvc.Core/Rendering/HtmlString.cs +++ b/src/Microsoft.AspNet.Mvc.Core/Rendering/HtmlString.cs @@ -1,12 +1,15 @@ // 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.IO; + namespace Microsoft.AspNet.Mvc.Rendering { public class HtmlString { private static readonly HtmlString _empty = new HtmlString(string.Empty); + private readonly StringCollectionTextWriter _writer; private readonly string _input; public HtmlString(string input) @@ -14,6 +17,15 @@ public HtmlString(string input) _input = input; } + /// + /// Initializes a new instance of that is backed by . + /// + /// + public HtmlString([NotNull] StringCollectionTextWriter writer) + { + _writer = writer; + } + public static HtmlString Empty { get @@ -22,8 +34,30 @@ public static HtmlString Empty } } + /// + /// Writes the value in this instance of to the target . + /// + /// The to write contents to. + public void WriteTo(TextWriter targetWriter) + { + if (_writer != null) + { + _writer.CopyTo(targetWriter); + } + else + { + targetWriter.Write(_input); + } + } + + /// public override string ToString() { + if (_writer != null) + { + return _writer.ToString(); + } + return _input; } } diff --git a/src/Microsoft.AspNet.Mvc.Core/Rendering/StringCollectionTextWriter.cs b/src/Microsoft.AspNet.Mvc.Core/Rendering/StringCollectionTextWriter.cs new file mode 100644 index 0000000000..b8475e273c --- /dev/null +++ b/src/Microsoft.AspNet.Mvc.Core/Rendering/StringCollectionTextWriter.cs @@ -0,0 +1,190 @@ +// 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.IO; +using System.Text; +using System.Threading.Tasks; + +namespace Microsoft.AspNet.Mvc.Rendering +{ + /// + /// A that represents individual write operations as a sequence of strings. + /// + /// + /// This is primarily designed to avoid creating large in-memory strings. + /// Refer to https://aspnetwebstack.codeplex.com/workitem/585 for more details. + /// + public class StringCollectionTextWriter : TextWriter + { + private static readonly Task _completedTask = Task.FromResult(0); + private readonly Encoding _encoding; + + /// + /// Creates a new instance of . + /// + /// The character in which the output is written. + public StringCollectionTextWriter(Encoding encoding) + { + _encoding = encoding; + Buffer = new BufferEntryCollection(); + } + + /// + public override Encoding Encoding + { + get { return _encoding; } + } + + /// + /// A collection of entries buffered by this instance of . + /// + public BufferEntryCollection Buffer { get; private set; } + + /// + public override void Write(char value) + { + Buffer.Add(value.ToString()); + } + + /// + public override void Write([NotNull] char[] buffer, int index, int count) + { + if (index < 0) + { + throw new ArgumentOutOfRangeException(nameof(index)); + } + if (count < 0 || (buffer.Length - index < count)) + { + throw new ArgumentOutOfRangeException(nameof(count)); + } + + Buffer.Add(buffer, index, count); + } + + /// + public override void Write(string value) + { + if (string.IsNullOrEmpty(value)) + { + return; + } + + Buffer.Add(value); + } + + /// + public override Task WriteAsync(char value) + { + Write(value); + return _completedTask; + } + + /// + public override Task WriteAsync([NotNull] char[] buffer, int index, int count) + { + Write(buffer, index, count); + return _completedTask; + } + + /// + public override Task WriteAsync(string value) + { + Write(value); + return _completedTask; + } + + /// + public override void WriteLine() + { + Buffer.Add(Environment.NewLine); + } + + /// + public override void WriteLine(string value) + { + Write(value); + WriteLine(); + } + + /// + public override Task WriteLineAsync(char value) + { + WriteLine(value); + return _completedTask; + } + + /// + public override Task WriteLineAsync(char[] value, int start, int offset) + { + WriteLine(value, start, offset); + return _completedTask; + } + + /// + public override Task WriteLineAsync(string value) + { + WriteLine(value); + return _completedTask; + } + + /// + public override Task WriteLineAsync() + { + WriteLine(); + return _completedTask; + } + + /// + public void CopyTo(TextWriter writer) + { + var targetStringCollectionWriter = writer as StringCollectionTextWriter; + if (targetStringCollectionWriter != null) + { + targetStringCollectionWriter.Buffer.Add(Buffer); + } + else + { + WriteList(writer, Buffer); + } + } + + /// + public Task CopyToAsync(TextWriter writer) + { + var targetStringCollectionWriter = writer as StringCollectionTextWriter; + if (targetStringCollectionWriter != null) + { + targetStringCollectionWriter.Buffer.Add(Buffer); + } + else + { + return WriteListAsync(writer, Buffer); + } + + return _completedTask; + } + + /// + public override string ToString() + { + return string.Join(string.Empty, Buffer); + } + + private static void WriteList(TextWriter writer, BufferEntryCollection values) + { + foreach (var value in values) + { + writer.Write(value); + } + } + + private static async Task WriteListAsync(TextWriter writer, BufferEntryCollection values) + { + foreach (var value in values) + { + await writer.WriteAsync(value); + } + } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Mvc.Razor/RazorPage.cs b/src/Microsoft.AspNet.Mvc.Razor/RazorPage.cs index 710cfb611b..3b01f1e05a 100644 --- a/src/Microsoft.AspNet.Mvc.Razor/RazorPage.cs +++ b/src/Microsoft.AspNet.Mvc.Razor/RazorPage.cs @@ -227,7 +227,7 @@ public virtual void WriteTo(TextWriter writer, object value) var htmlString = value as HtmlString; if (htmlString != null) { - writer.Write(htmlString.ToString()); + writer.Write(htmlString); } else { diff --git a/src/Microsoft.AspNet.Mvc.Razor/RazorTextWriter.cs b/src/Microsoft.AspNet.Mvc.Razor/RazorTextWriter.cs index 9ea894453c..32fdecf300 100644 --- a/src/Microsoft.AspNet.Mvc.Razor/RazorTextWriter.cs +++ b/src/Microsoft.AspNet.Mvc.Razor/RazorTextWriter.cs @@ -5,25 +5,22 @@ using System.IO; using System.Text; using System.Threading.Tasks; +using Microsoft.AspNet.Mvc.Rendering; namespace Microsoft.AspNet.Mvc.Razor { /// - /// A that represents individual write operations as a sequence of strings when buffering. - /// The writer is backed by an unbuffered writer. When Flush or FlushAsync is invoked, the writer - /// copies all content to the unbuffered writier and switches to writing to the unbuffered writer for all further - /// write operations. + /// A that is backed by a unbuffered writer (over the Response stream) and a buffered + /// . When Flush or FlushAsync is invoked, the writer + /// copies all content from the buffered writer to the unbuffered one and switches to writing to the unbuffered + /// writer for all further write operations. /// /// - /// This is primarily designed to avoid creating large in-memory strings. - /// Refer to https://aspnetwebstack.codeplex.com/workitem/585 for more details. + /// This type is designed to avoid creating large in-memory strings when buffering and supporting the contract that + /// expects. /// public class RazorTextWriter : TextWriter, IBufferedTextWriter { - private static readonly Task _completedTask = Task.FromResult(0); - private readonly TextWriter _unbufferedWriter; - private readonly Encoding _encoding; - /// /// Creates a new instance of . /// @@ -32,36 +29,44 @@ public class RazorTextWriter : TextWriter, IBufferedTextWriter /// The character in which the output is written. public RazorTextWriter(TextWriter unbufferedWriter, Encoding encoding) { - _unbufferedWriter = unbufferedWriter; - _encoding = encoding; - Buffer = new BufferEntryCollection(); + UnbufferedWriter = unbufferedWriter; + BufferedWriter = new StringCollectionTextWriter(encoding); + TargetWriter = BufferedWriter; } /// public override Encoding Encoding { - get { return _encoding; } + get { return BufferedWriter.Encoding; } } /// public bool IsBuffering { get; private set; } = true; - /// - /// A collection of entries buffered by this instance of . - /// - public BufferEntryCollection Buffer { get; private set; } + // Internal for unit testing + internal StringCollectionTextWriter BufferedWriter { get; } + + private TextWriter UnbufferedWriter { get; } + + private TextWriter TargetWriter { get; set; } /// public override void Write(char value) { - if (IsBuffering) - { - Buffer.Add(value.ToString()); - } - else + TargetWriter.Write(value); + } + + /// + public override void Write(object value) + { + var htmlString = value as HtmlString; + if (htmlString != null) { - _unbufferedWriter.Write(value); + htmlString.WriteTo(TargetWriter); + return; } + + base.Write(value); } /// @@ -69,25 +74,14 @@ public override void Write([NotNull] char[] buffer, int index, int count) { if (index < 0) { - throw new ArgumentOutOfRangeException("index"); - } - if (count < 0) - { - throw new ArgumentOutOfRangeException("count"); + throw new ArgumentOutOfRangeException(nameof(index)); } - if (buffer.Length - index < count) + if (count < 0 || (buffer.Length - index < count)) { - throw new ArgumentOutOfRangeException("count"); + throw new ArgumentOutOfRangeException(nameof(count)); } - if (IsBuffering) - { - Buffer.Add(buffer, index, count); - } - else - { - _unbufferedWriter.Write(buffer, index, count); - } + TargetWriter.Write(buffer, index, count); } /// @@ -95,141 +89,71 @@ public override void Write(string value) { if (!string.IsNullOrEmpty(value)) { - if (IsBuffering) - { - Buffer.Add(value); - } - else - { - _unbufferedWriter.Write(value); - } + TargetWriter.Write(value); } } /// public override Task WriteAsync(char value) { - if (IsBuffering) - { - Write(value); - return _completedTask; - } - else - { - return _unbufferedWriter.WriteAsync(value); - } + return TargetWriter.WriteAsync(value); } /// public override Task WriteAsync([NotNull] char[] buffer, int index, int count) { - if (IsBuffering) + if (index < 0) { - Write(buffer, index, count); - return _completedTask; + throw new ArgumentOutOfRangeException(nameof(index)); } - else + if (count < 0 || (buffer.Length - index < count)) { - return _unbufferedWriter.WriteAsync(buffer, index, count); + throw new ArgumentOutOfRangeException(nameof(count)); } + return TargetWriter.WriteAsync(buffer, index, count); } /// public override Task WriteAsync(string value) { - if (IsBuffering) - { - Write(value); - return _completedTask; - } - else - { - return _unbufferedWriter.WriteAsync(value); - } + return TargetWriter.WriteAsync(value); } /// public override void WriteLine() { - if (IsBuffering) - { - Buffer.Add(Environment.NewLine); - } - else - { - _unbufferedWriter.WriteLine(); - } + TargetWriter.WriteLine(); } /// public override void WriteLine(string value) { - if (IsBuffering) - { - Write(value); - WriteLine(); - } - else - { - _unbufferedWriter.WriteLine(value); - } + TargetWriter.WriteLine(value); } /// public override Task WriteLineAsync(char value) { - if (IsBuffering) - { - WriteLine(value); - return _completedTask; - } - else - { - return _unbufferedWriter.WriteLineAsync(value); - } + return TargetWriter.WriteLineAsync(value); } /// public override Task WriteLineAsync(char[] value, int start, int offset) { - if (IsBuffering) - { - WriteLine(value, start, offset); - return _completedTask; - } - else - { - return _unbufferedWriter.WriteLineAsync(value, start, offset); - } + return TargetWriter.WriteLineAsync(value, start, offset); } /// public override Task WriteLineAsync(string value) { - if (IsBuffering) - { - WriteLine(value); - return _completedTask; - } - else - { - return _unbufferedWriter.WriteLineAsync(value); - } + return TargetWriter.WriteLineAsync(value); } /// public override Task WriteLineAsync() { - if (IsBuffering) - { - WriteLine(); - return _completedTask; - } - else - { - return _unbufferedWriter.WriteLineAsync(); - } + return TargetWriter.WriteLineAsync(); } /// @@ -242,10 +166,11 @@ public override void Flush() if (IsBuffering) { IsBuffering = false; - CopyTo(_unbufferedWriter); + TargetWriter = UnbufferedWriter; + CopyTo(UnbufferedWriter); } - _unbufferedWriter.Flush(); + UnbufferedWriter.Flush(); } /// @@ -259,60 +184,37 @@ public override async Task FlushAsync() if (IsBuffering) { IsBuffering = false; - await CopyToAsync(_unbufferedWriter); + TargetWriter = UnbufferedWriter; + await CopyToAsync(UnbufferedWriter); } - await _unbufferedWriter.FlushAsync(); + await UnbufferedWriter.FlushAsync(); } /// public void CopyTo(TextWriter writer) { - var targetRazorTextWriter = writer as RazorTextWriter; - if (targetRazorTextWriter != null && targetRazorTextWriter.IsBuffering) - { - targetRazorTextWriter.Buffer.Add(Buffer); - } - else - { - // If the target writer is not buffering, we can directly copy to it's unbuffered writer - var targetWriter = targetRazorTextWriter != null ? targetRazorTextWriter._unbufferedWriter : writer; - WriteList(targetWriter, Buffer); - } + writer = UnWrapRazorTextWriter(writer); + BufferedWriter.CopyTo(writer); } /// public Task CopyToAsync(TextWriter writer) { - var targetRazorTextWriter = writer as RazorTextWriter; - if (targetRazorTextWriter != null && targetRazorTextWriter.IsBuffering) - { - targetRazorTextWriter.Buffer.Add(Buffer); - } - else - { - // If the target writer is not buffering, we can directly copy to it's unbuffered writer - var targetWriter = targetRazorTextWriter != null ? targetRazorTextWriter._unbufferedWriter : writer; - return WriteListAsync(targetWriter, Buffer); - } - - return _completedTask; + writer = UnWrapRazorTextWriter(writer); + return BufferedWriter.CopyToAsync(writer); } - private static void WriteList(TextWriter writer, BufferEntryCollection values) + private static TextWriter UnWrapRazorTextWriter(TextWriter writer) { - foreach (var value in values) + var targetRazorTextWriter = writer as RazorTextWriter; + if (targetRazorTextWriter != null) { - writer.Write(value); + writer = targetRazorTextWriter.IsBuffering ? targetRazorTextWriter.BufferedWriter : + targetRazorTextWriter.UnbufferedWriter; } - } - private static async Task WriteListAsync(TextWriter writer, BufferEntryCollection values) - { - foreach (var value in values) - { - await writer.WriteAsync(value); - } + return writer; } } } \ No newline at end of file diff --git a/src/Microsoft.AspNet.Mvc.Razor/RazorView.cs b/src/Microsoft.AspNet.Mvc.Razor/RazorView.cs index 6064bea6fd..2ea9e674f6 100644 --- a/src/Microsoft.AspNet.Mvc.Razor/RazorView.cs +++ b/src/Microsoft.AspNet.Mvc.Razor/RazorView.cs @@ -61,13 +61,30 @@ public virtual async Task RenderAsync([NotNull] ViewContext context) _pageExecutionFeature = context.HttpContext.GetFeature(); - if (!_isPartial) + if (_isPartial) + { + await RenderPartialAsync(context); + } + else { var bodyWriter = await RenderPageAsync(_razorPage, context, executeViewStart: true); await RenderLayoutAsync(context, bodyWriter); } + } + + private async Task RenderPartialAsync(ViewContext context) + { + if (EnableInstrumentation) + { + // When instrmenting, we need to Decorate the output in an instrumented writer which + // RenderPageAsync does. + var bodyWriter = await RenderPageAsync(_razorPage, context, executeViewStart: false); + await bodyWriter.CopyToAsync(context.Writer); + } else { + // For the non-instrumented case, we don't need to buffer contents. For Html.Partial, the writer is + // an in memory writer and for Partial views, we directly write to the Response. await RenderPageCoreAsync(_razorPage, context); } } diff --git a/test/Microsoft.AspNet.Mvc.Razor.Test/BufferEntryCollectionTest.cs b/test/Microsoft.AspNet.Mvc.Core.Test/Rendering/BufferEntryCollectionTest.cs similarity index 99% rename from test/Microsoft.AspNet.Mvc.Razor.Test/BufferEntryCollectionTest.cs rename to test/Microsoft.AspNet.Mvc.Core.Test/Rendering/BufferEntryCollectionTest.cs index c0d7b2aedc..251634f016 100644 --- a/test/Microsoft.AspNet.Mvc.Razor.Test/BufferEntryCollectionTest.cs +++ b/test/Microsoft.AspNet.Mvc.Core.Test/Rendering/BufferEntryCollectionTest.cs @@ -6,7 +6,7 @@ using System.Linq; using Xunit; -namespace Microsoft.AspNet.Mvc.Razor +namespace Microsoft.AspNet.Mvc.Rendering { public class BufferEntryCollectionTest { diff --git a/test/Microsoft.AspNet.Mvc.Core.Test/Rendering/StringCollectionTextWriterTest.cs b/test/Microsoft.AspNet.Mvc.Core.Test/Rendering/StringCollectionTextWriterTest.cs new file mode 100644 index 0000000000..7497cf8c7b --- /dev/null +++ b/test/Microsoft.AspNet.Mvc.Core.Test/Rendering/StringCollectionTextWriterTest.cs @@ -0,0 +1,160 @@ +// 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 System.IO; +using System.Text; +using System.Threading.Tasks; +using Microsoft.AspNet.Testing; +using Xunit; + +namespace Microsoft.AspNet.Mvc.Rendering +{ + public class StringCollectionTextWriterTest + { + [Fact] + [ReplaceCulture] + public void Write_WritesDataTypes_ToBuffer() + { + // Arrange + var expected = new[] { "True", "3", "18446744073709551615", "Hello world", "3.14", "2.718", "m" }; + var writer = new StringCollectionTextWriter(Encoding.UTF8); + + // Act + writer.Write(true); + writer.Write(3); + writer.Write(ulong.MaxValue); + writer.Write(new TestClass()); + writer.Write(3.14); + writer.Write(2.718m); + writer.Write('m'); + + // Assert + Assert.Equal(expected, writer.Buffer.BufferEntries); + } + + [Fact] + [ReplaceCulture] + public void WriteLine_WritesDataTypes_ToBuffer() + { + // Arrange + var newLine = Environment.NewLine; + var expected = new List { "False", newLine, "1.1", newLine, "3", newLine }; + var writer = new StringCollectionTextWriter(Encoding.UTF8); + + // Act + writer.WriteLine(false); + writer.WriteLine(1.1f); + writer.WriteLine(3L); + + // Assert + Assert.Equal(expected, writer.Buffer.BufferEntries); + } + + [Fact] + public async Task Write_WritesCharBuffer() + { + // Arrange + var input1 = new ArraySegment(new char[] { 'a', 'b', 'c', 'd' }, 1, 3); + var input2 = new ArraySegment(new char[] { 'e', 'f' }, 0, 2); + var input3 = new ArraySegment(new char[] { 'g', 'h', 'i', 'j' }, 3, 1); + var writer = new StringCollectionTextWriter(Encoding.UTF8); + + // Act + writer.Write(input1.Array, input1.Offset, input1.Count); + await writer.WriteAsync(input2.Array, input2.Offset, input2.Count); + await writer.WriteLineAsync(input3.Array, input3.Offset, input3.Count); + + // Assert + var buffer = writer.Buffer.BufferEntries; + Assert.Equal(4, buffer.Count); + Assert.Equal("bcd", buffer[0]); + Assert.Equal("ef", buffer[1]); + Assert.Equal("j", buffer[2]); + Assert.Equal(Environment.NewLine, buffer[3]); + } + + [Fact] + public async Task WriteLines_WritesCharBuffer() + { + // Arrange + var newLine = Environment.NewLine; + var writer = new StringCollectionTextWriter(Encoding.UTF8); + + // Act + writer.WriteLine(); + await writer.WriteLineAsync(); + + // Assert + var actual = writer.Buffer.BufferEntries; + Assert.Equal(new[] { newLine, newLine }, actual); + } + + [Fact] + public async Task Write_WritesStringBuffer() + { + // Arrange + var newLine = Environment.NewLine; + var input1 = "Hello"; + var input2 = "from"; + var input3 = "ASP"; + var input4 = ".Net"; + var writer = new StringCollectionTextWriter(Encoding.UTF8); + + // Act + writer.Write(input1); + writer.WriteLine(input2); + await writer.WriteAsync(input3); + await writer.WriteLineAsync(input4); + + // Assert + var actual = writer.Buffer.BufferEntries; + Assert.Equal(new[] { input1, input2, newLine, input3, input4, newLine }, actual); + } + + [Fact] + public void Copy_CopiesContent_IfTargetTextWriterIsAStringCollectionTextWriter() + { + // Arrange + var source = new StringCollectionTextWriter(Encoding.UTF8); + var target = new StringCollectionTextWriter(Encoding.UTF8); + + // Act + source.Write("Hello world"); + source.Write(new char[1], 0, 1); + source.CopyTo(target); + + // Assert + // Make sure content was written to the source. + Assert.Equal(2, source.Buffer.BufferEntries.Count); + Assert.Equal(1, target.Buffer.BufferEntries.Count); + Assert.Same(source.Buffer.BufferEntries, target.Buffer.BufferEntries[0]); + } + + [Fact] + public void Copy_WritesContent_IfTargetTextWriterIsNotAStringCollectionTextWriter() + { + // Arrange + var source = new StringCollectionTextWriter(Encoding.UTF8); + var target = new StringWriter(); + var expected = @"Hello world" + Environment.NewLine + "abc"; + + // Act + source.WriteLine("Hello world"); + source.Write(new[] { 'x', 'a', 'b', 'c' }, 1, 3); + source.CopyTo(target); + + // Assert + Assert.Equal(expected, target.ToString()); + } + + private class TestClass + { + public override string ToString() + { + return "Hello world"; + } + } + } +} \ No newline at end of file diff --git a/test/Microsoft.AspNet.Mvc.FunctionalTests/RazorInstrumentationTests.cs b/test/Microsoft.AspNet.Mvc.FunctionalTests/RazorInstrumentationTests.cs index bc0e0a88bf..d3763ea973 100644 --- a/test/Microsoft.AspNet.Mvc.FunctionalTests/RazorInstrumentationTests.cs +++ b/test/Microsoft.AspNet.Mvc.FunctionalTests/RazorInstrumentationTests.cs @@ -17,44 +17,87 @@ public class RazorInstrumentationTests { private readonly IServiceProvider _services = TestHelper.CreateServices("RazorInstrumentationWebsite"); private readonly Action _app = new Startup().Configure; - private readonly string _expected = string.Join(Environment.NewLine, - @"
", - @"2147483647", - "", - @"viewstart-content", - @"

", - @"page-content", - @"

", - @"
"); - private readonly IEnumerable> _expectedLineMappings = new[] - { - Tuple.Create(93, 2, true), - Tuple.Create(96, 16, false), - Tuple.Create(112, 2, true), - Tuple.Create(0, 2, true), - Tuple.Create(2, 8, true), - Tuple.Create(10, 16, false), - Tuple.Create(26, 1, true), - Tuple.Create(27, 21, true), - Tuple.Create(0, 7, true), - Tuple.Create(8, 12, false), - Tuple.Create(20, 2, true), - Tuple.Create(23, 12, false), - Tuple.Create(35, 8, true), - }; - - public static IEnumerable ActionNames + + public static IEnumerable InstrumentationData { get { - yield return new[] { "FullPath" }; - yield return new[] { "ViewDiscoveryPath" }; + var expected = string.Join(Environment.NewLine, + @"
", + @"2147483647", + "", + @"viewstart-content", + @"

", + @"page-content", + @"

", + @"
"); + + var expectedLineMappings = new[] + { + Tuple.Create(93, 2, true), + Tuple.Create(96, 16, false), + Tuple.Create(112, 2, true), + Tuple.Create(0, 2, true), + Tuple.Create(2, 8, true), + Tuple.Create(10, 16, false), + Tuple.Create(26, 1, true), + Tuple.Create(27, 21, true), + Tuple.Create(0, 7, true), + Tuple.Create(8, 12, false), + Tuple.Create(20, 2, true), + Tuple.Create(23, 12, false), + Tuple.Create(35, 8, true), + }; + + yield return new object[] { "FullPath", expected, expectedLineMappings }; + yield return new object[] { "ViewDiscoveryPath", expected, expectedLineMappings }; + + var expected2 = string.Join(Environment.NewLine, + "
", + "2147483647", + "", + "viewstart-content", + "view-with-partial-content", + "", + @"

partial-content

", + "", + @"

partial-content

", + "
"); + var expectedLineMappings2 = new[] + { + Tuple.Create(93, 2, true), + Tuple.Create(96, 16, false), + Tuple.Create(112, 2, true), + Tuple.Create(0, 27, true), + Tuple.Create(28, 39, false), + // Html.PartialAsync() + Tuple.Create(29, 4, true), + Tuple.Create(33, 8, true), + Tuple.Create(41, 4, false), + Tuple.Create(45, 1, true), + Tuple.Create(46, 20, true), + Tuple.Create(67, 2, true), + // Html.RenderPartial() + Tuple.Create(29, 4, true), + Tuple.Create(33, 8, true), + Tuple.Create(41, 4, false), + Tuple.Create(45, 1, true), + Tuple.Create(46, 20, true), + Tuple.Create(0, 7, true), + Tuple.Create(8, 12, false), + Tuple.Create(20, 2, true), + Tuple.Create(23, 12, false), + Tuple.Create(35, 8, true) + }; + yield return new object[] { "ViewWithPartial", expected2, expectedLineMappings2 }; } } [Theory] - [MemberData(nameof(ActionNames))] - public async Task ViewsAreServedWithoutInstrumentationByDefault(string actionName) + [MemberData(nameof(InstrumentationData))] + public async Task ViewsAreServedWithoutInstrumentationByDefault(string actionName, + string expected, + IEnumerable> expectedLineMappings) { // Arrange var context = new TestPageExecutionContext(); @@ -66,13 +109,15 @@ public async Task ViewsAreServedWithoutInstrumentationByDefault(string actionNam var body = await client.GetStringAsync("http://localhost/Home/" + actionName); // Assert - Assert.Equal(_expected, body.Trim()); + Assert.Equal(expected, body.Trim()); Assert.Empty(context.Values); } [Theory] - [MemberData(nameof(ActionNames))] - public async Task ViewsAreInstrumentedWhenPageExecutionListenerFeatureIsEnabled(string actionName) + [MemberData(nameof(InstrumentationData))] + public async Task ViewsAreInstrumentedWhenPageExecutionListenerFeatureIsEnabled(string actionName, + string expected, + IEnumerable> expectedLineMappings) { // Arrange var context = new TestPageExecutionContext(); @@ -85,13 +130,15 @@ public async Task ViewsAreInstrumentedWhenPageExecutionListenerFeatureIsEnabled( var body = await client.GetStringAsync("http://localhost/Home/" + actionName); // Assert - Assert.Equal(_expected, body.Trim()); - Assert.Equal(_expectedLineMappings, context.Values); + Assert.Equal(expected, body.Trim()); + Assert.Equal(expectedLineMappings, context.Values); } [Theory] - [MemberData(nameof(ActionNames))] - public async Task ViewsCanSwitchFromRegularToInstrumented(string actionName) + [MemberData(nameof(InstrumentationData))] + public async Task ViewsCanSwitchFromRegularToInstrumented(string actionName, + string expected, + IEnumerable> expectedLineMappings) { // Arrange - 1 var context = new TestPageExecutionContext(); @@ -103,7 +150,7 @@ public async Task ViewsCanSwitchFromRegularToInstrumented(string actionName) var body = await client.GetStringAsync("http://localhost/Home/" + actionName); // Assert - 1 - Assert.Equal(_expected, body.Trim()); + Assert.Equal(expected, body.Trim()); Assert.Empty(context.Values); // Arrange - 2 @@ -113,14 +160,30 @@ public async Task ViewsCanSwitchFromRegularToInstrumented(string actionName) body = await client.GetStringAsync("http://localhost/Home/" + actionName); // Assert - 2 - Assert.Equal(_expected, body.Trim()); - Assert.Equal(_expectedLineMappings, context.Values); + Assert.Equal(expected, body.Trim()); + Assert.Equal(expectedLineMappings, context.Values); } [Fact] public async Task SwitchingFromNonInstrumentedToInstrumentedWorksForLayoutAndViewStarts() { // Arrange - 1 + var expectedLineMappings = new[] + { + Tuple.Create(93, 2, true), + Tuple.Create(96, 16, false), + Tuple.Create(112, 2, true), + Tuple.Create(0, 2, true), + Tuple.Create(2, 8, true), + Tuple.Create(10, 16, false), + Tuple.Create(26, 1, true), + Tuple.Create(27, 21, true), + Tuple.Create(0, 7, true), + Tuple.Create(8, 12, false), + Tuple.Create(20, 2, true), + Tuple.Create(23, 12, false), + Tuple.Create(35, 8, true), + }; var context = new TestPageExecutionContext(); var services = GetServiceProvider(context); var server = TestServer.Create(services, _app); @@ -130,7 +193,6 @@ public async Task SwitchingFromNonInstrumentedToInstrumentedWorksForLayoutAndVie var body = await client.GetStringAsync("http://localhost/Home/FullPath"); // Assert - 1 - Assert.Equal(_expected, body.Trim()); Assert.Empty(context.Values); // Arrange - 2 @@ -140,8 +202,7 @@ public async Task SwitchingFromNonInstrumentedToInstrumentedWorksForLayoutAndVie body = await client.GetStringAsync("http://localhost/Home/ViewDiscoveryPath"); // Assert - 2 - Assert.Equal(_expected, body.Trim()); - Assert.Equal(_expectedLineMappings, context.Values); + Assert.Equal(expectedLineMappings, context.Values); } private IServiceProvider GetServiceProvider(TestPageExecutionContext pageExecutionContext) diff --git a/test/Microsoft.AspNet.Mvc.Razor.Test/RazorPageTest.cs b/test/Microsoft.AspNet.Mvc.Razor.Test/RazorPageTest.cs index 1b44b1098a..7fa4921b1c 100644 --- a/test/Microsoft.AspNet.Mvc.Razor.Test/RazorPageTest.cs +++ b/test/Microsoft.AspNet.Mvc.Razor.Test/RazorPageTest.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using System.IO; +using System.Text; using System.Threading.Tasks; using Microsoft.AspNet.Mvc.Rendering; using Microsoft.AspNet.PageExecutionInstrumentation; @@ -524,6 +525,32 @@ public async Task WriteAttribute_CallsBeginAndEndContext_OnPrefixAndSuffixValues context.Verify(); } + [Fact] + public async Task Write_WithHtmlString_WritesValueWithoutEncoding() + { + // Arrange + var writer = new RazorTextWriter(TextWriter.Null, Encoding.UTF8); + var stringCollectionWriter = new StringCollectionTextWriter(Encoding.UTF8); + stringCollectionWriter.Write("text1"); + stringCollectionWriter.Write("text2"); + + var page = CreatePage(p => + { + p.Write(new HtmlString("Hello world")); + p.Write(new HtmlString(stringCollectionWriter)); + }); + page.ViewContext.Writer = writer; + + // Act + await page.ExecuteAsync(); + + // Assert + var buffer = writer.BufferedWriter.Buffer; + Assert.Equal(2, buffer.BufferEntries.Count); + Assert.Equal("Hello world", buffer.BufferEntries[0]); + Assert.Same(stringCollectionWriter.Buffer.BufferEntries, buffer.BufferEntries[1]); + } + private static TestableRazorPage CreatePage(Action executeAction, ViewContext context = null) { diff --git a/test/Microsoft.AspNet.Mvc.Razor.Test/RazorTextWriterTest.cs b/test/Microsoft.AspNet.Mvc.Razor.Test/RazorTextWriterTest.cs index bc845188d7..22df755649 100644 --- a/test/Microsoft.AspNet.Mvc.Razor.Test/RazorTextWriterTest.cs +++ b/test/Microsoft.AspNet.Mvc.Razor.Test/RazorTextWriterTest.cs @@ -32,7 +32,7 @@ public void Write_WritesDataTypes_ToBuffer() writer.Write('m'); // Assert - Assert.Equal(expected, writer.Buffer.BufferEntries); + Assert.Equal(expected, writer.BufferedWriter.Buffer.BufferEntries); } [Fact] @@ -55,7 +55,7 @@ public void Write_WritesDataTypes_ToUnderlyingStream_WhenNotBuffering() writer.Write(2.718m); // Assert - Assert.Empty(writer.Buffer.BufferEntries); + Assert.Empty(writer.BufferedWriter.Buffer.BufferEntries); foreach (var item in expected) { unbufferedWriter.Verify(v => v.Write(item), Times.Once()); @@ -81,7 +81,7 @@ public async Task Write_WritesCharValues_ToUnderlyingStream_WhenNotBuffering() await writer.WriteLineAsync(buffer1); // Assert - Assert.Empty(writer.Buffer.BufferEntries); + Assert.Empty(writer.BufferedWriter.Buffer.BufferEntries); unbufferedWriter.Verify(v => v.Write('x'), Times.Once()); unbufferedWriter.Verify(v => v.Write(buffer1, 1, 2), Times.Once()); unbufferedWriter.Verify(v => v.Write(buffer1, 0, 4), Times.Once()); @@ -106,7 +106,7 @@ public async Task Write_WritesStringValues_ToUnbufferedStream_WhenNotBuffering() await writer.WriteLineAsync("gh"); // Assert - Assert.Empty(writer.Buffer.BufferEntries); + Assert.Empty(writer.BufferedWriter.Buffer.BufferEntries); unbufferedWriter.Verify(v => v.Write("a"), Times.Once()); unbufferedWriter.Verify(v => v.WriteLine("ab"), Times.Once()); unbufferedWriter.Verify(v => v.WriteAsync("ef"), Times.Once()); @@ -128,7 +128,7 @@ public void WriteLine_WritesDataTypes_ToBuffer() writer.WriteLine(3L); // Assert - Assert.Equal(expected, writer.Buffer.BufferEntries); + Assert.Equal(expected, writer.BufferedWriter.Buffer.BufferEntries); } [Fact] @@ -146,7 +146,7 @@ public void WriteLine_WritesDataTypes_ToUnbufferedStream_WhenNotBuffering() writer.WriteLine(3L); // Assert - Assert.Empty(writer.Buffer.BufferEntries); + Assert.Empty(writer.BufferedWriter.Buffer.BufferEntries); unbufferedWriter.Verify(v => v.Write("False"), Times.Once()); unbufferedWriter.Verify(v => v.Write("1.1"), Times.Once()); unbufferedWriter.Verify(v => v.Write("3"), Times.Once()); @@ -168,7 +168,7 @@ public async Task Write_WritesCharBuffer() await writer.WriteLineAsync(input3.Array, input3.Offset, input3.Count); // Assert - var buffer = writer.Buffer.BufferEntries; + var buffer = writer.BufferedWriter.Buffer.BufferEntries; Assert.Equal(4, buffer.Count); Assert.Equal("bcd", buffer[0]); Assert.Equal("ef", buffer[1]); @@ -188,7 +188,7 @@ public async Task WriteLines_WritesCharBuffer() await writer.WriteLineAsync(); // Assert - var actual = writer.Buffer.BufferEntries; + var actual = writer.BufferedWriter.Buffer.BufferEntries; Assert.Equal(new[] { newLine, newLine }, actual); } @@ -210,7 +210,7 @@ public async Task Write_WritesStringBuffer() await writer.WriteLineAsync(input4); // Assert - var actual = writer.Buffer.BufferEntries; + var actual = writer.BufferedWriter.Buffer.BufferEntries; Assert.Equal(new[] { input1, input2, newLine, input3, input4, newLine }, actual); } @@ -228,9 +228,9 @@ public void Copy_CopiesContent_IfTargetTextWriterIsARazorTextWriterAndBuffering( // Assert // Make sure content was written to the source. - Assert.Equal(2, source.Buffer.BufferEntries.Count); - Assert.Equal(1, target.Buffer.BufferEntries.Count); - Assert.Same(source.Buffer.BufferEntries, target.Buffer.BufferEntries[0]); + Assert.Equal(2, source.BufferedWriter.Buffer.BufferEntries.Count); + Assert.Equal(1, target.BufferedWriter.Buffer.BufferEntries.Count); + Assert.Same(source.BufferedWriter.Buffer.BufferEntries, target.BufferedWriter.Buffer.BufferEntries[0]); } [Fact] @@ -249,8 +249,8 @@ public void Copy_CopiesContent_IfTargetTextWriterIsARazorTextWriterAndNotBufferi // Assert // Make sure content was written to the source. - Assert.Equal(2, source.Buffer.BufferEntries.Count); - Assert.Empty(target.Buffer.BufferEntries); + Assert.Equal(2, source.BufferedWriter.Buffer.BufferEntries.Count); + Assert.Empty(target.BufferedWriter.Buffer.BufferEntries); unbufferedWriter.Verify(v => v.Write("Hello world"), Times.Once()); unbufferedWriter.Verify(v => v.Write("bc"), Times.Once()); } @@ -286,9 +286,9 @@ public async Task CopyAsync_WritesContent_IfTargetTextWriterIsARazorTextWriterAn await source.CopyToAsync(target); // Assert - Assert.Equal(3, source.Buffer.BufferEntries.Count); - Assert.Equal(1, target.Buffer.BufferEntries.Count); - Assert.Same(source.Buffer.BufferEntries, target.Buffer.BufferEntries[0]); + Assert.Equal(3, source.BufferedWriter.Buffer.BufferEntries.Count); + Assert.Equal(1, target.BufferedWriter.Buffer.BufferEntries.Count); + Assert.Same(source.BufferedWriter.Buffer.BufferEntries, target.BufferedWriter.Buffer.BufferEntries[0]); } [Fact] @@ -307,8 +307,8 @@ public async Task CopyAsync_WritesContent_IfTargetTextWriterIsARazorTextWriterAn // Assert // Make sure content was written to the source. - Assert.Equal(3, source.Buffer.BufferEntries.Count); - Assert.Empty(target.Buffer.BufferEntries); + Assert.Equal(3, source.BufferedWriter.Buffer.BufferEntries.Count); + Assert.Empty(target.BufferedWriter.Buffer.BufferEntries); unbufferedWriter.Verify(v => v.WriteAsync("Hello from Asp.Net"), Times.Once()); unbufferedWriter.Verify(v => v.WriteAsync(Environment.NewLine), Times.Once()); unbufferedWriter.Verify(v => v.WriteAsync("xyz"), Times.Once()); diff --git a/test/Microsoft.AspNet.Mvc.Razor.Test/RazorViewTest.cs b/test/Microsoft.AspNet.Mvc.Razor.Test/RazorViewTest.cs index 5c4ac4bfc7..5af5c8c47e 100644 --- a/test/Microsoft.AspNet.Mvc.Razor.Test/RazorViewTest.cs +++ b/test/Microsoft.AspNet.Mvc.Razor.Test/RazorViewTest.cs @@ -35,7 +35,7 @@ public async Task RenderAsync_ThrowsIfContextualizeHasNotBeenInvoked() } [Fact] - public async Task RenderAsync_AsPartial_DoesNotCreateOutputBuffer() + public async Task RenderAsync_AsPartial_DoesNotBufferOutput() { // Arrange TextWriter actual = null; @@ -56,7 +56,7 @@ public async Task RenderAsync_AsPartial_DoesNotCreateOutputBuffer() // Assert Assert.Same(expected, actual); - Assert.Equal("Hello world", expected.ToString()); + Assert.Equal("Hello world", viewContext.Writer.ToString()); } [Fact] @@ -653,20 +653,26 @@ public async Task RenderAsync_UsesPageExecutionFeatureFromRequest_ToWrapWriter() public async Task RenderAsync_UsesPageExecutionFeatureFromRequest_ToGetExecutionContext() { // Arrange - var writer = Mock.Of(); + var writer = new StringWriter(); var executed = false; var feature = new Mock(MockBehavior.Strict); var pageContext = Mock.Of(); - feature.Setup(f => f.GetContext("/MyPartialPage.cshtml", writer)) + feature.Setup(f => f.GetContext("/MyPartialPage.cshtml", It.IsAny())) .Returns(pageContext) .Verifiable(); + feature.Setup(f => f.DecorateWriter(It.IsAny())) + .Returns((RazorTextWriter r) => r) + .Verifiable(); + var page = new TestableRazorPage(v => { - Assert.Same(writer, v.Output); + Assert.IsType(v.Output); Assert.Same(pageContext, v.PageExecutionContext); executed = true; + + v.Write("Hello world"); }); page.Path = "/MyPartialPage.cshtml"; @@ -684,6 +690,7 @@ public async Task RenderAsync_UsesPageExecutionFeatureFromRequest_ToGetExecution // Assert feature.Verify(); Assert.True(executed); + Assert.Equal("Hello world", viewContext.Writer.ToString()); } [Theory] diff --git a/test/WebSites/RazorInstrumentationWebsite/HomeController.cs b/test/WebSites/RazorInstrumentationWebsite/HomeController.cs index 14a87a4c6e..a945842683 100644 --- a/test/WebSites/RazorInstrumentationWebsite/HomeController.cs +++ b/test/WebSites/RazorInstrumentationWebsite/HomeController.cs @@ -17,5 +17,10 @@ public ActionResult ViewDiscoveryPath() { return View(); } + + public ActionResult ViewWithPartial() + { + return View(); + } } } \ No newline at end of file diff --git a/test/WebSites/RazorInstrumentationWebsite/Views/Home/VIewWithPartial.cshtml b/test/WebSites/RazorInstrumentationWebsite/Views/Home/VIewWithPartial.cshtml new file mode 100644 index 0000000000..8f42baf024 --- /dev/null +++ b/test/WebSites/RazorInstrumentationWebsite/Views/Home/VIewWithPartial.cshtml @@ -0,0 +1,5 @@ +view-with-partial-content +@await Html.PartialAsync("_PartialView") +@{ +await Html.RenderPartialAsync("_PartialView"); +} \ No newline at end of file diff --git a/test/WebSites/RazorInstrumentationWebsite/Views/Home/_PartialView.cshtml b/test/WebSites/RazorInstrumentationWebsite/Views/Home/_PartialView.cshtml new file mode 100644 index 0000000000..3b945a4613 --- /dev/null +++ b/test/WebSites/RazorInstrumentationWebsite/Views/Home/_PartialView.cshtml @@ -0,0 +1,4 @@ +@{ + var cls = "class"; +} +

partial-content

\ No newline at end of file From 0d603a38cf30db3c7100f26897561111588bee73 Mon Sep 17 00:00:00 2001 From: YishaiGalatzer Date: Sun, 12 Oct 2014 16:37:17 -0700 Subject: [PATCH 18/39] PR feedback and sort/clean MvcServices --- .../Compilation/CompilerCache.cs | 9 ++ .../Compilation/ICompilerCache.cs | 12 +- .../Razor/IRazorCompilationService.cs | 3 +- .../Razor/RazorCompilationService.cs | 2 - .../VirtualPathRazorPageFactory.cs | 5 +- src/Microsoft.AspNet.Mvc/MvcServices.cs | 124 +++++++++++------- 6 files changed, 98 insertions(+), 57 deletions(-) diff --git a/src/Microsoft.AspNet.Mvc.Razor/Compilation/CompilerCache.cs b/src/Microsoft.AspNet.Mvc.Razor/Compilation/CompilerCache.cs index 298d45c7b5..4618542916 100644 --- a/src/Microsoft.AspNet.Mvc.Razor/Compilation/CompilerCache.cs +++ b/src/Microsoft.AspNet.Mvc.Razor/Compilation/CompilerCache.cs @@ -9,11 +9,19 @@ namespace Microsoft.AspNet.Mvc.Razor { + /// public class CompilerCache : ICompilerCache { private readonly ConcurrentDictionary _cache; private static readonly Type[] EmptyType = new Type[0]; + /// + /// Sets up the runtime compilation cache. + /// + /// + /// An representing the assemblies + /// used to search for pre-compiled views. + /// public CompilerCache([NotNull] IAssemblyProvider provider) : this(GetFileInfos(provider.CandidateAssemblies)) { @@ -64,6 +72,7 @@ private static bool Match(Type t) return false; } + /// public CompilationResult GetOrAdd([NotNull] RelativeFileInfo fileInfo, bool enableInstrumentation, [NotNull] Func compile) diff --git a/src/Microsoft.AspNet.Mvc.Razor/Compilation/ICompilerCache.cs b/src/Microsoft.AspNet.Mvc.Razor/Compilation/ICompilerCache.cs index d11d4a4a30..3d06c9ab89 100644 --- a/src/Microsoft.AspNet.Mvc.Razor/Compilation/ICompilerCache.cs +++ b/src/Microsoft.AspNet.Mvc.Razor/Compilation/ICompilerCache.cs @@ -2,12 +2,22 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; -using Microsoft.AspNet.FileSystems; namespace Microsoft.AspNet.Mvc.Razor { + /// + /// Caches the result of runtime compilation for the duration of the app lifetime. + /// public interface ICompilerCache { + /// + /// Get an existing compilation result, or create and add a new one if it is + /// not available in the cache. + /// + /// A representing the file. + /// to generate instrumentation. + /// An delegate that will generate a compilation result. + /// A cached . CompilationResult GetOrAdd([NotNull] RelativeFileInfo fileInfo, bool enableInstrumentation, [NotNull] Func compile); diff --git a/src/Microsoft.AspNet.Mvc.Razor/Razor/IRazorCompilationService.cs b/src/Microsoft.AspNet.Mvc.Razor/Razor/IRazorCompilationService.cs index d9a59c2443..a56e9eb146 100644 --- a/src/Microsoft.AspNet.Mvc.Razor/Razor/IRazorCompilationService.cs +++ b/src/Microsoft.AspNet.Mvc.Razor/Razor/IRazorCompilationService.cs @@ -14,7 +14,8 @@ public interface IRazorCompilationService /// A instance that represents the file to compile. /// /// Indicates that the page should be instrumented. - /// A that represents the results of parsing and compiling the file. + /// + /// A that represents the results of parsing and compiling the file. /// CompilationResult Compile(RelativeFileInfo fileInfo, bool isInstrumented); } diff --git a/src/Microsoft.AspNet.Mvc.Razor/Razor/RazorCompilationService.cs b/src/Microsoft.AspNet.Mvc.Razor/Razor/RazorCompilationService.cs index 2258be0038..a6a34eb1f2 100644 --- a/src/Microsoft.AspNet.Mvc.Razor/Razor/RazorCompilationService.cs +++ b/src/Microsoft.AspNet.Mvc.Razor/Razor/RazorCompilationService.cs @@ -1,10 +1,8 @@ // 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.Linq; using Microsoft.AspNet.Razor; -using Microsoft.Framework.DependencyInjection; namespace Microsoft.AspNet.Mvc.Razor { diff --git a/src/Microsoft.AspNet.Mvc.Razor/VirtualPathRazorPageFactory.cs b/src/Microsoft.AspNet.Mvc.Razor/VirtualPathRazorPageFactory.cs index 3fd41f329a..eb1addcbd9 100644 --- a/src/Microsoft.AspNet.Mvc.Razor/VirtualPathRazorPageFactory.cs +++ b/src/Microsoft.AspNet.Mvc.Razor/VirtualPathRazorPageFactory.cs @@ -40,8 +40,9 @@ private IRazorCompilationService RazorCompilationService { if (_razorcompilationService == null) { - // it is ok to use the cached service provider because this service has - // a lifetime of Scoped. + // it is ok to use the cached service provider because both this, and the + // resolved service are in a lifetime of Scoped. + // We don't want to get it upgront because it will force Roslyn to load. _razorcompilationService = _serviceProvider.GetService(); } diff --git a/src/Microsoft.AspNet.Mvc/MvcServices.cs b/src/Microsoft.AspNet.Mvc/MvcServices.cs index 9154842654..d14339ab96 100644 --- a/src/Microsoft.AspNet.Mvc/MvcServices.cs +++ b/src/Microsoft.AspNet.Mvc/MvcServices.cs @@ -31,58 +31,47 @@ public static IEnumerable GetDefaultServices(IConfiguration { var describe = new ServiceDescriber(configuration); + // + // Options and core services. + // yield return describe.Transient, MvcOptionsSetup>(); + yield return describe.Transient(); + yield return describe.Transient(typeof(INestedProviderManager<>), typeof(NestedProviderManager<>)); + yield return describe.Transient(typeof(INestedProviderManagerAsync<>), typeof(NestedProviderManagerAsync<>)); + yield return describe.Transient(); + // + // Core action discovery, filters and action execution. + // + yield return describe.Transient(); yield return describe.Transient(); yield return describe.Singleton(); - - yield return describe.Singleton(); - yield return describe.Scoped(); + yield return describe.Transient(); // This provider needs access to the per-request services, but might be used many times for a given // request. yield return describe.Scoped, DefaultActionConstraintProvider>(); - yield return describe.Transient(); - yield return describe.Transient(); - yield return describe.Transient(); - - // The host is designed to be discarded after consumption and is very inexpensive to initialize. - yield return describe.Transient(); - - yield return describe.Singleton(); - yield return describe.Singleton(); - yield return describe.Singleton(); - - // The provider is inexpensive to initialize and provides ViewEngines that may require request - // specific services. - yield return describe.Transient(); - yield return describe.Scoped(); - // The ViewStartProvider needs to be able to consume scoped instances of IRazorPageFactory - yield return describe.Scoped(); - yield return describe.Transient(); - - // Transient since the IViewLocationExpanders returned by the instance is cached by view engines. - yield return describe.Transient(); - // Caches view locations that are valid for the lifetime of the application. - yield return describe.Singleton(); - - // Only want one ITagHelperActivator so it can cache Type activation information. Types won't conflict. - yield return describe.Singleton(); - - yield return describe.Singleton(); - // Virtual path view factory needs to stay scoped so views can get get scoped services. - yield return describe.Scoped(); - yield return describe.Singleton(); + yield return describe.Singleton(); + yield return describe.Scoped(); yield return describe.Transient, - ControllerActionDescriptorProvider>(); + ControllerActionDescriptorProvider>(); yield return describe.Transient, ControllerActionInvokerProvider>(); yield return describe.Singleton(); + // The IGlobalFilterProvider is used to build the action descriptors (likely once) and so should + // remain transient to avoid keeping it in memory. + yield return describe.Transient(); + + yield return describe.Transient, DefaultFilterProvider>(); + + // + // Dataflow - ModelBinding, Validation and Formatting + // yield return describe.Transient(); yield return describe.Scoped(); @@ -98,17 +87,53 @@ public static IEnumerable GetDefaultServices(IConfiguration yield return describe.Instance( new JsonOutputFormatter(JsonOutputFormatter.CreateDefaultSettings(), indent: false)); - // The IGlobalFilterProvider is used to build the action descriptors (likely once) and so should - // remain transient to avoid keeping it in memory. - yield return describe.Transient(); - - yield return describe.Transient, DefaultFilterProvider>(); - yield return describe.Transient(); yield return describe.Scoped(); + yield return describe.Transient(); + + // + // Razor, Views and runtime compilation + // + + // The provider is inexpensive to initialize and provides ViewEngines that may require request + // specific services. + yield return describe.Scoped(); + yield return describe.Transient(); + // Transient since the IViewLocationExpanders returned by the instance is cached by view engines. + yield return describe.Transient(); + // Caches view locations that are valid for the lifetime of the application. + yield return describe.Singleton(); + yield return describe.Singleton(); + + // The host is designed to be discarded after consumption and is very inexpensive to initialize. + yield return describe.Transient(); + + yield return describe.Singleton(); + yield return describe.Singleton(); + + // Both the compiler cache and roslyn compilation service hold on the compilation related + // caches. RazorCompilation service is just an adapter service, and it is scoped + // since it will typically be resolved multiple times per request. + yield return describe.Scoped(); + // The ViewStartProvider needs to be able to consume scoped instances of IRazorPageFactory + yield return describe.Scoped(); + yield return describe.Transient(); + yield return describe.Singleton(); + // Virtual path view factory needs to stay scoped so views can get get scoped services. + yield return describe.Scoped(); + + // + // View and rendering helpers + // + + yield return describe.Transient(); + yield return describe.Transient(typeof(IHtmlHelper<>), typeof(HtmlHelper<>)); yield return describe.Scoped(); + // Only want one ITagHelperActivator so it can cache Type activation information. Types won't conflict. + yield return describe.Singleton(); + yield return describe.Transient(); yield return describe.Singleton(); yield return describe.Transient(); @@ -116,7 +141,9 @@ public static IEnumerable GetDefaultServices(IConfiguration DefaultViewComponentInvokerProvider>(); yield return describe.Transient(); - yield return describe.Transient(); + // + // Security and Authorization + // yield return describe.Transient(); yield return describe.Singleton(); @@ -124,19 +151,14 @@ public static IEnumerable GetDefaultServices(IConfiguration yield return describe.Singleton(); + // + // Api Description + // + yield return describe.Singleton(); yield return describe.Transient, DefaultApiDescriptionProvider>(); - - yield return describe.Transient(typeof(INestedProviderManager<>), typeof(NestedProviderManager<>)); - - yield return describe.Transient(typeof(INestedProviderManagerAsync<>), typeof(NestedProviderManagerAsync<>)); - - yield return describe.Transient(); - yield return describe.Transient(typeof(IHtmlHelper<>), typeof(HtmlHelper<>)); - - yield return describe.Transient(); } } } From d912f6cd39c5213d6c1022baedd439e99dd54650 Mon Sep 17 00:00:00 2001 From: Pranav K Date: Mon, 13 Oct 2014 05:14:43 -0700 Subject: [PATCH 19/39] Removing unused types Fixes #1344 --- .../Validation/CompositeModelValidator.cs | 60 ------------ .../Validation/ErrorModelValidator.cs | 33 ------- .../InvalidModelValidatorProvider.cs | 64 ------------ .../InjectDescriptor.cs | 40 -------- .../Validation/ErrorModelValidatorTest.cs | 24 ----- .../InvalidModelValidatorProviderTest.cs | 98 ------------------- 6 files changed, 319 deletions(-) delete mode 100644 src/Microsoft.AspNet.Mvc.ModelBinding/Validation/CompositeModelValidator.cs delete mode 100644 src/Microsoft.AspNet.Mvc.ModelBinding/Validation/ErrorModelValidator.cs delete mode 100644 src/Microsoft.AspNet.Mvc.ModelBinding/Validation/InvalidModelValidatorProvider.cs delete mode 100644 src/Microsoft.AspNet.Mvc.Razor.Host/InjectDescriptor.cs delete mode 100644 test/Microsoft.AspNet.Mvc.ModelBinding.Test/Validation/ErrorModelValidatorTest.cs delete mode 100644 test/Microsoft.AspNet.Mvc.ModelBinding.Test/Validation/InvalidModelValidatorProviderTest.cs diff --git a/src/Microsoft.AspNet.Mvc.ModelBinding/Validation/CompositeModelValidator.cs b/src/Microsoft.AspNet.Mvc.ModelBinding/Validation/CompositeModelValidator.cs deleted file mode 100644 index 0f3355c093..0000000000 --- a/src/Microsoft.AspNet.Mvc.ModelBinding/Validation/CompositeModelValidator.cs +++ /dev/null @@ -1,60 +0,0 @@ -// 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.Collections.Generic; - -namespace Microsoft.AspNet.Mvc.ModelBinding -{ - public class CompositeModelValidator : IModelValidator - { - private readonly IEnumerable _validators; - - public CompositeModelValidator(IEnumerable validators) - { - _validators = validators; - } - - public bool IsRequired - { - get { return false; } - } - - public IEnumerable Validate(ModelValidationContext context) - { - var propertiesValid = true; - var metadata = context.ModelMetadata; - - foreach (var propertyMetadata in metadata.Properties) - { - var propertyContext = new ModelValidationContext(context, propertyMetadata); - - foreach (var propertyValidator in _validators) - { - foreach (var validationResult in propertyValidator.Validate(propertyContext)) - { - propertiesValid = false; - yield return CreateSubPropertyResult(propertyMetadata, validationResult); - } - } - } - - if (propertiesValid) - { - foreach (var typeValidator in _validators) - { - foreach (var typeResult in typeValidator.Validate(context)) - { - yield return typeResult; - } - } - } - } - - private static ModelValidationResult CreateSubPropertyResult(ModelMetadata propertyMetadata, - ModelValidationResult propertyResult) - { - return new ModelValidationResult(propertyMetadata.PropertyName + '.' + propertyResult.MemberName, - propertyResult.Message); - } - } -} diff --git a/src/Microsoft.AspNet.Mvc.ModelBinding/Validation/ErrorModelValidator.cs b/src/Microsoft.AspNet.Mvc.ModelBinding/Validation/ErrorModelValidator.cs deleted file mode 100644 index 5ce180f1d3..0000000000 --- a/src/Microsoft.AspNet.Mvc.ModelBinding/Validation/ErrorModelValidator.cs +++ /dev/null @@ -1,33 +0,0 @@ -// 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; - -namespace Microsoft.AspNet.Mvc.ModelBinding -{ - /// - /// A to represent an error. This validator will always throw an exception regardless - /// of the actual model value. - /// This is used to perform meta-validation - that is to verify the validation attributes make sense. - /// - public class ErrorModelValidator : IModelValidator - { - private readonly string _errorMessage; - - public ErrorModelValidator([NotNull] string errorMessage) - { - _errorMessage = errorMessage; - } - - public bool IsRequired - { - get { return false; } - } - - public IEnumerable Validate(ModelValidationContext context) - { - throw new InvalidOperationException(_errorMessage); - } - } -} diff --git a/src/Microsoft.AspNet.Mvc.ModelBinding/Validation/InvalidModelValidatorProvider.cs b/src/Microsoft.AspNet.Mvc.ModelBinding/Validation/InvalidModelValidatorProvider.cs deleted file mode 100644 index 1935a5543a..0000000000 --- a/src/Microsoft.AspNet.Mvc.ModelBinding/Validation/InvalidModelValidatorProvider.cs +++ /dev/null @@ -1,64 +0,0 @@ -// 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 System.ComponentModel.DataAnnotations; -using System.Linq; -using System.Reflection; - -namespace Microsoft.AspNet.Mvc.ModelBinding -{ - public class InvalidModelValidatorProvider : AssociatedValidatorProvider - { - protected override IEnumerable GetValidators(ModelMetadata metadata, - IEnumerable attributes) - { - if (metadata.ContainerType == null || string.IsNullOrEmpty(metadata.PropertyName)) - { - // Validate that the type's fields and nonpublic properties don't have any validation attributes on - // them. Validation only runs against public properties - var type = metadata.ModelType; - var nonPublicProperties = type.GetProperties(BindingFlags.NonPublic | BindingFlags.Instance); - foreach (var nonPublicProperty in nonPublicProperties) - { - if (nonPublicProperty.GetCustomAttributes(typeof(ValidationAttribute), inherit: true).Any()) - { - var message = Resources.FormatValidationAttributeOnNonPublicProperty(nonPublicProperty.Name, - type); - yield return new ErrorModelValidator(message); - } - } - - var bindingFlags = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance; - var allFields = metadata.ModelType.GetFields(bindingFlags); - foreach (var field in allFields) - { - if (field.GetCustomAttributes(typeof(ValidationAttribute), inherit: true).Any()) - { - var message = Resources.FormatValidationAttributeOnField(field.Name, type); - yield return new ErrorModelValidator(message); - } - } - } - else - { - // Validate that value-typed properties marked as [Required] are also marked as - // [DataMember(IsRequired=true)]. Certain formatters may not recognize a member as required if it's - // marked as [Required] but not [DataMember(IsRequired=true)]. This is not a problem for reference - // types because [Required] will still cause a model error to be raised after a null value is - // deserialized. - if (metadata.ModelType.GetTypeInfo().IsValueType && - attributes.Any(attribute => attribute is RequiredAttribute)) - { - if (!DataMemberModelValidatorProvider.IsRequiredDataMember(metadata.ContainerType, attributes)) - { - var message = Resources.FormatMissingDataMemberIsRequired(metadata.PropertyName, - metadata.ContainerType); - yield return new ErrorModelValidator(message); - } - } - } - } - } -} diff --git a/src/Microsoft.AspNet.Mvc.Razor.Host/InjectDescriptor.cs b/src/Microsoft.AspNet.Mvc.Razor.Host/InjectDescriptor.cs deleted file mode 100644 index 36b7a566a9..0000000000 --- a/src/Microsoft.AspNet.Mvc.Razor.Host/InjectDescriptor.cs +++ /dev/null @@ -1,40 +0,0 @@ -// 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.Mvc.Razor.Host; - -namespace Microsoft.AspNet.Mvc.Razor -{ - /// - /// Represents information about an injected property. - /// - public class InjectDescriptor - { - public InjectDescriptor(string typeName, string memberName) - { - if (string.IsNullOrEmpty(typeName)) - { - throw new ArgumentException(Resources.ArgumentCannotBeNullOrEmpy, "typeName"); - } - - if (string.IsNullOrEmpty(memberName)) - { - throw new ArgumentException(Resources.ArgumentCannotBeNullOrEmpy, "memberName"); - } - - TypeName = typeName; - MemberName = memberName; - } - - /// - /// Gets the type name of the injected property - /// - public string TypeName { get; private set; } - - /// - /// Gets the name of the injected property. - /// - public string MemberName { get; private set; } - } -} \ No newline at end of file diff --git a/test/Microsoft.AspNet.Mvc.ModelBinding.Test/Validation/ErrorModelValidatorTest.cs b/test/Microsoft.AspNet.Mvc.ModelBinding.Test/Validation/ErrorModelValidatorTest.cs deleted file mode 100644 index a4963c1412..0000000000 --- a/test/Microsoft.AspNet.Mvc.ModelBinding.Test/Validation/ErrorModelValidatorTest.cs +++ /dev/null @@ -1,24 +0,0 @@ -// 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.Testing; -using Xunit; - -namespace Microsoft.AspNet.Mvc.ModelBinding -{ - public class ErrorModelValidatorTest - { - private readonly DataAnnotationsModelMetadataProvider _metadataProvider = new DataAnnotationsModelMetadataProvider(); - - [Fact] - public void ValidateThrowsException() - { - // Arrange - var validator = new ErrorModelValidator("error"); - - // Act and Assert - ExceptionAssert.Throws(() => validator.Validate(null), "error"); - } - } -} diff --git a/test/Microsoft.AspNet.Mvc.ModelBinding.Test/Validation/InvalidModelValidatorProviderTest.cs b/test/Microsoft.AspNet.Mvc.ModelBinding.Test/Validation/InvalidModelValidatorProviderTest.cs deleted file mode 100644 index 880873e0bb..0000000000 --- a/test/Microsoft.AspNet.Mvc.ModelBinding.Test/Validation/InvalidModelValidatorProviderTest.cs +++ /dev/null @@ -1,98 +0,0 @@ -// 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.ComponentModel.DataAnnotations; -using System.Linq; -using System.Runtime.Serialization; -using Microsoft.AspNet.Testing; -using Xunit; - -namespace Microsoft.AspNet.Mvc.ModelBinding -{ - public class InvalidModelValidatorProviderTest - { - private static DataAnnotationsModelMetadataProvider _metadataProvider = new DataAnnotationsModelMetadataProvider(); - - [Fact] - public void GetValidatorsReturnsNothingForValidModel() - { - // Arrange - var validatorProvider = new InvalidModelValidatorProvider(); - - // Act - var validators = validatorProvider.GetValidators(_metadataProvider.GetMetadataForType(null, typeof(ValidModel))); - - // Assert - Assert.Empty(validators); - } - - [Fact] - public void GetValidatorsReturnsInvalidModelValidatorsForInvalidModelType() - { - // Arrange - var name = typeof(InvalidModel).FullName; - var validatorProvider = new InvalidModelValidatorProvider(); - - // Act - var validators = validatorProvider.GetValidators(_metadataProvider.GetMetadataForType(null, typeof(InvalidModel))); - - // Assert - Assert.Equal(2, validators.Count()); - ExceptionAssert.Throws(() => validators.ElementAt(0).Validate(null), - "Non-public property 'Internal' on type '" + name + "' is attributed with one or more validation attributes. Validation attributes on non-public properties are not supported. Consider using a public property for validation instead."); - ExceptionAssert.Throws(() => validators.ElementAt(1).Validate(null), - "Field 'Field' on type '" + name + "' is attributed with one or more validation attributes. Validation attributes on fields are not supported. Consider using a public property for validation instead."); - } - - [Fact] - public void GetValidatorsReturnsInvalidModelValidatorsForInvalidModelProperty() - { - // Arrange - var name = typeof(InvalidModel).FullName; - var validatorProvider = new InvalidModelValidatorProvider(); - - // Act - var validators = validatorProvider.GetValidators(_metadataProvider.GetMetadataForProperty(null, typeof(InvalidModel), "Value")); - - // Assert - Assert.Equal(1, validators.Count()); - ExceptionAssert.Throws(() => validators.First().Validate(null), - "Property 'Value' on type '" + name + "' is invalid. Value-typed properties marked as [Required] must also be marked with [DataMember(IsRequired=true)] to be recognized as required. Consider attributing the declaring type with [DataContract] and the property with [DataMember(IsRequired=true)]."); - } - - [DataContract] - public class ValidModel - { - [Required] - [DataMember] - [StringLength(10)] - public string Ref { get; set; } - - [DataMember] - internal string Internal { get; set; } - - [Required] - [DataMember(IsRequired = true)] - public int Value { get; set; } - - public string Field; - } - - public class InvalidModel - { - [Required] - public string Ref { get; set; } - - [StringLength(10)] - [RegularExpression("pattern")] - internal string Internal { get; set; } - - [Required] - public int Value { get; set; } - - [StringLength(10)] - public string Field; - } - } -} From 4166c8cbc961b00d337abf18f119cc50dd1d5e36 Mon Sep 17 00:00:00 2001 From: YishaiGalatzer Date: Mon, 13 Oct 2014 10:13:32 -0700 Subject: [PATCH 20/39] Fix unit test --- .../RazorCompilationServiceTest.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/test/Microsoft.AspNet.Mvc.Razor.Test/RazorCompilationServiceTest.cs b/test/Microsoft.AspNet.Mvc.Razor.Test/RazorCompilationServiceTest.cs index 9272dc74f1..87086c76bd 100644 --- a/test/Microsoft.AspNet.Mvc.Razor.Test/RazorCompilationServiceTest.cs +++ b/test/Microsoft.AspNet.Mvc.Razor.Test/RazorCompilationServiceTest.cs @@ -86,7 +86,8 @@ private static GeneratorResults GetGeneratorResult() new Block( new BlockBuilder { Type = BlockType.Comment }), new RazorError[0], - new CodeBuilderResult("", new LineMapping[0])); + new CodeBuilderResult("", new LineMapping[0]), + new CodeTree()); } } } From a633ef4f973368fb7801dd2f6ad80b304494b4a5 Mon Sep 17 00:00:00 2001 From: Pranav K Date: Thu, 9 Oct 2014 17:02:42 -0700 Subject: [PATCH 21/39] Modify Razor components to use RazorViewEngineOptions.FileSystem Fixes #1302 --- .../MvcRazorHost.cs | 19 +---- .../Compilation/ExpiringFileInfoCache.cs | 21 ++---- ...ringFileInfoCache.cs => IFileInfoCache.cs} | 0 .../RazorViewEngineOptions.cs | 25 +++++++ .../Razor/PreCompileViews/RazorPreCompiler.cs | 20 +++-- .../ViewStartProvider.cs | 8 +- src/Microsoft.AspNet.Mvc/MvcServices.cs | 8 +- .../RazorPreCompileModule.cs | 10 ++- .../RazorViewEngineOptionsSetup.cs | 32 ++++++++ .../Compilation/ExpiringFileInfoCacheTest.cs | 75 ++++++------------- .../RazorViewEngineOptionsTest.cs | 22 ++++++ .../RazorViewEngineOptionsSetupTest.cs | 33 ++++++++ test/Microsoft.AspNet.Mvc.Test/project.json | 6 +- 13 files changed, 174 insertions(+), 105 deletions(-) rename src/Microsoft.AspNet.Mvc.Razor/Compilation/{IExpiringFileInfoCache.cs => IFileInfoCache.cs} (100%) create mode 100644 src/Microsoft.AspNet.Mvc/RazorViewEngineOptionsSetup.cs create mode 100644 test/Microsoft.AspNet.Mvc.Razor.Test/RazorViewEngineOptionsTest.cs create mode 100644 test/Microsoft.AspNet.Mvc.Test/RazorViewEngineOptionsSetupTest.cs diff --git a/src/Microsoft.AspNet.Mvc.Razor.Host/MvcRazorHost.cs b/src/Microsoft.AspNet.Mvc.Razor.Host/MvcRazorHost.cs index aaf87e13ff..11a628d597 100644 --- a/src/Microsoft.AspNet.Mvc.Razor.Host/MvcRazorHost.cs +++ b/src/Microsoft.AspNet.Mvc.Razor.Host/MvcRazorHost.cs @@ -11,10 +11,6 @@ using Microsoft.AspNet.Razor.Parser; using Microsoft.AspNet.Razor.Runtime.TagHelpers; -#if ASPNET50 || ASPNETCORE50 -using Microsoft.Framework.Runtime; -#endif - namespace Microsoft.AspNet.Mvc.Razor { public class MvcRazorHost : RazorEngineHost, IMvcRazorHost @@ -41,17 +37,7 @@ public class MvcRazorHost : RazorEngineHost, IMvcRazorHost // This field holds the type name without the generic decoration (MyBaseType) private readonly string _baseType; -#if ASPNET50 || ASPNETCORE50 - /// - /// Initializes a new instance of with the specified - /// . - /// - /// Contains information about the executing application. - public MvcRazorHost(IApplicationEnvironment appEnvironment) - : this(new PhysicalFileSystem(appEnvironment.ApplicationBasePath)) - { - } -#elif NET45 +#if NET45 /// /// Initializes a new instance of with the specified /// . @@ -62,12 +48,11 @@ public MvcRazorHost(string root) : { } #endif - /// /// Initializes a new instance of using the specified . /// /// A rooted at the application base path. - protected internal MvcRazorHost([NotNull] IFileSystem fileSystem) + public MvcRazorHost(IFileSystem fileSystem) : base(new CSharpRazorCodeLanguage()) { _fileSystem = fileSystem; diff --git a/src/Microsoft.AspNet.Mvc.Razor/Compilation/ExpiringFileInfoCache.cs b/src/Microsoft.AspNet.Mvc.Razor/Compilation/ExpiringFileInfoCache.cs index 0c37c64e9b..68af815d4d 100644 --- a/src/Microsoft.AspNet.Mvc.Razor/Compilation/ExpiringFileInfoCache.cs +++ b/src/Microsoft.AspNet.Mvc.Razor/Compilation/ExpiringFileInfoCache.cs @@ -5,7 +5,6 @@ using System.Collections.Concurrent; using Microsoft.AspNet.FileSystems; using Microsoft.Framework.OptionsModel; -using Microsoft.Framework.Runtime; namespace Microsoft.AspNet.Mvc.Razor { @@ -17,15 +16,13 @@ public class ExpiringFileInfoCache : IFileInfoCache private readonly ConcurrentDictionary _fileInfoCache = new ConcurrentDictionary(StringComparer.Ordinal); - private readonly PhysicalFileSystem _fileSystem; + private readonly IFileSystem _fileSystem; private readonly TimeSpan _offset; - protected virtual IFileSystem FileSystem + public ExpiringFileInfoCache(IOptionsAccessor optionsAccessor) { - get - { - return _fileSystem; - } + _fileSystem = optionsAccessor.Options.FileSystem; + _offset = optionsAccessor.Options.ExpirationBeforeCheckingFilesOnDisk; } protected virtual DateTime UtcNow @@ -36,14 +33,6 @@ protected virtual DateTime UtcNow } } - public ExpiringFileInfoCache(IApplicationEnvironment env, - IOptionsAccessor optionsAccessor) - { - // TODO: Inject the IFileSystem but only when we get it from the host - _fileSystem = new PhysicalFileSystem(env.ApplicationBasePath); - _offset = optionsAccessor.Options.ExpirationBeforeCheckingFilesOnDisk; - } - /// public IFileInfo GetFileInfo([NotNull] string virtualPath) { @@ -59,7 +48,7 @@ public IFileInfo GetFileInfo([NotNull] string virtualPath) } else { - FileSystem.TryGetFileInfo(virtualPath, out fileInfo); + _fileSystem.TryGetFileInfo(virtualPath, out fileInfo); expiringFileInfo = new ExpiringFileInfo() { diff --git a/src/Microsoft.AspNet.Mvc.Razor/Compilation/IExpiringFileInfoCache.cs b/src/Microsoft.AspNet.Mvc.Razor/Compilation/IFileInfoCache.cs similarity index 100% rename from src/Microsoft.AspNet.Mvc.Razor/Compilation/IExpiringFileInfoCache.cs rename to src/Microsoft.AspNet.Mvc.Razor/Compilation/IFileInfoCache.cs diff --git a/src/Microsoft.AspNet.Mvc.Razor/OptionDescriptors/RazorViewEngineOptions.cs b/src/Microsoft.AspNet.Mvc.Razor/OptionDescriptors/RazorViewEngineOptions.cs index 53d534ecd7..3585041721 100644 --- a/src/Microsoft.AspNet.Mvc.Razor/OptionDescriptors/RazorViewEngineOptions.cs +++ b/src/Microsoft.AspNet.Mvc.Razor/OptionDescriptors/RazorViewEngineOptions.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using Microsoft.AspNet.FileSystems; using Microsoft.AspNet.Mvc.Razor.OptionDescriptors; namespace Microsoft.AspNet.Mvc.Razor @@ -13,6 +14,7 @@ namespace Microsoft.AspNet.Mvc.Razor public class RazorViewEngineOptions { private TimeSpan _expirationBeforeCheckingFilesOnDisk = TimeSpan.FromSeconds(2); + private IFileSystem _fileSystem; /// /// Controls the caching behavior. @@ -47,5 +49,28 @@ public TimeSpan ExpirationBeforeCheckingFilesOnDisk /// public IList ViewLocationExpanders { get; } = new List(); + + /// + /// Gets or sets the used by to locate Razor files on + /// disk. + /// + /// + /// At startup, this is initialized to an instance of that is rooted at the + /// application root. + /// + public IFileSystem FileSystem + { + get { return _fileSystem; } + + set + { + if (value == null) + { + throw new ArgumentNullException(nameof(value)); + } + + _fileSystem = value; + } + } } } diff --git a/src/Microsoft.AspNet.Mvc.Razor/Razor/PreCompileViews/RazorPreCompiler.cs b/src/Microsoft.AspNet.Mvc.Razor/Razor/PreCompileViews/RazorPreCompiler.cs index a30c6ba5a9..82194d760d 100644 --- a/src/Microsoft.AspNet.Mvc.Razor/Razor/PreCompileViews/RazorPreCompiler.cs +++ b/src/Microsoft.AspNet.Mvc.Razor/Razor/PreCompileViews/RazorPreCompiler.cs @@ -8,6 +8,7 @@ using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; using Microsoft.Framework.DependencyInjection; +using Microsoft.Framework.OptionsModel; using Microsoft.Framework.Runtime; namespace Microsoft.AspNet.Mvc.Razor @@ -18,29 +19,26 @@ public class RazorPreCompiler private readonly IFileSystem _fileSystem; private readonly IMvcRazorHost _host; - protected virtual string FileExtension - { - get - { - return ".cshtml"; - } - } - public RazorPreCompiler([NotNull] IServiceProvider designTimeServiceProvider) : - this(designTimeServiceProvider, designTimeServiceProvider.GetService()) + this(designTimeServiceProvider, + designTimeServiceProvider.GetService(), + designTimeServiceProvider.GetService>()) { } public RazorPreCompiler([NotNull] IServiceProvider designTimeServiceProvider, - [NotNull] IMvcRazorHost host) + [NotNull] IMvcRazorHost host, + [NotNull] IOptionsAccessor optionsAccessor) { _serviceProvider = designTimeServiceProvider; _host = host; var appEnv = _serviceProvider.GetService(); - _fileSystem = new PhysicalFileSystem(appEnv.ApplicationBasePath); + _fileSystem = optionsAccessor.Options.FileSystem; } + protected virtual string FileExtension { get; } = ".cshtml"; + public virtual void CompileViews([NotNull] IBeforeCompileContext context) { var descriptors = CreateCompilationDescriptors(context); diff --git a/src/Microsoft.AspNet.Mvc.Razor/ViewStartProvider.cs b/src/Microsoft.AspNet.Mvc.Razor/ViewStartProvider.cs index f99d1a61d7..8116ca486b 100644 --- a/src/Microsoft.AspNet.Mvc.Razor/ViewStartProvider.cs +++ b/src/Microsoft.AspNet.Mvc.Razor/ViewStartProvider.cs @@ -5,7 +5,7 @@ using System.Collections.Generic; using System.Linq; using Microsoft.AspNet.FileSystems; -using Microsoft.Framework.Runtime; +using Microsoft.Framework.OptionsModel; namespace Microsoft.AspNet.Mvc.Razor { @@ -15,10 +15,10 @@ public class ViewStartProvider : IViewStartProvider private readonly IFileSystem _fileSystem; private readonly IRazorPageFactory _pageFactory; - public ViewStartProvider(IApplicationEnvironment appEnv, - IRazorPageFactory pageFactory) + public ViewStartProvider(IRazorPageFactory pageFactory, + IOptionsAccessor optionsAccessor) { - _fileSystem = new PhysicalFileSystem(appEnv.ApplicationBasePath); + _fileSystem = optionsAccessor.Options.FileSystem; _pageFactory = pageFactory; } diff --git a/src/Microsoft.AspNet.Mvc/MvcServices.cs b/src/Microsoft.AspNet.Mvc/MvcServices.cs index d14339ab96..e1c66d5218 100644 --- a/src/Microsoft.AspNet.Mvc/MvcServices.cs +++ b/src/Microsoft.AspNet.Mvc/MvcServices.cs @@ -35,6 +35,8 @@ public static IEnumerable GetDefaultServices(IConfiguration // Options and core services. // yield return describe.Transient, MvcOptionsSetup>(); + yield return describe.Transient, RazorViewEngineOptionsSetup>(); + yield return describe.Transient(); yield return describe.Transient(typeof(INestedProviderManager<>), typeof(NestedProviderManager<>)); yield return describe.Transient(typeof(INestedProviderManagerAsync<>), typeof(NestedProviderManagerAsync<>)); @@ -106,7 +108,11 @@ public static IEnumerable GetDefaultServices(IConfiguration yield return describe.Singleton(); // The host is designed to be discarded after consumption and is very inexpensive to initialize. - yield return describe.Transient(); + yield return describe.Transient(serviceProvider => + { + var optionsAccessor = serviceProvider.GetService>(); + return new MvcRazorHost(optionsAccessor.Options.FileSystem); + }); yield return describe.Singleton(); yield return describe.Singleton(); diff --git a/src/Microsoft.AspNet.Mvc/RazorPreCompileModule.cs b/src/Microsoft.AspNet.Mvc/RazorPreCompileModule.cs index 560a157143..0f72b07400 100644 --- a/src/Microsoft.AspNet.Mvc/RazorPreCompileModule.cs +++ b/src/Microsoft.AspNet.Mvc/RazorPreCompileModule.cs @@ -6,9 +6,10 @@ using Microsoft.AspNet.Mvc.Razor; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; -using Microsoft.Framework.Runtime; using Microsoft.Framework.DependencyInjection; using Microsoft.Framework.DependencyInjection.Fallback; +using Microsoft.Framework.OptionsModel; +using Microsoft.Framework.Runtime; namespace Microsoft.AspNet.Mvc { @@ -21,9 +22,16 @@ public RazorPreCompileModule(IServiceProvider services) _appServices = services; } + protected virtual string FileExtension { get; } = ".cshtml"; + public virtual void BeforeCompile(IBeforeCompileContext context) { var sc = new ServiceCollection(); + var appEnv = _appServices.GetService(); + + var setup = new RazorViewEngineOptionsSetup(appEnv); + var accessor = new OptionsAccessor(new[] { setup }); + sc.AddInstance>(accessor); sc.Add(MvcServices.GetDefaultServices()); var sp = sc.BuildServiceProvider(_appServices); diff --git a/src/Microsoft.AspNet.Mvc/RazorViewEngineOptionsSetup.cs b/src/Microsoft.AspNet.Mvc/RazorViewEngineOptionsSetup.cs new file mode 100644 index 0000000000..d93e10d43c --- /dev/null +++ b/src/Microsoft.AspNet.Mvc/RazorViewEngineOptionsSetup.cs @@ -0,0 +1,32 @@ +// 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.FileSystems; +using Microsoft.AspNet.Mvc.Razor; +using Microsoft.Framework.OptionsModel; +using Microsoft.Framework.Runtime; + +namespace Microsoft.AspNet.Mvc +{ + /// + /// Sets up default options for . + /// + public class RazorViewEngineOptionsSetup : OptionsAction + { + /// + /// Initializes a new instance of . + /// + /// for the application. + public RazorViewEngineOptionsSetup(IApplicationEnvironment applicationEnvironment) + : base(options => ConfigureRazor(options, applicationEnvironment)) + { + Order = DefaultOrder.DefaultFrameworkSortOrder; + } + + private static void ConfigureRazor(RazorViewEngineOptions razorOptions, + IApplicationEnvironment applicationEnvironment) + { + razorOptions.FileSystem = new PhysicalFileSystem(applicationEnvironment.ApplicationBasePath); + } + } +} \ No newline at end of file diff --git a/test/Microsoft.AspNet.Mvc.Razor.Test/Compilation/ExpiringFileInfoCacheTest.cs b/test/Microsoft.AspNet.Mvc.Razor.Test/Compilation/ExpiringFileInfoCacheTest.cs index 3efe7db857..7414bf09e7 100644 --- a/test/Microsoft.AspNet.Mvc.Razor.Test/Compilation/ExpiringFileInfoCacheTest.cs +++ b/test/Microsoft.AspNet.Mvc.Razor.Test/Compilation/ExpiringFileInfoCacheTest.cs @@ -6,7 +6,6 @@ using System.IO; using Microsoft.AspNet.FileSystems; using Microsoft.Framework.OptionsModel; -using Microsoft.Framework.Runtime; using Moq; using Xunit; @@ -16,30 +15,16 @@ public class ExpiringFileInfoCacheTest { private const string FileName = "myView.cshtml"; - public IApplicationEnvironment ApplicationEnvironment - { - get - { - var mock = new Mock(MockBehavior.Strict); - mock.Setup(ae => ae.ApplicationBasePath).Returns(Directory.GetCurrentDirectory()); - - return mock.Object; - } - } - - public RazorViewEngineOptions Options - { - get - { - return new RazorViewEngineOptions(); - } - } + public DummyFileSystem TestFileSystem { get; } = new DummyFileSystem(); public IOptionsAccessor OptionsAccessor { get { - var options = Options; + var options = new RazorViewEngineOptions + { + FileSystem = TestFileSystem + }; var mock = new Mock>(MockBehavior.Strict); mock.Setup(oa => oa.Options).Returns(options); @@ -50,18 +35,18 @@ public IOptionsAccessor OptionsAccessor public ControllableExpiringFileInfoCache GetCache(IOptionsAccessor optionsAccessor) { - return new ControllableExpiringFileInfoCache(ApplicationEnvironment, optionsAccessor); + return new ControllableExpiringFileInfoCache(optionsAccessor); } - public void CreateFile(string FileName, ControllableExpiringFileInfoCache cache) + public void CreateFile(string fileName) { var fileInfo = new DummyFileInfo() { - Name = FileName, + Name = fileName, LastModified = DateTime.Now, }; - cache.UnderlyingFileSystem.AddFile(fileInfo); + TestFileSystem.AddFile(fileInfo); } public void Sleep(ControllableExpiringFileInfoCache cache, int offsetMilliseconds) @@ -96,7 +81,7 @@ public void GettingFileInfoReturnsTheSameDataWithDefaultOptions() // Arrange var cache = GetCache(OptionsAccessor); - CreateFile(FileName, cache); + CreateFile(FileName); // Act var fileInfo1 = cache.GetFileInfo(FileName); @@ -114,12 +99,12 @@ public void GettingFileInfoReturnsTheSameDataWithDefaultOptionsEvenWhenFilesHave // Arrange var cache = GetCache(OptionsAccessor); - CreateFile(FileName, cache); + CreateFile(FileName); // Act var fileInfo1 = cache.GetFileInfo(FileName); - CreateFile(FileName, cache); + CreateFile(FileName); var fileInfo2 = cache.GetFileInfo(FileName); @@ -139,13 +124,13 @@ public void GettingFileInfoReturnsNewDataWithDefaultOptionsAfterExpirationAndFil // Arrange var cache = GetCache(optionsAccessor); - CreateFile(FileName, cache); + CreateFile(FileName); // Act var fileInfo1 = cache.GetFileInfo(FileName); Sleep(optionsAccessor, cache, 500); - CreateFile(FileName, cache); + CreateFile(FileName); var fileInfo2 = cache.GetFileInfo(FileName); @@ -164,7 +149,7 @@ public void GettingFileInfoReturnsNewDataWithDefaultOptionsAfterExpiration() var cache = GetCache(optionsAccessor); - CreateFile(FileName, cache); + CreateFile(FileName); // Act var fileInfo1 = cache.GetFileInfo(FileName); @@ -213,7 +198,7 @@ public void GettingFileInfoReturnsNewDataWithCustomImmediateExpiration(TimeSpan string FileName = "myfile4.cshtml"; var cache = GetCache(optionsAccessor); - CreateFile(FileName, cache); + CreateFile(FileName); // Act var fileInfo1 = cache.GetFileInfo(FileName); @@ -254,7 +239,7 @@ public void GettingFileInfoReturnsNewDataWithCustomExpiration(TimeSpan expiratio string FileName = "myfile5.cshtml"; var cache = GetCache(optionsAccessor); - CreateFile(FileName, cache); + CreateFile(FileName); // Act var fileInfo1 = cache.GetFileInfo(FileName); @@ -280,7 +265,7 @@ public void GettingFileInfoReturnsSameDataWithCustomExpiration(TimeSpan expirati string FileName = "myfile6.cshtml"; var cache = GetCache(optionsAccessor); - CreateFile(FileName, cache); + CreateFile(FileName); // Act var fileInfo1 = cache.GetFileInfo(FileName); @@ -305,7 +290,7 @@ public void GettingFileInfoReturnsSameDataWithMaxExpiration() string FileName = "myfile7.cshtml"; var cache = GetCache(optionsAccessor); - CreateFile(FileName, cache); + CreateFile(FileName); // Act var fileInfo1 = cache.GetFileInfo(FileName); @@ -322,14 +307,12 @@ public void GettingFileInfoReturnsSameDataWithMaxExpiration() public class ControllableExpiringFileInfoCache : ExpiringFileInfoCache { - public ControllableExpiringFileInfoCache(IApplicationEnvironment env, - IOptionsAccessor optionsAccessor) - : base(env, optionsAccessor) + public ControllableExpiringFileInfoCache(IOptionsAccessor optionsAccessor) + : base(optionsAccessor) { } private DateTime? _internalUtcNow { get; set; } - private DummyFileSystem _underlyingFileSystem = new DummyFileSystem(); protected override DateTime UtcNow { @@ -344,14 +327,6 @@ protected override DateTime UtcNow } } - protected override IFileSystem FileSystem - { - get - { - return UnderlyingFileSystem; - } - } - public void Sleep(int milliSeconds) { if (milliSeconds <= 0) @@ -361,14 +336,6 @@ public void Sleep(int milliSeconds) _internalUtcNow = UtcNow.AddMilliseconds(milliSeconds); } - - public DummyFileSystem UnderlyingFileSystem - { - get - { - return _underlyingFileSystem; - } - } } public class DummyFileSystem : IFileSystem diff --git a/test/Microsoft.AspNet.Mvc.Razor.Test/RazorViewEngineOptionsTest.cs b/test/Microsoft.AspNet.Mvc.Razor.Test/RazorViewEngineOptionsTest.cs new file mode 100644 index 0000000000..12ffb3abc4 --- /dev/null +++ b/test/Microsoft.AspNet.Mvc.Razor.Test/RazorViewEngineOptionsTest.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 System; +using Xunit; + +namespace Microsoft.AspNet.Mvc.Razor +{ + public class RazorViewEngineOptionsTest + { + [Fact] + public void FileSystemThrows_IfNullIsAsseigned() + { + // Arrange + var options = new RazorViewEngineOptions(); + + // Act and Assert + var ex = Assert.Throws(() => options.FileSystem = null); + Assert.Equal("value", ex.ParamName); + } + } +} \ No newline at end of file diff --git a/test/Microsoft.AspNet.Mvc.Test/RazorViewEngineOptionsSetupTest.cs b/test/Microsoft.AspNet.Mvc.Test/RazorViewEngineOptionsSetupTest.cs new file mode 100644 index 0000000000..2226461a36 --- /dev/null +++ b/test/Microsoft.AspNet.Mvc.Test/RazorViewEngineOptionsSetupTest.cs @@ -0,0 +1,33 @@ +// 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.IO; +using Microsoft.AspNet.FileSystems; +using Microsoft.AspNet.Mvc.Razor; +using Microsoft.Framework.Runtime; +using Moq; +using Xunit; + +namespace Microsoft.AspNet.Mvc +{ + public class RazorViewEngineOptionsSetupTest + { + [Fact] + public void RazorViewEngineOptionsSetup_SetsUpFileSystem() + { + // Arrange + var options = new RazorViewEngineOptions(); + var appEnv = new Mock(); + appEnv.SetupGet(e => e.ApplicationBasePath) + .Returns(Directory.GetCurrentDirectory()); + var optionsSetup = new RazorViewEngineOptionsSetup(appEnv.Object); + + // Act + optionsSetup.Invoke(options); + + // Assert + Assert.NotNull(options.FileSystem); + Assert.IsType(options.FileSystem); + } + } +} \ No newline at end of file diff --git a/test/Microsoft.AspNet.Mvc.Test/project.json b/test/Microsoft.AspNet.Mvc.Test/project.json index 6a8a995178..55c791c321 100644 --- a/test/Microsoft.AspNet.Mvc.Test/project.json +++ b/test/Microsoft.AspNet.Mvc.Test/project.json @@ -10,6 +10,10 @@ "test": "Xunit.KRunner" }, "frameworks": { - "aspnet50": { } + "aspnet50": { + "dependencies": { + "Moq": "4.2.1312.1622" + } + } } } From 3f54492930bf4ac2f2741de0f6bb7a060c3af9d4 Mon Sep 17 00:00:00 2001 From: jacalvar Date: Tue, 7 Oct 2014 15:17:11 -0700 Subject: [PATCH 22/39] [Fixes #885] API Explorer: Link Generation 1) Expose the simplified relative path template by cleaning up constraints, optional and catch all tokens from the template. 2) Expose the parameters on the route template as API parameters. 3) Combine parameters from the route and the action descriptor when the parameter doesn't come from the body. #886 will refine this. 4) Expose optionality and constraints for path parameters. Open question: Should we explicitly expose IsCatchAll? --- .../ApiExplorerSamples/ProductsController.cs | 4 +- .../Views/ApiExplorer/_ApiDescription.cshtml | 8 +- .../Description/ApiParameterDescription.cs | 5 + .../Description/ApiParameterSource.cs | 1 + .../DefaultApiDescriptionProvider.cs | 197 ++++++++++-- .../DefaultApiDescriptionProviderTest.cs | 281 +++++++++++++++++- .../ApiExplorerTest.cs | 252 +++++++++++++++- .../ApiExplorerDataFilter.cs | 5 +- ...eAndPathParametersInformationController.cs | 38 +++ 9 files changed, 746 insertions(+), 45 deletions(-) create mode 100644 test/WebSites/ApiExplorerWebSite/Controllers/ApiExplorerRouteAndPathParametersInformationController.cs diff --git a/samples/MvcSample.Web/Controllers/ApiExplorerSamples/ProductsController.cs b/samples/MvcSample.Web/Controllers/ApiExplorerSamples/ProductsController.cs index fd64e2b472..538791b98d 100644 --- a/samples/MvcSample.Web/Controllers/ApiExplorerSamples/ProductsController.cs +++ b/samples/MvcSample.Web/Controllers/ApiExplorerSamples/ProductsController.cs @@ -9,7 +9,7 @@ namespace MvcSample.Web.ApiExplorerSamples [Route("api/Products")] public class ProductsController : Controller { - [HttpGet("{id}")] + [HttpGet("{id:int}")] public Product GetById(int id) { return null; @@ -22,7 +22,7 @@ public IEnumerable SearchByName(string name) } [Produces("application/json", Type = typeof(ProductOrderConfirmation))] - [HttpPut("{id}/Buy")] + [HttpPut("{id:int}/Buy")] public IActionResult Buy(int projectId, int quantity = 1) { return null; diff --git a/samples/MvcSample.Web/Views/ApiExplorer/_ApiDescription.cshtml b/samples/MvcSample.Web/Views/ApiExplorer/_ApiDescription.cshtml index b0e24064b4..f3e73a79b2 100644 --- a/samples/MvcSample.Web/Views/ApiExplorer/_ApiDescription.cshtml +++ b/samples/MvcSample.Web/Views/ApiExplorer/_ApiDescription.cshtml @@ -13,7 +13,11 @@
    @foreach (var parameter in Model.ParameterDescriptions) { -
  • @parameter.Name - @parameter.Type.FullName - @parameter.Source.ToString()
  • +
  • + @parameter.Name - @(parameter?.Type?.FullName ?? "Unknown") - @parameter.Source.ToString() + - Constraint: @(parameter?.Constraint?.GetType()?.Name?.Replace("RouteConstraint", "") ?? " none") + - Default value: @(parameter?.DefaultValue ?? " none") +
  • }
} @@ -22,7 +26,7 @@ {

Response Formats:

    - @foreach(var response in Model.SupportedResponseFormats) + @foreach (var response in Model.SupportedResponseFormats) {
  • @response.MediaType.RawValue - @response.Formatter.GetType().Name
  • } diff --git a/src/Microsoft.AspNet.Mvc.Core/Description/ApiParameterDescription.cs b/src/Microsoft.AspNet.Mvc.Core/Description/ApiParameterDescription.cs index a9dba0415a..80a85b576a 100644 --- a/src/Microsoft.AspNet.Mvc.Core/Description/ApiParameterDescription.cs +++ b/src/Microsoft.AspNet.Mvc.Core/Description/ApiParameterDescription.cs @@ -3,6 +3,7 @@ using System; using Microsoft.AspNet.Mvc.ModelBinding; +using Microsoft.AspNet.Routing; namespace Microsoft.AspNet.Mvc.Description { @@ -18,6 +19,10 @@ public class ApiParameterDescription public ApiParameterSource Source { get; set; } + public IRouteConstraint Constraint { get; set; } + + public object DefaultValue { get; set; } + public Type Type { get; set; } } } \ No newline at end of file diff --git a/src/Microsoft.AspNet.Mvc.Core/Description/ApiParameterSource.cs b/src/Microsoft.AspNet.Mvc.Core/Description/ApiParameterSource.cs index 81376af670..ce4121f295 100644 --- a/src/Microsoft.AspNet.Mvc.Core/Description/ApiParameterSource.cs +++ b/src/Microsoft.AspNet.Mvc.Core/Description/ApiParameterSource.cs @@ -8,5 +8,6 @@ public enum ApiParameterSource { Body, Query, + Path } } \ No newline at end of file diff --git a/src/Microsoft.AspNet.Mvc.Core/Description/DefaultApiDescriptionProvider.cs b/src/Microsoft.AspNet.Mvc.Core/Description/DefaultApiDescriptionProvider.cs index 8f53217742..ad8e03f8d1 100644 --- a/src/Microsoft.AspNet.Mvc.Core/Description/DefaultApiDescriptionProvider.cs +++ b/src/Microsoft.AspNet.Mvc.Core/Description/DefaultApiDescriptionProvider.cs @@ -3,10 +3,13 @@ using System; using System.Collections.Generic; +using System.Diagnostics.Contracts; using System.Linq; using System.Threading.Tasks; using Microsoft.AspNet.Mvc.HeaderValueAbstractions; using Microsoft.AspNet.Mvc.ModelBinding; +using Microsoft.AspNet.Routing; +using Microsoft.AspNet.Routing.Template; using Microsoft.Framework.DependencyInjection; namespace Microsoft.AspNet.Mvc.Description @@ -19,6 +22,7 @@ public class DefaultApiDescriptionProvider : INestedProvider /// Creates a new instance of . @@ -27,10 +31,12 @@ public class DefaultApiDescriptionProvider : INestedProviderThe . public DefaultApiDescriptionProvider( IOutputFormattersProvider formattersProvider, + IInlineConstraintResolver constraintResolver, IModelMetadataProvider modelMetadataProvider) { _formattersProvider = formattersProvider; _modelMetadataProvider = modelMetadataProvider; + _constraintResolver = constraintResolver; } /// @@ -60,25 +66,23 @@ public void Invoke(ApiDescriptionProviderContext context, Action callNext) } private ApiDescription CreateApiDescription( - ControllerActionDescriptor action, - string httpMethod, + ControllerActionDescriptor action, + string httpMethod, string groupName) { + var parsedTemplate = ParseTemplate(action); + var apiDescription = new ApiDescription() { ActionDescriptor = action, GroupName = groupName, HttpMethod = httpMethod, - RelativePath = GetRelativePath(action), + RelativePath = GetRelativePath(parsedTemplate), }; - if (action.Parameters != null) - { - foreach (var parameter in action.Parameters) - { - apiDescription.ParameterDescriptions.Add(GetParameter(parameter)); - } - } + var templateParameters = parsedTemplate?.Parameters?.ToList() ?? new List(); + + GetParameters(apiDescription, action.Parameters, templateParameters); var responseMetadataAttributes = GetResponseMetadataAttributes(action); @@ -103,13 +107,13 @@ private ApiDescription CreateApiDescription( apiDescription.ResponseType = runtimeReturnType; apiDescription.ResponseModelMetadata = _modelMetadataProvider.GetMetadataForType( - modelAccessor: null, + modelAccessor: null, modelType: runtimeReturnType); var formats = GetResponseFormats( - action, - responseMetadataAttributes, - declaredReturnType, + action, + responseMetadataAttributes, + declaredReturnType, runtimeReturnType); foreach (var format in formats) @@ -121,6 +125,44 @@ private ApiDescription CreateApiDescription( return apiDescription; } + private void GetParameters( + ApiDescription apiDescription, + IList parameterDescriptors, + IList templateParameters) + { + if (parameterDescriptors != null) + { + foreach (var parameter in parameterDescriptors) + { + // Process together parameters that appear on the path template and on the + // action descriptor and do not come from the body. + TemplatePart templateParameter = null; + if (parameter.BodyParameterInfo == null) + { + templateParameter = templateParameters + .FirstOrDefault(p => p.Name.Equals(parameter.Name, StringComparison.OrdinalIgnoreCase)); + + if (templateParameter != null) + { + templateParameters.Remove(templateParameter); + } + } + + apiDescription.ParameterDescriptions.Add(GetParameter(parameter, templateParameter)); + } + } + + if (templateParameters.Count > 0) + { + // Process parameters that only appear on the path template if any. + foreach (var templateParameter in templateParameters) + { + var parameterDescription = GetParameter(parameterDescriptor: null, templateParameter: templateParameter); + apiDescription.ParameterDescriptions.Add(parameterDescription); + } + } + } + private IEnumerable GetHttpMethods(ControllerActionDescriptor action) { if (action.ActionConstraints != null && action.ActionConstraints.Count > 0) @@ -133,23 +175,93 @@ private IEnumerable GetHttpMethods(ControllerActionDescriptor action) } } - private string GetRelativePath(ControllerActionDescriptor action) + private RouteTemplate ParseTemplate(ControllerActionDescriptor action) { - // This is a placeholder for functionality which will correctly generate the relative path - // stub of an action. See: #885 if (action.AttributeRouteInfo != null && action.AttributeRouteInfo.Template != null) { - return action.AttributeRouteInfo.Template; + return TemplateParser.Parse(action.AttributeRouteInfo.Template, _constraintResolver); } return null; } - private ApiParameterDescription GetParameter(ParameterDescriptor parameter) + private string GetRelativePath(RouteTemplate parsedTemplate) + { + if (parsedTemplate == null) + { + return null; + } + + var segments = new List(); + + foreach (var segment in parsedTemplate.Segments) + { + var currentSegment = ""; + foreach (var part in segment.Parts) + { + if (part.IsLiteral) + { + currentSegment += part.Text; + } + else if (part.IsParameter) + { + currentSegment += "{" + part.Name + "}"; + } + } + + segments.Add(currentSegment); + } + + return string.Join("/", segments); + } + + private ApiParameterDescription GetParameter( + ParameterDescriptor parameterDescriptor, + TemplatePart templateParameter) { // This is a placeholder based on currently available functionality for parameters. See #886. - var resourceParameter = new ApiParameterDescription() + ApiParameterDescription parameterDescription = null; + + if (templateParameter != null && parameterDescriptor == null) + { + // The parameter is part of the route template but not part of the ActionDescriptor. + + // For now if a parameter is part of the template we will asume its value comes from the path. + // We will be more accurate when we implement #886. + parameterDescription = CreateParameterFromTemplate(templateParameter); + } + else if (templateParameter != null && parameterDescriptor != null) + { + // The parameter is part of the route template and part of the ActionDescriptor. + parameterDescription = CreateParameterFromTemplateAndParameterDescriptor( + templateParameter, + parameterDescriptor); + } + else if(templateParameter == null && parameterDescriptor != null) + { + // The parameter is part of the ActionDescriptor but is not part of the route template. + parameterDescription = CreateParameterFromParameterDescriptor(parameterDescriptor); + } + else + { + // We will never call this method with templateParameter == null && parameterDescriptor == null + Contract.Assert(parameterDescriptor != null); + } + + if (parameterDescription.Type != null) + { + parameterDescription.ModelMetadata = _modelMetadataProvider.GetMetadataForType( + modelAccessor: null, + modelType: parameterDescription.Type); + } + + return parameterDescription; + } + + private static ApiParameterDescription CreateParameterFromParameterDescriptor(ParameterDescriptor parameter) + { + var resourceParameter = new ApiParameterDescription { IsOptional = parameter.IsOptional, Name = parameter.Name, @@ -158,8 +270,8 @@ private ApiParameterDescription GetParameter(ParameterDescriptor parameter) if (parameter.ParameterBindingInfo != null) { - resourceParameter.Type = parameter.ParameterBindingInfo.ParameterType; resourceParameter.Source = ApiParameterSource.Query; + resourceParameter.Type = parameter.ParameterBindingInfo.ParameterType; } if (parameter.BodyParameterInfo != null) @@ -168,16 +280,49 @@ private ApiParameterDescription GetParameter(ParameterDescriptor parameter) resourceParameter.Source = ApiParameterSource.Body; } - if (resourceParameter.Type != null) + return resourceParameter; + } + + private static ApiParameterDescription CreateParameterFromTemplateAndParameterDescriptor( + TemplatePart templateParameter, + ParameterDescriptor parameter) + { + var resourceParameter = new ApiParameterDescription + { + Source = ApiParameterSource.Path, + IsOptional = parameter.IsOptional && IsOptionalParameter(templateParameter), + Name = parameter.Name, + ParameterDescriptor = parameter, + Constraint = templateParameter.InlineConstraint, + DefaultValue = templateParameter.DefaultValue, + }; + + if (parameter.ParameterBindingInfo != null) { - resourceParameter.ModelMetadata = _modelMetadataProvider.GetMetadataForType( - modelAccessor: null, - modelType: resourceParameter.Type); + resourceParameter.Type = parameter.ParameterBindingInfo.ParameterType; } return resourceParameter; } + private static bool IsOptionalParameter(TemplatePart templateParameter) + { + return templateParameter.IsOptional || templateParameter.DefaultValue != null; + } + + private static ApiParameterDescription CreateParameterFromTemplate(TemplatePart templateParameter) + { + return new ApiParameterDescription + { + Source = ApiParameterSource.Path, + IsOptional = IsOptionalParameter(templateParameter), + Name = templateParameter.Name, + ParameterDescriptor = null, + Constraint = templateParameter.InlineConstraint, + DefaultValue = templateParameter.DefaultValue, + }; + } + private IReadOnlyList GetResponseFormats( ControllerActionDescriptor action, IApiResponseMetadataProvider[] responseMetadataAttributes, @@ -220,7 +365,7 @@ private IReadOnlyList GetResponseFormats( } } } - } + } return results; } diff --git a/test/Microsoft.AspNet.Mvc.Core.Test/Description/DefaultApiDescriptionProviderTest.cs b/test/Microsoft.AspNet.Mvc.Core.Test/Description/DefaultApiDescriptionProviderTest.cs index c674364e84..514431a1e0 100644 --- a/test/Microsoft.AspNet.Mvc.Core.Test/Description/DefaultApiDescriptionProviderTest.cs +++ b/test/Microsoft.AspNet.Mvc.Core.Test/Description/DefaultApiDescriptionProviderTest.cs @@ -8,6 +8,8 @@ using Microsoft.AspNet.Mvc.HeaderValueAbstractions; using Microsoft.AspNet.Mvc.ModelBinding; using Microsoft.AspNet.Mvc.Routing; +using Microsoft.AspNet.Routing; +using Microsoft.AspNet.Routing.Constraints; using Moq; using Xunit; @@ -151,25 +153,269 @@ public void GetApiDescription_PopulatesParameters() Assert.Equal(typeof(string), username.Type); } - // This is a placeholder based on current functionality - see #885 + [Theory] + [InlineData("api/products/{id}", false, null, null)] + [InlineData("api/products/{id?}", true, null, null)] + [InlineData("api/products/{id=5}", true, null, "5")] + [InlineData("api/products/{id:int}", false, typeof(IntRouteConstraint), null)] + [InlineData("api/products/{id:int?}", true, typeof(IntRouteConstraint), null)] + [InlineData("api/products/{id:int=5}", true, null, "5")] + [InlineData("api/products/{*id}", false, null, null)] + [InlineData("api/products/{*id:int}", false, typeof(IntRouteConstraint), null)] + [InlineData("api/products/{*id:int=5}", true, typeof(IntRouteConstraint), "5")] + public void GetApiDescription_PopulatesParameters_ThatAppearOnlyOnRouteTemplate( + string template, + bool isOptional, + Type constraintType, + object defaultValue) + { + // Arrange + var action = CreateActionDescriptor(); + action.AttributeRouteInfo = new AttributeRouteInfo { Template = template }; + + // Act + var descriptions = GetApiDescriptions(action); + + // Assert + var description = Assert.Single(descriptions); + + var parameter = Assert.Single(description.ParameterDescriptions); + Assert.Equal(ApiParameterSource.Path, parameter.Source); + Assert.Equal(isOptional, parameter.IsOptional); + Assert.Equal("id", parameter.Name); + Assert.Null(parameter.ParameterDescriptor); + + if (constraintType != null) + { + Assert.IsType(constraintType, parameter.Constraint); + } + + if (defaultValue != null) + { + Assert.Equal(defaultValue, parameter.DefaultValue); + } + else + { + Assert.Null(parameter.DefaultValue); + } + } + + [Theory] + [InlineData("api/products/{id}", false, null, null)] + [InlineData("api/products/{id?}", true, null, null)] + [InlineData("api/products/{id=5}", true, null, "5")] + [InlineData("api/products/{id:int}", false, typeof(IntRouteConstraint), null)] + [InlineData("api/products/{id:int?}", true, typeof(IntRouteConstraint), null)] + [InlineData("api/products/{id:int=5}", true, typeof(IntRouteConstraint), "5")] + [InlineData("api/products/{*id}", false, null, null)] + [InlineData("api/products/{*id:int}", false, typeof(IntRouteConstraint), null)] + [InlineData("api/products/{*id:int=5}", true, typeof(IntRouteConstraint), "5")] + public void GetApiDescription_PopulatesParametersThatAppearOnRouteTemplate_AndHaveAssociatedParameterDescriptor( + string template, + bool isOptional, + Type constraintType, + object defaultValue) + { + // Arrange + var action = CreateActionDescriptor(); + action.AttributeRouteInfo = new AttributeRouteInfo { Template = template }; + + var parameterDescriptor = new ParameterDescriptor + { + Name = "id", + IsOptional = true, + ParameterBindingInfo = new ParameterBindingInfo("id", typeof(int)) + }; + action.Parameters = new List { parameterDescriptor }; + + // Act + var descriptions = GetApiDescriptions(action); + + // Assert + var description = Assert.Single(descriptions); + + var parameter = Assert.Single(description.ParameterDescriptions); + Assert.Equal(ApiParameterSource.Path, parameter.Source); + Assert.Equal(isOptional, parameter.IsOptional); + Assert.Equal("id", parameter.Name); + Assert.Equal(parameterDescriptor, parameter.ParameterDescriptor); + + if (constraintType != null) + { + Assert.IsType(constraintType, parameter.Constraint); + } + + if (defaultValue != null) + { + Assert.Equal(defaultValue, parameter.DefaultValue); + } + else + { + Assert.Null(parameter.DefaultValue); + } + } + + [Theory] + [InlineData("api/products/{id}", false, null, null)] + [InlineData("api/products/{id?}", true, null, null)] + [InlineData("api/products/{id=5}", true, null, "5")] + [InlineData("api/products/{id:int}", false, typeof(IntRouteConstraint), null)] + [InlineData("api/products/{id:int?}", true, typeof(IntRouteConstraint), null)] + [InlineData("api/products/{id:int=5}", true, typeof(IntRouteConstraint), "5")] + [InlineData("api/products/{*id}", false, null, null)] + [InlineData("api/products/{*id:int}", false, typeof(IntRouteConstraint), null)] + [InlineData("api/products/{*id:int=5}", true, typeof(IntRouteConstraint), "5")] + public void GetApiDescription_CreatesDifferentParameters_IfParameterDescriptorIsFromBody( + string template, + bool isOptional, + Type constraintType, + object defaultValue) + { + // Arrange + var action = CreateActionDescriptor(); + action.AttributeRouteInfo = new AttributeRouteInfo { Template = template }; + + var parameterDescriptor = new ParameterDescriptor + { + Name = "id", + IsOptional = false, + BodyParameterInfo = new BodyParameterInfo(typeof(int)) + }; + action.Parameters = new List { parameterDescriptor }; + + // Act + var descriptions = GetApiDescriptions(action); + + // Assert + var description = Assert.Single(descriptions); + + var bodyParameter = Assert.Single(description.ParameterDescriptions, p => p.Source == ApiParameterSource.Body); + Assert.False(bodyParameter.IsOptional); + Assert.Equal("id", bodyParameter.Name); + Assert.Equal(parameterDescriptor, bodyParameter.ParameterDescriptor); + + var pathParameter = Assert.Single(description.ParameterDescriptions, p => p.Source == ApiParameterSource.Path); + Assert.Equal(isOptional, pathParameter.IsOptional); + Assert.Equal("id", pathParameter.Name); + Assert.Null(pathParameter.ParameterDescriptor); + + if (constraintType != null) + { + Assert.IsType(constraintType, pathParameter.Constraint); + } + + if (defaultValue != null) + { + Assert.Equal(defaultValue, pathParameter.DefaultValue); + } + else + { + Assert.Null(pathParameter.DefaultValue); + } + } + + [Theory] + [InlineData("api/products/{id}", false, false)] + [InlineData("api/products/{id}", true, false)] + [InlineData("api/products/{id?}", false, false)] + [InlineData("api/products/{id?}", true, true)] + [InlineData("api/products/{id=5}", false, false)] + [InlineData("api/products/{id=5}", true, true)] + public void GetApiDescription_ParameterFromPathAndDescriptor_IsOptionalOnly_IfBothAreOptional( + string template, + bool isDescriptorParameterOptional, + bool expectedOptional) + { + // Arrange + var action = CreateActionDescriptor(); + action.AttributeRouteInfo = new AttributeRouteInfo { Template = template }; + + var parameterDescriptor = new ParameterDescriptor + { + Name = "id", + IsOptional = isDescriptorParameterOptional, + ParameterBindingInfo = new ParameterBindingInfo("id", typeof(int)) + }; + action.Parameters = new List { parameterDescriptor }; + + // Act + var descriptions = GetApiDescriptions(action); + + // Assert + var description = Assert.Single(descriptions); + var parameter = Assert.Single(description.ParameterDescriptions); + Assert.Equal(expectedOptional, parameter.IsOptional); + } + + [Theory] + [InlineData("api/Products/{id}", "api/Products/{id}")] + [InlineData("api/Products/{id?}", "api/Products/{id}")] + [InlineData("api/Products/{id:int}", "api/Products/{id}")] + [InlineData("api/Products/{id:int?}", "api/Products/{id}")] + [InlineData("api/Products/{*id}", "api/Products/{id}")] + [InlineData("api/Products/{*id:int}", "api/Products/{id}")] + [InlineData("api/Products/{id1}-{id2:int}", "api/Products/{id1}-{id2}")] + [InlineData("api/{id1}/{id2?}/{id3:int}/{id4:int?}/{*id5:int}", "api/{id1}/{id2}/{id3}/{id4}/{id5}")] + public void GetApiDescription_PopulatesRelativePath(string template, string relativePath) + { + // Arrange + var action = CreateActionDescriptor(); + action.AttributeRouteInfo = new AttributeRouteInfo(); + action.AttributeRouteInfo.Template = template; + + // Act + var descriptions = GetApiDescriptions(action); + + // Assert + var description = Assert.Single(descriptions); + Assert.Equal(relativePath, description.RelativePath); + } + [Fact] - public void GetApiDescription_PopluatesRelativePath() + public void GetApiDescription_DetectsMultipleParameters_OnTheSameSegment() { // Arrange var action = CreateActionDescriptor(); action.AttributeRouteInfo = new AttributeRouteInfo(); - action.AttributeRouteInfo.Template = "api/Products/{id}"; + action.AttributeRouteInfo.Template = "api/Products/{id1}-{id2:int}"; // Act var descriptions = GetApiDescriptions(action); // Assert var description = Assert.Single(descriptions); - Assert.Equal("api/Products/{id}", description.RelativePath); + var id1 = Assert.Single(description.ParameterDescriptions, p => p.Name == "id1"); + Assert.Equal(ApiParameterSource.Path, id1.Source); + Assert.Null(id1.Constraint); + + var id2 = Assert.Single(description.ParameterDescriptions, p => p.Name == "id2"); + Assert.Equal(ApiParameterSource.Path, id2.Source); + Assert.IsType(id2.Constraint); } [Fact] - public void GetApiDescription_PopluatesResponseType_WithProduct() + public void GetApiDescription_DetectsMultipleParameters_OnDifferentSegments() + { + // Arrange + var action = CreateActionDescriptor(); + action.AttributeRouteInfo = new AttributeRouteInfo(); + action.AttributeRouteInfo.Template = "api/Products/{id1}-{id2}/{id3:int}/{id4:int?}/{*id5:int}"; + + // Act + var descriptions = GetApiDescriptions(action); + + // Assert + var description = Assert.Single(descriptions); + + Assert.Single(description.ParameterDescriptions, p => p.Name == "id1"); + Assert.Single(description.ParameterDescriptions, p => p.Name == "id2"); + Assert.Single(description.ParameterDescriptions, p => p.Name == "id3"); + Assert.Single(description.ParameterDescriptions, p => p.Name == "id4"); + Assert.Single(description.ParameterDescriptions, p => p.Name == "id5"); + } + + [Fact] + public void GetApiDescription_PopulatesResponseType_WithProduct() { // Arrange var action = CreateActionDescriptor(nameof(ReturnsProduct)); @@ -184,7 +430,7 @@ public void GetApiDescription_PopluatesResponseType_WithProduct() } [Fact] - public void GetApiDescription_PopluatesResponseType_WithTaskOfProduct() + public void GetApiDescription_PopulatesResponseType_WithTaskOfProduct() { // Arrange var action = CreateActionDescriptor(nameof(ReturnsTaskOfProduct)); @@ -205,7 +451,7 @@ public void GetApiDescription_PopluatesResponseType_WithTaskOfProduct() [InlineData(nameof(ReturnsTaskOfObject))] [InlineData(nameof(ReturnsTaskOfActionResult))] [InlineData(nameof(ReturnsTaskOfJsonResult))] - public void GetApiDescription_DoesNotPopluatesResponseInformation_WhenUnknown(string methodName) + public void GetApiDescription_DoesNotPopulatesResponseInformation_WhenUnknown(string methodName) { // Arrange var action = CreateActionDescriptor(methodName); @@ -223,7 +469,7 @@ public void GetApiDescription_DoesNotPopluatesResponseInformation_WhenUnknown(st [Theory] [InlineData(nameof(ReturnsVoid))] [InlineData(nameof(ReturnsTask))] - public void GetApiDescription_DoesNotPopluatesResponseInformation_WhenVoid(string methodName) + public void GetApiDescription_DoesNotPopulatesResponseInformation_WhenVoid(string methodName) { // Arrange var action = CreateActionDescriptor(methodName); @@ -247,7 +493,7 @@ public void GetApiDescription_DoesNotPopluatesResponseInformation_WhenVoid(strin [InlineData(nameof(ReturnsTask))] [InlineData(nameof(ReturnsTaskOfActionResult))] [InlineData(nameof(ReturnsTaskOfJsonResult))] - public void GetApiDescription_PopluatesResponseInformation_WhenSetByFilter(string methodName) + public void GetApiDescription_PopulatesResponseInformation_WhenSetByFilter(string methodName) { // Arrange var action = CreateActionDescriptor(methodName); @@ -323,7 +569,7 @@ public void GetApiDescription_IncludesResponseFormats_FilteredByType() action.FilterDescriptors.Add(new FilterDescriptor(filter, FilterScope.Action)); var formatters = CreateFormatters(); - + // This will just format Order formatters[0].SupportedTypes.Add(typeof(Order)); @@ -349,13 +595,19 @@ private IReadOnlyList GetApiDescriptions(ActionDescriptor action return GetApiDescriptions(action, CreateFormatters()); } - private IReadOnlyList GetApiDescriptions(ActionDescriptor action, List formatters) + private IReadOnlyList GetApiDescriptions( + ActionDescriptor action, + List formatters) { var context = new ApiDescriptionProviderContext(new ActionDescriptor[] { action }); var formattersProvider = new Mock(MockBehavior.Strict); formattersProvider.Setup(fp => fp.OutputFormatters).Returns(formatters); + var constraintResolver = new Mock(); + constraintResolver.Setup(c => c.ResolveConstraint("int")) + .Returns(new IntRouteConstraint()); + var modelMetadataProvider = new Mock(MockBehavior.Strict); modelMetadataProvider .Setup(mmp => mmp.GetMetadataForType(null, It.IsAny())) @@ -364,7 +616,11 @@ private IReadOnlyList GetApiDescriptions(ActionDescriptor action return new ModelMetadata(modelMetadataProvider.Object, null, accessor, type, null); }); - var provider = new DefaultApiDescriptionProvider(formattersProvider.Object, modelMetadataProvider.Object); + var provider = new DefaultApiDescriptionProvider( + formattersProvider.Object, + constraintResolver.Object, + modelMetadataProvider.Object); + provider.Invoke(context, () => { }); return context.Results; } @@ -396,7 +652,6 @@ private ControllerActionDescriptor CreateActionDescriptor(string methodName = nu methodName ?? "ReturnsObject", BindingFlags.Instance | BindingFlags.NonPublic); - return action; } diff --git a/test/Microsoft.AspNet.Mvc.FunctionalTests/ApiExplorerTest.cs b/test/Microsoft.AspNet.Mvc.FunctionalTests/ApiExplorerTest.cs index cf40035607..0c26d7d42a 100644 --- a/test/Microsoft.AspNet.Mvc.FunctionalTests/ApiExplorerTest.cs +++ b/test/Microsoft.AspNet.Mvc.FunctionalTests/ApiExplorerTest.cs @@ -3,12 +3,12 @@ using System; using System.Collections.Generic; +using System.Net.Http; using System.Threading.Tasks; using Microsoft.AspNet.Builder; using Microsoft.AspNet.TestHost; using Xunit; using Newtonsoft.Json; -using System.Net.Http; namespace Microsoft.AspNet.Mvc.FunctionalTests { @@ -139,6 +139,254 @@ public async Task ApiExplorer_GroupName_SetByAttributeOnAction() Assert.Equal(description.GroupName, "SetOnAction"); } + [Fact] + public async Task ApiExplorer_RouteTemplate_DisplaysFixedRoute() + { + // Arrange + var server = TestServer.Create(_provider, _app); + var client = server.CreateClient(); + + // Act + var response = await client.GetAsync("http://localhost/ApiExplorerRouteAndPathParametersInformation"); + + var body = await response.Content.ReadAsStringAsync(); + var result = JsonConvert.DeserializeObject>(body); + + // Assert + var description = Assert.Single(result); + Assert.Equal(description.RelativePath, "ApiExplorerRouteAndPathParametersInformation"); + } + + [Fact] + public async Task ApiExplorer_RouteTemplate_DisplaysRouteWithParameters() + { + // Arrange + var server = TestServer.Create(_provider, _app); + var client = server.CreateClient(); + + // Act + var response = await client.GetAsync("http://localhost/ApiExplorerRouteAndPathParametersInformation/5"); + + var body = await response.Content.ReadAsStringAsync(); + var result = JsonConvert.DeserializeObject>(body); + + // Assert + var description = Assert.Single(result); + Assert.Equal(description.RelativePath, "ApiExplorerRouteAndPathParametersInformation/{id}"); + + var parameter = Assert.Single(description.ParameterDescriptions); + Assert.Equal("id", parameter.Name); + Assert.False(parameter.IsOptional); + Assert.Equal("Path", parameter.Source); + Assert.Null(parameter.ConstraintType); + } + + [Fact] + public async Task ApiExplorer_RouteTemplate_StripsInlineConstraintsFromThePath() + { + // Arrange + var server = TestServer.Create(_provider, _app); + var client = server.CreateClient(); + var url = "http://localhost/ApiExplorerRouteAndPathParametersInformation/Constraint/5"; + + // Act + var response = await client.GetAsync(url); + + var body = await response.Content.ReadAsStringAsync(); + var result = JsonConvert.DeserializeObject>(body); + + // Assert + var description = Assert.Single(result); + Assert.Equal("ApiExplorerRouteAndPathParametersInformation/Constraint/{integer}", description.RelativePath); + + var parameter = Assert.Single(description.ParameterDescriptions); + Assert.Equal("integer", parameter.Name); + Assert.False(parameter.IsOptional); + Assert.Equal("Path", parameter.Source); + Assert.Equal("IntRouteConstraint", parameter.ConstraintType); + } + + [Fact] + public async Task ApiExplorer_RouteTemplate_StripsCatchAllsFromThePath() + { + // Arrange + var server = TestServer.Create(_provider, _app); + var client = server.CreateClient(); + var url = "http://localhost/ApiExplorerRouteAndPathParametersInformation/CatchAll/5"; + + // Act + var response = await client.GetAsync(url); + + var body = await response.Content.ReadAsStringAsync(); + var result = JsonConvert.DeserializeObject>(body); + + // Assert + var description = Assert.Single(result); + Assert.Equal("ApiExplorerRouteAndPathParametersInformation/CatchAll/{parameter}", description.RelativePath); + + var parameter = Assert.Single(description.ParameterDescriptions); + Assert.Equal("parameter", parameter.Name); + Assert.False(parameter.IsOptional); + Assert.Equal("Path", parameter.Source); + } + + [Fact] + public async Task ApiExplorer_RouteTemplate_StripsCatchAllsWithConstraintsFromThePath() + { + // Arrange + var server = TestServer.Create(_provider, _app); + var client = server.CreateClient(); + var url = "http://localhost/ApiExplorerRouteAndPathParametersInformation/CatchAllAndConstraint/5"; + + // Act + var response = await client.GetAsync(url); + + var body = await response.Content.ReadAsStringAsync(); + var result = JsonConvert.DeserializeObject>(body); + + // Assert + var description = Assert.Single(result); + Assert.Equal( + "ApiExplorerRouteAndPathParametersInformation/CatchAllAndConstraint/{integer}", + description.RelativePath); + + var parameter = Assert.Single(description.ParameterDescriptions); + Assert.Equal("integer", parameter.Name); + Assert.False(parameter.IsOptional); + Assert.Equal("Path", parameter.Source); + Assert.Equal("IntRouteConstraint", parameter.ConstraintType); + } + + [Fact] + public async Task ApiExplorer_RouteTemplateStripsMultipleConstraints_OnTheSamePathSegment() + { + // Arrange + var server = TestServer.Create(_provider, _app); + var client = server.CreateClient(); + + var url = "http://localhost/ApiExplorerRouteAndPathParametersInformation/" + + "MultipleParametersInSegment/12-01-1987"; + + var expectedRelativePath = "ApiExplorerRouteAndPathParametersInformation/" + + "MultipleParametersInSegment/{month}-{day}-{year}"; + + // Act + var response = await client.GetAsync(url); + + var body = await response.Content.ReadAsStringAsync(); + var result = JsonConvert.DeserializeObject>(body); + + // Assert + var description = Assert.Single(result); + Assert.Equal(expectedRelativePath, description.RelativePath); + + var month = Assert.Single(description.ParameterDescriptions, p => p.Name == "month"); + Assert.False(month.IsOptional); + Assert.Equal("Path", month.Source); + Assert.Equal("RangeRouteConstraint", month.ConstraintType); + + var day = Assert.Single(description.ParameterDescriptions, p => p.Name == "day"); + Assert.False(day.IsOptional); + Assert.Equal("Path", day.Source); + Assert.Equal("IntRouteConstraint", day.ConstraintType); + + var year = Assert.Single(description.ParameterDescriptions, p => p.Name == "year"); + Assert.False(year.IsOptional); + Assert.Equal("Path", year.Source); + Assert.Equal("IntRouteConstraint", year.ConstraintType); + } + + [Fact] + public async Task ApiExplorer_RouteTemplateStripsMultipleConstraints_InMultipleSegments() + { + // Arrange + var server = TestServer.Create(_provider, _app); + var client = server.CreateClient(); + var url = "http://localhost/ApiExplorerRouteAndPathParametersInformation/" + + "MultipleParametersInMultipleSegments/12/01/1987"; + + var expectedRelativePath = "ApiExplorerRouteAndPathParametersInformation/" + + "MultipleParametersInMultipleSegments/{month}/{day}/{year}"; + + // Act + var response = await client.GetAsync(url); + + var body = await response.Content.ReadAsStringAsync(); + var result = JsonConvert.DeserializeObject>(body); + + // Assert + var description = Assert.Single(result); + Assert.Equal(expectedRelativePath, description.RelativePath); + + var month = Assert.Single(description.ParameterDescriptions, p => p.Name == "month"); + Assert.False(month.IsOptional); + Assert.Equal("Path", month.Source); + Assert.Equal("RangeRouteConstraint", month.ConstraintType); + + var day = Assert.Single(description.ParameterDescriptions, p => p.Name == "day"); + Assert.False(day.IsOptional); + Assert.Equal("Path", day.Source); + Assert.Equal("IntRouteConstraint", day.ConstraintType); + + var year = Assert.Single(description.ParameterDescriptions, p => p.Name == "year"); + Assert.True(year.IsOptional); + Assert.Equal("Path", year.Source); + Assert.Equal("IntRouteConstraint", year.ConstraintType); + } + + [Fact] + public async Task ApiExplorer_DescribeParameters_FromAllSources() + { + // Arrange + var server = TestServer.Create(_provider, _app); + var client = server.CreateClient(); + var url = "http://localhost/ApiExplorerRouteAndPathParametersInformation/MultipleTypesOfParameters/1/2/3"; + + var expectedRelativePath = "ApiExplorerRouteAndPathParametersInformation/" + + "MultipleTypesOfParameters/{path}/{pathAndQuery}/{pathAndFromBody}"; + + // Act + var response = await client.GetAsync(url); + + var body = await response.Content.ReadAsStringAsync(); + var result = JsonConvert.DeserializeObject>(body); + + // Assert + var description = Assert.Single(result); + Assert.Equal(expectedRelativePath, description.RelativePath); + + var path = Assert.Single(description.ParameterDescriptions, p => p.Name == "path"); + Assert.Equal("Path", path.Source); + + var pathAndQuery = Assert.Single(description.ParameterDescriptions, p => p.Name == "pathAndQuery"); + Assert.Equal("Path", pathAndQuery.Source); + + Assert.Single(description.ParameterDescriptions, p => p.Name == "pathAndFromBody" && p.Source == "Body"); + Assert.Single(description.ParameterDescriptions, p => p.Name == "pathAndFromBody" && p.Source == "Path"); + } + + [Fact] + public async Task ApiExplorer_RouteTemplate_MakesParametersOptional() + { + // Arrange + var server = TestServer.Create(_provider, _app); + var client = server.CreateClient(); + + // Act + var response = await client.GetAsync("http://localhost/ApiExplorerRouteAndPathParametersInformation/Optional/"); + + var body = await response.Content.ReadAsStringAsync(); + var result = JsonConvert.DeserializeObject>(body); + + // Assert + var description = Assert.Single(result); + Assert.Equal("ApiExplorerRouteAndPathParametersInformation/Optional/{id}", description.RelativePath); + + var id = Assert.Single(description.ParameterDescriptions, p => p.Name == "id"); + Assert.True(id.IsOptional); + Assert.Equal("Path", id.Source); + } + [Fact] public async Task ApiExplorer_HttpMethod_All() { @@ -500,6 +748,8 @@ private class ApiExplorerParameterData public string Source { get; set; } public string Type { get; set; } + + public string ConstraintType { get; set; } } // Used to serialize data between client and server diff --git a/test/WebSites/ApiExplorerWebSite/ApiExplorerDataFilter.cs b/test/WebSites/ApiExplorerWebSite/ApiExplorerDataFilter.cs index 49e28d566f..8c23c32e1e 100644 --- a/test/WebSites/ApiExplorerWebSite/ApiExplorerDataFilter.cs +++ b/test/WebSites/ApiExplorerWebSite/ApiExplorerDataFilter.cs @@ -55,7 +55,8 @@ private ApiExplorerData CreateSerializableData(ApiDescription description) IsOptional = parameter.IsOptional, Name = parameter.Name, Source = parameter.Source.ToString(), - Type = parameter.Type.FullName, + Type = parameter?.Type?.FullName, + ConstraintType = parameter?.Constraint?.GetType()?.Name, }; data.ParameterDescriptions.Add(parameterData); @@ -101,6 +102,8 @@ private class ApiExplorerParameterData public string Source { get; set; } public string Type { get; set; } + + public string ConstraintType { get; set; } } // Used to serialize data between client and server diff --git a/test/WebSites/ApiExplorerWebSite/Controllers/ApiExplorerRouteAndPathParametersInformationController.cs b/test/WebSites/ApiExplorerWebSite/Controllers/ApiExplorerRouteAndPathParametersInformationController.cs new file mode 100644 index 0000000000..16036e0b65 --- /dev/null +++ b/test/WebSites/ApiExplorerWebSite/Controllers/ApiExplorerRouteAndPathParametersInformationController.cs @@ -0,0 +1,38 @@ +// 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 ApiExplorer +{ + [Route("ApiExplorerRouteAndPathParametersInformation")] + public class ApiExplorerRouteAndPathParametersInformationController + { + [HttpGet] + public void Get() { } + + [HttpGet("{id}")] + public void Get(int id) { } + + [HttpGet("Optional/{id?}")] + public void GetOptional(int id = 0) { } + + [HttpGet("Constraint/{integer:int}")] + public void GetInteger(int integer) { } + + [HttpGet("CatchAll/{*parameter}")] + public void GetCatchAll(string parameter) { } + + [HttpGet("MultipleParametersInSegment/{month:range(1,12)}-{day:int}-{year:int}")] + public void GetMultipleParametersInSegment(string month, string day, string year) { } + + [HttpGet("MultipleParametersInMultipleSegments/{month:range(1,12)}/{day:int?}/{year:int?}")] + public void GetMultipleParametersInMultipleSegments(string month, string day, string year = "") { } + + [HttpGet("MultipleTypesOfParameters/{path}/{pathAndQuery}/{pathAndFromBody}")] + public void MultipleTypesOfParameters(string query, string pathAndQuery, [FromBody] string pathAndFromBody) { } + + [HttpGet("CatchAllAndConstraint/{*integer:int}")] + public void GetIntegers(string integer) { } + } +} \ No newline at end of file From 39376617cced9197554ed7b09d1484793ef4e245 Mon Sep 17 00:00:00 2001 From: jacalvar Date: Tue, 7 Oct 2014 18:21:59 -0700 Subject: [PATCH 23/39] [Fixes #809] Multiple [Http*] verbs should create multiple actions --- .../DefaultActionDiscoveryConventions.cs | 29 ++++++--- .../DefaultActionDiscoveryConventionsTests.cs | 63 ++++++++++++------- 2 files changed, 62 insertions(+), 30 deletions(-) diff --git a/src/Microsoft.AspNet.Mvc.Core/DefaultActionDiscoveryConventions.cs b/src/Microsoft.AspNet.Mvc.Core/DefaultActionDiscoveryConventions.cs index 9cf4f829d1..a67d1cd8d9 100644 --- a/src/Microsoft.AspNet.Mvc.Core/DefaultActionDiscoveryConventions.cs +++ b/src/Microsoft.AspNet.Mvc.Core/DefaultActionDiscoveryConventions.cs @@ -170,14 +170,29 @@ private static IEnumerable GetHttpConstrainedActions( { var httpMethodProviders = actionAttributes.HttpMethodProviderAttributes; var httpMethods = httpMethodProviders.SelectMany(x => x.HttpMethods).Distinct().ToArray(); - - yield return new ActionInfo() + if (httpMethods.Length > 0) { - HttpMethods = httpMethods, - ActionName = actionName, - Attributes = actionAttributes.Attributes, - RequireActionNameMatch = true, - }; + foreach (var httpMethod in httpMethods) + { + yield return new ActionInfo() + { + HttpMethods = new string[] { httpMethod }, + ActionName = actionName, + Attributes = actionAttributes.Attributes, + RequireActionNameMatch = true, + }; + } + } + else + { + yield return new ActionInfo() + { + HttpMethods = httpMethods, + ActionName = actionName, + Attributes = actionAttributes.Attributes, + RequireActionNameMatch = true, + }; + } } private static IEnumerable GetAttributeRoutedActions( diff --git a/test/Microsoft.AspNet.Mvc.Core.Test/DefaultActionDiscoveryConventionsTests.cs b/test/Microsoft.AspNet.Mvc.Core.Test/DefaultActionDiscoveryConventionsTests.cs index 0d56132772..0fdc895aac 100644 --- a/test/Microsoft.AspNet.Mvc.Core.Test/DefaultActionDiscoveryConventionsTests.cs +++ b/test/Microsoft.AspNet.Mvc.Core.Test/DefaultActionDiscoveryConventionsTests.cs @@ -181,12 +181,17 @@ public void GetActions_ConventionallyRoutedAction_WithHttpConstraints() var actionInfos = conventions.GetActions(typeInfo.GetMethod(actionName), typeInfo); // Assert - var action = Assert.Single(actionInfos); - Assert.Equal("Update", action.ActionName); - Assert.True(action.RequireActionNameMatch); - Assert.Equal(new[] { "PUT", "PATCH" }, action.HttpMethods); - Assert.Null(action.AttributeRoute); - Assert.IsType(Assert.Single(action.Attributes)); + Assert.Equal(2, actionInfos.Count()); + Assert.Single(actionInfos, a => a.HttpMethods.Contains("PUT")); + Assert.Single(actionInfos, a => a.HttpMethods.Contains("PATCH")); + + foreach (var action in actionInfos) + { + Assert.Equal("Update", action.ActionName); + Assert.True(action.RequireActionNameMatch); + Assert.Null(action.AttributeRoute); + Assert.IsType(Assert.Single(action.Attributes)); + } } [Fact] @@ -224,15 +229,21 @@ public void GetActions_ConventionallyRoutedAction_WithMultipleHttpConstraints() var actionInfos = conventions.GetActions(typeInfo.GetMethod(actionName), typeInfo); // Assert - var action = Assert.Single(actionInfos); - Assert.Equal("Details", action.ActionName); - Assert.True(action.RequireActionNameMatch); - Assert.Equal(new[] { "GET", "POST" }, action.HttpMethods.OrderBy(m => m, StringComparer.Ordinal)); - Assert.Null(action.AttributeRoute); + Assert.Equal(2, actionInfos.Count()); + Assert.Single(actionInfos, a => a.HttpMethods.Contains("GET")); + Assert.Single(actionInfos, a => a.HttpMethods.Contains("POST")); - Assert.Equal(2, action.Attributes.Length); - Assert.Single(action.Attributes, a => a is HttpGetAttribute); - Assert.Single(action.Attributes, a => a is HttpPostAttribute); + foreach (var action in actionInfos) + { + + Assert.Equal("Details", action.ActionName); + Assert.True(action.RequireActionNameMatch); + Assert.Null(action.AttributeRoute); + + Assert.Equal(2, action.Attributes.Length); + Assert.Single(action.Attributes, a => a is HttpGetAttribute); + Assert.Single(action.Attributes, a => a is HttpPostAttribute); + } } [Fact] @@ -247,16 +258,22 @@ public void GetActions_ConventionallyRoutedAction_WithMultipleOverlappingHttpCon var actionInfos = conventions.GetActions(typeInfo.GetMethod(actionName), typeInfo); // Assert - var action = Assert.Single(actionInfos); - Assert.Equal("List", action.ActionName); - Assert.True(action.RequireActionNameMatch); - Assert.Equal(new[] { "GET", "POST", "PUT" }, action.HttpMethods.OrderBy(m => m, StringComparer.Ordinal)); - Assert.Null(action.AttributeRoute); + Assert.Equal(3, actionInfos.Count()); + Assert.Single(actionInfos, a => a.HttpMethods.Contains("GET")); + Assert.Single(actionInfos, a => a.HttpMethods.Contains("POST")); + Assert.Single(actionInfos, a => a.HttpMethods.Contains("PUT")); - Assert.Equal(3, action.Attributes.Length); - Assert.Single(action.Attributes, a => a is HttpPutAttribute); - Assert.Single(action.Attributes, a => a is HttpGetAttribute); - Assert.Single(action.Attributes, a => a is AcceptVerbsAttribute); + foreach (var action in actionInfos) + { + Assert.Equal("List", action.ActionName); + Assert.True(action.RequireActionNameMatch); + Assert.Null(action.AttributeRoute); + + Assert.Equal(3, action.Attributes.Length); + Assert.Single(action.Attributes, a => a is HttpPutAttribute); + Assert.Single(action.Attributes, a => a is HttpGetAttribute); + Assert.Single(action.Attributes, a => a is AcceptVerbsAttribute); + } } [Fact] From 4ec6da1ed3e00b50f566ca841e885dbd038c2699 Mon Sep 17 00:00:00 2001 From: Pranav K Date: Fri, 3 Oct 2014 14:24:37 -0700 Subject: [PATCH 24/39] Adding RenderSectionAsync to RazorPage Fixes #845 --- .../Views/Home/FlushPoint.cshtml | 5 +- .../MvcSample.Web/Views/Shared/_Layout.cshtml | 4 +- .../TaskHelper.cs | 17 +- src/Microsoft.AspNet.Mvc.Razor/IRazorPage.cs | 5 +- .../Properties/Resources.Designer.cs | 32 +-- src/Microsoft.AspNet.Mvc.Razor/RazorPage.cs | 95 +++++++-- .../RenderAsyncDelegate.cs | 10 + src/Microsoft.AspNet.Mvc.Razor/Resources.resx | 7 +- .../RazorPageTest.cs | 196 +++++++++++++----- .../RazorViewTest.cs | 51 ++--- .../Views/FlushPoint/PageWithLayout.cshtml | 2 +- .../PageWithPartialsAndViewComponents.cshtml | 4 +- .../Views/Shared/_LayoutWithFlush.cshtml | 2 +- .../Shared/_LayoutWithPartialAndFlush.cshtml | 2 +- 14 files changed, 284 insertions(+), 148 deletions(-) rename src/{Microsoft.AspNet.Mvc.Core/Internal => Microsoft.AspNet.Mvc.Common}/TaskHelper.cs (56%) create mode 100644 src/Microsoft.AspNet.Mvc.Razor/RenderAsyncDelegate.cs diff --git a/samples/MvcSample.Web/Views/Home/FlushPoint.cshtml b/samples/MvcSample.Web/Views/Home/FlushPoint.cshtml index 1f79342528..4e95f0aabf 100644 --- a/samples/MvcSample.Web/Views/Home/FlushPoint.cshtml +++ b/samples/MvcSample.Web/Views/Home/FlushPoint.cshtml @@ -45,10 +45,9 @@ Marketing: Marketing@example.com -@* Remove the Wait() calls once we add support for async sections *@ @{ - FlushAsync().Wait(); - Task.Delay(TimeSpan.FromSeconds(1)).Wait(); + await FlushAsync(); + await Task.Delay(TimeSpan.FromSeconds(1)); }
    diff --git a/samples/MvcSample.Web/Views/Shared/_Layout.cshtml b/samples/MvcSample.Web/Views/Shared/_Layout.cshtml index 767b0b972e..2e843399cb 100644 --- a/samples/MvcSample.Web/Views/Shared/_Layout.cshtml +++ b/samples/MvcSample.Web/Views/Shared/_Layout.cshtml @@ -11,7 +11,7 @@ body { padding-top: 0px; } } - @RenderSection("header", required: false) + @await RenderSectionAsync("header", required: false) - @RenderSection("footer", required: false) + @await RenderSectionAsync("footer", required: false) diff --git a/src/Microsoft.AspNet.Mvc.Core/Internal/TaskHelper.cs b/src/Microsoft.AspNet.Mvc.Common/TaskHelper.cs similarity index 56% rename from src/Microsoft.AspNet.Mvc.Core/Internal/TaskHelper.cs rename to src/Microsoft.AspNet.Mvc.Common/TaskHelper.cs index f12375b07b..b290361363 100644 --- a/src/Microsoft.AspNet.Mvc.Core/Internal/TaskHelper.cs +++ b/src/Microsoft.AspNet.Mvc.Common/TaskHelper.cs @@ -3,12 +3,12 @@ using System.Threading.Tasks; -namespace Microsoft.AspNet.Mvc.Internal +namespace Microsoft.AspNet.Mvc { /// /// Utility methods for dealing with . /// - public static class TaskHelper + internal static class TaskHelper { /// /// Waits for the task to complete and throws the first faulting exception if the task is faulted. @@ -21,5 +21,18 @@ public static void WaitAndThrowIfFaulted(Task task) { task.GetAwaiter().GetResult(); } + + /// + /// Waits for the task to complete and throws the first faulting exception if the task is faulted. + /// It preserves the original stack trace when throwing the exception. + /// + /// + /// Invoking this method is equivalent to calling on the + /// if it is not completed. + /// + public static TVal WaitAndThrowIfFaulted(Task task) + { + return task.GetAwaiter().GetResult(); + } } } \ No newline at end of file diff --git a/src/Microsoft.AspNet.Mvc.Razor/IRazorPage.cs b/src/Microsoft.AspNet.Mvc.Razor/IRazorPage.cs index 4b3bdbd373..db57d73146 100644 --- a/src/Microsoft.AspNet.Mvc.Razor/IRazorPage.cs +++ b/src/Microsoft.AspNet.Mvc.Razor/IRazorPage.cs @@ -22,7 +22,6 @@ public interface IRazorPage /// /// Gets or sets the action invoked to render the body. /// - // TODO: https://github.com/aspnet/Mvc/issues/845 tracks making this async Action RenderBodyDelegate { get; set; } /// @@ -53,12 +52,12 @@ public interface IRazorPage /// /// Gets or sets the sections that can be rendered by this page. /// - Dictionary PreviousSectionWriters { get; set; } + Dictionary PreviousSectionWriters { get; set; } /// /// Gets the sections that are defined by this page. /// - Dictionary SectionWriters { get; } + Dictionary SectionWriters { get; } /// /// Renders the page and writes the output to the . diff --git a/src/Microsoft.AspNet.Mvc.Razor/Properties/Resources.Designer.cs b/src/Microsoft.AspNet.Mvc.Razor/Properties/Resources.Designer.cs index b18e7753ce..4e6e08cd25 100644 --- a/src/Microsoft.AspNet.Mvc.Razor/Properties/Resources.Designer.cs +++ b/src/Microsoft.AspNet.Mvc.Razor/Properties/Resources.Designer.cs @@ -205,17 +205,17 @@ internal static string FormatRazorPage_NullModelMetadata(object p0, object p1) /// /// {0} can only be called from a layout page. /// - internal static string RenderBodyCannotBeCalled + internal static string RazorPage_MethodCannotBeCalled { - get { return GetString("RenderBodyCannotBeCalled"); } + get { return GetString("RazorPage_MethodCannotBeCalled"); } } /// /// {0} can only be called from a layout page. /// - internal static string FormatRenderBodyCannotBeCalled(object p0) + internal static string FormatRazorPage_MethodCannotBeCalled(object p0) { - return string.Format(CultureInfo.CurrentCulture, GetString("RenderBodyCannotBeCalled"), p0); + return string.Format(CultureInfo.CurrentCulture, GetString("RazorPage_MethodCannotBeCalled"), p0); } /// @@ -251,7 +251,7 @@ internal static string FormatSectionAlreadyDefined(object p0) } /// - /// {0} has already been called for the section named '{1}'. + /// The section named '{0}' has already been rendered. /// internal static string SectionAlreadyRendered { @@ -259,11 +259,11 @@ internal static string SectionAlreadyRendered } /// - /// {0} has already been called for the section named '{1}'. + /// The section named '{0}' has already been rendered. /// - internal static string FormatSectionAlreadyRendered(object p0, object p1) + internal static string FormatSectionAlreadyRendered(object p0) { - return string.Format(CultureInfo.CurrentCulture, GetString("SectionAlreadyRendered"), p0, p1); + return string.Format(CultureInfo.CurrentCulture, GetString("SectionAlreadyRendered"), p0); } /// @@ -362,22 +362,6 @@ internal static string FormatViewMustBeContextualized(object p0, object p1) return string.Format(CultureInfo.CurrentCulture, GetString("ViewMustBeContextualized"), p0, p1); } - /// - /// The method '{0}' cannot be invoked by this view. - /// - internal static string View_MethodCannotBeCalled - { - get { return GetString("View_MethodCannotBeCalled"); } - } - - /// - /// The method '{0}' cannot be invoked by this view. - /// - internal static string FormatView_MethodCannotBeCalled(object p0) - { - return string.Format(CultureInfo.CurrentCulture, GetString("View_MethodCannotBeCalled"), p0); - } - private static string GetString(string name, params string[] formatterNames) { var value = _resourceManager.GetString(name); diff --git a/src/Microsoft.AspNet.Mvc.Razor/RazorPage.cs b/src/Microsoft.AspNet.Mvc.Razor/RazorPage.cs index 3b01f1e05a..bc46922d89 100644 --- a/src/Microsoft.AspNet.Mvc.Razor/RazorPage.cs +++ b/src/Microsoft.AspNet.Mvc.Razor/RazorPage.cs @@ -30,7 +30,7 @@ public abstract class RazorPage : IRazorPage public RazorPage() { - SectionWriters = new Dictionary(StringComparer.OrdinalIgnoreCase); + SectionWriters = new Dictionary(StringComparer.OrdinalIgnoreCase); _writerScopes = new Stack(); } @@ -105,10 +105,10 @@ public dynamic ViewBag public bool IsLayoutBeingRendered { get; set; } /// - public Dictionary PreviousSectionWriters { get; set; } + public Dictionary PreviousSectionWriters { get; set; } /// - public Dictionary SectionWriters { get; private set; } + public Dictionary SectionWriters { get; private set; } /// public abstract Task ExecuteAsync(); @@ -215,7 +215,7 @@ public virtual void Write(object value) /// public virtual void WriteTo(TextWriter writer, object value) { - if (value != null) + if (value != null && value != HtmlString.Empty) { var helperResult = value as HelperResult; if (helperResult != null) @@ -415,7 +415,8 @@ protected virtual HelperResult RenderBody() { if (RenderBodyDelegate == null) { - throw new InvalidOperationException(Resources.FormatRenderBodyCannotBeCalled("RenderBody")); + var message = Resources.FormatRazorPage_MethodCannotBeCalled(nameof(RenderBody)); + throw new InvalidOperationException(message); } _renderedBody = true; @@ -424,11 +425,11 @@ protected virtual HelperResult RenderBody() /// /// Creates a named content section in the page that can be invoked in a Layout page using - /// or . + /// or . /// /// The name of the section to create. - /// The to execute when rendering the section. - public void DefineSection(string name, HelperResult section) + /// The to execute when rendering the section. + public void DefineSection(string name, RenderAsyncDelegate section) { if (SectionWriters.ContainsKey(name)) { @@ -439,33 +440,84 @@ public void DefineSection(string name, HelperResult section) public bool IsSectionDefined([NotNull] string name) { - EnsureMethodCanBeInvoked("IsSectionDefined"); + EnsureMethodCanBeInvoked(nameof(IsSectionDefined)); return PreviousSectionWriters.ContainsKey(name); } - public HelperResult RenderSection([NotNull] string name) + /// + /// In layout pages, renders the content of the section named . + /// + /// The name of the section to render. + /// Returns a HtmlString that contains the rendered HTML. + public HtmlString RenderSection([NotNull] string name) { return RenderSection(name, required: true); } - public HelperResult RenderSection([NotNull] string name, bool required) + /// + /// In layout pages, renders the content of the section named . + /// + /// The section to render. + /// Indicates if this section must be rendered. + /// Returns a HtmlString that contains the rendered HTML. + public HtmlString RenderSection([NotNull] string name, bool required) + { + EnsureMethodCanBeInvoked(nameof(RenderSection)); + + var task = RenderSectionAsyncCore(name, required); + return TaskHelper.WaitAndThrowIfFaulted(task); + } + + /// + /// In layout pages, asynchronously renders the content of the section named . + /// + /// The section to render. + /// A that on completion returns a containing + /// the rendered HTML. + public Task RenderSectionAsync([NotNull] string name) + { + return RenderSectionAsync(name, required: true); + } + + /// + /// In layout pages, asynchronously renders the content of the section named . + /// + /// The section to render. + /// A that on completion returns a containing + /// the rendered HTML. + /// if is true and the section + /// was not registered using the @section in the Razor page. + public async Task RenderSectionAsync([NotNull] string name, bool required) + { + EnsureMethodCanBeInvoked(nameof(RenderSectionAsync)); + return await RenderSectionAsyncCore(name, required); + } + + private async Task RenderSectionAsyncCore(string sectionName, bool required) { - EnsureMethodCanBeInvoked("RenderSection"); - if (_renderedSections.Contains(name)) + if (_renderedSections.Contains(sectionName)) { - throw new InvalidOperationException(Resources.FormatSectionAlreadyRendered("RenderSection", name)); + var message = Resources.FormatSectionAlreadyRendered(sectionName); + throw new InvalidOperationException(message); } - HelperResult action; - if (PreviousSectionWriters.TryGetValue(name, out action)) + RenderAsyncDelegate renderDelegate; + if (PreviousSectionWriters.TryGetValue(sectionName, out renderDelegate)) { - _renderedSections.Add(name); - return action; + _renderedSections.Add(sectionName); + + using (var writer = new StringCollectionTextWriter(Output.Encoding)) + { + await renderDelegate(writer); + + // Returning a disposed StringCollectionTextWriter is safe. + return new HtmlString(writer); + } } else if (required) { // If the section is not found, and it is not optional, throw an error. - throw new InvalidOperationException(Resources.FormatSectionNotDefined(name)); + throw new InvalidOperationException(Resources.FormatSectionNotDefined(sectionName)); } else { @@ -518,7 +570,8 @@ public void EnsureBodyAndSectionsWereRendered() if (RenderBodyDelegate != null && !_renderedBody) { // If a body was defined, then RenderBody should have been called. - throw new InvalidOperationException(Resources.FormatRenderBodyNotCalled("RenderBody")); + var message = Resources.FormatRenderBodyNotCalled(nameof(RenderBody)); + throw new InvalidOperationException(message); } } @@ -536,7 +589,7 @@ private void EnsureMethodCanBeInvoked(string methodName) { if (PreviousSectionWriters == null) { - throw new InvalidOperationException(Resources.FormatView_MethodCannotBeCalled(methodName)); + throw new InvalidOperationException(Resources.FormatRazorPage_MethodCannotBeCalled(methodName)); } } } diff --git a/src/Microsoft.AspNet.Mvc.Razor/RenderAsyncDelegate.cs b/src/Microsoft.AspNet.Mvc.Razor/RenderAsyncDelegate.cs new file mode 100644 index 0000000000..1b1ba47fc0 --- /dev/null +++ b/src/Microsoft.AspNet.Mvc.Razor/RenderAsyncDelegate.cs @@ -0,0 +1,10 @@ +// 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.IO; +using System.Threading.Tasks; + +namespace Microsoft.AspNet.Mvc.Razor +{ + public delegate Task RenderAsyncDelegate(TextWriter writer); +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Mvc.Razor/Resources.resx b/src/Microsoft.AspNet.Mvc.Razor/Resources.resx index 2492e79319..cca4dab354 100644 --- a/src/Microsoft.AspNet.Mvc.Razor/Resources.resx +++ b/src/Microsoft.AspNet.Mvc.Razor/Resources.resx @@ -153,7 +153,7 @@ The {0} was unable to provide metadata for expression '{1}'. - + {0} can only be called from a layout page. @@ -163,7 +163,7 @@ Section '{0}' is already defined. - {0} has already been called for the section named '{1}'. + The section named '{0}' has already been rendered. Section '{0}' is not defined. @@ -183,7 +183,4 @@ The '{0}' method must be called before '{1}' can be invoked. - - The method '{0}' cannot be invoked by this view. - \ No newline at end of file diff --git a/test/Microsoft.AspNet.Mvc.Razor.Test/RazorPageTest.cs b/test/Microsoft.AspNet.Mvc.Razor.Test/RazorPageTest.cs index 7fa4921b1c..4ef8994fef 100644 --- a/test/Microsoft.AspNet.Mvc.Razor.Test/RazorPageTest.cs +++ b/test/Microsoft.AspNet.Mvc.Razor.Test/RazorPageTest.cs @@ -17,6 +17,8 @@ namespace Microsoft.AspNet.Mvc.Razor { public class RazorPageTest { + private readonly RenderAsyncDelegate _nullRenderAsyncDelegate = async writer => { }; + [Fact] public async Task WritingScopesRedirectContentWrittenToViewContextWriter() { @@ -135,8 +137,8 @@ public async Task DefineSection_ThrowsIfSectionIsAlreadyDefined() var viewContext = CreateViewContext(); var page = CreatePage(v => { - v.DefineSection("qux", new HelperResult(action: null)); - v.DefineSection("qux", new HelperResult(action: null)); + v.DefineSection("qux", _nullRenderAsyncDelegate); + v.DefineSection("qux", _nullRenderAsyncDelegate); }); // Act @@ -151,23 +153,22 @@ public async Task DefineSection_ThrowsIfSectionIsAlreadyDefined() public async Task RenderSection_RendersSectionFromPreviousPage() { // Arrange - var expected = new HelperResult(action: null); + var expected = "Hello world"; var viewContext = CreateViewContext(); - HelperResult actual = null; var page = CreatePage(v => { - actual = v.RenderSection("bar"); + v.Write(v.RenderSection("bar")); }); - page.PreviousSectionWriters = new Dictionary + page.PreviousSectionWriters = new Dictionary { - { "bar", expected } + { "bar", writer => writer.WriteAsync(expected) } }; // Act await page.ExecuteAsync(); // Assert - Assert.Same(actual, expected); + Assert.Equal(expected, page.RenderedContent); } [Fact] @@ -184,7 +185,7 @@ public async Task RenderSection_ThrowsIfPreviousSectionWritersIsNotSet() await page.ExecuteAsync(); // Assert - Assert.Equal("The method 'RenderSection' cannot be invoked by this view.", + Assert.Equal("RenderSection can only be called from a layout page.", ex.Message); } @@ -192,14 +193,13 @@ public async Task RenderSection_ThrowsIfPreviousSectionWritersIsNotSet() public async Task RenderSection_ThrowsIfRequiredSectionIsNotFound() { // Arrange - var expected = new HelperResult(action: null); var page = CreatePage(v => { v.RenderSection("bar"); }); - page.PreviousSectionWriters = new Dictionary + page.PreviousSectionWriters = new Dictionary { - { "baz", expected } + { "baz", _nullRenderAsyncDelegate } }; // Act @@ -217,7 +217,7 @@ public void IsSectionDefined_ThrowsIfPreviousSectionWritersIsNotRegistered() // Act and Assert ExceptionAssert.Throws(() => page.IsSectionDefined("foo"), - "The method 'IsSectionDefined' cannot be invoked by this view."); + "IsSectionDefined can only be called from a layout page."); } [Fact] @@ -231,9 +231,9 @@ public async Task IsSectionDefined_ReturnsFalseIfSectionNotDefined() v.RenderSection("baz"); v.RenderBodyPublic(); }); - page.PreviousSectionWriters = new Dictionary + page.PreviousSectionWriters = new Dictionary { - { "baz", new HelperResult(writer => { }) } + { "baz", _nullRenderAsyncDelegate } }; page.RenderBodyDelegate = CreateBodyAction("body-content"); @@ -255,9 +255,9 @@ public async Task IsSectionDefined_ReturnsTrueIfSectionDefined() v.RenderSection("baz"); v.RenderBodyPublic(); }); - page.PreviousSectionWriters = new Dictionary + page.PreviousSectionWriters = new Dictionary { - { "baz", new HelperResult(writer => { }) } + { "baz", _nullRenderAsyncDelegate } }; page.RenderBodyDelegate = CreateBodyAction("body-content"); @@ -278,32 +278,92 @@ public async Task RenderSection_ThrowsIfSectionIsRenderedMoreThanOnce() v.RenderSection("header"); v.RenderSection("header"); }); - page.PreviousSectionWriters = new Dictionary + page.PreviousSectionWriters = new Dictionary { - { "header", new HelperResult(writer => { }) } + { "header", _nullRenderAsyncDelegate } }; // Act var ex = await Assert.ThrowsAsync(page.ExecuteAsync); // Assert - Assert.Equal("RenderSection has already been called for the section named 'header'.", ex.Message); + Assert.Equal("The section named 'header' has already been rendered.", ex.Message); } [Fact] - public async Task EnsureBodyAndSectionsWereRendered_ThrowsIfDefinedSectionIsNotRendered() + public async Task RenderSectionAsync_ThrowsIfSectionIsRenderedMoreThanOnce() + { + // Arrange + var expected = new HelperResult(action: null); + var page = CreatePage(async v => + { + await v.RenderSectionAsync("header"); + await v.RenderSectionAsync("header"); + }); + page.PreviousSectionWriters = new Dictionary + { + { "header", _nullRenderAsyncDelegate } + }; + + // Act + var ex = await Assert.ThrowsAsync(page.ExecuteAsync); + + // Assert + Assert.Equal("The section named 'header' has already been rendered.", ex.Message); + } + + [Fact] + public async Task RenderSectionAsync_ThrowsIfSectionIsRenderedMoreThanOnce_WithSyncMethod() + { + // Arrange + var expected = new HelperResult(action: null); + var page = CreatePage(async v => + { + v.RenderSection("header"); + await v.RenderSectionAsync("header"); + }); + page.PreviousSectionWriters = new Dictionary + { + { "header", _nullRenderAsyncDelegate } + }; + + // Act + var ex = await Assert.ThrowsAsync(page.ExecuteAsync); + + // Assert + Assert.Equal("The section named 'header' has already been rendered.", ex.Message); + } + + [Fact] + public async Task RenderSectionAsync_ThrowsIfNotInvokedFromLayoutPage() { // Arrange var expected = new HelperResult(action: null); + var page = CreatePage(async v => + { + await v.RenderSectionAsync("header"); + }); + + // Act + var ex = await Assert.ThrowsAsync(page.ExecuteAsync); + + // Assert + Assert.Equal("RenderSectionAsync can only be called from a layout page.", ex.Message); + } + + [Fact] + public async Task EnsureBodyAndSectionsWereRendered_ThrowsIfDefinedSectionIsNotRendered() + { + // Arrange var page = CreatePage(v => { v.RenderSection("sectionA"); }); - page.PreviousSectionWriters = new Dictionary + page.PreviousSectionWriters = new Dictionary { - { "header", expected }, - { "footer", expected }, - { "sectionA", expected }, + { "header", _nullRenderAsyncDelegate }, + { "footer", _nullRenderAsyncDelegate }, + { "sectionA", _nullRenderAsyncDelegate }, }; // Act @@ -337,35 +397,38 @@ public async Task EnsureBodyAndSectionsWereRendered_ThrowsIfRenderBodyIsNotCalle public async Task ExecuteAsync_RendersSectionsAndBody() { // Arrange - var expected = @"Layout start -Header section -body content -Footer section -Layout end -"; - var page = CreatePage(v => + var expected = string.Join(Environment.NewLine, + "Layout start", + "Header section", + "Async Header section", + "body content", + "Async Footer section", + "Footer section", + "Layout end"); + var page = CreatePage(async v => { v.WriteLiteral("Layout start" + Environment.NewLine); v.Write(v.RenderSection("header")); + v.Write(await v.RenderSectionAsync("async-header")); v.Write(v.RenderBodyPublic()); + v.Write(await v.RenderSectionAsync("async-footer")); v.Write(v.RenderSection("footer")); - v.WriteLiteral("Layout end" + Environment.NewLine); - + v.WriteLiteral("Layout end"); }); page.RenderBodyDelegate = CreateBodyAction("body content" + Environment.NewLine); - page.PreviousSectionWriters = new Dictionary + page.PreviousSectionWriters = new Dictionary { { - "footer", new HelperResult(writer => - { - writer.WriteLine("Footer section"); - }) + "footer", writer => writer.WriteLineAsync("Footer section") }, { - "header", new HelperResult(writer => - { - writer.WriteLine("Header section"); - }) + "header", writer => writer.WriteLineAsync("Header section") + }, + { + "async-header", writer => writer.WriteLineAsync("Async Header section") + }, + { + "async-footer", writer => writer.WriteLineAsync("Async Footer section") }, }; @@ -373,7 +436,7 @@ Layout end await page.ExecuteAsync(); // Assert - var actual = ((StringWriter)page.Output).ToString(); + var actual = page.RenderedContent; Assert.Equal(expected, actual); } @@ -399,7 +462,7 @@ public async Task Href_ReadsUrlHelperFromServiceCollection() await page.ExecuteAsync(); // Assert - var actual = ((StringWriter)page.Output).ToString(); + var actual = page.RenderedContent; Assert.Equal(expected, actual); helper.Verify(); } @@ -410,9 +473,9 @@ public async Task FlushAsync_InvokesFlushOnWriter() // Arrange var writer = new Mock(); var context = CreateViewContext(writer.Object); - var page = CreatePage(p => + var page = CreatePage(async p => { - p.FlushAsync().Wait(); + await p.FlushAsync(); }, context); // Act @@ -429,10 +492,10 @@ public async Task FlushAsync_ThrowsIfTheLayoutHasBeenSet() var expected = @"A layout page cannot be rendered after 'FlushAsync' has been invoked."; var writer = new Mock(); var context = CreateViewContext(writer.Object); - var page = CreatePage(p => + var page = CreatePage(async p => { p.Layout = "foo"; - p.FlushAsync().Wait(); + await p.FlushAsync(); }, context); // Act and Assert @@ -449,10 +512,10 @@ public async Task FlushAsync_DoesNotThrowWhenIsRenderingLayoutIsSet() var page = CreatePage(p => { p.Layout = "bar"; - p.DefineSection("test-section", new HelperResult(_ => + p.DefineSection("test-section", async _ => { - p.FlushAsync().Wait(); - })); + await p.FlushAsync(); + }); }, context); // Act @@ -460,7 +523,8 @@ public async Task FlushAsync_DoesNotThrowWhenIsRenderingLayoutIsSet() page.IsLayoutBeingRendered = true; // Assert - Assert.DoesNotThrow(() => page.SectionWriters["test-section"].WriteTo(TextWriter.Null)); + var renderAsyncDelegate = page.SectionWriters["test-section"]; + await Assert.DoesNotThrowAsync(() => renderAsyncDelegate(TextWriter.Null)); } [Fact] @@ -553,14 +617,27 @@ public async Task Write_WithHtmlString_WritesValueWithoutEncoding() private static TestableRazorPage CreatePage(Action executeAction, ViewContext context = null) + { + return CreatePage(page => + { + executeAction(page); + return Task.FromResult(0); + }, context); + } + + + private static TestableRazorPage CreatePage(Func executeAction, + ViewContext context = null) { context = context ?? CreateViewContext(); var view = new Mock { CallBase = true }; if (executeAction != null) { view.Setup(v => v.ExecuteAsync()) - .Callback(() => executeAction(view.Object)) - .Returns(Task.FromResult(0)); + .Returns(() => + { + return executeAction(view.Object); + }); } view.Object.ViewContext = context; @@ -585,6 +662,15 @@ private static Action CreateBodyAction(string value) public abstract class TestableRazorPage : RazorPage { + public string RenderedContent + { + get + { + var writer = Assert.IsType(Output); + return writer.ToString(); + } + } + public HelperResult RenderBodyPublic() { return base.RenderBody(); diff --git a/test/Microsoft.AspNet.Mvc.Razor.Test/RazorViewTest.cs b/test/Microsoft.AspNet.Mvc.Razor.Test/RazorViewTest.cs index 5af5c8c47e..018e69444e 100644 --- a/test/Microsoft.AspNet.Mvc.Razor.Test/RazorViewTest.cs +++ b/test/Microsoft.AspNet.Mvc.Razor.Test/RazorViewTest.cs @@ -16,6 +16,7 @@ namespace Microsoft.AspNet.Mvc.Razor public class RazorViewTest { private const string LayoutPath = "~/Shared/_Layout.cshtml"; + private readonly RenderAsyncDelegate _nullRenderAsyncDelegate = async writer => { }; [Fact] public async Task RenderAsync_ThrowsIfContextualizeHasNotBeenInvoked() @@ -264,14 +265,14 @@ public async Task RenderAsync_ExecutesLayoutPages() { v.WriteLiteral("body-content"); v.Layout = LayoutPath; - v.DefineSection("head", new HelperResult(writer => + v.DefineSection("head", async writer => { - writer.Write("head-content"); - })); - v.DefineSection("foot", new HelperResult(writer => + await writer.WriteAsync("head-content"); + }); + v.DefineSection("foot", async writer => { - writer.Write("foot-content"); - })); + await writer.WriteAsync("foot-content"); + }); }); var layout = new TestableRazorPage(v => { @@ -312,9 +313,9 @@ public async Task RenderAsync_ThrowsIfSectionsWereDefinedButNotRendered() // Arrange var page = new TestableRazorPage(v => { - v.DefineSection("head", new HelperResult(writer => { })); + v.DefineSection("head", _nullRenderAsyncDelegate); v.Layout = LayoutPath; - v.DefineSection("foot", new HelperResult(writer => { })); + v.DefineSection("foot", _nullRenderAsyncDelegate); }); var layout = new TestableRazorPage(v => { @@ -374,10 +375,10 @@ public async Task RenderAsync_ExecutesNestedLayoutPages() var page = new TestableRazorPage(v => { - v.DefineSection("foo", new HelperResult(writer => + v.DefineSection("foo", async writer => { - writer.WriteLine("foo-content"); - })); + await writer.WriteLineAsync("foo-content"); + }); v.Layout = "~/Shared/Layout1.cshtml"; v.WriteLiteral("body-content"); }); @@ -385,10 +386,7 @@ public async Task RenderAsync_ExecutesNestedLayoutPages() { v.Write("layout-1" + Environment.NewLine); v.Write(v.RenderSection("foo")); - v.DefineSection("bar", new HelperResult(writer => - { - writer.WriteLine("bar-content"); - })); + v.DefineSection("bar", writer => writer.WriteLineAsync("bar-content")); v.RenderBodyPublic(); v.Layout = "~/Shared/Layout2.cshtml"; }); @@ -431,12 +429,12 @@ body content { v.Layout = "layout-1"; v.WriteLiteral("body content" + Environment.NewLine); - v.DefineSection("foo", new HelperResult(_ => + v.DefineSection("foo", async _ => { v.WriteLiteral("section-content-1" + Environment.NewLine); - v.FlushAsync().Wait(); + await v.FlushAsync(); v.WriteLiteral("section-content-2"); - })); + }); }); var layout1 = new TestableRazorPage(v => @@ -475,12 +473,12 @@ public async Task FlushAsync_DoesNotThrowWhenInvokedInsideOfASection() var page = new TestableRazorPage(v => { v.Layout = "layout-1"; - v.DefineSection("foo", new HelperResult(_ => + v.DefineSection("foo", async _ => { v.WriteLiteral("section-content-1" + Environment.NewLine); - v.FlushAsync().Wait(); + await v.FlushAsync(); v.WriteLiteral("section-content-2"); - })); + }); }); var layout1 = new TestableRazorPage(v => @@ -538,11 +536,11 @@ public async Task RenderAsync_ThrowsIfFlushWasInvokedInsideRenderedSectionAndLay var expected = @"A layout page cannot be rendered after 'FlushAsync' has been invoked."; var page = new TestableRazorPage(v => { - v.DefineSection("foo", new HelperResult(writer => + v.DefineSection("foo", async writer => { writer.WriteLine("foo-content"); - v.FlushAsync().Wait(); - })); + await v.FlushAsync(); + }); v.Layout = "~/Shared/Layout1.cshtml"; v.WriteLiteral("body-content"); }); @@ -550,10 +548,7 @@ public async Task RenderAsync_ThrowsIfFlushWasInvokedInsideRenderedSectionAndLay { v.Write("layout-1" + Environment.NewLine); v.Write(v.RenderSection("foo")); - v.DefineSection("bar", new HelperResult(writer => - { - writer.WriteLine("bar-content"); - })); + v.DefineSection("bar", writer => writer.WriteLineAsync("bar-content")); v.RenderBodyPublic(); v.Layout = "~/Shared/Layout2.cshtml"; }); diff --git a/test/WebSites/RazorWebSite/Views/FlushPoint/PageWithLayout.cshtml b/test/WebSites/RazorWebSite/Views/FlushPoint/PageWithLayout.cshtml index b038ad9f42..a0710f2a89 100644 --- a/test/WebSites/RazorWebSite/Views/FlushPoint/PageWithLayout.cshtml +++ b/test/WebSites/RazorWebSite/Views/FlushPoint/PageWithLayout.cshtml @@ -7,7 +7,7 @@ RenderBody content @section content { @{ - FlushAsync().Wait(); + await FlushAsync(); WaitService.WaitForClient(); } Content that takes time to produce diff --git a/test/WebSites/RazorWebSite/Views/FlushPoint/PageWithPartialsAndViewComponents.cshtml b/test/WebSites/RazorWebSite/Views/FlushPoint/PageWithPartialsAndViewComponents.cshtml index b8f092fcd5..d83f5a82d2 100644 --- a/test/WebSites/RazorWebSite/Views/FlushPoint/PageWithPartialsAndViewComponents.cshtml +++ b/test/WebSites/RazorWebSite/Views/FlushPoint/PageWithPartialsAndViewComponents.cshtml @@ -7,9 +7,9 @@ RenderBody content @section content { @{ - FlushAsync().Wait(); + await FlushAsync(); WaitService.WaitForClient(); } - @Component.InvokeAsync("ComponentThatSetsTitle").Result + @await Component.InvokeAsync("ComponentThatSetsTitle") Content that takes time to produce } \ No newline at end of file diff --git a/test/WebSites/RazorWebSite/Views/Shared/_LayoutWithFlush.cshtml b/test/WebSites/RazorWebSite/Views/Shared/_LayoutWithFlush.cshtml index b6de9fc0cf..cb3d3ba7e1 100644 --- a/test/WebSites/RazorWebSite/Views/Shared/_LayoutWithFlush.cshtml +++ b/test/WebSites/RazorWebSite/Views/Shared/_LayoutWithFlush.cshtml @@ -5,7 +5,7 @@ WaitService.WaitForClient(); } @RenderBody() -@RenderSection("content") +@await RenderSectionAsync("content") @{ WaitService.NotifyClient(); } diff --git a/test/WebSites/RazorWebSite/Views/Shared/_LayoutWithPartialAndFlush.cshtml b/test/WebSites/RazorWebSite/Views/Shared/_LayoutWithPartialAndFlush.cshtml index d425369da1..090234c40f 100644 --- a/test/WebSites/RazorWebSite/Views/Shared/_LayoutWithPartialAndFlush.cshtml +++ b/test/WebSites/RazorWebSite/Views/Shared/_LayoutWithPartialAndFlush.cshtml @@ -6,7 +6,7 @@ WaitService.WaitForClient(); } @await Html.PartialAsync("_PartialThatSetsTitle") -@RenderSection("content") +@await RenderSectionAsync("content") @{ WaitService.NotifyClient(); } From dd587f743b36009690cb24ba6d578e2e92a64b87 Mon Sep 17 00:00:00 2001 From: Kiran Challa Date: Fri, 10 Oct 2014 17:54:20 -0700 Subject: [PATCH 25/39] [Fixes #1331] Dispose HttpResponseMessage once its written out in HttpResponseMessageOutputFormatter --- .../HttpResponseMessageOutputFormatter.cs | 35 +++++++------- ...HttpResponseMessageOutputFormatterTests.cs | 46 +++++++++++++++++++ 2 files changed, 65 insertions(+), 16 deletions(-) create mode 100644 test/Microsoft.AspNet.Mvc.WebApiCompatShimTest/HttpResponseMessageOutputFormatterTests.cs diff --git a/src/Microsoft.AspNet.Mvc.WebApiCompatShim/Formatters/HttpResponseMessageOutputFormatter.cs b/src/Microsoft.AspNet.Mvc.WebApiCompatShim/Formatters/HttpResponseMessageOutputFormatter.cs index 3dd743b842..0816da323b 100644 --- a/src/Microsoft.AspNet.Mvc.WebApiCompatShim/Formatters/HttpResponseMessageOutputFormatter.cs +++ b/src/Microsoft.AspNet.Mvc.WebApiCompatShim/Formatters/HttpResponseMessageOutputFormatter.cs @@ -40,29 +40,32 @@ public async Task WriteAsync(OutputFormatterContext context) throw new InvalidOperationException(message); } - response.StatusCode = (int)responseMessage.StatusCode; - - var responseFeature = context.ActionContext.HttpContext.GetFeature(); - if (responseFeature != null) + using (responseMessage) { - responseFeature.ReasonPhrase = responseMessage.ReasonPhrase; - } + response.StatusCode = (int)responseMessage.StatusCode; - var responseHeaders = responseMessage.Headers; - foreach (var header in responseHeaders) - { - response.Headers.AppendValues(header.Key, header.Value.ToArray()); - } + var responseFeature = context.ActionContext.HttpContext.GetFeature(); + if (responseFeature != null) + { + responseFeature.ReasonPhrase = responseMessage.ReasonPhrase; + } - if (responseMessage.Content != null) - { - var contentHeaders = responseMessage.Content.Headers; - foreach (var header in contentHeaders) + var responseHeaders = responseMessage.Headers; + foreach (var header in responseHeaders) { response.Headers.AppendValues(header.Key, header.Value.ToArray()); } - await responseMessage.Content.CopyToAsync(response.Body); + if (responseMessage.Content != null) + { + var contentHeaders = responseMessage.Content.Headers; + foreach (var header in contentHeaders) + { + response.Headers.AppendValues(header.Key, header.Value.ToArray()); + } + + await responseMessage.Content.CopyToAsync(response.Body); + } } } } diff --git a/test/Microsoft.AspNet.Mvc.WebApiCompatShimTest/HttpResponseMessageOutputFormatterTests.cs b/test/Microsoft.AspNet.Mvc.WebApiCompatShimTest/HttpResponseMessageOutputFormatterTests.cs new file mode 100644 index 0000000000..4c241ebf14 --- /dev/null +++ b/test/Microsoft.AspNet.Mvc.WebApiCompatShimTest/HttpResponseMessageOutputFormatterTests.cs @@ -0,0 +1,46 @@ +// 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.IO; +using System.Net.Http; +using System.Threading.Tasks; +using Microsoft.AspNet.Mvc.WebApiCompatShim; +using Microsoft.AspNet.PipelineCore; +using Moq; +using Moq.Protected; +using Xunit; + +namespace Microsoft.AspNet.Mvc.WebApiCompatShimTest +{ + public class HttpResponseMessageOutputFormatterTests + { + [Fact] + public async Task Disposed_CalledOn_HttpResponseMessage() + { + // Arrange + var formatter = new HttpResponseMessageOutputFormatter(); + var streamContent = new Mock(new MemoryStream()); + streamContent.Protected().Setup("Dispose", true).Verifiable(); + var httpResponseMessage = new HttpResponseMessage(); + httpResponseMessage.Content = streamContent.Object; + var outputFormatterContext = GetOutputFormatterContext(httpResponseMessage, typeof(HttpResponseMessage)); + + // Act + await formatter.WriteAsync(outputFormatterContext); + + // Assert + streamContent.Protected().Verify("Dispose", Times.Once(), true); + } + + private OutputFormatterContext GetOutputFormatterContext(object outputValue, Type outputType) + { + return new OutputFormatterContext + { + Object = outputValue, + DeclaredType = outputType, + ActionContext = new ActionContext(new DefaultHttpContext(), routeData: null, actionDescriptor: null) + }; + } + } +} \ No newline at end of file From 3f29de5a5f11aa19de2c773fb1b78950605fab29 Mon Sep 17 00:00:00 2001 From: Pranav K Date: Tue, 14 Oct 2014 06:12:28 -0700 Subject: [PATCH 26/39] Ensure PDB writer component is available before emiting PDB Ported from https://github.com/aspnet/KRuntime/commit/36cd4901f6b13d135b82dd026768a37ee36bdedb Fixes #1324 --- .../Compilation/RoslynCompilationService.cs | 36 +++++++++++++++---- 1 file changed, 29 insertions(+), 7 deletions(-) diff --git a/src/Microsoft.AspNet.Mvc.Razor/Compilation/RoslynCompilationService.cs b/src/Microsoft.AspNet.Mvc.Razor/Compilation/RoslynCompilationService.cs index e9dfea547b..9d2a2962ee 100644 --- a/src/Microsoft.AspNet.Mvc.Razor/Compilation/RoslynCompilationService.cs +++ b/src/Microsoft.AspNet.Mvc.Razor/Compilation/RoslynCompilationService.cs @@ -7,6 +7,7 @@ using System.IO; using System.Linq; using System.Reflection; +using System.Runtime.InteropServices; using Microsoft.AspNet.FileSystems; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; @@ -20,6 +21,7 @@ namespace Microsoft.AspNet.Mvc.Razor.Compilation /// public class RoslynCompilationService : ICompilationService { + private readonly Lazy _supportsPdbGeneration = new Lazy(SupportsPdbGeneration); private readonly ConcurrentDictionary _metadataFileCache = new ConcurrentDictionary(StringComparer.OrdinalIgnoreCase); @@ -70,13 +72,13 @@ public CompilationResult Compile(IFileInfo fileInfo, string compilationContent) { EmitResult result; - if (PlatformHelper.IsMono) + if (_supportsPdbGeneration.Value) { - result = compilation.Emit(ms, pdbStream: null); + result = compilation.Emit(ms, pdbStream: pdb); } else { - result = compilation.Emit(ms, pdbStream: pdb); + result = compilation.Emit(ms); } if (!result.Success) @@ -94,14 +96,14 @@ public CompilationResult Compile(IFileInfo fileInfo, string compilationContent) Assembly assembly; ms.Seek(0, SeekOrigin.Begin); - if (PlatformHelper.IsMono) + if (_supportsPdbGeneration.Value) { - assembly = _loader.LoadStream(ms, pdbStream: null); + pdb.Seek(0, SeekOrigin.Begin); + assembly = _loader.LoadStream(ms, pdb); } else { - pdb.Seek(0, SeekOrigin.Begin); - assembly = _loader.LoadStream(ms, pdb); + assembly = _loader.LoadStream(ms, pdbStream: null); } var type = assembly.GetExportedTypes() @@ -187,5 +189,25 @@ private static bool IsError(Diagnostic diagnostic) { return diagnostic.IsWarningAsError || diagnostic.Severity == DiagnosticSeverity.Error; } + + private static bool SupportsPdbGeneration() + { + try + { + if (PlatformHelper.IsMono) + { + return false; + } + + // Check for the pdb writer component that roslyn uses to generate pdbs + const string SymWriterGuid = "0AE2DEB0-F901-478b-BB9F-881EE8066788"; + + return Marshal.GetTypeFromCLSID(new Guid(SymWriterGuid)) != null; + } + catch + { + return false; + } + } } } From 0f5bbdf41701e2396d2ca12808874c5b9afb1529 Mon Sep 17 00:00:00 2001 From: SonjaKhan Date: Wed, 8 Oct 2014 12:15:39 -0700 Subject: [PATCH 27/39] updating ILogger, see aspnet/Logging#3 --- Mvc.sln | 2 +- .../Logging/LogFormatter.cs | 2 +- .../Logging/LoggerExtensions.cs | 4 +-- .../DefaultActionSelectorTests.cs | 30 ++++--------------- .../Logging/NullLogger.cs | 6 +++- .../Logging/TestLogger.cs | 7 +++-- .../Logging/TestSink.cs | 12 ++++---- .../{WriteCoreContext.cs => WriteContext.cs} | 2 +- .../MvcRouteHandlerTests.cs | 30 ++++--------------- .../Routing/AttributeRouteTests.cs | 20 +++---------- .../NullLoggerFactory.cs | 6 +++- 11 files changed, 42 insertions(+), 79 deletions(-) rename test/Microsoft.AspNet.Mvc.Core.Test/Logging/{WriteCoreContext.cs => WriteContext.cs} (94%) diff --git a/Mvc.sln b/Mvc.sln index 7750fe6354..9fc74b2893 100644 --- a/Mvc.sln +++ b/Mvc.sln @@ -1,7 +1,7 @@  Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio 14 -VisualStudioVersion = 14.0.22013.1 +VisualStudioVersion = 14.0.22115.0 MinimumVisualStudioVersion = 10.0.40219.1 Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "samples", "samples", "{DAAE4C74-D06F-4874-A166-33305D2643CE}" EndProject diff --git a/src/Microsoft.AspNet.Mvc.Core/Logging/LogFormatter.cs b/src/Microsoft.AspNet.Mvc.Core/Logging/LogFormatter.cs index ac280abfb7..45fee7a5fe 100644 --- a/src/Microsoft.AspNet.Mvc.Core/Logging/LogFormatter.cs +++ b/src/Microsoft.AspNet.Mvc.Core/Logging/LogFormatter.cs @@ -8,7 +8,7 @@ namespace Microsoft.AspNet.Mvc.Logging public static class LogFormatter { /// - /// A formatter for use with (write.State); @@ -96,15 +90,9 @@ public async void SelectAsync_MatchedActions_LogIsCorrect() Assert.Equal(typeof(DefaultActionSelector).FullName, scope.LoggerName); Assert.Equal("DefaultActionSelector.SelectAsync", scope.Scope); - // There is a record for IsEnabled and one for WriteCore. - Assert.Equal(2, sink.Writes.Count); - - var enabled = sink.Writes[0]; - Assert.Equal(typeof(DefaultActionSelector).FullName, enabled.LoggerName); - Assert.Equal("DefaultActionSelector.SelectAsync", enabled.Scope); - Assert.Null(enabled.State); + Assert.Equal(1, sink.Writes.Count); - var write = sink.Writes[1]; + var write = sink.Writes[0]; Assert.Equal(typeof(DefaultActionSelector).FullName, write.LoggerName); Assert.Equal("DefaultActionSelector.SelectAsync", write.Scope); var values = Assert.IsType(write.State); @@ -144,15 +132,9 @@ await Assert.ThrowsAsync(async () => Assert.Equal(typeof(DefaultActionSelector).FullName, scope.LoggerName); Assert.Equal("DefaultActionSelector.SelectAsync", scope.Scope); - // There is a record for IsEnabled and one for WriteCore. - Assert.Equal(2, sink.Writes.Count); - - var enabled = sink.Writes[0]; - Assert.Equal(typeof(DefaultActionSelector).FullName, enabled.LoggerName); - Assert.Equal("DefaultActionSelector.SelectAsync", enabled.Scope); - Assert.Null(enabled.State); + Assert.Equal(1, sink.Writes.Count); - var write = sink.Writes[1]; + var write = sink.Writes[0]; Assert.Equal(typeof(DefaultActionSelector).FullName, write.LoggerName); Assert.Equal("DefaultActionSelector.SelectAsync", write.Scope); var values = Assert.IsType(write.State); diff --git a/test/Microsoft.AspNet.Mvc.Core.Test/Logging/NullLogger.cs b/test/Microsoft.AspNet.Mvc.Core.Test/Logging/NullLogger.cs index af228397f8..17c68b7981 100644 --- a/test/Microsoft.AspNet.Mvc.Core.Test/Logging/NullLogger.cs +++ b/test/Microsoft.AspNet.Mvc.Core.Test/Logging/NullLogger.cs @@ -15,7 +15,11 @@ public IDisposable BeginScope(object state) return NullDisposable.Instance; } - public bool WriteCore(TraceType eventType, int eventId, object state, Exception exception, Func formatter) + public void Write(TraceType eventType, int eventId, object state, Exception exception, Func formatter) + { + } + + public bool IsEnabled(TraceType eventType) { return false; } diff --git a/test/Microsoft.AspNet.Mvc.Core.Test/Logging/TestLogger.cs b/test/Microsoft.AspNet.Mvc.Core.Test/Logging/TestLogger.cs index 8bf8301ae4..68110ae682 100644 --- a/test/Microsoft.AspNet.Mvc.Core.Test/Logging/TestLogger.cs +++ b/test/Microsoft.AspNet.Mvc.Core.Test/Logging/TestLogger.cs @@ -33,9 +33,9 @@ public IDisposable BeginScope(object state) return NullDisposable.Instance; } - public bool WriteCore(TraceType eventType, int eventId, object state, Exception exception, Func formatter) + public void Write(TraceType eventType, int eventId, object state, Exception exception, Func formatter) { - _sink.Write(new WriteCoreContext() + _sink.Write(new WriteContext() { EventType = eventType, EventId = eventId, @@ -45,7 +45,10 @@ public bool WriteCore(TraceType eventType, int eventId, object state, Exception LoggerName = _name, Scope = _scope }); + } + public bool IsEnabled(TraceType eventType) + { return true; } } diff --git a/test/Microsoft.AspNet.Mvc.Core.Test/Logging/TestSink.cs b/test/Microsoft.AspNet.Mvc.Core.Test/Logging/TestSink.cs index 29d48c3143..eefbb1c099 100644 --- a/test/Microsoft.AspNet.Mvc.Core.Test/Logging/TestSink.cs +++ b/test/Microsoft.AspNet.Mvc.Core.Test/Logging/TestSink.cs @@ -9,25 +9,25 @@ namespace Microsoft.AspNet.Mvc public class TestSink { public TestSink( - Func writeEnabled = null, + Func writeEnabled = null, Func beginEnabled = null) { WriteEnabled = writeEnabled; BeginEnabled = beginEnabled; Scopes = new List(); - Writes = new List(); + Writes = new List(); } - public Func WriteEnabled { get; set; } + public Func WriteEnabled { get; set; } public Func BeginEnabled { get; set; } public List Scopes { get; set; } - public List Writes { get; set; } + public List Writes { get; set; } - public void Write(WriteCoreContext context) + public void Write(WriteContext context) { if (WriteEnabled == null || WriteEnabled(context)) { @@ -43,7 +43,7 @@ public void Begin(BeginScopeContext context) } } - public static bool EnableWithTypeName(WriteCoreContext context) + public static bool EnableWithTypeName(WriteContext context) { return context.LoggerName.Equals(typeof(T).FullName); } diff --git a/test/Microsoft.AspNet.Mvc.Core.Test/Logging/WriteCoreContext.cs b/test/Microsoft.AspNet.Mvc.Core.Test/Logging/WriteContext.cs similarity index 94% rename from test/Microsoft.AspNet.Mvc.Core.Test/Logging/WriteCoreContext.cs rename to test/Microsoft.AspNet.Mvc.Core.Test/Logging/WriteContext.cs index 838aac7af8..a8b0ff291c 100644 --- a/test/Microsoft.AspNet.Mvc.Core.Test/Logging/WriteCoreContext.cs +++ b/test/Microsoft.AspNet.Mvc.Core.Test/Logging/WriteContext.cs @@ -6,7 +6,7 @@ namespace Microsoft.AspNet.Mvc { - public class WriteCoreContext + public class WriteContext { public TraceType EventType { get; set; } diff --git a/test/Microsoft.AspNet.Mvc.Core.Test/MvcRouteHandlerTests.cs b/test/Microsoft.AspNet.Mvc.Core.Test/MvcRouteHandlerTests.cs index 14e5edf4b3..a814ae0ae3 100644 --- a/test/Microsoft.AspNet.Mvc.Core.Test/MvcRouteHandlerTests.cs +++ b/test/Microsoft.AspNet.Mvc.Core.Test/MvcRouteHandlerTests.cs @@ -38,15 +38,9 @@ public async Task RouteAsync_Success_LogsCorrectValues() Assert.Equal(typeof(MvcRouteHandler).FullName, scope.LoggerName); Assert.Equal("MvcRouteHandler.RouteAsync", scope.Scope); - // There is a record for IsEnabled and one for WriteCore. - Assert.Equal(2, sink.Writes.Count); + Assert.Equal(1, sink.Writes.Count); - var enabled = sink.Writes[0]; - Assert.Equal(typeof(MvcRouteHandler).FullName, enabled.LoggerName); - Assert.Equal("MvcRouteHandler.RouteAsync", enabled.Scope); - Assert.Null(enabled.State); - - var write = sink.Writes[1]; + var write = sink.Writes[0]; Assert.Equal(typeof(MvcRouteHandler).FullName, write.LoggerName); Assert.Equal("MvcRouteHandler.RouteAsync", write.Scope); var values = Assert.IsType(write.State); @@ -82,15 +76,9 @@ public async Task RouteAsync_FailOnNoAction_LogsCorrectValues() Assert.Equal(typeof(MvcRouteHandler).FullName, scope.LoggerName); Assert.Equal("MvcRouteHandler.RouteAsync", scope.Scope); - // There is a record for IsEnabled and one for WriteCore. - Assert.Equal(2, sink.Writes.Count); - - var enabled = sink.Writes[0]; - Assert.Equal(typeof(MvcRouteHandler).FullName, enabled.LoggerName); - Assert.Equal("MvcRouteHandler.RouteAsync", enabled.Scope); - Assert.Null(enabled.State); + Assert.Equal(1, sink.Writes.Count); - var write = sink.Writes[1]; + var write = sink.Writes[0]; Assert.Equal(typeof(MvcRouteHandler).FullName, write.LoggerName); Assert.Equal("MvcRouteHandler.RouteAsync", write.Scope); var values = Assert.IsType(write.State); @@ -127,15 +115,9 @@ await Assert.ThrowsAsync(async () => Assert.Equal(typeof(MvcRouteHandler).FullName, scope.LoggerName); Assert.Equal("MvcRouteHandler.RouteAsync", scope.Scope); - // There is a record for IsEnabled and one for WriteCore. - Assert.Equal(2, sink.Writes.Count); - - var enabled = sink.Writes[0]; - Assert.Equal(typeof(MvcRouteHandler).FullName, enabled.LoggerName); - Assert.Equal("MvcRouteHandler.RouteAsync", enabled.Scope); - Assert.Null(enabled.State); + Assert.Equal(1, sink.Writes.Count); - var write = sink.Writes[1]; + var write = sink.Writes[0]; Assert.Equal(typeof(MvcRouteHandler).FullName, write.LoggerName); Assert.Equal("MvcRouteHandler.RouteAsync", write.Scope); var values = Assert.IsType(write.State); diff --git a/test/Microsoft.AspNet.Mvc.Core.Test/Routing/AttributeRouteTests.cs b/test/Microsoft.AspNet.Mvc.Core.Test/Routing/AttributeRouteTests.cs index 53c9bb48c0..d637a71e3a 100644 --- a/test/Microsoft.AspNet.Mvc.Core.Test/Routing/AttributeRouteTests.cs +++ b/test/Microsoft.AspNet.Mvc.Core.Test/Routing/AttributeRouteTests.cs @@ -682,15 +682,9 @@ public async void AttributeRoute_RouteAsyncHandled_LogsCorrectValues() Assert.Equal(typeof(AttributeRoute).FullName, scope.LoggerName); Assert.Equal("AttributeRoute.RouteAsync", scope.Scope); - // There is a record for IsEnabled and one for WriteCore. - Assert.Equal(2, sink.Writes.Count); + Assert.Equal(1, sink.Writes.Count); - var enabled = sink.Writes[0]; - Assert.Equal(typeof(AttributeRoute).FullName, enabled.LoggerName); - Assert.Equal("AttributeRoute.RouteAsync", enabled.Scope); - Assert.Null(enabled.State); - - var write = sink.Writes[1]; + var write = sink.Writes[0]; Assert.Equal(typeof(AttributeRoute).FullName, write.LoggerName); Assert.Equal("AttributeRoute.RouteAsync", write.Scope); var values = Assert.IsType(write.State); @@ -719,15 +713,9 @@ public async void AttributeRoute_RouteAsyncNotHandled_LogsCorrectValues() Assert.Equal(typeof(AttributeRoute).FullName, scope.LoggerName); Assert.Equal("AttributeRoute.RouteAsync", scope.Scope); - // There is a record for IsEnabled and one for WriteCore. - Assert.Equal(2, sink.Writes.Count); - - var enabled = sink.Writes[0]; - Assert.Equal(typeof(AttributeRoute).FullName, enabled.LoggerName); - Assert.Equal("AttributeRoute.RouteAsync", enabled.Scope); - Assert.Null(enabled.State); + Assert.Equal(1, sink.Writes.Count); - var write = sink.Writes[1]; + var write = sink.Writes[0]; Assert.Equal(typeof(AttributeRoute).FullName, write.LoggerName); Assert.Equal("AttributeRoute.RouteAsync", write.Scope); var values = Assert.IsType(write.State); diff --git a/test/Microsoft.AspNet.Mvc.FunctionalTests/NullLoggerFactory.cs b/test/Microsoft.AspNet.Mvc.FunctionalTests/NullLoggerFactory.cs index a5a0462970..02930abcf3 100644 --- a/test/Microsoft.AspNet.Mvc.FunctionalTests/NullLoggerFactory.cs +++ b/test/Microsoft.AspNet.Mvc.FunctionalTests/NullLoggerFactory.cs @@ -27,7 +27,11 @@ public IDisposable BeginScope(object state) return NullDisposable.Instance; } - public bool WriteCore(TraceType eventType, int eventId, object state, Exception exception, Func formatter) + public void Write(TraceType eventType, int eventId, object state, Exception exception, Func formatter) + { + } + + public bool IsEnabled(TraceType eventType) { return false; } From d9ebb37906712528b0bf3ab826fa10d4ce28a20f Mon Sep 17 00:00:00 2001 From: Pranav K Date: Tue, 14 Oct 2014 09:48:24 -0700 Subject: [PATCH 28/39] Removing Microsoft.AspNet.Mvc dependency from ModelBinding --- .../DefaultBodyModelValidatorTests.cs | 21 +++++++------------ .../project.json | 1 - 2 files changed, 8 insertions(+), 14 deletions(-) diff --git a/test/Microsoft.AspNet.Mvc.ModelBinding.Test/Validation/DefaultBodyModelValidatorTests.cs b/test/Microsoft.AspNet.Mvc.ModelBinding.Test/Validation/DefaultBodyModelValidatorTests.cs index be74654e5d..93448dd0d6 100644 --- a/test/Microsoft.AspNet.Mvc.ModelBinding.Test/Validation/DefaultBodyModelValidatorTests.cs +++ b/test/Microsoft.AspNet.Mvc.ModelBinding.Test/Validation/DefaultBodyModelValidatorTests.cs @@ -6,10 +6,7 @@ using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.Linq; -using Microsoft.AspNet.Mvc.OptionDescriptors; using Microsoft.AspNet.Testing; -using Microsoft.Framework.DependencyInjection; -using Microsoft.Framework.OptionsModel; using Moq; using Xunit; @@ -295,19 +292,17 @@ public void Validate_DoesNotUseOverridden_GetHashCodeOrEquals() private ModelValidationContext GetModelValidationContext(object model, Type type) { var modelStateDictionary = new ModelStateDictionary(); - var mvcOptions = new MvcOptions(); - var setup = new MvcOptionsSetup(); - setup.Invoke(mvcOptions); - var accessor = new Mock>(); - accessor.SetupGet(a => a.Options) - .Returns(mvcOptions); + var provider = new Mock(); + provider.SetupGet(p => p.ModelValidatorProviders) + .Returns(new IModelValidatorProvider[] + { + new DataAnnotationsModelValidatorProvider(), + new DataMemberModelValidatorProvider() + }); var modelMetadataProvider = new EmptyModelMetadataProvider(); return new ModelValidationContext( modelMetadataProvider, - new CompositeModelValidatorProvider( - new DefaultModelValidatorProviderProvider( - accessor.Object, Mock.Of(), - Mock.Of())), + new CompositeModelValidatorProvider(provider.Object), modelStateDictionary, new ModelMetadata( provider: modelMetadataProvider, diff --git a/test/Microsoft.AspNet.Mvc.ModelBinding.Test/project.json b/test/Microsoft.AspNet.Mvc.ModelBinding.Test/project.json index 0fbf217211..64dede055a 100644 --- a/test/Microsoft.AspNet.Mvc.ModelBinding.Test/project.json +++ b/test/Microsoft.AspNet.Mvc.ModelBinding.Test/project.json @@ -4,7 +4,6 @@ }, "dependencies": { "Microsoft.AspNet.Http": "1.0.0-*", - "Microsoft.AspNet.Mvc": "", "Microsoft.AspNet.Mvc.ModelBinding": "", "Microsoft.AspNet.PipelineCore": "1.0.0-*", "Microsoft.AspNet.Routing": "1.0.0-*", From a41b9dc983ae7d17afc3c90a57e1b0d085e08c76 Mon Sep 17 00:00:00 2001 From: Pranav K Date: Tue, 14 Oct 2014 07:28:35 -0700 Subject: [PATCH 29/39] Add Html.Partial - sync versions of Html.PartialAsync Fixes #1107 --- .../Rendering/HtmlHelperPartialExtensions.cs | 92 +++++++++ .../Rendering/DefaultTemplatesUtilities.cs | 10 +- .../Rendering/HtmlHelperDisplayTextTest.cs | 6 +- .../HtmlHelperPartialExtensionsTest.cs | 195 ++++++++++++++++++ 4 files changed, 297 insertions(+), 6 deletions(-) create mode 100644 test/Microsoft.AspNet.Mvc.Core.Test/Rendering/HtmlHelperPartialExtensionsTest.cs diff --git a/src/Microsoft.AspNet.Mvc.Core/Rendering/HtmlHelperPartialExtensions.cs b/src/Microsoft.AspNet.Mvc.Core/Rendering/HtmlHelperPartialExtensions.cs index de88d2a585..16c556b2a5 100644 --- a/src/Microsoft.AspNet.Mvc.Core/Rendering/HtmlHelperPartialExtensions.cs +++ b/src/Microsoft.AspNet.Mvc.Core/Rendering/HtmlHelperPartialExtensions.cs @@ -68,6 +68,98 @@ public static Task PartialAsync( return htmlHelper.PartialAsync(partialViewName, model, viewData: null); } + /// + /// Returns HTML markup for the specified partial view. + /// + /// The instance this method extends. + /// + /// The name of the partial view used to create the HTML markup. Must not be null. + /// + /// + /// Returns a new containing the created HTML. + /// + /// + /// This method synchronously calls and blocks on + /// + /// + public static HtmlString Partial( + [NotNull] this IHtmlHelper htmlHelper, + [NotNull] string partialViewName) + { + return Partial(htmlHelper, partialViewName, htmlHelper.ViewData.Model, viewData: null); + } + + /// + /// Returns HTML markup for the specified partial view. + /// + /// The instance this method extends. + /// + /// The name of the partial view used to create the HTML markup. Must not be null. + /// + /// A to pass into the partial view. + /// + /// Returns a new containing the created HTML. + /// + /// + /// This method synchronously calls and blocks on + /// + /// + public static HtmlString Partial( + [NotNull] this IHtmlHelper htmlHelper, + [NotNull] string partialViewName, + ViewDataDictionary viewData) + { + return Partial(htmlHelper, partialViewName, htmlHelper.ViewData.Model, viewData); + } + + /// + /// Returns HTML markup for the specified partial view. + /// + /// The instance this method extends. + /// + /// The name of the partial view used to create the HTML markup. Must not be null. + /// + /// A model to pass into the partial view. + /// + /// Returns a new containing the created HTML. + /// + /// + /// This method synchronously calls and blocks on + /// + /// + public static HtmlString Partial( + [NotNull] this IHtmlHelper htmlHelper, + [NotNull] string partialViewName, + object model) + { + return Partial(htmlHelper, partialViewName, model, viewData: null); + } + + /// + /// Returns HTML markup for the specified partial view. + /// + /// + /// The name of the partial view used to create the HTML markup. Must not be null. + /// + /// A model to pass into the partial view. + /// A to pass into the partial view. + /// + /// Returns a new containing the created HTML. + /// + /// + /// This method synchronously calls and blocks on + /// + /// + public static HtmlString Partial( + [NotNull] this IHtmlHelper htmlHelper, + [NotNull] string partialViewName, + object model, + ViewDataDictionary viewData) + { + var result = htmlHelper.PartialAsync(partialViewName, model, viewData); + return TaskHelper.WaitAndThrowIfFaulted(result); + } + /// /// Renders HTML markup for the specified partial view. /// diff --git a/test/Microsoft.AspNet.Mvc.Core.Test/Rendering/DefaultTemplatesUtilities.cs b/test/Microsoft.AspNet.Mvc.Core.Test/Rendering/DefaultTemplatesUtilities.cs index c02f465f89..ec64aeb464 100644 --- a/test/Microsoft.AspNet.Mvc.Core.Test/Rendering/DefaultTemplatesUtilities.cs +++ b/test/Microsoft.AspNet.Mvc.Core.Test/Rendering/DefaultTemplatesUtilities.cs @@ -6,17 +6,15 @@ using System.ComponentModel.DataAnnotations; using System.Globalization; using System.IO; -using System.Linq; using System.Threading.Tasks; using Microsoft.AspNet.Http; using Microsoft.AspNet.Mvc.ModelBinding; -using Microsoft.AspNet.Mvc.Rendering; using Microsoft.AspNet.Routing; using Microsoft.AspNet.Security.DataProtection; using Microsoft.Framework.OptionsModel; using Moq; -namespace Microsoft.AspNet.Mvc.Core +namespace Microsoft.AspNet.Mvc.Rendering { public class DefaultTemplatesUtilities { @@ -143,6 +141,12 @@ public static HtmlHelper GetHtmlHelper( return htmlHelper; } + public static string FormatOutput(IHtmlHelper helper, object model) + { + var metadata = helper.MetadataProvider.GetMetadataForType(() => model, model.GetType()); + return FormatOutput(metadata); + } + private static ICompositeViewEngine CreateViewEngine() { var view = new Mock(); diff --git a/test/Microsoft.AspNet.Mvc.Core.Test/Rendering/HtmlHelperDisplayTextTest.cs b/test/Microsoft.AspNet.Mvc.Core.Test/Rendering/HtmlHelperDisplayTextTest.cs index 011df639d8..905b5cbd86 100644 --- a/test/Microsoft.AspNet.Mvc.Core.Test/Rendering/HtmlHelperDisplayTextTest.cs +++ b/test/Microsoft.AspNet.Mvc.Core.Test/Rendering/HtmlHelperDisplayTextTest.cs @@ -5,11 +5,11 @@ using Microsoft.AspNet.Mvc.ModelBinding; using Xunit; -namespace Microsoft.AspNet.Mvc.Core +namespace Microsoft.AspNet.Mvc.Rendering { /// - /// Test the and - /// methods. + /// Test the and + /// methods. /// public class HtmlHelperDisplayTextTest { diff --git a/test/Microsoft.AspNet.Mvc.Core.Test/Rendering/HtmlHelperPartialExtensionsTest.cs b/test/Microsoft.AspNet.Mvc.Core.Test/Rendering/HtmlHelperPartialExtensionsTest.cs new file mode 100644 index 0000000000..1a4f644f14 --- /dev/null +++ b/test/Microsoft.AspNet.Mvc.Core.Test/Rendering/HtmlHelperPartialExtensionsTest.cs @@ -0,0 +1,195 @@ +// 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.Threading.Tasks; +using Microsoft.AspNet.Mvc.ModelBinding; +using Moq; +using Xunit; + +namespace Microsoft.AspNet.Mvc.Rendering +{ + public class HtmlHelperPartialExtensionsTest + { + public static TheoryData> PartialExtensionMethods + { + get + { + var vdd = new ViewDataDictionary(new EmptyModelMetadataProvider()); + return new TheoryData> + { + helper => helper.Partial("test"), + helper => helper.Partial("test", new object()), + helper => helper.Partial("test", vdd), + helper => helper.Partial("test", new object(), vdd) + }; + } + } + + [Theory] + [MemberData(nameof(PartialExtensionMethods))] + public void PartialMethods_DoesNotWrapThrownException(Func partialMethod) + { + // Arrange + var expected = new InvalidOperationException(); + var helper = new Mock(); + helper.Setup(h => h.PartialAsync("test", It.IsAny(), It.IsAny())) + .Callback(() => + { + // Workaround for compilation issue with Moq. + helper.ToString(); + throw expected; + }); + helper.SetupGet(h => h.ViewData) + .Returns(new ViewDataDictionary(new EmptyModelMetadataProvider())); + + // Act and Assert + var actual = Assert.Throws(() => partialMethod(helper.Object)); + Assert.Same(expected, actual); + } + + [Fact] + public void Partial_InvokesPartialAsyncWithCurrentModel() + { + // Arrange + var expected = new HtmlString("value"); + var model = new object(); + var viewData = new ViewDataDictionary(new EmptyModelMetadataProvider()) + { + Model = model + }; + var helper = new Mock(MockBehavior.Strict); + helper.Setup(h => h.PartialAsync("test", model, null)) + .Returns(Task.FromResult(expected)) + .Verifiable(); + helper.SetupGet(h => h.ViewData) + .Returns(viewData); + + // Act + var actual = helper.Object.Partial("test"); + + // Assert + Assert.Same(expected, actual); + helper.Verify(); + } + + [Fact] + public void PartialWithModel_InvokesPartialAsyncWithPassedInModel() + { + // Arrange + var expected = new HtmlString("value"); + var model = new object(); + var helper = new Mock(MockBehavior.Strict); + helper.Setup(h => h.PartialAsync("test", model, null)) + .Returns(Task.FromResult(expected)) + .Verifiable(); + + // Act + var actual = helper.Object.Partial("test", model); + + // Assert + Assert.Same(expected, actual); + helper.Verify(); + } + + [Fact] + public void PartialWithViewData_InvokesPartialAsyncWithPassedInViewData() + { + // Arrange + var expected = new HtmlString("value"); + var model = new object(); + var passedInViewData = new ViewDataDictionary(new EmptyModelMetadataProvider()); + var viewData = new ViewDataDictionary(new EmptyModelMetadataProvider()) + { + Model = model + }; + var helper = new Mock(MockBehavior.Strict); + helper.Setup(h => h.PartialAsync("test", model, passedInViewData)) + .Returns(Task.FromResult(expected)) + .Verifiable(); + helper.SetupGet(h => h.ViewData) + .Returns(viewData); + + // Act + var actual = helper.Object.Partial("test", passedInViewData); + + // Assert + Assert.Same(expected, actual); + helper.Verify(); + } + + [Fact] + public void PartialWithViewDataAndModel_InvokesPartialAsyncWithPassedInViewDataAndModel() + { + // Arrange + var expected = new HtmlString("value"); + var passedInModel = new object(); + var passedInViewData = new ViewDataDictionary(new EmptyModelMetadataProvider()); + var helper = new Mock(MockBehavior.Strict); + helper.Setup(h => h.PartialAsync("test", passedInModel, passedInViewData)) + .Returns(Task.FromResult(expected)) + .Verifiable(); + + // Act + var actual = helper.Object.Partial("test", passedInModel, passedInViewData); + + // Assert + Assert.Same(expected, actual); + helper.Verify(); + } + + [Fact] + public void Partial_InvokesAndRendersPartialAsyncOnHtmlHelperOfT() + { + // Arrange + var model = new TestModel(); + var helper = DefaultTemplatesUtilities.GetHtmlHelper(model); + var expected = DefaultTemplatesUtilities.FormatOutput(helper, model); + + // Act + var actual = helper.Partial("some-partial"); + + // Assert + Assert.Equal(expected, actual.ToString()); + } + + [Fact] + public void PartialWithModel_InvokesAndRendersPartialAsyncOnHtmlHelperOfT() + { + // Arrange + var model = new TestModel(); + var helper = DefaultTemplatesUtilities.GetHtmlHelper(); + var expected = DefaultTemplatesUtilities.FormatOutput(helper, model); + + // Act + var actual = helper.Partial("some-partial", model); + + // Assert + Assert.Equal(expected, actual.ToString()); + } + + [Fact] + public void PartialWithViewData_InvokesAndRendersPartialAsyncOnHtmlHelperOfT() + { + // Arrange + var model = new TestModel(); + var helper = DefaultTemplatesUtilities.GetHtmlHelper(model); + var viewData = new ViewDataDictionary(helper.MetadataProvider); + var expected = DefaultTemplatesUtilities.FormatOutput(helper, model); + + // Act + var actual = helper.Partial("some-partial", viewData); + + // Assert + Assert.Equal(expected, actual.ToString()); + } + + private sealed class TestModel + { + public override string ToString() + { + return "test-model-content"; + } + } + } +} \ No newline at end of file From 5fa8a9111163f5040afe6709fd0fa1bb05a11fe7 Mon Sep 17 00:00:00 2001 From: sornaks Date: Fri, 10 Oct 2014 15:46:35 -0700 Subject: [PATCH 30/39] -Issue #913 - Model-binding is being case-sensitive when binding Url data to Enum parameter. Fix: Using TypeConverter solves this problem. -Issue #1123 - TypeConverterModelBinder cannot bind "byte" and "short". Fix: Modified code to use TypeConverter which can handle these scenarios. -Removing the GetConverterDelegate method and making the code similar to the WebApi. --- .../Binders/MutableObjectModelBinder.cs | 2 +- .../Binders/TypeConverterModelBinder.cs | 2 +- .../Internal/TypeHelper.cs | 6 + .../Metadata/ModelMetadata.cs | 2 +- .../ValueProviders/ValueProviderResult.cs | 151 ++++-------------- .../project.json | 1 + .../Binders/TypeConverterModelBinderTest.cs | 8 +- .../ValueProviders/ValueProviderResultTest.cs | 22 +-- 8 files changed, 59 insertions(+), 135 deletions(-) diff --git a/src/Microsoft.AspNet.Mvc.ModelBinding/Binders/MutableObjectModelBinder.cs b/src/Microsoft.AspNet.Mvc.ModelBinding/Binders/MutableObjectModelBinder.cs index 1aeea1c7a7..5a88d51bd2 100644 --- a/src/Microsoft.AspNet.Mvc.ModelBinding/Binders/MutableObjectModelBinder.cs +++ b/src/Microsoft.AspNet.Mvc.ModelBinding/Binders/MutableObjectModelBinder.cs @@ -42,7 +42,7 @@ protected virtual bool CanUpdateProperty(ModelMetadata propertyMetadata) private static bool CanBindType(Type modelType) { // Simple types cannot use this binder - var isComplexType = !ValueProviderResult.CanConvertFromString(modelType); + var isComplexType = !TypeHelper.HasStringConverter(modelType); if (!isComplexType) { return false; diff --git a/src/Microsoft.AspNet.Mvc.ModelBinding/Binders/TypeConverterModelBinder.cs b/src/Microsoft.AspNet.Mvc.ModelBinding/Binders/TypeConverterModelBinder.cs index 7e2edc9f9a..800f14567f 100644 --- a/src/Microsoft.AspNet.Mvc.ModelBinding/Binders/TypeConverterModelBinder.cs +++ b/src/Microsoft.AspNet.Mvc.ModelBinding/Binders/TypeConverterModelBinder.cs @@ -13,7 +13,7 @@ public async Task BindModelAsync(ModelBindingContext bindingContext) { ModelBindingHelper.ValidateBindingContext(bindingContext); - if (!ValueProviderResult.CanConvertFromString(bindingContext.ModelType)) + if (!TypeHelper.HasStringConverter(bindingContext.ModelType)) { // this type cannot be converted return false; diff --git a/src/Microsoft.AspNet.Mvc.ModelBinding/Internal/TypeHelper.cs b/src/Microsoft.AspNet.Mvc.ModelBinding/Internal/TypeHelper.cs index 47bcdecbf1..760382cb51 100644 --- a/src/Microsoft.AspNet.Mvc.ModelBinding/Internal/TypeHelper.cs +++ b/src/Microsoft.AspNet.Mvc.ModelBinding/Internal/TypeHelper.cs @@ -2,6 +2,7 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; +using System.ComponentModel; using System.Reflection; namespace Microsoft.AspNet.Mvc.ModelBinding @@ -18,5 +19,10 @@ internal static bool IsSimpleType(Type type) type.Equals(typeof(DateTimeOffset)) || type.Equals(typeof(TimeSpan)); } + + internal static bool HasStringConverter(Type type) + { + return TypeDescriptor.GetConverter(type).CanConvertFrom(typeof(string)); + } } } \ No newline at end of file diff --git a/src/Microsoft.AspNet.Mvc.ModelBinding/Metadata/ModelMetadata.cs b/src/Microsoft.AspNet.Mvc.ModelBinding/Metadata/ModelMetadata.cs index 7faf1015c1..ee8faf91c2 100644 --- a/src/Microsoft.AspNet.Mvc.ModelBinding/Metadata/ModelMetadata.cs +++ b/src/Microsoft.AspNet.Mvc.ModelBinding/Metadata/ModelMetadata.cs @@ -107,7 +107,7 @@ public virtual bool ConvertEmptyStringToNull public virtual bool IsComplexType { - get { return !ValueProviderResult.CanConvertFromString(ModelType); } + get { return !TypeHelper.HasStringConverter(ModelType); } } public bool IsNullableValueType diff --git a/src/Microsoft.AspNet.Mvc.ModelBinding/ValueProviders/ValueProviderResult.cs b/src/Microsoft.AspNet.Mvc.ModelBinding/ValueProviders/ValueProviderResult.cs index a0d2ddbf04..b2d25afe12 100644 --- a/src/Microsoft.AspNet.Mvc.ModelBinding/ValueProviders/ValueProviderResult.cs +++ b/src/Microsoft.AspNet.Mvc.ModelBinding/ValueProviders/ValueProviderResult.cs @@ -3,6 +3,7 @@ using System; using System.Collections; +using System.ComponentModel; using System.Globalization; using System.Reflection; @@ -53,7 +54,7 @@ public virtual object ConvertTo([NotNull] Type type, CultureInfo culture) if (value == null) { // treat null route parameters as though they were the default value for the type - return type.GetTypeInfo().IsValueType ? Activator.CreateInstance(type) : + return type.GetTypeInfo().IsValueType ? Activator.CreateInstance(type) : null; } @@ -68,34 +69,8 @@ public virtual object ConvertTo([NotNull] Type type, CultureInfo culture) public static bool CanConvertFromString(Type destinationType) { - return GetConverterDelegate(destinationType) != null; - } - - private object ConvertSimpleType(CultureInfo culture, object value, Type destinationType) - { - if (value == null || value.GetType().IsAssignableFrom(destinationType)) - { - return value; - } - - // In case of a Nullable object, we try again with its underlying type. - destinationType = UnwrapNullableType(destinationType); - - // if this is a user-input value but the user didn't type anything, return no value - var valueAsString = value as string; - if (valueAsString != null && string.IsNullOrWhiteSpace(valueAsString)) - { - return null; - } - - var converter = GetConverterDelegate(destinationType); - if (converter == null) - { - var message = Resources.FormatValueProviderResult_NoConverterExists(value.GetType(), destinationType); - throw new InvalidOperationException(message); - } - - return converter(value, culture); + return TypeHelper.IsSimpleType(UnwrapNullableType(destinationType)) || + TypeHelper.HasStringConverter(destinationType); } private object UnwrapPossibleArrayType(CultureInfo culture, object value, Type destinationType) @@ -144,121 +119,57 @@ private object UnwrapPossibleArrayType(CultureInfo culture, object value, Type d return ConvertSimpleType(culture, value, destinationType); } - private static Func GetConverterDelegate(Type destinationType) + private object ConvertSimpleType(CultureInfo culture, object value, Type destinationType) { - destinationType = UnwrapNullableType(destinationType); - - if (destinationType == typeof(string)) - { - return (value, culture) => Convert.ToString(value, culture); - } - - if (destinationType == typeof(int)) - { - return (value, culture) => Convert.ToInt32(value, culture); - } - - if (destinationType == typeof(long)) - { - return (value, culture) => Convert.ToInt64(value, culture); - } - - if (destinationType == typeof(float)) + if (value == null || value.GetType().IsAssignableFrom(destinationType)) { - return (value, culture) => Convert.ToSingle(value, culture); + return value; } - if (destinationType == typeof(double)) - { - return (value, culture) => Convert.ToDouble(value, culture); - } + // In case of a Nullable object, we try again with its underlying type. + destinationType = UnwrapNullableType(destinationType); - if (destinationType == typeof(decimal)) + // if this is a user-input value but the user didn't type anything, return no value + var valueAsString = value as string; + if (valueAsString != null && string.IsNullOrWhiteSpace(valueAsString)) { - return (value, culture) => Convert.ToDecimal(value, culture); + return null; } - if (destinationType == typeof(bool)) + var converter = TypeDescriptor.GetConverter(destinationType); + var canConvertFrom = converter.CanConvertFrom(value.GetType()); + if (!canConvertFrom) { - return (value, culture) => Convert.ToBoolean(value, culture); + converter = TypeDescriptor.GetConverter(value.GetType()); } - - if (destinationType == typeof(DateTime)) + if (!(canConvertFrom || converter.CanConvertTo(destinationType))) { - return (value, culture) => + // EnumConverter cannot convert integer, so we verify manually + if (destinationType.IsEnum() && (value is int)) { - ThrowIfNotStringType(value, destinationType); - return DateTime.Parse((string)value, culture); - }; - } + return Enum.ToObject(destinationType, (int)value); + } - if (destinationType == typeof(DateTimeOffset)) - { - return (value, culture) => - { - ThrowIfNotStringType(value, destinationType); - return DateTimeOffset.Parse((string)value, culture); - }; + throw new InvalidOperationException( + Resources.FormatValueProviderResult_NoConverterExists(value.GetType(), destinationType)); } - if (destinationType == typeof(TimeSpan)) + try { - return (value, culture) => - { - ThrowIfNotStringType(value, destinationType); - return TimeSpan.Parse((string)value, culture); - }; + return canConvertFrom + ? converter.ConvertFrom(null, culture, value) + : converter.ConvertTo(null, culture, value, destinationType); } - - if (destinationType == typeof(Guid)) + catch (Exception ex) { - return (value, culture) => - { - ThrowIfNotStringType(value, destinationType); - return Guid.Parse((string)value); - }; - } - - if (destinationType.GetTypeInfo().IsEnum) - { - return (value, culture) => - { - // EnumConverter cannot convert integer, so we verify manually - if ((value is int)) - { - if (Enum.IsDefined(destinationType, value)) - { - return Enum.ToObject(destinationType, (int)value); - } - - throw new FormatException( - Resources.FormatValueProviderResult_CannotConvertEnum(value, - destinationType)); - } - else - { - ThrowIfNotStringType(value, destinationType); - return Enum.Parse(destinationType, (string)value); - } - }; + throw new InvalidOperationException( + Resources.FormatValueProviderResult_ConversionThrew(value.GetType(), destinationType), ex); } - - return null; } private static Type UnwrapNullableType(Type destinationType) { return Nullable.GetUnderlyingType(destinationType) ?? destinationType; } - - private static void ThrowIfNotStringType(object value, Type destinationType) - { - var type = value.GetType(); - if (type != typeof(string)) - { - var message = Resources.FormatValueProviderResult_NoConverterExists(type, destinationType); - throw new InvalidOperationException(message); - } - } } } diff --git a/src/Microsoft.AspNet.Mvc.ModelBinding/project.json b/src/Microsoft.AspNet.Mvc.ModelBinding/project.json index f2f1271d2a..664d517169 100644 --- a/src/Microsoft.AspNet.Mvc.ModelBinding/project.json +++ b/src/Microsoft.AspNet.Mvc.ModelBinding/project.json @@ -24,6 +24,7 @@ "System.Collections": "4.0.10-beta-*", "System.Collections.Concurrent": "4.0.0-beta-*", "System.ComponentModel": "4.0.0-beta-*", + "System.ComponentModel.TypeConverter": "4.0.0-beta-*", "System.Diagnostics.Contracts": "4.0.0-beta-*", "System.Diagnostics.Debug": "4.0.10-beta-*", "System.Diagnostics.Tools": "4.0.0-beta-*", diff --git a/test/Microsoft.AspNet.Mvc.ModelBinding.Test/Binders/TypeConverterModelBinderTest.cs b/test/Microsoft.AspNet.Mvc.ModelBinding.Test/Binders/TypeConverterModelBinderTest.cs index 4ed7d233c7..7c16625922 100644 --- a/test/Microsoft.AspNet.Mvc.ModelBinding.Test/Binders/TypeConverterModelBinderTest.cs +++ b/test/Microsoft.AspNet.Mvc.ModelBinding.Test/Binders/TypeConverterModelBinderTest.cs @@ -34,6 +34,8 @@ public async Task BindModel_ReturnsFalse_IfTypeCannotBeConverted(Type destinatio } [Theory] + [InlineData(typeof(byte))] + [InlineData(typeof(short))] [InlineData(typeof(int))] [InlineData(typeof(long))] [InlineData(typeof(Guid))] @@ -62,8 +64,8 @@ public async Task BindModel_ReturnsTrue_IfTypeCanBeConverted(Type destinationTyp public async Task BindModel_Error_FormatExceptionsTurnedIntoStringsInModelState() { // Arrange - var message = TestPlatformHelper.IsMono ? "Input string was not in the correct format" : - "Input string was not in a correct format."; + var message = "The parameter conversion from type 'System.String' to type 'System.Int32' failed." + + " See the inner exception for more information."; var bindingContext = GetBindingContext(typeof(int)); bindingContext.ValueProvider = new SimpleHttpValueProvider { @@ -78,7 +80,7 @@ public async Task BindModel_Error_FormatExceptionsTurnedIntoStringsInModelState( // Assert Assert.True(retVal); Assert.Null(bindingContext.Model); - Assert.Equal(false, bindingContext.ModelState.IsValid); + Assert.False(bindingContext.ModelState.IsValid); var error = Assert.Single(bindingContext.ModelState["theModelName"].Errors); Assert.Equal(message, error.ErrorMessage); } diff --git a/test/Microsoft.AspNet.Mvc.ModelBinding.Test/ValueProviders/ValueProviderResultTest.cs b/test/Microsoft.AspNet.Mvc.ModelBinding.Test/ValueProviders/ValueProviderResultTest.cs index 649adb7919..712317a8b7 100644 --- a/test/Microsoft.AspNet.Mvc.ModelBinding.Test/ValueProviders/ValueProviderResultTest.cs +++ b/test/Microsoft.AspNet.Mvc.ModelBinding.Test/ValueProviders/ValueProviderResultTest.cs @@ -289,6 +289,7 @@ public void ConvertToReturnsValueIfArrayElementInstanceOfDestinationType() [Theory] [InlineData(new object[] { new[] { 1, 0 } })] [InlineData(new object[] { new[] { "Value1", "Value0" } })] + [InlineData(new object[] { new[] { "Value1", "value0" } })] public void ConvertTo_ConvertsEnumArrays(object value) { // Arrange @@ -318,16 +319,17 @@ public void ConvertToReturnsValueIfInstanceOfDestinationType() } [Theory] - [InlineData(typeof(int), typeof(FormatException))] - [InlineData(typeof(double?), typeof(FormatException))] - [InlineData(typeof(MyEnum?), typeof(ArgumentException))] - public void ConvertToThrowsIfConverterThrows(Type destinationType, Type exceptionType) + [InlineData(typeof(int), typeof(InvalidOperationException), typeof(Exception))] + [InlineData(typeof(double?), typeof(InvalidOperationException), typeof(Exception))] + [InlineData(typeof(MyEnum?), typeof(InvalidOperationException), typeof(FormatException))] + public void ConvertToThrowsIfConverterThrows(Type destinationType, Type exceptionType, Type innerExceptionType) { // Arrange var vpr = new ValueProviderResult("this-is-not-a-valid-value", null, CultureInfo.InvariantCulture); // Act & Assert - Assert.Throws(exceptionType, () => vpr.ConvertTo(destinationType)); + var ex = Assert.Throws(exceptionType, () => vpr.ConvertTo(destinationType)); + Assert.IsType(innerExceptionType, ex.InnerException); } [Fact] @@ -354,12 +356,11 @@ public void ConvertToUsesProvidedCulture() var frCulture = new CultureInfo("fr-FR"); // Act - var cultureResult = (decimal)vpr.ConvertTo(typeof(decimal), frCulture); - var result = (decimal)vpr.ConvertTo(typeof(decimal)); + var cultureResult = vpr.ConvertTo(typeof(decimal), frCulture); // Assert Assert.Equal(12.5M, cultureResult); - Assert.Equal(125, result); + Assert.Throws(() => vpr.ConvertTo(typeof(decimal))); } [Fact] @@ -387,14 +388,17 @@ public static IEnumerable IntrinsicConversionData { get { - yield return new object[] { 42, 42M }; yield return new object[] { 42, 42L }; + yield return new object[] { 42, (short)42 }; yield return new object[] { 42, (float)42.0 }; yield return new object[] { 42, (double)42.0 }; yield return new object[] { 42M, 42 }; yield return new object[] { 42L, 42 }; + yield return new object[] { 42, (byte)42 }; + yield return new object[] { (short)42, 42 }; yield return new object[] { (float)42.0, 42 }; yield return new object[] { (double)42.0, 42 }; + yield return new object[] { (byte)42, 42 }; yield return new object[] { "2008-01-01", new DateTime(2008, 01, 01) }; yield return new object[] { "00:00:20", TimeSpan.FromSeconds(20) }; yield return new object[] From 2d67f2fc27c0707907483b65d0a1f800585a6abe Mon Sep 17 00:00:00 2001 From: Pranav K Date: Tue, 14 Oct 2014 16:08:30 -0700 Subject: [PATCH 31/39] Modify precompilation to always generate instrumentation --- .../Compilation/CompilerCacheEntry.cs | 2 ++ .../Razor/PreCompileViews/RazorPreCompiler.cs | 1 + 2 files changed, 3 insertions(+) diff --git a/src/Microsoft.AspNet.Mvc.Razor/Compilation/CompilerCacheEntry.cs b/src/Microsoft.AspNet.Mvc.Razor/Compilation/CompilerCacheEntry.cs index d6c1028153..90e2881182 100644 --- a/src/Microsoft.AspNet.Mvc.Razor/Compilation/CompilerCacheEntry.cs +++ b/src/Microsoft.AspNet.Mvc.Razor/Compilation/CompilerCacheEntry.cs @@ -22,6 +22,8 @@ public CompilerCacheEntry([NotNull] RazorFileInfo info, [NotNull] Type compiledT Length = info.Length; LastModified = info.LastModified; Hash = info.Hash; + // Precompiled views are always instrumented. + IsInstrumented = true; } /// diff --git a/src/Microsoft.AspNet.Mvc.Razor/Razor/PreCompileViews/RazorPreCompiler.cs b/src/Microsoft.AspNet.Mvc.Razor/Razor/PreCompileViews/RazorPreCompiler.cs index 82194d760d..22670fca49 100644 --- a/src/Microsoft.AspNet.Mvc.Razor/Razor/PreCompileViews/RazorPreCompiler.cs +++ b/src/Microsoft.AspNet.Mvc.Razor/Razor/PreCompileViews/RazorPreCompiler.cs @@ -32,6 +32,7 @@ public RazorPreCompiler([NotNull] IServiceProvider designTimeServiceProvider, { _serviceProvider = designTimeServiceProvider; _host = host; + _host.EnableInstrumentation = true; var appEnv = _serviceProvider.GetService(); _fileSystem = optionsAccessor.Options.FileSystem; From 1680616fe5ac13ce817ea2766e78c4cad6156a05 Mon Sep 17 00:00:00 2001 From: jacalvar Date: Wed, 1 Oct 2014 16:44:16 -0700 Subject: [PATCH 32/39] [Issue #1133] RedirectToActionResult and UrlHelper should use HostString, PathString, etc. To build Urls. Added tests to describe the current behaviour with Unicode hosts. --- .../UrlHelperTest.cs | 41 ++++++++++- .../LinkGenerationTests.cs | 70 +++++++++++++++++++ .../BasicWebSite.Home.ActionLinkView.html | 9 +++ .../Controllers/HomeController.cs | 17 +++++ .../Views/Home/ActionLinkView.cshtml | 9 +++ 5 files changed, 145 insertions(+), 1 deletion(-) create mode 100644 test/Microsoft.AspNet.Mvc.FunctionalTests/LinkGenerationTests.cs create mode 100644 test/Microsoft.AspNet.Mvc.FunctionalTests/compiler/resources/BasicWebSite.Home.ActionLinkView.html create mode 100644 test/WebSites/BasicWebSite/Views/Home/ActionLinkView.cshtml diff --git a/test/Microsoft.AspNet.Mvc.Core.Test/UrlHelperTest.cs b/test/Microsoft.AspNet.Mvc.Core.Test/UrlHelperTest.cs index ccad91af8b..0ae9023986 100644 --- a/test/Microsoft.AspNet.Mvc.Core.Test/UrlHelperTest.cs +++ b/test/Microsoft.AspNet.Mvc.Core.Test/UrlHelperTest.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. +// 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; @@ -392,6 +392,27 @@ public void RouteUrlWithProtocol() Assert.Equal("https://localhost/app/named/home2/newaction/someid", url); } + [Fact] + public void RouteUrl_WithUnicodeHost_DoesNotPunyEncodeTheHost() + { + // Arrange + var urlHelper = CreateUrlHelperWithRouteCollection("/app"); + + // Act + var url = urlHelper.RouteUrl(routeName: "namedroute", + values: new + { + Action = "newaction", + Controller = "home2", + id = "someid" + }, + protocol: "https", + host: "pingüino"); + + // Assert + Assert.Equal("https://pingüino/app/named/home2/newaction/someid", url); + } + [Fact] public void RouteUrlWithRouteNameAndDefaults() { @@ -472,6 +493,24 @@ public void UrlAction_RouteValuesAsDictionary_CaseSensitive() Assert.Equal("/app/home/contact/suppliedid?isprint=true", url); } + [Fact] + public void UrlAction_WithUnicodeHost_DoesNotPunyEncodeTheHost() + { + // Arrange + var urlHelper = CreateUrlHelperWithRouteCollection("/app"); + + // Act + var url = urlHelper.Action( + action: "contact", + controller: "home", + values: null, + protocol: "http", + host: "pingüino"); + + // Assert + Assert.Equal("http://pingüino/app/home/contact", url); + } + [Fact] public void UrlRouteUrl_RouteValuesAsDictionary_CaseSensitive() { diff --git a/test/Microsoft.AspNet.Mvc.FunctionalTests/LinkGenerationTests.cs b/test/Microsoft.AspNet.Mvc.FunctionalTests/LinkGenerationTests.cs new file mode 100644 index 0000000000..49320dc7de --- /dev/null +++ b/test/Microsoft.AspNet.Mvc.FunctionalTests/LinkGenerationTests.cs @@ -0,0 +1,70 @@ +// 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.Net.Http.Headers; +using System.Reflection; +using System.Threading.Tasks; +using Microsoft.AspNet.Builder; +using Microsoft.AspNet.TestHost; +using Xunit; + +namespace Microsoft.AspNet.Mvc.FunctionalTests +{ + public class LinkGenerationTests + { + private readonly IServiceProvider _provider = TestHelper.CreateServices("BasicWebSite"); + private readonly Action _app = new BasicWebSite.Startup().Configure; + + // Some tests require comparing the actual response body against an expected response baseline + // so they require a reference to the assembly on which the resources are located, in order to + // make the tests less verbose, we get a reference to the assembly with the resources and we + // use it on all the rest of the tests. + private readonly Assembly _resourcesAssembly = typeof(LinkGenerationTests).GetTypeInfo().Assembly; + + [Theory] + [InlineData("http://pingüino/Home/RedirectToActionReturningTaskAction")] + [InlineData("http://pingüino/Home/RedirectToRouteActionAsMethodAction")] + public async Task GeneratedLinksWithActionResults_AreRelativeLinks_WhenSetOnLocationHeader(string url) + { + // Arrange + var server = TestServer.Create(_provider, _app); + var client = server.CreateClient(); + + // Act + + // The host is not important as everything runs in memory and tests are isolated from each other. + var response = await client.GetAsync(url); + var responseContent = await response.Content.ReadAsStringAsync(); + + // Assert + Assert.Equal(HttpStatusCode.Redirect, response.StatusCode); + + Assert.Equal("/Home/ActionReturningTask", response.Headers.Location.ToString()); + } + + [Fact] + public async Task GeneratedLinks_AreNotPunyEncoded_WhenGeneratedOnViews() + { + // Arrange + var server = TestServer.Create(_provider, _app); + var client = server.CreateClient(); + + var expectedMediaType = MediaTypeHeaderValue.Parse("text/html; charset=utf-8"); + var expectedContent = await _resourcesAssembly + .ReadResourceAsStringAsync("compiler/resources/BasicWebSite.Home.ActionLinkView.html"); + + // Act + + // The host is not important as everything runs in memory and tests are isolated from each other. + var response = await client.GetAsync("http://localhost/Home/ActionLinkView"); + var responseContent = await response.Content.ReadAsStringAsync(); + + // Assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.Equal(expectedMediaType, response.Content.Headers.ContentType); + Assert.Equal(expectedContent, responseContent); + } + } +} \ No newline at end of file diff --git a/test/Microsoft.AspNet.Mvc.FunctionalTests/compiler/resources/BasicWebSite.Home.ActionLinkView.html b/test/Microsoft.AspNet.Mvc.FunctionalTests/compiler/resources/BasicWebSite.Home.ActionLinkView.html new file mode 100644 index 0000000000..77b00b1391 --- /dev/null +++ b/test/Microsoft.AspNet.Mvc.FunctionalTests/compiler/resources/BasicWebSite.Home.ActionLinkView.html @@ -0,0 +1,9 @@ + + + + Action Link with non unicode host + + + Pingüino + + \ No newline at end of file diff --git a/test/WebSites/BasicWebSite/Controllers/HomeController.cs b/test/WebSites/BasicWebSite/Controllers/HomeController.cs index fbc8be7e55..589bf7ba75 100644 --- a/test/WebSites/BasicWebSite/Controllers/HomeController.cs +++ b/test/WebSites/BasicWebSite/Controllers/HomeController.cs @@ -19,6 +19,23 @@ public IActionResult PlainView() return View(); } + public IActionResult ActionLinkView() + { + // This view contains a link generated with Html.ActionLink + // that provides a host with non unicode characters. + return View(); + } + + public IActionResult RedirectToActionReturningTaskAction() + { + return RedirectToAction("ActionReturningTask"); + } + + public IActionResult RedirectToRouteActionAsMethodAction() + { + return RedirectToRoute("ActionAsMethod", new { action = "ActionReturningTask", controller = "Home" }); + } + public IActionResult NoContentResult() { return new HttpStatusCodeResult(204); diff --git a/test/WebSites/BasicWebSite/Views/Home/ActionLinkView.cshtml b/test/WebSites/BasicWebSite/Views/Home/ActionLinkView.cshtml new file mode 100644 index 0000000000..d3565ec547 --- /dev/null +++ b/test/WebSites/BasicWebSite/Views/Home/ActionLinkView.cshtml @@ -0,0 +1,9 @@ + + + + Action Link with non unicode host + + + @Html.ActionLink("Pingüino", "ActionLinkView", "Home", "http", "pingüino", null, routeValues: null, htmlAttributes: null) + + \ No newline at end of file From aaaf02110cdaa8f4ebf2089ae5ff83c3c433e58d Mon Sep 17 00:00:00 2001 From: Doug Bunting Date: Tue, 7 Oct 2014 12:15:52 -0700 Subject: [PATCH 33/39] Add TagHelperSample.Web --- Mvc.sln | 13 +++ .../Controllers/HomeController.cs | 75 ++++++++++++++++ samples/TagHelperSample.Web/Models/User.cs | 18 ++++ samples/TagHelperSample.Web/Startup.cs | 15 ++++ .../TagHelperSample.Web.kproj | 18 ++++ .../Views/Home/Create.cshtml | 89 +++++++++++++++++++ .../Views/Home/Edit.cshtml | 61 +++++++++++++ .../Views/Home/Index.cshtml | 35 ++++++++ samples/TagHelperSample.Web/project.json | 24 +++++ 9 files changed, 348 insertions(+) create mode 100644 samples/TagHelperSample.Web/Controllers/HomeController.cs create mode 100644 samples/TagHelperSample.Web/Models/User.cs create mode 100644 samples/TagHelperSample.Web/Startup.cs create mode 100644 samples/TagHelperSample.Web/TagHelperSample.Web.kproj create mode 100644 samples/TagHelperSample.Web/Views/Home/Create.cshtml create mode 100644 samples/TagHelperSample.Web/Views/Home/Edit.cshtml create mode 100644 samples/TagHelperSample.Web/Views/Home/Index.cshtml create mode 100644 samples/TagHelperSample.Web/project.json diff --git a/Mvc.sln b/Mvc.sln index 9fc74b2893..788e9a8325 100644 --- a/Mvc.sln +++ b/Mvc.sln @@ -98,6 +98,8 @@ Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "WebApiCompatShimWebSite", " EndProject Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Microsoft.AspNet.Mvc.WebApiCompatShimTest", "test\Microsoft.AspNet.Mvc.WebApiCompatShimTest\Microsoft.AspNet.Mvc.WebApiCompatShimTest.kproj", "{5DE8E4D9-AACD-4B5F-819F-F091383FB996}" EndProject +Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "TagHelperSample.Web", "samples\TagHelperSample.Web\TagHelperSample.Web.kproj", "{2223120F-D675-40DA-8CD8-11DC14A0B2C7}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -518,6 +520,16 @@ Global {5DE8E4D9-AACD-4B5F-819F-F091383FB996}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU {5DE8E4D9-AACD-4B5F-819F-F091383FB996}.Release|Mixed Platforms.Build.0 = Release|Any CPU {5DE8E4D9-AACD-4B5F-819F-F091383FB996}.Release|x86.ActiveCfg = Release|Any CPU + {2223120F-D675-40DA-8CD8-11DC14A0B2C7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {2223120F-D675-40DA-8CD8-11DC14A0B2C7}.Debug|Any CPU.Build.0 = Debug|Any CPU + {2223120F-D675-40DA-8CD8-11DC14A0B2C7}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU + {2223120F-D675-40DA-8CD8-11DC14A0B2C7}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU + {2223120F-D675-40DA-8CD8-11DC14A0B2C7}.Debug|x86.ActiveCfg = Debug|Any CPU + {2223120F-D675-40DA-8CD8-11DC14A0B2C7}.Release|Any CPU.ActiveCfg = Release|Any CPU + {2223120F-D675-40DA-8CD8-11DC14A0B2C7}.Release|Any CPU.Build.0 = Release|Any CPU + {2223120F-D675-40DA-8CD8-11DC14A0B2C7}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU + {2223120F-D675-40DA-8CD8-11DC14A0B2C7}.Release|Mixed Platforms.Build.0 = Release|Any CPU + {2223120F-D675-40DA-8CD8-11DC14A0B2C7}.Release|x86.ActiveCfg = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -565,5 +577,6 @@ Global {23D30B8C-04B1-4577-A604-ED27EA1E4A0E} = {32285FA4-6B46-4D6B-A840-2B13E4C8B58E} {B2B7BC91-688E-4C1E-A71F-CE948D958DDF} = {16703B76-C9F7-4C75-AE6C-53D92E308E3C} {5DE8E4D9-AACD-4B5F-819F-F091383FB996} = {3BA657BF-28B1-42DA-B5B0-1C4601FCF7B1} + {2223120F-D675-40DA-8CD8-11DC14A0B2C7} = {DAAE4C74-D06F-4874-A166-33305D2643CE} EndGlobalSection EndGlobal diff --git a/samples/TagHelperSample.Web/Controllers/HomeController.cs b/samples/TagHelperSample.Web/Controllers/HomeController.cs new file mode 100644 index 0000000000..1a9188cdf9 --- /dev/null +++ b/samples/TagHelperSample.Web/Controllers/HomeController.cs @@ -0,0 +1,75 @@ + +using System.Collections.Generic; +using System.Linq; +using Microsoft.AspNet.Mvc; +using Microsoft.AspNet.Mvc.Rendering; +using TagHelperSample.Web.Models; + +namespace TagHelperSample.Web.Controllers +{ + public class HomeController : Controller + { + private static readonly IEnumerable _items = new SelectList(Enumerable.Range(7, 13)); + private static readonly Dictionary _users = new Dictionary(); + private static int _next; + + public HomeController() + { + // Unable to set ViewBag from constructor. Does this work in MVC 5.2? + ////ViewBag.Items = _items; + } + + // GET: // + public IActionResult Index() + { + return View(_users.Values); + } + + // GET: /Home/Create + public IActionResult Create() + { + ViewBag.Items = _items; + return View(); + } + + // POST: Home/Create + [HttpPost] + public IActionResult Create(User user) + { + if (user != null && ModelState.IsValid) + { + var id = _next++; + user.Id = id; + _users[id] = user; + return RedirectToAction("Index"); + } + + ViewBag.Items = _items; + return View(); + } + + // GET: /Home/Edit/5 + public IActionResult Edit(int id) + { + User user; + _users.TryGetValue(id, out user); + + ViewBag.Items = _items; + return View(user); + } + + // POST: Home/Edit/5 + [HttpPost] + public IActionResult Edit(int id, User user) + { + if (user != null && id == user.Id && _users.ContainsKey(id) && ModelState.IsValid) + { + _users[id] = user; + return RedirectToAction("Index"); + } + + ViewBag.Items = _items; + return View(); + } + } +} diff --git a/samples/TagHelperSample.Web/Models/User.cs b/samples/TagHelperSample.Web/Models/User.cs new file mode 100644 index 0000000000..eb1b574a67 --- /dev/null +++ b/samples/TagHelperSample.Web/Models/User.cs @@ -0,0 +1,18 @@ + +using System; + +namespace TagHelperSample.Web.Models +{ + public class User + { + public int Id { get; set; } + + public string Name { get; set; } + + public string Blurb { get; set; } + + public DateTimeOffset DateOfBirth { get; set; } + + public int YearsEmployeed { get; set; } + } +} \ No newline at end of file diff --git a/samples/TagHelperSample.Web/Startup.cs b/samples/TagHelperSample.Web/Startup.cs new file mode 100644 index 0000000000..bf2cd2d968 --- /dev/null +++ b/samples/TagHelperSample.Web/Startup.cs @@ -0,0 +1,15 @@ + +using Microsoft.AspNet.Builder; +using Microsoft.Framework.DependencyInjection; + +namespace TagHelperSample.Web +{ + public class Startup + { + public void Configure(IApplicationBuilder app) + { + app.UseServices(services => services.AddMvc()); + app.UseMvc(); + } + } +} diff --git a/samples/TagHelperSample.Web/TagHelperSample.Web.kproj b/samples/TagHelperSample.Web/TagHelperSample.Web.kproj new file mode 100644 index 0000000000..3d932a6a8f --- /dev/null +++ b/samples/TagHelperSample.Web/TagHelperSample.Web.kproj @@ -0,0 +1,18 @@ + + + + 14.0 + $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) + + + + 2223120f-d675-40da-8cd8-11dc14a0b2c7 + Web + TagHelperSample.Web + + + 2.0 + 31726 + + + \ No newline at end of file diff --git a/samples/TagHelperSample.Web/Views/Home/Create.cshtml b/samples/TagHelperSample.Web/Views/Home/Create.cshtml new file mode 100644 index 0000000000..63dd93cf61 --- /dev/null +++ b/samples/TagHelperSample.Web/Views/Home/Create.cshtml @@ -0,0 +1,89 @@ + +@using TagHelperSample.Web.Models +@model User + +

    Create

    + +@* anti-forgery is on by default *@ +@* form will special-case anything that looks like a URI i.e. contains a '/' or doesn't match an action *@ +
    +
    + @* validation summary tag helper will target just
    elements and append the list of errors *@ + @* - i.e. this helper, like + +
    + @* no special-case for the "for" attribute; may eventually need to opt out on per-element basis here and in *@ +
    +
    +
    +
    +
    + + + diff --git a/samples/TagHelperSample.Web/Views/Home/Edit.cshtml b/samples/TagHelperSample.Web/Views/Home/Edit.cshtml new file mode 100644 index 0000000000..15003e6c88 --- /dev/null +++ b/samples/TagHelperSample.Web/Views/Home/Edit.cshtml @@ -0,0 +1,61 @@ + +@using TagHelperSample.Web.Models +@model User + +

    Edit

    + +
    +
    +
    + + +
    +
    +
    +
    +
    +
    +
    +
    + +
    +
    + +
    +
    +
    + + + diff --git a/samples/TagHelperSample.Web/Views/Home/Index.cshtml b/samples/TagHelperSample.Web/Views/Home/Index.cshtml new file mode 100644 index 0000000000..f07d0bcf76 --- /dev/null +++ b/samples/TagHelperSample.Web/Views/Home/Index.cshtml @@ -0,0 +1,35 @@ + +@using TagHelperSample.Web.Models +@model IEnumerable + +

    Index

    +

    + Create New +

    + +@if (Model != null && Model.Count() != 0) +{ +
    + @foreach (var item in Model) + { +
    +
    +
    +
    +
    +
    +
    +