Browse Source

Introduce OpenIddict.Server.Quartz

pull/1054/head
Kévin Chalet 6 years ago
committed by GitHub
parent
commit
51bd3c5d72
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 2
      Directory.Packages.props
  2. 1
      NuGet.config
  3. 14
      OpenIddict.sln
  4. 2
      samples/Mvc.Server/Mvc.Server.csproj
  5. 16
      samples/Mvc.Server/Startup.cs
  6. 20
      src/OpenIddict.Abstractions/Resources/OpenIddictResources.resx
  7. 10
      src/OpenIddict.Abstractions/Resources/xlf/OpenIddictResources.fr.xlf
  8. 22
      src/OpenIddict.Server.Quartz/OpenIddict.Server.Quartz.csproj
  9. 92
      src/OpenIddict.Server.Quartz/OpenIddictServerQuartzBuilder.cs
  10. 101
      src/OpenIddict.Server.Quartz/OpenIddictServerQuartzExtensions.cs
  11. 208
      src/OpenIddict.Server.Quartz/OpenIddictServerQuartzJob.cs
  12. 30
      src/OpenIddict.Server.Quartz/OpenIddictServerQuartzOptions.cs
  13. 18
      test/OpenIddict.Server.Quartz.Tests/OpenIddict.Server.Quartz.Tests.csproj
  14. 127
      test/OpenIddict.Server.Quartz.Tests/OpenIddictServerQuartzBuilderTests.cs
  15. 105
      test/OpenIddict.Server.Quartz.Tests/OpenIddictServerQuartzExtensionsTests.cs
  16. 349
      test/OpenIddict.Server.Quartz.Tests/OpenIddictServerQuartzJobTests.cs
  17. 11
      test/OpenIddict.Server.Tests/OpenIddictServerBuilderTests.cs

2
Directory.Packages.props

@ -15,6 +15,8 @@
<PackageVersion Include="MongoDB.Driver" Version="2.10.4" />
<PackageVersion Include="Moq" Version="4.14.5" />
<PackageVersion Include="Portable.BouncyCastle" Version="1.8.6.7" />
<PackageVersion Include="Quartz.Extensions.DependencyInjection" Version="3.2.0-preview-20200810-0611" />
<PackageVersion Include="Quartz.Extensions.Hosting" Version="3.2.0-preview-20200810-0611" />
<PackageVersion Include="System.Collections.Immutable" Version="1.7.1" />
<PackageVersion Include="System.ComponentModel.Annotations" Version="4.7.0" />
<PackageVersion Include="System.Linq.Async" Version="4.1.1" />

1
NuGet.config

@ -1,6 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<packageSources>
<add key="quartznet" value="https://www.myget.org/F/quartznet/api/v3/index.json" />
<add key="nuget" value="https://api.nuget.org/v3/index.json" />
<add key="dotnet-tools" value="https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-tools/nuget/v3/index.json" />
<add key="dotnet-core" value="https://dotnetfeed.blob.core.windows.net/dotnet-core/index.json" />

14
OpenIddict.sln

@ -107,6 +107,10 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "workflows", "workflows", "{
.github\workflows\build.yml = .github\workflows\build.yml
EndProjectSection
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OpenIddict.Server.Quartz", "src\OpenIddict.Server.Quartz\OpenIddict.Server.Quartz.csproj", "{FD150A90-1997-45C7-9EBE-7C6E62E464E8}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OpenIddict.Server.Quartz.Tests", "test\OpenIddict.Server.Quartz.Tests\OpenIddict.Server.Quartz.Tests.csproj", "{01420929-33B8-4B60-A618-8C8B5F139630}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@ -245,6 +249,14 @@ Global
{D94B10D3-3DD3-4829-B305-17C48833AB33}.Debug|Any CPU.Build.0 = Debug|Any CPU
{D94B10D3-3DD3-4829-B305-17C48833AB33}.Release|Any CPU.ActiveCfg = Release|Any CPU
{D94B10D3-3DD3-4829-B305-17C48833AB33}.Release|Any CPU.Build.0 = Release|Any CPU
{FD150A90-1997-45C7-9EBE-7C6E62E464E8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{FD150A90-1997-45C7-9EBE-7C6E62E464E8}.Debug|Any CPU.Build.0 = Debug|Any CPU
{FD150A90-1997-45C7-9EBE-7C6E62E464E8}.Release|Any CPU.ActiveCfg = Release|Any CPU
{FD150A90-1997-45C7-9EBE-7C6E62E464E8}.Release|Any CPU.Build.0 = Release|Any CPU
{01420929-33B8-4B60-A618-8C8B5F139630}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{01420929-33B8-4B60-A618-8C8B5F139630}.Debug|Any CPU.Build.0 = Debug|Any CPU
{01420929-33B8-4B60-A618-8C8B5F139630}.Release|Any CPU.ActiveCfg = Release|Any CPU
{01420929-33B8-4B60-A618-8C8B5F139630}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@ -283,6 +295,8 @@ Global
{FBFDB9E2-4A44-4B90-B896-E094BFC05C03} = {5FC71D6A-A994-4F62-977F-88A7D25379D7}
{E62124D4-3660-4590-B4D1-787168BBBEDD} = {5FC71D6A-A994-4F62-977F-88A7D25379D7}
{D94B10D3-3DD3-4829-B305-17C48833AB33} = {5FC71D6A-A994-4F62-977F-88A7D25379D7}
{FD150A90-1997-45C7-9EBE-7C6E62E464E8} = {D544447C-D701-46BB-9A5B-C76C612A596B}
{01420929-33B8-4B60-A618-8C8B5F139630} = {5FC71D6A-A994-4F62-977F-88A7D25379D7}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {A710059F-0466-4D48-9B3A-0EF4F840B616}

2
samples/Mvc.Server/Mvc.Server.csproj

@ -10,11 +10,13 @@
<ProjectReference Include="..\..\src\OpenIddict.AspNetCore\OpenIddict.AspNetCore.csproj" />
<ProjectReference Include="..\..\src\OpenIddict.EntityFrameworkCore\OpenIddict.EntityFrameworkCore.csproj" />
<ProjectReference Include="..\..\src\OpenIddict.MongoDb\OpenIddict.MongoDb.csproj" />
<ProjectReference Include="..\..\src\OpenIddict.Server.Quartz\OpenIddict.Server.Quartz.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" />
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" />
<PackageReference Include="Quartz.Extensions.Hosting" />
</ItemGroup>
</Project>

16
samples/Mvc.Server/Startup.cs

@ -5,6 +5,7 @@ using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Mvc.Server.Models;
using Mvc.Server.Services;
using Quartz;
using static OpenIddict.Abstractions.OpenIddictConstants;
namespace Mvc.Server
@ -46,6 +47,18 @@ namespace Mvc.Server
options.ClaimsIdentity.RoleClaimType = Claims.Role;
});
// OpenIddict offers native integration with Quartz.NET to perform scheduled tasks
// (like pruning orphaned authorizations/tokens from the database) at regular intervals.
services.AddQuartz(options =>
{
options.UseMicrosoftDependencyInjectionJobFactory();
options.UseSimpleTypeLoader();
options.UseInMemoryStore();
});
// Register the Quartz.NET service and configure it to block shutdown until jobs are complete.
services.AddQuartzHostedService(options => options.WaitForJobsToComplete = true);
services.AddOpenIddict()
// Register the OpenIddict core components.
@ -97,6 +110,9 @@ namespace Mvc.Server
.EnableVerificationEndpointPassthrough()
.DisableTransportSecurityRequirement(); // During development, you can disable the HTTPS requirement.
// Enable Quartz.NET integration.
options.UseQuartz();
// Note: if you don't want to specify a client_id when sending
// a token or revocation request, uncomment the following line:
//

