diff --git a/src/Umbraco.Cms.Api.Delivery/Services/RequestRedirectService.cs b/src/Umbraco.Cms.Api.Delivery/Services/RequestRedirectService.cs
index 882525c8d073..daf5e1b1984f 100644
--- a/src/Umbraco.Cms.Api.Delivery/Services/RequestRedirectService.cs
+++ b/src/Umbraco.Cms.Api.Delivery/Services/RequestRedirectService.cs
@@ -42,31 +42,42 @@ public RequestRedirectService(
     {
         requestedPath = requestedPath.EnsureStartsWith("/");
 
+        IPublishedContent? startItem = GetStartItem();
+
         // must append the root content url segment if it is not hidden by config, because
         // the URL tracking is based on the actual URL, including the root content url segment
-        if (_globalSettings.HideTopLevelNodeFromPath == false)
+        if (_globalSettings.HideTopLevelNodeFromPath == false && startItem?.UrlSegment != null)
         {
-            IPublishedContent? startItem = GetStartItem();
-            if (startItem?.UrlSegment != null)
-            {
-                requestedPath = $"{startItem.UrlSegment.EnsureStartsWith("/")}{requestedPath}";
-            }
+            requestedPath = $"{startItem.UrlSegment.EnsureStartsWith("/")}{requestedPath}";
         }
 
         var culture = _requestCultureService.GetRequestedCulture();
 
-        // append the configured domain content ID to the path if we have a domain bound request,
-        // because URL tracking registers the tracked url like "{domain content ID}/{content path}"
-        Uri contentRoute = GetDefaultRequestUri(requestedPath);
-        DomainAndUri? domainAndUri = GetDomainAndUriForRoute(contentRoute);
-        if (domainAndUri != null)
+        // important: redirect URLs are always tracked without trailing slashes
+        requestedPath = requestedPath.TrimEnd("/");
+        IRedirectUrl? redirectUrl = _redirectUrlService.GetMostRecentRedirectUrl(requestedPath, culture);
+
+        // if a redirect URL was not found, try by appending the start item ID because URL tracking might have tracked
+        // a redirect with "{root content ID}/{content path}"
+        if (redirectUrl is null && startItem is not null)
         {
-            requestedPath = GetContentRoute(domainAndUri, contentRoute);
-            culture ??= domainAndUri.Culture;
+            redirectUrl = _redirectUrlService.GetMostRecentRedirectUrl($"{startItem.Id}{requestedPath}", culture);
+        }
+
+        // still no redirect URL found - try looking for a configured domain if we have a domain bound request,
+        // because URL tracking might have tracked a redirect with "{domain content ID}/{content path}"
+        if (redirectUrl is null)
+        {
+            Uri contentRoute = GetDefaultRequestUri(requestedPath);
+            DomainAndUri? domainAndUri = GetDomainAndUriForRoute(contentRoute);
+            if (domainAndUri is not null)
+            {
+                requestedPath = GetContentRoute(domainAndUri, contentRoute);
+                culture ??= domainAndUri.Culture;
+                redirectUrl = _redirectUrlService.GetMostRecentRedirectUrl(requestedPath, culture);
+            }
         }
 
-        // important: redirect URLs are always tracked without trailing slashes
-        IRedirectUrl? redirectUrl = _redirectUrlService.GetMostRecentRedirectUrl(requestedPath.TrimEnd("/"), culture);
         IPublishedContent? content = redirectUrl != null
             ? _apiPublishedContentCache.GetById(redirectUrl.ContentKey)
             : null;
diff --git a/src/Umbraco.Core/Cache/RepositoryCachePolicyOptions.cs b/src/Umbraco.Core/Cache/RepositoryCachePolicyOptions.cs
index ba7b251aa0fc..fa334e5c4a21 100644
--- a/src/Umbraco.Core/Cache/RepositoryCachePolicyOptions.cs
+++ b/src/Umbraco.Core/Cache/RepositoryCachePolicyOptions.cs
@@ -11,6 +11,7 @@ public class RepositoryCachePolicyOptions
     public RepositoryCachePolicyOptions(Func<int> performCount)
     {
         PerformCount = performCount;
+        CacheNullValues = false;
         GetAllCacheValidateCount = true;
         GetAllCacheAllowZeroCount = false;
     }
@@ -21,6 +22,7 @@ public RepositoryCachePolicyOptions(Func<int> performCount)
     public RepositoryCachePolicyOptions()
     {
         PerformCount = null;
+        CacheNullValues = false;
         GetAllCacheValidateCount = false;
         GetAllCacheAllowZeroCount = false;
     }
@@ -30,6 +32,11 @@ public RepositoryCachePolicyOptions()
     /// </summary>
     public Func<int>? PerformCount { get; set; }
 
+    /// <summary>
+    ///     True if the Get method will cache null results so that the db is not hit for repeated lookups
+    /// </summary>
+    public bool CacheNullValues { get; set; }
+
     /// <summary>
     ///     True/false as to validate the total item count when all items are returned from cache, the default is true but this
     ///     means that a db lookup will occur - though that lookup will probably be significantly less expensive than the
diff --git a/src/Umbraco.Core/Composing/TypeFinder.cs b/src/Umbraco.Core/Composing/TypeFinder.cs
index e3b7ddef9bb0..5e02336ef5e8 100644
--- a/src/Umbraco.Core/Composing/TypeFinder.cs
+++ b/src/Umbraco.Core/Composing/TypeFinder.cs
@@ -34,7 +34,7 @@ public class TypeFinder : ITypeFinder
         "ServiceStack.", "SqlCE4Umbraco,", "Superpower,", // used by Serilog
         "System.", "TidyNet,", "TidyNet.", "WebDriver,", "itextsharp,", "mscorlib,", "NUnit,", "NUnit.", "NUnit3.",
         "Selenium.", "ImageProcessor", "MiniProfiler.", "Owin,", "SQLite",
-        "ReSharperTestRunner", "ReSharperTestRunner32", "ReSharperTestRunner64", // These are used by the Jetbrains Rider IDE and Visual Studio ReSharper Extension
+        "ReSharperTestRunner", "ReSharperTestRunner32", "ReSharperTestRunner64", "ReSharperTestRunnerArm32", "ReSharperTestRunnerArm64", // These are used by the Jetbrains Rider IDE and Visual Studio ReSharper Extension
     };
 
     private static readonly ConcurrentDictionary<string, Type?> TypeNamesCache = new();
diff --git a/src/Umbraco.Core/Configuration/Models/ModelsBuilderSettings.cs b/src/Umbraco.Core/Configuration/Models/ModelsBuilderSettings.cs
index be86cf1f2b7c..127b7d9330df 100644
--- a/src/Umbraco.Core/Configuration/Models/ModelsBuilderSettings.cs
+++ b/src/Umbraco.Core/Configuration/Models/ModelsBuilderSettings.cs
@@ -16,6 +16,7 @@ public class ModelsBuilderSettings
     internal const string StaticModelsDirectory = "~/umbraco/models";
     internal const bool StaticAcceptUnsafeModelsDirectory = false;
     internal const int StaticDebugLevel = 0;
+    internal const bool StaticIncludeVersionNumberInGeneratedModels = true;
     private bool _flagOutOfDateModels = true;
 
     /// <summary>
@@ -78,4 +79,16 @@ public bool FlagOutOfDateModels
     /// <remarks>0 means minimal (safe on live site), anything else means more and more details (maybe not safe).</remarks>
     [DefaultValue(StaticDebugLevel)]
     public int DebugLevel { get; set; } = StaticDebugLevel;
+
+    /// <summary>
+    ///     Gets or sets a value indicating whether the version number should be included in generated models.
+    /// </summary>
+    /// <remarks>
+    ///     By default this is written to the <see cref="System.CodeDom.Compiler.GeneratedCodeAttribute"/> output in
+    ///     generated code for each property of the model. This can be useful for debugging purposes but isn't essential,
+    ///     and it has the causes the generated code to change every time Umbraco is upgraded. In turn, this leads
+    ///     to unnecessary code file changes that need to be checked into source control. Default is <c>true</c>.
+    /// </remarks>
+    [DefaultValue(StaticIncludeVersionNumberInGeneratedModels)]
+    public bool IncludeVersionNumberInGeneratedModels { get; set; } = StaticIncludeVersionNumberInGeneratedModels;
 }
diff --git a/src/Umbraco.Core/Configuration/Models/SecuritySettings.cs b/src/Umbraco.Core/Configuration/Models/SecuritySettings.cs
index f1005e5d1b93..e68162e6efe0 100644
--- a/src/Umbraco.Core/Configuration/Models/SecuritySettings.cs
+++ b/src/Umbraco.Core/Configuration/Models/SecuritySettings.cs
@@ -19,6 +19,8 @@ public class SecuritySettings
     internal const bool StaticAllowEditInvariantFromNonDefault = false;
     internal const bool StaticAllowConcurrentLogins = false;
     internal const string StaticAuthCookieName = "UMB_UCONTEXT";
+    internal const bool StaticUsernameIsEmail = true;
+    internal const bool StaticMemberRequireUniqueEmail = true;
 
     internal const string StaticAllowedUserNameCharacters =
         "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-._@+\\";
@@ -58,7 +60,14 @@ public class SecuritySettings
     /// <summary>
     ///     Gets or sets a value indicating whether the user's email address is to be considered as their username.
     /// </summary>
-    public bool UsernameIsEmail { get; set; } = true;
+    [DefaultValue(StaticUsernameIsEmail)]
+    public bool UsernameIsEmail { get; set; } = StaticUsernameIsEmail;
+
+    /// <summary>
+    ///     Gets or sets a value indicating whether the member's email address must be unique.
+    /// </summary>
+    [DefaultValue(StaticMemberRequireUniqueEmail)]
+    public bool MemberRequireUniqueEmail { get; set; } = StaticMemberRequireUniqueEmail;
 
     /// <summary>
     ///     Gets or sets the set of allowed characters for a username
