Merge remote-tracking branch 'origin/netcore/netcore' into netcore/feature/net5

This commit is contained in:
Bjarke Berg
2021-01-25 08:54:37 +01:00
18 changed files with 204 additions and 68 deletions

View File

@@ -4,7 +4,6 @@
using System;
using System.Collections.Generic;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Umbraco.Core.Events;
namespace Umbraco.Core.DependencyInjection
@@ -28,8 +27,28 @@ namespace Umbraco.Core.DependencyInjection
// Register the handler as transient. This ensures that anything can be injected into it.
var descriptor = new UniqueServiceDescriptor(typeof(INotificationHandler<TNotification>), typeof(TNotificationHandler), ServiceLifetime.Transient);
// TODO: Waiting on feedback here https://github.com/umbraco/Umbraco-CMS/pull/9556/files#r548365396 about whether
// we perform this duplicate check or not.
if (!builder.Services.Contains(descriptor))
{
builder.Services.Add(descriptor);
}
return builder;
}
/// <summary>
/// Registers a notification async handler against the Umbraco service collection.
/// </summary>
/// <typeparam name="TNotification">The type of notification.</typeparam>
/// <typeparam name="TNotificationAsyncHandler">The type of notification async handler.</typeparam>
/// <param name="builder">The Umbraco builder.</param>
/// <returns>The <see cref="IUmbracoBuilder"/>.</returns>
public static IUmbracoBuilder AddNotificationAsyncHandler<TNotification, TNotificationAsyncHandler>(this IUmbracoBuilder builder)
where TNotificationAsyncHandler : INotificationAsyncHandler<TNotification>
where TNotification : INotification
{
// Register the handler as transient. This ensures that anything can be injected into it.
var descriptor = new ServiceDescriptor(typeof(INotificationAsyncHandler<TNotification>), typeof(TNotificationAsyncHandler), ServiceLifetime.Transient);
if (!builder.Services.Contains(descriptor))
{
builder.Services.Add(descriptor);

View File

@@ -146,7 +146,7 @@ namespace Umbraco.Core.DependencyInjection
Services.AddSingleton<ManifestWatcher>();
Services.AddSingleton<UmbracoRequestPaths>();
this.AddNotificationHandler<UmbracoApplicationStarting, AppPluginsManifestWatcherNotificationHandler>();
this.AddNotificationAsyncHandler<UmbracoApplicationStarting, AppPluginsManifestWatcherNotificationHandler>();
Services.AddUnique<InstallStatusTracker>();

View File

@@ -15,17 +15,30 @@ namespace Umbraco.Core.Events
/// </content>
public partial class EventAggregator : IEventAggregator
{
private static readonly ConcurrentDictionary<Type, NotificationAsyncHandlerWrapper> s_notificationAsyncHandlers
= new ConcurrentDictionary<Type, NotificationAsyncHandlerWrapper>();
private static readonly ConcurrentDictionary<Type, NotificationHandlerWrapper> s_notificationHandlers
= new ConcurrentDictionary<Type, NotificationHandlerWrapper>();
private Task PublishNotificationAsync(INotification notification, CancellationToken cancellationToken = default)
{
Type notificationType = notification.GetType();
NotificationHandlerWrapper handler = s_notificationHandlers.GetOrAdd(
NotificationAsyncHandlerWrapper asyncHandler = s_notificationAsyncHandlers.GetOrAdd(
notificationType,
t => (NotificationAsyncHandlerWrapper)Activator.CreateInstance(typeof(NotificationAsyncHandlerWrapperImpl<>).MakeGenericType(notificationType)));
return asyncHandler.HandleAsync(notification, cancellationToken, _serviceFactory, PublishCoreAsync);
}
private void PublishNotification(INotification notification)
{
Type notificationType = notification.GetType();
NotificationHandlerWrapper asyncHandler = s_notificationHandlers.GetOrAdd(
notificationType,
t => (NotificationHandlerWrapper)Activator.CreateInstance(typeof(NotificationHandlerWrapperImpl<>).MakeGenericType(notificationType)));
return handler.HandleAsync(notification, cancellationToken, _serviceFactory, PublishCoreAsync);
asyncHandler.Handle(notification, _serviceFactory, PublishCore);
}
private async Task PublishCoreAsync(
@@ -38,9 +51,27 @@ namespace Umbraco.Core.Events
await handler(notification, cancellationToken).ConfigureAwait(false);
}
}
private void PublishCore(
IEnumerable<Action<INotification>> allHandlers,
INotification notification)
{
foreach (Action<INotification> handler in allHandlers)
{
handler(notification);
}
}
}
internal abstract class NotificationHandlerWrapper
{
public abstract void Handle(
INotification notification,
ServiceFactory serviceFactory,
Action<IEnumerable<Action<INotification>>, INotification> publish);
}
internal abstract class NotificationAsyncHandlerWrapper
{
public abstract Task HandleAsync(
INotification notification,
@@ -49,7 +80,7 @@ namespace Umbraco.Core.Events
Func<IEnumerable<Func<INotification, CancellationToken, Task>>, INotification, CancellationToken, Task> publish);
}
internal class NotificationHandlerWrapperImpl<TNotification> : NotificationHandlerWrapper
internal class NotificationAsyncHandlerWrapperImpl<TNotification> : NotificationAsyncHandlerWrapper
where TNotification : INotification
{
public override Task HandleAsync(
@@ -59,7 +90,7 @@ namespace Umbraco.Core.Events
Func<IEnumerable<Func<INotification, CancellationToken, Task>>, INotification, CancellationToken, Task> publish)
{
IEnumerable<Func<INotification, CancellationToken, Task>> handlers = serviceFactory
.GetInstances<INotificationHandler<TNotification>>()
.GetInstances<INotificationAsyncHandler<TNotification>>()
.Select(x => new Func<INotification, CancellationToken, Task>(
(theNotification, theToken) =>
x.HandleAsync((TNotification)theNotification, theToken)));
@@ -67,4 +98,22 @@ namespace Umbraco.Core.Events
return publish(handlers, notification, cancellationToken);
}
}
internal class NotificationHandlerWrapperImpl<TNotification> : NotificationHandlerWrapper
where TNotification : INotification
{
public override void Handle(
INotification notification,
ServiceFactory serviceFactory,
Action<IEnumerable<Action<INotification>>, INotification> publish)
{
IEnumerable<Action<INotification>> handlers = serviceFactory
.GetInstances<INotificationHandler<TNotification>>()
.Select(x => new Action<INotification>(
(theNotification) =>
x.Handle((TNotification)theNotification)));
publish(handlers, notification);
}
}
}

View File

@@ -38,8 +38,23 @@ namespace Umbraco.Core.Events
throw new ArgumentNullException(nameof(notification));
}
PublishNotification(notification);
return PublishNotificationAsync(notification, cancellationToken);
}
/// <inheritdoc/>
public void Publish<TNotification>(TNotification notification)
where TNotification : INotification
{
// TODO: Introduce codegen efficient Guard classes to reduce noise.
if (notification == null)
{
throw new ArgumentNullException(nameof(notification));
}
PublishNotification(notification);
Task.WaitAll(PublishNotificationAsync(notification));
}
}
/// <summary>