20
src/OpenIddict.Abstractions/Resources/OpenIddictResources.resx

@ -480,6 +480,11 @@ Consider using 'options.AddSigningCredentials(SigningCredentials)' instead.</val
<value>Endpoint addresses cannot start with '{0}'.</value>
<comment>{Locked}</comment>
</data>
<data name="ID1081" xml:space="preserve">
<value>Dependency injection support must be enabled in Quartz.NET when using the OpenIddict server integration.
To enable DI support, call 'services.AddQuartz(options =&gt; options.UseMicrosoftDependencyInjectionJobFactory())'.</value>
<comment>{Locked}</comment>
</data>
<data name="ID1082" xml:space="preserve">
<value>Reference tokens cannot be used when disabling token storage.</value>
<comment>{Locked}</comment>
@ -1363,6 +1368,15 @@ To register the Entity Framework Core stores, reference the 'OpenIddict.EntityFr
Consider creating non-generic classes derived from the default entities for the application, authorization, scope and token entities.</value>
<comment>{Locked}</comment>
</data>
<data name="ID1277" xml:space="preserve">
<value>The core services must be registered when enabling the OpenIddict server Quartz.NET integration.
To register the OpenIddict core services, reference the 'OpenIddict.Core' package and call 'services.AddOpenIddict().AddCore()' from 'ConfigureServices'.</value>
<comment>{Locked}</comment>
</data>
<data name="ID1278" xml:space="preserve">
<value>The maximum refire count cannot be negative.</value>
<comment>{Locked}</comment>
</data>
<data name="ID3000" xml:space="preserve">
<value>The security token is missing.</value>
</data>
@ -2469,4 +2483,10 @@ This may indicate that the hashed entry is corrupted or malformed.</value>
<value>An exception occurred while trying to revoke the token '{Identifier}'.</value>
<comment>{Locked}</comment>
</data>
<data name="ID9000" xml:space="preserve">
<value>Removes orphaned tokens and authorizations from the database.</value>
</data>
<data name="ID9001" xml:space="preserve">
<value>Starts the scheduled task at regular intervals.</value>
</data>
</root>

10
src/OpenIddict.Abstractions/Resources/xlf/OpenIddictResources.fr.xlf

@ -587,6 +587,16 @@
<target state="translated">Les URLs de rappel doit être des URLs valides et absolues.</target>
<note />
</trans-unit>
<trans-unit id="ID9000">
<source>Removes orphaned tokens and authorizations from the database.</source>
<target state="translated">Supprime les jetons et autorisations orphelines de la base de données.</target>
<note />
</trans-unit>
<trans-unit id="ID9001">
<source>Starts the scheduled task at regular intervals.</source>
<target state="translated">Démarre la tâche planifiée à intervalles réguliers.</target>
<note />
</trans-unit>
</body>
</file>
</xliff>

22
src/OpenIddict.Server.Quartz/OpenIddict.Server.Quartz.csproj

@ -0,0 +1,22 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>net461;netcoreapp2.1;netcoreapp3.1;netstandard2.0;netstandard2.1</TargetFrameworks>
<Nullable>enable</Nullable>
<IsShipping>false</IsShipping>
</PropertyGroup>
<PropertyGroup>
<Description>Quartz.NET integration package for the OpenIddict server services.</Description>
<PackageTags>$(PackageTags);quartz;server</PackageTags>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\OpenIddict.Server\OpenIddict.Server.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Quartz.Extensions.DependencyInjection" />
</ItemGroup>
</Project>

92
src/OpenIddict.Server.Quartz/OpenIddictServerQuartzBuilder.cs

