Merge branch 'v9/dev' into v9/contrib

# Conflicts:
#	src/Umbraco.Infrastructure/HostedServices/RecurringHostedServiceBase.cs
This commit is contained in:
Sebastiaan Janssen
2022-04-12 13:41:34 +02:00
87 changed files with 2050 additions and 640 deletions

View File

@@ -46,6 +46,11 @@ namespace Umbraco.Cms.Core.Cache
{
var payloads = Deserialize(json);
Refresh(payloads);
}
public override void Refresh(JsonPayload[] payloads)
{
foreach (var payload in payloads)
{
foreach (var alias in GetCacheKeysForAlias(payload.Alias))
@@ -55,11 +60,13 @@ namespace Umbraco.Cms.Core.Cache
if (macroRepoCache)
{
macroRepoCache.Result.Clear(RepositoryCacheKeys.GetKey<IMacro, int>(payload.Id));
macroRepoCache.Result.Clear(RepositoryCacheKeys.GetKey<IMacro, string>(payload.Alias)); // Repository caching of macro definition by alias
}
}
base.Refresh(json);
base.Refresh(payloads);
}
#endregion
#region Json

View File

@@ -0,0 +1,73 @@
// Copyright (c) Umbraco.
// See LICENSE for more details.
using System.Collections.Generic;
namespace Umbraco.Cms.Core.Configuration.Models
{
/// <summary>
/// An enumeration of options available for control over installation of default Umbraco data.
/// </summary>
public enum InstallDefaultDataOption
{
/// <summary>
/// Do not install any items of this type (other than Umbraco defined essential ones).
/// </summary>
None,
/// <summary>
/// Only install the default data specified in the <see cref="InstallDefaultDataSettings.Values"/>
/// </summary>
Values,
/// <summary>
/// Install all default data, except that specified in the <see cref="InstallDefaultDataSettings.Values"/>
/// </summary>
ExceptValues,
/// <summary>
/// Install all default data.
/// </summary>
All
}
/// <summary>
/// Typed configuration options for installation of default data.
/// </summary>
public class InstallDefaultDataSettings
{
/// <summary>
/// Gets or sets a value indicating whether to create default data on installation.
/// </summary>
public InstallDefaultDataOption InstallData { get; set; } = InstallDefaultDataOption.All;
/// <summary>
/// Gets or sets a value indicating which default data (languages, data types, etc.) should be created when <see cref="InstallData"/> is
/// set to <see cref="InstallDefaultDataOption.Values"/> or <see cref="InstallDefaultDataOption.ExceptValues"/>.
/// </summary>
/// <remarks>
/// <para>
/// For languages, the values provided should be the ISO codes for the languages to be included or excluded, e.g. "en-US".
/// If removing the single default language, ensure that a different one is created via some other means (such
/// as a restore from Umbraco Deploy schema data).
/// </para>
/// <para>
/// For data types, the values provided should be the Guid values used by Umbraco for the data type, listed at:
/// <see cref="Constants.DataTypes"/>
/// Some data types - such as the string label - cannot be excluded from install as they are required for core Umbraco
/// functionality.
/// Otherwise take care not to remove data types required for default Umbraco media and member types, unless you also
/// choose to exclude them.
/// </para>
/// <para>
/// For media types, the values provided should be the Guid values used by Umbraco for the media type, listed at:
/// https://github.com/umbraco/Umbraco-CMS/blob/v9/dev/src/Umbraco.Infrastructure/Migrations/Install/DatabaseDataCreator.cs.
/// </para>
/// <para>
/// For member types, the values provided should be the Guid values used by Umbraco for the member type, listed at:
/// https://github.com/umbraco/Umbraco-CMS/blob/v9/dev/src/Umbraco.Infrastructure/Migrations/Install/DatabaseDataCreator.cs.
/// </para>
/// </remarks>
public IList<string> Values { get; set; } = new List<string>();
}
}

View File

@@ -56,6 +56,21 @@ namespace Umbraco.Cms.Core
public const string ConfigPackageMigration = ConfigPrefix + "PackageMigration";
public const string ConfigContentDashboard = ConfigPrefix + "ContentDashboard";
public const string ConfigHelpPage = ConfigPrefix + "HelpPage";
public const string ConfigInstallDefaultData = ConfigPrefix + "InstallDefaultData";
public static class NamedOptions
{
public static class InstallDefaultData
{
public const string Languages = "Languages";
public const string DataTypes = "DataTypes";
public const string MediaTypes = "MediaTypes";
public const string MemberTypes = "MemberTypes";
}
}
}
}
}