View File

@@ -13,7 +13,7 @@ namespace Umbraco.Core.Events
public interface IEventAggregator
{
/// <summary>
/// Asynchronously send a notification to multiple handlers
/// Asynchronously send a notification to multiple handlers of both sync and async
/// </summary>
/// <typeparam name="TNotification">The type of notification being handled.</typeparam>
/// <param name="notification">The notification object.</param>
@@ -21,5 +21,13 @@ namespace Umbraco.Core.Events
/// <returns>A task that represents the publish operation.</returns>
Task PublishAsync<TNotification>(TNotification notification, CancellationToken cancellationToken = default)
where TNotification : INotification;
/// <summary>
/// Synchronously send a notification to multiple handlers of both sync and async
/// </summary>
/// <typeparam name="TNotification">The type of notification being handled.</typeparam>
/// <param name="notification">The notification object.</param>
void Publish<TNotification>(TNotification notification)
where TNotification : INotification;
}
}

View File

@@ -12,6 +12,20 @@ namespace Umbraco.Core.Events
/// <typeparam name="TNotification">The type of notification being handled.</typeparam>
public interface INotificationHandler<in TNotification>
where TNotification : INotification
{
/// <summary>
/// Handles a notification
/// </summary>
/// <param name="notification">The notification</param>
void Handle(TNotification notification);
}
/// <summary>
/// Defines a handler for a async notification.
/// </summary>
/// <typeparam name="TNotification">The type of notification being handled.</typeparam>
public interface INotificationAsyncHandler<in TNotification>
where TNotification : INotification
{
/// <summary>
/// Handles a notification

View File

@@ -11,7 +11,7 @@ namespace Umbraco.Core.Runtime
/// <summary>
/// Starts monitoring AppPlugins directory during debug runs, to restart site when a plugin manifest changes.
/// </summary>
public sealed class AppPluginsManifestWatcherNotificationHandler : INotificationHandler<UmbracoApplicationStarting>
public sealed class AppPluginsManifestWatcherNotificationHandler : INotificationAsyncHandler<UmbracoApplicationStarting>
{
private readonly ManifestWatcher _manifestWatcher;
private readonly IHostingEnvironment _hostingEnvironment;

View File

@@ -1,5 +1,3 @@
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Options;
using Umbraco.Core.Configuration.Models;
using Umbraco.Core.Events;
@@ -21,7 +19,7 @@ namespace Umbraco.Core.Runtime
_globalSettings = globalSettings.Value;
}
public Task HandleAsync(UmbracoApplicationStarting notification, CancellationToken cancellationToken)
public void Handle(UmbracoApplicationStarting notification)
{
// ensure we have some essential directories
// every other component can then initialize safely
@@ -31,7 +29,6 @@ namespace Umbraco.Core.Runtime
_ioHelper.EnsurePathExists(_hostingEnvironment.MapPathContentRoot(Constants.SystemDirectories.PartialViews));
_ioHelper.EnsurePathExists(_hostingEnvironment.MapPathContentRoot(Constants.SystemDirectories.MacroPartials));
return Task.CompletedTask;
}
}
}

