diff --git a/src/Umbraco.Cms.Api.Common/Umbraco.Cms.Api.Common.csproj b/src/Umbraco.Cms.Api.Common/Umbraco.Cms.Api.Common.csproj
index 6ecc446e46..4c14cb084b 100644
--- a/src/Umbraco.Cms.Api.Common/Umbraco.Cms.Api.Common.csproj
+++ b/src/Umbraco.Cms.Api.Common/Umbraco.Cms.Api.Common.csproj
@@ -7,11 +7,14 @@
Umbraco.Cms.Api.Common
Umbraco.Cms.Api.Common
+
+
+
+
+
-
-
diff --git a/src/Umbraco.Cms.Api.Management/Umbraco.Cms.Api.Management.csproj b/src/Umbraco.Cms.Api.Management/Umbraco.Cms.Api.Management.csproj
index c6e8ca3a93..431f73cd32 100644
--- a/src/Umbraco.Cms.Api.Management/Umbraco.Cms.Api.Management.csproj
+++ b/src/Umbraco.Cms.Api.Management/Umbraco.Cms.Api.Management.csproj
@@ -10,7 +10,6 @@
-
diff --git a/src/Umbraco.Cms.Persistence.EFCore.Sqlite/Umbraco.Cms.Persistence.EFCore.Sqlite.csproj b/src/Umbraco.Cms.Persistence.EFCore.Sqlite/Umbraco.Cms.Persistence.EFCore.Sqlite.csproj
index d3274a5005..d1939fda24 100644
--- a/src/Umbraco.Cms.Persistence.EFCore.Sqlite/Umbraco.Cms.Persistence.EFCore.Sqlite.csproj
+++ b/src/Umbraco.Cms.Persistence.EFCore.Sqlite/Umbraco.Cms.Persistence.EFCore.Sqlite.csproj
@@ -6,7 +6,7 @@
-
+
diff --git a/src/Umbraco.Cms.Persistence.EFCore/Composition/UmbracoEFCoreComposer.cs b/src/Umbraco.Cms.Persistence.EFCore/Composition/UmbracoEFCoreComposer.cs
index 7d5bf0dd11..f0f2416b32 100644
--- a/src/Umbraco.Cms.Persistence.EFCore/Composition/UmbracoEFCoreComposer.cs
+++ b/src/Umbraco.Cms.Persistence.EFCore/Composition/UmbracoEFCoreComposer.cs
@@ -1,4 +1,14 @@
+using System.Collections.Concurrent;
+using System.Diagnostics;
+using System.Diagnostics.CodeAnalysis;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.DependencyInjection.Extensions;
+using Microsoft.Extensions.Options;
+using OpenIddict.Abstractions;
+using OpenIddict.EntityFrameworkCore;
+using OpenIddict.EntityFrameworkCore.Models;
using Umbraco.Cms.Core.Composing;
using Umbraco.Cms.Core.DependencyInjection;
using Umbraco.Cms.Core.Events;
@@ -23,7 +33,7 @@ public class UmbracoEFCoreComposer : IComposer
.AddCore(options =>
{
options
- .UseEntityFrameworkCore()
+ .UseCustomEntityFrameworkCore() // TODO Revert to this after .NET 8 Preview 7: .UseEntityFrameworkCore()
.UseDbContext();
});
}
@@ -54,3 +64,394 @@ public class EFCoreCreateTablesNotificationHandler : INotificationAsyncHandler : OpenIddictEntityFrameworkCoreAuthorizationStore, IOpenIddictAuthorizationStore
+ where TAuthorization : OpenIddictEntityFrameworkCoreAuthorization
+ where TApplication : OpenIddictEntityFrameworkCoreApplication
+ where TToken : OpenIddictEntityFrameworkCoreToken
+ where TContext : DbContext
+ where TKey : notnull, IEquatable
+{
+ public MyOpenIddictAuthorizationStore(
+ IMemoryCache cache,
+ TContext context,
+ IOptionsMonitor options)
+ : base(cache, context, options)
+ {
+ }
+
+ private DbSet Applications => Context.Set();
+
+ public override async ValueTask GetApplicationIdAsync(TAuthorization authorization, CancellationToken cancellationToken)
+ {
+ ArgumentNullException.ThrowIfNull(authorization);
+
+ // If the application is not attached to the authorization, try to load it manually.
+ if (authorization.Application is null)
+ {
+ var reference = Context.Entry(authorization).Reference(entry => entry.Application);
+ if (reference.EntityEntry.State is EntityState.Detached)
+ {
+ return null;
+ }
+
+ await reference.LoadAsync(cancellationToken: cancellationToken);
+ }
+
+ if (authorization.Application is null)
+ {
+ return null;
+ }
+
+ return ConvertIdentifierToString(authorization.Application.Id);
+ }
+
+ public override async ValueTask SetApplicationIdAsync(TAuthorization authorization, string? identifier, CancellationToken cancellationToken)
+ {
+ ArgumentNullException.ThrowIfNull(authorization);
+
+ if (!string.IsNullOrEmpty(identifier))
+ {
+ var key = ConvertIdentifierFromString(identifier);
+
+ authorization.Application = await Applications.AsQueryable()
+ .AsTracking()
+ .FirstOrDefaultAsync(application => application.Id!.Equals(key), cancellationToken) ??
+ throw new InvalidOperationException();
+ }
+
+ else
+ {
+ // If the application is not attached to the authorization, try to load it manually.
+ if (authorization.Application is null)
+ {
+ var reference = Context.Entry(authorization).Reference(entry => entry.Application);
+ if (reference.EntityEntry.State is EntityState.Detached)
+ {
+ return;
+ }
+
+ await reference.LoadAsync(cancellationToken: cancellationToken);
+ }
+
+ authorization.Application = null;
+ }
+ }
+}
+
+// TODO Revert to this after .NET 8 Preview 7
+internal class MyOpenIddictAuthorizationStoreResolver : IOpenIddictAuthorizationStoreResolver
+{
+ private readonly TypeResolutionCache _cache;
+ private readonly IOptionsMonitor _options;
+ private readonly IServiceProvider _provider;
+
+ public MyOpenIddictAuthorizationStoreResolver(
+ TypeResolutionCache cache,
+ IOptionsMonitor options,
+ IServiceProvider provider)
+ {
+ _cache = cache ?? throw new ArgumentNullException(nameof(cache));
+ _options = options ?? throw new ArgumentNullException(nameof(options));
+ _provider = provider ?? throw new ArgumentNullException(nameof(provider));
+ }
+
+ public IOpenIddictAuthorizationStore Get() where TAuthorization : class
+ {
+ var store = _provider.GetService>();
+ if (store is not null)
+ {
+ return store;
+ }
+
+ var type = _cache.GetOrAdd(typeof(TAuthorization), key =>
+ {
+ var root = OpenIddictHelpers.FindGenericBaseType(key, typeof(OpenIddictEntityFrameworkCoreAuthorization<,,>)) ??
+ throw new InvalidOperationException();
+ var context = _options.CurrentValue.DbContextType ??
+ throw new InvalidOperationException();
+ return typeof(MyOpenIddictAuthorizationStore<,,,,>).MakeGenericType(
+ /* TAuthorization: */ key,
+ /* TApplication: */ root.GenericTypeArguments[1],
+ /* TToken: */ root.GenericTypeArguments[2],
+ /* TContext: */ context,
+ /* TKey: */ root.GenericTypeArguments[0]);
+ });
+
+ return (IOpenIddictAuthorizationStore)_provider.GetRequiredService(type);
+ }
+
+ [SuppressMessage("Design", "CA1034:Nested types should not be visible")]
+ public sealed class TypeResolutionCache : ConcurrentDictionary { }
+}
+
+// TODO Revert to this after .NET 8 Preview 7
+internal class MyOpenIddictTokenStore : OpenIddictEntityFrameworkCoreTokenStore, IOpenIddictTokenStore
+ where TToken : OpenIddictEntityFrameworkCoreToken
+ where TApplication : OpenIddictEntityFrameworkCoreApplication
+ where TAuthorization : OpenIddictEntityFrameworkCoreAuthorization
+ where TContext : DbContext
+ where TKey : notnull, IEquatable
+{
+ public MyOpenIddictTokenStore(
+ IMemoryCache cache,
+ TContext context,
+ IOptionsMonitor options)
+ : base(cache, context, options)
+ {
+ }
+
+ private DbSet Applications => Context.Set();
+ private DbSet Authorizations => Context.Set();
+
+ public override async ValueTask GetApplicationIdAsync(TToken token, CancellationToken cancellationToken)
+ {
+ ArgumentNullException.ThrowIfNull(token);
+
+ // If the application is not attached to the token, try to load it manually.
+ if (token.Application is null)
+ {
+ var reference = Context.Entry(token).Reference(entry => entry.Application);
+ if (reference.EntityEntry.State is EntityState.Detached)
+ {
+ return null;
+ }
+
+ await reference.LoadAsync(cancellationToken: cancellationToken);
+ }
+
+ if (token.Application is null)
+ {
+ return null;
+ }
+
+ return ConvertIdentifierToString(token.Application.Id);
+ }
+
+ public override async ValueTask GetAuthorizationIdAsync(TToken token, CancellationToken cancellationToken)
+ {
+ ArgumentNullException.ThrowIfNull(token);
+
+ // If the authorization is not attached to the token, try to load it manually.
+ if (token.Authorization is null)
+ {
+ var reference = Context.Entry(token).Reference(entry => entry.Authorization);
+ if (reference.EntityEntry.State is EntityState.Detached)
+ {
+ return null;
+ }
+
+ await reference.LoadAsync(cancellationToken: cancellationToken);
+ }
+
+ if (token.Authorization is null)
+ {
+ return null;
+ }
+
+ return ConvertIdentifierToString(token.Authorization.Id);
+ }
+
+ public override async ValueTask SetApplicationIdAsync(TToken token, string? identifier, CancellationToken cancellationToken)
+ {
+ ArgumentNullException.ThrowIfNull(token);
+
+ if (!string.IsNullOrEmpty(identifier))
+ {
+ var key = ConvertIdentifierFromString(identifier);
+
+ // Warning: FindAsync() is deliberately not used to work around a breaking change introduced
+ // in Entity Framework Core 3.x (where a ValueTask instead of a Task is now returned).
+ token.Application = await Applications.AsQueryable()
+ .AsTracking()
+ .FirstOrDefaultAsync(application => application.Id!.Equals(key), cancellationToken) ??
+ throw new InvalidOperationException();
+ }
+
+ else
+ {
+ // If the application is not attached to the token, try to load it manually.
+ if (token.Application is null)
+ {
+ var reference = Context.Entry(token).Reference(entry => entry.Application);
+ if (reference.EntityEntry.State is EntityState.Detached)
+ {
+ return;
+ }
+
+ await reference.LoadAsync(cancellationToken: cancellationToken);
+ }
+
+ token.Application = null;
+ }
+ }
+
+ public override async ValueTask SetAuthorizationIdAsync(TToken token, string? identifier, CancellationToken cancellationToken)
+ {
+ ArgumentNullException.ThrowIfNull(token);
+
+ if (!string.IsNullOrEmpty(identifier))
+ {
+ var key = ConvertIdentifierFromString(identifier);
+
+ // Warning: FindAsync() is deliberately not used to work around a breaking change introduced
+ // in Entity Framework Core 3.x (where a ValueTask instead of a Task is now returned).
+ token.Authorization = await Authorizations.AsQueryable()
+ .AsTracking()
+ .FirstOrDefaultAsync(authorization => authorization.Id!.Equals(key), cancellationToken) ??
+ throw new InvalidOperationException();
+ }
+
+ else
+ {
+ // If the authorization is not attached to the token, try to load it manually.
+ if (token.Authorization is null)
+ {
+ var reference = Context.Entry(token).Reference(entry => entry.Authorization);
+ if (reference.EntityEntry.State is EntityState.Detached)
+ {
+ return;
+ }
+
+ await reference.LoadAsync(cancellationToken: cancellationToken);
+ }
+
+ token.Authorization = null;
+ }
+ }
+}
+
+// TODO Revert to this after .NET 8 Preview 7
+internal class MyOpenIddictTokenStoreResolver : IOpenIddictTokenStoreResolver
+{
+ private readonly TypeResolutionCache _cache;
+ private readonly IOptionsMonitor _options;
+ private readonly IServiceProvider _provider;
+
+ public MyOpenIddictTokenStoreResolver(
+ TypeResolutionCache cache,
+ IOptionsMonitor options,
+ IServiceProvider provider)
+ {
+ _cache = cache ?? throw new ArgumentNullException(nameof(cache));
+ _options = options ?? throw new ArgumentNullException(nameof(options));
+ _provider = provider ?? throw new ArgumentNullException(nameof(provider));
+ }
+
+ public IOpenIddictTokenStore Get() where TToken : class
+ {
+ var store = _provider.GetService>();
+ if (store is not null)
+ {
+ return store;
+ }
+
+ var type = _cache.GetOrAdd(typeof(TToken), key =>
+ {
+ var root = OpenIddictHelpers.FindGenericBaseType(key, typeof(OpenIddictEntityFrameworkCoreToken<,,>)) ??
+ throw new InvalidOperationException();
+ var context = _options.CurrentValue.DbContextType ??
+ throw new InvalidOperationException();
+ return typeof(MyOpenIddictTokenStore<,,,,>).MakeGenericType(
+ /* TToken: */ key,
+ /* TApplication: */ root.GenericTypeArguments[1],
+ /* TAuthorization: */ root.GenericTypeArguments[2],
+ /* TContext: */ context,
+ /* TKey: */ root.GenericTypeArguments[0]);
+ });
+
+ return (IOpenIddictTokenStore)_provider.GetRequiredService(type);
+ }
+
+ [SuppressMessage("Design", "CA1034:Nested types should not be visible")]
+ public sealed class TypeResolutionCache : ConcurrentDictionary { }
+}
+
+// TODO Revert to this after .NET 8 Preview 7
+internal static class OpenIddictHelpers
+{
+ public static Type FindGenericBaseType(Type type, Type definition)
+ => FindGenericBaseTypes(type, definition).FirstOrDefault()!;
+
+ public static IEnumerable FindGenericBaseTypes(Type type, Type definition)
+ {
+ ArgumentNullException.ThrowIfNull(type);
+
+ ArgumentNullException.ThrowIfNull(definition);
+
+ if (!definition.IsGenericTypeDefinition)
+ {
+ throw new ArgumentException(null, nameof(definition));
+ }
+
+ if (definition.IsInterface)
+ {
+ foreach (var contract in type.GetInterfaces())
+ {
+ if (!contract.IsGenericType && !contract.IsConstructedGenericType)
+ {
+ continue;
+ }
+
+ if (contract.GetGenericTypeDefinition() == definition)
+ {
+ yield return contract;
+ }
+ }
+ }
+
+ else
+ {
+ for (var candidate = type; candidate is not null; candidate = candidate.BaseType)
+ {
+ if (!candidate.IsGenericType && !candidate.IsConstructedGenericType)
+ {
+ continue;
+ }
+
+ if (candidate.GetGenericTypeDefinition() == definition)
+ {
+ yield return candidate;
+ }
+ }
+ }
+ }
+}
+
+// TODO Revert to this after .NET 8 Preview 7
+internal static class OpenIddictExtension
+{
+ public static OpenIddictEntityFrameworkCoreBuilder UseCustomEntityFrameworkCore(this OpenIddictCoreBuilder builder)
+ {
+ ArgumentNullException.ThrowIfNull(builder);
+
+ // Since Entity Framework Core may be used with databases performing case-insensitive
+ // or culture-sensitive comparisons, ensure the additional filtering logic is enforced
+ // in case case-sensitive stores were registered before this extension was called.
+ builder.Configure(options => options.DisableAdditionalFiltering = false);
+
+ builder.SetDefaultApplicationEntity()
+ .SetDefaultAuthorizationEntity()
+ .SetDefaultScopeEntity()
+ .SetDefaultTokenEntity();
+
+ builder.ReplaceApplicationStoreResolver()
+ .ReplaceAuthorizationStoreResolver()
+ .ReplaceScopeStoreResolver()
+ .ReplaceTokenStoreResolver();
+
+ builder.Services.TryAddSingleton();
+ builder.Services.TryAddSingleton();
+ builder.Services.TryAddSingleton();
+ builder.Services.TryAddSingleton();
+
+ builder.Services.TryAddScoped(typeof(OpenIddictEntityFrameworkCoreApplicationStore<,,,,>));
+ builder.Services.TryAddScoped(typeof(MyOpenIddictAuthorizationStore<,,,,>));
+ builder.Services.TryAddScoped(typeof(OpenIddictEntityFrameworkCoreScopeStore<,,>));
+ builder.Services.TryAddScoped(typeof(MyOpenIddictTokenStore<,,,,>));
+
+ return new OpenIddictEntityFrameworkCoreBuilder(builder.Services);
+ }
+}
diff --git a/src/Umbraco.Cms.Persistence.EFCore/Extensions/UmbracoEFCoreServiceCollectionExtensions.cs b/src/Umbraco.Cms.Persistence.EFCore/Extensions/UmbracoEFCoreServiceCollectionExtensions.cs
index 52c187dba3..d0a5299ca5 100644
--- a/src/Umbraco.Cms.Persistence.EFCore/Extensions/UmbracoEFCoreServiceCollectionExtensions.cs
+++ b/src/Umbraco.Cms.Persistence.EFCore/Extensions/UmbracoEFCoreServiceCollectionExtensions.cs
@@ -48,9 +48,6 @@ public static class UmbracoEFCoreServiceCollectionExtensions
private static ConnectionStrings GetConnectionStringAndProviderName(IServiceProvider serviceProvider)
{
- string? connectionString = null;
- string? providerName = null;
-
ConnectionStrings connectionStrings = serviceProvider.GetRequiredService>().CurrentValue;
// Replace data directory
diff --git a/src/Umbraco.Cms.Persistence.EFCore/Umbraco.Cms.Persistence.EFCore.csproj b/src/Umbraco.Cms.Persistence.EFCore/Umbraco.Cms.Persistence.EFCore.csproj
index 8d7a835ab1..4191bdb9ea 100644
--- a/src/Umbraco.Cms.Persistence.EFCore/Umbraco.Cms.Persistence.EFCore.csproj
+++ b/src/Umbraco.Cms.Persistence.EFCore/Umbraco.Cms.Persistence.EFCore.csproj
@@ -8,7 +8,6 @@
-
diff --git a/src/Umbraco.Cms.Persistence.Sqlite/Umbraco.Cms.Persistence.Sqlite.csproj b/src/Umbraco.Cms.Persistence.Sqlite/Umbraco.Cms.Persistence.Sqlite.csproj
index 92d929ce12..5c37b88d10 100644
--- a/src/Umbraco.Cms.Persistence.Sqlite/Umbraco.Cms.Persistence.Sqlite.csproj
+++ b/src/Umbraco.Cms.Persistence.Sqlite/Umbraco.Cms.Persistence.Sqlite.csproj
@@ -5,7 +5,7 @@
-
+
diff --git a/src/Umbraco.Core/Umbraco.Core.csproj b/src/Umbraco.Core/Umbraco.Core.csproj
index 6a385aeba6..69b657f062 100644
--- a/src/Umbraco.Core/Umbraco.Core.csproj
+++ b/src/Umbraco.Core/Umbraco.Core.csproj
@@ -24,7 +24,7 @@
-
+
diff --git a/src/Umbraco.Infrastructure/Umbraco.Infrastructure.csproj b/src/Umbraco.Infrastructure/Umbraco.Infrastructure.csproj
index c527c9efa2..a8d1a87702 100644
--- a/src/Umbraco.Infrastructure/Umbraco.Infrastructure.csproj
+++ b/src/Umbraco.Infrastructure/Umbraco.Infrastructure.csproj
@@ -26,7 +26,6 @@
-
diff --git a/src/Umbraco.Web.UI.New/Umbraco.Web.UI.New.csproj b/src/Umbraco.Web.UI.New/Umbraco.Web.UI.New.csproj
index 79cd00810b..abd41a7d6b 100644
--- a/src/Umbraco.Web.UI.New/Umbraco.Web.UI.New.csproj
+++ b/src/Umbraco.Web.UI.New/Umbraco.Web.UI.New.csproj
@@ -11,7 +11,7 @@
-
+
all
diff --git a/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj b/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj
index 44a72b7380..ebb17d6046 100644
--- a/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj
+++ b/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj
@@ -24,7 +24,6 @@
-
all
diff --git a/templates/UmbracoPackageRcl/.template.config/template.json b/templates/UmbracoPackageRcl/.template.config/template.json
index be7b1c04e8..c887e8902c 100644
--- a/templates/UmbracoPackageRcl/.template.config/template.json
+++ b/templates/UmbracoPackageRcl/.template.config/template.json
@@ -29,13 +29,13 @@
"datatype": "choice",
"choices": [
{
- "displayName": ".NET 7.0",
- "description": "Target net7.0",
- "choice": "net7.0"
+ "displayName": ".NET 8.0",
+ "description": "Target net8.0",
+ "choice": "net8.0"
}
],
- "defaultValue": "net7.0",
- "replaces": "net7.0"
+ "defaultValue": "net8.0",
+ "replaces": "net8.0"
},
"UmbracoVersion": {
"displayName": "Umbraco version",
diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Tests.UnitTests.csproj b/tests/Umbraco.Tests.UnitTests/Umbraco.Tests.UnitTests.csproj
index 7d37843ce0..bb58a6f090 100644
--- a/tests/Umbraco.Tests.UnitTests/Umbraco.Tests.UnitTests.csproj
+++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Tests.UnitTests.csproj
@@ -10,10 +10,10 @@
-
-
+
+
-
+