From d9fe3058027e8edd951b4e5a6c6c86797d450edb Mon Sep 17 00:00:00 2001 From: Ryan Nowak Date: Wed, 8 Oct 2014 09:19:42 -0700 Subject: [PATCH] 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