View File

@@ -1,5 +1,3 @@
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using Umbraco.Core.Events;
using Umbraco.Core.Persistence;
@@ -39,7 +37,7 @@ namespace Umbraco.Infrastructure.Cache
}
/// <inheritdoc/>
public Task HandleAsync(UmbracoApplicationStarting notification, CancellationToken cancellationToken)
public void Handle(UmbracoApplicationStarting notification)
{
// The scheduled tasks - TouchServerTask and InstructionProcessTask - run as .NET Core hosted services.
// The former (as well as other hosted services that run outside of an HTTP request context) depends on the application URL
@@ -50,8 +48,6 @@ namespace Umbraco.Infrastructure.Cache
_requestAccessor.EndRequest += EndRequest;
Startup();
return Task.CompletedTask;
}
private void Startup()

View File

@@ -1,5 +1,3 @@
using System.Threading;
using System.Threading.Tasks;
using Umbraco.Core.Events;
using Umbraco.ModelsBuilder.Embedded.BackOffice;
using Umbraco.ModelsBuilder.Embedded.DependencyInjection;
@@ -19,11 +17,10 @@ namespace Umbraco.ModelsBuilder.Embedded
/// <summary>
/// Handles the <see cref="UmbracoApplicationStarting"/> notification to disable MB controller features
/// </summary>
public Task HandleAsync(UmbracoApplicationStarting notification, CancellationToken cancellationToken)
public void Handle(UmbracoApplicationStarting notification)
{
// disable the embedded dashboard controller
_features.Disabled.Controllers.Add<ModelsBuilderDashboardController>();
return Task.CompletedTask;
}
}
}

View File

