Fixed merge conflicts

This commit is contained in:
Kenn Jacobsen
2021-03-26 16:49:16 +01:00
265 changed files with 5474 additions and 2045 deletions

View File

@@ -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();

View File

@@ -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

View File

@@ -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>()

View File

@@ -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)

View File

@@ -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

View File

@@ -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>();

View File

@@ -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>();

View File

@@ -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;
}
}
}

View File

@@ -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)
{

View File

@@ -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)
{

View File

@@ -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)
{

View File

@@ -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)

View File

@@ -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;

View File

@@ -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)
{

View File

@@ -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)
{

View File

@@ -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)
{

View File

@@ -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)

View File

@@ -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
{

View File

@@ -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
{

View File

@@ -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
{

View File

@@ -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
{

View File

@@ -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 &amp; Fatal messages
/// </summary>
int GetNumberOfErrors(LogTimePeriod logTimePeriod);

View File

@@ -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

View File

@@ -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),

View File

@@ -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
}
}

View File

@@ -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();
}
}
}
}

View File

@@ -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();
}
}
}

View File

@@ -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")]

View File

@@ -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; }
}
}

View File

@@ -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,
};
}
}

View File

@@ -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;
}
}
}

View File

@@ -1,4 +1,4 @@
using System;
using System;
using Umbraco.Cms.Core.Models.Identity;
using Umbraco.Cms.Infrastructure.Persistence.Dtos;

View File

@@ -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));
}
}
}

View File

@@ -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>();

View File

@@ -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

View File

@@ -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 &amp; permission pairs.
/// </summary>
/// <param name="permissionSet"></param>
void ReplaceContentPermissions(EntityPermissionSet permissionSet);

View File

@@ -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)
{

View File

@@ -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));

View File

@@ -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);
}
}
}

View File

@@ -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)
{

View File

@@ -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; }

View File

@@ -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;

View File

@@ -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

View File

@@ -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

View File

@@ -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)

View File

@@ -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 });

View File

@@ -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)
{

View File

@@ -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>

View File

@@ -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);
}
}

View File

@@ -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();

View File

@@ -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

View File

@@ -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",

View File

@@ -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();

View File

@@ -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)

View File

@@ -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)
{

View File

@@ -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())

View File

@@ -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.");

View File

@@ -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();

View File

@@ -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);

View File

@@ -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

View File

@@ -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

View File

@@ -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();

View File

@@ -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.");

View File

@@ -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)
{

View File

@@ -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)
{

View File

@@ -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)

View File

@@ -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

View File

@@ -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
{

View File

@@ -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);
}
}
}

View File

@@ -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;
}

View File

@@ -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()
{
}
}
}

View File

@@ -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>
{ }
}

View File

@@ -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);
}
}

View File

@@ -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)]

View File

@@ -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

View File

@@ -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)
{

View File

@@ -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));
}
}

View File

@@ -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

View File

@@ -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;

View File

@@ -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);
}
}
}

View File

@@ -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;

View File

@@ -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)

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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();
}
}
}
}

View File

@@ -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;

View File

@@ -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
}
}

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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
}
}

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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)
{ }
}
}

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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