diff --git a/.appveyor.yml b/.appveyor.yml index 711303fa0f..018881c604 100644 --- a/.appveyor.yml +++ b/.appveyor.yml @@ -2,7 +2,7 @@ init: - git config --global core.autocrlf true branches: only: - - dev + - master - /^release\/.*$/ - /^(.*\/)?ci-.*$/ build_script: diff --git a/.travis.yml b/.travis.yml index e75fe73221..d56301c453 100644 --- a/.travis.yml +++ b/.travis.yml @@ -12,7 +12,7 @@ os: osx_image: xcode8.2 branches: only: - - dev + - master - /^release\/.*$/ - /^(.*\/)?ci-.*$/ before_install: diff --git a/.vsts-pipelines/builds/ci-internal.yml b/.vsts-pipelines/builds/ci-internal.yml index dc7b8a3cb9..c2c5336fd0 100644 --- a/.vsts-pipelines/builds/ci-internal.yml +++ b/.vsts-pipelines/builds/ci-internal.yml @@ -7,7 +7,7 @@ resources: - repository: buildtools type: git name: aspnet-BuildTools - ref: refs/heads/release/2.2 + ref: refs/heads/master phases: - template: .vsts-pipelines/templates/project-ci.yml@buildtools diff --git a/.vsts-pipelines/builds/ci-public.yml b/.vsts-pipelines/builds/ci-public.yml index f5087d9c30..507c89b025 100644 --- a/.vsts-pipelines/builds/ci-public.yml +++ b/.vsts-pipelines/builds/ci-public.yml @@ -9,7 +9,7 @@ resources: type: github endpoint: DotNet-Bot GitHub Connection name: aspnet/BuildTools - ref: refs/heads/release/2.2 + ref: refs/heads/master phases: - template: .vsts-pipelines/templates/project-ci.yml@buildtools diff --git a/README.md b/README.md index 775ed6bd0c..babc35f1d9 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ ASP.NET Core MVC **Note: For ASP.NET MVC 5.x, Web API 2.x, and Web Pages 3.x (not ASP.NET Core), see https://github.com/aspnet/AspNetWebStack** -Travis: [![Travis](https://travis-ci.org/aspnet/Mvc.svg?branch=release/2.2)](https://travis-ci.org/aspnet/Mvc) +Travis: [![Travis](https://travis-ci.org/aspnet/Mvc.svg?branch=master)](https://travis-ci.org/aspnet/Mvc) ASP.NET Core MVC gives you a powerful, patterns-based way to build dynamic websites that enables a clean separation of concerns and gives you full control over markup for enjoyable, agile development. ASP.NET Core MVC includes many features that enable fast, TDD-friendly development for creating sophisticated applications that use the latest web standards. diff --git a/benchmarkapps/BasicApi/benchmarks.json b/benchmarkapps/BasicApi/benchmarks.json index aff5eb280d..2ebc74dde2 100644 --- a/benchmarkapps/BasicApi/benchmarks.json +++ b/benchmarkapps/BasicApi/benchmarks.json @@ -7,7 +7,7 @@ "PresetHeaders": "Json", "ReadyStateText": "Application started.", "Source": { - "BranchOrCommit": "release/2.2", + "BranchOrCommit": "dev", "Project": "benchmarkapps/BasicApi/BasicApi.csproj", "Repository": "https://github.com/aspnet/mvc.git" } @@ -19,20 +19,20 @@ }, "BasicApi.GetUsingQueryString": { "ClientProperties": { - "Scripts": "https://raw.githubusercontent.com/aspnet/Mvc/release/2.2/benchmarkapps/BasicApi/getWithToken.lua" + "Scripts": "https://raw.githubusercontent.com/aspnet/Mvc/dev/benchmarkapps/BasicApi/getWithToken.lua" }, "Path": "/pet/findByStatus", "Query": "?status=available" }, "BasicApi.GetUsingRouteValue": { "ClientProperties": { - "Scripts": "https://raw.githubusercontent.com/aspnet/Mvc/release/2.2/benchmarkapps/BasicApi/getWithToken.lua" + "Scripts": "https://raw.githubusercontent.com/aspnet/Mvc/dev/benchmarkapps/BasicApi/getWithToken.lua" }, "Path": "/pet/-1" }, "BasicApi.GetUsingRouteValueWithoutAuthorization": { "ClientProperties": { - "Scripts": "https://raw.githubusercontent.com/aspnet/Mvc/release/2.2/benchmarkapps/BasicApi/getWithToken.lua" + "Scripts": "https://raw.githubusercontent.com/aspnet/Mvc/dev/benchmarkapps/BasicApi/getWithToken.lua" }, "Path": "/pet/anonymous/-1" }, @@ -41,7 +41,7 @@ }, "BasicApi.Post": { "ClientProperties": { - "Scripts": "https://raw.githubusercontent.com/aspnet/Mvc/release/2.2/benchmarkapps/BasicApi/postJsonWithToken.lua" + "Scripts": "https://raw.githubusercontent.com/aspnet/Mvc/dev/benchmarkapps/BasicApi/postJsonWithToken.lua" }, "Path": "/pet" }, diff --git a/benchmarkapps/BasicViews/benchmarks.json b/benchmarkapps/BasicViews/benchmarks.json index 6a50d9386d..82e0812a21 100644 --- a/benchmarkapps/BasicViews/benchmarks.json +++ b/benchmarkapps/BasicViews/benchmarks.json @@ -7,7 +7,7 @@ "PresetHeaders": "Html", "ReadyStateText": "Application started.", "Source": { - "BranchOrCommit": "release/2.2", + "BranchOrCommit": "dev", "Project": "benchmarkapps/BasicViews/BasicViews.csproj", "Repository": "https://github.com/aspnet/mvc.git" } @@ -20,19 +20,19 @@ }, "BasicViews.Post": { "ClientProperties": { - "Scripts": "https://raw.githubusercontent.com/aspnet/Mvc/release/2.2/benchmarkapps/BasicViews/postWithToken.lua" + "Scripts": "https://raw.githubusercontent.com/aspnet/Mvc/dev/benchmarkapps/BasicViews/postWithToken.lua" }, "Path": "/Home/Index" }, "BasicViews.PostIgnoringToken": { "ClientProperties": { - "Scripts": "https://raw.githubusercontent.com/aspnet/Mvc/release/2.2/benchmarkapps/BasicViews/postWithToken.lua" + "Scripts": "https://raw.githubusercontent.com/aspnet/Mvc/dev/benchmarkapps/BasicViews/postWithToken.lua" }, "Path": "/Home/IndexWithoutToken" }, "BasicViews.PostWithoutToken": { "ClientProperties": { - "Scripts": "https://raw.githubusercontent.com/aspnet/Mvc/release/2.2/benchmarkapps/BasicViews/post.lua" + "Scripts": "https://raw.githubusercontent.com/aspnet/Mvc/dev/benchmarkapps/BasicViews/post.lua" }, "Path": "/Home/IndexWithoutToken" } diff --git a/build/dependencies.props b/build/dependencies.props index 063add026b..189943f524 100644 --- a/build/dependencies.props +++ b/build/dependencies.props @@ -16,97 +16,97 @@ 0.43.0 2.1.1.1 2.1.1 - 2.2.0-rtm-35519 - 2.2.0-rtm-35519 - 2.2.0-rtm-35519 - 2.2.0-rtm-35519 - 2.2.0-rtm-35519 - 2.2.0-rtm-35519 - 2.2.0-rtm-35519 - 2.2.0-rtm-35519 - 2.2.0-rtm-35519 - 2.2.0-rtm-35519 - 2.2.0-rtm-35519 - 2.2.0-rtm-35519 - 2.2.0-a-rtm-fix-wildcard-16567 - 2.2.0-rtm-35519 - 2.2.0-rtm-35519 + 3.0.0-alpha1-10654 + 3.0.0-alpha1-10654 + 3.0.0-alpha1-10654 + 3.0.0-alpha1-10654 + 3.0.0-alpha1-10654 + 3.0.0-alpha1-10654 + 3.0.0-alpha1-10654 + 3.0.0-alpha1-10654 + 3.0.0-alpha1-10654 + 3.0.0-alpha1-10654 + 3.0.0-alpha1-10654 + 3.0.0-alpha1-10654 + 3.0.0-a-alpha1-w-m-16569 + 3.0.0-alpha1-10654 + 3.0.0-alpha1-10654 2.0.0 - 2.2.0-rtm-35519 - 2.2.0-rtm-35519 - 2.2.0-rtm-35519 - 2.2.0-rtm-35519 - 2.2.0-rtm-35519 - 2.2.0-rtm-35519 - 2.2.0-rtm-35519 - 2.2.0-rtm-35519 - 2.2.0-rtm-35519 - 2.2.0-rtm-35519 - 2.2.0-rtm-35519 - 2.2.0-rtm-35519 - 2.2.0-rtm-35519 - 2.2.0-rtm-35519 - 2.2.0-rtm-35519 - 2.2.0-rtm-35519 - 2.2.0-a-rtm-allow-required-parameters-17081 - 2.2.0-a-rtm-allow-required-parameters-17081 - 2.2.0-rtm-35519 - 2.2.0-rtm-35519 - 2.2.0-rtm-35519 - 2.2.0-rtm-35519 - 2.2.0-rtm-35519 - 2.2.0-rtm-35519 - 2.2.0-rtm-35519 + 3.0.0-alpha1-10654 + 3.0.0-alpha1-10654 + 3.0.0-alpha1-10654 + 3.0.0-alpha1-10654 + 3.0.0-alpha1-10654 + 3.0.0-alpha1-10654 + 3.0.0-alpha1-10654 + 3.0.0-alpha1-10654 + 3.0.0-alpha1-10654 + 3.0.0-alpha1-10654 + 3.0.0-alpha1-10654 + 3.0.0-alpha1-10654 + 3.0.0-alpha1-10654 + 3.0.0-alpha1-10654 + 3.0.0-alpha1-10654 + 3.0.0-alpha1-10654 + 3.0.0-a-alpha1-master-builder-17095 + 3.0.0-a-alpha1-master-builder-17095 + 3.0.0-alpha1-10654 + 3.0.0-alpha1-10654 + 3.0.0-alpha1-10654 + 3.0.0-alpha1-10654 + 3.0.0-alpha1-10654 + 3.0.0-alpha1-10654 + 3.0.0-alpha1-10654 5.2.6 - 15.6.82 + 15.8.166 2.8.0 2.8.0 - 2.2.0-rtm-35519 + 3.0.0-alpha1-10654 1.7.0 - 2.2.0-rtm-35519 - 2.2.0-rtm-35519 - 2.2.0-rtm-35519 - 2.2.0-rtm-35519 - 2.2.0-rtm-35519 - 2.2.0-rtm-35519 - 2.2.0-rtm-35519 - 2.1.0 - 2.2.0-rtm-35519 - 2.2.0-rtm-35519 - 2.2.0-rtm-35519 - 2.2.0-rtm-35519 - 2.2.0-rtm-35519 - 2.2.0-rtm-35519 - 2.2.0-rtm-35519 - 2.2.0-rtm-35519 - 2.2.0-rtm-35519 - 2.2.0-rtm-35519 - 2.2.0-rtm-35519 - 2.2.0-rtm-35519 - 2.2.0-rtm-35519 - 2.2.0-rtm-35519 - 2.2.0-rtm-35519 - 2.2.0-rtm-35519 - 2.2.0-rtm-35519 - 2.2.0-rtm-35519 - 2.2.0-rtm-35519 - 2.2.0-rtm-35519 - 2.2.0-rtm-35519 - 2.2.0-rtm-35519 + 3.0.0-alpha1-10654 + 3.0.0-alpha1-10654 + 3.0.0-alpha1-10654 + 3.0.0-alpha1-10654 + 3.0.0-alpha1-10654 + 3.0.0-alpha1-10654 + 3.0.0-alpha1-10654 + 3.0.0-preview1-26907-05 + 3.0.0-alpha1-10654 + 3.0.0-alpha1-10654 + 3.0.0-alpha1-10654 + 3.0.0-alpha1-10654 + 3.0.0-alpha1-10654 + 3.0.0-alpha1-10654 + 3.0.0-alpha1-10654 + 3.0.0-alpha1-10654 + 3.0.0-alpha1-10654 + 3.0.0-alpha1-10654 + 3.0.0-alpha1-10654 + 3.0.0-alpha1-10654 + 3.0.0-alpha1-10654 + 3.0.0-alpha1-10654 + 3.0.0-alpha1-10654 + 3.0.0-alpha1-10654 + 3.0.0-alpha1-10654 + 3.0.0-alpha1-10654 + 3.0.0-alpha1-10654 + 3.0.0-alpha1-10654 + 3.0.0-alpha1-10654 + 3.0.0-alpha1-10654 2.0.9 2.1.3 2.2.0-preview3-27014-02 - 2.2.0-rtm-35519 - 2.2.0-rtm-35519 + 3.0.0-alpha1-10654 + 3.0.0-alpha1-10654 15.6.1 4.10.0 2.0.3 1.0.1 11.0.2 - 4.5.0 - 4.5.0 + 4.6.0-preview1-26907-04 + 4.6.0-preview1-26907-04 4.3.2 - 4.5.1 + 4.6.0-preview1-26907-04 0.10.0 2.3.1 2.4.0 diff --git a/build/repo.props b/build/repo.props index 3bd17f1b84..d1a3039193 100644 --- a/build/repo.props +++ b/build/repo.props @@ -3,6 +3,7 @@ true + true @@ -14,7 +15,6 @@ Internal.AspNetCore.Universe.Lineup - 2.2.0-* https://dotnet.myget.org/F/aspnetcore-dev/api/v3/index.json diff --git a/global.json b/global.json index b5a5299b58..ffa7db4c53 100644 --- a/global.json +++ b/global.json @@ -3,6 +3,6 @@ "version": "2.2.100-preview2-009404" }, "msbuild-sdks": { - "Internal.AspNetCore.Sdk": "2.2.0-preview2-20181003.2" + "Internal.AspNetCore.Sdk": "3.0.0-alpha1-20181011.11" } } diff --git a/korebuild-lock.txt b/korebuild-lock.txt index 783131ae5e..27b605e3c6 100644 --- a/korebuild-lock.txt +++ b/korebuild-lock.txt @@ -1,2 +1,2 @@ -version:2.2.0-preview2-20181003.2 -commithash:41935e62d7853060283c801f49992e2c73a95927 +version:3.0.0-alpha1-20181011.11 +commithash:f57aa8ddda0abdd74ada55853587bedb4f364065 diff --git a/korebuild.json b/korebuild.json index d217d06e3e..8a276a7f35 100644 --- a/korebuild.json +++ b/korebuild.json @@ -1,4 +1,4 @@ { - "$schema": "https://raw.githubusercontent.com/aspnet/BuildTools/release/2.2/tools/korebuild.schema.json", - "channel": "release/2.2" + "$schema": "https://raw.githubusercontent.com/aspnet/BuildTools/master/tools/korebuild.schema.json", + "channel": "master" } diff --git a/samples/MvcSandbox/AuthorizationMiddleware/AuthorizationAppBuilderExtensions.cs b/samples/MvcSandbox/AuthorizationMiddleware/AuthorizationAppBuilderExtensions.cs new file mode 100644 index 0000000000..6a2fa48911 --- /dev/null +++ b/samples/MvcSandbox/AuthorizationMiddleware/AuthorizationAppBuilderExtensions.cs @@ -0,0 +1,21 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using MvcSandbox.AuthorizationMiddleware; + +namespace Microsoft.AspNetCore.Builder +{ + public static class AuthorizationAppBuilderExtensions + { + public static IApplicationBuilder UseAuthorization(this IApplicationBuilder app) + { + if (app == null) + { + throw new ArgumentNullException(nameof(app)); + } + + return app.UseMiddleware(); + } + } +} \ No newline at end of file diff --git a/samples/MvcSandbox/AuthorizationMiddleware/AuthorizationEndpointConventionBuilder.cs b/samples/MvcSandbox/AuthorizationMiddleware/AuthorizationEndpointConventionBuilder.cs new file mode 100644 index 0000000000..5fb990e1f3 --- /dev/null +++ b/samples/MvcSandbox/AuthorizationMiddleware/AuthorizationEndpointConventionBuilder.cs @@ -0,0 +1,16 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using Microsoft.AspNetCore.Routing; + +namespace MvcSandbox.AuthorizationMiddleware +{ + public static class AuthorizationEndpointConventionBuilder + { + public static T RequireAuthorization(this T builder, params string[] roles) where T : IEndpointConventionBuilder + { + builder.Apply(model => model.Metadata.Add(new AuthorizeMetadataAttribute(roles))); + return builder; + } + } +} diff --git a/samples/MvcSandbox/AuthorizationMiddleware/AuthorizationMiddleware.cs b/samples/MvcSandbox/AuthorizationMiddleware/AuthorizationMiddleware.cs new file mode 100644 index 0000000000..23a5e45f3f --- /dev/null +++ b/samples/MvcSandbox/AuthorizationMiddleware/AuthorizationMiddleware.cs @@ -0,0 +1,52 @@ +// Copyright (c) .NET Foundation. 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.Threading.Tasks; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.Features; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Primitives; + +namespace MvcSandbox.AuthorizationMiddleware +{ + public class AuthorizationMiddleware + { + private readonly RequestDelegate _next; + + public AuthorizationMiddleware(RequestDelegate next) + { + if (next == null) + { + throw new ArgumentNullException(nameof(next)); + } + + _next = next; + } + + public async Task Invoke(HttpContext httpContext) + { + var endpoint = httpContext.Features.Get()?.Endpoint; + var metadata = endpoint?.Metadata?.GetMetadata(); + + // Only run authorization if endpoint has metadata + if (metadata != null) + { + // Check if role querystring value is a valid role + if (!httpContext.Request.Query.TryGetValue("role", out var role) || + !metadata.Roles.Contains(role.ToString(), StringComparer.OrdinalIgnoreCase)) + { + httpContext.Response.StatusCode = 401; + httpContext.Response.ContentType = "text/plain"; + await httpContext.Response.WriteAsync($"Unauthorized access to '{endpoint.DisplayName}'."); + return; + } + } + + await _next(httpContext); + } + } +} \ No newline at end of file diff --git a/samples/MvcSandbox/AuthorizationMiddleware/AuthorizeMetadataAttribute.cs b/samples/MvcSandbox/AuthorizationMiddleware/AuthorizeMetadataAttribute.cs new file mode 100644 index 0000000000..7c95327d0d --- /dev/null +++ b/samples/MvcSandbox/AuthorizationMiddleware/AuthorizeMetadataAttribute.cs @@ -0,0 +1,20 @@ +// Copyright (c) .NET Foundation. 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.Threading.Tasks; + +namespace MvcSandbox.AuthorizationMiddleware +{ + public class AuthorizeMetadataAttribute : Attribute + { + public AuthorizeMetadataAttribute(string[] roles) + { + Roles = roles; + } + + public string[] Roles { get; } + } +} diff --git a/samples/MvcSandbox/Controllers/HomeController.cs b/samples/MvcSandbox/Controllers/HomeController.cs index 2aa4ff6829..87406bce53 100644 --- a/samples/MvcSandbox/Controllers/HomeController.cs +++ b/samples/MvcSandbox/Controllers/HomeController.cs @@ -2,6 +2,7 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using Microsoft.AspNetCore.Mvc; +using MvcSandbox.AuthorizationMiddleware; namespace MvcSandbox.Controllers { diff --git a/samples/MvcSandbox/Controllers/LoginController.cs b/samples/MvcSandbox/Controllers/LoginController.cs new file mode 100644 index 0000000000..5110850fbf --- /dev/null +++ b/samples/MvcSandbox/Controllers/LoginController.cs @@ -0,0 +1,16 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using Microsoft.AspNetCore.Mvc; + +namespace MvcSandbox.Controllers +{ + [Route("[controller]/[action]")] + public class LoginController : Controller + { + public IActionResult Index() + { + return View(); + } + } +} diff --git a/samples/MvcSandbox/HealthChecks/HealthChecksEndpointRouteBuilderExtensions.cs b/samples/MvcSandbox/HealthChecks/HealthChecksEndpointRouteBuilderExtensions.cs new file mode 100644 index 0000000000..b89acdd2b4 --- /dev/null +++ b/samples/MvcSandbox/HealthChecks/HealthChecksEndpointRouteBuilderExtensions.cs @@ -0,0 +1,28 @@ +// Copyright (c) .NET Foundation. 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.Threading.Tasks; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Routing; + +namespace Microsoft.AspNetCore.Builder +{ + public static class HealthChecksEndpointRouteBuilderExtensions + { + private static readonly Random _random = new Random(); + + public static IEndpointConventionBuilder MapHealthChecks(this IEndpointRouteBuilder builder, string pattern) + { + return builder.MapGet( + pattern, + async httpContext => + { + await httpContext.Response.WriteAsync(_random.Next() % 2 == 0 ? "Up!" : "Down!"); + }); + } + } +} diff --git a/samples/MvcSandbox/Startup.cs b/samples/MvcSandbox/Startup.cs index d9f96bf08b..12c1291669 100644 --- a/samples/MvcSandbox/Startup.cs +++ b/samples/MvcSandbox/Startup.cs @@ -1,11 +1,18 @@ // Copyright (c) .NET Foundation. 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.Linq; +using System.Text; +using System.Threading.Tasks; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Routing; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; +using MvcSandbox.AuthorizationMiddleware; namespace MvcSandbox { @@ -20,14 +27,46 @@ namespace MvcSandbox // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. public void Configure(IApplicationBuilder app) { - app.UseDeveloperExceptionPage(); - app.UseStaticFiles(); - app.UseMvc(routes => + app.UseEndpointRouting(builder => { - routes.MapRoute( + builder.MapGet( + requestDelegate: WriteEndpoints, + pattern: "/endpoints", + displayName: "Home"); + + builder.MapMvcRoute( name: "default", template: "{controller=Home}/{action=Index}/{id?}"); + + builder.MapMvcControllers(); + builder.MapRazorPages(); + + builder.MapHealthChecks("/healthz"); }); + + app.UseDeveloperExceptionPage(); + app.UseStaticFiles(); + + app.UseAuthorization(); + + app.UseEndpoint(); + } + + private static Task WriteEndpoints(HttpContext httpContext) + { + var dataSource = httpContext.RequestServices.GetRequiredService(); + + var sb = new StringBuilder(); + sb.AppendLine("Endpoints:"); + foreach (var endpoint in dataSource.Endpoints.OfType().OrderBy(e => e.RoutePattern.RawText, StringComparer.OrdinalIgnoreCase)) + { + sb.AppendLine($"- {endpoint.RoutePattern.RawText} '{endpoint.DisplayName}'"); + } + + var response = httpContext.Response; + response.StatusCode = 200; + response.ContentType = "text/plain"; + return response.WriteAsync(sb.ToString()); } public static void Main(string[] args) diff --git a/src/Microsoft.AspNetCore.Mvc.Core/Builder/DefaultEndpointConventionBuilder.cs b/src/Microsoft.AspNetCore.Mvc.Core/Builder/DefaultEndpointConventionBuilder.cs new file mode 100644 index 0000000000..8427ba5c3b --- /dev/null +++ b/src/Microsoft.AspNetCore.Mvc.Core/Builder/DefaultEndpointConventionBuilder.cs @@ -0,0 +1,24 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using Microsoft.AspNetCore.Routing; + +namespace Microsoft.AspNetCore.Builder +{ + internal class DefaultEndpointConventionBuilder : IEndpointConventionBuilder + { + public DefaultEndpointConventionBuilder() + { + Conventions = new List>(); + } + + public List> Conventions { get; } + + public void Apply(Action convention) + { + Conventions.Add(convention); + } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Mvc.Core/Builder/MvcApplicationBuilderExtensions.cs b/src/Microsoft.AspNetCore.Mvc.Core/Builder/MvcApplicationBuilderExtensions.cs index 06e26ed407..6cde8eccd5 100644 --- a/src/Microsoft.AspNetCore.Mvc.Core/Builder/MvcApplicationBuilderExtensions.cs +++ b/src/Microsoft.AspNetCore.Mvc.Core/Builder/MvcApplicationBuilderExtensions.cs @@ -90,9 +90,7 @@ namespace Microsoft.AspNetCore.Builder if (options.Value.EnableEndpointRouting) { var mvcEndpointDataSource = app.ApplicationServices - .GetRequiredService>() - .OfType() - .First(); + .GetRequiredService(); var parameterPolicyFactory = app.ApplicationServices .GetRequiredService(); @@ -122,11 +120,21 @@ namespace Microsoft.AspNetCore.Builder } } + // Include all controllers with attribute routing and Razor pages + var defaultEndpointConventionBuilder = new DefaultEndpointConventionBuilder(); + mvcEndpointDataSource.AttributeRoutingConventionResolvers.Add((actionDescriptor) => + { + return defaultEndpointConventionBuilder; + }); + if (!app.Properties.TryGetValue(EndpointRoutingRegisteredKey, out _)) { // Matching middleware has not been registered yet // For back-compat register middleware so an endpoint is matched and then immediately used - app.UseEndpointRouting(); + app.UseEndpointRouting(routerBuilder => + { + routerBuilder.DataSources.Add(mvcEndpointDataSource); + }); } return app.UseEndpoint(); diff --git a/src/Microsoft.AspNetCore.Mvc.Core/Builder/MvcEndpointInfo.cs b/src/Microsoft.AspNetCore.Mvc.Core/Builder/MvcEndpointInfo.cs index 161189b871..289a1a2f34 100644 --- a/src/Microsoft.AspNetCore.Mvc.Core/Builder/MvcEndpointInfo.cs +++ b/src/Microsoft.AspNetCore.Mvc.Core/Builder/MvcEndpointInfo.cs @@ -9,7 +9,7 @@ using Microsoft.AspNetCore.Routing.Patterns; namespace Microsoft.AspNetCore.Builder { - internal class MvcEndpointInfo + internal class MvcEndpointInfo : DefaultEndpointConventionBuilder { public MvcEndpointInfo( string name, @@ -43,6 +43,7 @@ namespace Microsoft.AspNetCore.Builder public string Name { get; } public string Pattern { get; } + public Type ControllerType { get; set; } // Non-inline defaults public RouteValueDictionary Defaults { get; } diff --git a/src/Microsoft.AspNetCore.Mvc.Core/Builder/MvcEndpointRouteBuilderExtensions.cs b/src/Microsoft.AspNetCore.Mvc.Core/Builder/MvcEndpointRouteBuilderExtensions.cs new file mode 100644 index 0000000000..a3bd2d9951 --- /dev/null +++ b/src/Microsoft.AspNetCore.Mvc.Core/Builder/MvcEndpointRouteBuilderExtensions.cs @@ -0,0 +1,148 @@ +// Copyright (c) .NET Foundation. 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.Text; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Controllers; +using Microsoft.AspNetCore.Mvc.Internal; +using Microsoft.AspNetCore.Routing; +using Microsoft.Extensions.DependencyInjection; + +namespace Microsoft.AspNetCore.Builder +{ + public static class MvcEndpointRouteBuilderExtensions + { + public static IEndpointConventionBuilder MapMvcControllers( + this IEndpointRouteBuilder routeBuilder) + { + return MapMvcControllers(routeBuilder); + } + + public static IEndpointConventionBuilder MapMvcControllers( + this IEndpointRouteBuilder routeBuilder) where TController : ControllerBase + { + var mvcEndpointDataSource = routeBuilder.DataSources.OfType().FirstOrDefault(); + + if (mvcEndpointDataSource == null) + { + mvcEndpointDataSource = routeBuilder.ServiceProvider.GetRequiredService(); + routeBuilder.DataSources.Add(mvcEndpointDataSource); + } + + var conventionBuilder = new DefaultEndpointConventionBuilder(); + + mvcEndpointDataSource.AttributeRoutingConventionResolvers.Add(actionDescriptor => + { + if (actionDescriptor is ControllerActionDescriptor controllerActionDescriptor && + typeof(TController).IsAssignableFrom(controllerActionDescriptor.ControllerTypeInfo)) + { + return conventionBuilder; + } + + return null; + }); + + return conventionBuilder; + } + + public static IEndpointConventionBuilder MapMvcRoute( + this IEndpointRouteBuilder routeBuilder, + string name, + string template) + { + return MapMvcRoute(routeBuilder, name, template, defaults: null); + } + + public static IEndpointConventionBuilder MapMvcRoute( + this IEndpointRouteBuilder routeBuilder, + string name, + string template, + object defaults) + { + return MapMvcRoute(routeBuilder, name, template, defaults, constraints: null); + } + + public static IEndpointConventionBuilder MapMvcRoute( + this IEndpointRouteBuilder routeBuilder, + string name, + string template, + object defaults, + object constraints) + { + return MapMvcRoute(routeBuilder, name, template, defaults, constraints, dataTokens: null); + } + + public static IEndpointConventionBuilder MapMvcRoute( + this IEndpointRouteBuilder routeBuilder, + string name, + string template, + object defaults, + object constraints, + object dataTokens) + { + return MapMvcRoute(routeBuilder, name, template, defaults, constraints, dataTokens); + } + + public static IEndpointConventionBuilder MapMvcRoute( + this IEndpointRouteBuilder routeBuilder, + string name, + string template) where TController : ControllerBase + { + return MapMvcRoute(routeBuilder, name, template, defaults: null); + } + + public static IEndpointConventionBuilder MapMvcRoute( + this IEndpointRouteBuilder routeBuilder, + string name, + string template, + object defaults) where TController : ControllerBase + { + return MapMvcRoute(routeBuilder, name, template, defaults, constraints: null); + } + + public static IEndpointConventionBuilder MapMvcRoute( + this IEndpointRouteBuilder routeBuilder, + string name, + string template, + object defaults, + object constraints) where TController : ControllerBase + { + return MapMvcRoute(routeBuilder, name, template, defaults, constraints, dataTokens: null); + } + + public static IEndpointConventionBuilder MapMvcRoute( + this IEndpointRouteBuilder routeBuilder, + string name, + string template, + object defaults, + object constraints, + object dataTokens) where TController : ControllerBase + { + var mvcEndpointDataSource = routeBuilder.DataSources.OfType().FirstOrDefault(); + + if (mvcEndpointDataSource == null) + { + mvcEndpointDataSource = routeBuilder.ServiceProvider.GetRequiredService(); + routeBuilder.DataSources.Add(mvcEndpointDataSource); + } + + var endpointInfo = new MvcEndpointInfo( + name, + template, + new RouteValueDictionary(defaults), + new RouteValueDictionary(constraints), + new RouteValueDictionary(dataTokens), + routeBuilder.ServiceProvider.GetRequiredService()); + + endpointInfo.ControllerType = typeof(TController); + + mvcEndpointDataSource.ConventionalEndpointInfos.Add(endpointInfo); + + return endpointInfo; + } + } +} diff --git a/src/Microsoft.AspNetCore.Mvc.Core/DependencyInjection/MvcCoreServiceCollectionExtensions.cs b/src/Microsoft.AspNetCore.Mvc.Core/DependencyInjection/MvcCoreServiceCollectionExtensions.cs index 95a4afb21b..d645322384 100644 --- a/src/Microsoft.AspNetCore.Mvc.Core/DependencyInjection/MvcCoreServiceCollectionExtensions.cs +++ b/src/Microsoft.AspNetCore.Mvc.Core/DependencyInjection/MvcCoreServiceCollectionExtensions.cs @@ -270,8 +270,7 @@ namespace Microsoft.Extensions.DependencyInjection // // Endpoint Routing / Endpoints // - services.TryAddEnumerable( - ServiceDescriptor.Singleton()); + services.TryAddSingleton(); services.TryAddSingleton(); // diff --git a/src/Microsoft.AspNetCore.Mvc.Core/Internal/MvcEndpointDataSource.cs b/src/Microsoft.AspNetCore.Mvc.Core/Internal/MvcEndpointDataSource.cs index df110bee25..8f4612672b 100644 --- a/src/Microsoft.AspNetCore.Mvc.Core/Internal/MvcEndpointDataSource.cs +++ b/src/Microsoft.AspNetCore.Mvc.Core/Internal/MvcEndpointDataSource.cs @@ -10,6 +10,7 @@ using System.Threading; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc.Abstractions; +using Microsoft.AspNetCore.Mvc.Controllers; using Microsoft.AspNetCore.Mvc.Infrastructure; using Microsoft.AspNetCore.Mvc.Routing; using Microsoft.AspNetCore.Routing; @@ -57,6 +58,7 @@ namespace Microsoft.AspNetCore.Mvc.Internal _parameterPolicyFactory = parameterPolicyFactory; ConventionalEndpointInfos = new List(); + AttributeRoutingConventionResolvers = new List>(); // IMPORTANT: this needs to be the last thing we do in the constructor. Change notifications can happen immediately! // @@ -72,6 +74,8 @@ namespace Microsoft.AspNetCore.Mvc.Internal public List ConventionalEndpointInfos { get; } + public List> AttributeRoutingConventionResolvers { get; } + public override IReadOnlyList Endpoints { get @@ -134,6 +138,16 @@ namespace Microsoft.AspNetCore.Mvc.Internal // - Home/Login foreach (var endpointInfo in ConventionalEndpointInfos) { + if (endpointInfo.ControllerType != null && + endpointInfo.ControllerType != typeof(ControllerBase)) + { + if (!ValidateControllerConstraint(action, endpointInfo)) + { + // Action descriptor does not belong to a controller of the specified type + continue; + } + } + // An 'endpointInfo' is applicable if: // 1. it has a parameter (or default value) for 'required' non-null route value // 2. it does not have a parameter (or default value) for 'required' null route value @@ -164,11 +178,20 @@ namespace Microsoft.AspNetCore.Mvc.Internal endpointInfo.DataTokens, endpointInfo.ParameterPolicies, suppressLinkGeneration: false, - suppressPathMatching: false); + suppressPathMatching: false, + endpointInfo.Conventions); } } else { + var conventionBuilder = ResolveActionConventionBuilder(action); + if (conventionBuilder == null) + { + // No convention builder for this action + // Do not create an endpoint for it + continue; + } + var attributeRoutePattern = RoutePatternFactory.Parse(action.AttributeRouteInfo.Template); CreateEndpoints( @@ -183,7 +206,8 @@ namespace Microsoft.AspNetCore.Mvc.Internal dataTokens: null, allParameterPolicies: null, action.AttributeRouteInfo.SuppressLinkGeneration, - action.AttributeRouteInfo.SuppressPathMatching); + action.AttributeRouteInfo.SuppressPathMatching, + conventionBuilder.Conventions); } } @@ -205,6 +229,30 @@ namespace Microsoft.AspNetCore.Mvc.Internal } } + private DefaultEndpointConventionBuilder ResolveActionConventionBuilder(ActionDescriptor action) + { + foreach (var filter in AttributeRoutingConventionResolvers) + { + var conventionBuilder = filter(action); + if (conventionBuilder != null) + { + return conventionBuilder; + } + } + + return null; + } + + private static bool ValidateControllerConstraint(ActionDescriptor action, MvcEndpointInfo endpointInfo) + { + if (action is ControllerActionDescriptor controllerActionDescriptor) + { + return endpointInfo.ControllerType.IsAssignableFrom(controllerActionDescriptor.ControllerTypeInfo); + } + + return false; + } + // CreateEndpoints processes the route pattern, replacing area/controller/action parameters with endpoint values // Because of default values it is possible for a route pattern to resolve to multiple endpoints private int CreateEndpoints( @@ -219,7 +267,8 @@ namespace Microsoft.AspNetCore.Mvc.Internal RouteValueDictionary dataTokens, IDictionary> allParameterPolicies, bool suppressLinkGeneration, - bool suppressPathMatching) + bool suppressPathMatching, + List> conventions) { var newPathSegments = routePattern.PathSegments.ToList(); var hasLinkGenerationEndpoint = false; @@ -264,7 +313,8 @@ namespace Microsoft.AspNetCore.Mvc.Internal routeOrder++, dataTokens, suppressLinkGeneration, - true); + true, + conventions); endpoints.Add(ep); hasLinkGenerationEndpoint = true; @@ -282,7 +332,8 @@ namespace Microsoft.AspNetCore.Mvc.Internal routeOrder++, dataTokens, suppressLinkGeneration, - suppressPathMatching); + suppressPathMatching, + conventions); endpoints.Add(subEndpoint); } @@ -299,7 +350,8 @@ namespace Microsoft.AspNetCore.Mvc.Internal routeOrder++, dataTokens, suppressLinkGeneration, - suppressPathMatching); + suppressPathMatching, + conventions); endpoints.Add(finalEndpoint); return routeOrder; @@ -406,6 +458,7 @@ namespace Microsoft.AspNetCore.Mvc.Internal { newPathSegments[i] = RoutePatternFactory.Segment(segmentParts); } + } private bool UseDefaultValuePlusRemainingSegmentsOptional( @@ -536,7 +589,8 @@ namespace Microsoft.AspNetCore.Mvc.Internal int order, RouteValueDictionary dataTokens, bool suppressLinkGeneration, - bool suppressPathMatching) + bool suppressPathMatching, + List> conventions) { RequestDelegate requestDelegate = (context) => { @@ -551,7 +605,10 @@ namespace Microsoft.AspNetCore.Mvc.Internal var defaults = new RouteValueDictionary(nonInlineDefaults); EnsureRequiredValuesInDefaults(actionRouteValues, defaults, segments); - var metadataCollection = BuildEndpointMetadata( + var model = new RouteEndpointModel(requestDelegate, RoutePatternFactory.Pattern(patternRawText, defaults, parameterPolicies: null, segments), order); + + AddEndpointMetadata( + model.Metadata, action, routeName, new RouteValueDictionary(actionRouteValues), @@ -559,17 +616,23 @@ namespace Microsoft.AspNetCore.Mvc.Internal suppressLinkGeneration, suppressPathMatching); - var endpoint = new RouteEndpoint( - requestDelegate, - RoutePatternFactory.Pattern(patternRawText, defaults, parameterPolicies: null, segments), - order, - metadataCollection, - action.DisplayName); + model.DisplayName = action.DisplayName; - return endpoint; + // REVIEW: When should conventions be run + // Metadata should have lower precedence that data source metadata + if (conventions != null) + { + foreach (var convention in conventions) + { + convention(model); + } + } + + return (RouteEndpoint)model.Build(); } - private static EndpointMetadataCollection BuildEndpointMetadata( + private static void AddEndpointMetadata( + IList metadata, ActionDescriptor action, string routeName, RouteValueDictionary requiredValues, @@ -577,14 +640,15 @@ namespace Microsoft.AspNetCore.Mvc.Internal bool suppressLinkGeneration, bool suppressPathMatching) { - var metadata = new List(); - // Add action metadata first so it has a low precedence if (action.EndpointMetadata != null) { - metadata.AddRange(action.EndpointMetadata); + foreach (var d in action.EndpointMetadata) + { + metadata.Add(d); + } } - + metadata.Add(action); if (dataTokens != null) @@ -597,8 +661,10 @@ namespace Microsoft.AspNetCore.Mvc.Internal // Add filter descriptors to endpoint metadata if (action.FilterDescriptors != null && action.FilterDescriptors.Count > 0) { - metadata.AddRange(action.FilterDescriptors.OrderBy(f => f, FilterDescriptorOrderComparer.Comparer) - .Select(f => f.Filter)); + foreach (var filter in action.FilterDescriptors.OrderBy(f => f, FilterDescriptorOrderComparer.Comparer).Select(f => f.Filter)) + { + metadata.Add(filter); + } } if (action.ActionConstraints != null && action.ActionConstraints.Count > 0) @@ -637,9 +703,6 @@ namespace Microsoft.AspNetCore.Mvc.Internal { metadata.Add(new SuppressMatchingMetadata()); } - - var metadataCollection = new EndpointMetadataCollection(metadata); - return metadataCollection; } // Ensure route values are a subset of defaults diff --git a/src/Microsoft.AspNetCore.Mvc.RazorPages/Builder/RazorPagesEndpointRouteBuilderExtensions.cs b/src/Microsoft.AspNetCore.Mvc.RazorPages/Builder/RazorPagesEndpointRouteBuilderExtensions.cs new file mode 100644 index 0000000000..bb69573b7e --- /dev/null +++ b/src/Microsoft.AspNetCore.Mvc.RazorPages/Builder/RazorPagesEndpointRouteBuilderExtensions.cs @@ -0,0 +1,47 @@ +// Copyright (c) .NET Foundation. 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.Text; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Internal; +using Microsoft.AspNetCore.Mvc.RazorPages; +using Microsoft.AspNetCore.Routing; +using Microsoft.Extensions.DependencyInjection; + +namespace Microsoft.AspNetCore.Builder +{ + public static class RazorPagesEndpointRouteBuilderExtensions + { + public static IEndpointConventionBuilder MapRazorPages( + this IEndpointRouteBuilder routeBuilder, + string basePath = null) + { + var mvcEndpointDataSource = routeBuilder.DataSources.OfType().FirstOrDefault(); + + if (mvcEndpointDataSource == null) + { + mvcEndpointDataSource = routeBuilder.ServiceProvider.GetRequiredService(); + routeBuilder.DataSources.Add(mvcEndpointDataSource); + } + + var conventionBuilder = new DefaultEndpointConventionBuilder(); + + mvcEndpointDataSource.AttributeRoutingConventionResolvers.Add(actionDescriptor => + { + if (actionDescriptor is PageActionDescriptor pageActionDescriptor) + { + // TODO: Filter pages by path + return conventionBuilder; + } + + return null; + }); + + return conventionBuilder; + } + } +} diff --git a/test/Microsoft.AspNetCore.Mvc.Core.Test/Builder/MvcApplicationBuilderExtensionsTest.cs b/test/Microsoft.AspNetCore.Mvc.Core.Test/Builder/MvcApplicationBuilderExtensionsTest.cs index 3bd2cfa1f6..1d69fd9d5e 100644 --- a/test/Microsoft.AspNetCore.Mvc.Core.Test/Builder/MvcApplicationBuilderExtensionsTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.Core.Test/Builder/MvcApplicationBuilderExtensionsTest.cs @@ -10,6 +10,7 @@ using Microsoft.AspNetCore.Builder.Internal; using Microsoft.AspNetCore.Mvc.Internal; using Microsoft.AspNetCore.Routing; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; using Moq; using Xunit; @@ -56,12 +57,10 @@ namespace Microsoft.AspNetCore.Mvc.Core.Builder template: "{controller=Home}/{action=Index}/{id?}"); }); - var mvcEndpointDataSource = appBuilder.ApplicationServices - .GetRequiredService>() - .OfType() - .First(); + var routeOptions = appBuilder.ApplicationServices + .GetRequiredService>(); - Assert.Empty(mvcEndpointDataSource.ConventionalEndpointInfos); + Assert.Empty(routeOptions.Value.EndpointDataSources); } [Fact] @@ -83,10 +82,10 @@ namespace Microsoft.AspNetCore.Mvc.Core.Builder template: "{controller=Home}/{action=Index}/{id?}"); }); - var mvcEndpointDataSource = appBuilder.ApplicationServices - .GetRequiredService>() - .OfType() - .First(); + var routeOptions = appBuilder.ApplicationServices + .GetRequiredService>(); + + var mvcEndpointDataSource = (MvcEndpointDataSource)Assert.Single(routeOptions.Value.EndpointDataSources, ds => ds is MvcEndpointDataSource); var endpointInfo = Assert.Single(mvcEndpointDataSource.ConventionalEndpointInfos); Assert.Equal("default", endpointInfo.Name); diff --git a/test/Microsoft.AspNetCore.Mvc.Core.Test/DependencyInjection/MvcCoreServiceCollectionExtensionsTest.cs b/test/Microsoft.AspNetCore.Mvc.Core.Test/DependencyInjection/MvcCoreServiceCollectionExtensionsTest.cs index e4e56bebd2..ee3b28f076 100644 --- a/test/Microsoft.AspNetCore.Mvc.Core.Test/DependencyInjection/MvcCoreServiceCollectionExtensionsTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.Core.Test/DependencyInjection/MvcCoreServiceCollectionExtensionsTest.cs @@ -315,13 +315,6 @@ namespace Microsoft.AspNetCore.Mvc typeof(ApiBehaviorApplicationModelProvider), } }, - { - typeof(EndpointDataSource), - new Type[] - { - typeof(MvcEndpointDataSource), - } - }, { typeof(IStartupFilter), new Type[] diff --git a/test/Microsoft.AspNetCore.Mvc.Core.Test/Internal/MvcEndpointDataSourceTests.cs b/test/Microsoft.AspNetCore.Mvc.Core.Test/Internal/MvcEndpointDataSourceTests.cs index a3e0ebe5c3..d354543cc6 100644 --- a/test/Microsoft.AspNetCore.Mvc.Core.Test/Internal/MvcEndpointDataSourceTests.cs +++ b/test/Microsoft.AspNetCore.Mvc.Core.Test/Internal/MvcEndpointDataSourceTests.cs @@ -1359,6 +1359,12 @@ namespace Microsoft.AspNetCore.Mvc.Internal mvcEndpointInvokerFactory ?? new MvcEndpointInvokerFactory(new ActionInvokerFactory(Array.Empty())), serviceProvider.GetRequiredService()); + var defaultEndpointConventionBuilder = new DefaultEndpointConventionBuilder(); + dataSource.AttributeRoutingConventionResolvers.Add((actionDescriptor) => + { + return defaultEndpointConventionBuilder; + }); + return dataSource; } diff --git a/test/Microsoft.AspNetCore.Mvc.FunctionalTests/RoutingTestsBase.cs b/test/Microsoft.AspNetCore.Mvc.FunctionalTests/RoutingTestsBase.cs index a58e745b47..c2a0455553 100644 --- a/test/Microsoft.AspNetCore.Mvc.FunctionalTests/RoutingTestsBase.cs +++ b/test/Microsoft.AspNetCore.Mvc.FunctionalTests/RoutingTestsBase.cs @@ -1457,6 +1457,24 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests Assert.Equal(actionName, result.Action); } + [Fact] + public async Task RazorPage_WithLinks_GeneratesLinksCorrectly() + { + // Arrange & Act + var response = await Client.GetAsync("http://localhost/PageWithLinks"); + + // Assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + + var document = await response.GetHtmlDocumentAsync(); + + var editLink = document.RequiredQuerySelector("#editlink"); + Assert.Equal("/Edit/10", editLink.GetAttribute("href")); + + var contactLink = document.RequiredQuerySelector("#contactlink"); + Assert.Equal("/Home/Contact", contactLink.GetAttribute("href")); + } + [Fact] public async Task CanRunMiddlewareAfterRouting() { diff --git a/test/Microsoft.AspNetCore.Mvc.Test/MvcServiceCollectionExtensionsTest.cs b/test/Microsoft.AspNetCore.Mvc.Test/MvcServiceCollectionExtensionsTest.cs index 70fea5804a..4c1bfc7e78 100644 --- a/test/Microsoft.AspNetCore.Mvc.Test/MvcServiceCollectionExtensionsTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.Test/MvcServiceCollectionExtensionsTest.cs @@ -353,6 +353,7 @@ namespace Microsoft.AspNetCore.Mvc new Type[] { typeof(MvcCoreRouteOptionsSetup), + typeof(MvcCoreRouteOptionsSetup), } }, { diff --git a/test/WebSites/RoutingWebSite/Pages/Edit.cshtml b/test/WebSites/RoutingWebSite/Pages/Edit.cshtml new file mode 100644 index 0000000000..b7f2033dc9 --- /dev/null +++ b/test/WebSites/RoutingWebSite/Pages/Edit.cshtml @@ -0,0 +1,3 @@ +@page "{id}" + +Hello from Edit page diff --git a/test/WebSites/RoutingWebSite/Pages/PageWithLinks.cshtml b/test/WebSites/RoutingWebSite/Pages/PageWithLinks.cshtml new file mode 100644 index 0000000000..660d9443f4 --- /dev/null +++ b/test/WebSites/RoutingWebSite/Pages/PageWithLinks.cshtml @@ -0,0 +1,8 @@ +@page +@addTagHelper "*, Microsoft.AspNetCore.Mvc.TagHelpers" +@{ +} + +Edit +
+Contact diff --git a/version.props b/version.props index 0f03397e5d..575dd6248e 100644 --- a/version.props +++ b/version.props @@ -1,7 +1,7 @@  - 2.2.0 - rtm + 3.0.0 + alpha1 t000 a- @@ -9,5 +9,13 @@ $(VersionPrefix)-$(VersionSuffix)-final $(FeatureBranchVersionPrefix)$(VersionSuffix)-$([System.Text.RegularExpressions.Regex]::Replace('$(FeatureBranchVersionSuffix)', '[^\w-]', '-')) $(VersionSuffix)-$(BuildNumber) + + 0.3.0 + alpha1 + + $(ExperimentalVersionPrefix) + $(ExperimentalVersionPrefix)-$(ExperimentalVersionSuffix)-final + $(FeatureBranchVersionPrefix)$(ExperimentalVersionSuffix)-$([System.Text.RegularExpressions.Regex]::Replace('$(FeatureBranchVersionSuffix)', '[^\w-]', '-')) + $(ExperimentalVersionSuffix)-$(BuildNumber)