@@ -1,6 +1,5 @@
using System;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
@@ -8,7 +7,6 @@ using Umbraco.Configuration;
using Umbraco.Core;
using Umbraco.Core.Configuration.Models;
using Umbraco.Core.Events;
using Umbraco.Core.Hosting;
using Umbraco.Extensions;
using Umbraco.ModelsBuilder.Embedded.Building;
using Umbraco.Web.Cache;
@@ -52,10 +50,9 @@ namespace Umbraco.ModelsBuilder.Embedded
/// <summary>
/// Handles the <see cref="UmbracoApplicationStarting"/> notification
/// </summary>
public Task HandleAsync(UmbracoApplicationStarting notification, CancellationToken cancellationToken)
public void Handle(UmbracoApplicationStarting notification)
{
Install();
return Task.CompletedTask;
}
private void Install()

View File

@@ -1,8 +1,6 @@
using System;
using System.Collections.Generic;
using System.Reflection;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Routing;
using Microsoft.Extensions.Options;
using Umbraco.Core.Configuration;
@@ -47,7 +45,7 @@ namespace Umbraco.ModelsBuilder.Embedded
/// <summary>
/// Handles the <see cref="UmbracoApplicationStarting"/> notification
/// </summary>
public Task HandleAsync(UmbracoApplicationStarting notification, CancellationToken cancellationToken)
public void Handle(UmbracoApplicationStarting notification)
{
// always setup the dashboard
// note: UmbracoApiController instances are automatically registered
@@ -57,14 +55,12 @@ namespace Umbraco.ModelsBuilder.Embedded
{
FileService.SavingTemplate += FileService_SavingTemplate;
}
return Task.CompletedTask;
}
/// <summary>
/// Handles the <see cref="ServerVariablesParsing"/> notification
/// </summary>
public Task HandleAsync(ServerVariablesParsing notification, CancellationToken cancellationToken)
public void Handle(ServerVariablesParsing notification)
{
IDictionary<string, object> serverVars = notification.ServerVariables;
@@ -96,8 +92,6 @@ namespace Umbraco.ModelsBuilder.Embedded
umbracoUrls["modelsBuilderBaseUrl"] = _linkGenerator.GetUmbracoApiServiceBaseUrl<ModelsBuilderDashboardController>(controller => controller.BuildModels());
umbracoPlugins["modelsBuilder"] = GetModelsBuilderSettings();
return Task.CompletedTask;
}
private Dictionary<string, object> GetModelsBuilderSettings()

View File

@@ -1,6 +1,4 @@
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Options;
using Umbraco.Core.Configuration;
using Umbraco.Core.Configuration.Models;
@@ -40,10 +38,9 @@ namespace Umbraco.ModelsBuilder.Embedded
/// <summary>
/// Handles the <see cref="UmbracoApplicationStarting"/> notification
/// </summary>
public Task HandleAsync(UmbracoApplicationStarting notification, CancellationToken cancellationToken)
public void Handle(UmbracoApplicationStarting notification)
{
Install();
return Task.CompletedTask;
}
private void Install()

View File