diff --git a/src/Umbraco.Core/EmbeddedResources/Snippets/LoginStatus.cshtml b/src/Umbraco.Core/EmbeddedResources/Snippets/LoginStatus.cshtml
index 8f5477bca476..aa70da23c8e6 100644
--- a/src/Umbraco.Core/EmbeddedResources/Snippets/LoginStatus.cshtml
+++ b/src/Umbraco.Core/EmbeddedResources/Snippets/LoginStatus.cshtml
@@ -5,7 +5,7 @@
 @using Umbraco.Extensions
 
 @{
-    var isLoggedIn = Context.User?.Identity?.IsAuthenticated ?? false;
+    var isLoggedIn = Context.User.GetMemberIdentity()?.IsAuthenticated ?? false;
     var logoutModel = new PostRedirectModel();
     // You can modify this to redirect to a different URL instead of the current one
     logoutModel.RedirectUrl = null;
@@ -15,7 +15,7 @@
 {
     <div class="login-status">
 
-        <p>Welcome back <strong>@Context?.User?.Identity?.Name</strong>!</p>
+        <p>Welcome back <strong>@Context.User?.GetMemberIdentity()?.Name</strong>!</p>
 
         @using (Html.BeginUmbracoForm<UmbLoginStatusController>("HandleLogout", new { RedirectUrl = logoutModel.RedirectUrl }))
         {
diff --git a/src/Umbraco.Core/PublishedCache/ITagQuery.cs b/src/Umbraco.Core/PublishedCache/ITagQuery.cs
index e0c6a135c90e..8df363dbe447 100644
--- a/src/Umbraco.Core/PublishedCache/ITagQuery.cs
+++ b/src/Umbraco.Core/PublishedCache/ITagQuery.cs
@@ -33,7 +33,7 @@ public interface ITagQuery
     /// <summary>
     ///     Gets all document tags.
     /// </summary>
-    ///   /// <remarks>
+    /// <remarks>
     ///     If no culture is specified, it retrieves tags with an invariant culture.
     ///     If a culture is specified, it only retrieves tags for that culture.
     ///     Use "*" to retrieve tags for all cultures.
diff --git a/src/Umbraco.Core/Services/IMemberService.cs b/src/Umbraco.Core/Services/IMemberService.cs
index a1be0b4a4cba..7d78a979c836 100644
--- a/src/Umbraco.Core/Services/IMemberService.cs
+++ b/src/Umbraco.Core/Services/IMemberService.cs
@@ -210,6 +210,21 @@ IMember CreateMemberWithIdentity(string username, string email, string name, str
     /// </returns>
     IMember? GetById(int id);
 
+    /// <summary>
+    ///     Get an list of <see cref="IMember"/> for all members with the specified email.
+    /// </summary>
+    //// <param name="email">Email to use for retrieval</param>
+    /// <returns>
+    ///     <see cref="IEnumerable{IMember}" />
+    /// </returns>
+    IEnumerable<IMember> GetMembersByEmail(string email)
+        =>
+        // TODO (V16): Remove this default implementation.
+        // The following is very inefficient, but will return the correct data, so probably better than throwing a NotImplementedException
+        // in the default implentation here, for, presumably rare, cases where a custom IMemberService implementation has been registered and
+        // does not override this method.
+        GetAllMembers().Where(x => x.Email.Equals(email));
+
     /// <summary>
     ///     Gets all Members for the specified MemberType alias
     /// </summary>
diff --git a/src/Umbraco.Core/Services/MemberService.cs b/src/Umbraco.Core/Services/MemberService.cs
index 43b5b8f28ba0..493ab313a73a 100644
--- a/src/Umbraco.Core/Services/MemberService.cs
+++ b/src/Umbraco.Core/Services/MemberService.cs
@@ -389,16 +389,23 @@ public IEnumerable<IMember> GetAll(
         }
 
         /// <summary>
-        /// Get an <see cref="IMember"/> by email
+        /// Get an <see cref="IMember"/> by email. If RequireUniqueEmailForMembers is set to false, then the first member found with the specified email will be returned.
         /// </summary>
         /// <param name="email">Email to use for retrieval</param>
         /// <returns><see cref="IMember"/></returns>
-        public IMember? GetByEmail(string email)
+        public IMember? GetByEmail(string email) => GetMembersByEmail(email).FirstOrDefault();
+
+        /// <summary>
+        /// Get an list of <see cref="IMember"/> for all members with the specified email.
+        /// </summary>
+        /// <param name="email">Email to use for retrieval</param>
+        /// <returns><see cref="IEnumerable{IMember}"/></returns>
+        public IEnumerable<IMember> GetMembersByEmail(string email)
         {
             using ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true);
             scope.ReadLock(Constants.Locks.MemberTree);
             IQuery<IMember> query = Query<IMember>().Where(x => x.Email.Equals(email));
-            return _memberRepository.Get(query)?.FirstOrDefault();
+            return _memberRepository.Get(query);
         }
 
         /// <summary>
diff --git a/src/Umbraco.Infrastructure/Cache/DefaultRepositoryCachePolicy.cs b/src/Umbraco.Infrastructure/Cache/DefaultRepositoryCachePolicy.cs
index 7f7f8d678422..9494ed2eea58 100644
--- a/src/Umbraco.Infrastructure/Cache/DefaultRepositoryCachePolicy.cs
+++ b/src/Umbraco.Infrastructure/Cache/DefaultRepositoryCachePolicy.cs
@@ -24,6 +24,8 @@ public class DefaultRepositoryCachePolicy<TEntity, TId> : RepositoryCachePolicyB
     private static readonly TEntity[] _emptyEntities = new TEntity[0]; // const
     private readonly RepositoryCachePolicyOptions _options;
 
+    private const string NullRepresentationInCache = "*NULL*";
+
     public DefaultRepositoryCachePolicy(IAppPolicyCache cache, IScopeAccessor scopeAccessor, RepositoryCachePolicyOptions options)
         : base(cache, scopeAccessor) =>
         _options = options ?? throw new ArgumentNullException(nameof(options));
@@ -116,6 +118,7 @@ public override void Delete(TEntity entity, Action<TEntity> persistDeleted)
         {
             // whatever happens, clear the cache
             var cacheKey = GetEntityCacheKey(entity.Id);
+
             Cache.Clear(cacheKey);
 
             // if there's a GetAllCacheAllowZeroCount cache, ensure it is cleared
@@ -127,20 +130,36 @@ public override void Delete(TEntity entity, Action<TEntity> persistDeleted)
     public override TEntity? Get(TId? id, Func<TId?, TEntity?> performGet, Func<TId[]?, IEnumerable<TEntity>?> performGetAll)
     {
         var cacheKey = GetEntityCacheKey(id);
+
         TEntity? fromCache = Cache.GetCacheItem<TEntity>(cacheKey);
 
-        // if found in cache then return else fetch and cache
-        if (fromCache != null)
+        // If found in cache then return immediately.
+        if (fromCache is not null)
         {
             return fromCache;
         }
 
+        // Because TEntity can never be a string, we will never be in a position where the proxy value collides withs a real value.
+        // Therefore this point can only be reached if there is a proxy null value => becomes null when cast to TEntity above OR the item simply does not exist.
+        // If we've cached a "null" value, return null.
+        if (_options.CacheNullValues && Cache.GetCacheItem<string>(cacheKey) == NullRepresentationInCache)
+        {
+            return null;
+        }
+
+        // Otherwise go to the database to retrieve.
         TEntity? entity = performGet(id);
 
         if (entity != null && entity.HasIdentity)
         {
+            // If we've found an identified entity, cache it for subsequent retrieval.
             InsertEntity(cacheKey, entity);
         }
+        else if (entity is null && _options.CacheNullValues)
+        {
+            // If we've not found an entity, and we're caching null values, cache a "null" value.
+            InsertNull(cacheKey);
+        }
 
         return entity;
     }
@@ -248,6 +267,15 @@ protected string GetEntityCacheKey(TId? id)
     protected virtual void InsertEntity(string cacheKey, TEntity entity)
         => Cache.Insert(cacheKey, () => entity, TimeSpan.FromMinutes(5), true);
 
+    protected virtual void InsertNull(string cacheKey)
+    {
+        // We can't actually cache a null value, as in doing so wouldn't be able to distinguish between
+        // a value that does exist but isn't yet cached, or a value that has been explicitly cached with a null value.
+        // Both would return null when we retrieve from the cache and we couldn't distinguish between the two.
+        // So we cache a special value that represents null, and then we can check for that value when we retrieve from the cache.
+        Cache.Insert(cacheKey, () => NullRepresentationInCache, TimeSpan.FromMinutes(5), true);
+    }
+
     protected virtual void InsertEntities(TId[]? ids, TEntity[]? entities)
     {
         if (ids?.Length == 0 && entities?.Length == 0 && _options.GetAllCacheAllowZeroCount)
diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_13_3_0/AlignUpgradedDatabase.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_13_3_0/AlignUpgradedDatabase.cs
index 6ee48ce0e7a3..f45b5d371b66 100644
--- a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_13_3_0/AlignUpgradedDatabase.cs
+++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_13_3_0/AlignUpgradedDatabase.cs
@@ -1,4 +1,4 @@
-using NPoco;
+using NPoco;
 using Umbraco.Cms.Infrastructure.Persistence;
 using Umbraco.Cms.Infrastructure.Persistence.Dtos;
 using ColumnInfo = Umbraco.Cms.Infrastructure.Persistence.SqlSyntax.ColumnInfo;
@@ -153,16 +153,26 @@ JOIN sys.columns columns
 ");
         var currentConstraintName = Database.ExecuteScalar<string>(constraintNameQuery);
 
-
-        // only rename the constraint if necessary
+        // Only rename the constraint if necessary.
         if (currentConstraintName == expectedConstraintName)
         {
             return;
         }
 
-        Sql<ISqlContext> renameConstraintQuery = Database.SqlContext.Sql(
-            $"EXEC sp_rename N'{currentConstraintName}', N'{expectedConstraintName}', N'OBJECT'");
-        Database.Execute(renameConstraintQuery);
+        if (currentConstraintName is null)
+        {
+            // Constraint does not exist, so we need to create it.
+            Sql<ISqlContext> createConstraintStatement = Database.SqlContext.Sql(@$"
+ALTER TABLE umbracoContentVersion ADD CONSTRAINT [DF_umbracoContentVersion_versionDate] DEFAULT (getdate()) FOR [versionDate]");
+            Database.Execute(createConstraintStatement);
+        }
+        else
+        {
+            // Constraint exists, and differs from the expected name, so we need to rename it.
+            Sql<ISqlContext> renameConstraintQuery = Database.SqlContext.Sql(
+                $"EXEC sp_rename N'{currentConstraintName}', N'{expectedConstraintName}', N'OBJECT'");
+            Database.Execute(renameConstraintQuery);
+        }
     }
 
     private void UpdateExternalLoginIndexes(IEnumerable<Tuple<string, string, string, bool>> indexes)
diff --git a/src/Umbraco.Infrastructure/ModelsBuilder/Building/TextBuilder.cs b/src/Umbraco.Infrastructure/ModelsBuilder/Building/TextBuilder.cs
index 22160b0ef49f..7484741b5812 100644
--- a/src/Umbraco.Infrastructure/ModelsBuilder/Building/TextBuilder.cs
+++ b/src/Umbraco.Infrastructure/ModelsBuilder/Building/TextBuilder.cs
@@ -143,14 +143,17 @@ public void WriteClrType(StringBuilder sb, Type type)
     //
     // note that the blog post above clearly states that "Nor should it be applied at the type level if the type being generated is a partial class."
     // and since our models are partial classes, we have to apply the attribute against the individual members, not the class itself.
-    private static void WriteGeneratedCodeAttribute(StringBuilder sb, string tabs) => sb.AppendFormat(
+    private void WriteGeneratedCodeAttribute(StringBuilder sb, string tabs) => sb.AppendFormat(
         "{0}[global::System.CodeDom.Compiler.GeneratedCodeAttribute(\"Umbraco.ModelsBuilder.Embedded\", \"{1}\")]\n",
-        tabs, ApiVersion.Current.Version);
+        tabs,
+        Config.IncludeVersionNumberInGeneratedModels ? ApiVersion.Current.Version : null);
 
     // writes an attribute that specifies that an output may be null.
     // (useful for consuming projects with nullable reference types enabled)
     private static void WriteMaybeNullAttribute(StringBuilder sb, string tabs, bool isReturn = false) =>
-        sb.AppendFormat("{0}[{1}global::System.Diagnostics.CodeAnalysis.MaybeNull]\n", tabs,
+        sb.AppendFormat(
+            "{0}[{1}global::System.Diagnostics.CodeAnalysis.MaybeNull]\n",
+            tabs,
             isReturn ? "return: " : string.Empty);
 
     private static string MixinStaticGetterName(string clrName) => string.Format("Get{0}", clrName);
diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/DictionaryRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/DictionaryRepository.cs
index 909c9cfec23e..bf4799e938a0 100644
--- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/DictionaryRepository.cs
+++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/DictionaryRepository.cs
@@ -102,11 +102,10 @@ protected override IRepositoryCachePolicy<IDictionaryItem, int> CreateCachePolic
         var options = new RepositoryCachePolicyOptions
         {
             // allow zero to be cached
-            GetAllCacheAllowZeroCount = true,
+            GetAllCacheAllowZeroCount = true
         };
 
-        return new SingleItemsOnlyRepositoryCachePolicy<IDictionaryItem, int>(GlobalIsolatedCache, ScopeAccessor,
-            options);
+        return new SingleItemsOnlyRepositoryCachePolicy<IDictionaryItem, int>(GlobalIsolatedCache, ScopeAccessor, options);
     }
 
     protected IDictionaryItem ConvertFromDto(DictionaryDto dto)
@@ -190,11 +189,10 @@ protected override IRepositoryCachePolicy<IDictionaryItem, Guid> CreateCachePoli
             var options = new RepositoryCachePolicyOptions
             {
                 // allow zero to be cached
-                GetAllCacheAllowZeroCount = true,
+                GetAllCacheAllowZeroCount = true
             };
 
-            return new SingleItemsOnlyRepositoryCachePolicy<IDictionaryItem, Guid>(GlobalIsolatedCache, ScopeAccessor,
-                options);
+            return new SingleItemsOnlyRepositoryCachePolicy<IDictionaryItem, Guid>(GlobalIsolatedCache, ScopeAccessor, options);
         }
     }
 
@@ -228,12 +226,13 @@ protected override IRepositoryCachePolicy<IDictionaryItem, string> CreateCachePo
         {
             var options = new RepositoryCachePolicyOptions
             {
+                // allow null to be cached
+                CacheNullValues = true,
                 // allow zero to be cached
-                GetAllCacheAllowZeroCount = true,
+                GetAllCacheAllowZeroCount = true
             };
 
-            return new SingleItemsOnlyRepositoryCachePolicy<IDictionaryItem, string>(GlobalIsolatedCache, ScopeAccessor,
-                options);
+            return new SingleItemsOnlyRepositoryCachePolicy<IDictionaryItem, string>(GlobalIsolatedCache, ScopeAccessor, options);
         }
     }
 
diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/TagRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/TagRepository.cs
index ecc6600d4c97..a722bea2a03a 100644
--- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/TagRepository.cs
+++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/TagRepository.cs
@@ -387,7 +387,9 @@ private static IEnumerable<TaggedEntity> Map(IEnumerable<TaggedEntityDto> dtos)
         }).ToList();
 
     /// <inheritdoc />