View File

@@ -211,7 +211,6 @@ namespace Umbraco.Cms.Core
/// </summary>
public static readonly Guid ListViewMembersGuid = new Guid(ListViewMembers);
/// <summary>
/// Guid for Date Picker with time as string
/// </summary>

View File

@@ -90,6 +90,19 @@ namespace Umbraco.Cms.Core.DependencyInjection
.AddUmbracoOptions<ContentDashboardSettings>()
.AddUmbracoOptions<HelpPageSettings>();
builder.Services.Configure<InstallDefaultDataSettings>(
Constants.Configuration.NamedOptions.InstallDefaultData.Languages,
builder.Config.GetSection($"{Constants.Configuration.ConfigInstallDefaultData}:{Constants.Configuration.NamedOptions.InstallDefaultData.Languages}"));
builder.Services.Configure<InstallDefaultDataSettings>(
Constants.Configuration.NamedOptions.InstallDefaultData.DataTypes,
builder.Config.GetSection($"{Constants.Configuration.ConfigInstallDefaultData}:{Constants.Configuration.NamedOptions.InstallDefaultData.DataTypes}"));
builder.Services.Configure<InstallDefaultDataSettings>(
Constants.Configuration.NamedOptions.InstallDefaultData.MediaTypes,
builder.Config.GetSection($"{Constants.Configuration.ConfigInstallDefaultData}:{Constants.Configuration.NamedOptions.InstallDefaultData.MediaTypes}"));
builder.Services.Configure<InstallDefaultDataSettings>(
Constants.Configuration.NamedOptions.InstallDefaultData.MemberTypes,
builder.Config.GetSection($"{Constants.Configuration.ConfigInstallDefaultData}:{Constants.Configuration.NamedOptions.InstallDefaultData.MemberTypes}"));
builder.Services.Configure<RequestHandlerSettings>(options => options.MergeReplacements(builder.Config));
return builder;

View File

@@ -0,0 +1,14 @@
using System;
using System.Collections.Generic;
using Umbraco.Cms.Core.Models;
namespace Umbraco.Cms.Core.Persistence.Repositories
{
[Obsolete("This interface will be merged with IMacroRepository in Umbraco 11")]
public interface IMacroWithAliasRepository : IMacroRepository
{
IMacro GetByAlias(string alias);
IEnumerable<IMacro> GetAllByAlias(string[] aliases);
}
}

View File

@@ -1,14 +1,42 @@
using System;
using System.Collections.Generic;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Models.Entities;
namespace Umbraco.Cms.Core.Persistence.Repositories
{
public interface ITrackedReferencesRepository
{
IEnumerable<RelationItem> GetPagedRelationsForItems(int[] ids, long pageIndex, int pageSize, bool filterMustBeIsDependency,out long totalRecords);
IEnumerable<RelationItem> GetPagedItemsWithRelations(int[] ids, long pageIndex, int pageSize, bool filterMustBeIsDependency,out long totalRecords);
IEnumerable<RelationItem> GetPagedDescendantsInReferences(int parentId, long pageIndex, int pageSize, bool filterMustBeIsDependency,out long totalRecords);
/// <summary>
/// Gets a page of items which are in relation with the current item.
/// Basically, shows the items which depend on the current item.
/// </summary>
/// <param name="id">The identifier of the entity to retrieve relations for.</param>
/// <param name="pageIndex">The page index.</param>
/// <param name="pageSize">The page size.</param>
/// <param name="filterMustBeIsDependency">A boolean indicating whether to filter only the RelationTypes which are dependencies (isDependency field is set to true).</param>
/// <param name="totalRecords">The total count of the items with reference to the current item.</param>
/// <returns>An enumerable list of <see cref="RelationItem"/> objects.</returns>
IEnumerable<RelationItem> GetPagedRelationsForItem(int id, long pageIndex, int pageSize, bool filterMustBeIsDependency, out long totalRecords);
/// <summary>
/// Gets a page of items used in any kind of relation from selected integer ids.
/// </summary>
/// <param name="ids">The identifiers of the entities to check for relations.</param>
/// <param name="pageIndex">The page index.</param>
/// <param name="pageSize">The page size.</param>
/// <param name="filterMustBeIsDependency">A boolean indicating whether to filter only the RelationTypes which are dependencies (isDependency field is set to true).</param>
/// <param name="totalRecords">The total count of the items in any kind of relation.</param>
/// <returns>An enumerable list of <see cref="RelationItem"/> objects.</returns>
IEnumerable<RelationItem> GetPagedItemsWithRelations(int[] ids, long pageIndex, int pageSize, bool filterMustBeIsDependency, out long totalRecords);
/// <summary>
/// Gets a page of the descending items that have any references, given a parent id.
/// </summary>
/// <param name="parentId">The unique identifier of the parent to retrieve descendants for.</param>
/// <param name="pageIndex">The page index.</param>
/// <param name="pageSize">The page size.</param>
/// <param name="filterMustBeIsDependency">A boolean indicating whether to filter only the RelationTypes which are dependencies (isDependency field is set to true).</param>
/// <param name="totalRecords">The total count of descending items.</param>
/// <returns>An enumerable list of <see cref="RelationItem"/> objects.</returns>
IEnumerable<RelationItem> GetPagedDescendantsInReferences(int parentId, long pageIndex, int pageSize, bool filterMustBeIsDependency, out long totalRecords);
}
}