@@ -28,6 +28,22 @@ namespace Umbraco.Tests.UnitTests.Umbraco.Core.Events
_builder = new UmbracoBuilder(register, Mock.Of<IConfiguration>(), TestHelper.GetMockedTypeLoader());
}
[Test]
public async Task CanPublishAsyncEvents()
{
_builder.Services.AddScoped<Adder>();
_builder.AddNotificationAsyncHandler<Notification, NotificationAsyncHandlerA>();
_builder.AddNotificationAsyncHandler<Notification, NotificationAsyncHandlerB>();
_builder.AddNotificationAsyncHandler<Notification, NotificationAsyncHandlerC>();
ServiceProvider provider = _builder.Services.BuildServiceProvider();
var notification = new Notification();
IEventAggregator aggregator = provider.GetService<IEventAggregator>();
await aggregator.PublishAsync(notification);
Assert.AreEqual(A + B + C, notification.SubscriberCount);
}
[Test]
public async Task CanPublishEvents()
{
@@ -51,19 +67,17 @@ namespace Umbraco.Tests.UnitTests.Umbraco.Core.Events
public class NotificationHandlerA : INotificationHandler<Notification>
{
public Task HandleAsync(Notification notification, CancellationToken cancellationToken)
public void Handle(Notification notification)
{
notification.SubscriberCount += A;
return Task.CompletedTask;
}
}
public class NotificationHandlerB : INotificationHandler<Notification>
{
public Task HandleAsync(Notification notification, CancellationToken cancellationToken)
public void Handle(Notification notification)
{
notification.SubscriberCount += B;
return Task.CompletedTask;
}
}
@@ -73,13 +87,42 @@ namespace Umbraco.Tests.UnitTests.Umbraco.Core.Events
public NotificationHandlerC(Adder adder) => _adder = adder;
public void Handle(Notification notification)
{
notification.SubscriberCount = _adder.Add(notification.SubscriberCount, C);
}
}
public class NotificationAsyncHandlerA : INotificationAsyncHandler<Notification>
{
public Task HandleAsync(Notification notification, CancellationToken cancellationToken)
{
notification.SubscriberCount += A;
return Task.CompletedTask;
}
}
public class NotificationAsyncHandlerB : INotificationAsyncHandler<Notification>
{
public Task HandleAsync(Notification notification, CancellationToken cancellationToken)
{
notification.SubscriberCount += B;
return Task.CompletedTask;
}
}
public class NotificationAsyncHandlerC : INotificationAsyncHandler<Notification>
{
private readonly Adder _adder;
public NotificationAsyncHandlerC(Adder adder) => _adder = adder;
public Task HandleAsync(Notification notification, CancellationToken cancellationToken)
{
notification.SubscriberCount = _adder.Add(notification.SubscriberCount, C);
return Task.CompletedTask;
}
}
public class Adder
{
public int Add(int a, int b) => a + b;

View File

@@ -1,8 +1,6 @@
// Copyright (c) Umbraco.
// See LICENSE for more details.
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using Umbraco.Core.Events;
using Umbraco.Core.Logging;
@@ -46,7 +44,7 @@ namespace Umbraco.Web.Common.Profiler
}
/// <inheritdoc/>
public Task HandleAsync(UmbracoApplicationStarting notification, CancellationToken cancellationToken)
public void Handle(UmbracoApplicationStarting notification)
{
if (_profile)
{
@@ -57,8 +55,6 @@ namespace Umbraco.Web.Common.Profiler
// Stop the profiling of the booting process
_profiler.StopBoot();
}
return Task.CompletedTask;
}
}
}

View File