-    public IEnumerable<ITag> GetTagsForEntityType(TaggableObjectTypes objectType, string? group = null,
+    public IEnumerable<ITag> GetTagsForEntityType(
+        TaggableObjectTypes objectType,
+        string? group = null,
         string? culture = null)
     {
         Sql<ISqlContext> sql = GetTagsSql(culture, true);
@@ -401,6 +403,9 @@ public IEnumerable<ITag> GetTagsForEntityType(TaggableObjectTypes objectType, st
                 .Where<NodeDto>(dto => dto.NodeObjectType == nodeObjectType);
         }
 
+        sql = sql
+            .Where<NodeDto>(dto => !dto.Trashed);
+
         if (group.IsNullOrWhiteSpace() == false)
         {
             sql = sql
diff --git a/src/Umbraco.Infrastructure/PropertyEditors/RichTextEditorPastedImages.cs b/src/Umbraco.Infrastructure/PropertyEditors/RichTextEditorPastedImages.cs
index 8dbe6ad5b351..64e349c245f1 100644
--- a/src/Umbraco.Infrastructure/PropertyEditors/RichTextEditorPastedImages.cs
+++ b/src/Umbraco.Infrastructure/PropertyEditors/RichTextEditorPastedImages.cs
@@ -8,12 +8,14 @@
 using Microsoft.Extensions.DependencyInjection;
 using Microsoft.Extensions.Logging;
 using Microsoft.Extensions.Options;
+using Umbraco.Cms.Core.Cache;
 using Umbraco.Cms.Core.Configuration.Models;
 using Umbraco.Cms.Core.Exceptions;
 using Umbraco.Cms.Core.Hosting;
 using Umbraco.Cms.Core.IO;
 using Umbraco.Cms.Core.Media;
 using Umbraco.Cms.Core.Models;
+using Umbraco.Cms.Core.Models.Membership;
 using Umbraco.Cms.Core.Models.PublishedContent;
 using Umbraco.Cms.Core.Routing;
 using Umbraco.Cms.Core.Services;
@@ -38,6 +40,9 @@ public sealed class RichTextEditorPastedImages
     private readonly IUmbracoContextAccessor _umbracoContextAccessor;
     private readonly string _tempFolderAbsolutePath;
     private readonly IImageUrlGenerator _imageUrlGenerator;
+    private readonly IEntityService _entityService;
+    private readonly IUserService _userService;
+    private readonly AppCaches _appCaches;
     private readonly ContentSettings _contentSettings;
     private readonly Dictionary<string, GuidUdi> _uploadedImages = new();
 
@@ -67,6 +72,7 @@ public RichTextEditorPastedImages(
     {
     }
 
+    [Obsolete("Use the non-obsolete constructor. Scheduled for removal in v14")]
     public RichTextEditorPastedImages(
         IUmbracoContextAccessor umbracoContextAccessor,
         ILogger<RichTextEditorPastedImages> logger,
@@ -79,6 +85,39 @@ public RichTextEditorPastedImages(
         IPublishedUrlProvider publishedUrlProvider,
         IImageUrlGenerator imageUrlGenerator,
         IOptions<ContentSettings> contentSettings)
+        : this(
+            umbracoContextAccessor,
+            logger,
+            hostingEnvironment,
+            mediaService,
+            contentTypeBaseServiceProvider,
+            mediaFileManager,
+            mediaUrlGenerators,
+            shortStringHelper,
+            publishedUrlProvider,
+            imageUrlGenerator,
+            StaticServiceProvider.Instance.GetRequiredService<IEntityService>(),
+            StaticServiceProvider.Instance.GetRequiredService<IUserService>(),
+            StaticServiceProvider.Instance.GetRequiredService<AppCaches>(),
+            contentSettings)
+    {
+    }
+
+    public RichTextEditorPastedImages(
+        IUmbracoContextAccessor umbracoContextAccessor,
+        ILogger<RichTextEditorPastedImages> logger,
+        IHostingEnvironment hostingEnvironment,
+        IMediaService mediaService,
+        IContentTypeBaseServiceProvider contentTypeBaseServiceProvider,
+        MediaFileManager mediaFileManager,
+        MediaUrlGeneratorCollection mediaUrlGenerators,
+        IShortStringHelper shortStringHelper,
+        IPublishedUrlProvider publishedUrlProvider,
+        IImageUrlGenerator imageUrlGenerator,
+        IEntityService entityService,
+        IUserService userService,
+        AppCaches appCaches,
+        IOptions<ContentSettings> contentSettings)
     {
         _umbracoContextAccessor =
             umbracoContextAccessor ?? throw new ArgumentNullException(nameof(umbracoContextAccessor));
@@ -92,6 +131,9 @@ public RichTextEditorPastedImages(
         _shortStringHelper = shortStringHelper;
         _publishedUrlProvider = publishedUrlProvider;
         _imageUrlGenerator = imageUrlGenerator;
+        _entityService = entityService;
+        _userService = userService;
+        _appCaches = appCaches;
         _contentSettings = contentSettings.Value;
 
         _tempFolderAbsolutePath = _hostingEnvironment.MapPathContentRoot(Constants.SystemDirectories.TempImageUploads);
@@ -270,7 +312,7 @@ private void PersistMediaItem(Guid mediaParentFolder, int userId, HtmlNode img,
                 : Constants.Conventions.MediaTypes.Image;
 
             IMedia mediaFile = mediaParentFolder == Guid.Empty
-                ? _mediaService.CreateMedia(mediaItemName, Constants.System.Root, mediaType, userId)
+                ? _mediaService.CreateMedia(mediaItemName, GetDefaultMediaRoot(userId), mediaType, userId)
                 : _mediaService.CreateMedia(mediaItemName, mediaParentFolder, mediaType, userId);
 
             var fileInfo = new FileInfo(absoluteTempImagePath);
@@ -354,4 +396,11 @@ private void PersistMediaItem(Guid mediaParentFolder, int userId, HtmlNode img,
     }
 
     private bool IsValidPath(string imagePath) => imagePath.StartsWith(_tempFolderAbsolutePath);
+
+    private int GetDefaultMediaRoot(int userId)
+    {
+        IUser user = _userService.GetUserById(userId) ?? throw new ArgumentException("User could not be found");
+        var userStartNodes = user.CalculateMediaStartNodeIds(_entityService, _appCaches);
+        return userStartNodes?.FirstOrDefault() ?? Constants.System.Root;
+    }
 }
diff --git a/src/Umbraco.Infrastructure/Runtime/FileSystemMainDomLock.cs b/src/Umbraco.Infrastructure/Runtime/FileSystemMainDomLock.cs
index 6dcd3ef9b0b3..27662f979a1b 100644
--- a/src/Umbraco.Infrastructure/Runtime/FileSystemMainDomLock.cs
+++ b/src/Umbraco.Infrastructure/Runtime/FileSystemMainDomLock.cs
@@ -15,6 +15,7 @@ internal class FileSystemMainDomLock : IMainDomLock
     private readonly string _lockFilePath;
     private readonly ILogger<FileSystemMainDomLock> _logger;
     private readonly string _releaseSignalFilePath;
+    private bool _disposed;
     private Task? _listenForReleaseSignalFileTask;
 
     private FileStream? _lockFileStream;
@@ -88,16 +89,14 @@ public Task ListenAsync()
             ListeningLoop,
             _cancellationTokenSource.Token,
             TaskCreationOptions.LongRunning,
-            TaskScheduler.Default);
+            TaskScheduler.Default)
+            .Unwrap(); // Because ListeningLoop is an async method, we need to use Unwrap to return the inner task.
 
         return _listenForReleaseSignalFileTask;
     }
 
-    public void Dispose()
-    {
-        _lockFileStream?.Close();
-        _lockFileStream = null;
-    }
+    /// <summary>Releases the resources used by this <see cref="FileSystemMainDomLock" />.</summary>
+    public void Dispose() => Dispose(true);
 
     public void CreateLockReleaseSignalFile() =>
         File.Open(_releaseSignalFilePath, FileMode.OpenOrCreate, FileAccess.ReadWrite,
@@ -107,7 +106,27 @@ public void CreateLockReleaseSignalFile() =>
     public void DeleteLockReleaseSignalFile() =>
         File.Delete(_releaseSignalFilePath);
 
-    private void ListeningLoop()
+    /// <summary>Releases the resources used by this <see cref="FileSystemMainDomLock" />.</summary>
+    /// <param name="disposing">true to release both managed resources.</param>
+    protected virtual void Dispose(bool disposing)
+    {
+        if (disposing && !_disposed)
+        {
+            _logger.LogInformation($"{nameof(FileSystemMainDomLock)} Disposing...");
+            _cancellationTokenSource.Cancel();
+            _cancellationTokenSource.Dispose();
+            ReleaseLock();
+            _disposed = true;
+        }
+    }
+
+    private void ReleaseLock()
+    {
+        _lockFileStream?.Close();
+        _lockFileStream = null;
+    }
+
+    private async Task ListeningLoop()
     {
         while (true)
         {
@@ -126,12 +145,12 @@ private void ListeningLoop()
                 {
                     _logger.LogDebug("Found lock release signal file, releasing lock on {lockFilePath}", _lockFilePath);
                 }
-                _lockFileStream?.Close();
-                _lockFileStream = null;
+
+                ReleaseLock();
                 break;
             }
 
-            Thread.Sleep(_globalSettings.CurrentValue.MainDomReleaseSignalPollingInterval);
+            await Task.Delay(_globalSettings.CurrentValue.MainDomReleaseSignalPollingInterval, _cancellationTokenSource.Token);
         }
     }
 }
diff --git a/src/Umbraco.Infrastructure/Security/BackOfficeUserStore.cs b/src/Umbraco.Infrastructure/Security/BackOfficeUserStore.cs
index 0d2767dd25c8..21f8978e7188 100644
--- a/src/Umbraco.Infrastructure/Security/BackOfficeUserStore.cs
+++ b/src/Umbraco.Infrastructure/Security/BackOfficeUserStore.cs
@@ -281,7 +281,20 @@ public override Task<IdentityResult> DeleteAsync(
         cancellationToken.ThrowIfCancellationRequested();
         ThrowIfDisposed();
 
-        IUser? user = _userService.GetUserById(UserIdToInt(userId));
+        // In the external login flow - see BackOfficeController.ExternalSignInAsync - we can have a situation where an
+        // error has occured but the user is signed in. For that reason, at the end of the process, if errors are
+        // recorded, the user is signed out.
+        // Before signing out, we request the user in order to update the security stamp - see UmbracoSignInManager.SignOutAsync.
+        // But we can have a situation where the signed in principal has the ID from the external provider, which may not be something
+        // we can parse to an integer.
+        // If that's the case, return null rather than throwing an exception. Without an Umbraco user, we can't update the security stamp,
+        // so no need to fail here.
+        if (!TryUserIdToInt(userId, out var userIdAsInt))
+        {
+            return Task.FromResult((BackOfficeIdentityUser?)null)!;
+        }
+
+        IUser? user = _userService.GetUserById(userIdAsInt);
         if (user == null)
         {
             return Task.FromResult((BackOfficeIdentityUser?)null)!;
diff --git a/src/Umbraco.Infrastructure/Security/UmbracoUserStore.cs b/src/Umbraco.Infrastructure/Security/UmbracoUserStore.cs
index 35a8f2eea9f0..544a8dbd745a 100644
--- a/src/Umbraco.Infrastructure/Security/UmbracoUserStore.cs
+++ b/src/Umbraco.Infrastructure/Security/UmbracoUserStore.cs
@@ -33,18 +33,29 @@ protected UmbracoUserStore(IdentityErrorDescriber describer)
 
     protected static int UserIdToInt(string? userId)
     {
-        if (int.TryParse(userId, NumberStyles.Integer, CultureInfo.InvariantCulture, out var result))
+        if (TryUserIdToInt(userId, out int result))
         {
             return result;
         }
 
+        throw new InvalidOperationException($"Unable to convert user ID ({userId})to int using InvariantCulture");
+    }
+
+    protected static bool TryUserIdToInt(string? userId, out int result)
+    {
+        if (int.TryParse(userId, NumberStyles.Integer, CultureInfo.InvariantCulture, out result))
+        {
+            return true;
+        }
+
         if (Guid.TryParse(userId, out Guid key))
         {
             // Reverse the IntExtensions.ToGuid
-            return BitConverter.ToInt32(key.ToByteArray(), 0);
+            result = BitConverter.ToInt32(key.ToByteArray(), 0);
+            return true;
         }
 
-        throw new InvalidOperationException($"Unable to convert user ID ({userId})to int using InvariantCulture");
+        return false;
     }
 
     protected static string UserIdToString(int userId) => string.Intern(userId.ToString(CultureInfo.InvariantCulture));
diff --git a/src/Umbraco.Web.BackOffice/Controllers/MediaController.cs b/src/Umbraco.Web.BackOffice/Controllers/MediaController.cs
index abd1b2b78738..e6a131950e38 100644
--- a/src/Umbraco.Web.BackOffice/Controllers/MediaController.cs
+++ b/src/Umbraco.Web.BackOffice/Controllers/MediaController.cs
@@ -789,8 +789,7 @@ public async Task<IActionResult> PostAddFile([FromForm] string path, [FromForm]
                     continue;
                 }
 
-                using var stream = new MemoryStream();
-                await formFile.CopyToAsync(stream);
+                await using var stream = formFile.OpenReadStream();
                 if (_fileStreamSecurityValidator != null && _fileStreamSecurityValidator.IsConsideredSafe(stream) == false)
                 {
                     tempFiles.Notifications.Add(new BackOfficeNotification(
diff --git a/src/Umbraco.Web.BackOffice/Controllers/MemberController.cs b/src/Umbraco.Web.BackOffice/Controllers/MemberController.cs
index 8851a73a2b07..d03fa87a4ac6 100644
--- a/src/Umbraco.Web.BackOffice/Controllers/MemberController.cs
+++ b/src/Umbraco.Web.BackOffice/Controllers/MemberController.cs
@@ -8,8 +8,11 @@
 using Microsoft.AspNetCore.Mvc;
 using Microsoft.Extensions.DependencyInjection;
 using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Options;
 using Umbraco.Cms.Core;
+using Umbraco.Cms.Core.Configuration.Models;
 using Umbraco.Cms.Core.ContentApps;
+using Umbraco.Cms.Core.DependencyInjection;
 using Umbraco.Cms.Core.Dictionary;
 using Umbraco.Cms.Core.Events;
 using Umbraco.Cms.Core.Mapping;
@@ -26,7 +29,6 @@
 using Umbraco.Cms.Web.BackOffice.ModelBinders;
 using Umbraco.Cms.Web.Common.Attributes;
 using Umbraco.Cms.Web.Common.Authorization;
-using Umbraco.Cms.Web.Common.DependencyInjection;
 using Umbraco.Cms.Web.Common.Filters;
 using Umbraco.Cms.Web.Common.Security;
 using Umbraco.Extensions;
@@ -55,6 +57,7 @@ public class MemberController : ContentControllerBase
     private readonly ITwoFactorLoginService _twoFactorLoginService;
     private readonly IShortStringHelper _shortStringHelper;
     private readonly IUmbracoMapper _umbracoMapper;
+    private readonly SecuritySettings _securitySettings;
 
     /// <summary>
     ///     Initializes a new instance of the <see cref="MemberController" /> class.
@@ -75,6 +78,7 @@ public class MemberController : ContentControllerBase
     /// <param name="passwordChanger">The password changer</param>
     /// <param name="scopeProvider">The core scope provider</param>
     /// <param name="twoFactorLoginService">The two factor login service</param>
+    /// <param name="securitySettings">The security settings</param>
     [ActivatorUtilitiesConstructor]
     public MemberController(
         ICultureDictionary cultureDictionary,
@@ -92,7 +96,8 @@ public MemberController(
         IJsonSerializer jsonSerializer,
         IPasswordChanger<MemberIdentityUser> passwordChanger,
         ICoreScopeProvider scopeProvider,
-        ITwoFactorLoginService twoFactorLoginService)
+        ITwoFactorLoginService twoFactorLoginService,
+        IOptions<SecuritySettings> securitySettings)
         : base(cultureDictionary, loggerFactory, shortStringHelper, eventMessages, localizedTextService, jsonSerializer)
     {
         _propertyEditors = propertyEditors;
@@ -108,9 +113,49 @@ public MemberController(
         _passwordChanger = passwordChanger;
         _scopeProvider = scopeProvider;
         _twoFactorLoginService = twoFactorLoginService;
+        _securitySettings = securitySettings.Value;
     }
 
-    [Obsolete("Use constructor that also takes an ITwoFactorLoginService. Scheduled for removal in V13")]
+    [Obsolete("Please use the constructor that takes all paramters. Scheduled for removal in V14")]
+    public MemberController(
+        ICultureDictionary cultureDictionary,
+        ILoggerFactory loggerFactory,
+        IShortStringHelper shortStringHelper,
+        IEventMessagesFactory eventMessages,
+        ILocalizedTextService localizedTextService,
+        PropertyEditorCollection propertyEditors,
+        IUmbracoMapper umbracoMapper,
+        IMemberService memberService,
+        IMemberTypeService memberTypeService,
+        IMemberManager memberManager,
+        IDataTypeService dataTypeService,
+        IBackOfficeSecurityAccessor backOfficeSecurityAccessor,
+        IJsonSerializer jsonSerializer,
+        IPasswordChanger<MemberIdentityUser> passwordChanger,
+        ICoreScopeProvider scopeProvider,
+        ITwoFactorLoginService twoFactorLoginService)
+        : this(
+              cultureDictionary,
+              loggerFactory,
+              shortStringHelper,
+              eventMessages,
+              localizedTextService,
+              propertyEditors,
+              umbracoMapper,
+              memberService,
+              memberTypeService,
+              memberManager,
+              dataTypeService,
+              backOfficeSecurityAccessor,
+              jsonSerializer,
+              passwordChanger,
+              scopeProvider,
+              twoFactorLoginService,
+              StaticServiceProvider.Instance.GetRequiredService<IOptions<SecuritySettings>>())
+    {
+    }
+
+    [Obsolete("Please use the constructor that takes all paramters. Scheduled for removal in V14")]
     public MemberController(
         ICultureDictionary cultureDictionary,
         ILoggerFactory loggerFactory,
@@ -461,7 +506,7 @@ private async Task<ActionResult<bool>> CreateMemberAsync(MemberSave contentItem)
         }
 
         // now re-look up the member, which will now exist
-        IMember? member = _memberService.GetByEmail(contentItem.Email);
+        IMember? member = _memberService.GetByUsername(contentItem.Username);
 
         if (member is null)
         {
@@ -678,6 +723,17 @@ private async Task<bool> ValidateMemberDataAsync(MemberSave contentItem)
             return false;
         }
 
+        // User names can only contain the configured allowed characters. This is validated by ASP.NET Identity on create
+        // as the setting is applied to the IdentityOptions, but we need to check ourselves for updates.
+        var allowedUserNameCharacters = _securitySettings.AllowedUserNameCharacters;
+        if (contentItem.Username.Any(c => allowedUserNameCharacters.Contains(c) == false))
+        {
+            ModelState.AddPropertyError(
+                new ValidationResult("Username contains invalid characters"),
+                $"{Constants.PropertyEditors.InternalGenericPropertiesPrefix}login");
+            return false;
+        }
+
         if (contentItem.Password != null && !contentItem.Password.NewPassword.IsNullOrWhiteSpace())
         {
             IdentityResult validPassword = await _memberManager.ValidatePasswordAsync(contentItem.Password.NewPassword);
@@ -699,13 +755,16 @@ private async Task<bool> ValidateMemberDataAsync(MemberSave contentItem)
             return false;
         }
 
-        IMember? byEmail = _memberService.GetByEmail(contentItem.Email);
-        if (byEmail != null && byEmail.Key != contentItem.Key)
+        if (_securitySettings.MemberRequireUniqueEmail)
         {
-            ModelState.AddPropertyError(
-                new ValidationResult("Email address is already in use", new[] { "value" }),
-                $"{Constants.PropertyEditors.InternalGenericPropertiesPrefix}email");
-            return false;
+            IMember? byEmail = _memberService.GetByEmail(contentItem.Email);
+            if (byEmail != null && byEmail.Key != contentItem.Key)
+            {
+                ModelState.AddPropertyError(
+                    new ValidationResult("Email address is already in use", new[] { "value" }),
+                    $"{Constants.PropertyEditors.InternalGenericPropertiesPrefix}email");
+                return false;
+            }
         }
 
         return true;
diff --git a/src/Umbraco.Web.BackOffice/Controllers/PreviewController.cs b/src/Umbraco.Web.BackOffice/Controllers/PreviewController.cs
index f1531ecb8d8f..676e18a58905 100644
--- a/src/Umbraco.Web.BackOffice/Controllers/PreviewController.cs
+++ b/src/Umbraco.Web.BackOffice/Controllers/PreviewController.cs
@@ -1,10 +1,13 @@
 using System.Globalization;
+using System.Text.RegularExpressions;
 using Microsoft.AspNetCore.Authorization;
 using Microsoft.AspNetCore.Mvc;
 using Microsoft.AspNetCore.Mvc.ViewEngines;
+using Microsoft.Extensions.DependencyInjection;
 using Microsoft.Extensions.Options;
 using Umbraco.Cms.Core;
 using Umbraco.Cms.Core.Configuration.Models;
+using Umbraco.Cms.Core.DependencyInjection;
 using Umbraco.Cms.Core.Editors;
 using Umbraco.Cms.Core.Features;
 using Umbraco.Cms.Core.Hosting;
@@ -28,7 +31,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers;
 
 [DisableBrowserCache]
 [Area(Constants.Web.Mvc.BackOfficeArea)]
-public class PreviewController : Controller
+public partial class PreviewController : Controller
 {
     private readonly IBackOfficeSecurityAccessor _backofficeSecurityAccessor;
     private readonly ICookieManager _cookieManager;
@@ -40,7 +43,9 @@ public class PreviewController : Controller
     private readonly IRuntimeMinifier _runtimeMinifier;
     private readonly IUmbracoContextAccessor _umbracoContextAccessor;
     private readonly ICompositeViewEngine _viewEngines;
+    private readonly WebRoutingSettings _webRoutingSettings;
 
+    [Obsolete("Please use the non-obsolete constructor.")]
     public PreviewController(
         UmbracoFeatures features,
         IOptionsSnapshot<GlobalSettings> globalSettings,
@@ -52,9 +57,38 @@ public PreviewController(
         IRuntimeMinifier runtimeMinifier,
         ICompositeViewEngine viewEngines,
         IUmbracoContextAccessor umbracoContextAccessor)
+        : this(
+            features,
+            globalSettings,
+            StaticServiceProvider.Instance.GetRequiredService<IOptionsSnapshot<WebRoutingSettings>>(),
+            publishedSnapshotService,
+            backofficeSecurityAccessor,
+            localizationService,
+            hostingEnvironment,
+            cookieManager,
+            runtimeMinifier,
+            viewEngines,
+            umbracoContextAccessor)
+    {
+    }
+
+    [ActivatorUtilitiesConstructor]
+    public PreviewController(
+        UmbracoFeatures features,
+        IOptionsSnapshot<GlobalSettings> globalSettings,
+        IOptionsSnapshot<WebRoutingSettings> webRoutingSettings,
+        IPublishedSnapshotService publishedSnapshotService,
+        IBackOfficeSecurityAccessor backofficeSecurityAccessor,
+        ILocalizationService localizationService,
+        IHostingEnvironment hostingEnvironment,
+        ICookieManager cookieManager,
+        IRuntimeMinifier runtimeMinifier,
+        ICompositeViewEngine viewEngines,
+        IUmbracoContextAccessor umbracoContextAccessor)
     {
         _features = features;
         _globalSettings = globalSettings.Value;
+        _webRoutingSettings = webRoutingSettings.Value;
         _publishedSnapshotService = publishedSnapshotService;
         _backofficeSecurityAccessor = backofficeSecurityAccessor;
         _localizationService = localizationService;
@@ -181,6 +215,43 @@ public ActionResult End(string? redir = null)
         // Expire Client-side cookie that determines whether the user has accepted to be in Preview Mode when visiting the website.
         _cookieManager.ExpireCookie(Constants.Web.AcceptPreviewCookieName);
 
+        // are we attempting a redirect to the default route (by ID with optional culture)?
+        Match match = DefaultPreviewRedirectRegex().Match(redir ?? string.Empty);
+        if (match.Success)
+        {
+            var id = int.Parse(match.Groups["id"].Value);
+
+            // first try to resolve the published URL
+            if (_umbracoContextAccessor.TryGetUmbracoContext(out IUmbracoContext? umbracoContext) &&
+                umbracoContext.Content is not null)
+            {
+                IPublishedContent? publishedContent = umbracoContext.Content.GetById(id);
+                if (publishedContent is null)
+                {
+                    // content is not published, redirect to root
+                    return Redirect("/");
+                }
+
+                var culture = publishedContent.ContentType.VariesByCulture()
+                              && match.Groups.TryGetValue("culture", out Group? group)
+                    ? group.Value
+                    : null;
+
+                var publishedUrl = publishedContent.Url(culture);
+                if (WebPath.IsWellFormedWebPath(publishedUrl, UriKind.RelativeOrAbsolute))
+                {
+                    return Redirect(publishedUrl);
+                }
+            }
+
+            // could not resolve the published URL - are we allowed to route content by ID?
+            if (_webRoutingSettings.DisableFindContentByIdPath)
+            {
+                // no we are not - redirect to root instead
+                return Redirect("/");
+            }
+        }
+
         if (WebPath.IsWellFormedWebPath(redir, UriKind.Relative)
             && Uri.TryCreate(redir, UriKind.Relative, out Uri? url))
         {
@@ -189,4 +260,7 @@ public ActionResult End(string? redir = null)
 
         return Redirect("/");
     }
+
+    [GeneratedRegex("^\\/(?<id>\\d*)(\\?culture=(?<culture>[\\w-]*))?$")]
+    private static partial Regex DefaultPreviewRedirectRegex();
 }
diff --git a/src/Umbraco.Web.BackOffice/Controllers/UsersController.cs b/src/Umbraco.Web.BackOffice/Controllers/UsersController.cs
index c855a87ea4b2..960afa365e9a 100644
--- a/src/Umbraco.Web.BackOffice/Controllers/UsersController.cs
+++ b/src/Umbraco.Web.BackOffice/Controllers/UsersController.cs
@@ -322,7 +322,7 @@ public ActionResult<string[]> PostClearAvatar(int id)
     /// <returns></returns>
     [OutgoingEditorModelEvent]
     [Authorize(Policy = AuthorizationPolicies.AdminUserEditsRequireAdmin)]
-    public ActionResult<IEnumerable<UserDisplay?>> GetByIds([FromJsonPath] int[] ids)
+    public ActionResult<IEnumerable<UserDisplay?>> GetByIds([FromQuery] int[] ids)
     {
         if (ids == null)
         {
@@ -714,6 +714,15 @@ private async Task SendUserInviteEmailAsync(UserBasic? userDisplay, string? from
 
         var hasErrors = false;
 
+        // User names can only contain the configured allowed characters. This is validated by ASP.NET Identity on create
+        // as the setting is applied to the BackOfficeIdentityOptions, but we need to check ourselves for updates.
+        var allowedUserNameCharacters = _securitySettings.AllowedUserNameCharacters;
+        if (userSave.Username.Any(c => allowedUserNameCharacters.Contains(c) == false))
+        {
+            ModelState.AddModelError("Username", "Username contains invalid characters");
+            hasErrors = true;
+        }
+
         // we need to check if there's any Deny Local login providers present, if so we need to ensure that the user's email address cannot be changed
         var hasDenyLocalLogin = _externalLogins.HasDenyLocalLogin();
         if (hasDenyLocalLogin)
diff --git a/src/Umbraco.Web.BackOffice/DependencyInjection/UmbracoBuilder.BackOfficeAuth.cs b/src/Umbraco.Web.BackOffice/DependencyInjection/UmbracoBuilder.BackOfficeAuth.cs
index 0945d3459b0a..fd3bfe71bc31 100644
--- a/src/Umbraco.Web.BackOffice/DependencyInjection/UmbracoBuilder.BackOfficeAuth.cs
+++ b/src/Umbraco.Web.BackOffice/DependencyInjection/UmbracoBuilder.BackOfficeAuth.cs
@@ -202,6 +202,7 @@ private static void CreatePolicies(AuthorizationOptions options, string backOffi
         {
             policy.AuthenticationSchemes.Add(backOfficeAuthenticationScheme);
             policy.Requirements.Add(new AdminUsersRequirement());
+            policy.Requirements.Add(new AdminUsersRequirement("ids"));
             policy.Requirements.Add(new AdminUsersRequirement("userIds"));
         });
 
diff --git a/src/Umbraco.Web.BackOffice/Filters/MemberSaveModelValidator.cs b/src/Umbraco.Web.BackOffice/Filters/MemberSaveModelValidator.cs
index 6b29803e0521..68d37bba3c32 100644
--- a/src/Umbraco.Web.BackOffice/Filters/MemberSaveModelValidator.cs
+++ b/src/Umbraco.Web.BackOffice/Filters/MemberSaveModelValidator.cs
@@ -2,8 +2,12 @@
 using Microsoft.AspNetCore.Mvc;
 using Microsoft.AspNetCore.Mvc.Filters;
 using Microsoft.AspNetCore.Mvc.ModelBinding;
+using Microsoft.Extensions.DependencyInjection;
 using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Options;
 using Umbraco.Cms.Core;
+using Umbraco.Cms.Core.Configuration.Models;
+using Umbraco.Cms.Core.DependencyInjection;
 using Umbraco.Cms.Core.Models;
 using Umbraco.Cms.Core.Models.ContentEditing;
 using Umbraco.Cms.Core.Security;
@@ -16,27 +20,29 @@ namespace Umbraco.Cms.Web.BackOffice.Filters;
 /// <summary>
 ///     Validator for <see cref="ContentItemSave" />
 /// </summary>
-internal class
-    MemberSaveModelValidator : ContentModelValidator<IMember, MemberSave, IContentProperties<ContentPropertyBasic>>
+internal class MemberSaveModelValidator : ContentModelValidator<IMember, MemberSave, IContentProperties<ContentPropertyBasic>>
 {
     private readonly IBackOfficeSecurity? _backofficeSecurity;
     private readonly IMemberService _memberService;
     private readonly IMemberTypeService _memberTypeService;
     private readonly IShortStringHelper _shortStringHelper;
+    private readonly SecuritySettings _securitySettings;
 
     public MemberSaveModelValidator(
-        ILogger<MemberSaveModelValidator> logger,
-        IBackOfficeSecurity? backofficeSecurity,
-        IMemberTypeService memberTypeService,
-        IMemberService memberService,
-        IShortStringHelper shortStringHelper,
-        IPropertyValidationService propertyValidationService)
-        : base(logger, propertyValidationService)
+       ILogger<MemberSaveModelValidator> logger,
+       IBackOfficeSecurity? backofficeSecurity,
+       IMemberTypeService memberTypeService,
+       IMemberService memberService,
+       IShortStringHelper shortStringHelper,
+       IPropertyValidationService propertyValidationService,
+       SecuritySettings securitySettings)
+       : base(logger, propertyValidationService)
     {
         _backofficeSecurity = backofficeSecurity;
         _memberTypeService = memberTypeService ?? throw new ArgumentNullException(nameof(memberTypeService));
         _memberService = memberService ?? throw new ArgumentNullException(nameof(memberService));
         _shortStringHelper = shortStringHelper ?? throw new ArgumentNullException(nameof(shortStringHelper));
+        _securitySettings = securitySettings;
     }
 
     public override bool ValidatePropertiesData(
@@ -64,8 +70,7 @@ public override bool ValidatePropertiesData(
                 $"{Constants.PropertyEditors.InternalGenericPropertiesPrefix}email");
         }
 
-        var validEmail = ValidateUniqueEmail(model);
-        if (validEmail == false)
+        if (_securitySettings.MemberRequireUniqueEmail && ValidateUniqueEmail(model) is false)
         {
             modelState.AddPropertyError(
                 new ValidationResult("Email address is already in use", new[] { "value" }),
diff --git a/src/Umbraco.Web.BackOffice/Filters/MemberSaveValidationAttribute.cs b/src/Umbraco.Web.BackOffice/Filters/MemberSaveValidationAttribute.cs
index 61e119b66a0e..568d1e82402f 100644
--- a/src/Umbraco.Web.BackOffice/Filters/MemberSaveValidationAttribute.cs
+++ b/src/Umbraco.Web.BackOffice/Filters/MemberSaveValidationAttribute.cs
@@ -1,6 +1,8 @@
 using Microsoft.AspNetCore.Mvc;
 using Microsoft.AspNetCore.Mvc.Filters;
 using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Options;
+using Umbraco.Cms.Core.Configuration.Models;
 using Umbraco.Cms.Core.Models.ContentEditing;
 using Umbraco.Cms.Core.Security;
 using Umbraco.Cms.Core.Services;
@@ -25,6 +27,7 @@ private sealed class MemberSaveValidationFilter : IActionFilter
         private readonly IMemberTypeService _memberTypeService;
         private readonly IPropertyValidationService _propertyValidationService;
         private readonly IShortStringHelper _shortStringHelper;
+        private readonly SecuritySettings _securitySettings;
 
         public MemberSaveValidationFilter(
             ILoggerFactory loggerFactory,
@@ -32,16 +35,16 @@ public MemberSaveValidationFilter(
             IMemberTypeService memberTypeService,
             IMemberService memberService,
             IShortStringHelper shortStringHelper,
-            IPropertyValidationService propertyValidationService)
+            IPropertyValidationService propertyValidationService,
+            IOptions<SecuritySettings> securitySettings)
         {
-            _loggerFactory = loggerFactory ?? throw new ArgumentNullException(nameof(loggerFactory));
-            _backofficeSecurityAccessor = backofficeSecurityAccessor ??
-                                          throw new ArgumentNullException(nameof(backofficeSecurityAccessor));
-            _memberTypeService = memberTypeService ?? throw new ArgumentNullException(nameof(memberTypeService));
-            _memberService = memberService ?? throw new ArgumentNullException(nameof(memberService));
-            _shortStringHelper = shortStringHelper ?? throw new ArgumentNullException(nameof(shortStringHelper));
-            _propertyValidationService = propertyValidationService ??
-                                         throw new ArgumentNullException(nameof(propertyValidationService));
+            _loggerFactory = loggerFactory;
+            _backofficeSecurityAccessor = backofficeSecurityAccessor;
+            _memberTypeService = memberTypeService;
+            _memberService = memberService;
+            _shortStringHelper = shortStringHelper;
+            _propertyValidationService = propertyValidationService;
+            _securitySettings = securitySettings.Value;
         }
 
         public void OnActionExecuting(ActionExecutingContext context)
@@ -53,7 +56,8 @@ public void OnActionExecuting(ActionExecutingContext context)
                 _memberTypeService,
                 _memberService,
                 _shortStringHelper,
-                _propertyValidationService);
+                _propertyValidationService,
+                _securitySettings);
             //now do each validation step
             if (contentItemValidator.ValidateExistingContent(model, context))
             {
diff --git a/src/Umbraco.Web.BackOffice/Trees/ContentTreeController.cs b/src/Umbraco.Web.BackOffice/Trees/ContentTreeController.cs
index 4cdd8cef7c3c..0ef895e2072a 100644
--- a/src/Umbraco.Web.BackOffice/Trees/ContentTreeController.cs
+++ b/src/Umbraco.Web.BackOffice/Trees/ContentTreeController.cs
@@ -317,13 +317,7 @@ protected MenuItemCollection GetAllNodeMenuItems(IUmbracoEntity item)
 
         if (_emailSender.CanSendRequiredEmail())
         {
-            menu.Items.Add(new MenuItem("notify", LocalizedTextService)
-            {
-                Icon = "icon-megaphone",
-                SeparatorBefore = true,
-                OpensDialog = true,
-                UseLegacyIcon = false
-            });
+            AddActionNode<ActionNotify>(item, menu, hasSeparator: true, opensDialog: true, useLegacyIcon: false);
         }
 
         if ((item is DocumentEntitySlim documentEntity && documentEntity.IsContainer) == false)
diff --git a/src/Umbraco.Web.Common/Extensions/HttpContextExtensions.cs b/src/Umbraco.Web.Common/Extensions/HttpContextExtensions.cs
index 0a84f318f6f1..fd46ef6903af 100644
--- a/src/Umbraco.Web.Common/Extensions/HttpContextExtensions.cs
+++ b/src/Umbraco.Web.Common/Extensions/HttpContextExtensions.cs
@@ -62,9 +62,16 @@ public static async Task<AuthenticateResult> AuthenticateBackOfficeAsync(this Ht
         // Update the HttpContext's user with the authenticated user's principal to ensure
         // that subsequent requests within the same context will recognize the user
         // as authenticated.
-        if (result.Succeeded)
+        if (result is { Succeeded: true, Principal.Identity: not null })
         {
-            httpContext.User = result.Principal;
+            // We need to get existing identities that are not the backoffice kind and flow them to the new identity
+            // Otherwise we can't log in as both a member and a backoffice user
+            // For instance if you've enabled basic auth.
+            ClaimsPrincipal? authenticatedPrincipal = result.Principal;
+            IEnumerable<ClaimsIdentity> existingIdentities = httpContext.User.Identities.Where(x => x.IsAuthenticated && x.AuthenticationType != authenticatedPrincipal.Identity.AuthenticationType);
+            authenticatedPrincipal.AddIdentities(existingIdentities);
+
+            httpContext.User = authenticatedPrincipal;
         }
 
         return result;
diff --git a/src/Umbraco.Web.Common/Extensions/MemberClaimsPrincipalExtensions.cs b/src/Umbraco.Web.Common/Extensions/MemberClaimsPrincipalExtensions.cs
new file mode 100644
index 000000000000..03205f7baa76
--- /dev/null
+++ b/src/Umbraco.Web.Common/Extensions/MemberClaimsPrincipalExtensions.cs
@@ -0,0 +1,18 @@
+using System.Security.Claims;
+using Microsoft.AspNetCore.Identity;
+
+namespace Umbraco.Extensions;
+
+public static class MemberClaimsPrincipalExtensions
+{
+    /// <summary>
+    /// Tries to get specifically the member identity from the ClaimsPrincipal
+    /// </summary>
+    /// <remarks>
+    /// The identity returned is the one with default authentication type.
+    /// </remarks>
+    /// <param name="principal">The principal to find the identity in.</param>
+    /// <returns>The default authenticated authentication type identity.</returns>
+    public static ClaimsIdentity? GetMemberIdentity(this ClaimsPrincipal principal)
+        => principal.Identities.FirstOrDefault(x => x.AuthenticationType == IdentityConstants.ApplicationScheme);
+}
diff --git a/src/Umbraco.Web.Common/RuntimeMinification/SmidgeOptionsSetup.cs b/src/Umbraco.Web.Common/RuntimeMinification/SmidgeOptionsSetup.cs
index 37701928c61f..2452e30f661f 100644
--- a/src/Umbraco.Web.Common/RuntimeMinification/SmidgeOptionsSetup.cs
+++ b/src/Umbraco.Web.Common/RuntimeMinification/SmidgeOptionsSetup.cs
@@ -1,4 +1,5 @@
 using Microsoft.Extensions.Options;
+using Smidge.Cache;
 using Smidge.Options;
 using Umbraco.Cms.Core.Configuration.Models;
 
@@ -6,17 +7,31 @@ namespace Umbraco.Cms.Web.Common.RuntimeMinification;
 
 public class SmidgeOptionsSetup : IConfigureOptions<SmidgeOptions>
 {
-    private readonly IOptions<RuntimeMinificationSettings> _runtimeMinificatinoSettings;
+    private readonly IOptions<RuntimeMinificationSettings> _runtimeMinificationSettings;
 
     public SmidgeOptionsSetup(IOptions<RuntimeMinificationSettings> runtimeMinificatinoSettings)
-        => _runtimeMinificatinoSettings = runtimeMinificatinoSettings;
+        => _runtimeMinificationSettings = runtimeMinificatinoSettings;
 
     /// <summary>
-    ///     Configures Smidge to use in-memory caching if configured that way or if certain cache busters are used
+    /// Configures Smidge to use in-memory caching if configured that way or if certain cache busters are used.
+    /// Also sets the cache buster type such that public facing bundles will use the configured method.
     /// </summary>
-    /// <param name="options"></param>
+    /// <param name="options">Instance of <see cref="SmidgeOptions"></see> to configure.</param>
     public void Configure(SmidgeOptions options)
-        => options.CacheOptions.UseInMemoryCache = _runtimeMinificatinoSettings.Value.UseInMemoryCache ||
-                                                   _runtimeMinificatinoSettings.Value.CacheBuster ==
+    {
+        options.CacheOptions.UseInMemoryCache = _runtimeMinificationSettings.Value.UseInMemoryCache ||
+                                                   _runtimeMinificationSettings.Value.CacheBuster ==
                                                    RuntimeMinificationCacheBuster.Timestamp;
+
+        Type cacheBusterType = _runtimeMinificationSettings.Value.CacheBuster switch
+        {
+            RuntimeMinificationCacheBuster.AppDomain => typeof(AppDomainLifetimeCacheBuster),
+            RuntimeMinificationCacheBuster.Version => typeof(UmbracoSmidgeConfigCacheBuster),
+            RuntimeMinificationCacheBuster.Timestamp => typeof(TimestampCacheBuster),
+            _ => throw new ArgumentOutOfRangeException("CacheBuster", $"{_runtimeMinificationSettings.Value.CacheBuster} is not a valid value for RuntimeMinificationCacheBuster."),
+        };
+
+        options.DefaultBundleOptions.DebugOptions.SetCacheBusterType(cacheBusterType);
+        options.DefaultBundleOptions.ProductionOptions.SetCacheBusterType(cacheBusterType);
+    }
 }
diff --git a/src/Umbraco.Web.Common/RuntimeMinification/SmidgeRuntimeMinifier.cs b/src/Umbraco.Web.Common/RuntimeMinification/SmidgeRuntimeMinifier.cs
index b06f8d06880a..cd0b80d1dd14 100644
--- a/src/Umbraco.Web.Common/RuntimeMinification/SmidgeRuntimeMinifier.cs
+++ b/src/Umbraco.Web.Common/RuntimeMinification/SmidgeRuntimeMinifier.cs
@@ -79,7 +79,7 @@ public SmidgeRuntimeMinifier(
             RuntimeMinificationCacheBuster.AppDomain => typeof(AppDomainLifetimeCacheBuster),
             RuntimeMinificationCacheBuster.Version => typeof(UmbracoSmidgeConfigCacheBuster),
             RuntimeMinificationCacheBuster.Timestamp => typeof(TimestampCacheBuster),
-            _ => throw new NotImplementedException(),
+            _ => throw new ArgumentOutOfRangeException("CacheBuster", $"{runtimeMinificationSettings.Value.CacheBuster} is not a valid value for RuntimeMinificationCacheBuster."),
         };
 
         _cacheBusterType = cacheBusterType;
diff --git a/src/Umbraco.Web.Common/Security/ConfigureMemberIdentityOptions.cs b/src/Umbraco.Web.Common/Security/ConfigureMemberIdentityOptions.cs
index 0fcc41d9d02c..1c9b88b6cfdc 100644
--- a/src/Umbraco.Web.Common/Security/ConfigureMemberIdentityOptions.cs
+++ b/src/Umbraco.Web.Common/Security/ConfigureMemberIdentityOptions.cs
@@ -24,7 +24,7 @@ public void Configure(IdentityOptions options)
         options.SignIn.RequireConfirmedEmail = false; // not implemented
         options.SignIn.RequireConfirmedPhoneNumber = false; // not implemented
 
-        options.User.RequireUniqueEmail = true;
+        options.User.RequireUniqueEmail = _securitySettings.MemberRequireUniqueEmail;
 
         // Support validation of member names using Down-Level Logon Name format
         options.User.AllowedUserNameCharacters = _securitySettings.AllowedUserNameCharacters;
diff --git a/src/Umbraco.Web.Common/Security/MemberManager.cs b/src/Umbraco.Web.Common/Security/MemberManager.cs
index 46d07deb88f5..40146275dec1 100644
--- a/src/Umbraco.Web.Common/Security/MemberManager.cs
+++ b/src/Umbraco.Web.Common/Security/MemberManager.cs
@@ -1,4 +1,5 @@
 using System.Globalization;
+using System.Security.Claims;
 using Microsoft.AspNetCore.Http;
 using Microsoft.AspNetCore.Identity;
 using Microsoft.Extensions.Logging;
@@ -124,8 +125,11 @@ public virtual async Task<bool> IsMemberAuthorizedAsync(
     /// <inheritdoc />
     public virtual bool IsLoggedIn()
     {
-        HttpContext? httpContext = _httpContextAccessor.HttpContext;
-        return httpContext?.User.Identity?.IsAuthenticated ?? false;
+        // We have to try and specifically find the member identity, it's entirely possible for there to be both backoffice and member.
+        ClaimsIdentity? memberIdentity = _httpContextAccessor.HttpContext?.User.GetMemberIdentity();
+
+        return memberIdentity is not null &&
+               memberIdentity.IsAuthenticated;
     }
 
     /// <inheritdoc />
@@ -181,23 +185,27 @@ public virtual Task<IReadOnlyDictionary<string, bool>> IsProtectedAsync(IEnumera
     /// <inheritdoc />
     public virtual async Task<MemberIdentityUser?> GetCurrentMemberAsync()
     {
-        if (_currentMember == null)
+        if (_currentMember is not null)
         {
-            if (!IsLoggedIn())
-            {
-                return null;
-            }
+            return _currentMember;
+        }
 
-            _currentMember = await GetUserAsync(_httpContextAccessor.HttpContext?.User!);
+        if (IsLoggedIn() is false)
+        {
+            return null;
         }
 
+        // Create a principal the represents the member security context.
+        var memberPrincipal = new ClaimsPrincipal(_httpContextAccessor.HttpContext?.User.GetMemberIdentity()!);
+        _currentMember = await GetUserAsync(memberPrincipal);
+
         return _currentMember;
     }
 
     public virtual IPublishedContent? AsPublishedMember(MemberIdentityUser user) => _store.GetPublishedMember(user);
 
     /// <summary>
-    ///     This will check if the member has access to this path
+    /// This will check if the member has access to this path.
     /// </summary>
     /// <param name="path"></param>
     /// <returns></returns>
diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/buttons/umbbuttongroup.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/buttons/umbbuttongroup.directive.js
index 8c7836a2e69e..2a526ae2af4d 100644
--- a/src/Umbraco.Web.UI.Client/src/common/directives/components/buttons/umbbuttongroup.directive.js
+++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/buttons/umbbuttongroup.directive.js
@@ -90,9 +90,7 @@ Use this directive to render a button with a dropdown of alternative actions.
 **/
 (function () {
   'use strict';
-
   function ButtonGroupDirective() {
-
     function controller($scope, localizationService) {
       $scope.toggleStyle = null;
       $scope.blockElement = false;
@@ -125,18 +123,24 @@ Use this directive to render a button with a dropdown of alternative actions.
       // As the <localize /> directive doesn't support Angular expressions as fallback, we instead listen for changes
       // to the label key of the default button, and if detected, we update the button label with the localized value
       // received from the localization service
-      $scope.$watch("defaultButton.labelKey", function () {
-        if (!$scope.defaultButton.labelKey) return;
+      $scope.$watch("defaultButton", localizeDefaultButtonLabel);
+      $scope.$watch("defaultButton.labelKey", localizeDefaultButtonLabel);
+
+      function localizeDefaultButtonLabel() {
+        if (!$scope.defaultButton?.labelKey) return;
         localizationService.localize($scope.defaultButton.labelKey).then(value => {
           if (value && value.indexOf("[") === 0) return;
           $scope.defaultButton.label = value;
         });
-      });
+      }
 
       // In a similar way, we must listen for changes to the sub buttons (or their label keys), and if detected, update
       // the label with the localized value received from the localization service
-      $scope.$watch("defaultButton.subButtons", function () {
-        if (!Array.isArray($scope.subButtons)) return;
+      $scope.$watch("subButtons", localizeSubButtons, true);
+      $scope.$watch("defaultButton.subButtons", localizeSubButtons, true);
+
+      function localizeSubButtons() {
+        if (!$scope.subButtons || !Array.isArray($scope.subButtons)) return;
         $scope.subButtons.forEach(function (sub) {
           if (!sub.labelKey) return;
           localizationService.localize(sub.labelKey).then(value => {
@@ -144,7 +148,7 @@ Use this directive to render a button with a dropdown of alternative actions.
             sub.label = value;
           });
         });
-      }, true);
+      }
 
     }
 
diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/content/umbtabbedcontent.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/content/umbtabbedcontent.directive.js
index e76da32a545b..fe16d1cf9d2f 100644
--- a/src/Umbraco.Web.UI.Client/src/common/directives/components/content/umbtabbedcontent.directive.js
+++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/content/umbtabbedcontent.directive.js
@@ -218,6 +218,10 @@
 
                 return !canEditCulture || !canEditSegment;
             }
+
+            $scope.isPreview = function(property) {
+              return ((property.readonly || !$scope.allowUpdate) && !property.supportsReadOnly) || ($scope.propertyEditorDisabled(property) && $scope.allowUpdate);
+            }
         }
 
         var directive = {
diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/property/umbproperty.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/property/umbproperty.directive.js
index 11efb4b81150..073df54a7ed9 100644
--- a/src/Umbraco.Web.UI.Client/src/common/directives/components/property/umbproperty.directive.js
+++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/property/umbproperty.directive.js
@@ -25,11 +25,12 @@
                 propertyAlias: "@",
                 showInherit: "<",
                 inheritsFrom: "<",
-                hideLabel: "<?"
+                hideLabel: "<?",
+                preview: "<?"
             }
         });
 
-    
+
 
     function UmbPropertyController($scope, userService, serverValidationManager, udiService, angularHelper) {
 
@@ -55,7 +56,7 @@
         // returns the validation path for the property to be used as the validation key for server side validation logic
         vm.getValidationPath = function () {
 
-            var parentValidationPath = vm.parentUmbProperty ? vm.parentUmbProperty.getValidationPath() : null;            
+            var parentValidationPath = vm.parentUmbProperty ? vm.parentUmbProperty.getValidationPath() : null;
             var propAlias = vm.propertyAlias ? vm.propertyAlias : vm.property.alias;
             // the elementKey will be empty when this is not a nested property
             var valPath = vm.elementKey ? vm.elementKey + "/" + propAlias : propAlias;
diff --git a/src/Umbraco.Web.UI.Client/src/common/resources/publicaccess.resource.js b/src/Umbraco.Web.UI.Client/src/common/resources/publicaccess.resource.js
index d91924a2eba3..3e0ac9062014 100644
--- a/src/Umbraco.Web.UI.Client/src/common/resources/publicaccess.resource.js
+++ b/src/Umbraco.Web.UI.Client/src/common/resources/publicaccess.resource.js
@@ -79,7 +79,7 @@ function publicAccessResource($http, umbRequestHelper) {
                 publicAccess.groups = groups;
             }
             else if (Utilities.isArray(usernames) && usernames.length) {
-                publicAccess.usernames = usernames;
+                publicAccess.usernames = usernames.map(u => encodeURIComponent(u));
             }
             else {
                 throw "must supply either userName/password or roles";
diff --git a/src/Umbraco.Web.UI.Client/src/common/resources/users.resource.js b/src/Umbraco.Web.UI.Client/src/common/resources/users.resource.js
index 0b69bec3f5e0..c514be49a03f 100644
--- a/src/Umbraco.Web.UI.Client/src/common/resources/users.resource.js
+++ b/src/Umbraco.Web.UI.Client/src/common/resources/users.resource.js
@@ -318,12 +318,14 @@
           */
         function getUsers(userIds) {
 
+            var idQuery = "";
+            userIds.forEach(id => idQuery += `ids=${id}&`);
             return umbRequestHelper.resourcePromise(
                 $http.get(
                     umbRequestHelper.getApiUrl(
                         "userApiBaseUrl",
                         "GetByIds",
-                        { ids: userIds })),
+                        idQuery)),
                 "Failed to retrieve data for users " + userIds);
         }
 
diff --git a/src/Umbraco.Web.UI.Client/src/views/common/drawers/help/help.controller.js b/src/Umbraco.Web.UI.Client/src/views/common/drawers/help/help.controller.js
index f69467b0a1d7..ede50dc93e86 100644
--- a/src/Umbraco.Web.UI.Client/src/views/common/drawers/help/help.controller.js
+++ b/src/Umbraco.Web.UI.Client/src/views/common/drawers/help/help.controller.js
@@ -67,7 +67,7 @@
               if(browserInfo != null){
                 vm.systemInfo.push({name :"Browser", data: browserInfo.name + " " + browserInfo.version});
               }
-              vm.systemInfo.push({name :"Browser OS", data: getPlatform()});
+              vm.systemInfo.push({name :"Browser (user agent)", data: getPlatform()});
             } );
             tourService.getGroupedTours().then(function(groupedTours) {
                 vm.tours = groupedTours;
@@ -257,7 +257,7 @@
         }
 
         function getPlatform() {
-          return window.navigator.platform;
+          return navigator.userAgent;
         }
 
         evts.push(eventsService.on("appState.tour.complete", function (event, tour) {
diff --git a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/linkpicker/linkpicker.controller.js b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/linkpicker/linkpicker.controller.js
index 673e1a5d3dd0..e3799def3c72 100644
--- a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/linkpicker/linkpicker.controller.js
+++ b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/linkpicker/linkpicker.controller.js
@@ -189,6 +189,7 @@ angular.module("umbraco").controller("Umbraco.Editors.LinkPickerController",
           startNodeId: startNodeId,
           startNodeIsVirtual: startNodeIsVirtual,
           dataTypeKey: dialogOptions.dataTypeKey,
+          disableFolderSelect: true,
           submit: function (model) {
             var media = model.selection[0];
 
diff --git a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/mediaentryeditor/mediaentryeditor.controller.js b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/mediaentryeditor/mediaentryeditor.controller.js
index 05be10c5d010..aed213383980 100644
--- a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/mediaentryeditor/mediaentryeditor.controller.js
+++ b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/mediaentryeditor/mediaentryeditor.controller.js
@@ -3,15 +3,15 @@ angular.module("umbraco")
         function ($scope, localizationService, entityResource, editorService, overlayService, eventsService, mediaHelper) {
 
             var unsubscribe = [];
-            
+
             const vm = this;
-            
+
             vm.loading = true;
             vm.model = $scope.model;
             vm.mediaEntry = vm.model.mediaEntry;
             vm.currentCrop = null;
             vm.title = "";
-            
+
             vm.focalPointChanged = focalPointChanged;
             vm.onImageLoaded = onImageLoaded;
             vm.openMedia = openMedia;
@@ -20,7 +20,7 @@ angular.module("umbraco")
             vm.deselectCrop = deselectCrop;
             vm.resetCrop = resetCrop;
             vm.submitAndClose = submitAndClose;
-            vm.close = close;   
+            vm.close = close;
 
             function init() {
 
@@ -58,6 +58,12 @@ angular.module("umbraco")
                     return;
                 }
 
+                // the focal point can be null in some cases - most often right after a save. this throws the crop
+                // thumbnails (previews) off, so let's enforce the default focal point.
+                if (!vm.mediaEntry.focalPoint){
+                  vm.mediaEntry.focalPoint = {left: 0.5, top: 0.5};
+                }
+
                 vm.loading = true;
 
                 entityResource.getById(vm.mediaEntry.mediaKey, "Media").then(function (mediaEntity) {
@@ -85,12 +91,12 @@ angular.module("umbraco")
                     });
                 });
             }
-            
+
             function onImageLoaded(isCroppable, hasDimensions) {
                 vm.isCroppable = isCroppable;
                 vm.hasDimensions = hasDimensions;
             }
-            
+
             function repickMedia() {
                 vm.model.propertyEditor.changeMediaFor(vm.model.mediaEntry, onMediaReplaced);
             }
@@ -105,7 +111,7 @@ angular.module("umbraco")
 
                 updateMedia();
             }
-            
+
             function openMedia() {
 
                 const mediaEditor = {
@@ -117,7 +123,7 @@ angular.module("umbraco")
                         editorService.close();
                     }
                 };
-                
+
                 editorService.mediaEditor(mediaEditor);
             }
 
@@ -131,23 +137,24 @@ angular.module("umbraco")
                 // set form to dirty to track changes
                 setDirty();
             }
-            
+
             function selectCrop(targetCrop) {
                 vm.currentCrop = targetCrop;
                 setDirty();
                 // TODO: start watchin values of crop, first when changed set to dirty.
             }
-            
+
             function deselectCrop() {
                 vm.currentCrop = null;
             }
-            
+
             function resetCrop() {
                 if (vm.currentCrop) {
-                    $scope.$evalAsync( () => {
-                        vm.model.propertyEditor.resetCrop(vm.currentCrop);
-                        vm.forceUpdateCrop = Math.random();
-                    });
+                  vm.model.propertyEditor.resetCrop(vm.currentCrop);
+                  // deselecting the crop here has a dual purpose:
+                  // 1. it replicates the behaviour of the image cropper (e.g. on media items).
+                  // 2. it ensures that the newly reset crop does not get overwritten by a new crop with default values.
+                  deselectCrop();
                 }
             }
 
@@ -160,7 +167,7 @@ angular.module("umbraco")
                     vm.model.submit(vm.model);
                 }
             }
-            
+
             function close() {
                 if (vm.model && vm.model.close)
                 {
@@ -169,7 +176,7 @@ angular.module("umbraco")
                       const labelKeys = vm.model.createFlow === true
                           ? ["mediaPicker_confirmCancelMediaEntryCreationHeadline", "mediaPicker_confirmCancelMediaEntryCreationMessage"]
                           : ["prompt_discardChanges", "mediaPicker_confirmCancelMediaEntryHasChanges"];
-                        
+
                         localizationService.localizeMany(labelKeys).then(localizations => {
                             const confirm = {
                                 title: localizations[0],
@@ -196,7 +203,7 @@ angular.module("umbraco")
             }
 
             init();
-            
+
             $scope.$on("$destroy", function () {
                 unsubscribe.forEach(x => x());
             });
diff --git a/src/Umbraco.Web.UI.Client/src/views/components/content/umb-tabbed-content.html b/src/Umbraco.Web.UI.Client/src/views/components/content/umb-tabbed-content.html
index 2dd0b83130aa..ccb2a5e0af3a 100644
--- a/src/Umbraco.Web.UI.Client/src/views/components/content/umb-tabbed-content.html
+++ b/src/Umbraco.Web.UI.Client/src/views/components/content/umb-tabbed-content.html
@@ -15,12 +15,13 @@
                     property="property"
                     node="contentNodeModel"
                     show-inherit="contentNodeModel.variants.length > 1 && property.variation !== 'CultureAndSegment'"
-                    inherits-from="defaultVariant.displayName">
+                    inherits-from="defaultVariant.displayName"
+                    preview="isPreview(property)">
 
                     <umb-property-editor
                         model="property"
                         node="contentNodeModel"
-                        preview="((property.readonly || !allowUpdate) && !property.supportsReadOnly) || (propertyEditorDisabled(property) && allowUpdate)"
+                        preview="isPreview(property)"
                         allow-unlock="!property.readonly && allowUpdate && allowEditInvariantFromNonDefault"
                         on-unlock="unlockInvariantValue(property)"
                         ng-attr-readonly="{{property.readonly || !allowUpdate || undefined}}">
@@ -49,12 +50,13 @@
                     property="property"
                     node="contentNodeModel"
                     show-inherit="contentNodeModel.variants.length > 1 && property.variation !== 'CultureAndSegment'"
-                    inherits-from="defaultVariant.displayName">
+                    inherits-from="defaultVariant.displayName"
+                    preview="isPreview(property)">
 
                     <umb-property-editor
                         model="property"
                         node="contentNodeModel"
-                        preview="((property.readonly || !allowUpdate) && !property.supportsReadOnly) || (propertyEditorDisabled(property) && allowUpdate)"
+                        preview="isPreview(property)"
                         allow-unlock="!property.readonly && allowUpdate && allowEditInvariantFromNonDefault"
                         on-unlock="unlockInvariantValue(property)"
                         ng-attr-readonly="{{property.readonly || !allowUpdate || undefined}}">
diff --git a/src/Umbraco.Web.UI.Client/src/views/components/property/umb-property.html b/src/Umbraco.Web.UI.Client/src/views/components/property/umb-property.html
index e3a65a215bdf..e4166e8e98d0 100644
--- a/src/Umbraco.Web.UI.Client/src/views/components/property/umb-property.html
+++ b/src/Umbraco.Web.UI.Client/src/views/components/property/umb-property.html
@@ -9,7 +9,7 @@
 
                 <div class="control-header" ng-hide="(vm.hideLabel || vm.property.hideLabel) === true">
 
-                    <label data-element="property-label-{{vm.property.alias}}" class="control-label" for="{{vm.property.alias}}" ng-attr-title="{{vm.controlLabelTitle}}" aria-label="Property alias: {{vm.controlAriaLabel}}">{{vm.property.label}}<span ng-if="vm.property.validation.mandatory || vm.property.ncMandatory"><strong class="umb-control-required">*</strong></span></label>
+                    <label data-element="property-label-{{vm.property.alias}}" class="control-label" for="{{vm.preview ? undefined : vm.property.alias}}" ng-attr-title="{{vm.controlLabelTitle}}" aria-label="Property alias: {{vm.controlAriaLabel}}">{{vm.property.label}}<span ng-if="vm.property.validation.mandatory || vm.property.ncMandatory"><strong class="umb-control-required">*</strong></span></label>
 
                     <umb-property-actions actions="vm.propertyActions"></umb-property-actions>
 
diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/listview/listview.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/listview/listview.controller.js
index 0edcd67c1924..c9fc26e0535e 100644
--- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/listview/listview.controller.js
+++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/listview/listview.controller.js
@@ -781,7 +781,7 @@ function listViewController($scope, $interpolate, $routeParams, $injector, $time
       $scope.options.allowBulkDelete;
 
     if ($scope.isTrashed === false) {
-      getContentTypesCallback(id).then(function (listViewAllowedTypes) {
+      getContentTypesCallback($scope.contentId).then(function (listViewAllowedTypes) {
         $scope.listViewAllowedTypes = listViewAllowedTypes;
 
         var blueprints = false;
diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/rte/rte.component.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/rte/rte.component.js
index 2794f0bd16fa..bb9006779dc9 100644
--- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/rte/rte.component.js
+++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/rte/rte.component.js
@@ -83,6 +83,7 @@
           }
       }));
 
+
       vm.layout = []; // The layout object specific to this Block Editor, will be a direct reference from Property Model.
       vm.availableBlockTypes = []; // Available block entries of this property editor.
       vm.labels = {};
@@ -190,6 +191,9 @@
           vm.containerHeight = "auto";
           vm.containerOverflow = "inherit"
 
+          // Add client validation for the markup part.
+          unsubscribe.push($scope.$watch(() => vm.model?.value?.markup, validate));
+
           //queue file loading
           tinyMceAssets.forEach(function (tinyJsAsset) {
               assetPromises.push(assetsService.loadJs(tinyJsAsset, $scope));
@@ -337,6 +341,18 @@
         }
       }
 
+      function validate() {
+        var isValid = !vm.model.validation.mandatory || (
+            vm.model.value != null
+            && vm.model.value.markup != null
+            && vm.model.value.markup != ""
+        );
+        vm.propertyForm.$setValidity("required", isValid);
+        if (vm.umbProperty) {
+          vm.umbProperty.setPropertyError(vm.model.validation.mandatoryMessage || "Value cannot be empty");
+        }
+    };
+
       // Called when we save the value, the server may return an updated data and our value is re-synced
       // we need to deal with that here so that our model values are all in sync so we basically re-initialize.
       function onServerValueChanged(newVal, oldVal) {
diff --git a/src/Umbraco.Web.UI.Client/src/views/users/views/user/details.html b/src/Umbraco.Web.UI.Client/src/views/users/views/user/details.html
index eaa92b7a6e78..1eb6840fd3b5 100644
--- a/src/Umbraco.Web.UI.Client/src/views/users/views/user/details.html
+++ b/src/Umbraco.Web.UI.Client/src/views/users/views/user/details.html
@@ -1,4 +1,4 @@
-<div ng-controller="Umbraco.Editors.Users.DetailsController as vm" class="umb-user-details-details">
+<div ng-controller="Umbraco.Editors.Users.DetailsController as vm" class="umb-user-details-details">
 
     <div class="umb-user-details-details__main-content">
 
@@ -45,6 +45,8 @@
                                ng-model="model.user.username"
                                umb-auto-focus name="username"
                                required
+                               autocomplete="off"
+                               no-password-manager
                                val-server-field="Username" />
                         <span ng-messages="userProfileForm.username.$error" show-validation-on-submit>
                             <span class="help-inline" ng-message="required"><localize key="general_required">Required</localize></span>
diff --git a/src/Umbraco.Web.UI.Client/src/views/webhooks/edit.controller.js b/src/Umbraco.Web.UI.Client/src/views/webhooks/edit.controller.js
index 4a12d5254d04..ce5de98d4c2b 100644
--- a/src/Umbraco.Web.UI.Client/src/views/webhooks/edit.controller.js
+++ b/src/Umbraco.Web.UI.Client/src/views/webhooks/edit.controller.js
@@ -247,6 +247,7 @@
         function save() {
 
             if (!formHelper.submitForm({ scope: $scope })) {
+              vm.saveButtonState = 'error';
               return;
             }
 
diff --git a/src/Umbraco.Web.UI.Client/src/views/webhooks/edit.html b/src/Umbraco.Web.UI.Client/src/views/webhooks/edit.html
index f64ef5d69bce..5a4678c5eb92 100644
--- a/src/Umbraco.Web.UI.Client/src/views/webhooks/edit.html
+++ b/src/Umbraco.Web.UI.Client/src/views/webhooks/edit.html
@@ -5,7 +5,7 @@
     <form name="editWebhookForm" novalidate val-form-manager>
 
         <umb-editor-view ng-if="!vm.loading">
-    
+
             <umb-editor-header
                 name="vm.webhook.name"
                 name-locked="true"
@@ -15,7 +15,7 @@
                 hide-alias="true"
                 hide-description="true">
             </umb-editor-header>
-    
+
             <umb-editor-container class="form-horizontal">
 
                 <div class="umb-package-details">
@@ -50,6 +50,8 @@
                               alias="webhookEvents"
                               required="true">
 
+                            <input type="hidden" name="eventsValidator" ng-model="eventsValidator" ng-required="!vm.webhook.events.length" />
+
                             <umb-node-preview
                                 ng-repeat="event in vm.webhook.events"
                                 ng-show="event"
@@ -198,9 +200,9 @@
                 </umb-editor-footer-content-right>
 
             </umb-editor-footer>
-    
+
         </umb-editor-view>
 
     </form>
-    
+
 </div>
diff --git a/src/Umbraco.Web.UI.Login/src/auth.element.ts b/src/Umbraco.Web.UI.Login/src/auth.element.ts
index 3f5bfd1428d9..e195f3986a21 100644
--- a/src/Umbraco.Web.UI.Login/src/auth.element.ts
+++ b/src/Umbraco.Web.UI.Login/src/auth.element.ts
@@ -179,7 +179,7 @@ export default class UmbAuthElement extends LitElement {
     });
     this._usernameLabel = createLabel({
       forId: 'username-input',
-      localizeAlias: this.usernameIsEmail ? 'general_email' : 'auth_username',
+      localizeAlias: this.usernameIsEmail ? 'general_email' : 'general_username',
       localizeFallback: this.usernameIsEmail ? 'Email' : 'Username',
     });
     this._passwordLabel = createLabel({forId: 'password-input', localizeAlias: 'general_password', localizeFallback: 'Password'});
diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/TagRepositoryTest.cs b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/TagRepositoryTest.cs
index 1ce5eeefd99b..297bd5069900 100644
--- a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/TagRepositoryTest.cs
+++ b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/TagRepositoryTest.cs
@@ -638,6 +638,89 @@ public void Can_Get_Tags_For_Property_For_Group()
         }
     }
 
+    [Test]
+    public void Can_Get_Tags_For_Entity_Type_Excluding_Trashed_Entity()
+    {
+        var provider = ScopeProvider;
+        using (ScopeProvider.CreateScope())
+        {
+            var template = TemplateBuilder.CreateTextPageTemplate();
+            FileService.SaveTemplate(template);
+
+            var contentType = ContentTypeBuilder.CreateSimpleContentType("test", "Test", defaultTemplateId: template.Id);
+            ContentTypeRepository.Save(contentType);
+
+            var content1 = ContentBuilder.CreateSimpleContent(contentType);
+            content1.PublishCulture(CultureImpact.Invariant);
+            content1.PublishedState = PublishedState.Publishing;
+            DocumentRepository.Save(content1);
+
+            var content2 = ContentBuilder.CreateSimpleContent(contentType);
+            content2.PublishCulture(CultureImpact.Invariant);
+            content2.PublishedState = PublishedState.Publishing;
+            content2.Trashed = true;
+            DocumentRepository.Save(content2);
+
+            var mediaType = MediaTypeBuilder.CreateImageMediaType("image2");
+            MediaTypeRepository.Save(mediaType);
+
+            var media1 = MediaBuilder.CreateMediaImage(mediaType, -1);
+            MediaRepository.Save(media1);
+
+            var media2 = MediaBuilder.CreateMediaImage(mediaType, -1);
+            media2.Trashed = true;
+            MediaRepository.Save(media2);
+
+            var repository = CreateRepository(provider);
+            Tag[] tags =
+            {
+                new Tag {Text = "tag1", Group = "test"},
+                new Tag {Text = "tag2", Group = "test1"},
+                new Tag {Text = "tag3", Group = "test"}
+            };
+
+            Tag[] tags2 =
+{
+                new Tag {Text = "tag4", Group = "test"},
+                new Tag {Text = "tag5", Group = "test1"},
+                new Tag {Text = "tag6", Group = "test"}
+            };
+
+            repository.Assign(
+                content1.Id,
+                contentType.PropertyTypes.First().Id,
+                tags,
+                false);
+
+            repository.Assign(
+                content2.Id,
+                contentType.PropertyTypes.First().Id,
+                tags2,
+                false);
+
+            repository.Assign(
+                media1.Id,
+                contentType.PropertyTypes.First().Id,
+                tags,
+                false);
+
+            repository.Assign(
+                media2.Id,
+                contentType.PropertyTypes.First().Id,
+                tags2,
+                false);
+
+            var result1 = repository.GetTagsForEntityType(TaggableObjectTypes.Content).ToArray();
+            var result2 = repository.GetTagsForEntityType(TaggableObjectTypes.Media).ToArray();
+            var result3 = repository.GetTagsForEntityType(TaggableObjectTypes.All).ToArray();
+
+            const string ExpectedTags = "tag1,tag2,tag3";
+            Assert.AreEqual(ExpectedTags, string.Join(",", result1.Select(x => x.Text)));
+            Assert.AreEqual(ExpectedTags, string.Join(",", result2.Select(x => x.Text)));
+            Assert.AreEqual(ExpectedTags, string.Join(",", result3.Select(x => x.Text)));
+        }
+    }
+
     [Test]
     public void Can_Get_Tags_For_Entity_Type()
     {
diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Web.BackOffice/Controllers/MemberControllerUnitTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Web.BackOffice/Controllers/MemberControllerUnitTests.cs
index 6a9559f14c83..2cfcf526ed8a 100644
--- a/tests/Umbraco.Tests.UnitTests/Umbraco.Web.BackOffice/Controllers/MemberControllerUnitTests.cs
+++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Web.BackOffice/Controllers/MemberControllerUnitTests.cs
@@ -70,11 +70,13 @@ public void PostSaveMember_WhenModelStateIsNotValid_ExpectFailureResponse(
         IBackOfficeSecurityAccessor backOfficeSecurityAccessor,
         IPasswordChanger<MemberIdentityUser> passwordChanger,
         IOptions<GlobalSettings> globalSettings,
-        IUser user,
         ITwoFactorLoginService twoFactorLoginService)
     {
         // arrange
         SetupMemberTestData(out var fakeMemberData, out _, ContentSaveAction.SaveNew);
+
+        var securitySettings = Options.Create(new SecuritySettings());
+
         var sut = CreateSut(
             memberService,
             memberTypeService,
@@ -84,7 +86,8 @@ public void PostSaveMember_WhenModelStateIsNotValid_ExpectFailureResponse(
             backOfficeSecurityAccessor,
             passwordChanger,
             globalSettings,
-            twoFactorLoginService);
+            twoFactorLoginService,
+            securitySettings);
         sut.ModelState.AddModelError("key", "Invalid model state");
 
         Mock.Get(umbracoMembersUserManager)
@@ -116,7 +119,6 @@ public async Task PostSaveMember_SaveNew_NoCustomField_WhenAllIsSetupCorrectly_E
         IBackOfficeSecurity backOfficeSecurity,
         IPasswordChanger<MemberIdentityUser> passwordChanger,
         IOptions<GlobalSettings> globalSettings,
-        IUser user,
         ITwoFactorLoginService twoFactorLoginService)
     {
         // arrange
@@ -138,6 +140,8 @@ public async Task PostSaveMember_SaveNew_NoCustomField_WhenAllIsSetupCorrectly_E
             .Returns(() => member);
         Mock.Get(memberService).Setup(x => x.GetByUsername(It.IsAny<string>())).Returns(() => member);
 
+        var securitySettings = Options.Create(new SecuritySettings());
+
         var sut = CreateSut(
             memberService,
             memberTypeService,
@@ -147,7 +151,8 @@ public async Task PostSaveMember_SaveNew_NoCustomField_WhenAllIsSetupCorrectly_E
             backOfficeSecurityAccessor,
             passwordChanger,
             globalSettings,
-            twoFactorLoginService);
+            twoFactorLoginService,
+            securitySettings);
 
         // act
         var result = await sut.PostSave(fakeMemberData);
@@ -170,7 +175,6 @@ public async Task PostSaveMember_SaveNew_CustomField_WhenAllIsSetupCorrectly_Exp
         IBackOfficeSecurity backOfficeSecurity,
         IPasswordChanger<MemberIdentityUser> passwordChanger,
         IOptions<GlobalSettings> globalSettings,
-        IUser user,
         ITwoFactorLoginService twoFactorLoginService)
     {
         // arrange
@@ -192,6 +196,8 @@ public async Task PostSaveMember_SaveNew_CustomField_WhenAllIsSetupCorrectly_Exp
             .Returns(() => member);
         Mock.Get(memberService).Setup(x => x.GetByUsername(It.IsAny<string>())).Returns(() => member);
 
+        var securitySettings = Options.Create(new SecuritySettings());
+
         var sut = CreateSut(
             memberService,
             memberTypeService,
@@ -201,7 +207,8 @@ public async Task PostSaveMember_SaveNew_CustomField_WhenAllIsSetupCorrectly_Exp
             backOfficeSecurityAccessor,
             passwordChanger,
             globalSettings,
-            twoFactorLoginService);
+            twoFactorLoginService,
+            securitySettings);
 
         // act
         var result = await sut.PostSave(fakeMemberData);
@@ -256,6 +263,8 @@ public async Task PostSaveMember_SaveExisting_WhenAllIsSetupCorrectly_ExpectSucc
             .Returns(() => null)
             .Returns(() => member);
 
+        var securitySettings = Options.Create(new SecuritySettings());
+
         var sut = CreateSut(
             memberService,
             memberTypeService,
@@ -265,7 +274,8 @@ public async Task PostSaveMember_SaveExisting_WhenAllIsSetupCorrectly_ExpectSucc
             backOfficeSecurityAccessor,
             passwordChanger,
             globalSettings,
-            twoFactorLoginService);
+            twoFactorLoginService,
+            securitySettings);
 
         // act
         var result = await sut.PostSave(fakeMemberData);
@@ -316,6 +326,8 @@ public async Task PostSaveMember_SaveExisting_WhenAllIsSetupWithPasswordIncorrec
             .Returns(() => null)
             .Returns(() => member);
 
+        var securitySettings = Options.Create(new SecuritySettings());
+
         var sut = CreateSut(
             memberService,
             memberTypeService,
@@ -325,7 +337,8 @@ public async Task PostSaveMember_SaveExisting_WhenAllIsSetupWithPasswordIncorrec
             backOfficeSecurityAccessor,
             passwordChanger,
             globalSettings,
-            twoFactorLoginService);
+            twoFactorLoginService,
+            securitySettings);
 
         // act
         var result = await sut.PostSave(fakeMemberData);
@@ -382,7 +395,6 @@ public void PostSaveMember_SaveNew_WhenMemberEmailAlreadyExists_ExpectFailRespon
         IBackOfficeSecurity backOfficeSecurity,
         IPasswordChanger<MemberIdentityUser> passwordChanger,
         IOptions<GlobalSettings> globalSettings,
-        IUser user,
         ITwoFactorLoginService twoFactorLoginService)
     {
         // arrange
@@ -403,6 +415,8 @@ public void PostSaveMember_SaveNew_WhenMemberEmailAlreadyExists_ExpectFailRespon
                 x => x.GetByEmail(It.IsAny<string>()))
             .Returns(() => member);
 
+        var securitySettings = Options.Create(new SecuritySettings());
+
         var sut = CreateSut(
             memberService,
             memberTypeService,
@@ -412,7 +426,8 @@ public void PostSaveMember_SaveNew_WhenMemberEmailAlreadyExists_ExpectFailRespon
             backOfficeSecurityAccessor,
             passwordChanger,
             globalSettings,
-            twoFactorLoginService);
+            twoFactorLoginService,
+            securitySettings);
 
         // act
         var result = sut.PostSave(fakeMemberData).Result;
@@ -424,6 +439,66 @@ public void PostSaveMember_SaveNew_WhenMemberEmailAlreadyExists_ExpectFailRespon
         Assert.AreEqual(StatusCodes.Status400BadRequest, validation?.StatusCode);
     }
 
+    [Test]
+    [AutoMoqData]
+    public void PostSaveMember_SaveNew_WhenMemberEmailAlreadyExists_AndDuplicateEmailsAreAllowed_ExpectSuccessResponse(
+        [Frozen] IMemberManager umbracoMembersUserManager,
+        IMemberService memberService,
+        IMemberTypeService memberTypeService,
+        IMemberGroupService memberGroupService,
+        IDataTypeService dataTypeService,
+        IBackOfficeSecurityAccessor backOfficeSecurityAccessor,
+        IBackOfficeSecurity backOfficeSecurity,
+        IPasswordChanger<MemberIdentityUser> passwordChanger,
+        IOptions<GlobalSettings> globalSettings,
+        ITwoFactorLoginService twoFactorLoginService)
+    {
+        // arrange
+        var member = SetupMemberTestData(out var fakeMemberData, out var memberDisplay, ContentSaveAction.SaveNew);
+        Mock.Get(umbracoMembersUserManager)
+            .Setup(x => x.CreateAsync(It.IsAny<MemberIdentityUser>(), It.IsAny<string>()))
+            .ReturnsAsync(() => IdentityResult.Success);
+        Mock.Get(umbracoMembersUserManager)
+            .Setup(x => x.ValidatePasswordAsync(It.IsAny<string>()))
+            .ReturnsAsync(() => IdentityResult.Success);
+        Mock.Get(umbracoMembersUserManager)
+            .Setup(x => x.GetRolesAsync(It.IsAny<MemberIdentityUser>()))
+            .ReturnsAsync(() => Array.Empty<string>());
+        Mock.Get(memberTypeService).Setup(x => x.GetDefault()).Returns("fakeAlias");
+        Mock.Get(backOfficeSecurityAccessor).Setup(x => x.BackOfficeSecurity).Returns(backOfficeSecurity);
+        Mock.Get(memberService).SetupSequence(
+                x => x.GetByEmail(It.IsAny<string>()))
+            .Returns(() => null)
+            .Returns(() => member);
+        Mock.Get(memberService).Setup(x => x.GetByUsername(It.IsAny<string>())).Returns(() => member);
+
+        Mock.Get(memberService).SetupSequence(
+                x => x.GetByEmail(It.IsAny<string>()))
+            .Returns(() => member);
+
+        var securitySettings = Options.Create(new SecuritySettings { MemberRequireUniqueEmail = false });
+
+        var sut = CreateSut(
+            memberService,
+            memberTypeService,
+            memberGroupService,
+            umbracoMembersUserManager,
+            dataTypeService,
+            backOfficeSecurityAccessor,
+            passwordChanger,
+            globalSettings,
+            twoFactorLoginService,
+            securitySettings);
+
+        // act
+        var result = sut.PostSave(fakeMemberData).Result;
+        var validation = result.Result as ValidationErrorResult;
+
+        // assert
+        Assert.IsNull(result.Result);
+        Assert.IsNotNull(result.Value);
+    }
+
     [Test]
     [AutoMoqData]
     public async Task PostSaveMember_SaveExistingMember_WithNoRoles_Add1Role_ExpectSuccessResponse(
@@ -472,6 +547,9 @@ public async Task PostSaveMember_SaveExistingMember_WithNoRoles_Add1Role_ExpectS
                 x => x.GetByEmail(It.IsAny<string>()))
             .Returns(() => null)
             .Returns(() => member);
+
+        var securitySettings = Options.Create(new SecuritySettings());
+
         var sut = CreateSut(
             memberService,
             memberTypeService,
@@ -481,7 +559,8 @@ public async Task PostSaveMember_SaveExistingMember_WithNoRoles_Add1Role_ExpectS
             backOfficeSecurityAccessor,
             passwordChanger,
             globalSettings,
-            twoFactorLoginService);
+            twoFactorLoginService,
+            securitySettings);
 
         // act
         var result = await sut.PostSave(fakeMemberData);
@@ -512,6 +591,7 @@ public async Task PostSaveMember_SaveExistingMember_WithNoRoles_Add1Role_ExpectS
     /// <param name="passwordChanger">Password changer class</param>
     /// <param name="globalSettings">The global settings</param>
     /// <param name="twoFactorLoginService">The two factor login service</param>
+    /// <param name="securitySettings">The security settings</param>
     /// <returns>A member controller for the tests</returns>
     private MemberController CreateSut(
         IMemberService memberService,
@@ -522,7 +602,8 @@ private MemberController CreateSut(
         IBackOfficeSecurityAccessor backOfficeSecurityAccessor,
         IPasswordChanger<MemberIdentityUser> passwordChanger,
         IOptions<GlobalSettings> globalSettings,
-        ITwoFactorLoginService twoFactorLoginService)
+        ITwoFactorLoginService twoFactorLoginService,
+        IOptions<SecuritySettings> securitySettings)
     {
         var httpContextAccessor = new HttpContextAccessor();
 
@@ -623,7 +704,8 @@ private MemberController CreateSut(
             new ConfigurationEditorJsonSerializer(),
             passwordChanger,
             scopeProvider,
-            twoFactorLoginService);
+            twoFactorLoginService,
+            securitySettings);
     }
 
     /// <summary>
diff --git a/version.json b/version.json
index 0d1b19886972..c976fce1001a 100644
--- a/version.json
+++ b/version.json
@@ -1,6 +1,6 @@
 {
   "$schema": "https://raw.githubusercontent.com/dotnet/Nerdbank.GitVersioning/main/src/NerdBank.GitVersioning/version.schema.json",
-  "version": "13.6.0",
+  "version": "13.7.0",
   "assemblyVersion": {
     "precision": "build"
   },