@ -0,0 +1,92 @@
/*
* Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0)
* See https://github.com/openiddict/openiddict-core for more information concerning
* the license and the contributors participating to this project.
*/
using System;
using System.ComponentModel;
using OpenIddict.Server.Quartz;
using SR = OpenIddict.Abstractions.OpenIddictResources;
namespace Microsoft.Extensions.DependencyInjection
{
/// <summary>
/// Exposes the necessary methods required to configure
/// the OpenIddict server Quartz.NET integration.
/// </summary>
public class OpenIddictServerQuartzBuilder
{
/// <summary>
/// Initializes a new instance of <see cref="OpenIddictServerQuartzBuilder"/>.
/// </summary>
/// <param name="services">The services collection.</param>
public OpenIddictServerQuartzBuilder(IServiceCollection services)
=> Services = services ?? throw new ArgumentNullException(nameof(services));
/// <summary>
/// Gets the services collection.
/// </summary>
[EditorBrowsable(EditorBrowsableState.Never)]
public IServiceCollection Services { get; }
/// <summary>
/// Amends the default OpenIddict server Quartz.NET configuration.
/// </summary>
/// <param name="configuration">The delegate used to configure the OpenIddict options.</param>
/// <remarks>This extension can be safely called multiple times.</remarks>
/// <returns>The <see cref="OpenIddictServerQuartzBuilder"/>.</returns>
public OpenIddictServerQuartzBuilder Configure(Action<OpenIddictServerQuartzOptions> configuration)
{
if (configuration == null)
{
throw new ArgumentNullException(nameof(configuration));
}
Services.Configure(configuration);
return this;
}
/// <summary>
/// Disables authorizations pruning.
/// </summary>
/// <returns>The <see cref="OpenIddictServerQuartzBuilder"/>.</returns>
public OpenIddictServerQuartzBuilder DisableAuthorizationsPruning()
=> Configure(options => options.DisableAuthorizationsPruning = true);
/// <summary>
/// Disables tokens pruning.
/// </summary>
/// <returns>The <see cref="OpenIddictServerQuartzBuilder"/>.</returns>
public OpenIddictServerQuartzBuilder DisableTokensPruning()
=> Configure(options => options.DisableTokensPruning = true);
/// <summary>
/// Sets the number of times a failed Quartz.NET job can be retried.
/// </summary>
/// <param name="count">The number of times a failed Quartz.NET job can be retried.</param>
/// <returns>The <see cref="OpenIddictServerQuartzBuilder"/>.</returns>
public OpenIddictServerQuartzBuilder SetMaximumRefireCount(int count)
{
if (count < 0)
{
throw new ArgumentOutOfRangeException(nameof(count), SR.GetResourceString(SR.ID1278));
}
return Configure(options => options.MaximumRefireCount = count);
}
/// <inheritdoc/>
[EditorBrowsable(EditorBrowsableState.Never)]
public override bool Equals(object? obj) => base.Equals(obj);
/// <inheritdoc/>
[EditorBrowsable(EditorBrowsableState.Never)]
public override int GetHashCode() => base.GetHashCode();
/// <inheritdoc/>
[EditorBrowsable(EditorBrowsableState.Never)]
public override string? ToString() => base.ToString();
}
}

101
src/OpenIddict.Server.Quartz/OpenIddictServerQuartzExtensions.cs

@ -0,0 +1,101 @@
/*
* Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0)
* See https://github.com/openiddict/openiddict-core for more information concerning
* the license and the contributors participating to this project.
*/
using System;
using System.Linq;
using Microsoft.Extensions.DependencyInjection.Extensions;
using OpenIddict.Server.Quartz;
using Quartz;
using SR = OpenIddict.Abstractions.OpenIddictResources;
namespace Microsoft.Extensions.DependencyInjection
{
/// <summary>
/// Exposes extensions allowing to register the OpenIddict server Quartz.NET integration.
/// </summary>
public static class OpenIddictServerQuartzExtensions
{
/// <summary>
/// Registers the OpenIddict server Quartz.NET integration in the DI container.
/// </summary>
/// <param name="builder">The services builder used by OpenIddict to register new services.</param>
/// <remarks>This extension can be safely called multiple times.</remarks>
/// <returns>The <see cref="OpenIddictServerQuartzBuilder"/>.</returns>
public static OpenIddictServerQuartzBuilder UseQuartz(this OpenIddictServerBuilder builder)
{
if (builder == null)
{
throw new ArgumentNullException(nameof(builder));
}
// Warning: the AddQuartz() method is deliberately not used as it's not idempotent.
// Calling it at this point may override user-defined services (e.g Quartz DI support).
builder.Services.TryAddTransient<OpenIddictServerQuartzJob>();
// To ensure this method can be safely called multiple times, the job details
// of the OpenIddict server job are only added if no existing IJobDetail instance
// pointing to OpenIddictServerQuartzJob was already registered in the DI container.
if (!builder.Services.Any(descriptor => descriptor.ServiceType == typeof(IJobDetail) &&
descriptor.ImplementationInstance is IJobDetail job &&
job.Key.Equals(OpenIddictServerQuartzJob.Identity)))
{
builder.Services.AddSingleton(
JobBuilder.Create<OpenIddictServerQuartzJob>()
.StoreDurably()
.WithIdentity(OpenIddictServerQuartzJob.Identity)
.WithDescription(SR.GetResourceString(SR.ID9000))
.Build());
}
// To ensure this method can be safely called multiple times, the trigger details
// of the OpenIddict server job are only added if no existing ITrigger instance
// pointing to OpenIddictServerQuartzJob was already registered in the DI container.
if (!builder.Services.Any(descriptor => descriptor.ServiceType == typeof(ITrigger) &&
descriptor.ImplementationInstance is ITrigger trigger &&
trigger.JobKey.Equals(OpenIddictServerQuartzJob.Identity)))
{
// Note: this trigger uses a quite long interval (1 hour), which means it may be
// potentially never reached if the application is shut down or recycled. As such,
// this trigger is set up to fire immediately to ensure it's executed at least once.
builder.Services.AddSingleton(
TriggerBuilder.Create()
.ForJob(OpenIddictServerQuartzJob.Identity)
.WithSimpleSchedule(options => options.WithIntervalInHours(1).RepeatForever())
.WithDescription(SR.GetResourceString(SR.ID9001))
.StartNow()
.Build());
}
return new OpenIddictServerQuartzBuilder(builder.Services);
}
/// <summary>
/// Registers the OpenIddict server Quartz.NET integration in the DI container.
/// </summary>
/// <param name="builder">The services builder used by OpenIddict to register new services.</param>
/// <param name="configuration">The configuration delegate used to configure the server services.</param>
/// <remarks>This extension can be safely called multiple times.</remarks>
/// <returns>The <see cref="OpenIddictServerBuilder"/>.</returns>
public static OpenIddictServerBuilder UseQuartz(
this OpenIddictServerBuilder builder, Action<OpenIddictServerQuartzBuilder> configuration)
{
if (builder == null)
{
throw new ArgumentNullException(nameof(builder));
}
if (configuration == null)
{
throw new ArgumentNullException(nameof(configuration));
}
configuration(builder.UseQuartz());
return builder;
}
}
}

