gets surface controllers and front-end api controllers auto-routed, adds tests

This commit is contained in:
Shannon
2021-02-02 14:48:01 +11:00
parent 2f97265bc0
commit c024db9d3c
13 changed files with 367 additions and 143 deletions

View File

@@ -0,0 +1,70 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Text;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.ApplicationParts;
using Microsoft.Extensions.DependencyInjection;
using NUnit.Framework;
using Umbraco.Core.Cache;
using Umbraco.Core.Logging;
using Umbraco.Core.Persistence;
using Umbraco.Core.Services;
using Umbraco.Tests.Integration.TestServerTest;
using Umbraco.Web;
using Umbraco.Web.Routing;
using Umbraco.Web.Website.Controllers;
namespace Umbraco.Tests.Integration.Umbraco.Web.Website.Routing
{
[TestFixture]
public class SurfaceControllerTests : UmbracoTestServerTestBase
{
[Test]
public async Task Auto_Routes_For_Default_Action()
{
string url = PrepareSurfaceControllerUrl<TestSurfaceController>(x => x.Index());
// Act
HttpResponseMessage response = await Client.GetAsync(url);
string body = await response.Content.ReadAsStringAsync();
// Assert
Assert.AreEqual(HttpStatusCode.OK, response.StatusCode);
}
[Test]
public async Task Auto_Routes_For_Custom_Action()
{
string url = PrepareSurfaceControllerUrl<TestSurfaceController>(x => x.News());
// Act
HttpResponseMessage response = await Client.GetAsync(url);
string body = await response.Content.ReadAsStringAsync();
// Assert
Assert.AreEqual(HttpStatusCode.Forbidden, response.StatusCode);
}
}
// Test controllers must be non-nested, else we need to jump through some hoops with custom
// IApplicationFeatureProvider<ControllerFeature>
// For future notes if we want this, some example code of this is here
// https://tpodolak.com/blog/2020/06/22/asp-net-core-adding-controllers-directly-integration-tests/
public class TestSurfaceController : SurfaceController
{
public TestSurfaceController(IUmbracoContextAccessor umbracoContextAccessor, IUmbracoDatabaseFactory databaseFactory, ServiceContext services, AppCaches appCaches, IProfilingLogger profilingLogger, IPublishedUrlProvider publishedUrlProvider)
: base(umbracoContextAccessor, databaseFactory, services, appCaches, profilingLogger, publishedUrlProvider)
{
}
public IActionResult Index() => Ok();
public IActionResult News() => Forbid();
}
}

View File