View File

@@ -1,5 +1,5 @@
using Microsoft.Extensions.Logging;
using Umbraco.Cms.Core.Hosting;
using Umbraco.Cms.Core.IO;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Serialization;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.Core.Strings;
@@ -28,5 +28,17 @@ namespace Umbraco.Cms.Core.PropertyEditors.ParameterEditors
DefaultConfiguration.Add("minNumber",0 );
DefaultConfiguration.Add("maxNumber", 0);
}
protected override IDataValueEditor CreateValueEditor() => DataValueEditorFactory.Create<MultipleContentPickerParameterEditor.MultipleContentPickerParamateterValueEditor>(Attribute);
internal class MultipleContentPickerParamateterValueEditor : MultiplePickerParamateterValueEditorBase
{
public MultipleContentPickerParamateterValueEditor(ILocalizedTextService localizedTextService, IShortStringHelper shortStringHelper, IJsonSerializer jsonSerializer, IIOHelper ioHelper, DataEditorAttribute attribute, IEntityService entityService) : base(localizedTextService, shortStringHelper, jsonSerializer, ioHelper, attribute, entityService)
{
}
public override string UdiEntityType { get; } = Constants.UdiEntityType.Document;
public override UmbracoObjectTypes UmbracoObjectType { get; } = UmbracoObjectTypes.Document;
}
}
}

View File

@@ -1,5 +1,9 @@
using Microsoft.Extensions.Logging;
using Umbraco.Cms.Core.Hosting;
using System;
using System.Collections.Generic;
using System.Reflection.Metadata;
using Umbraco.Cms.Core.IO;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Models.Editors;
using Umbraco.Cms.Core.Serialization;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.Core.Strings;
@@ -26,5 +30,17 @@ namespace Umbraco.Cms.Core.PropertyEditors.ParameterEditors
{
DefaultConfiguration.Add("multiPicker", "1");
}
protected override IDataValueEditor CreateValueEditor() => DataValueEditorFactory.Create<MultipleMediaPickerPropertyValueEditor>(Attribute);
internal class MultipleMediaPickerPropertyValueEditor : MultiplePickerParamateterValueEditorBase
{
public MultipleMediaPickerPropertyValueEditor(ILocalizedTextService localizedTextService, IShortStringHelper shortStringHelper, IJsonSerializer jsonSerializer, IIOHelper ioHelper, DataEditorAttribute attribute, IEntityService entityService) : base(localizedTextService, shortStringHelper, jsonSerializer, ioHelper, attribute, entityService)
{
}
public override string UdiEntityType { get; } = Constants.UdiEntityType.Media;
public override UmbracoObjectTypes UmbracoObjectType { get; } = UmbracoObjectTypes.Media;
}
}
}

View File