208
src/OpenIddict.Server.Quartz/OpenIddictServerQuartzJob.cs

@ -0,0 +1,208 @@
/*
* Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0)
* See https://github.com/openiddict/openiddict-core for more information concerning
* the license and the contributors participating to this project.
*/
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
using OpenIddict.Abstractions;
using Quartz;
using SR = OpenIddict.Abstractions.OpenIddictResources;
namespace OpenIddict.Server.Quartz
{
/// <summary>
/// Represents a Quartz.NET job performing scheduled tasks for the OpenIddict server feature.
/// </summary>
[DisallowConcurrentExecution]
public class OpenIddictServerQuartzJob : IJob
{
private readonly IOptionsMonitor<OpenIddictServerQuartzOptions> _options;
private readonly IServiceProvider _provider;
/// <summary>
/// Creates a new instance of the <see cref="OpenIddictServerQuartzJob"/> class.
/// </summary>
public OpenIddictServerQuartzJob() => throw new InvalidOperationException(SR.GetResourceString(SR.ID1081));
/// <summary>
/// Creates a new instance of the <see cref="OpenIddictServerQuartzJob"/> class.
/// </summary>
/// <param name="options">The OpenIddict server Quartz.NET options.</param>
/// <param name="provider">The service provider.</param>
public OpenIddictServerQuartzJob(IOptionsMonitor<OpenIddictServerQuartzOptions> options, IServiceProvider provider)
{
_options = options;
_provider = provider;
}
/// <summary>
/// Gets the default identity assigned to this job.
/// </summary>
public static JobKey Identity { get; } = new JobKey(
name: typeof(OpenIddictServerQuartzJob).Name,
group: typeof(OpenIddictServerQuartzJob).Assembly.GetName().Name!);
/// <inheritdoc/>
public async Task Execute(IJobExecutionContext context)
{
if (context == null)
{
throw new ArgumentNullException(nameof(context));
}
List<Exception>? exceptions = null;
// Note: this job is registered as a transient service. As such, it cannot directly depend on scoped services
// like the core managers. To work around this limitation, a scope is manually created for each invocation.
var scope = _provider.CreateScope();
try
{
// Note: this background task is responsible of automatically removing orphaned tokens/authorizations
// (i.e tokens that are no longer valid and ad-hoc authorizations that have no valid tokens associated).
// Since ad-hoc authorizations and their associated tokens are removed as part of the same operation
// when they no longer have any token attached, it's more efficient to remove the authorizations first.
// Note: the authorization/token managers MUST be resolved from the scoped provider
// as they depend on scoped stores that should be disposed as soon as possible.
if (!_options.CurrentValue.DisableAuthorizationsPruning)
{
var manager = scope.ServiceProvider.GetService<IOpenIddictAuthorizationManager>();
if (manager == null)
{
// Inform Quartz.NET that the triggers associated with this job should be removed,
// as the future invocations will always fail until the application is correctly
// re-configured to register the OpenIddict core services in the DI container.
throw new JobExecutionException(new InvalidOperationException(SR.GetResourceString(SR.ID1277)))
{
RefireImmediately = false,
UnscheduleAllTriggers = true,
UnscheduleFiringTrigger = true
};
}
try
{
await manager.PruneAsync(context.CancellationToken);
}
// OutOfMemoryExceptions are treated as fatal errors and are always re-thrown as-is.
catch (OutOfMemoryException)
{
throw;
}
// OperationCanceledExceptions are typically thrown the host is about to shut down.
// To allow the host to shut down as fast as possible, this exception type is special-cased
// to prevent further processing in this job and inform Quartz.NET it shouldn't be refired.
catch (OperationCanceledException exception) when (exception.CancellationToken == context.CancellationToken)
{
throw new JobExecutionException(exception)
{
RefireImmediately = false
};
}
// AggregateExceptions are generally thrown by the manager itself when one or multiple exception(s)
// occurred while trying to prune the entities. In this case, add the inner exceptions to the collection.
catch (AggregateException exception)
{
exceptions ??= new List<Exception>(capacity: 1);
exceptions.AddRange(exception.InnerExceptions);
}
// Other exceptions are assumed to be transient and are added to the exceptions collection
// to be re-thrown later (typically, at the very end of this job, as an AggregateException).
catch (Exception exception)
{
exceptions ??= new List<Exception>(capacity: 1);
exceptions.Add(exception);
}
}
if (!_options.CurrentValue.DisableTokensPruning)
{
var manager = scope.ServiceProvider.GetService<IOpenIddictTokenManager>();
if (manager == null)
{
// Inform Quartz.NET that the triggers associated with this job should be removed,
// as the future invocations will always fail until the application is correctly
// re-configured to register the OpenIddict core services in the DI container.
throw new JobExecutionException(new InvalidOperationException(SR.GetResourceString(SR.ID1277)))
{
RefireImmediately = false,
UnscheduleAllTriggers = true,
UnscheduleFiringTrigger = true
};
}
try
{
await manager.PruneAsync(context.CancellationToken);
}
// OutOfMemoryExceptions are treated as fatal errors and are always re-thrown as-is.
catch (OutOfMemoryException)
{
throw;
}
// OperationCanceledExceptions are typically thrown the host is about to shut down.
// To allow the host to shut down as fast as possible, this exception type is special-cased
// to prevent further processing in this job and inform Quartz.NET it shouldn't be refired.
catch (OperationCanceledException exception) when (exception.CancellationToken == context.CancellationToken)
{
throw new JobExecutionException(exception)
{
RefireImmediately = false
};
}
// AggregateExceptions are generally thrown by the manager itself when one or multiple exception(s)
// occurred while trying to prune the entities. In this case, add the inner exceptions to the collection.
catch (AggregateException exception)
{
exceptions ??= new List<Exception>(capacity: 1);
exceptions.AddRange(exception.InnerExceptions);
}
// Other exceptions are assumed to be transient and are added to the exceptions collection
// to be re-thrown later (typically, at the very end of this job, as an AggregateException).
catch (Exception exception)
{
exceptions ??= new List<Exception>(capacity: 1);
exceptions.Add(exception);
}
}
if (exceptions != null)
{
throw new JobExecutionException(new AggregateException(exceptions))
{
// Only refire the job if the maximum refire count set in the options wasn't reached.
RefireImmediately = context.RefireCount < _options.CurrentValue.MaximumRefireCount
};
}
}
finally
{
if (scope is IAsyncDisposable disposable)
{
await disposable.DisposeAsync();
}
else
{
scope.Dispose();
}
}
}
}
}