@@ -1,3 +1,4 @@
using System;
using Microsoft.AspNetCore.Routing; using Microsoft.AspNetCore.Routing;
using Microsoft.Extensions.Options; using Microsoft.Extensions.Options;
using Umbraco.Core; using Umbraco.Core;
@@ -8,6 +9,7 @@ using Umbraco.Web.BackOffice.Controllers;
using Umbraco.Web.Common.Controllers; using Umbraco.Web.Common.Controllers;
using Umbraco.Web.Common.Extensions; using Umbraco.Web.Common.Extensions;
using Umbraco.Web.Common.Routing; using Umbraco.Web.Common.Routing;
using Umbraco.Web.Mvc;
using Umbraco.Web.WebApi; using Umbraco.Web.WebApi;
namespace Umbraco.Web.BackOffice.Routing namespace Umbraco.Web.BackOffice.Routing
@@ -15,7 +17,7 @@ namespace Umbraco.Web.BackOffice.Routing
/// <summary> /// <summary>
/// Creates routes for the back office area /// Creates routes for the back office area
/// </summary> /// </summary>
public class BackOfficeAreaRoutes : IAreaRoutes public sealed class BackOfficeAreaRoutes : IAreaRoutes
{ {
private readonly GlobalSettings _globalSettings; private readonly GlobalSettings _globalSettings;
private readonly IHostingEnvironment _hostingEnvironment; private readonly IHostingEnvironment _hostingEnvironment;
@@ -23,6 +25,9 @@ namespace Umbraco.Web.BackOffice.Routing
private readonly UmbracoApiControllerTypeCollection _apiControllers; private readonly UmbracoApiControllerTypeCollection _apiControllers;
private readonly string _umbracoPathSegment; private readonly string _umbracoPathSegment;
/// <summary>
/// Initializes a new instance of the <see cref="BackOfficeAreaRoutes"/> class.
/// </summary>
public BackOfficeAreaRoutes( public BackOfficeAreaRoutes(
IOptions<GlobalSettings> globalSettings, IOptions<GlobalSettings> globalSettings,
IHostingEnvironment hostingEnvironment, IHostingEnvironment hostingEnvironment,
@@ -36,6 +41,7 @@ namespace Umbraco.Web.BackOffice.Routing
_umbracoPathSegment = _globalSettings.GetUmbracoMvcArea(_hostingEnvironment); _umbracoPathSegment = _globalSettings.GetUmbracoMvcArea(_hostingEnvironment);
} }
/// <inheritdoc/>
public void CreateRoutes(IEndpointRouteBuilder endpoints) public void CreateRoutes(IEndpointRouteBuilder endpoints)
{ {
switch (_runtimeState.Level) switch (_runtimeState.Level)
@@ -50,7 +56,7 @@ namespace Umbraco.Web.BackOffice.Routing
case RuntimeLevel.Run: case RuntimeLevel.Run:
MapMinimalBackOffice(endpoints); MapMinimalBackOffice(endpoints);
AutoRouteBackOfficeControllers(endpoints); AutoRouteBackOfficeApiControllers(endpoints);
break; break;
case RuntimeLevel.BootFailed: case RuntimeLevel.BootFailed:
case RuntimeLevel.Unknown: case RuntimeLevel.Unknown:
@@ -85,26 +91,30 @@ namespace Umbraco.Web.BackOffice.Routing
} }
/// <summary> /// <summary>
/// Auto-routes all back office controllers /// Auto-routes all back office api controllers
/// </summary> /// </summary>
private void AutoRouteBackOfficeControllers(IEndpointRouteBuilder endpoints) private void AutoRouteBackOfficeApiControllers(IEndpointRouteBuilder endpoints)
{ {
// TODO: We could investigate dynamically routing plugin controllers so we don't have to eagerly type scan for them, // TODO: We could investigate dynamically routing plugin controllers so we don't have to eagerly type scan for them,
// it would probably work well, see https://www.strathweb.com/2019/08/dynamic-controller-routing-in-asp-net-core-3-0/ // it would probably work well, see https://www.strathweb.com/2019/08/dynamic-controller-routing-in-asp-net-core-3-0/
// will probably be what we use for front-end routing too. BTW the orig article about migrating from IRouter to endpoint // will probably be what we use for front-end routing too. BTW the orig article about migrating from IRouter to endpoint
// routing for things like a CMS is here https://github.com/dotnet/aspnetcore/issues/4221 // routing for things like a CMS is here https://github.com/dotnet/aspnetcore/issues/4221
foreach (var controller in _apiControllers) foreach (Type controller in _apiControllers)
{ {
PluginControllerMetadata meta = PluginController.GetMetadata(controller);
// exclude front-end api controllers // exclude front-end api controllers
var meta = PluginController.GetMetadata(controller); if (!meta.IsBackOffice)
if (!meta.IsBackOffice) continue; {
continue;
}
endpoints.MapUmbracoApiRoute( endpoints.MapUmbracoApiRoute(
meta.ControllerType, meta.ControllerType,
_umbracoPathSegment, _umbracoPathSegment,
meta.AreaName, meta.AreaName,
true, meta.IsBackOffice,
defaultAction: string.Empty); // no default action (this is what we had before) defaultAction: string.Empty); // no default action (this is what we had before)
} }
} }

View File

@@ -1,4 +1,4 @@
using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Routing; using Microsoft.AspNetCore.Routing;
using Microsoft.Extensions.Options; using Microsoft.Extensions.Options;
using Umbraco.Core; using Umbraco.Core;
@@ -15,7 +15,7 @@ namespace Umbraco.Web.BackOffice.Routing
/// <summary> /// <summary>
/// Creates routes for the preview hub /// Creates routes for the preview hub
/// </summary> /// </summary>
public class PreviewRoutes : IAreaRoutes public sealed class PreviewRoutes : IAreaRoutes
{ {
private readonly IRuntimeState _runtimeState; private readonly IRuntimeState _runtimeState;
private readonly string _umbracoPathSegment; private readonly string _umbracoPathSegment;

View File

@@ -1,4 +1,4 @@
using Umbraco.Core.Composing; using Umbraco.Core.Composing;
namespace Umbraco.Web.Common.Controllers namespace Umbraco.Web.Common.Controllers
{ {
@@ -7,8 +7,9 @@ namespace Umbraco.Web.Common.Controllers
/// </summary> /// </summary>
public abstract class UmbracoApiController : UmbracoApiControllerBase, IDiscoverable public abstract class UmbracoApiController : UmbracoApiControllerBase, IDiscoverable
{ {
// TODO: Should this only exist in the back office project? These really are only ever used for the back office AFAIK /// <summary>
/// Initializes a new instance of the <see cref="UmbracoApiController"/> class.
/// </summary>
protected UmbracoApiController() protected UmbracoApiController()
{ {
} }

View File

@@ -2,7 +2,6 @@ using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Umbraco.Web.Common.Attributes; using Umbraco.Web.Common.Attributes;
using Umbraco.Web.Common.Authorization; using Umbraco.Web.Common.Authorization;
using Umbraco.Web.Common.Filters;
using Umbraco.Web.Features; using Umbraco.Web.Features;
namespace Umbraco.Web.Common.Controllers namespace Umbraco.Web.Common.Controllers
@@ -18,9 +17,10 @@ namespace Umbraco.Web.Common.Controllers
[UmbracoApiController] [UmbracoApiController]
public abstract class UmbracoApiControllerBase : ControllerBase, IUmbracoFeature public abstract class UmbracoApiControllerBase : ControllerBase, IUmbracoFeature
{ {
// TODO: Should this only exist in the back office project? These really are only ever used for the back office AFAIK /// <summary>
/// Initializes a new instance of the <see cref="UmbracoApiControllerBase"/> class.
public UmbracoApiControllerBase() /// </summary>
protected UmbracoApiControllerBase()
{ {
} }
} }

View File

@@ -29,21 +29,20 @@ namespace Umbraco.Web.Common.Extensions
var pattern = new StringBuilder(rootSegment); var pattern = new StringBuilder(rootSegment);
if (!prefixPathSegment.IsNullOrWhiteSpace()) if (!prefixPathSegment.IsNullOrWhiteSpace())
{ {
pattern.Append("/").Append(prefixPathSegment); pattern.Append('/').Append(prefixPathSegment);
} }
if (includeControllerNameInRoute) if (includeControllerNameInRoute)
{ {
pattern.Append("/").Append(controllerName); pattern.Append('/').Append(controllerName);
} }
pattern.Append("/").Append("{action}/{id?}"); pattern.Append('/').Append("{action}/{id?}");
var defaults = defaultAction.IsNullOrWhiteSpace() var defaults = defaultAction.IsNullOrWhiteSpace()
? (object)new { controller = controllerName } ? (object)new { controller = controllerName }
: new { controller = controllerName, action = defaultAction }; : new { controller = controllerName, action = defaultAction };
if (areaName.IsNullOrWhiteSpace()) if (areaName.IsNullOrWhiteSpace())
{ {
endpoints.MapControllerRoute( endpoints.MapControllerRoute(
@@ -70,6 +69,7 @@ namespace Umbraco.Web.Common.Extensions
/// <summary> /// <summary>
/// Used to map Umbraco controllers consistently /// Used to map Umbraco controllers consistently
/// </summary> /// </summary>
/// <typeparam name="T">The <see cref="ControllerBase"/> type to route</typeparam>
public static void MapUmbracoRoute<T>( public static void MapUmbracoRoute<T>(
this IEndpointRouteBuilder endpoints, this IEndpointRouteBuilder endpoints,
string rootSegment, string rootSegment,
@@ -82,8 +82,9 @@ namespace Umbraco.Web.Common.Extensions
=> endpoints.MapUmbracoRoute(typeof(T), rootSegment, areaName, prefixPathSegment, defaultAction, includeControllerNameInRoute, constraints); => endpoints.MapUmbracoRoute(typeof(T), rootSegment, areaName, prefixPathSegment, defaultAction, includeControllerNameInRoute, constraints);
/// <summary> /// <summary>
/// Used to map Umbraco api controllers consistently /// Used to map controllers as Umbraco API routes consistently
/// </summary> /// </summary>
/// <typeparam name="T">The <see cref="ControllerBase"/> type to route</typeparam>
public static void MapUmbracoApiRoute<T>( public static void MapUmbracoApiRoute<T>(
this IEndpointRouteBuilder endpoints, this IEndpointRouteBuilder endpoints,
string rootSegment, string rootSegment,
@@ -95,7 +96,7 @@ namespace Umbraco.Web.Common.Extensions
=> endpoints.MapUmbracoApiRoute(typeof(T), rootSegment, areaName, isBackOffice, defaultAction, constraints); => endpoints.MapUmbracoApiRoute(typeof(T), rootSegment, areaName, isBackOffice, defaultAction, constraints);
/// <summary> /// <summary>
/// Used to map Umbraco api controllers consistently /// Used to map controllers as Umbraco API routes consistently
/// </summary> /// </summary>
public static void MapUmbracoApiRoute( public static void MapUmbracoApiRoute(
this IEndpointRouteBuilder endpoints, this IEndpointRouteBuilder endpoints,

View File

@@ -2,13 +2,15 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Dynamic; using System.Dynamic;
using System.Linq; using System.Linq;
using Umbraco.Core;
using Microsoft.AspNetCore.Routing;
using System.Reflection;
using Umbraco.Web.Common.Install;
using Umbraco.Core.Hosting;
using System.Linq.Expressions; using System.Linq.Expressions;
using System.Reflection;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Routing;
using Umbraco.Core;
using Umbraco.Core.Hosting;
using Umbraco.Web.Common.Controllers; using Umbraco.Web.Common.Controllers;
using Umbraco.Web.Common.Install;
using Umbraco.Web.Mvc;
namespace Umbraco.Extensions namespace Umbraco.Extensions
{ {
@@ -17,8 +19,6 @@ namespace Umbraco.Extensions
/// <summary> /// <summary>
/// Return the back office url if the back office is installed /// Return the back office url if the back office is installed
/// </summary> /// </summary>
/// <param name="url"></param>
/// <returns></returns>
public static string GetBackOfficeUrl(this LinkGenerator linkGenerator, IHostingEnvironment hostingEnvironment) public static string GetBackOfficeUrl(this LinkGenerator linkGenerator, IHostingEnvironment hostingEnvironment)
{ {
@@ -26,7 +26,10 @@ namespace Umbraco.Extensions
try try
{ {
backOfficeControllerType = Assembly.Load("Umbraco.Web.BackOffice")?.GetType("Umbraco.Web.BackOffice.Controllers.BackOfficeController"); backOfficeControllerType = Assembly.Load("Umbraco.Web.BackOffice")?.GetType("Umbraco.Web.BackOffice.Controllers.BackOfficeController");
if (backOfficeControllerType == null) return "/"; // this would indicate that the installer is installed without the back office if (backOfficeControllerType == null)
{
return "/"; // this would indicate that the installer is installed without the back office
}
} }
catch catch
{ {
@@ -39,47 +42,33 @@ namespace Umbraco.Extensions
/// <summary> /// <summary>
/// Returns the URL for the installer /// Returns the URL for the installer
/// </summary> /// </summary>
/// <param name="linkGenerator"></param>
/// <returns></returns>
public static string GetInstallerUrl(this LinkGenerator linkGenerator) public static string GetInstallerUrl(this LinkGenerator linkGenerator)
{ => linkGenerator.GetPathByAction(nameof(InstallController.Index), ControllerExtensions.GetControllerName<InstallController>(), new { area = Constants.Web.Mvc.InstallArea });
return linkGenerator.GetPathByAction(nameof(InstallController.Index), ControllerExtensions.GetControllerName<InstallController>(), new { area = Constants.Web.Mvc.InstallArea });
}
/// <summary> /// <summary>
/// Returns the URL for the installer api /// Returns the URL for the installer api
/// </summary> /// </summary>
/// <param name="linkGenerator"></param>
/// <returns></returns>
public static string GetInstallerApiUrl(this LinkGenerator linkGenerator) public static string GetInstallerApiUrl(this LinkGenerator linkGenerator)
{ => linkGenerator.GetPathByAction(
return linkGenerator.GetPathByAction(nameof(InstallApiController.GetSetup), nameof(InstallApiController.GetSetup),
ControllerExtensions.GetControllerName<InstallApiController>(), ControllerExtensions.GetControllerName<InstallApiController>(),
new { area = Constants.Web.Mvc.InstallArea }).TrimEnd(nameof(InstallApiController.GetSetup)); new { area = Constants.Web.Mvc.InstallArea }).TrimEnd(nameof(InstallApiController.GetSetup));
}
/// <summary> /// <summary>
/// Return the Url for a Web Api service /// Return the Url for a Web Api service
/// </summary> /// </summary>
/// <typeparam name="T"></typeparam> /// <typeparam name="T">The <see cref="UmbracoApiControllerBase"/></typeparam>
/// <param name="url"></param>
/// <param name="actionName"></param>
/// <param name="id"></param>
/// <returns></returns>
public static string GetUmbracoApiService<T>(this LinkGenerator linkGenerator, string actionName, object id = null) public static string GetUmbracoApiService<T>(this LinkGenerator linkGenerator, string actionName, object id = null)
where T : UmbracoApiControllerBase where T : UmbracoApiControllerBase => linkGenerator.GetUmbracoControllerUrl(
{ actionName,
return linkGenerator.GetUmbracoApiService(actionName, typeof(T), new Dictionary<string, object>() typeof(T),
{ new Dictionary<string, object>()
["id"] = id {
}); ["id"] = id
} });
public static string GetUmbracoApiService<T>(this LinkGenerator linkGenerator, string actionName, IDictionary<string, object> values) public static string GetUmbracoApiService<T>(this LinkGenerator linkGenerator, string actionName, IDictionary<string, object> values)
where T : UmbracoApiControllerBase where T : UmbracoApiControllerBase => linkGenerator.GetUmbracoControllerUrl(actionName, typeof(T), values);
{
return linkGenerator.GetUmbracoApiService(actionName, typeof(T), values);
}
public static string GetUmbracoApiServiceBaseUrl<T>(this LinkGenerator linkGenerator, Expression<Func<T, object>> methodSelector) public static string GetUmbracoApiServiceBaseUrl<T>(this LinkGenerator linkGenerator, Expression<Func<T, object>> methodSelector)
where T : UmbracoApiControllerBase where T : UmbracoApiControllerBase
@@ -93,66 +82,86 @@ namespace Umbraco.Extensions
} }
/// <summary> /// <summary>
/// Return the Url for a Web Api service /// Return the Url for an Umbraco controller
/// </summary> /// </summary>
/// <param name="url"></param> public static string GetUmbracoControllerUrl(this LinkGenerator linkGenerator, string actionName, string controllerName, string area, IDictionary<string, object> dict = null)
/// <param name="actionName"></param>
/// <param name="controllerName"></param>
/// <param name="area"></param>
/// <param name="id"></param>
/// <returns></returns>
public static string GetUmbracoApiService(this LinkGenerator linkGenerator, string actionName, string controllerName, string area, IDictionary<string,object> dict = null)
{ {
if (actionName == null) throw new ArgumentNullException(nameof(actionName)); if (actionName == null)
if (string.IsNullOrWhiteSpace(actionName)) throw new ArgumentException("Value can't be empty or consist only of white-space characters.", nameof(actionName)); {
if (controllerName == null) throw new ArgumentNullException(nameof(controllerName)); throw new ArgumentNullException(nameof(actionName));
if (string.IsNullOrWhiteSpace(controllerName)) throw new ArgumentException("Value can't be empty or consist only of white-space characters.", nameof(controllerName)); }
if (string.IsNullOrWhiteSpace(actionName))
{
throw new ArgumentException("Value can't be empty or consist only of white-space characters.", nameof(actionName));
}
if (controllerName == null)
{
throw new ArgumentNullException(nameof(controllerName));
}
if (string.IsNullOrWhiteSpace(controllerName))
{
throw new ArgumentException("Value can't be empty or consist only of white-space characters.", nameof(controllerName));
}
if (dict is null) if (dict is null)
{ {
dict = new Dictionary<string, object>(); dict = new Dictionary<string, object>();
} }
if (!area.IsNullOrWhiteSpace()) if (!area.IsNullOrWhiteSpace())
{ {
dict["area"] = area; dict["area"] = area;
} }
IDictionary<string, object> values = dict.Aggregate(
var values = dict.Aggregate(new ExpandoObject() as IDictionary<string, object>, new ExpandoObject() as IDictionary<string, object>,
(a, p) => { a.Add(p.Key, p.Value); return a; }); (a, p) =>
{
a.Add(p.Key, p.Value);
return a;
});
return linkGenerator.GetPathByAction(actionName, controllerName, values); return linkGenerator.GetPathByAction(actionName, controllerName, values);
} }
/// <summary> /// <summary>
/// Return the Url for a Web Api service /// Return the Url for an Umbraco controller
/// </summary> /// </summary>
/// <param name="url"></param> public static string GetUmbracoControllerUrl(this LinkGenerator linkGenerator, string actionName, Type controllerType, IDictionary<string, object> values = null)
/// <param name="actionName"></param>
/// <param name="apiControllerType"></param>
/// <param name="id"></param>
/// <returns></returns>
public static string GetUmbracoApiService(this LinkGenerator linkGenerator, string actionName, Type apiControllerType, IDictionary<string,object> values = null)
{ {
if (actionName == null) throw new ArgumentNullException(nameof(actionName)); if (actionName == null)
if (string.IsNullOrWhiteSpace(actionName)) throw new ArgumentException("Value can't be empty or consist only of white-space characters.", nameof(actionName)); {
if (apiControllerType == null) throw new ArgumentNullException(nameof(apiControllerType)); throw new ArgumentNullException(nameof(actionName));
}
var area = ""; if (string.IsNullOrWhiteSpace(actionName))
{
throw new ArgumentException("Value can't be empty or consist only of white-space characters.", nameof(actionName));
}
if (!typeof(UmbracoApiControllerBase).IsAssignableFrom(apiControllerType)) if (controllerType == null)
throw new InvalidOperationException($"The controller {apiControllerType} is of type {typeof(UmbracoApiControllerBase)}"); {
throw new ArgumentNullException(nameof(controllerType));
}
var metaData = PluginController.GetMetadata(apiControllerType); var area = string.Empty;
if (!typeof(ControllerBase).IsAssignableFrom(controllerType))
{
throw new InvalidOperationException($"The controller {controllerType} is of type {typeof(ControllerBase)}");
}
PluginControllerMetadata metaData = PluginController.GetMetadata(controllerType);
if (metaData.AreaName.IsNullOrWhiteSpace() == false) if (metaData.AreaName.IsNullOrWhiteSpace() == false)
{ {
//set the area to the plugin area // set the area to the plugin area
area = metaData.AreaName; area = metaData.AreaName;
} }
return linkGenerator.GetUmbracoApiService(actionName, ControllerExtensions.GetControllerName(apiControllerType), area, values);
return linkGenerator.GetUmbracoControllerUrl(actionName, ControllerExtensions.GetControllerName(controllerType), area, values);
} }
public static string GetUmbracoApiService<T>(this LinkGenerator linkGenerator, Expression<Func<T, object>> methodSelector) public static string GetUmbracoApiService<T>(this LinkGenerator linkGenerator, Expression<Func<T, object>> methodSelector)
@@ -170,6 +179,7 @@ namespace Umbraco.Extensions
{ {
return linkGenerator.GetUmbracoApiService<T>(method.Name); return linkGenerator.GetUmbracoApiService<T>(method.Name);
} }
return linkGenerator.GetUmbracoApiService<T>(method.Name, methodParams); return linkGenerator.GetUmbracoApiService<T>(method.Name, methodParams);
} }
} }

View File

@@ -22,11 +22,12 @@ namespace Umbraco.Web.Website.Controllers
// [MergeParentContextViewData] // [MergeParentContextViewData]
public abstract class SurfaceController : PluginController public abstract class SurfaceController : PluginController
{ {
/// <summary>
/// Initializes a new instance of the <see cref="SurfaceController"/> class.
/// </summary>
protected SurfaceController(IUmbracoContextAccessor umbracoContextAccessor, IUmbracoDatabaseFactory databaseFactory, ServiceContext services, AppCaches appCaches, IProfilingLogger profilingLogger, IPublishedUrlProvider publishedUrlProvider) protected SurfaceController(IUmbracoContextAccessor umbracoContextAccessor, IUmbracoDatabaseFactory databaseFactory, ServiceContext services, AppCaches appCaches, IProfilingLogger profilingLogger, IPublishedUrlProvider publishedUrlProvider)
: base(umbracoContextAccessor, databaseFactory, services, appCaches, profilingLogger) : base(umbracoContextAccessor, databaseFactory, services, appCaches, profilingLogger)
{ => PublishedUrlProvider = publishedUrlProvider;
PublishedUrlProvider = publishedUrlProvider;
}
protected IPublishedUrlProvider PublishedUrlProvider { get; } protected IPublishedUrlProvider PublishedUrlProvider { get; }
@@ -52,49 +53,37 @@ namespace Umbraco.Web.Website.Controllers
/// Redirects to the Umbraco page with the given id /// Redirects to the Umbraco page with the given id
/// </summary> /// </summary>
protected RedirectToUmbracoPageResult RedirectToUmbracoPage(Guid contentKey) protected RedirectToUmbracoPageResult RedirectToUmbracoPage(Guid contentKey)
{ => new RedirectToUmbracoPageResult(contentKey, PublishedUrlProvider, UmbracoContextAccessor);
return new RedirectToUmbracoPageResult(contentKey, PublishedUrlProvider, UmbracoContextAccessor);
}
/// <summary> /// <summary>
/// Redirects to the Umbraco page with the given id and passes provided querystring /// Redirects to the Umbraco page with the given id and passes provided querystring
/// </summary> /// </summary>
protected RedirectToUmbracoPageResult RedirectToUmbracoPage(Guid contentKey, QueryString queryString) protected RedirectToUmbracoPageResult RedirectToUmbracoPage(Guid contentKey, QueryString queryString)
{ => new RedirectToUmbracoPageResult(contentKey, queryString, PublishedUrlProvider, UmbracoContextAccessor);
return new RedirectToUmbracoPageResult(contentKey, queryString, PublishedUrlProvider, UmbracoContextAccessor);
}
/// <summary> /// <summary>
/// Redirects to the Umbraco page with the given published content /// Redirects to the Umbraco page with the given published content
/// </summary> /// </summary>
protected RedirectToUmbracoPageResult RedirectToUmbracoPage(IPublishedContent publishedContent) protected RedirectToUmbracoPageResult RedirectToUmbracoPage(IPublishedContent publishedContent)
{ => new RedirectToUmbracoPageResult(publishedContent, PublishedUrlProvider, UmbracoContextAccessor);
return new RedirectToUmbracoPageResult(publishedContent, PublishedUrlProvider, UmbracoContextAccessor);
}
/// <summary> /// <summary>
/// Redirects to the Umbraco page with the given published content and passes provided querystring /// Redirects to the Umbraco page with the given published content and passes provided querystring
/// </summary> /// </summary>
protected RedirectToUmbracoPageResult RedirectToUmbracoPage(IPublishedContent publishedContent, QueryString queryString) protected RedirectToUmbracoPageResult RedirectToUmbracoPage(IPublishedContent publishedContent, QueryString queryString)
{ => new RedirectToUmbracoPageResult(publishedContent, queryString, PublishedUrlProvider, UmbracoContextAccessor);
return new RedirectToUmbracoPageResult(publishedContent, queryString, PublishedUrlProvider, UmbracoContextAccessor);
}
/// <summary> /// <summary>
/// Redirects to the currently rendered Umbraco page /// Redirects to the currently rendered Umbraco page
/// </summary> /// </summary>
protected RedirectToUmbracoPageResult RedirectToCurrentUmbracoPage() protected RedirectToUmbracoPageResult RedirectToCurrentUmbracoPage()
{ => new RedirectToUmbracoPageResult(CurrentPage, PublishedUrlProvider, UmbracoContextAccessor);
return new RedirectToUmbracoPageResult(CurrentPage, PublishedUrlProvider, UmbracoContextAccessor);
}
/// <summary> /// <summary>
/// Redirects to the currently rendered Umbraco page and passes provided querystring /// Redirects to the currently rendered Umbraco page and passes provided querystring
/// </summary> /// </summary>
protected RedirectToUmbracoPageResult RedirectToCurrentUmbracoPage(QueryString queryString) protected RedirectToUmbracoPageResult RedirectToCurrentUmbracoPage(QueryString queryString)
{ => new RedirectToUmbracoPageResult(CurrentPage, queryString, PublishedUrlProvider, UmbracoContextAccessor);
return new RedirectToUmbracoPageResult(CurrentPage, queryString, PublishedUrlProvider, UmbracoContextAccessor);
}
/// <summary> /// <summary>
/// Redirects to the currently rendered Umbraco URL /// Redirects to the currently rendered Umbraco URL
@@ -105,17 +94,13 @@ namespace Umbraco.Web.Website.Controllers
/// Server.Transfer.* /// Server.Transfer.*
/// </remarks> /// </remarks>
protected RedirectToUmbracoUrlResult RedirectToCurrentUmbracoUrl() protected RedirectToUmbracoUrlResult RedirectToCurrentUmbracoUrl()
{ => new RedirectToUmbracoUrlResult(UmbracoContext);
return new RedirectToUmbracoUrlResult(UmbracoContext);
}
/// <summary> /// <summary>
/// Returns the currently rendered Umbraco page /// Returns the currently rendered Umbraco page
/// </summary> /// </summary>
protected UmbracoPageResult CurrentUmbracoPage() protected UmbracoPageResult CurrentUmbracoPage()
{ => new UmbracoPageResult(ProfilingLogger);
return new UmbracoPageResult(ProfilingLogger);
}
/// <summary> /// <summary>
/// we need to recursively find the route definition based on the parent view context /// we need to recursively find the route definition based on the parent view context
@@ -126,9 +111,9 @@ namespace Umbraco.Web.Website.Controllers
while (!(currentContext is null)) while (!(currentContext is null))
{ {
var currentRouteData = currentContext.RouteData; var currentRouteData = currentContext.RouteData;
if (currentRouteData.Values.ContainsKey(Core.Constants.Web.UmbracoRouteDefinitionDataToken)) if (currentRouteData.Values.ContainsKey(Constants.Web.UmbracoRouteDefinitionDataToken))
{ {
return Attempt.Succeed((UmbracoRouteValues)currentRouteData.Values[Core.Constants.Web.UmbracoRouteDefinitionDataToken]); return Attempt.Succeed((UmbracoRouteValues)currentRouteData.Values[Constants.Web.UmbracoRouteDefinitionDataToken]);
} }
} }

View File

@@ -1,11 +1,9 @@
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Razor;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options; using Microsoft.Extensions.Options;
using Umbraco.Core.DependencyInjection; using Umbraco.Core.DependencyInjection;
using Umbraco.Extensions; using Umbraco.Extensions;
using Umbraco.Infrastructure.DependencyInjection; using Umbraco.Infrastructure.DependencyInjection;
using Umbraco.Infrastructure.PublishedCache.DependencyInjection;
using Umbraco.ModelsBuilder.Embedded.DependencyInjection; using Umbraco.ModelsBuilder.Embedded.DependencyInjection;
using Umbraco.Web.Common.Routing; using Umbraco.Web.Common.Routing;
using Umbraco.Web.Website.Collections; using Umbraco.Web.Website.Collections;
@@ -44,6 +42,8 @@ namespace Umbraco.Web.Website.DependencyInjection
builder.Services.AddSingleton<IUmbracoRenderingDefaults, UmbracoRenderingDefaults>(); builder.Services.AddSingleton<IUmbracoRenderingDefaults, UmbracoRenderingDefaults>();
builder.Services.AddSingleton<IRoutableDocumentFilter, RoutableDocumentFilter>(); builder.Services.AddSingleton<IRoutableDocumentFilter, RoutableDocumentFilter>();
builder.Services.AddSingleton<FrontEndRoutes>();
builder builder
.AddDistributedCache() .AddDistributedCache()
.AddModelsBuilder(); .AddModelsBuilder();

View File

@@ -35,34 +35,20 @@ namespace Umbraco.Extensions
public static class HtmlHelperRenderExtensions public static class HtmlHelperRenderExtensions
{ {
private static T GetRequiredService<T>(IHtmlHelper htmlHelper) private static T GetRequiredService<T>(IHtmlHelper htmlHelper)
{ => GetRequiredService<T>(htmlHelper.ViewContext);
return GetRequiredService<T>(htmlHelper.ViewContext);
}
private static T GetRequiredService<T>(ViewContext viewContext) private static T GetRequiredService<T>(ViewContext viewContext)
{ => viewContext.HttpContext.RequestServices.GetRequiredService<T>();
return viewContext.HttpContext.RequestServices.GetRequiredService<T>();
}
/// <summary> /// <summary>
/// Renders the markup for the profiler /// Renders the markup for the profiler
/// </summary> /// </summary>
/// <param name="helper"></param>
/// <returns></returns>
public static IHtmlContent RenderProfiler(this IHtmlHelper helper) public static IHtmlContent RenderProfiler(this IHtmlHelper helper)
{ => new HtmlString(GetRequiredService<IProfilerHtml>(helper).Render());
return new HtmlString(GetRequiredService<IProfilerHtml>(helper).Render());
}
/// <summary> /// <summary>
/// Renders a partial view that is found in the specified area /// Renders a partial view that is found in the specified area
/// </summary> /// </summary>
/// <param name="helper"></param>
/// <param name="partial"></param>
/// <param name="area"></param>
/// <param name="model"></param>
/// <param name="viewData"></param>
/// <returns></returns>
public static IHtmlContent AreaPartial(this IHtmlHelper helper, string partial, string area, object model = null, ViewDataDictionary viewData = null) public static IHtmlContent AreaPartial(this IHtmlHelper helper, string partial, string area, object model = null, ViewDataDictionary viewData = null)
{ {
var originalArea = helper.ViewContext.RouteData.DataTokens["area"]; var originalArea = helper.ViewContext.RouteData.DataTokens["area"];
@@ -76,8 +62,6 @@ namespace Umbraco.Extensions
/// Will render the preview badge when in preview mode which is not required ever unless the MVC page you are /// Will render the preview badge when in preview mode which is not required ever unless the MVC page you are
/// using does not inherit from UmbracoViewPage /// using does not inherit from UmbracoViewPage
/// </summary> /// </summary>
/// <param name="helper"></param>
/// <returns></returns>
/// <remarks> /// <remarks>
/// See: http://issues.umbraco.org/issue/U4-1614 /// See: http://issues.umbraco.org/issue/U4-1614
/// </remarks> /// </remarks>
@@ -109,9 +93,9 @@ namespace Umbraco.Extensions
Func<object, ViewDataDictionary, string> contextualKeyBuilder = null) Func<object, ViewDataDictionary, string> contextualKeyBuilder = null)
{ {
var cacheKey = new StringBuilder(partialViewName); var cacheKey = new StringBuilder(partialViewName);
//let's always cache by the current culture to allow variants to have different cache results // let's always cache by the current culture to allow variants to have different cache results
var cultureName = System.Threading.Thread.CurrentThread.CurrentUICulture.Name; var cultureName = System.Threading.Thread.CurrentThread.CurrentUICulture.Name;
if (!String.IsNullOrEmpty(cultureName)) if (!string.IsNullOrEmpty(cultureName))
{ {
cacheKey.AppendFormat("{0}-", cultureName); cacheKey.AppendFormat("{0}-", cultureName);
} }
@@ -123,16 +107,19 @@ namespace Umbraco.Extensions
{ {
throw new InvalidOperationException("Cannot cache by page if the UmbracoContext has not been initialized, this parameter can only be used in the context of an Umbraco request"); throw new InvalidOperationException("Cannot cache by page if the UmbracoContext has not been initialized, this parameter can only be used in the context of an Umbraco request");
} }
cacheKey.AppendFormat("{0}-", umbracoContext.PublishedRequest?.PublishedContent?.Id ?? 0); cacheKey.AppendFormat("{0}-", umbracoContext.PublishedRequest?.PublishedContent?.Id ?? 0);
} }
if (cacheByMember) if (cacheByMember)
{ {
//TODO reintroduce when members are migrated // TODO reintroduce when members are migrated
throw new NotImplementedException("Reintroduce when members are migrated"); throw new NotImplementedException("Reintroduce when members are migrated");
// var helper = Current.MembershipHelper; // var helper = Current.MembershipHelper;
// var currentMember = helper.GetCurrentMember(); // var currentMember = helper.GetCurrentMember();
// cacheKey.AppendFormat("m{0}-", currentMember?.Id ?? 0); // cacheKey.AppendFormat("m{0}-", currentMember?.Id ?? 0);
} }
if (contextualKeyBuilder != null) if (contextualKeyBuilder != null)
{ {
var contextualKey = contextualKeyBuilder(model, viewData); var contextualKey = contextualKeyBuilder(model, viewData);

View File

@@ -0,0 +1,52 @@
using System;
using System.Collections.Generic;
using System.Dynamic;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using Microsoft.AspNetCore.Routing;
using Umbraco.Core;
using Umbraco.Web.Website.Controllers;
namespace Umbraco.Extensions
{
public static class LinkGeneratorExtensions
{
/// <summary>
/// Return the Url for a Surface Controller
/// </summary>
/// <typeparam name="T">The <see cref="SurfaceController"/></typeparam>
public static string GetUmbracoSurfaceUrl<T>(this LinkGenerator linkGenerator, Expression<Func<T, object>> methodSelector)
where T : SurfaceController
{
MethodInfo method = ExpressionHelper.GetMethodInfo(methodSelector);
IDictionary<string, object> methodParams = ExpressionHelper.GetMethodParams(methodSelector);
if (method == null)
{
throw new MissingMethodException(
$"Could not find the method {methodSelector} on type {typeof(T)} or the result ");
}
if (methodParams.Any() == false)
{
return linkGenerator.GetUmbracoSurfaceUrl<T>(method.Name);
}
return linkGenerator.GetUmbracoSurfaceUrl<T>(method.Name, methodParams);
}
/// <summary>
/// Return the Url for a Surface Controller
/// </summary>
/// <typeparam name="T">The <see cref="SurfaceController"/></typeparam>
public static string GetUmbracoSurfaceUrl<T>(this LinkGenerator linkGenerator, string actionName, object id = null)
where T : SurfaceController => linkGenerator.GetUmbracoControllerUrl(
actionName,
typeof(T),
new Dictionary<string, object>()
{
["id"] = id
});
}
}

View File

@@ -37,6 +37,9 @@ namespace Umbraco.Extensions
{ {
app.UseEndpoints(endpoints => app.UseEndpoints(endpoints =>
{ {
FrontEndRoutes surfaceRoutes = app.ApplicationServices.GetRequiredService<FrontEndRoutes>();
surfaceRoutes.CreateRoutes(endpoints);
endpoints.MapDynamicControllerRoute<UmbracoRouteValueTransformer>("/{**slug}"); endpoints.MapDynamicControllerRoute<UmbracoRouteValueTransformer>("/{**slug}");
}); });

View File

@@ -0,0 +1,105 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Routing;
using Microsoft.Extensions.Options;
using Umbraco.Core;
using Umbraco.Core.Configuration;
using Umbraco.Core.Configuration.Models;
using Umbraco.Core.Hosting;
using Umbraco.Web.Common.Controllers;
using Umbraco.Web.Common.Extensions;
using Umbraco.Web.Common.Routing;
using Umbraco.Web.Mvc;
using Umbraco.Web.WebApi;
using Umbraco.Web.Website.Collections;
namespace Umbraco.Web.Website.Routing
{
/// <summary>
/// Creates routes for surface controllers
/// </summary>
public sealed class FrontEndRoutes : IAreaRoutes
{
private readonly GlobalSettings _globalSettings;
private readonly IHostingEnvironment _hostingEnvironment;
private readonly IRuntimeState _runtimeState;
private readonly SurfaceControllerTypeCollection _surfaceControllerTypeCollection;
private readonly UmbracoApiControllerTypeCollection _apiControllers;
private readonly string _umbracoPathSegment;
/// <summary>
/// Initializes a new instance of the <see cref="FrontEndRoutes"/> class.
/// </summary>
public FrontEndRoutes(
IOptions<GlobalSettings> globalSettings,
IHostingEnvironment hostingEnvironment,
IRuntimeState runtimeState,
SurfaceControllerTypeCollection surfaceControllerTypeCollection,
UmbracoApiControllerTypeCollection apiControllers)
{
_globalSettings = globalSettings.Value;
_hostingEnvironment = hostingEnvironment;
_runtimeState = runtimeState;
_surfaceControllerTypeCollection = surfaceControllerTypeCollection;
_apiControllers = apiControllers;
_umbracoPathSegment = _globalSettings.GetUmbracoMvcArea(_hostingEnvironment);
}
/// <inheritdoc/>
public void CreateRoutes(IEndpointRouteBuilder endpoints)
{
if (_runtimeState.Level != RuntimeLevel.Run)
{
return;
}
AutoRouteSurfaceControllers(endpoints);
AutoRouteFrontEndApiControllers(endpoints);
}
/// <summary>
/// Auto-routes all front-end surface controllers
/// </summary>
private void AutoRouteSurfaceControllers(IEndpointRouteBuilder endpoints)
{
foreach (Type controller in _surfaceControllerTypeCollection)
{
// exclude front-end api controllers
PluginControllerMetadata meta = PluginController.GetMetadata(controller);
endpoints.MapUmbracoRoute(
meta.ControllerType,
_umbracoPathSegment,
meta.AreaName,
"Surface");
}
}
/// <summary>
/// Auto-routes all front-end api controllers
/// </summary>
private void AutoRouteFrontEndApiControllers(IEndpointRouteBuilder endpoints)
{
foreach (Type controller in _apiControllers)
{
PluginControllerMetadata meta = PluginController.GetMetadata(controller);
// exclude back-end api controllers
if (meta.IsBackOffice)
{
continue;
}
endpoints.MapUmbracoApiRoute(
meta.ControllerType,
_umbracoPathSegment,
meta.AreaName,
meta.IsBackOffice,
defaultAction: string.Empty); // no default action (this is what we had before)
}
}
}
}