@@ -0,0 +1,61 @@
using System;
using System.Collections.Generic;
using Umbraco.Cms.Core.IO;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Models.Editors;
using Umbraco.Cms.Core.Serialization;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.Core.Strings;
namespace Umbraco.Cms.Core.PropertyEditors.ParameterEditors
{
internal abstract class MultiplePickerParamateterValueEditorBase : DataValueEditor, IDataValueReference
{
private readonly IEntityService _entityService;
public MultiplePickerParamateterValueEditorBase(
ILocalizedTextService localizedTextService,
IShortStringHelper shortStringHelper,
IJsonSerializer jsonSerializer,
IIOHelper ioHelper,
DataEditorAttribute attribute,
IEntityService entityService)
: base(localizedTextService, shortStringHelper, jsonSerializer, ioHelper, attribute)
{
_entityService = entityService;
}
public abstract string UdiEntityType { get; }
public abstract UmbracoObjectTypes UmbracoObjectType { get; }
public IEnumerable<UmbracoEntityReference> GetReferences(object value)
{
var asString = value is string str ? str : value?.ToString();
if (string.IsNullOrEmpty(asString))
{
yield break;
}
foreach (var udiStr in asString.Split(','))
{
if (UdiParser.TryParse(udiStr, out Udi udi))
{
yield return new UmbracoEntityReference(udi);
}
// this is needed to support the legacy case when the multiple media picker parameter editor stores ints not udis
if (int.TryParse(udiStr, out var id))
{
Attempt<Guid> guidAttempt = _entityService.GetKey(id, UmbracoObjectType);
Guid guid = guidAttempt.Success ? guidAttempt.Result : Guid.Empty;
if (guid != Guid.Empty)
{
yield return new UmbracoEntityReference(new GuidUdi(Constants.UdiEntityType.Media, guid));
}
}
}
}
}
}

View File