30
src/OpenIddict.Server.Quartz/OpenIddictServerQuartzOptions.cs

@ -0,0 +1,30 @@
/*
* Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0)
* See https://github.com/openiddict/openiddict-core for more information concerning
* the license and the contributors participating to this project.
*/
namespace OpenIddict.Server.Quartz
{
/// <summary>
/// Provides various settings needed to configure the OpenIddict Quartz.NET server integration.
/// </summary>
public class OpenIddictServerQuartzOptions
{
/// <summary>
/// Gets or sets a boolean indicating whether authorizations pruning should be disabled.
/// </summary>
public bool DisableAuthorizationsPruning { get; set; }
/// <summary>
/// Gets or sets a boolean indicating whether tokens pruning should be disabled.
/// </summary>
public bool DisableTokensPruning { get; set; }
/// <summary>
/// Gets or sets the number of times a failed Quartz.NET job can be retried.
/// By default, failed jobs are automatically retried twice after the initial failure.
/// </summary>
public int MaximumRefireCount { get; set; } = 2;
}
}

18
test/OpenIddict.Server.Quartz.Tests/OpenIddict.Server.Quartz.Tests.csproj

@ -0,0 +1,18 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>net461;netcoreapp2.1;netcoreapp3.1</TargetFrameworks>
<IsPackable>false</IsPackable>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Moq" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\OpenIddict.Server.Quartz\OpenIddict.Server.Quartz.csproj" />
</ItemGroup>
</Project>

127
test/OpenIddict.Server.Quartz.Tests/OpenIddictServerQuartzBuilderTests.cs

@ -0,0 +1,127 @@
using System;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
using Xunit;
using SR = OpenIddict.Abstractions.OpenIddictResources;
namespace OpenIddict.Server.Quartz.Tests
{
public class OpenIddictServerQuartzBuilderTests
{
[Fact]
public void Constructor_ThrowsAnExceptionForNullServices()
{
// Arrange
var services = (IServiceCollection) null!;
// Act and assert
var exception = Assert.Throws<ArgumentNullException>(() => new OpenIddictServerQuartzBuilder(services));
Assert.Equal("services", exception.ParamName);
}
[Fact]
public void Configure_DelegateIsCorrectlyRegistered()
{
// Arrange
var services = CreateServices();
var builder = CreateBuilder(services);
var configuration = new Action<OpenIddictServerQuartzOptions>(options => { });
// Act
builder.Configure(configuration);
// Assert
Assert.Contains(services, service => service.ServiceType == typeof(IConfigureOptions<OpenIddictServerQuartzOptions>) &&
service.ImplementationInstance is ConfigureNamedOptions<OpenIddictServerQuartzOptions> options &&
options.Action == configuration && string.IsNullOrEmpty(options.Name));
}
[Fact]
public void Configure_ThrowsAnExceptionWhenConfigurationIsNull()
{
// Arrange
var services = CreateServices();
var builder = CreateBuilder(services);
// Act and assert
var exception = Assert.Throws<ArgumentNullException>(() => builder.Configure(configuration: null!));
Assert.Equal("configuration", exception.ParamName);
}
[Fact]
public void DisableAuthorizationsPruning_AuthorizationsPruningIsDisabled()
{
// Arrange
var services = CreateServices();
var builder = CreateBuilder(services);
// Act
builder.DisableAuthorizationsPruning();
var options = GetOptions(services);
// Assert
Assert.True(options.DisableAuthorizationsPruning);
}
[Fact]
public void DisableTokensPruning_TokensPruningIsDisabled()
{
// Arrange
var services = CreateServices();
var builder = CreateBuilder(services);
// Act
builder.DisableTokensPruning();
var options = GetOptions(services);
// Assert
Assert.True(options.DisableTokensPruning);
}
[Fact]
public void SetMaximumRefireCount_ThrowsAnExceptionForNegativeCount()
{
// Arrange
var services = CreateServices();
var builder = CreateBuilder(services);
// Act and assert
var exception = Assert.Throws<ArgumentOutOfRangeException>(() => builder.SetMaximumRefireCount(-1));
Assert.Equal("count", exception.ParamName);
Assert.StartsWith(SR.GetResourceString(SR.ID1278), exception.Message);
}
[Fact]
public void SetMaximumRefireCount_MaximumRefireCountIsSet()
{
// Arrange
var services = CreateServices();
var builder = CreateBuilder(services);
// Act
builder.SetMaximumRefireCount(42);
var options = GetOptions(services);
// Assert
Assert.Equal(42, options.MaximumRefireCount);
}
private static IServiceCollection CreateServices()
=> new ServiceCollection().AddOptions();
private static OpenIddictServerQuartzBuilder CreateBuilder(IServiceCollection services)
=> new OpenIddictServerQuartzBuilder(services);
private static OpenIddictServerQuartzOptions GetOptions(IServiceCollection services)
{
var provider = services.BuildServiceProvider();
var options = provider.GetRequiredService<IOptions<OpenIddictServerQuartzOptions>>();
return options.Value;
}
}
}

105
test/OpenIddict.Server.Quartz.Tests/OpenIddictServerQuartzExtensionsTests.cs

