From 092097a57c150534f40c0cd2d972fda0255036b4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20Chalet?= Date: Thu, 3 Oct 2019 22:49:15 +0200 Subject: [PATCH] Cross-compile the EF Core stores to support EF Core 2.x on .NET Standard 2.0 --- Directory.Build.props | 2 +- Directory.Build.targets | 14 +++ eng/Versions.props | 6 +- .../OpenIddict.EntityFrameworkCore.csproj | 6 +- .../OpenIddictEntityFrameworkCoreHelpers.cs | 53 +++++++++++ .../Stores/OpenIddictApplicationStore.cs | 25 ++++- .../Stores/OpenIddictAuthorizationStore.cs | 76 +++++++++++++-- .../Stores/OpenIddictScopeStore.cs | 28 +++++- .../Stores/OpenIddictTokenStore.cs | 92 ++++++++++++++++--- ...penIddict.EntityFrameworkCore.Tests.csproj | 3 +- 10 files changed, 271 insertions(+), 34 deletions(-) diff --git a/Directory.Build.props b/Directory.Build.props index 9addc9b9..460d53d7 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -4,7 +4,7 @@ preview - $(NoWarn);CS1591 + $(NoWarn);CS1591;NU5128 true true $(MSBuildThisFileDirectory)\eng\key.snk diff --git a/Directory.Build.targets b/Directory.Build.targets index b04e7792..495ff58c 100644 --- a/Directory.Build.targets +++ b/Directory.Build.targets @@ -12,4 +12,18 @@ $(_ProjectCopyright) + + + + + + reactive + + + + diff --git a/eng/Versions.props b/eng/Versions.props index 3873ae16..718f9766 100644 --- a/eng/Versions.props +++ b/eng/Versions.props @@ -23,12 +23,16 @@ '$(TargetFramework)' == 'netcoreapp3.0' ">3.0.0 + + 2.1.0 + 3.0.0 + + 1.0.0 1.8.5 4.4.0 6.3.0 - 3.0.0 2019.1.3 12.0.2 1.0.2 diff --git a/src/OpenIddict.EntityFrameworkCore/OpenIddict.EntityFrameworkCore.csproj b/src/OpenIddict.EntityFrameworkCore/OpenIddict.EntityFrameworkCore.csproj index 6f59b8cb..21cfecba 100644 --- a/src/OpenIddict.EntityFrameworkCore/OpenIddict.EntityFrameworkCore.csproj +++ b/src/OpenIddict.EntityFrameworkCore/OpenIddict.EntityFrameworkCore.csproj @@ -1,7 +1,7 @@  - netstandard2.1 + netstandard2.0;netstandard2.1 @@ -23,4 +23,8 @@ + + $(DefineConstants);SUPPORTS_BCL_ASYNC_ENUMERABLE + + diff --git a/src/OpenIddict.EntityFrameworkCore/OpenIddictEntityFrameworkCoreHelpers.cs b/src/OpenIddict.EntityFrameworkCore/OpenIddictEntityFrameworkCoreHelpers.cs index c3f58649..ba00bb4d 100644 --- a/src/OpenIddict.EntityFrameworkCore/OpenIddictEntityFrameworkCoreHelpers.cs +++ b/src/OpenIddict.EntityFrameworkCore/OpenIddictEntityFrameworkCoreHelpers.cs @@ -5,8 +5,12 @@ */ using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; using JetBrains.Annotations; using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Query; using OpenIddict.EntityFrameworkCore; using OpenIddict.EntityFrameworkCore.Models; @@ -112,5 +116,54 @@ namespace Microsoft.EntityFrameworkCore .ApplyConfiguration(new OpenIddictScopeConfiguration()) .ApplyConfiguration(new OpenIddictTokenConfiguration()); } + +#if !SUPPORTS_BCL_ASYNC_ENUMERABLE + /// + /// Converts an EF Core/IX-based async enumeration to a BCL enumeration. + /// + /// The type of the returned entities. + /// The EF Core/IX async enumeration. + /// The that can be used to abort the operation. + /// The non-streamed async enumeration containing the results. + internal static IAsyncEnumerable AsAsyncEnumerable( + [NotNull] this AsyncEnumerable source, CancellationToken cancellationToken = default) + { + return ExecuteAsync(cancellationToken); + + async IAsyncEnumerable ExecuteAsync(CancellationToken cancellationToken) + { + foreach (var element in await source.ToListAsync(cancellationToken)) + { + yield return element; + } + } + } + + /// + /// Executes the query and returns the results as a non-streamed async enumeration. + /// + /// The type of the returned entities. + /// The query source. + /// The that can be used to abort the operation. + /// The non-streamed async enumeration containing the results. + internal static IAsyncEnumerable AsAsyncEnumerable( + [NotNull] this IQueryable source, CancellationToken cancellationToken = default) + { + if (source is null) + { + throw new ArgumentNullException(nameof(source)); + } + + return ExecuteAsync(cancellationToken); + + async IAsyncEnumerable ExecuteAsync(CancellationToken cancellationToken) + { + foreach (var element in await source.ToListAsync(cancellationToken)) + { + yield return element; + } + } + } +#endif } } \ No newline at end of file diff --git a/src/OpenIddict.EntityFrameworkCore/Stores/OpenIddictApplicationStore.cs b/src/OpenIddict.EntityFrameworkCore/Stores/OpenIddictApplicationStore.cs index ca74f0d2..c0ba3477 100644 --- a/src/OpenIddict.EntityFrameworkCore/Stores/OpenIddictApplicationStore.cs +++ b/src/OpenIddict.EntityFrameworkCore/Stores/OpenIddictApplicationStore.cs @@ -16,6 +16,7 @@ using System.Threading.Tasks; using JetBrains.Annotations; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Query; using Microsoft.EntityFrameworkCore.Storage; using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Options; @@ -327,7 +328,14 @@ namespace OpenIddict.EntityFrameworkCore /// Exposes a compiled query allowing to retrieve all the applications /// associated with the specified post_logout_redirect_uri. /// - private static readonly Func> FindByPostLogoutRedirectUri = + private static readonly +#if SUPPORTS_BCL_ASYNC_ENUMERABLE + Func> +#else + Func> +#endif + FindByPostLogoutRedirectUri = + // To optimize the efficiency of the query a bit, only applications whose stringified // PostLogoutRedirectUris contains the specified URL are returned. Once the applications // are retrieved, a second pass is made to ensure only valid elements are returned. @@ -353,6 +361,9 @@ namespace OpenIddict.EntityFrameworkCore } return FindByPostLogoutRedirectUri(Context, address) +#if !SUPPORTS_BCL_ASYNC_ENUMERABLE + .AsAsyncEnumerable(cancellationToken) +#endif .WhereAwait(async application => (await GetPostLogoutRedirectUrisAsync(application, cancellationToken)) .Contains(address, StringComparer.Ordinal)); } @@ -361,7 +372,14 @@ namespace OpenIddict.EntityFrameworkCore /// Exposes a compiled query allowing to retrieve all the /// applications associated with the specified redirect_uri. /// - private static readonly Func> FindByRedirectUri = + private static readonly +#if SUPPORTS_BCL_ASYNC_ENUMERABLE + Func> +#else + Func> +#endif + FindByRedirectUri = + // To optimize the efficiency of the query a bit, only applications whose stringified // RedirectUris property contains the specified URL are returned. Once the applications // are retrieved, a second pass is made to ensure only valid elements are returned. @@ -387,6 +405,9 @@ namespace OpenIddict.EntityFrameworkCore } return FindByRedirectUri(Context, address) +#if !SUPPORTS_BCL_ASYNC_ENUMERABLE + .AsAsyncEnumerable(cancellationToken) +#endif .WhereAwait(async application => (await GetRedirectUrisAsync(application, cancellationToken)) .Contains(address, StringComparer.Ordinal)); } diff --git a/src/OpenIddict.EntityFrameworkCore/Stores/OpenIddictAuthorizationStore.cs b/src/OpenIddict.EntityFrameworkCore/Stores/OpenIddictAuthorizationStore.cs index 77c5a04b..2d609471 100644 --- a/src/OpenIddict.EntityFrameworkCore/Stores/OpenIddictAuthorizationStore.cs +++ b/src/OpenIddict.EntityFrameworkCore/Stores/OpenIddictAuthorizationStore.cs @@ -16,6 +16,7 @@ using System.Threading.Tasks; using JetBrains.Annotations; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Query; using Microsoft.EntityFrameworkCore.Storage; using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Options; @@ -247,7 +248,14 @@ namespace OpenIddict.EntityFrameworkCore /// Exposes a compiled query allowing to retrieve the authorizations corresponding /// to the specified subject and associated with the application identifier. /// - private static readonly Func> FindBySubjectAndClient = + private static readonly +#if SUPPORTS_BCL_ASYNC_ENUMERABLE + Func> +#else + Func> +#endif + FindBySubjectAndClient = + // Note: due to a bug in Entity Framework Core's query visitor, the authorizations can't be // filtered using authorization.Application.Id.Equals(key). To work around this issue, // this compiled query uses an explicit join before applying the equality check. @@ -282,13 +290,24 @@ namespace OpenIddict.EntityFrameworkCore throw new ArgumentException("The client cannot be null or empty.", nameof(client)); } - return FindBySubjectAndClient(Context, ConvertIdentifierFromString(client), subject); + return FindBySubjectAndClient(Context, ConvertIdentifierFromString(client), subject) +#if !SUPPORTS_BCL_ASYNC_ENUMERABLE + .AsAsyncEnumerable(cancellationToken) +#endif + ; } /// /// Exposes a compiled query allowing to retrieve the authorizations matching the specified parameters. /// - private static readonly Func> FindBySubjectClientAndStatus = + private static readonly +#if SUPPORTS_BCL_ASYNC_ENUMERABLE + Func> +#else + Func> +#endif + FindBySubjectClientAndStatus = + // Note: due to a bug in Entity Framework Core's query visitor, the authorizations can't be // filtered using authorization.Application.Id.Equals(key). To work around this issue, // this compiled query uses an explicit join before applying the equality check. @@ -329,13 +348,24 @@ namespace OpenIddict.EntityFrameworkCore throw new ArgumentException("The status cannot be null or empty.", nameof(status)); } - return FindBySubjectClientAndStatus(Context, ConvertIdentifierFromString(client), subject, status); + return FindBySubjectClientAndStatus(Context, ConvertIdentifierFromString(client), subject, status) +#if !SUPPORTS_BCL_ASYNC_ENUMERABLE + .AsAsyncEnumerable(cancellationToken) +#endif + ; } /// /// Exposes a compiled query allowing to retrieve the authorizations matching the specified parameters. /// - private static readonly Func> FindBySubjectClientStatusAndType = + private static readonly +#if SUPPORTS_BCL_ASYNC_ENUMERABLE + Func> +#else + Func> +#endif + FindBySubjectClientStatusAndType = + // Note: due to a bug in Entity Framework Core's query visitor, the authorizations can't be // filtered using authorization.Application.Id.Equals(key). To work around this issue, // this compiled query uses an explicit join before applying the equality check. @@ -384,7 +414,11 @@ namespace OpenIddict.EntityFrameworkCore throw new ArgumentException("The type cannot be null or empty.", nameof(type)); } - return FindBySubjectClientStatusAndType(Context, ConvertIdentifierFromString(client), subject, status, type); + return FindBySubjectClientStatusAndType(Context, ConvertIdentifierFromString(client), subject, status, type) +#if !SUPPORTS_BCL_ASYNC_ENUMERABLE + .AsAsyncEnumerable(cancellationToken) +#endif + ; } /// @@ -409,7 +443,14 @@ namespace OpenIddict.EntityFrameworkCore /// Exposes a compiled query allowing to retrieve the list of /// authorizations corresponding to the specified application identifier. /// - private static readonly Func> FindByApplicationId = + private static readonly +#if SUPPORTS_BCL_ASYNC_ENUMERABLE + Func> +#else + Func> +#endif + FindByApplicationId = + // Note: due to a bug in Entity Framework Core's query visitor, the authorizations can't be // filtered using authorization.Application.Id.Equals(key). To work around this issue, // this compiled query uses an explicit join before applying the equality check. @@ -436,7 +477,11 @@ namespace OpenIddict.EntityFrameworkCore throw new ArgumentException("The identifier cannot be null or empty.", nameof(identifier)); } - return FindByApplicationId(Context, ConvertIdentifierFromString(identifier)); + return FindByApplicationId(Context, ConvertIdentifierFromString(identifier)) +#if !SUPPORTS_BCL_ASYNC_ENUMERABLE + .AsAsyncEnumerable(cancellationToken) +#endif + ; } /// @@ -470,7 +515,14 @@ namespace OpenIddict.EntityFrameworkCore /// Exposes a compiled query allowing to retrieve all the /// authorizations corresponding to the specified subject. /// - private static readonly Func> FindBySubject = + private static readonly +#if SUPPORTS_BCL_ASYNC_ENUMERABLE + Func> +#else + Func> +#endif + FindBySubject = + EF.CompileAsyncQuery((TContext context, string subject) => from authorization in context.Set() .Include(authorization => authorization.Application) @@ -492,7 +544,11 @@ namespace OpenIddict.EntityFrameworkCore throw new ArgumentException("The subject cannot be null or empty.", nameof(subject)); } - return FindBySubject(Context, subject); + return FindBySubject(Context, subject) +#if !SUPPORTS_BCL_ASYNC_ENUMERABLE + .AsAsyncEnumerable(cancellationToken) +#endif + ; } /// diff --git a/src/OpenIddict.EntityFrameworkCore/Stores/OpenIddictScopeStore.cs b/src/OpenIddict.EntityFrameworkCore/Stores/OpenIddictScopeStore.cs index c9e04c11..5a56c621 100644 --- a/src/OpenIddict.EntityFrameworkCore/Stores/OpenIddictScopeStore.cs +++ b/src/OpenIddict.EntityFrameworkCore/Stores/OpenIddictScopeStore.cs @@ -14,6 +14,7 @@ using System.Threading; using System.Threading.Tasks; using JetBrains.Annotations; using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Query; using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Options; using Newtonsoft.Json; @@ -235,8 +236,13 @@ namespace OpenIddict.EntityFrameworkCore /// /// Exposes a compiled query allowing to retrieve a list of scopes using their name. /// - private static readonly Func, IAsyncEnumerable> FindByNames = - EF.CompileAsyncQuery((TContext context, ImmutableArray names) => + private static readonly +#if SUPPORTS_BCL_ASYNC_ENUMERABLE + Func, IAsyncEnumerable> +#else + Func, AsyncEnumerable> +#endif + FindByNames = EF.CompileAsyncQuery((TContext context, ImmutableArray names) => from scope in context.Set().AsTracking() where names.Contains(scope.Name) select scope); @@ -255,13 +261,24 @@ namespace OpenIddict.EntityFrameworkCore throw new ArgumentException("Scope names cannot be null or empty.", nameof(names)); } - return FindByNames(Context, names); + return FindByNames(Context, names) +#if !SUPPORTS_BCL_ASYNC_ENUMERABLE + .AsAsyncEnumerable(cancellationToken) +#endif + ; } /// /// Exposes a compiled query allowing to retrieve all the scopes that contain the specified resource. /// - private static readonly Func> FindByResource = + private static readonly +#if SUPPORTS_BCL_ASYNC_ENUMERABLE + Func> +#else + Func> +#endif + FindByResource = + // To optimize the efficiency of the query a bit, only scopes whose stringified // Resources column contains the specified resource are returned. Once the scopes // are retrieved, a second pass is made to ensure only valid elements are returned. @@ -287,6 +304,9 @@ namespace OpenIddict.EntityFrameworkCore } return FindByResource(Context, resource) +#if !SUPPORTS_BCL_ASYNC_ENUMERABLE + .AsAsyncEnumerable(cancellationToken) +#endif .WhereAwait(async scope => (await GetResourcesAsync(scope, cancellationToken)).Contains(resource, StringComparer.Ordinal)); } diff --git a/src/OpenIddict.EntityFrameworkCore/Stores/OpenIddictTokenStore.cs b/src/OpenIddict.EntityFrameworkCore/Stores/OpenIddictTokenStore.cs index a7a35855..f8e902ad 100644 --- a/src/OpenIddict.EntityFrameworkCore/Stores/OpenIddictTokenStore.cs +++ b/src/OpenIddict.EntityFrameworkCore/Stores/OpenIddictTokenStore.cs @@ -6,7 +6,6 @@ using System; using System.Collections.Generic; -using System.Collections.Immutable; using System.ComponentModel; using System.Data; using System.Linq; @@ -16,6 +15,7 @@ using System.Threading.Tasks; using JetBrains.Annotations; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Query; using Microsoft.EntityFrameworkCore.Storage; using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Options; @@ -201,7 +201,14 @@ namespace OpenIddict.EntityFrameworkCore /// Exposes a compiled query allowing to retrieve the tokens corresponding /// to the specified subject and associated with the application identifier. /// - private static readonly Func> FindBySubjectAndClient = + private static readonly +#if SUPPORTS_BCL_ASYNC_ENUMERABLE + Func> +#else + Func> +#endif + FindBySubjectAndClient = + // Note: due to a bug in Entity Framework Core's query visitor, the authorizations can't be // filtered using token.Application.Id.Equals(key). To work around this issue, // this compiled query uses an explicit join before applying the equality check. @@ -237,13 +244,24 @@ namespace OpenIddict.EntityFrameworkCore throw new ArgumentException("The client cannot be null or empty.", nameof(client)); } - return FindBySubjectAndClient(Context, ConvertIdentifierFromString(client), subject); + return FindBySubjectAndClient(Context, ConvertIdentifierFromString(client), subject) +#if !SUPPORTS_BCL_ASYNC_ENUMERABLE + .AsAsyncEnumerable(cancellationToken) +#endif + ; } /// /// Exposes a compiled query allowing to retrieve the tokens matching the specified parameters. /// - private static readonly Func> FindBySubjectClientAndStatus = + private static readonly +#if SUPPORTS_BCL_ASYNC_ENUMERABLE + Func> +#else + Func> +#endif + FindBySubjectClientAndStatus = + // Note: due to a bug in Entity Framework Core's query visitor, the authorizations can't be // filtered using token.Application.Id.Equals(key). To work around this issue, // this compiled query uses an explicit join before applying the equality check. @@ -285,13 +303,24 @@ namespace OpenIddict.EntityFrameworkCore throw new ArgumentException("The status cannot be null or empty.", nameof(status)); } - return FindBySubjectClientAndStatus(Context, ConvertIdentifierFromString(client), subject, status); + return FindBySubjectClientAndStatus(Context, ConvertIdentifierFromString(client), subject, status) +#if !SUPPORTS_BCL_ASYNC_ENUMERABLE + .AsAsyncEnumerable(cancellationToken) +#endif + ; } /// /// Exposes a compiled query allowing to retrieve the tokens matching the specified parameters. /// - private static readonly Func> FindBySubjectClientStatusAndType = + private static readonly +#if SUPPORTS_BCL_ASYNC_ENUMERABLE + Func> +#else + Func> +#endif + FindBySubjectClientStatusAndType = + // Note: due to a bug in Entity Framework Core's query visitor, the authorizations can't be // filtered using token.Application.Id.Equals(key). To work around this issue, // this compiled query uses an explicit join before applying the equality check. @@ -341,14 +370,25 @@ namespace OpenIddict.EntityFrameworkCore throw new ArgumentException("The type cannot be null or empty.", nameof(type)); } - return FindBySubjectClientStatusAndType(Context, ConvertIdentifierFromString(client), subject, status, type); + return FindBySubjectClientStatusAndType(Context, ConvertIdentifierFromString(client), subject, status, type) +#if !SUPPORTS_BCL_ASYNC_ENUMERABLE + .AsAsyncEnumerable(cancellationToken) +#endif + ; } /// /// Exposes a compiled query allowing to retrieve the list of /// tokens corresponding to the specified application identifier. /// - private static readonly Func> FindByApplicationId = + private static readonly +#if SUPPORTS_BCL_ASYNC_ENUMERABLE + Func> +#else + Func> +#endif + FindByApplicationId = + // Note: due to a bug in Entity Framework Core's query visitor, the tokens can't be // filtered using token.Application.Id.Equals(key). To work around this issue, // this compiled query uses an explicit join before applying the equality check. @@ -375,14 +415,25 @@ namespace OpenIddict.EntityFrameworkCore throw new ArgumentException("The identifier cannot be null or empty.", nameof(identifier)); } - return FindByApplicationId(Context, ConvertIdentifierFromString(identifier)); + return FindByApplicationId(Context, ConvertIdentifierFromString(identifier)) +#if !SUPPORTS_BCL_ASYNC_ENUMERABLE + .AsAsyncEnumerable(cancellationToken) +#endif + ; } /// /// Exposes a compiled query allowing to retrieve the list of /// tokens corresponding to the specified authorization identifier. /// - private static readonly Func> FindByAuthorizationId = + private static readonly +#if SUPPORTS_BCL_ASYNC_ENUMERABLE + Func> +#else + Func> +#endif + FindByAuthorizationId = + // Note: due to a bug in Entity Framework Core's query visitor, the tokens can't be // filtered using token.Authorization.Id.Equals(key). To work around this issue, // this compiled query uses an explicit join before applying the equality check. @@ -409,7 +460,11 @@ namespace OpenIddict.EntityFrameworkCore throw new ArgumentException("The identifier cannot be null or empty.", nameof(identifier)); } - return FindByAuthorizationId(Context, ConvertIdentifierFromString(identifier)); + return FindByAuthorizationId(Context, ConvertIdentifierFromString(identifier)) +#if !SUPPORTS_BCL_ASYNC_ENUMERABLE + .AsAsyncEnumerable(cancellationToken) +#endif + ; } /// @@ -480,8 +535,13 @@ namespace OpenIddict.EntityFrameworkCore /// Exposes a compiled query allowing to retrieve the /// list of tokens corresponding to the specified subject. /// - private static readonly Func> FindBySubject = - EF.CompileAsyncQuery((TContext context, string subject) => + private static readonly +#if SUPPORTS_BCL_ASYNC_ENUMERABLE + Func> +#else + Func> +#endif + FindBySubject = EF.CompileAsyncQuery((TContext context, string subject) => from token in context.Set() .Include(token => token.Application) .Include(token => token.Authorization) @@ -502,7 +562,11 @@ namespace OpenIddict.EntityFrameworkCore throw new ArgumentException("The subject cannot be null or empty.", nameof(subject)); } - return FindBySubject(Context, subject); + return FindBySubject(Context, subject) +#if !SUPPORTS_BCL_ASYNC_ENUMERABLE + .AsAsyncEnumerable(cancellationToken) +#endif + ; } /// diff --git a/test/OpenIddict.EntityFrameworkCore.Tests/OpenIddict.EntityFrameworkCore.Tests.csproj b/test/OpenIddict.EntityFrameworkCore.Tests/OpenIddict.EntityFrameworkCore.Tests.csproj index 1d0e1e67..249ebab4 100644 --- a/test/OpenIddict.EntityFrameworkCore.Tests/OpenIddict.EntityFrameworkCore.Tests.csproj +++ b/test/OpenIddict.EntityFrameworkCore.Tests/OpenIddict.EntityFrameworkCore.Tests.csproj @@ -1,7 +1,8 @@  - netcoreapp3.0 + netcoreapp2.1;netcoreapp3.0;net461 + netcoreapp2.1;netcoreapp3.0