@@ -466,7 +466,7 @@
syncTreeNode($scope.content, data.path, false, args.reloadChildren);
eventsService.emit("content.saved", { content: $scope.content, action: args.action });
eventsService.emit("content.saved", { content: $scope.content, action: args.action, valid: true });
resetNestedFieldValiation(fieldsToRollback);
ensureDirtyIsSetIfAnyVariantIsDirty();
@@ -474,8 +474,15 @@
return $q.when(data);
},
function (err) {
syncTreeNode($scope.content, $scope.content.path);
if (err.status === 400 && err.data) {
// content was saved but is invalid.
eventsService.emit("content.saved", { content: $scope.content, action: args.action, valid: false });
}
resetNestedFieldValiation(fieldsToRollback);
return $q.reject(err);
@@ -981,7 +988,7 @@
$scope.appChanged = function (activeApp) {
$scope.activeApp = activeApp;
_.forEach($scope.content.apps, function (app) {
app.active = false;
if (app.alias === $scope.activeApp.alias) {

View File

@@ -84,7 +84,7 @@ function contentEditingHelper(fileManager, $q, $location, $routeParams, editorSt
//when true, the url will change but it won't actually re-route
//this is merely here for compatibility, if only the content/media/members used this service we'd prob be ok but tons of editors
//use this service unfortunately and probably packages too.
args.softRedirect = false;
args.softRedirect = false;
}
@@ -123,7 +123,13 @@ function contentEditingHelper(fileManager, $q, $location, $routeParams, editorSt
self.handleSaveError({
showNotifications: args.showNotifications,
softRedirect: args.softRedirect,
err: err
err: err,
rebindCallback: function () {
// if the error contains data, we want to map that back as we want to continue editing this save. Especially important when the content is new as the returned data will contain ID etc.
if(err.data) {
rebindCallback.apply(self, [args.content, err.data]);
}
}
});
//update editor state to what is current
@@ -298,7 +304,7 @@ function contentEditingHelper(fileManager, $q, $location, $routeParams, editorSt
}
// if publishing is allowed also allow schedule publish
// we add this manually becuase it doesn't have a permission so it wont
// we add this manually becuase it doesn't have a permission so it wont
// get picked up by the loop through permissions
if (_.contains(args.content.allowedActions, "U")) {
buttons.subButtons.push(createButtonDefinition("SCHEDULE"));
@@ -622,7 +628,7 @@ function contentEditingHelper(fileManager, $q, $location, $routeParams, editorSt
if (!args.err) {
throw "args.err cannot be null";
}
//When the status is a 400 status with a custom header: X-Status-Reason: Validation failed, we have validation errors.
//Otherwise the error is probably due to invalid data (i.e. someone mucking around with the ids or something).
//Or, some strange server error
@@ -640,7 +646,7 @@ function contentEditingHelper(fileManager, $q, $location, $routeParams, editorSt
if (!this.redirectToCreatedContent(args.err.data.id, args.softRedirect) || args.softRedirect) {
// If we are not redirecting it's because this is not newly created content, else in some cases we are
// soft-redirecting which means the URL will change but the route wont (i.e. creating content).
// soft-redirecting which means the URL will change but the route wont (i.e. creating content).
// In this case we need to detect what properties have changed and re-bind them with the server data.
if (args.rebindCallback && angular.isFunction(args.rebindCallback)) {
@@ -687,7 +693,7 @@ function contentEditingHelper(fileManager, $q, $location, $routeParams, editorSt
if (!this.redirectToCreatedContent(args.redirectId ? args.redirectId : args.savedContent.id, args.softRedirect) || args.softRedirect) {
// If we are not redirecting it's because this is not newly created content, else in some cases we are
// soft-redirecting which means the URL will change but the route wont (i.e. creating content).
// soft-redirecting which means the URL will change but the route wont (i.e. creating content).
// In this case we need to detect what properties have changed and re-bind them with the server data.
if (args.rebindCallback && angular.isFunction(args.rebindCallback)) {
@@ -723,7 +729,7 @@ function contentEditingHelper(fileManager, $q, $location, $routeParams, editorSt
navigationService.setSoftRedirect();
}
//change to new path
$location.path("/" + $routeParams.section + "/" + $routeParams.tree + "/" + $routeParams.method + "/" + id);
$location.path("/" + $routeParams.section + "/" + $routeParams.tree + "/" + $routeParams.method + "/" + id);
//don't add a browser history for this
$location.replace();
return true;

View File

@@ -104,19 +104,18 @@
margin: 0 auto;
list-style: none;
width: 100%;
display: flex;
flex-flow: row wrap;
justify-content: flex-start;
}
.umb-card-grid li {
font-size: 12px;
text-align: center;
box-sizing: border-box;
position: relative;
width: 100px;
margin-bottom: 5px;
}
.umb-card-grid.-six-in-row li {
@@ -142,18 +141,20 @@
.umb-card-grid .umb-card-grid-item {
position: relative;
display: block;
width: 100%;
//height: 100%;
padding-top: 100%;
width: 100%;
height: 100%;
padding: 10px 5px;
border-radius: @baseBorderRadius * 2;
transition: background-color 120ms;
font-size: 13px;
line-height: 1.3em;
display: flex;
flex-direction: column;
align-items: center;
justify-content: flex-start;
> span {
position: absolute;
top: 10px;
bottom: 10px;
left: 10px;
right: 10px;
position: relative;
display: flex;
align-items: center;
justify-content: center;