@@ -8,12 +8,17 @@ namespace Umbraco.Cms.Core.Security
{
/// <summary>
/// Handles password hashing and formatting for legacy hashing algorithms
/// Handles password hashing and formatting for legacy hashing algorithms.
/// </summary>
/// <remarks>
/// Should probably be internal.
/// </remarks>
public class LegacyPasswordSecurity
{
// TODO: Remove v11
// Used for tests
[EditorBrowsable(EditorBrowsableState.Never)]
[Obsolete("We shouldn't be altering our public API to make test code easier, removing v11")]
public string HashPasswordForStorage(string algorithmType, string password)
{
if (string.IsNullOrWhiteSpace(password))
@@ -24,13 +29,15 @@ namespace Umbraco.Cms.Core.Security
return FormatPasswordForStorage(algorithmType, hashed, salt);
}
// TODO: Remove v11
// Used for tests
[EditorBrowsable(EditorBrowsableState.Never)]
[Obsolete("We shouldn't be altering our public API to make test code easier, removing v11")]
public string FormatPasswordForStorage(string algorithmType, string hashedPassword, string salt)
{
if (IsLegacySHA1Algorithm(algorithmType))
if (!SupportHashAlgorithm(algorithmType))
{
return hashedPassword;
throw new InvalidOperationException($"{algorithmType} is not supported");
}
return salt + hashedPassword;
@@ -45,10 +52,15 @@ namespace Umbraco.Cms.Core.Security
/// <returns></returns>
public bool VerifyPassword(string algorithm, string password, string dbPassword)
{
if (string.IsNullOrWhiteSpace(dbPassword)) throw new ArgumentException("Value cannot be null or whitespace.", nameof(dbPassword));
if (string.IsNullOrWhiteSpace(dbPassword))
{
throw new ArgumentException("Value cannot be null or whitespace.", nameof(dbPassword));
}
if (dbPassword.StartsWith(Constants.Security.EmptyPasswordPrefix))
{
return false;
}
try
{
@@ -61,7 +73,6 @@ namespace Umbraco.Cms.Core.Security
//This can happen if the length of the password is wrong and a salt cannot be extracted.
return false;
}
}
/// <summary>
@@ -69,12 +80,13 @@ namespace Umbraco.Cms.Core.Security
/// </summary>
public bool VerifyLegacyHashedPassword(string password, string dbPassword)
{
var hashAlgorith = new HMACSHA1
var hashAlgorithm = new HMACSHA1
{
//the legacy salt was actually the password :(
Key = Encoding.Unicode.GetBytes(password)
};
var hashed = Convert.ToBase64String(hashAlgorith.ComputeHash(Encoding.Unicode.GetBytes(password)));
var hashed = Convert.ToBase64String(hashAlgorithm.ComputeHash(Encoding.Unicode.GetBytes(password)));
return dbPassword == hashed;
}
@@ -87,6 +99,8 @@ namespace Umbraco.Cms.Core.Security
/// <param name="salt"></param>
/// <returns></returns>
// TODO: Do we need this method? We shouldn't be using this class to create new password hashes for storage
// TODO: Remove v11
[Obsolete("We shouldn't be altering our public API to make test code easier, removing v11")]
public string HashNewPassword(string algorithm, string newPassword, out string salt)
{
salt = GenerateSalt();
@@ -102,15 +116,15 @@ namespace Umbraco.Cms.Core.Security
/// <returns></returns>
public string ParseStoredHashPassword(string algorithm, string storedString, out string salt)
{
if (string.IsNullOrWhiteSpace(storedString)) throw new ArgumentException("Value cannot be null or whitespace.", nameof(storedString));
// This is for the <= v4 hashing algorithm for which there was no salt
if (IsLegacySHA1Algorithm(algorithm))
if (string.IsNullOrWhiteSpace(storedString))
{
salt = string.Empty;
return storedString;
throw new ArgumentException("Value cannot be null or whitespace.", nameof(storedString));
}
if (!SupportHashAlgorithm(algorithm))
{
throw new InvalidOperationException($"{algorithm} is not supported");
}
var saltLen = GenerateSalt();
salt = storedString.Substring(0, saltLen.Length);
@@ -133,12 +147,12 @@ namespace Umbraco.Cms.Core.Security
/// <returns></returns>
private string HashPassword(string algorithmType, string pass, string salt)
{
if (IsLegacySHA1Algorithm(algorithmType))
if (!SupportHashAlgorithm(algorithmType))
{
return HashLegacySHA1Password(pass);
throw new InvalidOperationException($"{algorithmType} is not supported");
}
//This is the correct way to implement this (as per the sql membership provider)
// This is the correct way to implement this (as per the sql membership provider)
var bytes = Encoding.Unicode.GetBytes(pass);
var saltBytes = Convert.FromBase64String(salt);
@@ -209,42 +223,17 @@ namespace Umbraco.Cms.Core.Security
{
// This is for the v6-v8 hashing algorithm
if (algorithm.InvariantEquals(Constants.Security.AspNetUmbraco8PasswordHashAlgorithmName))
{
return true;
}
// This is for the <= v4 hashing algorithm
if (IsLegacySHA1Algorithm(algorithm))
// Default validation value for old machine keys (switched to HMACSHA256 aspnet 4 https://docs.microsoft.com/en-us/aspnet/whitepapers/aspnet4/breaking-changes)
if (algorithm.InvariantEquals("SHA1"))
{
return true;
}
return false;
}
private bool IsLegacySHA1Algorithm(string algorithm) => algorithm.InvariantEquals(Constants.Security.AspNetUmbraco4PasswordHashAlgorithmName);
/// <summary>
/// Hashes the password with the old v4 algorithm
/// </summary>
/// <param name="password">The password.</param>
/// <returns>The encoded password.</returns>
private string HashLegacySHA1Password(string password)
{
using var hashAlgorithm = GetLegacySHA1Algorithm(password);
var hash = Convert.ToBase64String(hashAlgorithm.ComputeHash(Encoding.Unicode.GetBytes(password)));
return hash;
}
/// <summary>
/// Returns the old v4 algorithm and settings
/// </summary>
/// <param name="password"></param>
/// <returns></returns>
private HashAlgorithm GetLegacySHA1Algorithm(string password)
{
return new HMACSHA1
{
//the legacy salt was actually the password :(
Key = Encoding.Unicode.GetBytes(password)
};
}
}
}

View File

@@ -1,4 +1,4 @@
using System;
using System;
using System.Collections.Generic;
using Umbraco.Cms.Core.Models;
@@ -17,13 +17,6 @@ namespace Umbraco.Cms.Core.Services
/// <returns>An <see cref="IMacro"/> object</returns>
IMacro GetByAlias(string alias);
///// <summary>
///// Gets a list all available <see cref="IMacro"/> objects
///// </summary>
///// <param name="aliases">Optional array of aliases to limit the results</param>
///// <returns>An enumerable list of <see cref="IMacro"/> objects</returns>
//IEnumerable<IMacro> GetAll(params string[] aliases);
IEnumerable<IMacro> GetAll();
IEnumerable<IMacro> GetAll(params int[] ids);

View File

@@ -0,0 +1,17 @@
using System;
using System.Collections.Generic;
using Umbraco.Cms.Core.Models;
namespace Umbraco.Cms.Core.Services
{
[Obsolete("This interface will be merged with IMacroService in Umbraco 11")]
public interface IMacroWithAliasService : IMacroService
{
/// <summary>
/// Gets a list of available <see cref="IMacro"/> objects by alias.
/// </summary>
/// <param name="aliases">Optional array of aliases to limit the results</param>
/// <returns>An enumerable list of <see cref="IMacro"/> objects</returns>
IEnumerable<IMacro> GetAll(params string[] aliases);
}
}

View File

@@ -4,12 +4,35 @@ namespace Umbraco.Cms.Core.Services
{
public interface ITrackedReferencesService
{
PagedResult<RelationItem> GetPagedRelationsForItems(int[] ids, long pageIndex, int pageSize, bool filterMustBeIsDependency);
/// <summary>
/// Gets a paged result of items which are in relation with the current item.
/// Basically, shows the items which depend on the current item.
/// </summary>
/// <param name="id">The identifier of the entity to retrieve relations for.</param>
/// <param name="pageIndex">The page index.</param>
/// <param name="pageSize">The page size.</param>
/// <param name="filterMustBeIsDependency">A boolean indicating whether to filter only the RelationTypes which are dependencies (isDependency field is set to true).</param>
/// <returns>A paged result of <see cref="RelationItem"/> objects.</returns>
PagedResult<RelationItem> GetPagedRelationsForItem(int id, long pageIndex, int pageSize, bool filterMustBeIsDependency);
/// <summary>
/// Gets a paged result of the descending items that have any references, given a parent id.
/// </summary>
/// <param name="parentId">The unique identifier of the parent to retrieve descendants for.</param>
/// <param name="pageIndex">The page index.</param>
/// <param name="pageSize">The page size.</param>
/// <param name="filterMustBeIsDependency">A boolean indicating whether to filter only the RelationTypes which are dependencies (isDependency field is set to true).</param>
/// <returns>A paged result of <see cref="RelationItem"/> objects.</returns>
PagedResult<RelationItem> GetPagedDescendantsInReferences(int parentId, long pageIndex, int pageSize, bool filterMustBeIsDependency);
/// <summary>
/// Gets a paged result of items used in any kind of relation from selected integer ids.
/// </summary>
/// <param name="ids">The identifiers of the entities to check for relations.</param>
/// <param name="pageIndex">The page index.</param>
/// <param name="pageSize">The page size.</param>
/// <param name="filterMustBeIsDependency">A boolean indicating whether to filter only the RelationTypes which are dependencies (isDependency field is set to true).</param>
/// <returns>A paged result of <see cref="RelationItem"/> objects.</returns>
PagedResult<RelationItem> GetPagedItemsWithRelations(int[] ids, long pageIndex, int pageSize, bool filterMustBeIsDependency);
}
}

View File

@@ -6,18 +6,14 @@ namespace Umbraco.Cms.Core
{
public static class StaticApplicationLogging
{
private static ILoggerFactory _loggerFactory;
private static ILoggerFactory s_loggerFactory;
public static void Initialize(ILoggerFactory loggerFactory)
{
_loggerFactory = loggerFactory;
}
public static void Initialize(ILoggerFactory loggerFactory) => s_loggerFactory = loggerFactory;
public static ILogger<object> Logger => CreateLogger<object>();
public static ILogger<T> CreateLogger<T>()
{
return _loggerFactory?.CreateLogger<T>() ?? NullLoggerFactory.Instance.CreateLogger<T>();
}
public static ILogger<T> CreateLogger<T>() => s_loggerFactory?.CreateLogger<T>() ?? NullLoggerFactory.Instance.CreateLogger<T>();
public static ILogger CreateLogger(Type type) => s_loggerFactory?.CreateLogger(type) ?? NullLogger.Instance;
}
}