@ -0,0 +1,105 @@
using System;
using Microsoft.Extensions.DependencyInjection;
using Quartz;
using Xunit;
namespace OpenIddict.Server.Quartz.Tests
{
public class OpenIddictServerQuartzExtensionsTests
{
[Fact]
public void UseQuartz_ThrowsAnExceptionForNullBuilder()
{
// Arrange
var builder = (OpenIddictServerBuilder) null!;
// Act and assert
var exception = Assert.Throws<ArgumentNullException>(() => builder.UseQuartz());
Assert.Equal("builder", exception.ParamName);
}
[Fact]
public void UseQuartz_ThrowsAnExceptionForNullConfiguration()
{
// Arrange
var services = new ServiceCollection();
var builder = new OpenIddictServerBuilder(services);
// Act and assert
var exception = Assert.Throws<ArgumentNullException>(() => builder.UseQuartz(configuration: null!));
Assert.Equal("configuration", exception.ParamName);
}
[Fact]
public void UseQuartz_RegistersJobService()
{
// Arrange
var services = new ServiceCollection();
var builder = new OpenIddictServerBuilder(services);
// Act
builder.UseQuartz();
// Assert
Assert.Contains(services, service => service.ServiceType == typeof(OpenIddictServerQuartzJob) &&
service.ImplementationType == typeof(OpenIddictServerQuartzJob) &&
service.Lifetime == ServiceLifetime.Transient);
}
[Fact]
public void UseQuartz_RegistersJobDetails()
{
// Arrange
var services = new ServiceCollection();
var builder = new OpenIddictServerBuilder(services);
// Act
builder.UseQuartz();
// Assert
Assert.Contains(services, service => service.ServiceType == typeof(IJobDetail) &&
service.ImplementationInstance is IJobDetail job &&
job.Key.Equals(OpenIddictServerQuartzJob.Identity));
}
[Fact]
public void UseQuartz_RegistersTriggerDetails()
{
// Arrange
var services = new ServiceCollection();
var builder = new OpenIddictServerBuilder(services);
// Act
builder.UseQuartz();
// Assert
Assert.Contains(services, service => service.ServiceType == typeof(ITrigger) &&
service.ImplementationInstance is ITrigger trigger &&
trigger.JobKey.Equals(OpenIddictServerQuartzJob.Identity));
}
[Fact]
public void UseQuartz_CanBeSafelyInvokedMultipleTimes()
{
// Arrange
var services = new ServiceCollection();
var builder = new OpenIddictServerBuilder(services);
// Act
builder.UseQuartz();
builder.UseQuartz();
builder.UseQuartz();
// Assert
Assert.Single(services, service => service.ServiceType == typeof(IJobDetail) &&
service.ImplementationInstance is IJobDetail job &&
job.Key.Equals(OpenIddictServerQuartzJob.Identity));
Assert.Single(services, service => service.ServiceType == typeof(ITrigger) &&
service.ImplementationInstance is ITrigger trigger &&
trigger.JobKey.Equals(OpenIddictServerQuartzJob.Identity));
}
}
}

349
test/OpenIddict.Server.Quartz.Tests/OpenIddictServerQuartzJobTests.cs

