Fixed merge conflicts
This commit is contained in:
@@ -3,6 +3,7 @@
|
||||
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Umbraco.Cms.Core.Events;
|
||||
using Umbraco.Cms.Core.Services;
|
||||
using Umbraco.Cms.Core.Sync;
|
||||
using Umbraco.Cms.Infrastructure.Persistence;
|
||||
|
||||
@@ -13,34 +14,41 @@ namespace Umbraco.Cms.Core.Cache
|
||||
/// </summary>
|
||||
public sealed class DatabaseServerMessengerNotificationHandler : INotificationHandler<UmbracoApplicationStarting>, INotificationHandler<UmbracoRequestEnd>
|
||||
{
|
||||
private readonly IServerMessenger _messenger;
|
||||
private readonly IServerMessenger _messenger;
|
||||
private readonly IUmbracoDatabaseFactory _databaseFactory;
|
||||
private readonly IDistributedCacheBinder _distributedCacheBinder;
|
||||
private readonly ILogger<DatabaseServerMessengerNotificationHandler> _logger;
|
||||
private readonly IRuntimeState _runtimeState;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="DatabaseServerMessengerNotificationHandler"/> class.
|
||||
/// </summary>
|
||||
public DatabaseServerMessengerNotificationHandler(
|
||||
IServerMessenger serverMessenger,
|
||||
IServerMessenger serverMessenger,
|
||||
IUmbracoDatabaseFactory databaseFactory,
|
||||
IDistributedCacheBinder distributedCacheBinder,
|
||||
ILogger<DatabaseServerMessengerNotificationHandler> logger)
|
||||
{
|
||||
ILogger<DatabaseServerMessengerNotificationHandler> logger,
|
||||
IRuntimeState runtimeState)
|
||||
{
|
||||
_databaseFactory = databaseFactory;
|
||||
_distributedCacheBinder = distributedCacheBinder;
|
||||
_logger = logger;
|
||||
_messenger = serverMessenger;
|
||||
_runtimeState = runtimeState;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void Handle(UmbracoApplicationStarting notification)
|
||||
{
|
||||
if (_databaseFactory.CanConnect == false)
|
||||
{
|
||||
_logger.LogWarning("Cannot connect to the database, distributed calls will not be enabled for this server.");
|
||||
}
|
||||
else if (_runtimeState.Level != RuntimeLevel.Run)
|
||||
{
|
||||
_logger.LogWarning("Cannot connect to the database, distributed calls will not be enabled for this server.");
|
||||
_logger.LogWarning("Distributed calls are not available outside the Run runtime level");
|
||||
}
|
||||
else
|
||||
else
|
||||
{
|
||||
_distributedCacheBinder.BindEvents();
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@ using Umbraco.Cms.Core.Models.Membership;
|
||||
using Umbraco.Cms.Core.Services;
|
||||
using Umbraco.Cms.Core.Services.Changes;
|
||||
using Umbraco.Cms.Core.Services.Implement;
|
||||
using Umbraco.Cms.Infrastructure.Services.Notifications;
|
||||
using Umbraco.Extensions;
|
||||
|
||||
namespace Umbraco.Cms.Core.Cache
|
||||
@@ -18,7 +19,11 @@ namespace Umbraco.Cms.Core.Cache
|
||||
/// <summary>
|
||||
/// Default <see cref="IDistributedCacheBinder"/> implementation.
|
||||
/// </summary>
|
||||
public partial class DistributedCacheBinder
|
||||
public partial class DistributedCacheBinder :
|
||||
INotificationHandler<DictionaryItemDeletedNotification>,
|
||||
INotificationHandler<DictionaryItemSavedNotification>,
|
||||
INotificationHandler<LanguageSavedNotification>,
|
||||
INotificationHandler<LanguageDeletedNotification>
|
||||
{
|
||||
private List<Action> _unbinders;
|
||||
|
||||
@@ -59,12 +64,6 @@ namespace Umbraco.Cms.Core.Cache
|
||||
Bind(() => UserService.DeletedUser += UserService_DeletedUser,
|
||||
() => UserService.DeletedUser -= UserService_DeletedUser);
|
||||
|
||||
// bind to dictionary events
|
||||
Bind(() => LocalizationService.DeletedDictionaryItem += LocalizationService_DeletedDictionaryItem,
|
||||
() => LocalizationService.DeletedDictionaryItem -= LocalizationService_DeletedDictionaryItem);
|
||||
Bind(() => LocalizationService.SavedDictionaryItem += LocalizationService_SavedDictionaryItem,
|
||||
() => LocalizationService.SavedDictionaryItem -= LocalizationService_SavedDictionaryItem);
|
||||
|
||||
// bind to data type events
|
||||
Bind(() => DataTypeService.Deleted += DataTypeService_Deleted,
|
||||
() => DataTypeService.Deleted -= DataTypeService_Deleted);
|
||||
@@ -83,12 +82,6 @@ namespace Umbraco.Cms.Core.Cache
|
||||
Bind(() => DomainService.Deleted += DomainService_Deleted,
|
||||
() => DomainService.Deleted -= DomainService_Deleted);
|
||||
|
||||
// bind to language events
|
||||
Bind(() => LocalizationService.SavedLanguage += LocalizationService_SavedLanguage,
|
||||
() => LocalizationService.SavedLanguage -= LocalizationService_SavedLanguage);
|
||||
Bind(() => LocalizationService.DeletedLanguage += LocalizationService_DeletedLanguage,
|
||||
() => LocalizationService.DeletedLanguage -= LocalizationService_DeletedLanguage);
|
||||
|
||||
// bind to content type events
|
||||
Bind(() => ContentTypeService.Changed += ContentTypeService_Changed,
|
||||
() => ContentTypeService.Changed -= ContentTypeService_Changed);
|
||||
@@ -193,17 +186,20 @@ namespace Umbraco.Cms.Core.Cache
|
||||
#endregion
|
||||
|
||||
#region LocalizationService / Dictionary
|
||||
|
||||
private void LocalizationService_SavedDictionaryItem(ILocalizationService sender, SaveEventArgs<IDictionaryItem> e)
|
||||
public void Handle(DictionaryItemSavedNotification notification)
|
||||
{
|
||||
foreach (var entity in e.SavedEntities)
|
||||
foreach (IDictionaryItem entity in notification.SavedEntities)
|
||||
{
|
||||
_distributedCache.RefreshDictionaryCache(entity.Id);
|
||||
}
|
||||
}
|
||||
|
||||
private void LocalizationService_DeletedDictionaryItem(ILocalizationService sender, DeleteEventArgs<IDictionaryItem> e)
|
||||
public void Handle(DictionaryItemDeletedNotification notification)
|
||||
{
|
||||
foreach (var entity in e.DeletedEntities)
|
||||
foreach (IDictionaryItem entity in notification.DeletedEntities)
|
||||
{
|
||||
_distributedCache.RemoveDictionaryCache(entity.Id);
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
@@ -245,23 +241,25 @@ namespace Umbraco.Cms.Core.Cache
|
||||
/// <summary>
|
||||
/// Fires when a language is deleted
|
||||
/// </summary>
|
||||
/// <param name="sender"></param>
|
||||
/// <param name="e"></param>
|
||||
private void LocalizationService_DeletedLanguage(ILocalizationService sender, DeleteEventArgs<ILanguage> e)
|
||||
/// <param name="notification"></param>
|
||||
public void Handle(LanguageDeletedNotification notification)
|
||||
{
|
||||
foreach (var entity in e.DeletedEntities)
|
||||
foreach (ILanguage entity in notification.DeletedEntities)
|
||||
{
|
||||
_distributedCache.RemoveLanguageCache(entity);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Fires when a language is saved
|
||||
/// </summary>
|
||||
/// <param name="sender"></param>
|
||||
/// <param name="e"></param>
|
||||
private void LocalizationService_SavedLanguage(ILocalizationService sender, SaveEventArgs<ILanguage> e)
|
||||
/// <param name="notification"></param>
|
||||
public void Handle(LanguageSavedNotification notification)
|
||||
{
|
||||
foreach (var entity in e.SavedEntities)
|
||||
foreach (ILanguage entity in notification.SavedEntities)
|
||||
{
|
||||
_distributedCache.RefreshLanguageCache(entity);
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
@@ -65,6 +65,13 @@ namespace Umbraco.Cms.Core.Compose
|
||||
.AddNotificationHandler<ContentMovingNotification, RedirectTrackingHandler>()
|
||||
.AddNotificationHandler<ContentMovedNotification, RedirectTrackingHandler>();
|
||||
|
||||
// Add notification handlers for DistributedCache
|
||||
builder
|
||||
.AddNotificationHandler<DictionaryItemDeletedNotification, DistributedCacheBinder>()
|
||||
.AddNotificationHandler<DictionaryItemSavedNotification, DistributedCacheBinder>()
|
||||
.AddNotificationHandler<LanguageSavedNotification, DistributedCacheBinder>()
|
||||
.AddNotificationHandler<LanguageDeletedNotification, DistributedCacheBinder>();
|
||||
|
||||
// add notification handlers for auditing
|
||||
builder
|
||||
.AddNotificationHandler<MemberSavedNotification, AuditNotificationsHandler>()
|
||||
|
||||
@@ -1,22 +1,20 @@
|
||||
using System;
|
||||
using System;
|
||||
using System.IO;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.Configuration.Json;
|
||||
using Microsoft.Extensions.FileProviders;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using Umbraco.Cms.Core.Configuration;
|
||||
|
||||
namespace Umbraco.Cms.Core.Configuration
|
||||
{
|
||||
public class JsonConfigManipulator : IConfigManipulator
|
||||
{
|
||||
private static readonly object s_locker = new object();
|
||||
private readonly IConfiguration _configuration;
|
||||
private readonly object _locker = new object();
|
||||
|
||||
public JsonConfigManipulator(IConfiguration configuration)
|
||||
{
|
||||
_configuration = configuration;
|
||||
}
|
||||
public JsonConfigManipulator(IConfiguration configuration) => _configuration = configuration;
|
||||
|
||||
public string UmbracoConnectionPath { get; } = $"ConnectionStrings:{ Cms.Core.Constants.System.UmbracoConnectionName}";
|
||||
public void RemoveConnectionString()
|
||||
@@ -53,11 +51,13 @@ namespace Umbraco.Cms.Core.Configuration
|
||||
JToken token = json;
|
||||
foreach (var propertyName in key.Split(new[] { ':' }))
|
||||
{
|
||||
if (token is null) break;
|
||||
if (token is null)
|
||||
break;
|
||||
token = CaseSelectPropertyValues(token, propertyName);
|
||||
}
|
||||
|
||||
if (token is null) return;
|
||||
if (token is null)
|
||||
return;
|
||||
|
||||
var writer = new JTokenWriter();
|
||||
writer.WriteValue(value);
|
||||
@@ -162,11 +162,9 @@ namespace Umbraco.Cms.Core.Configuration
|
||||
token?.Parent?.Remove();
|
||||
}
|
||||
|
||||
|
||||
|
||||
private static void SaveJson(JsonConfigurationProvider provider, JObject json)
|
||||
private void SaveJson(JsonConfigurationProvider provider, JObject json)
|
||||
{
|
||||
lock (s_locker)
|
||||
lock (_locker)
|
||||
{
|
||||
if (provider.Source.FileProvider is PhysicalFileProvider physicalFileProvider)
|
||||
{
|
||||
@@ -184,26 +182,26 @@ namespace Umbraco.Cms.Core.Configuration
|
||||
}
|
||||
}
|
||||
|
||||
private static JObject GetJson(JsonConfigurationProvider provider)
|
||||
private JObject GetJson(JsonConfigurationProvider provider)
|
||||
{
|
||||
if (provider.Source.FileProvider is PhysicalFileProvider physicalFileProvider)
|
||||
lock (_locker)
|
||||
{
|
||||
var jsonFilePath = Path.Combine(physicalFileProvider.Root, provider.Source.Path);
|
||||
|
||||
var serializer = new JsonSerializer();
|
||||
using (var sr = new StreamReader(jsonFilePath))
|
||||
using (var jsonTextReader = new JsonTextReader(sr))
|
||||
if (provider.Source.FileProvider is PhysicalFileProvider physicalFileProvider)
|
||||
{
|
||||
return serializer.Deserialize<JObject>(jsonTextReader);
|
||||
var jsonFilePath = Path.Combine(physicalFileProvider.Root, provider.Source.Path);
|
||||
|
||||
var serializer = new JsonSerializer();
|
||||
using (var sr = new StreamReader(jsonFilePath))
|
||||
using (var jsonTextReader = new JsonTextReader(sr))
|
||||
{
|
||||
return serializer.Deserialize<JObject>(jsonTextReader);
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
private JsonConfigurationProvider GetJsonConfigurationProvider(string requiredKey = null)
|
||||
{
|
||||
if (_configuration is IConfigurationRoot configurationRoot)
|
||||
|
||||
@@ -66,7 +66,8 @@ namespace Umbraco.Cms.Infrastructure.DependencyInjection
|
||||
.AddRepositories()
|
||||
.AddServices()
|
||||
.AddCoreMappingProfiles()
|
||||
.AddFileSystems();
|
||||
.AddFileSystems()
|
||||
.AddWebAssets();
|
||||
|
||||
// register persistence mappers - required by database factory so needs to be done here
|
||||
// means the only place the collection can be modified is in a runtime - afterwards it
|
||||
|
||||
@@ -18,6 +18,7 @@ namespace Umbraco.Cms.Infrastructure.DependencyInjection
|
||||
// repositories
|
||||
builder.Services.AddUnique<IAuditRepository, AuditRepository>();
|
||||
builder.Services.AddUnique<IAuditEntryRepository, AuditEntryRepository>();
|
||||
builder.Services.AddUnique<ICacheInstructionRepository, CacheInstructionRepository>();
|
||||
builder.Services.AddUnique<IContentTypeRepository, ContentTypeRepository>();
|
||||
builder.Services.AddUnique<IDataTypeContainerRepository, DataTypeContainerRepository>();
|
||||
builder.Services.AddUnique<IDataTypeRepository, DataTypeRepository>();
|
||||
|
||||
@@ -37,6 +37,7 @@ namespace Umbraco.Cms.Infrastructure.DependencyInjection
|
||||
builder.Services.AddUnique<IPublicAccessService, PublicAccessService>();
|
||||
builder.Services.AddUnique<IDomainService, DomainService>();
|
||||
builder.Services.AddUnique<IAuditService, AuditService>();
|
||||
builder.Services.AddUnique<ICacheInstructionService, CacheInstructionService>();
|
||||
builder.Services.AddUnique<ITagService, TagService>();
|
||||
builder.Services.AddUnique<IContentService, ContentService>();
|
||||
builder.Services.AddUnique<IUserService, UserService>();
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Umbraco.Cms.Core.DependencyInjection;
|
||||
using Umbraco.Cms.Core.Events;
|
||||
using Umbraco.Cms.Infrastructure.WebAssets;
|
||||
|
||||
namespace Umbraco.Cms.Infrastructure.DependencyInjection
|
||||
{
|
||||
public static partial class UmbracoBuilderExtensions
|
||||
{
|
||||
internal static IUmbracoBuilder AddWebAssets(this IUmbracoBuilder builder)
|
||||
{
|
||||
builder.Services.AddSingleton<BackOfficeWebAssets>();
|
||||
return builder;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -75,7 +75,7 @@ namespace Umbraco.Cms.Infrastructure.HostedServices
|
||||
_profilingLogger = profilingLogger;
|
||||
}
|
||||
|
||||
internal override async Task PerformExecuteAsync(object state)
|
||||
public override async Task PerformExecuteAsync(object state)
|
||||
{
|
||||
if (_healthChecksSettings.Notification.Enabled == false)
|
||||
{
|
||||
|
||||
@@ -58,7 +58,7 @@ namespace Umbraco.Cms.Infrastructure.HostedServices
|
||||
_httpClientFactory = httpClientFactory;
|
||||
}
|
||||
|
||||
internal override async Task PerformExecuteAsync(object state)
|
||||
public override async Task PerformExecuteAsync(object state)
|
||||
{
|
||||
if (_keepAliveSettings.DisableKeepAliveTask)
|
||||
{
|
||||
|
||||
@@ -59,7 +59,7 @@ namespace Umbraco.Cms.Infrastructure.HostedServices
|
||||
_profilingLogger = profilingLogger;
|
||||
}
|
||||
|
||||
internal override Task PerformExecuteAsync(object state)
|
||||
public override Task PerformExecuteAsync(object state)
|
||||
{
|
||||
switch (_serverRegistrar.CurrentServerRole)
|
||||
{
|
||||
|
||||
@@ -54,7 +54,7 @@ namespace Umbraco.Cms.Infrastructure.HostedServices
|
||||
// Hat-tip: https://stackoverflow.com/a/14207615/489433
|
||||
await PerformExecuteAsync(state);
|
||||
|
||||
internal abstract Task PerformExecuteAsync(object state);
|
||||
public abstract Task PerformExecuteAsync(object state);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public Task StopAsync(CancellationToken cancellationToken)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using System;
|
||||
using System;
|
||||
using System.Net.Http;
|
||||
using System.Runtime.Serialization;
|
||||
using System.Text;
|
||||
@@ -35,7 +35,7 @@ namespace Umbraco.Cms.Infrastructure.HostedServices
|
||||
/// Runs the background task to send the anonymous ID
|
||||
/// to telemetry service
|
||||
/// </summary>
|
||||
internal override async Task PerformExecuteAsync(object state)
|
||||
public override async Task PerformExecuteAsync(object state)
|
||||
{
|
||||
// Try & get a value stored in umbracoSettings.config on the backoffice XML element ID attribute
|
||||
var backofficeIdentifierRaw = _globalSettings.Value.Id;
|
||||
|
||||
@@ -60,7 +60,7 @@ namespace Umbraco.Cms.Infrastructure.HostedServices
|
||||
_serverMessenger = serverMessenger;
|
||||
}
|
||||
|
||||
internal override Task PerformExecuteAsync(object state)
|
||||
public override Task PerformExecuteAsync(object state)
|
||||
{
|
||||
if (Suspendable.ScheduledPublishing.CanRun == false)
|
||||
{
|
||||
|
||||
@@ -36,7 +36,7 @@ namespace Umbraco.Cms.Infrastructure.HostedServices.ServerRegistration
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
internal override Task PerformExecuteAsync(object state)
|
||||
public override Task PerformExecuteAsync(object state)
|
||||
{
|
||||
if (_runtimeState.Level != RuntimeLevel.Run)
|
||||
{
|
||||
|
||||
@@ -47,7 +47,7 @@ namespace Umbraco.Cms.Infrastructure.HostedServices.ServerRegistration
|
||||
_globalSettings = globalSettings.Value;
|
||||
}
|
||||
|
||||
internal override Task PerformExecuteAsync(object state)
|
||||
public override Task PerformExecuteAsync(object state)
|
||||
{
|
||||
if (_runtimeState.Level != RuntimeLevel.Run)
|
||||
{
|
||||
|
||||
@@ -42,7 +42,7 @@ namespace Umbraco.Cms.Infrastructure.HostedServices
|
||||
_tempFolders = _ioHelper.GetTempFolders();
|
||||
}
|
||||
|
||||
internal override Task PerformExecuteAsync(object state)
|
||||
public override Task PerformExecuteAsync(object state)
|
||||
{
|
||||
// Ensure we do not run if not main domain
|
||||
if (_mainDom.IsMainDom == false)
|
||||
|
||||
@@ -9,7 +9,7 @@ namespace Umbraco.Cms.Core.Logging.Serilog.Enrichers
|
||||
/// <summary>
|
||||
/// Enrich log events with a HttpRequestId GUID.
|
||||
/// Original source - https://github.com/serilog-web/classic/blob/master/src/SerilogWeb.Classic/Classic/Enrichers/HttpRequestIdEnricher.cs
|
||||
/// Nupkg: 'Serilog.Web.Classic' contains handlers & extra bits we do not want
|
||||
/// Nupkg: 'Serilog.Web.Classic' contains handlers and extra bits we do not want
|
||||
/// </summary>
|
||||
public class HttpRequestIdEnricher : ILogEventEnricher
|
||||
{
|
||||
|
||||
@@ -10,7 +10,7 @@ namespace Umbraco.Cms.Core.Logging.Serilog.Enrichers
|
||||
/// Enrich log events with a HttpRequestNumber unique within the current
|
||||
/// logging session.
|
||||
/// Original source - https://github.com/serilog-web/classic/blob/master/src/SerilogWeb.Classic/Classic/Enrichers/HttpRequestNumberEnricher.cs
|
||||
/// Nupkg: 'Serilog.Web.Classic' contains handlers & extra bits we do not want
|
||||
/// Nupkg: 'Serilog.Web.Classic' contains handlers and extra bits we do not want
|
||||
/// </summary>
|
||||
public class HttpRequestNumberEnricher : ILogEventEnricher
|
||||
{
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
using Serilog.Core;
|
||||
using System;
|
||||
using Serilog.Core;
|
||||
using Serilog.Events;
|
||||
using System;
|
||||
using Umbraco.Cms.Core.Net;
|
||||
|
||||
namespace Umbraco.Cms.Core.Logging.Serilog.Enrichers
|
||||
@@ -8,7 +8,7 @@ namespace Umbraco.Cms.Core.Logging.Serilog.Enrichers
|
||||
/// <summary>
|
||||
/// Enrich log events with the HttpSessionId property.
|
||||
/// Original source - https://github.com/serilog-web/classic/blob/master/src/SerilogWeb.Classic/Classic/Enrichers/HttpSessionIdEnricher.cs
|
||||
/// Nupkg: 'Serilog.Web.Classic' contains handlers & extra bits we do not want
|
||||
/// Nupkg: 'Serilog.Web.Classic' contains handlers and extra bits we do not want
|
||||
/// </summary>
|
||||
public class HttpSessionIdEnricher : ILogEventEnricher
|
||||
{
|
||||
|
||||
@@ -9,7 +9,7 @@ using Umbraco.Extensions;
|
||||
namespace Umbraco.Cms.Core.Logging.Serilog
|
||||
{
|
||||
///<summary>
|
||||
/// Implements <see cref="ILogger"/> on top of Serilog.
|
||||
/// Implements MS ILogger on top of Serilog.
|
||||
///</summary>
|
||||
public class SerilogLogger : IDisposable
|
||||
{
|
||||
|
||||
@@ -22,7 +22,7 @@ namespace Umbraco.Cms.Core.Logging.Viewer
|
||||
|
||||
/// <summary>
|
||||
/// A count of number of errors
|
||||
/// By counting Warnings with Exceptions, Errors & Fatal messages
|
||||
/// By counting Warnings with Exceptions, Errors & Fatal messages
|
||||
/// </summary>
|
||||
int GetNumberOfErrors(LogTimePeriod logTimePeriod);
|
||||
|
||||
|
||||
@@ -156,7 +156,7 @@ namespace Umbraco.Cms.Infrastructure.Migrations.Install
|
||||
{
|
||||
_configManipulator.SaveConnectionString(EmbeddedDatabaseConnectionString, Constants.DbProviderNames.SqlCe);
|
||||
|
||||
var path = _hostingEnvironment.MapPathContentRoot(Path.Combine("App_Data", "Umbraco.sdf"));
|
||||
var path = _hostingEnvironment.MapPathContentRoot(Path.Combine(Constants.SystemDirectories.Data, "Umbraco.sdf"));
|
||||
if (File.Exists(path) == false)
|
||||
{
|
||||
// this should probably be in a "using (new SqlCeEngine)" clause but not sure
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using System;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Microsoft.Extensions.Logging;
|
||||
@@ -76,6 +76,7 @@ namespace Umbraco.Cms.Infrastructure.Migrations.Install
|
||||
typeof (AccessRuleDto),
|
||||
typeof (CacheInstructionDto),
|
||||
typeof (ExternalLoginDto),
|
||||
typeof (ExternalLoginTokenDto),
|
||||
typeof (RedirectUrlDto),
|
||||
typeof (LockDto),
|
||||
typeof (UserGroupDto),
|
||||
|
||||
@@ -196,11 +196,15 @@ namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade
|
||||
|
||||
// to 8.9.0
|
||||
To<ExternalLoginTableUserData>("{B5838FF5-1D22-4F6C-BCEB-F83ACB14B575}");
|
||||
// to 8.10.0
|
||||
To<AddPropertyTypeLabelOnTopColumn>("{D6A8D863-38EC-44FB-91EC-ACD6A668BD18}");
|
||||
|
||||
// to 8.10.0
|
||||
To<AddPropertyTypeLabelOnTopColumn>("{D6A8D863-38EC-44FB-91EC-ACD6A668BD18}");
|
||||
|
||||
// to 9.0.0
|
||||
To<MigrateLogViewerQueriesFromFileToDb>("{22D801BA-A1FF-4539-BFCC-2139B55594F8}");
|
||||
To<ExternalLoginTableIndexes>("{50A43237-A6F4-49E2-A7A6-5DAD65C84669}");
|
||||
To<ExternalLoginTokenTable>("{3D8DADEF-0FDA-4377-A5F0-B52C2110E8F2}");
|
||||
|
||||
//FINAL
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Umbraco.Cms.Infrastructure.Persistence.Dtos;
|
||||
|
||||
namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_9_0_0
|
||||
{
|
||||
public class ExternalLoginTableIndexes : MigrationBase
|
||||
{
|
||||
public ExternalLoginTableIndexes(IMigrationContext context)
|
||||
: base(context)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds new indexes to the External Login table
|
||||
/// </summary>
|
||||
public override void Migrate()
|
||||
{
|
||||
// Before adding these indexes we need to remove duplicate data.
|
||||
// Get all logins by latest
|
||||
var logins = Database.Fetch<ExternalLoginDto>()
|
||||
.OrderByDescending(x => x.CreateDate)
|
||||
.ToList();
|
||||
|
||||
var toDelete = new List<int>();
|
||||
// used to track duplicates so they can be removed
|
||||
var keys = new HashSet<(string, string)>();
|
||||
foreach(ExternalLoginDto login in logins)
|
||||
{
|
||||
if (!keys.Add((login.ProviderKey, login.LoginProvider)))
|
||||
{
|
||||
// if it already exists we need to remove this one
|
||||
toDelete.Add(login.Id);
|
||||
}
|
||||
}
|
||||
if (toDelete.Count > 0)
|
||||
{
|
||||
Database.DeleteMany<ExternalLoginDto>().Where(x => toDelete.Contains(x.Id)).Execute();
|
||||
}
|
||||
|
||||
var indexName1 = "IX_" + ExternalLoginDto.TableName + "_LoginProvider";
|
||||
|
||||
if (!IndexExists(indexName1))
|
||||
{
|
||||
Create
|
||||
.Index(indexName1)
|
||||
.OnTable(ExternalLoginDto.TableName)
|
||||
.OnColumn("loginProvider")
|
||||
.Ascending()
|
||||
.WithOptions()
|
||||
.Unique()
|
||||
.WithOptions()
|
||||
.NonClustered()
|
||||
.Do();
|
||||
}
|
||||
|
||||
var indexName2 = "IX_" + ExternalLoginDto.TableName + "_ProviderKey";
|
||||
|
||||
if (!IndexExists(indexName2))
|
||||
{
|
||||
Create
|
||||
.Index(indexName2)
|
||||
.OnTable(ExternalLoginDto.TableName)
|
||||
.OnColumn("loginProvider").Ascending()
|
||||
.OnColumn("providerKey").Ascending()
|
||||
.WithOptions()
|
||||
.NonClustered()
|
||||
.Do();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
using System.Collections.Generic;
|
||||
using Umbraco.Cms.Infrastructure.Persistence.Dtos;
|
||||
using Umbraco.Extensions;
|
||||
|
||||
namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_9_0_0
|
||||
{
|
||||
public class ExternalLoginTokenTable : MigrationBase
|
||||
{
|
||||
public ExternalLoginTokenTable(IMigrationContext context)
|
||||
: base(context)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds new External Login token table
|
||||
/// </summary>
|
||||
public override void Migrate()
|
||||
{
|
||||
IEnumerable<string> tables = SqlSyntax.GetTablesInSchema(Context.Database);
|
||||
if (tables.InvariantContains(ExternalLoginTokenDto.TableName))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Create.Table<ExternalLoginTokenDto>().Do();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,34 +1,39 @@
|
||||
using System;
|
||||
using System;
|
||||
using NPoco;
|
||||
using Umbraco.Cms.Infrastructure.Persistence.DatabaseAnnotations;
|
||||
using Umbraco.Cms.Infrastructure.Persistence.DatabaseModelDefinitions;
|
||||
|
||||
namespace Umbraco.Cms.Infrastructure.Persistence.Dtos
|
||||
{
|
||||
[TableName(Cms.Core.Constants.DatabaseSchema.Tables.ExternalLogin)]
|
||||
[TableName(TableName)]
|
||||
[ExplicitColumns]
|
||||
[PrimaryKey("Id")]
|
||||
internal class ExternalLoginDto
|
||||
{
|
||||
public const string TableName = Cms.Core.Constants.DatabaseSchema.Tables.ExternalLogin;
|
||||
|
||||
[Column("id")]
|
||||
[PrimaryKeyColumn(Name = "PK_umbracoExternalLogin")]
|
||||
[PrimaryKeyColumn]
|
||||
public int Id { get; set; }
|
||||
|
||||
// TODO: This is completely missing a FK!!?
|
||||
// TODO: This is completely missing a FK!!? ... IIRC that is because we want to change this to a GUID
|
||||
// to support both members and users for external logins and that will not have any referential integrity
|
||||
// This should be part of the members task for enabling external logins.
|
||||
|
||||
[Column("userId")]
|
||||
[Index(IndexTypes.NonClustered)]
|
||||
public int UserId { get; set; }
|
||||
|
||||
// TODO: There should be an index on both LoginProvider and ProviderKey
|
||||
|
||||
[Column("loginProvider")]
|
||||
[Length(4000)]
|
||||
[Length(4000)] // TODO: This value seems WAY too high, this is just a name
|
||||
[NullSetting(NullSetting = NullSettings.NotNull)]
|
||||
[Index(IndexTypes.UniqueNonClustered, Name = "IX_" + TableName + "_LoginProvider")]
|
||||
public string LoginProvider { get; set; }
|
||||
|
||||
[Column("providerKey")]
|
||||
[Length(4000)]
|
||||
[NullSetting(NullSetting = NullSettings.NotNull)]
|
||||
[Index(IndexTypes.NonClustered, ForColumns = "loginProvider,providerKey", Name = "IX_" + TableName + "_ProviderKey")]
|
||||
public string ProviderKey { get; set; }
|
||||
|
||||
[Column("createDate")]
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
using System;
|
||||
using NPoco;
|
||||
using Umbraco.Cms.Infrastructure.Persistence.DatabaseAnnotations;
|
||||
using Umbraco.Cms.Infrastructure.Persistence.DatabaseModelDefinitions;
|
||||
|
||||
namespace Umbraco.Cms.Infrastructure.Persistence.Dtos
|
||||
{
|
||||
[TableName(TableName)]
|
||||
[ExplicitColumns]
|
||||
[PrimaryKey("Id")]
|
||||
internal class ExternalLoginTokenDto
|
||||
{
|
||||
public const string TableName = Cms.Core.Constants.DatabaseSchema.Tables.ExternalLoginToken;
|
||||
|
||||
[Column("id")]
|
||||
[PrimaryKeyColumn]
|
||||
public int Id { get; set; }
|
||||
|
||||
[Column("externalLoginId")]
|
||||
[ForeignKey(typeof(ExternalLoginDto), Column = "id")]
|
||||
public int ExternalLoginId { get; set; }
|
||||
|
||||
[Column("name")]
|
||||
[Length(255)]
|
||||
[NullSetting(NullSetting = NullSettings.NotNull)]
|
||||
[Index(IndexTypes.UniqueNonClustered, ForColumns = "externalLoginId,name", Name = "IX_" + TableName + "_Name")]
|
||||
public string Name { get; set; }
|
||||
|
||||
[Column("value")]
|
||||
[SpecialDbType(SpecialDbTypes.NVARCHARMAX)]
|
||||
[NullSetting(NullSetting = NullSettings.NotNull)]
|
||||
public string Value { get; set; }
|
||||
|
||||
[Column("createDate")]
|
||||
[Constraint(Default = SystemMethods.CurrentDateTime)]
|
||||
public DateTime CreateDate { get; set; }
|
||||
|
||||
[ResultColumn]
|
||||
[Reference(ReferenceType.OneToOne, ColumnName = "ExternalLoginId")]
|
||||
public ExternalLoginDto ExternalLoginDto { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Umbraco.Cms.Core.Models;
|
||||
using Umbraco.Cms.Infrastructure.Persistence.Dtos;
|
||||
|
||||
namespace Umbraco.Cms.Infrastructure.Persistence.Factories
|
||||
{
|
||||
internal static class CacheInstructionFactory
|
||||
{
|
||||
public static IEnumerable<CacheInstruction> BuildEntities(IEnumerable<CacheInstructionDto> dtos) => dtos.Select(BuildEntity).ToList();
|
||||
|
||||
public static CacheInstruction BuildEntity(CacheInstructionDto dto) =>
|
||||
new CacheInstruction(dto.Id, dto.UtcStamp, dto.Instructions, dto.OriginIdentity, dto.InstructionCount);
|
||||
|
||||
public static CacheInstructionDto BuildDto(CacheInstruction entity) =>
|
||||
new CacheInstructionDto
|
||||
{
|
||||
Id = entity.Id,
|
||||
UtcStamp = entity.UtcStamp,
|
||||
Instructions = entity.Instructions,
|
||||
OriginIdentity = entity.OriginIdentity,
|
||||
InstructionCount = entity.InstructionCount,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -6,6 +6,15 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Factories
|
||||
{
|
||||
internal static class ExternalLoginFactory
|
||||
{
|
||||
public static IIdentityUserToken BuildEntity(ExternalLoginTokenDto dto)
|
||||
{
|
||||
var entity = new IdentityUserToken(dto.Id, dto.ExternalLoginDto.LoginProvider, dto.Name, dto.Value, dto.ExternalLoginDto.UserId.ToString(), dto.CreateDate);
|
||||
|
||||
// reset dirty initial properties (U4-1946)
|
||||
entity.ResetDirtyProperties(false);
|
||||
return entity;
|
||||
}
|
||||
|
||||
public static IIdentityUserLogin BuildEntity(ExternalLoginDto dto)
|
||||
{
|
||||
var entity = new IdentityUserLogin(dto.Id, dto.LoginProvider, dto.ProviderKey, dto.UserId.ToString(), dto.CreateDate)
|
||||
@@ -47,5 +56,19 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Factories
|
||||
|
||||
return dto;
|
||||
}
|
||||
|
||||
public static ExternalLoginTokenDto BuildDto(int externalLoginId, IExternalLoginToken token, int? id = null)
|
||||
{
|
||||
var dto = new ExternalLoginTokenDto
|
||||
{
|
||||
Id = id ?? default,
|
||||
ExternalLoginId = externalLoginId,
|
||||
Name = token.Name,
|
||||
Value = token.Value,
|
||||
CreateDate = DateTime.Now
|
||||
};
|
||||
|
||||
return dto;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using System;
|
||||
using System;
|
||||
using Umbraco.Cms.Core.Models.Identity;
|
||||
using Umbraco.Cms.Infrastructure.Persistence.Dtos;
|
||||
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
using System;
|
||||
using Umbraco.Cms.Core.Models.Identity;
|
||||
using Umbraco.Cms.Infrastructure.Persistence.Dtos;
|
||||
|
||||
namespace Umbraco.Cms.Infrastructure.Persistence.Mappers
|
||||
{
|
||||
[MapperFor(typeof(IIdentityUserToken))]
|
||||
[MapperFor(typeof(IdentityUserToken))]
|
||||
public sealed class ExternalLoginTokenMapper : BaseMapper
|
||||
{
|
||||
public ExternalLoginTokenMapper(Lazy<ISqlContext> sqlContext, MapperConfigurationStore maps)
|
||||
: base(sqlContext, maps)
|
||||
{ }
|
||||
|
||||
protected override void DefineMaps()
|
||||
{
|
||||
DefineMap<IdentityUserToken, ExternalLoginTokenDto>(nameof(IdentityUserToken.Id), nameof(ExternalLoginTokenDto.Id));
|
||||
DefineMap<IdentityUserToken, ExternalLoginTokenDto>(nameof(IdentityUserToken.CreateDate), nameof(ExternalLoginTokenDto.CreateDate));
|
||||
DefineMap<IdentityUserToken, ExternalLoginTokenDto>(nameof(IdentityUserToken.Name), nameof(ExternalLoginTokenDto.Name));
|
||||
DefineMap<IdentityUserToken, ExternalLoginTokenDto>(nameof(IdentityUserToken.Value), nameof(ExternalLoginTokenDto.Value));
|
||||
// separate table
|
||||
DefineMap<IdentityUserToken, ExternalLoginDto>(nameof(IdentityUserToken.UserId), nameof(ExternalLoginDto.UserId));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Umbraco.Cms.Core.Composing;
|
||||
|
||||
namespace Umbraco.Cms.Infrastructure.Persistence.Mappers
|
||||
@@ -50,6 +50,7 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Mappers
|
||||
Add<UmbracoEntityMapper>();
|
||||
Add<UserMapper>();
|
||||
Add<ExternalLoginMapper>();
|
||||
Add<ExternalLoginTokenMapper>();
|
||||
Add<UserGroupMapper>();
|
||||
Add<AuditEntryMapper>();
|
||||
Add<ConsentMapper>();
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using System;
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
@@ -1015,6 +1015,7 @@ namespace Umbraco.Extensions
|
||||
/// </summary>
|
||||
/// <param name="sql">The Sql statement.</param>
|
||||
/// <returns>The Sql statement.</returns>
|
||||
/// <remarks>NOTE: This method will not work for all queries, only simple ones!</remarks>
|
||||
public static Sql<ISqlContext> ForUpdate(this Sql<ISqlContext> sql)
|
||||
{
|
||||
// go find the first FROM clause, and append the lock hint
|
||||
|
||||
@@ -48,7 +48,7 @@ namespace Umbraco.Cms.Core.Persistence.Repositories
|
||||
|
||||
/// <summary>
|
||||
/// Used to bulk update the permissions set for a content item. This will replace all permissions
|
||||
/// assigned to an entity with a list of user id & permission pairs.
|
||||
/// assigned to an entity with a list of user id & permission pairs.
|
||||
/// </summary>
|
||||
/// <param name="permissionSet"></param>
|
||||
void ReplaceContentPermissions(EntityPermissionSet permissionSet);
|
||||
|
||||
@@ -27,9 +27,6 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement
|
||||
: base(scopeAccessor, cache, logger)
|
||||
{ }
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override Guid NodeObjectTypeId => throw new NotSupportedException();
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override IAuditEntry PerformGet(int id)
|
||||
{
|
||||
|
||||
@@ -117,11 +117,6 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
protected override Guid NodeObjectTypeId
|
||||
{
|
||||
get { throw new NotImplementedException(); }
|
||||
}
|
||||
|
||||
public void CleanLogs(int maximumAgeOfLogsInMinutes)
|
||||
{
|
||||
var oldestPermittedLogEntry = DateTime.Now.Subtract(new TimeSpan(0, maximumAgeOfLogsInMinutes, 0));
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using NPoco;
|
||||
using Umbraco.Cms.Core.Models;
|
||||
using Umbraco.Cms.Core.Persistence.Repositories;
|
||||
using Umbraco.Cms.Core.Scoping;
|
||||
using Umbraco.Cms.Infrastructure.Persistence.Dtos;
|
||||
using Umbraco.Cms.Infrastructure.Persistence.Factories;
|
||||
using Umbraco.Extensions;
|
||||
|
||||
namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents the NPoco implementation of <see cref="ICacheInstructionRepository"/>.
|
||||
/// </summary>
|
||||
internal class CacheInstructionRepository : ICacheInstructionRepository
|
||||
{
|
||||
private readonly IScopeAccessor _scopeAccessor;
|
||||
|
||||
public CacheInstructionRepository(IScopeAccessor scopeAccessor) => _scopeAccessor = scopeAccessor;
|
||||
|
||||
/// <inheritdoc/>
|
||||
private IScope AmbientScope => _scopeAccessor.AmbientScope;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public int CountAll()
|
||||
{
|
||||
Sql<ISqlContext> sql = AmbientScope.SqlContext.Sql().Select("COUNT(*)")
|
||||
.From<CacheInstructionDto>();
|
||||
|
||||
return AmbientScope.Database.ExecuteScalar<int>(sql);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public int CountPendingInstructions(int lastId) =>
|
||||
AmbientScope.Database.ExecuteScalar<int>("SELECT SUM(instructionCount) FROM umbracoCacheInstruction WHERE id > @lastId", new { lastId });
|
||||
|
||||
/// <inheritdoc/>
|
||||
public int GetMaxId() =>
|
||||
AmbientScope.Database.ExecuteScalar<int>("SELECT MAX(id) FROM umbracoCacheInstruction");
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool Exists(int id) => AmbientScope.Database.Exists<CacheInstructionDto>(id);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void Add(CacheInstruction cacheInstruction)
|
||||
{
|
||||
CacheInstructionDto dto = CacheInstructionFactory.BuildDto(cacheInstruction);
|
||||
AmbientScope.Database.Insert(dto);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public IEnumerable<CacheInstruction> GetPendingInstructions(int lastId, int maxNumberToRetrieve)
|
||||
{
|
||||
Sql<ISqlContext> sql = AmbientScope.SqlContext.Sql().SelectAll()
|
||||
.From<CacheInstructionDto>()
|
||||
.Where<CacheInstructionDto>(dto => dto.Id > lastId)
|
||||
.OrderBy<CacheInstructionDto>(dto => dto.Id);
|
||||
Sql<ISqlContext> topSql = sql.SelectTop(maxNumberToRetrieve);
|
||||
return AmbientScope.Database.Fetch<CacheInstructionDto>(topSql).Select(CacheInstructionFactory.BuildEntity);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void DeleteInstructionsOlderThan(DateTime pruneDate)
|
||||
{
|
||||
// Using 2 queries is faster than convoluted joins.
|
||||
var maxId = AmbientScope.Database.ExecuteScalar<int>("SELECT MAX(id) FROM umbracoCacheInstruction;");
|
||||
Sql deleteSql = new Sql().Append(@"DELETE FROM umbracoCacheInstruction WHERE utcStamp < @pruneDate AND id < @maxId", new { pruneDate, maxId });
|
||||
AmbientScope.Database.Execute(deleteSql);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -26,9 +26,6 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement
|
||||
: base(scopeAccessor, cache, logger)
|
||||
{ }
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override Guid NodeObjectTypeId => throw new NotSupportedException();
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override IConsent PerformGet(int id)
|
||||
{
|
||||
|
||||
@@ -27,10 +27,9 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement
|
||||
internal sealed class ContentRepositoryBase
|
||||
{
|
||||
/// <summary>
|
||||
///
|
||||
/// This is used for unit tests ONLY
|
||||
/// </summary>
|
||||
public static bool ThrowOnWarning = false;
|
||||
public static bool ThrowOnWarning { get; set; } = false;
|
||||
}
|
||||
|
||||
public abstract class ContentRepositoryBase<TId, TEntity, TRepository> : EntityRepositoryBase<TId, TEntity>, IContentRepository<TId, TEntity>
|
||||
@@ -72,6 +71,11 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement
|
||||
|
||||
protected abstract TRepository This { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the node object type for the repository's entity
|
||||
/// </summary>
|
||||
protected abstract Guid NodeObjectTypeId { get; }
|
||||
|
||||
protected ILanguageRepository LanguageRepository { get; }
|
||||
protected IDataTypeService DataTypeService { get; }
|
||||
protected IRelationRepository RelationRepository { get; }
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using System;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Data;
|
||||
using System.Globalization;
|
||||
@@ -44,6 +44,11 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement
|
||||
protected ILanguageRepository LanguageRepository { get; }
|
||||
protected abstract bool SupportsPublishing { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the node object type for the repository's entity
|
||||
/// </summary>
|
||||
protected abstract Guid NodeObjectTypeId { get; }
|
||||
|
||||
public IEnumerable<MoveEventInfo<TEntity>> Move(TEntity moving, EntityContainer container)
|
||||
{
|
||||
var parentId = Cms.Core.Constants.System.Root;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using System;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Data;
|
||||
using System.Globalization;
|
||||
@@ -112,7 +112,7 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement
|
||||
return Array.Empty<string>();
|
||||
}
|
||||
|
||||
protected override Guid NodeObjectTypeId => Cms.Core.Constants.ObjectTypes.DataType;
|
||||
protected Guid NodeObjectTypeId => Cms.Core.Constants.ObjectTypes.DataType;
|
||||
|
||||
#endregion
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using System;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Microsoft.Extensions.Logging;
|
||||
@@ -120,11 +120,6 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement
|
||||
return new List<string>();
|
||||
}
|
||||
|
||||
protected override Guid NodeObjectTypeId
|
||||
{
|
||||
get { throw new NotImplementedException(); }
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Unit of Work Implementation
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using System;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Microsoft.Extensions.Logging;
|
||||
@@ -956,6 +956,7 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement
|
||||
|
||||
// reading repository purely for looking up by GUID
|
||||
// TODO: ugly and to fix we need to decouple the IRepositoryQueryable -> IRepository -> IReadRepository which should all be separate things!
|
||||
// This sub-repository pattern is super old and totally unecessary anymore, caching can be handled in much nicer ways without this
|
||||
private class ContentByGuidReadRepository : EntityRepositoryBase<Guid, IContent>
|
||||
{
|
||||
private readonly DocumentRepository _outerRepo;
|
||||
@@ -966,8 +967,6 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement
|
||||
_outerRepo = outerRepo;
|
||||
}
|
||||
|
||||
protected override Guid NodeObjectTypeId => _outerRepo.NodeObjectTypeId;
|
||||
|
||||
protected override IContent PerformGet(Guid id)
|
||||
{
|
||||
var sql = _outerRepo.GetBaseQuery(QueryType.Single)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using System;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Data;
|
||||
using System.Linq;
|
||||
@@ -81,11 +81,6 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement
|
||||
return list;
|
||||
}
|
||||
|
||||
protected override Guid NodeObjectTypeId
|
||||
{
|
||||
get { throw new NotImplementedException(); }
|
||||
}
|
||||
|
||||
protected override void PersistNewItem(IDomain entity)
|
||||
{
|
||||
var exists = Database.ExecuteScalar<int>("SELECT COUNT(*) FROM umbracoDomain WHERE domainName = @domainName", new { domainName = entity.DomainName });
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using System;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Microsoft.Extensions.Logging;
|
||||
@@ -113,20 +113,11 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement
|
||||
return sql;
|
||||
}
|
||||
|
||||
protected override string GetBaseWhereClause()
|
||||
{
|
||||
return "umbracoNode.id = @id and nodeObjectType = @NodeObjectType";
|
||||
}
|
||||
protected override string GetBaseWhereClause() => "umbracoNode.id = @id and nodeObjectType = @NodeObjectType";
|
||||
|
||||
protected override IEnumerable<string> GetDeleteClauses()
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
protected override IEnumerable<string> GetDeleteClauses() => throw new NotImplementedException();
|
||||
|
||||
protected override Guid NodeObjectTypeId
|
||||
{
|
||||
get { return _containerObjectType; }
|
||||
}
|
||||
protected Guid NodeObjectTypeId => _containerObjectType;
|
||||
|
||||
protected override void PersistDeletedItem(EntityContainer entity)
|
||||
{
|
||||
|
||||
@@ -78,11 +78,6 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement
|
||||
return PerformCount(query);
|
||||
}));
|
||||
|
||||
/// <summary>
|
||||
/// Gets the node object type for the repository's entity
|
||||
/// </summary>
|
||||
protected abstract Guid NodeObjectTypeId { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the repository cache policy
|
||||
/// </summary>
|
||||
|
||||
@@ -6,6 +6,7 @@ using NPoco;
|
||||
using Umbraco.Cms.Core.Cache;
|
||||
using Umbraco.Cms.Core.Models.Entities;
|
||||
using Umbraco.Cms.Core.Models.Identity;
|
||||
using Umbraco.Cms.Core.Persistence;
|
||||
using Umbraco.Cms.Core.Persistence.Querying;
|
||||
using Umbraco.Cms.Core.Persistence.Repositories;
|
||||
using Umbraco.Cms.Core.Scoping;
|
||||
@@ -24,10 +25,7 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement
|
||||
: base(scopeAccessor, cache, logger)
|
||||
{ }
|
||||
|
||||
public void DeleteUserLogins(int memberId)
|
||||
{
|
||||
Database.Delete<ExternalLoginDto>("WHERE userId=@userId", new { userId = memberId });
|
||||
}
|
||||
public void DeleteUserLogins(int memberId) => Database.Delete<ExternalLoginDto>("WHERE userId=@userId", new { userId = memberId });
|
||||
|
||||
public void Save(int userId, IEnumerable<IExternalLogin> logins)
|
||||
{
|
||||
@@ -44,41 +42,37 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement
|
||||
var toDelete = new List<int>();
|
||||
var toInsert = new List<IExternalLogin>(logins);
|
||||
|
||||
var existingLogins = Database.Query<ExternalLoginDto>(sql).OrderByDescending(x => x.CreateDate).ToList();
|
||||
// used to track duplicates so they can be removed
|
||||
var keys = new HashSet<(string, string)>();
|
||||
|
||||
var existingLogins = Database.Fetch<ExternalLoginDto>(sql);
|
||||
|
||||
foreach (var existing in existingLogins)
|
||||
{
|
||||
if (!keys.Add((existing.ProviderKey, existing.LoginProvider)))
|
||||
{
|
||||
// if it already exists we need to remove this one
|
||||
toDelete.Add(existing.Id);
|
||||
}
|
||||
else
|
||||
{
|
||||
var found = logins.FirstOrDefault(x =>
|
||||
var found = logins.FirstOrDefault(x =>
|
||||
x.LoginProvider.Equals(existing.LoginProvider, StringComparison.InvariantCultureIgnoreCase)
|
||||
&& x.ProviderKey.Equals(existing.ProviderKey, StringComparison.InvariantCultureIgnoreCase));
|
||||
|
||||
if (found != null)
|
||||
{
|
||||
toUpdate.Add(existing.Id, found);
|
||||
// if it's an update then it's not an insert
|
||||
toInsert.RemoveAll(x => x.ProviderKey == found.ProviderKey && x.LoginProvider == found.LoginProvider);
|
||||
}
|
||||
else
|
||||
{
|
||||
toDelete.Add(existing.Id);
|
||||
}
|
||||
if (found != null)
|
||||
{
|
||||
toUpdate.Add(existing.Id, found);
|
||||
// if it's an update then it's not an insert
|
||||
toInsert.RemoveAll(x => x.ProviderKey == found.ProviderKey && x.LoginProvider == found.LoginProvider);
|
||||
}
|
||||
else
|
||||
{
|
||||
toDelete.Add(existing.Id);
|
||||
}
|
||||
}
|
||||
|
||||
// do the deletes, updates and inserts
|
||||
if (toDelete.Count > 0)
|
||||
{
|
||||
Database.DeleteMany<ExternalLoginDto>().Where(x => toDelete.Contains(x.Id)).Execute();
|
||||
}
|
||||
|
||||
foreach (var u in toUpdate)
|
||||
{
|
||||
Database.Update(ExternalLoginFactory.BuildDto(userId, u.Value, u.Key));
|
||||
}
|
||||
|
||||
Database.InsertBulk(toInsert.Select(i => ExternalLoginFactory.BuildDto(userId, i)));
|
||||
}
|
||||
|
||||
@@ -157,10 +151,7 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement
|
||||
return sql;
|
||||
}
|
||||
|
||||
protected override string GetBaseWhereClause()
|
||||
{
|
||||
return "umbracoExternalLogin.id = @id";
|
||||
}
|
||||
protected override string GetBaseWhereClause() => "umbracoExternalLogin.id = @id";
|
||||
|
||||
protected override IEnumerable<string> GetDeleteClauses()
|
||||
{
|
||||
@@ -171,11 +162,6 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement
|
||||
return list;
|
||||
}
|
||||
|
||||
protected override Guid NodeObjectTypeId
|
||||
{
|
||||
get { throw new NotImplementedException(); }
|
||||
}
|
||||
|
||||
protected override void PersistNewItem(IIdentityUserLogin entity)
|
||||
{
|
||||
entity.AddingEntity();
|
||||
@@ -198,5 +184,113 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement
|
||||
|
||||
entity.ResetDirtyProperties();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Query for user tokens
|
||||
/// </summary>
|
||||
/// <param name="query"></param>
|
||||
/// <returns></returns>
|
||||
public IEnumerable<IIdentityUserToken> Get(IQuery<IIdentityUserToken> query)
|
||||
{
|
||||
Sql<ISqlContext> sqlClause = GetBaseTokenQuery(false);
|
||||
|
||||
var translator = new SqlTranslator<IIdentityUserToken>(sqlClause, query);
|
||||
Sql<ISqlContext> sql = translator.Translate();
|
||||
|
||||
List<ExternalLoginTokenDto> dtos = Database.Fetch<ExternalLoginTokenDto>(sql);
|
||||
|
||||
foreach (ExternalLoginTokenDto dto in dtos)
|
||||
{
|
||||
yield return ExternalLoginFactory.BuildEntity(dto);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Count for user tokens
|
||||
/// </summary>
|
||||
/// <param name="query"></param>
|
||||
/// <returns></returns>
|
||||
public int Count(IQuery<IIdentityUserToken> query)
|
||||
{
|
||||
Sql<ISqlContext> sql = Sql().SelectCount().From<ExternalLoginDto>();
|
||||
return Database.ExecuteScalar<int>(sql);
|
||||
}
|
||||
|
||||
public void Save(int userId, IEnumerable<IExternalLoginToken> tokens)
|
||||
{
|
||||
// get the existing logins (provider + id)
|
||||
var existingUserLogins = Database
|
||||
.Fetch<ExternalLoginDto>(GetBaseQuery(false).Where<ExternalLoginDto>(x => x.UserId == userId))
|
||||
.ToDictionary(x => x.LoginProvider, x => x.Id);
|
||||
|
||||
// deduplicate the tokens
|
||||
tokens = tokens.DistinctBy(x => x.LoginProvider + x.Name).ToList();
|
||||
|
||||
var providers = tokens.Select(x => x.LoginProvider).Distinct().ToList();
|
||||
|
||||
Sql<ISqlContext> sql = GetBaseTokenQuery(true)
|
||||
.WhereIn<ExternalLoginDto>(x => x.LoginProvider, providers)
|
||||
.Where<ExternalLoginDto>(x => x.UserId == userId);
|
||||
|
||||
var toUpdate = new Dictionary<int, (IExternalLoginToken externalLoginToken, int externalLoginId)>();
|
||||
var toDelete = new List<int>();
|
||||
var toInsert = new List<IExternalLoginToken>(tokens);
|
||||
|
||||
var existingTokens = Database.Fetch<ExternalLoginTokenDto>(sql);
|
||||
|
||||
foreach (ExternalLoginTokenDto existing in existingTokens)
|
||||
{
|
||||
IExternalLoginToken found = tokens.FirstOrDefault(x =>
|
||||
x.LoginProvider.InvariantEquals(existing.ExternalLoginDto.LoginProvider)
|
||||
&& x.Name.InvariantEquals(existing.Name));
|
||||
|
||||
if (found != null)
|
||||
{
|
||||
toUpdate.Add(existing.Id, (found, existing.ExternalLoginId));
|
||||
// if it's an update then it's not an insert
|
||||
toInsert.RemoveAll(x => x.LoginProvider.InvariantEquals(found.LoginProvider) && x.Name.InvariantEquals(found.Name));
|
||||
}
|
||||
else
|
||||
{
|
||||
toDelete.Add(existing.Id);
|
||||
}
|
||||
}
|
||||
|
||||
// do the deletes, updates and inserts
|
||||
if (toDelete.Count > 0)
|
||||
{
|
||||
Database.DeleteMany<ExternalLoginTokenDto>().Where(x => toDelete.Contains(x.Id)).Execute();
|
||||
}
|
||||
|
||||
foreach (KeyValuePair<int, (IExternalLoginToken externalLoginToken, int externalLoginId)> u in toUpdate)
|
||||
{
|
||||
Database.Update(ExternalLoginFactory.BuildDto(u.Value.externalLoginId, u.Value.externalLoginToken, u.Key));
|
||||
}
|
||||
|
||||
var insertDtos = new List<ExternalLoginTokenDto>();
|
||||
foreach(IExternalLoginToken t in toInsert)
|
||||
{
|
||||
if (!existingUserLogins.TryGetValue(t.LoginProvider, out int externalLoginId))
|
||||
{
|
||||
throw new InvalidOperationException($"A token was attempted to be saved for login provider {t.LoginProvider} which is not assigned to this user");
|
||||
}
|
||||
insertDtos.Add(ExternalLoginFactory.BuildDto(externalLoginId, t));
|
||||
}
|
||||
Database.InsertBulk(insertDtos);
|
||||
}
|
||||
|
||||
private Sql<ISqlContext> GetBaseTokenQuery(bool forUpdate)
|
||||
=> forUpdate ? Sql()
|
||||
.Select<ExternalLoginTokenDto>(r => r.Select(x => x.ExternalLoginDto))
|
||||
.From<ExternalLoginTokenDto>()
|
||||
.Append(" WITH (UPDLOCK)") // ensure these table values are locked for updates, the ForUpdate ext method does not work here
|
||||
.InnerJoin<ExternalLoginDto>()
|
||||
.On<ExternalLoginTokenDto, ExternalLoginDto>(x => x.ExternalLoginId, x => x.Id)
|
||||
: Sql()
|
||||
.Select<ExternalLoginTokenDto>()
|
||||
.AndSelect<ExternalLoginDto>(x => x.LoginProvider, x => x.UserId)
|
||||
.From<ExternalLoginTokenDto>()
|
||||
.InnerJoin<ExternalLoginDto>()
|
||||
.On<ExternalLoginTokenDto, ExternalLoginDto>(x => x.ExternalLoginId, x => x.Id);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using System;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Microsoft.Extensions.Logging;
|
||||
@@ -33,8 +33,6 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement
|
||||
|
||||
#region Overrides of EntityRepositoryBase<string, IKeyValue>
|
||||
|
||||
protected override Guid NodeObjectTypeId => throw new NotSupportedException();
|
||||
|
||||
protected override Sql<ISqlContext> GetBaseQuery(bool isCount)
|
||||
{
|
||||
var sql = SqlContext.Sql();
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using System;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Microsoft.Extensions.Logging;
|
||||
@@ -124,8 +124,6 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement
|
||||
return list;
|
||||
}
|
||||
|
||||
protected override Guid NodeObjectTypeId => throw new NotImplementedException();
|
||||
|
||||
#endregion
|
||||
|
||||
#region Unit of Work Implementation
|
||||
|
||||
@@ -63,11 +63,6 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement
|
||||
return list;
|
||||
}
|
||||
|
||||
protected override Guid NodeObjectTypeId
|
||||
{
|
||||
get { throw new NotImplementedException(); }
|
||||
}
|
||||
|
||||
protected override void PersistNewItem(ILogViewerQuery entity)
|
||||
{
|
||||
var exists = Database.ExecuteScalar<int>($"SELECT COUNT(*) FROM {Core.Constants.DatabaseSchema.Tables.LogViewerQuery} WHERE name = @name",
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using System;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Microsoft.Extensions.Logging;
|
||||
@@ -136,8 +136,6 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement
|
||||
return list;
|
||||
}
|
||||
|
||||
protected override Guid NodeObjectTypeId => throw new NotImplementedException();
|
||||
|
||||
protected override void PersistNewItem(IMacro entity)
|
||||
{
|
||||
entity.AddingEntity();
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using System;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text.RegularExpressions;
|
||||
@@ -407,13 +407,10 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement
|
||||
return _mediaByGuidReadRepository.Exists(id);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A reading repository purely for looking up by GUID
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// TODO: This is ugly and to fix we need to decouple the IRepositoryQueryable -> IRepository -> IReadRepository which should all be separate things!
|
||||
/// Then we can do the same thing with repository instances and we wouldn't need to leave all these methods as not implemented because we wouldn't need to implement them
|
||||
/// </remarks>
|
||||
|
||||
// A reading repository purely for looking up by GUID
|
||||
// TODO: This is ugly and to fix we need to decouple the IRepositoryQueryable -> IRepository -> IReadRepository which should all be separate things!
|
||||
// This sub-repository pattern is super old and totally unecessary anymore, caching can be handled in much nicer ways without this
|
||||
private class MediaByGuidReadRepository : EntityRepositoryBase<Guid, IMedia>
|
||||
{
|
||||
private readonly MediaRepository _outerRepo;
|
||||
@@ -424,8 +421,6 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement
|
||||
_outerRepo = outerRepo;
|
||||
}
|
||||
|
||||
protected override Guid NodeObjectTypeId => _outerRepo.NodeObjectTypeId;
|
||||
|
||||
protected override IMedia PerformGet(Guid id)
|
||||
{
|
||||
var sql = _outerRepo.GetBaseQuery(QueryType.Single)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using System;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Microsoft.Extensions.Logging;
|
||||
@@ -89,7 +89,7 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement
|
||||
return list;
|
||||
}
|
||||
|
||||
protected override Guid NodeObjectTypeId => Cms.Core.Constants.ObjectTypes.MemberGroup;
|
||||
protected Guid NodeObjectTypeId => Cms.Core.Constants.ObjectTypes.MemberGroup;
|
||||
|
||||
protected override void PersistNewItem(IMemberGroup entity)
|
||||
{
|
||||
|
||||
@@ -314,6 +314,7 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement
|
||||
// persist the member dto
|
||||
dto.NodeId = nodeDto.NodeId;
|
||||
|
||||
// TODO: password parts of this file need updating
|
||||
// if the password is empty, generate one with the special prefix
|
||||
// this will hash the guid with a salt so should be nicely random
|
||||
if (entity.RawPasswordValue.IsNullOrWhiteSpace())
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using System;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
@@ -280,8 +280,6 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement
|
||||
return new List<string>();
|
||||
}
|
||||
|
||||
protected override Guid NodeObjectTypeId => throw new InvalidOperationException("This property won't be implemented.");
|
||||
|
||||
protected override void PersistDeletedItem(ContentPermissionSet entity)
|
||||
{
|
||||
throw new InvalidOperationException("This method won't be implemented.");
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using System;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Microsoft.Extensions.Logging;
|
||||
@@ -81,8 +81,6 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement
|
||||
return list;
|
||||
}
|
||||
|
||||
protected override Guid NodeObjectTypeId => throw new NotImplementedException();
|
||||
|
||||
protected override void PersistNewItem(PublicAccessEntry entity)
|
||||
{
|
||||
entity.AddingEntity();
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using System;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Security.Cryptography;
|
||||
@@ -79,11 +79,6 @@ JOIN umbracoNode ON umbracoRedirectUrl.contentKey=umbracoNode.uniqueID");
|
||||
return list;
|
||||
}
|
||||
|
||||
protected override Guid NodeObjectTypeId
|
||||
{
|
||||
get { throw new NotImplementedException(); }
|
||||
}
|
||||
|
||||
protected override void PersistNewItem(IRedirectUrl entity)
|
||||
{
|
||||
var dto = Map(entity);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using System;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Microsoft.Extensions.Logging;
|
||||
@@ -126,11 +126,6 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement
|
||||
return list;
|
||||
}
|
||||
|
||||
protected override Guid NodeObjectTypeId
|
||||
{
|
||||
get { throw new NotImplementedException(); }
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Unit of Work Implementation
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using System;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Microsoft.Extensions.Logging;
|
||||
@@ -121,11 +121,6 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement
|
||||
return list;
|
||||
}
|
||||
|
||||
protected override Guid NodeObjectTypeId
|
||||
{
|
||||
get { throw new NotImplementedException(); }
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Unit of Work Implementation
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using System;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Microsoft.Extensions.Logging;
|
||||
@@ -94,8 +94,6 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement
|
||||
return list;
|
||||
}
|
||||
|
||||
protected override Guid NodeObjectTypeId => throw new NotImplementedException();
|
||||
|
||||
protected override void PersistNewItem(IServerRegistration entity)
|
||||
{
|
||||
entity.AddingEntity();
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using System;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Microsoft.Extensions.Logging;
|
||||
@@ -81,8 +81,6 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement
|
||||
throw new InvalidOperationException("This method won't be implemented.");
|
||||
}
|
||||
|
||||
protected sealed override Guid NodeObjectTypeId => throw new InvalidOperationException("This property won't be implemented.");
|
||||
|
||||
protected sealed override void PersistNewItem(TEntity entity)
|
||||
{
|
||||
throw new InvalidOperationException("This method won't be implemented.");
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using System;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
@@ -25,9 +25,6 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement
|
||||
|
||||
#region Manage Tag Entities
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override Guid NodeObjectTypeId => throw new NotSupportedException();
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override ITag PerformGet(int id)
|
||||
{
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using System;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
@@ -140,7 +140,7 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement
|
||||
return list;
|
||||
}
|
||||
|
||||
protected override Guid NodeObjectTypeId => Cms.Core.Constants.ObjectTypes.Template;
|
||||
protected Guid NodeObjectTypeId => Cms.Core.Constants.ObjectTypes.Template;
|
||||
|
||||
protected override void PersistNewItem(ITemplate entity)
|
||||
{
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using System;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Microsoft.Extensions.Logging;
|
||||
@@ -291,8 +291,6 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement
|
||||
return list;
|
||||
}
|
||||
|
||||
protected override Guid NodeObjectTypeId => throw new InvalidOperationException("This property won't be implemented.");
|
||||
|
||||
protected override void PersistNewItem(IUserGroup entity)
|
||||
{
|
||||
entity.AddingEntity();
|
||||
@@ -403,8 +401,6 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement
|
||||
throw new InvalidOperationException("This method won't be implemented.");
|
||||
}
|
||||
|
||||
protected override Guid NodeObjectTypeId => throw new InvalidOperationException("This property won't be implemented.");
|
||||
|
||||
#endregion
|
||||
|
||||
protected override void PersistNewItem(UserGroupWithUsers entity)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using System;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Linq.Expressions;
|
||||
@@ -431,8 +431,6 @@ ORDER BY colName";
|
||||
return list;
|
||||
}
|
||||
|
||||
protected override Guid NodeObjectTypeId => throw new NotImplementedException();
|
||||
|
||||
protected override void PersistNewItem(IUser entity)
|
||||
{
|
||||
// the use may have no identity, ie ID is zero, and be v7 super
|
||||
|
||||
@@ -11,7 +11,6 @@ using Umbraco.Cms.Core.Services;
|
||||
using Umbraco.Cms.Infrastructure.Migrations.Install;
|
||||
using Umbraco.Cms.Infrastructure.Migrations.Upgrade;
|
||||
using Umbraco.Cms.Infrastructure.Persistence;
|
||||
using Umbraco.Core.Events;
|
||||
|
||||
namespace Umbraco.Cms.Core
|
||||
{
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Data;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Umbraco.Extensions;
|
||||
@@ -40,6 +42,12 @@ namespace Umbraco.Cms.Core.Scoping
|
||||
// eventually this may need to be injectable - for now we'll create it explicitly and let future needs determine if it should be injectable
|
||||
private IScopedNotificationPublisher _notificationPublisher;
|
||||
|
||||
private object _dictionaryLocker;
|
||||
private HashSet<int> _readLocks;
|
||||
private HashSet<int> _writeLocks;
|
||||
internal Dictionary<Guid, Dictionary<int, int>> ReadLocks;
|
||||
internal Dictionary<Guid, Dictionary<int, int>> WriteLocks;
|
||||
|
||||
// initializes a new scope
|
||||
private Scope(
|
||||
ScopeProvider scopeProvider,
|
||||
@@ -75,6 +83,7 @@ namespace Umbraco.Cms.Core.Scoping
|
||||
|
||||
Detachable = detachable;
|
||||
|
||||
_dictionaryLocker = new object();
|
||||
|
||||
#if DEBUG_SCOPES
|
||||
_scopeProvider.RegisterScope(this);
|
||||
@@ -451,7 +460,7 @@ namespace Umbraco.Cms.Core.Scoping
|
||||
|
||||
if (this != _scopeProvider.AmbientScope)
|
||||
{
|
||||
var failedMessage = $"The {nameof(Scope)} {this.GetDebugInfo()} being disposed is not the Ambient {nameof(Scope)} {_scopeProvider.AmbientScope.GetDebugInfo()}. This typically indicates that a child {nameof(Scope)} was not disposed, or flowed to a child thread that was not awaited, or concurrent threads are accessing the same {nameof(Scope)} (Ambient context) which is not supported. If using Task.Run (or similar) as a fire and forget tasks or to run threads in parallel you must suppress execution context flow with ExecutionContext.SuppressFlow() and ExecutionContext.RestoreFlow().";
|
||||
var failedMessage = $"The {nameof(Scope)} {this.InstanceId} being disposed is not the Ambient {nameof(Scope)} {(_scopeProvider.AmbientScope?.InstanceId.ToString() ?? "NULL")}. This typically indicates that a child {nameof(Scope)} was not disposed, or flowed to a child thread that was not awaited, or concurrent threads are accessing the same {nameof(Scope)} (Ambient context) which is not supported. If using Task.Run (or similar) as a fire and forget tasks or to run threads in parallel you must suppress execution context flow with ExecutionContext.SuppressFlow() and ExecutionContext.RestoreFlow().";
|
||||
|
||||
#if DEBUG_SCOPES
|
||||
Scope ambient = _scopeProvider.AmbientScope;
|
||||
@@ -464,16 +473,28 @@ namespace Umbraco.Cms.Core.Scoping
|
||||
ScopeInfo ambientInfos = _scopeProvider.GetScopeInfo(ambient);
|
||||
ScopeInfo disposeInfos = _scopeProvider.GetScopeInfo(this);
|
||||
throw new InvalidOperationException($"{failedMessage} (see ctor stack traces).\r\n"
|
||||
+ "- ambient ->\r\n" + ambientInfos.ToString() + "\r\n"
|
||||
+ "- dispose ->\r\n" + disposeInfos.ToString() + "\r\n");
|
||||
+ "- ambient ->\r\n" + ambientInfos.ToString() + "\r\n"
|
||||
+ "- dispose ->\r\n" + disposeInfos.ToString() + "\r\n");
|
||||
#else
|
||||
throw new InvalidOperationException(failedMessage);
|
||||
#endif
|
||||
}
|
||||
|
||||
// Replace the Ambient scope with the parent
|
||||
Scope parent = ParentScope;
|
||||
_scopeProvider.PopAmbientScope(this); // pop, the parent is on the stack so is now current
|
||||
// Decrement the lock counters on the parent if any.
|
||||
ClearLocks(InstanceId);
|
||||
if (ParentScope is null)
|
||||
{
|
||||
// We're the parent scope, make sure that locks of all scopes has been cleared
|
||||
// Since we're only reading we don't have to be in a lock
|
||||
if (ReadLocks?.Count > 0 || WriteLocks?.Count > 0)
|
||||
{
|
||||
var exception = new InvalidOperationException($"All scopes has not been disposed from parent scope: {InstanceId}, see log for more details.");
|
||||
_logger.LogError(exception, GenerateUnclearedScopesLogMessage());
|
||||
throw exception;
|
||||
}
|
||||
}
|
||||
|
||||
_scopeProvider.PopAmbientScope(this); // might be null = this is how scopes are removed from context objects
|
||||
|
||||
#if DEBUG_SCOPES
|
||||
_scopeProvider.Disposed(this);
|
||||
@@ -484,9 +505,9 @@ namespace Umbraco.Cms.Core.Scoping
|
||||
_completed = true;
|
||||
}
|
||||
|
||||
if (parent != null)
|
||||
if (ParentScope != null)
|
||||
{
|
||||
parent.ChildCompleted(_completed);
|
||||
ParentScope.ChildCompleted(_completed);
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -496,6 +517,42 @@ namespace Umbraco.Cms.Core.Scoping
|
||||
_disposed = true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generates a log message with all scopes that hasn't cleared their locks, including how many, and what locks they have requested.
|
||||
/// </summary>
|
||||
/// <returns>Log message.</returns>
|
||||
private string GenerateUnclearedScopesLogMessage()
|
||||
{
|
||||
// Dump the dicts into a message for the locks.
|
||||
StringBuilder builder = new StringBuilder();
|
||||
builder.AppendLine($"Lock counters aren't empty, suggesting a scope hasn't been properly disposed, parent id: {InstanceId}");
|
||||
WriteLockDictionaryToString(ReadLocks, builder, "read locks");
|
||||
WriteLockDictionaryToString(WriteLocks, builder, "write locks");
|
||||
return builder.ToString();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Writes a locks dictionary to a <see cref="StringBuilder"/> for logging purposes.
|
||||
/// </summary>
|
||||
/// <param name="dict">Lock dictionary to report on.</param>
|
||||
/// <param name="builder">String builder to write to.</param>
|
||||
/// <param name="dictName">The name to report the dictionary as.</param>
|
||||
private void WriteLockDictionaryToString(Dictionary<Guid, Dictionary<int, int>> dict, StringBuilder builder, string dictName)
|
||||
{
|
||||
if (dict?.Count > 0)
|
||||
{
|
||||
builder.AppendLine($"Remaining {dictName}:");
|
||||
foreach (var instance in dict)
|
||||
{
|
||||
builder.AppendLine($"Scope {instance.Key}");
|
||||
foreach (var lockCounter in instance.Value)
|
||||
{
|
||||
builder.AppendLine($"\tLock ID: {lockCounter.Key} - times requested: {lockCounter.Value}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void DisposeLastScope()
|
||||
{
|
||||
// figure out completed
|
||||
@@ -637,16 +694,198 @@ namespace Umbraco.Cms.Core.Scoping
|
||||
// true if Umbraco.CoreDebugSettings.LogUncompletedScope appSetting is set to "true"
|
||||
private bool LogUncompletedScopes => _coreDebugSettings.LogIncompletedScopes;
|
||||
|
||||
/// <inheritdoc />
|
||||
public void ReadLock(params int[] lockIds) => Database.SqlContext.SqlSyntax.ReadLock(Database, lockIds);
|
||||
/// <summary>
|
||||
/// Increment the counter of a locks dictionary, either ReadLocks or WriteLocks,
|
||||
/// for a specific scope instance and lock identifier. Must be called within a lock.
|
||||
/// </summary>
|
||||
/// <param name="lockId">Lock ID to increment.</param>
|
||||
/// <param name="instanceId">Instance ID of the scope requesting the lock.</param>
|
||||
/// <param name="locks">Reference to the dictionary to increment on</param>
|
||||
private void IncrementLock(int lockId, Guid instanceId, ref Dictionary<Guid, Dictionary<int, int>> locks)
|
||||
{
|
||||
// Since we've already checked that we're the parent in the WriteLockInner method, we don't need to check again.
|
||||
// If it's the very first time a lock has been requested the WriteLocks dict hasn't been instantiated yet.
|
||||
locks ??= new Dictionary<Guid, Dictionary<int, int>>();
|
||||
|
||||
// Try and get the dict associated with the scope id.
|
||||
var locksDictFound = locks.TryGetValue(instanceId, out var locksDict);
|
||||
if (locksDictFound)
|
||||
{
|
||||
locksDict.TryGetValue(lockId, out var value);
|
||||
locksDict[lockId] = value + 1;
|
||||
}
|
||||
else
|
||||
{
|
||||
// The scope hasn't requested a lock yet, so we have to create a dict for it.
|
||||
locks.Add(instanceId, new Dictionary<int, int>());
|
||||
locks[instanceId][lockId] = 1;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Clears all lock counters for a given scope instance, signalling that the scope has been disposed.
|
||||
/// </summary>
|
||||
/// <param name="instanceId">Instance ID of the scope to clear.</param>
|
||||
private void ClearLocks(Guid instanceId)
|
||||
{
|
||||
if (ParentScope is not null)
|
||||
{
|
||||
ParentScope.ClearLocks(instanceId);
|
||||
}
|
||||
else
|
||||
{
|
||||
lock (_dictionaryLocker)
|
||||
{
|
||||
ReadLocks?.Remove(instanceId);
|
||||
WriteLocks?.Remove(instanceId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void ReadLock(TimeSpan timeout, int lockId) => Database.SqlContext.SqlSyntax.ReadLock(Database, timeout, lockId);
|
||||
public void ReadLock(params int[] lockIds) => ReadLockInner(InstanceId, null, lockIds);
|
||||
|
||||
/// <inheritdoc />
|
||||
public void WriteLock(params int[] lockIds) => Database.SqlContext.SqlSyntax.WriteLock(Database, lockIds);
|
||||
public void ReadLock(TimeSpan timeout, int lockId) => ReadLockInner(InstanceId, timeout, lockId);
|
||||
|
||||
/// <inheritdoc />
|
||||
public void WriteLock(TimeSpan timeout, int lockId) => Database.SqlContext.SqlSyntax.WriteLock(Database, timeout, lockId);
|
||||
public void WriteLock(params int[] lockIds) => WriteLockInner(InstanceId, null, lockIds);
|
||||
|
||||
/// <inheritdoc />
|
||||
public void WriteLock(TimeSpan timeout, int lockId) => WriteLockInner(InstanceId, timeout, lockId);
|
||||
|
||||
/// <summary>
|
||||
/// Handles acquiring a read lock, will delegate it to the parent if there are any.
|
||||
/// </summary>
|
||||
/// <param name="instanceId">Instance ID of the requesting scope.</param>
|
||||
/// <param name="timeout">Optional database timeout in milliseconds.</param>
|
||||
/// <param name="lockIds">Array of lock object identifiers.</param>
|
||||
private void ReadLockInner(Guid instanceId, TimeSpan? timeout = null, params int[] lockIds)
|
||||
{
|
||||
if (ParentScope is not null)
|
||||
{
|
||||
// If we have a parent we delegate lock creation to parent.
|
||||
ParentScope.ReadLockInner(instanceId, timeout, lockIds);
|
||||
}
|
||||
else
|
||||
{
|
||||
// We are the outermost scope, handle the lock request.
|
||||
LockInner(instanceId, ref ReadLocks, ref _readLocks, ObtainReadLock, ObtainTimeoutReadLock, timeout, lockIds);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Handles acquiring a write lock with a specified timeout, will delegate it to the parent if there are any.
|
||||
/// </summary>
|
||||
/// <param name="instanceId">Instance ID of the requesting scope.</param>
|
||||
/// <param name="timeout">Optional database timeout in milliseconds.</param>
|
||||
/// <param name="lockIds">Array of lock object identifiers.</param>
|
||||
private void WriteLockInner(Guid instanceId, TimeSpan? timeout = null, params int[] lockIds)
|
||||
{
|
||||
if (ParentScope is not null)
|
||||
{
|
||||
// If we have a parent we delegate lock creation to parent.
|
||||
ParentScope.WriteLockInner(instanceId, timeout, lockIds);
|
||||
}
|
||||
else
|
||||
{
|
||||
// We are the outermost scope, handle the lock request.
|
||||
LockInner(instanceId, ref WriteLocks, ref _writeLocks, ObtainWriteLock, ObtainTimeoutWriteLock, timeout, lockIds);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Handles acquiring a lock, this should only be called from the outermost scope.
|
||||
/// </summary>
|
||||
/// <param name="instanceId">Instance ID of the scope requesting the lock.</param>
|
||||
/// <param name="locks">Reference to the applicable locks dictionary (ReadLocks or WriteLocks).</param>
|
||||
/// <param name="locksSet">Reference to the applicable locks hashset (_readLocks or _writeLocks).</param>
|
||||
/// <param name="obtainLock">Delegate used to request the lock from the database without a timeout.</param>
|
||||
/// <param name="obtainLockTimeout">Delegate used to request the lock from the database with a timeout.</param>
|
||||
/// <param name="timeout">Optional timeout parameter to specify a timeout.</param>
|
||||
/// <param name="lockIds">Lock identifiers to lock on.</param>
|
||||
private void LockInner(Guid instanceId, ref Dictionary<Guid, Dictionary<int, int>> locks, ref HashSet<int> locksSet,
|
||||
Action<int> obtainLock, Action<int, TimeSpan> obtainLockTimeout, TimeSpan? timeout = null,
|
||||
params int[] lockIds)
|
||||
{
|
||||
lock (_dictionaryLocker)
|
||||
{
|
||||
locksSet ??= new HashSet<int>();
|
||||
foreach (var lockId in lockIds)
|
||||
{
|
||||
// Only acquire the lock if we haven't done so yet.
|
||||
if (!locksSet.Contains(lockId))
|
||||
{
|
||||
IncrementLock(lockId, instanceId, ref locks);
|
||||
locksSet.Add(lockId);
|
||||
try
|
||||
{
|
||||
if (timeout is null)
|
||||
{
|
||||
// We just want an ordinary lock.
|
||||
obtainLock(lockId);
|
||||
}
|
||||
else
|
||||
{
|
||||
// We want a lock with a custom timeout
|
||||
obtainLockTimeout(lockId, timeout.Value);
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Something went wrong and we didn't get the lock
|
||||
// Since we at this point have determined that we haven't got any lock with an ID of LockID, it's safe to completely remove it instead of decrementing.
|
||||
locks[instanceId].Remove(lockId);
|
||||
// It needs to be removed from the HashSet as well, because that's how we determine to acquire a lock.
|
||||
locksSet.Remove(lockId);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// We already have a lock, but need to update the dictionary for debugging purposes.
|
||||
IncrementLock(lockId, instanceId, ref locks);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Obtains an ordinary read lock.
|
||||
/// </summary>
|
||||
/// <param name="lockId">Lock object identifier to lock.</param>
|
||||
private void ObtainReadLock(int lockId)
|
||||
{
|
||||
Database.SqlContext.SqlSyntax.ReadLock(Database, lockId);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Obtains a read lock with a custom timeout.
|
||||
/// </summary>
|
||||
/// <param name="lockId">Lock object identifier to lock.</param>
|
||||
/// <param name="timeout">TimeSpan specifying the timout period.</param>
|
||||
private void ObtainTimeoutReadLock(int lockId, TimeSpan timeout)
|
||||
{
|
||||
Database.SqlContext.SqlSyntax.ReadLock(Database, timeout, lockId);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Obtains an ordinary write lock.
|
||||
/// </summary>
|
||||
/// <param name="lockId">Lock object identifier to lock.</param>
|
||||
private void ObtainWriteLock(int lockId)
|
||||
{
|
||||
Database.SqlContext.SqlSyntax.WriteLock(Database, lockId);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Obtains a write lock with a custom timeout.
|
||||
/// </summary>
|
||||
/// <param name="lockId">Lock object identifier to lock.</param>
|
||||
/// <param name="timeout">TimeSpan specifying the timout period.</param>
|
||||
private void ObtainTimeoutWriteLock(int lockId, TimeSpan timeout)
|
||||
{
|
||||
Database.SqlContext.SqlSyntax.WriteLock(Database, timeout, lockId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -119,7 +119,7 @@ namespace Umbraco.Cms.Core.Scoping
|
||||
ConcurrentStack<IScope> stack = s_scopeStack.Value;
|
||||
|
||||
#if DEBUG_SCOPES
|
||||
// first, null-register the existing value
|
||||
// first, null-register the existing value
|
||||
if (stack != null && stack.TryPeek(out IScope ambientScope))
|
||||
{
|
||||
RegisterContext(ambientScope, null);
|
||||
@@ -276,7 +276,7 @@ namespace Umbraco.Cms.Core.Scoping
|
||||
#endregion
|
||||
|
||||
#region Ambient Scope
|
||||
|
||||
|
||||
IScope IScopeAccessor.AmbientScope => AmbientScope;
|
||||
|
||||
/// <summary>
|
||||
@@ -327,7 +327,7 @@ namespace Umbraco.Cms.Core.Scoping
|
||||
{
|
||||
MoveHttpContextScopeToCallContext();
|
||||
MoveHttpContextScopeContextToCallContext();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
@@ -439,7 +439,7 @@ namespace Umbraco.Cms.Core.Scoping
|
||||
{
|
||||
throw new InvalidOperationException($"The detatched scope context does not match the original");
|
||||
}
|
||||
|
||||
|
||||
ambientScope.OrigScope = null;
|
||||
ambientScope.OrigContext = null;
|
||||
ambientScope.Attached = false;
|
||||
@@ -466,7 +466,7 @@ namespace Umbraco.Cms.Core.Scoping
|
||||
if (newContext != null)
|
||||
{
|
||||
PushAmbientScopeContext(newContext);
|
||||
}
|
||||
}
|
||||
return scope;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,33 +0,0 @@
|
||||
using System;
|
||||
using Umbraco.Cms.Core.Composing;
|
||||
using Umbraco.Cms.Core.Runtime;
|
||||
|
||||
namespace Umbraco.Cms.Infrastructure.Search
|
||||
{
|
||||
/// <summary>
|
||||
/// Executes after all other examine components have executed
|
||||
/// </summary>
|
||||
public sealed class ExamineFinalComponent : IComponent
|
||||
{
|
||||
BackgroundIndexRebuilder _indexRebuilder;
|
||||
private readonly IMainDom _mainDom;
|
||||
|
||||
public ExamineFinalComponent(BackgroundIndexRebuilder indexRebuilder, IMainDom mainDom)
|
||||
{
|
||||
_indexRebuilder = indexRebuilder;
|
||||
_mainDom = mainDom;
|
||||
}
|
||||
|
||||
public void Initialize()
|
||||
{
|
||||
if (!_mainDom.IsMainDom) return;
|
||||
|
||||
// TODO: Instead of waiting 5000 ms, we could add an event handler on to fulfilling the first request, then start?
|
||||
_indexRebuilder.RebuildIndexes(true, TimeSpan.FromSeconds(5));
|
||||
}
|
||||
|
||||
public void Terminate()
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
using Umbraco.Cms.Core.Composing;
|
||||
|
||||
namespace Umbraco.Cms.Infrastructure.Search
|
||||
{
|
||||
// examine's final composer composes after all user composers
|
||||
// and *also* after ICoreComposer (in case IUserComposer is disabled)
|
||||
[ComposeAfter(typeof(IUserComposer))]
|
||||
[ComposeAfter(typeof(ICoreComposer))]
|
||||
public class ExamineFinalComposer : ComponentComposer<ExamineFinalComponent>
|
||||
{ }
|
||||
}
|
||||
@@ -35,6 +35,7 @@ namespace Umbraco.Cms.Infrastructure.Search
|
||||
private readonly IValueSetBuilder<IMember> _memberValueSetBuilder;
|
||||
private readonly BackgroundIndexRebuilder _backgroundIndexRebuilder;
|
||||
private readonly TaskHelper _taskHelper;
|
||||
private readonly IRuntimeState _runtimeState;
|
||||
private readonly IScopeProvider _scopeProvider;
|
||||
private readonly ServiceContext _services;
|
||||
private readonly IMainDom _mainDom;
|
||||
@@ -60,7 +61,8 @@ namespace Umbraco.Cms.Infrastructure.Search
|
||||
IValueSetBuilder<IMedia> mediaValueSetBuilder,
|
||||
IValueSetBuilder<IMember> memberValueSetBuilder,
|
||||
BackgroundIndexRebuilder backgroundIndexRebuilder,
|
||||
TaskHelper taskHelper)
|
||||
TaskHelper taskHelper,
|
||||
IRuntimeState runtimeState)
|
||||
{
|
||||
_services = services;
|
||||
_scopeProvider = scopeProvider;
|
||||
@@ -71,6 +73,7 @@ namespace Umbraco.Cms.Infrastructure.Search
|
||||
_memberValueSetBuilder = memberValueSetBuilder;
|
||||
_backgroundIndexRebuilder = backgroundIndexRebuilder;
|
||||
_taskHelper = taskHelper;
|
||||
_runtimeState = runtimeState;
|
||||
_mainDom = mainDom;
|
||||
_profilingLogger = profilingLogger;
|
||||
_logger = logger;
|
||||
@@ -114,7 +117,10 @@ namespace Umbraco.Cms.Infrastructure.Search
|
||||
s_deactivate_handlers = true;
|
||||
}
|
||||
|
||||
|
||||
if (_mainDom.IsMainDom && _runtimeState.Level >= RuntimeLevel.Run)
|
||||
{
|
||||
_backgroundIndexRebuilder.RebuildIndexes(true);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -102,6 +102,7 @@ namespace Umbraco.Cms.Core.Security
|
||||
|
||||
// we have to remember whether Logins property is dirty, since the UpdateMemberProperties will reset it.
|
||||
var isLoginsPropertyDirty = user.IsPropertyDirty(nameof(BackOfficeIdentityUser.Logins));
|
||||
var isTokensPropertyDirty = user.IsPropertyDirty(nameof(BackOfficeIdentityUser.LoginTokens));
|
||||
|
||||
UpdateMemberProperties(userEntity, user);
|
||||
|
||||
@@ -125,6 +126,16 @@ namespace Umbraco.Cms.Core.Security
|
||||
x.UserData)));
|
||||
}
|
||||
|
||||
if (isTokensPropertyDirty)
|
||||
{
|
||||
_externalLoginService.Save(
|
||||
userEntity.Id,
|
||||
user.LoginTokens.Select(x => new ExternalLoginToken(
|
||||
x.LoginProvider,
|
||||
x.Name,
|
||||
x.Value)));
|
||||
}
|
||||
|
||||
return Task.FromResult(IdentityResult.Success);
|
||||
}
|
||||
|
||||
@@ -151,6 +162,7 @@ namespace Umbraco.Cms.Core.Security
|
||||
{
|
||||
// we have to remember whether Logins property is dirty, since the UpdateMemberProperties will reset it.
|
||||
var isLoginsPropertyDirty = user.IsPropertyDirty(nameof(BackOfficeIdentityUser.Logins));
|
||||
var isTokensPropertyDirty = user.IsPropertyDirty(nameof(BackOfficeIdentityUser.LoginTokens));
|
||||
|
||||
if (UpdateMemberProperties(found, user))
|
||||
{
|
||||
@@ -166,6 +178,16 @@ namespace Umbraco.Cms.Core.Security
|
||||
x.ProviderKey,
|
||||
x.UserData)));
|
||||
}
|
||||
|
||||
if (isTokensPropertyDirty)
|
||||
{
|
||||
_externalLoginService.Save(
|
||||
found.Id,
|
||||
user.LoginTokens.Select(x => new ExternalLoginToken(
|
||||
x.LoginProvider,
|
||||
x.Name,
|
||||
x.Value)));
|
||||
}
|
||||
}
|
||||
|
||||
scope.Complete();
|
||||
@@ -543,7 +565,9 @@ namespace Umbraco.Cms.Core.Security
|
||||
{
|
||||
if (user != null)
|
||||
{
|
||||
user.SetLoginsCallback(new Lazy<IEnumerable<IIdentityUserLogin>>(() => _externalLoginService.GetAll(UserIdToInt(user.Id))));
|
||||
var userId = UserIdToInt(user.Id);
|
||||
user.SetLoginsCallback(new Lazy<IEnumerable<IIdentityUserLogin>>(() => _externalLoginService.GetExternalLogins(userId)));
|
||||
user.SetTokensCallback(new Lazy<IEnumerable<IIdentityUserToken>>(() => _externalLoginService.GetExternalLoginTokens(userId)));
|
||||
}
|
||||
|
||||
return user;
|
||||
@@ -757,24 +781,102 @@ namespace Umbraco.Cms.Core.Security
|
||||
[EditorBrowsable(EditorBrowsableState.Never)]
|
||||
public override Task<IList<BackOfficeIdentityUser>> GetUsersForClaimAsync(Claim claim, CancellationToken cancellationToken = default) => throw new NotImplementedException();
|
||||
|
||||
// TODO: We should support these
|
||||
|
||||
/// <summary>
|
||||
/// Not supported in Umbraco
|
||||
/// Overridden to support Umbraco's own data storage requirements
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The base class's implementation of this calls into FindTokenAsync and AddUserTokenAsync, both methods will only work with ORMs that are change
|
||||
/// tracking ORMs like EFCore.
|
||||
/// </remarks>
|
||||
/// <inheritdoc />
|
||||
public override Task SetTokenAsync(BackOfficeIdentityUser user, string loginProvider, string name, string value, CancellationToken cancellationToken)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
ThrowIfDisposed();
|
||||
|
||||
if (user == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(user));
|
||||
}
|
||||
|
||||
IIdentityUserToken token = user.LoginTokens.FirstOrDefault(x => x.LoginProvider.InvariantEquals(loginProvider) && x.Name.InvariantEquals(name));
|
||||
if (token == null)
|
||||
{
|
||||
user.LoginTokens.Add(new IdentityUserToken(loginProvider, name, value, user.Id));
|
||||
}
|
||||
else
|
||||
{
|
||||
token.Value = value;
|
||||
}
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Overridden to support Umbraco's own data storage requirements
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The base class's implementation of this calls into FindTokenAsync, RemoveUserTokenAsync and AddUserTokenAsync, both methods will only work with ORMs that are change
|
||||
/// tracking ORMs like EFCore.
|
||||
/// </remarks>
|
||||
/// <inheritdoc />
|
||||
public override Task RemoveTokenAsync(BackOfficeIdentityUser user, string loginProvider, string name, CancellationToken cancellationToken)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
ThrowIfDisposed();
|
||||
|
||||
if (user == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(user));
|
||||
}
|
||||
|
||||
IIdentityUserToken token = user.LoginTokens.FirstOrDefault(x => x.LoginProvider.InvariantEquals(loginProvider) && x.Name.InvariantEquals(name));
|
||||
if (token != null)
|
||||
{
|
||||
user.LoginTokens.Remove(token);
|
||||
}
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Overridden to support Umbraco's own data storage requirements
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The base class's implementation of this calls into FindTokenAsync, RemoveUserTokenAsync and AddUserTokenAsync, both methods will only work with ORMs that are change
|
||||
/// tracking ORMs like EFCore.
|
||||
/// </remarks>
|
||||
/// <inheritdoc />
|
||||
public override Task<string> GetTokenAsync(BackOfficeIdentityUser user, string loginProvider, string name, CancellationToken cancellationToken)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
ThrowIfDisposed();
|
||||
|
||||
if (user == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(user));
|
||||
}
|
||||
IIdentityUserToken token = user.LoginTokens.FirstOrDefault(x => x.LoginProvider.InvariantEquals(loginProvider) && x.Name.InvariantEquals(name));
|
||||
|
||||
return Task.FromResult(token?.Value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Not supported in Umbraco, see comments above on GetTokenAsync, RemoveTokenAsync, SetTokenAsync
|
||||
/// </summary>
|
||||
/// <inheritdoc />
|
||||
[EditorBrowsable(EditorBrowsableState.Never)]
|
||||
protected override Task<IdentityUserToken<string>> FindTokenAsync(BackOfficeIdentityUser user, string loginProvider, string name, CancellationToken cancellationToken) => throw new NotImplementedException();
|
||||
|
||||
/// <summary>
|
||||
/// Not supported in Umbraco
|
||||
/// Not supported in Umbraco, see comments above on GetTokenAsync, RemoveTokenAsync, SetTokenAsync
|
||||
/// </summary>
|
||||
/// <inheritdoc />
|
||||
[EditorBrowsable(EditorBrowsableState.Never)]
|
||||
protected override Task AddUserTokenAsync(IdentityUserToken<string> token) => throw new NotImplementedException();
|
||||
|
||||
/// <summary>
|
||||
/// Not supported in Umbraco
|
||||
/// Not supported in Umbraco, see comments above on GetTokenAsync, RemoveTokenAsync, SetTokenAsync
|
||||
/// </summary>
|
||||
/// <inheritdoc />
|
||||
[EditorBrowsable(EditorBrowsableState.Never)]
|
||||
|
||||
@@ -259,14 +259,7 @@ namespace Umbraco.Cms.Core.Security
|
||||
/// </summary>
|
||||
/// <returns>A generated password</returns>
|
||||
string GeneratePassword();
|
||||
|
||||
/// <summary>
|
||||
/// Hashes a password for a null user based on the default password hasher
|
||||
/// </summary>
|
||||
/// <param name="password">The password to hash</param>
|
||||
/// <returns>The hashed password</returns>
|
||||
string HashPassword(string password);
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Used to validate the password without an identity user
|
||||
/// Validation code is based on the default ValidatePasswordAsync code
|
||||
|
||||
@@ -595,6 +595,12 @@ namespace Umbraco.Cms.Core.Security
|
||||
}
|
||||
}
|
||||
|
||||
if (member.IsApproved != identityUserMember.IsApproved)
|
||||
{
|
||||
anythingChanged = true;
|
||||
member.IsApproved = identityUserMember.IsApproved;
|
||||
}
|
||||
|
||||
if (identityUserMember.IsPropertyDirty(nameof(MembersIdentityUser.UserName))
|
||||
&& member.Username != identityUserMember.UserName && identityUserMember.UserName.IsNullOrWhiteSpace() == false)
|
||||
{
|
||||
|
||||
@@ -37,7 +37,9 @@ namespace Umbraco.Cms.Core.Models.Identity
|
||||
private string _passwordHash;
|
||||
private DateTime? _lastPasswordChangeDateUtc;
|
||||
private ObservableCollection<IIdentityUserLogin> _logins;
|
||||
private ObservableCollection<IIdentityUserToken> _tokens;
|
||||
private Lazy<IEnumerable<IIdentityUserLogin>> _getLogins;
|
||||
private Lazy<IEnumerable<IIdentityUserToken>> _getTokens;
|
||||
private ObservableCollection<IdentityUserRole<string>> _roles;
|
||||
|
||||
/// <summary>
|
||||
@@ -180,6 +182,38 @@ namespace Umbraco.Cms.Core.Models.Identity
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the external login tokens collection
|
||||
/// </summary>
|
||||
public ICollection<IIdentityUserToken> LoginTokens
|
||||
{
|
||||
get
|
||||
{
|
||||
// return if it exists
|
||||
if (_tokens is not null)
|
||||
{
|
||||
return _tokens;
|
||||
}
|
||||
|
||||
_tokens = new ObservableCollection<IIdentityUserToken>();
|
||||
|
||||
// if the callback is there and hasn't been created yet then execute it and populate the logins
|
||||
// if (_getTokens != null && !_getTokens.IsValueCreated)
|
||||
if (_getTokens?.IsValueCreated != true)
|
||||
{
|
||||
foreach (IIdentityUserToken l in _getTokens.Value)
|
||||
{
|
||||
_tokens.Add(l);
|
||||
}
|
||||
}
|
||||
|
||||
// now assign events
|
||||
_tokens.CollectionChanged += LoginTokens_CollectionChanged;
|
||||
|
||||
return _tokens;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets user ID (Primary Key)
|
||||
/// </summary>
|
||||
@@ -266,10 +300,18 @@ namespace Umbraco.Cms.Core.Models.Identity
|
||||
/// Used to set a lazy call back to populate the user's Login list
|
||||
/// </summary>
|
||||
/// <param name="callback">The lazy value</param>
|
||||
public void SetLoginsCallback(Lazy<IEnumerable<IIdentityUserLogin>> callback) => _getLogins = callback ?? throw new ArgumentNullException(nameof(callback));
|
||||
internal void SetLoginsCallback(Lazy<IEnumerable<IIdentityUserLogin>> callback) => _getLogins = callback ?? throw new ArgumentNullException(nameof(callback));
|
||||
|
||||
/// <summary>
|
||||
/// Used to set a lazy call back to populate the user's token list
|
||||
/// </summary>
|
||||
/// <param name="callback">The lazy value</param>
|
||||
internal void SetTokensCallback(Lazy<IEnumerable<IIdentityUserToken>> callback) => _getTokens = callback ?? throw new ArgumentNullException(nameof(callback));
|
||||
|
||||
private void Logins_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e) => BeingDirty.OnPropertyChanged(nameof(Logins));
|
||||
|
||||
private void LoginTokens_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e) => BeingDirty.OnPropertyChanged(nameof(LoginTokens));
|
||||
|
||||
private void Roles_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e) => BeingDirty.OnPropertyChanged(nameof(Roles));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -99,19 +99,7 @@ namespace Umbraco.Cms.Core.Security
|
||||
string password = _passwordGenerator.GeneratePassword();
|
||||
return password;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generates a hashed password based on the default password hasher
|
||||
/// No existing identity user is required and this does not validate the password
|
||||
/// </summary>
|
||||
/// <param name="password">The password to hash</param>
|
||||
/// <returns>The hashed password</returns>
|
||||
public string HashPassword(string password)
|
||||
{
|
||||
string hashedPassword = PasswordHasher.HashPassword(null, password);
|
||||
return hashedPassword;
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Used to validate the password without an identity user
|
||||
/// Validation code is based on the default ValidatePasswordAsync code
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using System;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Microsoft.Extensions.Logging;
|
||||
@@ -11,7 +11,7 @@ using Umbraco.Cms.Infrastructure.Persistence.Dtos;
|
||||
|
||||
namespace Umbraco.Cms.Core.Services.Implement
|
||||
{
|
||||
public sealed class AuditService : ScopeRepositoryService, IAuditService
|
||||
public sealed class AuditService : RepositoryService, IAuditService
|
||||
{
|
||||
private readonly Lazy<bool> _isAvailable;
|
||||
private readonly IAuditRepository _auditRepository;
|
||||
|
||||
@@ -0,0 +1,452 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using Umbraco.Cms.Core.Cache;
|
||||
using Umbraco.Cms.Core.Configuration.Models;
|
||||
using Umbraco.Cms.Core.Events;
|
||||
using Umbraco.Cms.Core.Logging;
|
||||
using Umbraco.Cms.Core.Models;
|
||||
using Umbraco.Cms.Core.Persistence.Repositories;
|
||||
using Umbraco.Cms.Core.Scoping;
|
||||
using Umbraco.Cms.Core.Sync;
|
||||
using Umbraco.Extensions;
|
||||
|
||||
namespace Umbraco.Cms.Core.Services.Implement
|
||||
{
|
||||
/// <summary>
|
||||
/// Implements <see cref="ICacheInstructionService"/> providing a service for retrieving and saving cache instructions.
|
||||
/// </summary>
|
||||
public class CacheInstructionService : RepositoryService, ICacheInstructionService
|
||||
{
|
||||
private readonly IServerRoleAccessor _serverRoleAccessor;
|
||||
private readonly CacheRefresherCollection _cacheRefreshers;
|
||||
private readonly ICacheInstructionRepository _cacheInstructionRepository;
|
||||
private readonly IProfilingLogger _profilingLogger;
|
||||
private readonly ILogger<CacheInstructionService> _logger;
|
||||
private readonly GlobalSettings _globalSettings;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="CacheInstructionService"/> class.
|
||||
/// </summary>
|
||||
public CacheInstructionService(
|
||||
IScopeProvider provider,
|
||||
ILoggerFactory loggerFactory,
|
||||
IEventMessagesFactory eventMessagesFactory,
|
||||
IServerRoleAccessor serverRoleAccessor,
|
||||
CacheRefresherCollection cacheRefreshers,
|
||||
ICacheInstructionRepository cacheInstructionRepository,
|
||||
IProfilingLogger profilingLogger,
|
||||
ILogger<CacheInstructionService> logger,
|
||||
IOptions<GlobalSettings> globalSettings)
|
||||
: base(provider, loggerFactory, eventMessagesFactory)
|
||||
{
|
||||
_serverRoleAccessor = serverRoleAccessor;
|
||||
_cacheRefreshers = cacheRefreshers;
|
||||
_cacheInstructionRepository = cacheInstructionRepository;
|
||||
_profilingLogger = profilingLogger;
|
||||
_logger = logger;
|
||||
_globalSettings = globalSettings.Value;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool IsColdBootRequired(int lastId)
|
||||
{
|
||||
using (IScope scope = ScopeProvider.CreateScope(autoComplete: true))
|
||||
{
|
||||
if (lastId == 0)
|
||||
{
|
||||
var count = _cacheInstructionRepository.CountAll();
|
||||
|
||||
// If there are instructions but we haven't synced, then a cold boot is necessary.
|
||||
if (count > 0)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// If the last synced instruction is not found in the db, then a cold boot is necessary.
|
||||
if (!_cacheInstructionRepository.Exists(lastId))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool IsInstructionCountOverLimit(int lastId, int limit, out int count)
|
||||
{
|
||||
using (IScope scope = ScopeProvider.CreateScope(autoComplete: true))
|
||||
{
|
||||
// Check for how many instructions there are to process, each row contains a count of the number of instructions contained in each
|
||||
// row so we will sum these numbers to get the actual count.
|
||||
count = _cacheInstructionRepository.CountPendingInstructions(lastId);
|
||||
return count > limit;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public int GetMaxInstructionId()
|
||||
{
|
||||
using (IScope scope = ScopeProvider.CreateScope(autoComplete: true))
|
||||
{
|
||||
return _cacheInstructionRepository.GetMaxId();
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void DeliverInstructions(IEnumerable<RefreshInstruction> instructions, string localIdentity)
|
||||
{
|
||||
CacheInstruction entity = CreateCacheInstruction(instructions, localIdentity);
|
||||
|
||||
using (IScope scope = ScopeProvider.CreateScope())
|
||||
{
|
||||
_cacheInstructionRepository.Add(entity);
|
||||
scope.Complete();
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void DeliverInstructionsInBatches(IEnumerable<RefreshInstruction> instructions, string localIdentity)
|
||||
{
|
||||
// Write the instructions but only create JSON blobs with a max instruction count equal to MaxProcessingInstructionCount.
|
||||
using (IScope scope = ScopeProvider.CreateScope())
|
||||
{
|
||||
foreach (IEnumerable<RefreshInstruction> instructionsBatch in instructions.InGroupsOf(_globalSettings.DatabaseServerMessenger.MaxProcessingInstructionCount))
|
||||
{
|
||||
CacheInstruction entity = CreateCacheInstruction(instructionsBatch, localIdentity);
|
||||
_cacheInstructionRepository.Add(entity);
|
||||
}
|
||||
|
||||
scope.Complete();
|
||||
}
|
||||
}
|
||||
|
||||
private CacheInstruction CreateCacheInstruction(IEnumerable<RefreshInstruction> instructions, string localIdentity) =>
|
||||
new CacheInstruction(0, DateTime.UtcNow, JsonConvert.SerializeObject(instructions, Formatting.None), localIdentity, instructions.Sum(x => x.JsonIdCount));
|
||||
|
||||
/// <inheritdoc/>
|
||||
public CacheInstructionServiceProcessInstructionsResult ProcessInstructions(bool released, string localIdentity, DateTime lastPruned)
|
||||
{
|
||||
using (_profilingLogger.DebugDuration<CacheInstructionService>("Syncing from database..."))
|
||||
using (IScope scope = ScopeProvider.CreateScope())
|
||||
{
|
||||
var numberOfInstructionsProcessed = ProcessDatabaseInstructions(released, localIdentity, out int lastId);
|
||||
|
||||
// Check for pruning throttling.
|
||||
if (released || (DateTime.UtcNow - lastPruned) <= _globalSettings.DatabaseServerMessenger.TimeBetweenPruneOperations)
|
||||
{
|
||||
scope.Complete();
|
||||
return CacheInstructionServiceProcessInstructionsResult.AsCompleted(numberOfInstructionsProcessed, lastId);
|
||||
}
|
||||
|
||||
var instructionsWerePruned = false;
|
||||
switch (_serverRoleAccessor.CurrentServerRole)
|
||||
{
|
||||
case ServerRole.Single:
|
||||
case ServerRole.Master:
|
||||
PruneOldInstructions();
|
||||
instructionsWerePruned = true;
|
||||
break;
|
||||
}
|
||||
|
||||
scope.Complete();
|
||||
|
||||
return instructionsWerePruned
|
||||
? CacheInstructionServiceProcessInstructionsResult.AsCompletedAndPruned(numberOfInstructionsProcessed, lastId)
|
||||
: CacheInstructionServiceProcessInstructionsResult.AsCompleted(numberOfInstructionsProcessed, lastId);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Process instructions from the database.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Thread safety: this is NOT thread safe. Because it is NOT meant to run multi-threaded.
|
||||
/// </remarks>
|
||||
/// <returns>Number of instructions processed.</returns>
|
||||
private int ProcessDatabaseInstructions(bool released, string localIdentity, out int lastId)
|
||||
{
|
||||
// NOTE:
|
||||
// We 'could' recurse to ensure that no remaining instructions are pending in the table before proceeding but I don't think that
|
||||
// would be a good idea since instructions could keep getting added and then all other threads will probably get stuck from serving requests
|
||||
// (depending on what the cache refreshers are doing). I think it's best we do the one time check, process them and continue, if there are
|
||||
// pending requests after being processed, they'll just be processed on the next poll.
|
||||
//
|
||||
// TODO: not true if we're running on a background thread, assuming we can?
|
||||
|
||||
// Only retrieve the top 100 (just in case there are tons).
|
||||
// Even though MaxProcessingInstructionCount is by default 1000 we still don't want to process that many
|
||||
// rows in one request thread since each row can contain a ton of instructions (until 7.5.5 in which case
|
||||
// a row can only contain MaxProcessingInstructionCount).
|
||||
const int MaxInstructionsToRetrieve = 100;
|
||||
|
||||
// Only process instructions coming from a remote server, and ignore instructions coming from
|
||||
// the local server as they've already been processed. We should NOT assume that the sequence of
|
||||
// instructions in the database makes any sense whatsoever, because it's all async.
|
||||
|
||||
// Tracks which ones have already been processed to avoid duplicates
|
||||
var processed = new HashSet<RefreshInstruction>();
|
||||
var numberOfInstructionsProcessed = 0;
|
||||
|
||||
// It would have been nice to do this in a Query instead of Fetch using a data reader to save
|
||||
// some memory however we cannot do that because inside of this loop the cache refreshers are also
|
||||
// performing some lookups which cannot be done with an active reader open.
|
||||
lastId = 0;
|
||||
foreach (CacheInstruction instruction in _cacheInstructionRepository.GetPendingInstructions(lastId, MaxInstructionsToRetrieve))
|
||||
{
|
||||
// If this flag gets set it means we're shutting down! In this case, we need to exit asap and cannot
|
||||
// continue processing anything otherwise we'll hold up the app domain shutdown.
|
||||
if (released)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
if (instruction.OriginIdentity == localIdentity)
|
||||
{
|
||||
// Just skip that local one but update lastId nevertheless.
|
||||
lastId = instruction.Id;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Deserialize remote instructions & skip if it fails.
|
||||
if (!TryDeserializeInstructions(instruction, out JArray jsonInstructions))
|
||||
{
|
||||
lastId = instruction.Id; // skip
|
||||
continue;
|
||||
}
|
||||
|
||||
List<RefreshInstruction> instructionBatch = GetAllInstructions(jsonInstructions);
|
||||
|
||||
// Process as per-normal.
|
||||
var success = ProcessDatabaseInstructions(instructionBatch, instruction, processed, released, ref lastId);
|
||||
|
||||
// If they couldn't be all processed (i.e. we're shutting down) then exit.
|
||||
if (success == false)
|
||||
{
|
||||
_logger.LogInformation("The current batch of instructions was not processed, app is shutting down");
|
||||
break;
|
||||
}
|
||||
|
||||
numberOfInstructionsProcessed++;
|
||||
}
|
||||
|
||||
return numberOfInstructionsProcessed;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Attempts to deserialize the instructions to a JArray.
|
||||
/// </summary>
|
||||
private bool TryDeserializeInstructions(CacheInstruction instruction, out JArray jsonInstructions)
|
||||
{
|
||||
try
|
||||
{
|
||||
jsonInstructions = JsonConvert.DeserializeObject<JArray>(instruction.Instructions);
|
||||
return true;
|
||||
}
|
||||
catch (JsonException ex)
|
||||
{
|
||||
_logger.LogError(ex, "Failed to deserialize instructions ({DtoId}: '{DtoInstructions}').",
|
||||
instruction.Id,
|
||||
instruction.Instructions);
|
||||
jsonInstructions = null;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parses out the individual instructions to be processed.
|
||||
/// </summary>
|
||||
private static List<RefreshInstruction> GetAllInstructions(IEnumerable<JToken> jsonInstructions)
|
||||
{
|
||||
var result = new List<RefreshInstruction>();
|
||||
foreach (JToken jsonItem in jsonInstructions)
|
||||
{
|
||||
// Could be a JObject in which case we can convert to a RefreshInstruction.
|
||||
// Otherwise it could be another JArray - in which case we'll iterate that.
|
||||
if (jsonItem is JObject jsonObj)
|
||||
{
|
||||
RefreshInstruction instruction = jsonObj.ToObject<RefreshInstruction>();
|
||||
result.Add(instruction);
|
||||
}
|
||||
else
|
||||
{
|
||||
var jsonInnerArray = (JArray)jsonItem;
|
||||
result.AddRange(GetAllInstructions(jsonInnerArray)); // recurse
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Processes the instruction batch and checks for errors.
|
||||
/// </summary>
|
||||
/// <param name="processed">
|
||||
/// Tracks which instructions have already been processed to avoid duplicates
|
||||
/// </param>
|
||||
/// Returns true if all instructions in the batch were processed, otherwise false if they could not be due to the app being shut down
|
||||
/// </returns>
|
||||
private bool ProcessDatabaseInstructions(IReadOnlyCollection<RefreshInstruction> instructionBatch, CacheInstruction instruction, HashSet<RefreshInstruction> processed, bool released, ref int lastId)
|
||||
{
|
||||
// Execute remote instructions & update lastId.
|
||||
try
|
||||
{
|
||||
var result = NotifyRefreshers(instructionBatch, processed, released);
|
||||
if (result)
|
||||
{
|
||||
// If all instructions were processed, set the last id.
|
||||
lastId = instruction.Id;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(
|
||||
ex,
|
||||
"DISTRIBUTED CACHE IS NOT UPDATED. Failed to execute instructions ({DtoId}: '{DtoInstructions}'). Instruction is being skipped/ignored",
|
||||
instruction.Id,
|
||||
instruction.Instructions);
|
||||
|
||||
// We cannot throw here because this invalid instruction will just keep getting processed over and over and errors
|
||||
// will be thrown over and over. The only thing we can do is ignore and move on.
|
||||
lastId = instruction.Id;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Executes the instructions against the cache refresher instances.
|
||||
/// </summary>
|
||||
/// <returns>
|
||||
/// Returns true if all instructions were processed, otherwise false if the processing was interrupted (i.e. by app shutdown).
|
||||
/// </returns>
|
||||
private bool NotifyRefreshers(IEnumerable<RefreshInstruction> instructions, HashSet<RefreshInstruction> processed, bool released)
|
||||
{
|
||||
foreach (RefreshInstruction instruction in instructions)
|
||||
{
|
||||
// Check if the app is shutting down, we need to exit if this happens.
|
||||
if (released)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// This has already been processed.
|
||||
if (processed.Contains(instruction))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
switch (instruction.RefreshType)
|
||||
{
|
||||
case RefreshMethodType.RefreshAll:
|
||||
RefreshAll(instruction.RefresherId);
|
||||
break;
|
||||
case RefreshMethodType.RefreshByGuid:
|
||||
RefreshByGuid(instruction.RefresherId, instruction.GuidId);
|
||||
break;
|
||||
case RefreshMethodType.RefreshById:
|
||||
RefreshById(instruction.RefresherId, instruction.IntId);
|
||||
break;
|
||||
case RefreshMethodType.RefreshByIds:
|
||||
RefreshByIds(instruction.RefresherId, instruction.JsonIds);
|
||||
break;
|
||||
case RefreshMethodType.RefreshByJson:
|
||||
RefreshByJson(instruction.RefresherId, instruction.JsonPayload);
|
||||
break;
|
||||
case RefreshMethodType.RemoveById:
|
||||
RemoveById(instruction.RefresherId, instruction.IntId);
|
||||
break;
|
||||
}
|
||||
|
||||
processed.Add(instruction);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private void RefreshAll(Guid uniqueIdentifier)
|
||||
{
|
||||
ICacheRefresher refresher = GetRefresher(uniqueIdentifier);
|
||||
refresher.RefreshAll();
|
||||
}
|
||||
|
||||
private void RefreshByGuid(Guid uniqueIdentifier, Guid id)
|
||||
{
|
||||
ICacheRefresher refresher = GetRefresher(uniqueIdentifier);
|
||||
refresher.Refresh(id);
|
||||
}
|
||||
|
||||
private void RefreshById(Guid uniqueIdentifier, int id)
|
||||
{
|
||||
ICacheRefresher refresher = GetRefresher(uniqueIdentifier);
|
||||
refresher.Refresh(id);
|
||||
}
|
||||
|
||||
private void RefreshByIds(Guid uniqueIdentifier, string jsonIds)
|
||||
{
|
||||
ICacheRefresher refresher = GetRefresher(uniqueIdentifier);
|
||||
foreach (var id in JsonConvert.DeserializeObject<int[]>(jsonIds))
|
||||
{
|
||||
refresher.Refresh(id);
|
||||
}
|
||||
}
|
||||
|
||||
private void RefreshByJson(Guid uniqueIdentifier, string jsonPayload)
|
||||
{
|
||||
IJsonCacheRefresher refresher = GetJsonRefresher(uniqueIdentifier);
|
||||
refresher.Refresh(jsonPayload);
|
||||
}
|
||||
|
||||
private void RemoveById(Guid uniqueIdentifier, int id)
|
||||
{
|
||||
ICacheRefresher refresher = GetRefresher(uniqueIdentifier);
|
||||
refresher.Remove(id);
|
||||
}
|
||||
|
||||
private ICacheRefresher GetRefresher(Guid id)
|
||||
{
|
||||
ICacheRefresher refresher = _cacheRefreshers[id];
|
||||
if (refresher == null)
|
||||
{
|
||||
throw new InvalidOperationException("Cache refresher with ID \"" + id + "\" does not exist.");
|
||||
}
|
||||
|
||||
return refresher;
|
||||
}
|
||||
|
||||
private IJsonCacheRefresher GetJsonRefresher(Guid id) => GetJsonRefresher(GetRefresher(id));
|
||||
|
||||
private static IJsonCacheRefresher GetJsonRefresher(ICacheRefresher refresher)
|
||||
{
|
||||
if (refresher is not IJsonCacheRefresher jsonRefresher)
|
||||
{
|
||||
throw new InvalidOperationException("Cache refresher with ID \"" + refresher.RefresherUniqueId + "\" does not implement " + typeof(IJsonCacheRefresher) + ".");
|
||||
}
|
||||
|
||||
return jsonRefresher;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Remove old instructions from the database
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Always leave the last (most recent) record in the db table, this is so that not all instructions are removed which would cause
|
||||
/// the site to cold boot if there's been no instruction activity for more than TimeToRetainInstructions.
|
||||
/// See: http://issues.umbraco.org/issue/U4-7643#comment=67-25085
|
||||
/// </remarks>
|
||||
private void PruneOldInstructions()
|
||||
{
|
||||
DateTime pruneDate = DateTime.UtcNow - _globalSettings.DatabaseServerMessenger.TimeToRetainInstructions;
|
||||
_cacheInstructionRepository.DeleteInstructionsOlderThan(pruneDate);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
using System;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Umbraco.Cms.Core.Events;
|
||||
@@ -11,7 +11,7 @@ namespace Umbraco.Cms.Core.Services.Implement
|
||||
/// <summary>
|
||||
/// Implements <see cref="IConsentService"/>.
|
||||
/// </summary>
|
||||
internal class ConsentService : ScopeRepositoryService, IConsentService
|
||||
internal class ConsentService : RepositoryService, IConsentService
|
||||
{
|
||||
private readonly IConsentRepository _consentRepository;
|
||||
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Umbraco.Cms.Core.Events;
|
||||
using Umbraco.Cms.Core.Scoping;
|
||||
|
||||
namespace Umbraco.Cms.Core.Services.Implement
|
||||
{
|
||||
public abstract class ContentTypeServiceBase : ScopeRepositoryService
|
||||
public abstract class ContentTypeServiceBase : RepositoryService
|
||||
{
|
||||
protected ContentTypeServiceBase(IScopeProvider provider, ILoggerFactory loggerFactory, IEventMessagesFactory eventMessagesFactory)
|
||||
: base(provider, loggerFactory, eventMessagesFactory)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using System;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Microsoft.Extensions.Logging;
|
||||
@@ -19,7 +19,7 @@ namespace Umbraco.Cms.Core.Services.Implement
|
||||
/// <summary>
|
||||
/// Represents the DataType Service, which is an easy access to operations involving <see cref="IDataType"/>
|
||||
/// </summary>
|
||||
public class DataTypeService : ScopeRepositoryService, IDataTypeService
|
||||
public class DataTypeService : RepositoryService, IDataTypeService
|
||||
{
|
||||
private readonly IDataTypeRepository _dataTypeRepository;
|
||||
private readonly IDataTypeContainerRepository _dataTypeContainerRepository;
|
||||
|
||||
@@ -7,7 +7,7 @@ using Umbraco.Cms.Core.Scoping;
|
||||
|
||||
namespace Umbraco.Cms.Core.Services.Implement
|
||||
{
|
||||
public class DomainService : ScopeRepositoryService, IDomainService
|
||||
public class DomainService : RepositoryService, IDomainService
|
||||
{
|
||||
private readonly IDomainRepository _domainRepository;
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@ using Umbraco.Extensions;
|
||||
|
||||
namespace Umbraco.Cms.Core.Services.Implement
|
||||
{
|
||||
public class EntityService : ScopeRepositoryService, IEntityService
|
||||
public class EntityService : RepositoryService, IEntityService
|
||||
{
|
||||
private readonly IEntityRepository _entityRepository;
|
||||
private readonly Dictionary<string, UmbracoObjectTypes> _objectTypes;
|
||||
|
||||
@@ -8,7 +8,7 @@ using Umbraco.Cms.Core.Scoping;
|
||||
|
||||
namespace Umbraco.Cms.Core.Services.Implement
|
||||
{
|
||||
public class ExternalLoginService : ScopeRepositoryService, IExternalLoginService
|
||||
public class ExternalLoginService : RepositoryService, IExternalLoginService
|
||||
{
|
||||
private readonly IExternalLoginRepository _externalLoginRepository;
|
||||
|
||||
@@ -20,16 +20,28 @@ namespace Umbraco.Cms.Core.Services.Implement
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public IEnumerable<IIdentityUserLogin> GetAll(int userId)
|
||||
public IEnumerable<IIdentityUserLogin> GetExternalLogins(int userId)
|
||||
{
|
||||
using (var scope = ScopeProvider.CreateScope(autoComplete: true))
|
||||
{
|
||||
var asString = userId.ToString(); // TODO: This is temp until we update the external service to support guids for both users and members
|
||||
// TODO: This is temp until we update the external service to support guids for both users and members
|
||||
var asString = userId.ToString();
|
||||
return _externalLoginRepository.Get(Query<IIdentityUserLogin>().Where(x => x.UserId == asString))
|
||||
.ToList();
|
||||
}
|
||||
}
|
||||
|
||||
public IEnumerable<IIdentityUserToken> GetExternalLoginTokens(int userId)
|
||||
{
|
||||
using (var scope = ScopeProvider.CreateScope(autoComplete: true))
|
||||
{
|
||||
// TODO: This is temp until we update the external service to support guids for both users and members
|
||||
var asString = userId.ToString();
|
||||
return _externalLoginRepository.Get(Query<IIdentityUserToken>().Where(x => x.UserId == asString))
|
||||
.ToList();
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public IEnumerable<IIdentityUserLogin> Find(string loginProvider, string providerKey)
|
||||
{
|
||||
@@ -51,12 +63,11 @@ namespace Umbraco.Cms.Core.Services.Implement
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Save(IIdentityUserLogin login)
|
||||
public void Save(int userId, IEnumerable<IExternalLoginToken> tokens)
|
||||
{
|
||||
using (var scope = ScopeProvider.CreateScope())
|
||||
{
|
||||
_externalLoginRepository.Save(login);
|
||||
_externalLoginRepository.Save(userId, tokens);
|
||||
scope.Complete();
|
||||
}
|
||||
}
|
||||
@@ -70,7 +81,5 @@ namespace Umbraco.Cms.Core.Services.Implement
|
||||
scope.Complete();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,7 +19,7 @@ namespace Umbraco.Cms.Core.Services.Implement
|
||||
/// <summary>
|
||||
/// Represents the File Service, which is an easy access to operations involving <see cref="IFile"/> objects like Scripts, Stylesheets and Templates
|
||||
/// </summary>
|
||||
public class FileService : ScopeRepositoryService, IFileService
|
||||
public class FileService : RepositoryService, IFileService
|
||||
{
|
||||
private readonly IStylesheetRepository _stylesheetRepository;
|
||||
private readonly IScriptRepository _scriptRepository;
|
||||
|
||||
@@ -6,6 +6,7 @@ using Umbraco.Cms.Core.Events;
|
||||
using Umbraco.Cms.Core.Models;
|
||||
using Umbraco.Cms.Core.Persistence.Repositories;
|
||||
using Umbraco.Cms.Core.Scoping;
|
||||
using Umbraco.Cms.Infrastructure.Services.Notifications;
|
||||
using Umbraco.Extensions;
|
||||
|
||||
namespace Umbraco.Cms.Core.Services.Implement
|
||||
@@ -13,14 +14,19 @@ namespace Umbraco.Cms.Core.Services.Implement
|
||||
/// <summary>
|
||||
/// Represents the Localization Service, which is an easy access to operations involving <see cref="Language"/> and <see cref="DictionaryItem"/>
|
||||
/// </summary>
|
||||
public class LocalizationService : ScopeRepositoryService, ILocalizationService
|
||||
internal class LocalizationService : RepositoryService, ILocalizationService
|
||||
{
|
||||
private readonly IDictionaryRepository _dictionaryRepository;
|
||||
private readonly ILanguageRepository _languageRepository;
|
||||
private readonly IAuditRepository _auditRepository;
|
||||
|
||||
public LocalizationService(IScopeProvider provider, ILoggerFactory loggerFactory, IEventMessagesFactory eventMessagesFactory,
|
||||
IDictionaryRepository dictionaryRepository, IAuditRepository auditRepository, ILanguageRepository languageRepository)
|
||||
public LocalizationService(
|
||||
IScopeProvider provider,
|
||||
ILoggerFactory loggerFactory,
|
||||
IEventMessagesFactory eventMessagesFactory,
|
||||
IDictionaryRepository dictionaryRepository,
|
||||
IAuditRepository auditRepository,
|
||||
ILanguageRepository languageRepository)
|
||||
: base(provider, loggerFactory, eventMessagesFactory)
|
||||
{
|
||||
_dictionaryRepository = dictionaryRepository;
|
||||
@@ -88,9 +94,11 @@ namespace Umbraco.Cms.Core.Services.Implement
|
||||
|
||||
item.Translations = translations;
|
||||
}
|
||||
var saveEventArgs = new SaveEventArgs<IDictionaryItem>(item);
|
||||
|
||||
if (scope.Events.DispatchCancelable(SavingDictionaryItem, this, saveEventArgs))
|
||||
EventMessages eventMessages = EventMessagesFactory.Get();
|
||||
var savingNotification = new DictionaryItemSavingNotification(item, eventMessages);
|
||||
|
||||
if (scope.Notifications.PublishCancelable(savingNotification))
|
||||
{
|
||||
scope.Complete();
|
||||
return item;
|
||||
@@ -100,8 +108,7 @@ namespace Umbraco.Cms.Core.Services.Implement
|
||||
// ensure the lazy Language callback is assigned
|
||||
EnsureDictionaryItemLanguageCallback(item);
|
||||
|
||||
saveEventArgs.CanCancel = false;
|
||||
scope.Events.Dispatch(SavedDictionaryItem, this, saveEventArgs);
|
||||
scope.Notifications.Publish(new DictionaryItemSavedNotification(item, eventMessages).WithStateFrom(savingNotification));
|
||||
|
||||
scope.Complete();
|
||||
|
||||
@@ -232,7 +239,9 @@ namespace Umbraco.Cms.Core.Services.Implement
|
||||
{
|
||||
using (var scope = ScopeProvider.CreateScope())
|
||||
{
|
||||
if (scope.Events.DispatchCancelable(SavingDictionaryItem, this, new SaveEventArgs<IDictionaryItem>(dictionaryItem)))
|
||||
EventMessages eventMessages = EventMessagesFactory.Get();
|
||||
var savingNotification = new DictionaryItemSavingNotification(dictionaryItem, eventMessages);
|
||||
if (scope.Notifications.PublishCancelable(savingNotification))
|
||||
{
|
||||
scope.Complete();
|
||||
return;
|
||||
@@ -244,7 +253,7 @@ namespace Umbraco.Cms.Core.Services.Implement
|
||||
// ensure the lazy Language callback is assigned
|
||||
|
||||
EnsureDictionaryItemLanguageCallback(dictionaryItem);
|
||||
scope.Events.Dispatch(SavedDictionaryItem, this, new SaveEventArgs<IDictionaryItem>(dictionaryItem, false));
|
||||
scope.Notifications.Publish(new DictionaryItemSavedNotification(dictionaryItem, eventMessages).WithStateFrom(savingNotification));
|
||||
|
||||
Audit(AuditType.Save, "Save DictionaryItem", userId, dictionaryItem.Id, "DictionaryItem");
|
||||
scope.Complete();
|
||||
@@ -261,16 +270,16 @@ namespace Umbraco.Cms.Core.Services.Implement
|
||||
{
|
||||
using (var scope = ScopeProvider.CreateScope())
|
||||
{
|
||||
var deleteEventArgs = new DeleteEventArgs<IDictionaryItem>(dictionaryItem);
|
||||
if (scope.Events.DispatchCancelable(DeletingDictionaryItem, this, deleteEventArgs))
|
||||
EventMessages eventMessages = EventMessagesFactory.Get();
|
||||
var deletingNotification = new DictionaryItemDeletingNotification(dictionaryItem, eventMessages);
|
||||
if (scope.Notifications.PublishCancelable(deletingNotification))
|
||||
{
|
||||
scope.Complete();
|
||||
return;
|
||||
}
|
||||
|
||||
_dictionaryRepository.Delete(dictionaryItem);
|
||||
deleteEventArgs.CanCancel = false;
|
||||
scope.Events.Dispatch(DeletedDictionaryItem, this, deleteEventArgs);
|
||||
scope.Notifications.Publish(new DictionaryItemDeletedNotification(dictionaryItem, eventMessages).WithStateFrom(deletingNotification));
|
||||
|
||||
Audit(AuditType.Delete, "Delete DictionaryItem", userId, dictionaryItem.Id, "DictionaryItem");
|
||||
|
||||
@@ -374,16 +383,16 @@ namespace Umbraco.Cms.Core.Services.Implement
|
||||
throw new InvalidOperationException($"Cannot save language {language.IsoCode} with fallback {languages[language.FallbackLanguageId.Value].IsoCode} as it would create a fallback cycle.");
|
||||
}
|
||||
|
||||
var saveEventArgs = new SaveEventArgs<ILanguage>(language);
|
||||
if (scope.Events.DispatchCancelable(SavingLanguage, this, saveEventArgs))
|
||||
EventMessages eventMessages = EventMessagesFactory.Get();
|
||||
var savingNotification = new LanguageSavingNotification(language, eventMessages);
|
||||
if (scope.Notifications.PublishCancelable(savingNotification))
|
||||
{
|
||||
scope.Complete();
|
||||
return;
|
||||
}
|
||||
|
||||
_languageRepository.Save(language);
|
||||
saveEventArgs.CanCancel = false;
|
||||
scope.Events.Dispatch(SavedLanguage, this, saveEventArgs);
|
||||
scope.Notifications.Publish(new LanguageSavedNotification(language, eventMessages).WithStateFrom(savingNotification));
|
||||
|
||||
Audit(AuditType.Save, "Save Language", userId, language.Id, ObjectTypes.GetName(UmbracoObjectTypes.Language));
|
||||
|
||||
@@ -417,8 +426,9 @@ namespace Umbraco.Cms.Core.Services.Implement
|
||||
// write-lock languages to guard against race conds when dealing with default language
|
||||
scope.WriteLock(Cms.Core.Constants.Locks.Languages);
|
||||
|
||||
var deleteEventArgs = new DeleteEventArgs<ILanguage>(language);
|
||||
if (scope.Events.DispatchCancelable(DeletingLanguage, this, deleteEventArgs))
|
||||
EventMessages eventMessages = EventMessagesFactory.Get();
|
||||
var deletingLanguageNotification = new LanguageDeletingNotification(language, eventMessages);
|
||||
if (scope.Notifications.PublishCancelable(deletingLanguageNotification))
|
||||
{
|
||||
scope.Complete();
|
||||
return;
|
||||
@@ -426,9 +436,8 @@ namespace Umbraco.Cms.Core.Services.Implement
|
||||
|
||||
// NOTE: Other than the fall-back language, there aren't any other constraints in the db, so possible references aren't deleted
|
||||
_languageRepository.Delete(language);
|
||||
deleteEventArgs.CanCancel = false;
|
||||
|
||||
scope.Events.Dispatch(DeletedLanguage, this, deleteEventArgs);
|
||||
scope.Notifications.Publish(new LanguageDeletedNotification(language, eventMessages).WithStateFrom(deletingLanguageNotification));
|
||||
|
||||
Audit(AuditType.Delete, "Delete Language", userId, language.Id, ObjectTypes.GetName(UmbracoObjectTypes.Language));
|
||||
scope.Complete();
|
||||
@@ -463,47 +472,5 @@ namespace Umbraco.Cms.Core.Services.Implement
|
||||
return _dictionaryRepository.GetDictionaryItemKeyMap();
|
||||
}
|
||||
}
|
||||
|
||||
#region Events
|
||||
/// <summary>
|
||||
/// Occurs before Delete
|
||||
/// </summary>
|
||||
public static event TypedEventHandler<ILocalizationService, DeleteEventArgs<ILanguage>> DeletingLanguage;
|
||||
|
||||
/// <summary>
|
||||
/// Occurs after Delete
|
||||
/// </summary>
|
||||
public static event TypedEventHandler<ILocalizationService, DeleteEventArgs<ILanguage>> DeletedLanguage;
|
||||
|
||||
/// <summary>
|
||||
/// Occurs before Delete
|
||||
/// </summary>
|
||||
public static event TypedEventHandler<ILocalizationService, DeleteEventArgs<IDictionaryItem>> DeletingDictionaryItem;
|
||||
|
||||
/// <summary>
|
||||
/// Occurs after Delete
|
||||
/// </summary>
|
||||
public static event TypedEventHandler<ILocalizationService, DeleteEventArgs<IDictionaryItem>> DeletedDictionaryItem;
|
||||
|
||||
/// <summary>
|
||||
/// Occurs before Save
|
||||
/// </summary>
|
||||
public static event TypedEventHandler<ILocalizationService, SaveEventArgs<IDictionaryItem>> SavingDictionaryItem;
|
||||
|
||||
/// <summary>
|
||||
/// Occurs after Save
|
||||
/// </summary>
|
||||
public static event TypedEventHandler<ILocalizationService, SaveEventArgs<IDictionaryItem>> SavedDictionaryItem;
|
||||
|
||||
/// <summary>
|
||||
/// Occurs before Save
|
||||
/// </summary>
|
||||
public static event TypedEventHandler<ILocalizationService, SaveEventArgs<ILanguage>> SavingLanguage;
|
||||
|
||||
/// <summary>
|
||||
/// Occurs after Save
|
||||
/// </summary>
|
||||
public static event TypedEventHandler<ILocalizationService, SaveEventArgs<ILanguage>> SavedLanguage;
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ namespace Umbraco.Cms.Core.Services.Implement
|
||||
/// <summary>
|
||||
/// Represents the Macro Service, which is an easy access to operations involving <see cref="IMacro"/>
|
||||
/// </summary>
|
||||
public class MacroService : ScopeRepositoryService, IMacroService
|
||||
public class MacroService : RepositoryService, IMacroService
|
||||
{
|
||||
private readonly IMacroRepository _macroRepository;
|
||||
private readonly IAuditRepository _auditRepository;
|
||||
|
||||
@@ -21,7 +21,7 @@ namespace Umbraco.Cms.Core.Services.Implement
|
||||
/// <summary>
|
||||
/// Represents the Media Service, which is an easy access to operations involving <see cref="IMedia"/>
|
||||
/// </summary>
|
||||
public class MediaService : ScopeRepositoryService, IMediaService
|
||||
public class MediaService : RepositoryService, IMediaService
|
||||
{
|
||||
private readonly IMediaRepository _mediaRepository;
|
||||
private readonly IMediaTypeRepository _mediaTypeRepository;
|
||||
|
||||
@@ -17,7 +17,7 @@ namespace Umbraco.Cms.Core.Services.Implement
|
||||
/// <summary>
|
||||
/// Represents the MemberService.
|
||||
/// </summary>
|
||||
public class MemberService : ScopeRepositoryService, IMemberService
|
||||
public class MemberService : RepositoryService, IMemberService
|
||||
{
|
||||
private readonly IMemberRepository _memberRepository;
|
||||
private readonly IMemberTypeRepository _memberTypeRepository;
|
||||
|
||||
@@ -22,6 +22,7 @@ namespace Umbraco.Cms.Core.Services.Implement
|
||||
{
|
||||
private readonly IPackageInstallation _packageInstallation;
|
||||
private readonly IHostingEnvironment _hostingEnvironment;
|
||||
private readonly IEventAggregator _eventAggregator;
|
||||
private readonly IAuditService _auditService;
|
||||
private readonly ICreatedPackagesRepository _createdPackages;
|
||||
private readonly IInstalledPackagesRepository _installedPackages;
|
||||
@@ -32,13 +33,15 @@ namespace Umbraco.Cms.Core.Services.Implement
|
||||
ICreatedPackagesRepository createdPackages,
|
||||
IInstalledPackagesRepository installedPackages,
|
||||
IPackageInstallation packageInstallation,
|
||||
IHostingEnvironment hostingEnvironment)
|
||||
IHostingEnvironment hostingEnvironment,
|
||||
IEventAggregator eventAggregator)
|
||||
{
|
||||
_auditService = auditService;
|
||||
_createdPackages = createdPackages;
|
||||
_installedPackages = installedPackages;
|
||||
_packageInstallation = packageInstallation;
|
||||
_hostingEnvironment = hostingEnvironment;
|
||||
_eventAggregator = eventAggregator;
|
||||
}
|
||||
|
||||
#region Package Files
|
||||
@@ -117,8 +120,12 @@ namespace Umbraco.Cms.Core.Services.Implement
|
||||
var compiledPackage = GetCompiledPackageInfo(packageFile);
|
||||
if (compiledPackage == null) throw new InvalidOperationException("Could not read the package file " + packageFile);
|
||||
|
||||
if (ImportingPackage.IsRaisedEventCancelled(new ImportPackageEventArgs<string>(packageFile.Name, compiledPackage), this))
|
||||
// Trigger the Importing Package Notification and stop execution if event/user is cancelling it
|
||||
var importingPackageNotification = new ImportingPackageNotification(packageFile.Name, compiledPackage);
|
||||
if (_eventAggregator.PublishCancelable(importingPackageNotification))
|
||||
{
|
||||
return new InstallationSummary { MetaData = compiledPackage };
|
||||
}
|
||||
|
||||
var summary = _packageInstallation.InstallPackageData(packageDefinition, compiledPackage, userId);
|
||||
|
||||
@@ -126,7 +133,8 @@ namespace Umbraco.Cms.Core.Services.Implement
|
||||
|
||||
_auditService.Add(AuditType.PackagerInstall, userId, -1, "Package", $"Package data installed for package '{compiledPackage.Name}'.");
|
||||
|
||||
ImportedPackage.RaiseEvent(new ImportPackageEventArgs<InstallationSummary>(summary, compiledPackage, false), this);
|
||||
// trigger the ImportedPackage event
|
||||
_eventAggregator.Publish(new ImportedPackageNotification(summary, compiledPackage).WithStateFrom(importingPackageNotification));
|
||||
|
||||
return summary;
|
||||
}
|
||||
@@ -168,7 +176,7 @@ namespace Umbraco.Cms.Core.Services.Implement
|
||||
}
|
||||
|
||||
// trigger the UninstalledPackage event
|
||||
UninstalledPackage.RaiseEvent(new UninstallPackageEventArgs(allSummaries, false), this);
|
||||
_eventAggregator.Publish(new UninstallPackageNotification(allSummaries));
|
||||
|
||||
return summary;
|
||||
}
|
||||
@@ -236,26 +244,5 @@ namespace Umbraco.Cms.Core.Services.Implement
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Event Handlers
|
||||
|
||||
/// <summary>
|
||||
/// Occurs before Importing umbraco package
|
||||
/// </summary>
|
||||
public static event TypedEventHandler<IPackagingService, ImportPackageEventArgs<string>> ImportingPackage;
|
||||
|
||||
/// <summary>
|
||||
/// Occurs after a package is imported
|
||||
/// </summary>
|
||||
public static event TypedEventHandler<IPackagingService, ImportPackageEventArgs<InstallationSummary>> ImportedPackage;
|
||||
|
||||
/// <summary>
|
||||
/// Occurs after a package is uninstalled
|
||||
/// </summary>
|
||||
public static event TypedEventHandler<IPackagingService, UninstallPackageEventArgs> UninstalledPackage;
|
||||
|
||||
#endregion
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ using Umbraco.Extensions;
|
||||
|
||||
namespace Umbraco.Cms.Core.Services.Implement
|
||||
{
|
||||
public class PublicAccessService : ScopeRepositoryService, IPublicAccessService
|
||||
public class PublicAccessService : RepositoryService, IPublicAccessService
|
||||
{
|
||||
private readonly IPublicAccessRepository _publicAccessRepository;
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ using Umbraco.Cms.Core.Scoping;
|
||||
|
||||
namespace Umbraco.Cms.Core.Services.Implement
|
||||
{
|
||||
internal class RedirectUrlService : ScopeRepositoryService, IRedirectUrlService
|
||||
internal class RedirectUrlService : RepositoryService, IRedirectUrlService
|
||||
{
|
||||
private readonly IRedirectUrlRepository _redirectUrlRepository;
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ using Umbraco.Extensions;
|
||||
|
||||
namespace Umbraco.Cms.Core.Services.Implement
|
||||
{
|
||||
public class RelationService : ScopeRepositoryService, IRelationService
|
||||
public class RelationService : RepositoryService, IRelationService
|
||||
{
|
||||
private readonly IEntityService _entityService;
|
||||
private readonly IRelationRepository _relationRepository;
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Umbraco.Cms.Core.Events;
|
||||
using Umbraco.Cms.Core.Scoping;
|
||||
|
||||
namespace Umbraco.Cms.Core.Services.Implement
|
||||
{
|
||||
// TODO: that one does not add anything = kill
|
||||
public abstract class ScopeRepositoryService : RepositoryService
|
||||
{
|
||||
protected ScopeRepositoryService(IScopeProvider provider, ILoggerFactory loggerFactory, IEventMessagesFactory eventMessagesFactory)
|
||||
: base(provider, loggerFactory, eventMessagesFactory)
|
||||
{ }
|
||||
}
|
||||
}
|
||||
@@ -16,7 +16,7 @@ namespace Umbraco.Cms.Core.Services.Implement
|
||||
/// <summary>
|
||||
/// Manages server registrations in the database.
|
||||
/// </summary>
|
||||
public sealed class ServerRegistrationService : ScopeRepositoryService, IServerRegistrationService
|
||||
public sealed class ServerRegistrationService : RepositoryService, IServerRegistrationService
|
||||
{
|
||||
private readonly IServerRegistrationRepository _serverRegistrationRepository;
|
||||
private readonly IHostingEnvironment _hostingEnvironment;
|
||||
|
||||
@@ -14,7 +14,7 @@ namespace Umbraco.Cms.Core.Services.Implement
|
||||
/// <remarks>
|
||||
/// If there is unpublished content with tags, those tags will not be contained
|
||||
/// </remarks>
|
||||
public class TagService : ScopeRepositoryService, ITagService
|
||||
public class TagService : RepositoryService, ITagService
|
||||
{
|
||||
private readonly ITagRepository _tagRepository;
|
||||
|
||||
|
||||
@@ -22,7 +22,7 @@ namespace Umbraco.Cms.Core.Services.Implement
|
||||
/// <summary>
|
||||
/// Represents the UserService, which is an easy access to operations involving <see cref="IProfile"/>, <see cref="IMembershipUser"/> and eventually Backoffice Users.
|
||||
/// </summary>
|
||||
public class UserService : ScopeRepositoryService, IUserService
|
||||
public class UserService : RepositoryService, IUserService
|
||||
{
|
||||
private readonly IUserRepository _userRepository;
|
||||
private readonly IUserGroupRepository _userGroupRepository;
|
||||
|
||||
@@ -1,18 +0,0 @@
|
||||
// Copyright (c) Umbraco.
|
||||
// See LICENSE for more details.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using Umbraco.Cms.Core.Events;
|
||||
|
||||
namespace Umbraco.Cms.Infrastructure.Services.Notifications
|
||||
{
|
||||
public abstract class CancelableEnumerableObjectNotification<T> : CancelableObjectNotification<IEnumerable<T>>
|
||||
{
|
||||
protected CancelableEnumerableObjectNotification(T target, EventMessages messages) : base(new [] {target}, messages)
|
||||
{
|
||||
}
|
||||
protected CancelableEnumerableObjectNotification(IEnumerable<T> target, EventMessages messages) : base(target, messages)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user