diff --git a/src/Passwordless.AspNetCore/IdentityBuilderExtensions.cs b/src/Passwordless.AspNetCore/IdentityBuilderExtensions.cs
index 3c8b237..4fe083e 100644
--- a/src/Passwordless.AspNetCore/IdentityBuilderExtensions.cs
+++ b/src/Passwordless.AspNetCore/IdentityBuilderExtensions.cs
@@ -18,47 +18,53 @@ namespace Microsoft.Extensions.DependencyInjection;
///
public static class IdentityBuilderExtensions
{
- ///
- /// Adds the services to support
- ///
- /// The .
- /// Configures the .
- /// The .
[RequiresUnreferencedCode("This method is incompatible with assembly trimming.")]
[RequiresDynamicCode("This method is incompatible with native AOT compilation.")]
- public static IServiceCollection AddPasswordless(this IServiceCollection services, Action configure)
- where TUser : class, new()
+ private static IServiceCollection AddPasswordlessIdentity(
+ this IServiceCollection services,
+ [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)]
+ Type userType,
+ Action> configureOptions,
+ string? defaultScheme)
{
- return services.AddPasswordlessCore(typeof(TUser), configure, defaultScheme: null);
+ // Options
+ var optionsBuilder = services.AddOptions();
+ configureOptions(optionsBuilder);
+
+ // Default scheme
+ if (!string.IsNullOrEmpty(defaultScheme))
+ {
+ optionsBuilder.Configure(o => o.SignInScheme = defaultScheme);
+ }
+
+ // Services
+ services.TryAddScoped(
+ typeof(IPasswordlessService),
+ typeof(PasswordlessService<>).MakeGenericType(userType)
+ );
+
+ services.TryAddScoped();
+
+ return services;
}
///
/// Adds the services to support
///
- /// The current instance.
+ /// The .
/// Configures the .
- /// The .
+ /// The .
[RequiresUnreferencedCode("This method is incompatible with assembly trimming.")]
[RequiresDynamicCode("This method is incompatible with native AOT compilation.")]
- public static IdentityBuilder AddPasswordless(this IdentityBuilder builder, Action configure)
- {
- builder.Services.AddPasswordlessCore(builder.UserType, configure, IdentityConstants.ApplicationScheme);
- return builder;
- }
-
- [RequiresDynamicCode("Calls Microsoft.Extensions.DependencyInjection.IdentityBuilderExtensions.AddShared(Type, OptionsBuilder, String)")]
- [RequiresUnreferencedCode("Calls Microsoft.Extensions.DependencyInjection.IdentityBuilderExtensions.AddShared(Type, OptionsBuilder, String)")]
- private static IServiceCollection AddPasswordlessCore(this IServiceCollection services,
- [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] Type userType,
- Action configure,
- string? defaultScheme)
+ public static IServiceCollection AddPasswordless(
+ this IServiceCollection services,
+ Action configure)
+ where TUser : class, new()
{
- var optionsBuilder = services.AddOptions().Configure(configure);
-
- // Add the SDK services but don't configure it there since ASP.NET Core options are a superset of their options.
+ // Don't set up options here because we can't use the provided delegate as it's for a different type
services.AddPasswordlessSdk(_ => { });
- // Override SDK options to come from ASP.NET Core options
+ // Derive core options from ASP.NET Core options
services.AddOptions()
.Configure>((options, aspNetCoreOptionsAccessor) =>
{
@@ -68,106 +74,152 @@ private static IServiceCollection AddPasswordlessCore(this IServiceCollection se
options.ApiKey = aspNetCoreOptions.ApiKey;
});
- return services.AddShared(userType, optionsBuilder, defaultScheme);
+ return services.AddPasswordlessIdentity(typeof(TUser), o => o.Configure(configure), null);
}
///
/// Adds the services to support
///
- /// The current instance.
- /// The to use to bind to . Generally it's own section.
- /// The .
[RequiresUnreferencedCode("This method is incompatible with assembly trimming.")]
[RequiresDynamicCode("This method is incompatible with native AOT compilation.")]
- public static IServiceCollection AddPasswordless(this IdentityBuilder builder, IConfiguration configuration)
+ public static IServiceCollection AddPasswordless(
+ this IServiceCollection services,
+ IConfiguration configuration)
+ where TUser : class, new()
{
- return builder.AddPasswordless(configuration, builder.UserType, IdentityConstants.ApplicationScheme);
+ services.AddPasswordlessSdk(configuration);
+ return services.AddPasswordlessIdentity(typeof(TUser), o => o.Bind(configuration), null);
}
///
/// Adds the services to support
///
- /// The current instance.
- /// The to use to bind to . Generally it's own section.
- /// The .
[RequiresUnreferencedCode("This method is incompatible with assembly trimming.")]
[RequiresDynamicCode("This method is incompatible with native AOT compilation.")]
- public static IServiceCollection AddPasswordless(this IdentityBuilder builder, IConfiguration configuration)
+ public static IServiceCollection AddPasswordless(
+ this IServiceCollection services,
+ string configurationSection)
+ where TUser : class, new()
{
- return builder.AddPasswordless(configuration, typeof(TUserType), null);
+ services.AddPasswordlessSdk(configurationSection);
+ return services.AddPasswordlessIdentity(typeof(TUser), o => o.BindConfiguration(configurationSection), null);
}
+ ///
+ /// Adds the services to support
+ ///
+ /// The current instance.
+ /// Configures the .
+ /// The .
[RequiresUnreferencedCode("This method is incompatible with assembly trimming.")]
[RequiresDynamicCode("This method is incompatible with native AOT compilation.")]
- private static IServiceCollection AddPasswordless(this IdentityBuilder builder, IConfiguration configuration, Type? userType, string? defaultScheme)
+ public static IdentityBuilder AddPasswordless(
+ this IdentityBuilder identity,
+ Action configure)
{
- var optionsBuilder = builder.Services
- .AddOptions()
- .Bind(configuration);
+ // Don't set up options here because we can't use the provided delegate as it's for a different type
+ identity.Services.AddPasswordlessSdk(_ => { });
+
+ // Derive core options from ASP.NET Core options
+ identity.Services.AddOptions()
+ .Configure>((options, aspNetCoreOptionsAccessor) =>
+ {
+ var aspNetCoreOptions = aspNetCoreOptionsAccessor.Value;
+ options.ApiUrl = aspNetCoreOptions.ApiUrl;
+ options.ApiSecret = aspNetCoreOptions.ApiSecret;
+ options.ApiKey = aspNetCoreOptions.ApiKey;
+ });
- builder.Services.AddPasswordlessSdk(configuration);
+ identity.Services.AddPasswordlessIdentity(
+ identity.UserType,
+ o => o.Configure(configure),
+ IdentityConstants.ApplicationScheme
+ );
- return builder.Services.AddShared(userType ?? builder.UserType, optionsBuilder, IdentityConstants.ApplicationScheme);
+ return identity;
}
///
/// Adds the services to support
///
- /// The current instance.
- /// The configuration path to use to bind to .
- /// The .
+ /// The current instance.
+ /// The to use to bind to . Generally it's own section.
+ /// The .
[RequiresUnreferencedCode("This method is incompatible with assembly trimming.")]
[RequiresDynamicCode("This method is incompatible with native AOT compilation.")]
- public static IServiceCollection AddPasswordless(this IdentityBuilder builder, string path)
+ public static IdentityBuilder AddPasswordless(this IdentityBuilder identity, IConfiguration configuration)
{
- return builder.AddPasswordless(path, builder.UserType, IdentityConstants.ApplicationScheme);
+ identity.Services.AddPasswordlessSdk(configuration);
+
+ identity.Services.AddPasswordlessIdentity(
+ identity.UserType,
+ o => o.Bind(configuration),
+ IdentityConstants.ApplicationScheme
+ );
+
+ return identity;
}
///
/// Adds the services to support
///
- /// The current instance.
- /// The configuration path to use to bind to .
- /// The .
+ /// The current instance.
+ /// The to use to bind to . Generally it's own section.
+ /// The .
[RequiresUnreferencedCode("This method is incompatible with assembly trimming.")]
[RequiresDynamicCode("This method is incompatible with native AOT compilation.")]
- public static IServiceCollection AddPasswordless(this IdentityBuilder builder, string path)
+ public static IdentityBuilder AddPasswordless(this IdentityBuilder identity, IConfiguration configuration)
{
- return builder.AddPasswordless(path, typeof(TUserType), null);
+ identity.Services.AddPasswordlessSdk(configuration);
+
+ identity.Services.AddPasswordlessIdentity(
+ typeof(TUserType),
+ o => o.Bind(configuration),
+ IdentityConstants.ApplicationScheme
+ );
+
+ return identity;
}
+ ///
+ /// Adds the services to support
+ ///
+ /// The current instance.
+ /// The configuration path to use to bind to .
+ /// The .
[RequiresUnreferencedCode("This method is incompatible with assembly trimming.")]
[RequiresDynamicCode("This method is incompatible with native AOT compilation.")]
- private static IServiceCollection AddPasswordless(this IdentityBuilder builder, string path, Type userType, string? defaultScheme)
+ public static IdentityBuilder AddPasswordless(this IdentityBuilder identity, string configurationSection)
{
- var optionsBuilder = builder.Services
- .AddOptions()
- .BindConfiguration(path);
+ identity.Services.AddPasswordlessSdk(configurationSection);
- builder.Services.AddPasswordlessSdk(path);
+ identity.Services.AddPasswordlessIdentity(
+ identity.UserType,
+ o => o.BindConfiguration(configurationSection),
+ IdentityConstants.ApplicationScheme
+ );
- return builder.Services.AddShared(userType ?? builder.UserType, optionsBuilder, IdentityConstants.ApplicationScheme);
+ return identity;
}
+ ///
+ /// Adds the services to support
+ ///
+ /// The current instance.
+ /// The configuration path to use to bind to .
+ /// The .
[RequiresUnreferencedCode("This method is incompatible with assembly trimming.")]
[RequiresDynamicCode("This method is incompatible with native AOT compilation.")]
- private static IServiceCollection AddShared(this IServiceCollection services,
- [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)]
- Type userType,
- OptionsBuilder optionsBuilder,
- string? defaultScheme)
+ public static IdentityBuilder AddPasswordless(this IdentityBuilder identity, string configurationSection)
{
- if (!string.IsNullOrEmpty(defaultScheme))
- {
- optionsBuilder.Configure(o => o.SignInScheme = defaultScheme);
- }
+ identity.Services.AddPasswordlessSdk(configurationSection);
- services.TryAddScoped(
- typeof(IPasswordlessService),
- typeof(PasswordlessService<>).MakeGenericType(userType));
+ identity.Services.AddPasswordlessIdentity(
+ typeof(TUserType),
+ o => o.BindConfiguration(configurationSection),
+ IdentityConstants.ApplicationScheme
+ );
- services.TryAddScoped();
-
- return services;
+ return identity;
}
}
\ No newline at end of file
diff --git a/src/Passwordless/ServiceCollectionExtensions.cs b/src/Passwordless/ServiceCollectionExtensions.cs
index 8fd2164..8cedc7e 100644
--- a/src/Passwordless/ServiceCollectionExtensions.cs
+++ b/src/Passwordless/ServiceCollectionExtensions.cs
@@ -1,5 +1,4 @@
using System;
-using System.Diagnostics.CodeAnalysis;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Options;
using Passwordless;
@@ -12,67 +11,62 @@ namespace Microsoft.Extensions.DependencyInjection;
///
public static class ServiceCollectionExtensions
{
- ///
- /// Adds and configures Passwordless-related services.
- ///
- public static IServiceCollection AddPasswordlessSdk(
+ private static IServiceCollection AddPasswordlessSdk(
this IServiceCollection services,
- Action configureOptions)
+ Action> configureOptions)
{
- services.AddOptions().Configure(configureOptions);
- services.RegisterDependencies();
+ // Options
+ configureOptions(
+ services.AddOptions()
+ );
+
+ // Client
+ services.AddHttpClient((http, sp) =>
+ new PasswordlessClient(http, sp.GetRequiredService>().Value)
+ );
+
+ // TODO: Get rid of this service, all consumers should use the interface
+ services.AddTransient(sp => (PasswordlessClient)sp.GetRequiredService());
return services;
}
+ ///
+ /// Adds and configures Passwordless-related services.
+ ///
+ public static IServiceCollection AddPasswordlessSdk(
+ this IServiceCollection services,
+ Action configureOptions) =>
+ services.AddPasswordlessSdk(o => o.Configure(configureOptions));
+
///
/// Adds and configures Passwordless-related services.
///
#if NET6_0_OR_GREATER
- [RequiresUnreferencedCode("This method is incompatible with assembly trimming.")]
+ [System.Diagnostics.CodeAnalysis.RequiresUnreferencedCode("This method is incompatible with assembly trimming.")]
#endif
#if NET7_0_OR_GREATER
- [RequiresDynamicCode("This method is incompatible with native AOT compilation.")]
+ [System.Diagnostics.CodeAnalysis.RequiresDynamicCode("This method is incompatible with native AOT compilation.")]
#endif
public static IServiceCollection AddPasswordlessSdk(
this IServiceCollection services,
- IConfiguration configuration)
- {
- services.AddOptions().Configure(configuration.Bind);
- services.RegisterDependencies();
-
- return services;
- }
+ IConfiguration configuration) =>
+ services.AddPasswordlessSdk(o => o.Bind(configuration));
///
/// Adds and configures Passwordless-related services.
///
///
- ///
+ ///
///
#if NET6_0_OR_GREATER
- [RequiresUnreferencedCode("This method is incompatible with assembly trimming.")]
+ [System.Diagnostics.CodeAnalysis.RequiresUnreferencedCode("This method is incompatible with assembly trimming.")]
#endif
#if NET7_0_OR_GREATER
- [RequiresDynamicCode("This method is incompatible with native AOT compilation.")]
+ [System.Diagnostics.CodeAnalysis.RequiresDynamicCode("This method is incompatible with native AOT compilation.")]
#endif
public static IServiceCollection AddPasswordlessSdk(
this IServiceCollection services,
- string section)
- {
- services.AddOptions().BindConfiguration(section);
- services.RegisterDependencies();
-
- return services;
- }
-
- private static void RegisterDependencies(this IServiceCollection services)
- {
- services.AddHttpClient((http, sp) =>
- new PasswordlessClient(http, sp.GetRequiredService>().Value)
- );
-
- // TODO: Get rid of this service, all consumers should use the interface
- services.AddTransient(sp => (PasswordlessClient)sp.GetRequiredService());
- }
+ string configurationSection) =>
+ services.AddPasswordlessSdk(o => o.BindConfiguration(configurationSection));
}
\ No newline at end of file
diff --git a/tests/Passwordless.Tests/Passwordless.Tests.csproj b/tests/Passwordless.Tests/Passwordless.Tests.csproj
index d95da88..b120d81 100644
--- a/tests/Passwordless.Tests/Passwordless.Tests.csproj
+++ b/tests/Passwordless.Tests/Passwordless.Tests.csproj
@@ -16,6 +16,7 @@
+
diff --git a/tests/Passwordless.Tests/ServiceRegistrationTests.cs b/tests/Passwordless.Tests/ServiceRegistrationTests.cs
index f93d7ad..8cdb179 100644
--- a/tests/Passwordless.Tests/ServiceRegistrationTests.cs
+++ b/tests/Passwordless.Tests/ServiceRegistrationTests.cs
@@ -1,12 +1,69 @@
using System;
+using System.Collections.Generic;
using System.Threading.Tasks;
+using FluentAssertions;
+using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Options;
using Xunit;
namespace Passwordless.Tests;
public class ServiceRegistrationTests
{
+ [Fact]
+ public void I_can_register_a_client_with_options_derived_from_configuration()
+ {
+ // Arrange
+ var configuration = new ConfigurationBuilder()
+ .AddInMemoryCollection([
+ new KeyValuePair("Passwordless:ApiSecret", "foo"),
+ new KeyValuePair("Passwordless:ApiKey", "bar")
+ ])
+ .Build();
+
+ var services = new ServiceCollection()
+ .AddSingleton(configuration)
+ .AddPasswordlessSdk(configuration.GetSection("Passwordless"))
+ .BuildServiceProvider();
+
+ // Act
+ var options = services.GetRequiredService>();
+
+ // Assert
+ options.Value.ApiSecret.Should().Be("foo");
+ options.Value.ApiKey.Should().Be("bar");
+ }
+
+ [Fact]
+ public void I_can_register_a_client_and_update_its_options_at_any_point_in_time()
+ {
+ // Arrange
+ var configuration = new ConfigurationBuilder()
+ .AddInMemoryCollection([
+ new KeyValuePair("Passwordless:ApiSecret", "foo"),
+ new KeyValuePair("Passwordless:ApiKey", "bar")
+ ])
+ .Build();
+
+ var services = new ServiceCollection()
+ .AddSingleton(configuration)
+ .AddPasswordlessSdk(configuration.GetSection("Passwordless"))
+ .BuildServiceProvider();
+
+ // Act
+ foreach (var provider in configuration.Providers)
+ {
+ provider.Set("Passwordless:ApiSecret", "baz");
+ provider.Set("Passwordless:ApiKey", "zap");
+ }
+
+ // Assert
+ var options = services.GetRequiredService>();
+ options.Value.ApiSecret.Should().Be("baz");
+ options.Value.ApiKey.Should().Be("zap");
+ }
+
[Fact]
public async Task I_can_register_a_client_with_invalid_credentials_and_not_receive_an_error_until_it_is_used()
{
@@ -26,4 +83,27 @@ await Assert.ThrowsAnyAsync(async () =>
await passwordless.CreateRegisterTokenAsync(new RegisterOptions("user123", "User"))
);
}
+
+ [Fact]
+ public void I_can_register_a_client_and_retrieve_a_new_copy_for_each_scope()
+ {
+ // Arrange
+ var services = new ServiceCollection()
+ .AddPasswordlessSdk(o =>
+ {
+ o.ApiSecret = "foo";
+ o.ApiKey = "bar";
+ })
+ .BuildServiceProvider();
+
+ // Act
+ using var scope1 = services.CreateScope();
+ var client1 = scope1.ServiceProvider.GetRequiredService();
+
+ using var scope2 = services.CreateScope();
+ var client2 = scope2.ServiceProvider.GetRequiredService();
+
+ // Assert
+ client1.Should().NotBeSameAs(client2);
+ }
}
\ No newline at end of file