@ -0,0 +1,349 @@
using System;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
using Moq;
using OpenIddict.Abstractions;
using Quartz;
using Xunit;
using SR = OpenIddict.Abstractions.OpenIddictResources;
namespace OpenIddict.Server.Quartz.Tests
{
public class OpenIddictServerQuartzJobTests
{
[Fact]
public void Constructor_ThrowsAnException()
{
// Arrange, act and assert
var exception = Assert.Throws<InvalidOperationException>(() => new OpenIddictServerQuartzJob());
Assert.Equal(SR.GetResourceString(SR.ID1081), exception.Message);
}
[Fact]
public async Task Execute_UsesServiceScope()
{
// Arrange
var provider = Mock.Of<IServiceProvider>(provider =>
provider.GetService(typeof(IOpenIddictAuthorizationManager)) == Mock.Of<IOpenIddictAuthorizationManager>() &&
provider.GetService(typeof(IOpenIddictTokenManager)) == Mock.Of<IOpenIddictTokenManager>());
var scope = Mock.Of<IServiceScope>(scope => scope.ServiceProvider == provider);
var factory = Mock.Of<IServiceScopeFactory>(factory => factory.CreateScope() == scope);
var monitor = Mock.Of<IOptionsMonitor<OpenIddictServerQuartzOptions>>(
monitor => monitor.CurrentValue == new OpenIddictServerQuartzOptions());
var job = new OpenIddictServerQuartzJob(monitor,
Mock.Of<IServiceProvider>(provider => provider.GetService(typeof(IServiceScopeFactory)) == factory));
// Act
await job.Execute(Mock.Of<IJobExecutionContext>());
Mock.Get(factory).Verify(factory => factory.CreateScope(), Times.Once());
Mock.Get(scope).Verify(scope => scope.Dispose(), Times.Once());
}
[Fact]
public async Task Execute_IgnoresPruningWhenAuthorizationsPruningIsDisabled()
{
// Arrange
var manager = new Mock<IOpenIddictAuthorizationManager>();
var provider = Mock.Of<IServiceProvider>(provider =>
provider.GetService(typeof(IOpenIddictAuthorizationManager)) == manager.Object &&
provider.GetService(typeof(IOpenIddictTokenManager)) == Mock.Of<IOpenIddictTokenManager>());
var job = CreateJob(provider, new OpenIddictServerQuartzOptions
{
DisableAuthorizationsPruning = true
});
// Act
await job.Execute(Mock.Of<IJobExecutionContext>());
// Assert
manager.Verify(manager => manager.PruneAsync(It.IsAny<CancellationToken>()), Times.Never());
}
[Fact]
public async Task Execute_IgnoresPruningWhenTokensPruningIsDisabled()
{
// Arrange
var manager = new Mock<IOpenIddictAuthorizationManager>();
var provider = Mock.Of<IServiceProvider>(provider =>
provider.GetService(typeof(IOpenIddictAuthorizationManager)) == Mock.Of<IOpenIddictAuthorizationManager>() &&
provider.GetService(typeof(IOpenIddictTokenManager)) == manager.Object);
var job = CreateJob(provider, new OpenIddictServerQuartzOptions
{
DisableTokensPruning = true
});
// Act
await job.Execute(Mock.Of<IJobExecutionContext>());
// Assert
manager.Verify(manager => manager.PruneAsync(It.IsAny<CancellationToken>()), Times.Never());
}
[Fact]
public async Task Execute_UnschedulesTriggersWhenAuthorizationManagerIsMissing()
{
// Arrange
var provider = Mock.Of<IServiceProvider>(provider =>
provider.GetService(typeof(IOpenIddictAuthorizationManager)) == null);
var job = CreateJob(provider);
// Act and assert
var exception = await Assert.ThrowsAsync<JobExecutionException>(() => job.Execute(Mock.Of<IJobExecutionContext>()));
Assert.False(exception.RefireImmediately);
Assert.True(exception.UnscheduleAllTriggers);
Assert.True(exception.UnscheduleFiringTrigger);
Assert.IsType<InvalidOperationException>(exception.InnerException);
Assert.Equal(SR.GetResourceString(SR.ID1277), exception.InnerException!.Message);
}
[Fact]
public async Task Execute_UnschedulesTriggersWhenTokenManagerIsMissing()
{
// Arrange
var provider = Mock.Of<IServiceProvider>(provider =>
provider.GetService(typeof(IOpenIddictAuthorizationManager)) == Mock.Of<IOpenIddictAuthorizationManager>() &&
provider.GetService(typeof(IOpenIddictTokenManager)) == null);
var job = CreateJob(provider);
// Act and assert
var exception = await Assert.ThrowsAsync<JobExecutionException>(() => job.Execute(Mock.Of<IJobExecutionContext>()));
Assert.False(exception.RefireImmediately);
Assert.True(exception.UnscheduleAllTriggers);
Assert.True(exception.UnscheduleFiringTrigger);
Assert.IsType<InvalidOperationException>(exception.InnerException);
Assert.Equal(SR.GetResourceString(SR.ID1277), exception.InnerException!.Message);
}
[Fact]
public async Task Execute_RethrowsOutOfMemoryExceptionsThrownDuringAuthorizationsPruning()
{
// Arrange
var manager = new Mock<IOpenIddictAuthorizationManager>();
manager.Setup(manager => manager.PruneAsync(It.IsAny<CancellationToken>()))
.Throws(new OutOfMemoryException());
var provider = Mock.Of<IServiceProvider>(provider =>
provider.GetService(typeof(IOpenIddictAuthorizationManager)) == manager.Object &&
provider.GetService(typeof(IOpenIddictTokenManager)) == Mock.Of<IOpenIddictTokenManager>());
var job = CreateJob(provider);
// Act and assert
await Assert.ThrowsAsync<OutOfMemoryException>(() => job.Execute(Mock.Of<IJobExecutionContext>()));
}
[Fact]
public async Task Execute_RethrowsOutOfMemoryExceptionsThrownDuringTokensPruning()
{
// Arrange
var manager = new Mock<IOpenIddictTokenManager>();
manager.Setup(manager => manager.PruneAsync(It.IsAny<CancellationToken>()))
.Throws(new OutOfMemoryException());
var provider = Mock.Of<IServiceProvider>(provider =>
provider.GetService(typeof(IOpenIddictAuthorizationManager)) == Mock.Of<IOpenIddictAuthorizationManager>() &&
provider.GetService(typeof(IOpenIddictTokenManager)) == manager.Object);
var job = CreateJob(provider);
// Act and assert
await Assert.ThrowsAsync<OutOfMemoryException>(() => job.Execute(Mock.Of<IJobExecutionContext>()));
}
[Fact]
public async Task Execute_DisablesRefiringWhenJobIsCanceledDuringAuthorizationsPruning()
{
// Arrange
var token = new CancellationToken(canceled: true);
var manager = new Mock<IOpenIddictAuthorizationManager>();
manager.Setup(manager => manager.PruneAsync(It.IsAny<CancellationToken>()))
.Throws(new OperationCanceledException(token));
var provider = Mock.Of<IServiceProvider>(provider =>
provider.GetService(typeof(IOpenIddictAuthorizationManager)) == manager.Object &&
provider.GetService(typeof(IOpenIddictTokenManager)) == Mock.Of<IOpenIddictTokenManager>());
var context = Mock.Of<IJobExecutionContext>(context => context.CancellationToken == token);
var job = CreateJob(provider);
// Act and assert
var exception = await Assert.ThrowsAsync<JobExecutionException>(() => job.Execute(context));
Assert.False(exception.RefireImmediately);
manager.Verify(manager => manager.PruneAsync(It.IsAny<CancellationToken>()), Times.Once());
}
[Fact]
public async Task Execute_DisablesRefiringWhenJobIsCanceledDuringTokensPruning()
{
// Arrange
var token = new CancellationToken(canceled: true);
var manager = new Mock<IOpenIddictTokenManager>();
manager.Setup(manager => manager.PruneAsync(It.IsAny<CancellationToken>()))
.Throws(new OperationCanceledException(token));
var provider = Mock.Of<IServiceProvider>(provider =>
provider.GetService(typeof(IOpenIddictAuthorizationManager)) == Mock.Of<IOpenIddictAuthorizationManager>() &&
provider.GetService(typeof(IOpenIddictTokenManager)) == manager.Object);
var context = Mock.Of<IJobExecutionContext>(context => context.CancellationToken == token);
var job = CreateJob(provider);
// Act and assert
var exception = await Assert.ThrowsAsync<JobExecutionException>(() => job.Execute(context));
Assert.False(exception.RefireImmediately);
manager.Verify(manager => manager.PruneAsync(It.IsAny<CancellationToken>()), Times.Once());
}
[Fact]
public async Task Execute_AllowsRefiringWhenExceptionsAreThrown()
{
// Arrange
var provider = new Mock<IServiceProvider>();
provider.Setup(provider => provider.GetService(typeof(IOpenIddictAuthorizationManager)))
.Returns(CreateAuthorizationManager(new ApplicationException()));
provider.Setup(provider => provider.GetService(typeof(IOpenIddictTokenManager)))
.Returns(CreateTokenManager(new ApplicationException()));
var context = Mock.Of<IJobExecutionContext>(context => context.RefireCount == 0);
var job = CreateJob(provider.Object);
// Act and assert
var exception = await Assert.ThrowsAsync<JobExecutionException>(() => job.Execute(context));
Assert.True(exception.RefireImmediately);
Assert.IsType<AggregateException>(exception.InnerException);
Assert.Equal(2, ((AggregateException) exception.InnerException!).InnerExceptions.Count);
Assert.IsType<ApplicationException>(((AggregateException) exception.InnerException!).InnerExceptions[0]);
Assert.IsType<ApplicationException>(((AggregateException) exception.InnerException!).InnerExceptions[1]);
static IOpenIddictAuthorizationManager CreateAuthorizationManager(Exception exception)
{
var mock = new Mock<IOpenIddictAuthorizationManager>();
mock.Setup(manager => manager.PruneAsync(It.IsAny<CancellationToken>()))
.Throws(exception);
return mock.Object;
}
static IOpenIddictTokenManager CreateTokenManager(Exception exception)
{
var mock = new Mock<IOpenIddictTokenManager>();
mock.Setup(manager => manager.PruneAsync(It.IsAny<CancellationToken>()))
.Throws(exception);
return mock.Object;
}
}
[Fact]
public async Task Execute_AllowsRefiringWhenAggregateExceptionsAreThrown()
{
// Arrange
var provider = new Mock<IServiceProvider>();
provider.Setup(provider => provider.GetService(typeof(IOpenIddictAuthorizationManager)))
.Returns(CreateAuthorizationManager(new AggregateException(
new InvalidOperationException(), new ApplicationException())));
provider.Setup(provider => provider.GetService(typeof(IOpenIddictTokenManager)))
.Returns(CreateTokenManager(new AggregateException(
new InvalidOperationException(), new ApplicationException())));
var context = Mock.Of<IJobExecutionContext>(context => context.RefireCount == 0);
var job = CreateJob(provider.Object);
// Act and assert
var exception = await Assert.ThrowsAsync<JobExecutionException>(() => job.Execute(context));
Assert.True(exception.RefireImmediately);
Assert.IsType<AggregateException>(exception.InnerException);
Assert.Equal(4, ((AggregateException) exception.InnerException!).InnerExceptions.Count);
Assert.IsType<InvalidOperationException>(((AggregateException) exception.InnerException!).InnerExceptions[0]);
Assert.IsType<ApplicationException>(((AggregateException) exception.InnerException!).InnerExceptions[1]);
Assert.IsType<InvalidOperationException>(((AggregateException) exception.InnerException!).InnerExceptions[2]);
Assert.IsType<ApplicationException>(((AggregateException) exception.InnerException!).InnerExceptions[3]);
static IOpenIddictAuthorizationManager CreateAuthorizationManager(Exception exception)
{
var mock = new Mock<IOpenIddictAuthorizationManager>();
mock.Setup(manager => manager.PruneAsync(It.IsAny<CancellationToken>()))
.Throws(exception);
return mock.Object;
}
static IOpenIddictTokenManager CreateTokenManager(Exception exception)
{
var mock = new Mock<IOpenIddictTokenManager>();
mock.Setup(manager => manager.PruneAsync(It.IsAny<CancellationToken>()))
.Throws(exception);
return mock.Object;
}
}
[Fact]
public async Task Execute_DisallowsRefiringWhenMaximumRefireCountIsReached()
{
// Arrange
var manager = new Mock<IOpenIddictAuthorizationManager>();
manager.Setup(manager => manager.PruneAsync(It.IsAny<CancellationToken>()))
.Throws(new ApplicationException());
var provider = Mock.Of<IServiceProvider>(provider =>
provider.GetService(typeof(IOpenIddictAuthorizationManager)) == manager.Object &&
provider.GetService(typeof(IOpenIddictTokenManager)) == Mock.Of<IOpenIddictTokenManager>());
var context = Mock.Of<IJobExecutionContext>(context => context.RefireCount == 5);
var job = CreateJob(provider, new OpenIddictServerQuartzOptions
{
MaximumRefireCount = 5
});
// Act and assert
var exception = await Assert.ThrowsAsync<JobExecutionException>(() => job.Execute(context));
Assert.False(exception.RefireImmediately);
}
private static OpenIddictServerQuartzJob CreateJob(IServiceProvider provider, OpenIddictServerQuartzOptions? options = null)
{
var scope = Mock.Of<IServiceScope>(scope => scope.ServiceProvider == provider);
var factory = Mock.Of<IServiceScopeFactory>(factory => factory.CreateScope() == scope);
var monitor = Mock.Of<IOptionsMonitor<OpenIddictServerQuartzOptions>>(
monitor => monitor.CurrentValue == (options ?? new OpenIddictServerQuartzOptions()));
return new OpenIddictServerQuartzJob(monitor,
Mock.Of<IServiceProvider>(provider => provider.GetService(typeof(IServiceScopeFactory)) == factory));
}
}
}

11
test/OpenIddict.Server.Tests/OpenIddictServerBuilderTests.cs

@ -212,19 +212,20 @@ namespace OpenIddict.Server.Tests
}
[Fact]
public void Configure_OptionsAreCorrectlyAmended()
public void Configure_DelegateIsCorrectlyRegistered()
{
// Arrange
var services = CreateServices();
var builder = CreateBuilder(services);
var configuration = new Action<OpenIddictServerOptions>(options => { });
// Act
builder.Configure(configuration => configuration.AccessTokenLifetime = TimeSpan.FromDays(1));
var options = GetOptions(services);
builder.Configure(configuration);
// Assert
Assert.Equal(TimeSpan.FromDays(1), options.AccessTokenLifetime);
Assert.Contains(services, service => service.ServiceType == typeof(IConfigureOptions<OpenIddictServerOptions>) &&
service.ImplementationInstance is ConfigureNamedOptions<OpenIddictServerOptions> options &&
options.Action == configuration && string.IsNullOrEmpty(options.Name));
}
[Fact]

Loading…
Cancel
Save