diff --git a/src/Passwordless/PasswordlessClient.cs b/src/Passwordless/PasswordlessClient.cs
index 6cb7c20..941bf05 100644
--- a/src/Passwordless/PasswordlessClient.cs
+++ b/src/Passwordless/PasswordlessClient.cs
@@ -11,46 +11,43 @@ namespace Passwordless;
[DebuggerDisplay("{DebuggerToString(),nq}")]
public class PasswordlessClient : IPasswordlessClient, IDisposable
{
- private readonly HttpClient _client;
- private readonly bool _disposeClient;
+ private readonly HttpClient _http;
+ private readonly PasswordlessOptions _options;
- ///
- /// Initializes an instance of .
- ///
- public static PasswordlessClient Create(PasswordlessOptions options, IHttpClientFactory factory)
+ private PasswordlessClient(HttpClient http, bool disposeClient, PasswordlessOptions options)
{
- var client = factory.CreateClient();
- client.BaseAddress = new Uri(options.ApiUrl);
- client.DefaultRequestHeaders.Add("ApiSecret", options.ApiSecret);
- return new PasswordlessClient(client);
+ _http = new HttpClient(new PasswordlessHttpHandler(http, disposeClient), true)
+ {
+ BaseAddress = new Uri(options.ApiUrl),
+ DefaultRequestHeaders =
+ {
+ {"ApiSecret", options.ApiSecret}
+ }
+ };
+
+ _options = options;
}
///
/// Initializes an instance of .
///
- public PasswordlessClient(PasswordlessOptions passwordlessOptions)
+ public PasswordlessClient(HttpClient http, PasswordlessOptions options)
+ : this(http, false, options)
{
- _client = new HttpClient
- {
- BaseAddress = new Uri(passwordlessOptions.ApiUrl),
- };
- _client.DefaultRequestHeaders.Add("ApiSecret", passwordlessOptions.ApiSecret);
- _disposeClient = true;
}
///
/// Initializes an instance of .
///
- public PasswordlessClient(HttpClient client)
+ public PasswordlessClient(PasswordlessOptions options)
+ : this(new HttpClient(), true, options)
{
- _client = client;
- _disposeClient = false;
}
///
public async Task SetAliasAsync(SetAliasRequest request, CancellationToken cancellationToken)
{
- using var response = await _client.PostAsJsonAsync("alias",
+ using var response = await _http.PostAsJsonAsync("alias",
request,
PasswordlessSerializerContext.Default.SetAliasRequest,
cancellationToken);
@@ -61,7 +58,7 @@ public async Task SetAliasAsync(SetAliasRequest request, CancellationToken cance
///
public async Task CreateRegisterTokenAsync(RegisterOptions registerOptions, CancellationToken cancellationToken = default)
{
- using var response = await _client.PostAsJsonAsync("register/token",
+ using var response = await _http.PostAsJsonAsync("register/token",
registerOptions,
PasswordlessSerializerContext.Default.RegisterOptions,
cancellationToken);
@@ -83,7 +80,7 @@ public async Task CreateRegisterTokenAsync(RegisterOption
// We just want to return null if there is a problem.
request.SkipErrorHandling();
- using var response = await _client.SendAsync(request, cancellationToken);
+ using var response = await _http.SendAsync(request, cancellationToken);
if (response.IsSuccessStatusCode)
{
@@ -100,7 +97,7 @@ public async Task CreateRegisterTokenAsync(RegisterOption
///
public async Task DeleteUserAsync(string userId, CancellationToken cancellationToken = default)
{
- using var response = await _client.PostAsJsonAsync("users/delete",
+ using var response = await _http.PostAsJsonAsync("users/delete",
new DeleteUserRequest(userId),
PasswordlessSerializerContext.Default.DeleteUserRequest,
cancellationToken);
@@ -109,7 +106,7 @@ public async Task DeleteUserAsync(string userId, CancellationToken cancellationT
///
public async Task> ListUsersAsync(CancellationToken cancellationToken = default)
{
- var response = await _client.GetFromJsonAsync(
+ var response = await _http.GetFromJsonAsync(
"users/list",
PasswordlessSerializerContext.Default.ListResponsePasswordlessUserSummary,
cancellationToken);
@@ -120,7 +117,7 @@ public async Task> ListUsersAsync(Cancell
///
public async Task> ListAliasesAsync(string userId, CancellationToken cancellationToken = default)
{
- var response = await _client.GetFromJsonAsync(
+ var response = await _http.GetFromJsonAsync(
$"alias/list?userid={userId}",
PasswordlessSerializerContext.Default.ListResponseAliasPointer,
cancellationToken);
@@ -131,7 +128,7 @@ public async Task> ListAliasesAsync(string userId, C
///
public async Task> ListCredentialsAsync(string userId, CancellationToken cancellationToken = default)
{
- var response = await _client.GetFromJsonAsync(
+ var response = await _http.GetFromJsonAsync(
$"credentials/list?userid={userId}",
PasswordlessSerializerContext.Default.ListResponseCredential,
cancellationToken);
@@ -142,7 +139,7 @@ public async Task> ListCredentialsAsync(string userId,
///
public async Task DeleteCredentialAsync(string id, CancellationToken cancellationToken = default)
{
- using var response = await _client.PostAsJsonAsync("credentials/delete",
+ using var response = await _http.PostAsJsonAsync("credentials/delete",
new DeleteCredentialRequest(id),
PasswordlessSerializerContext.Default.DeleteCredentialRequest,
cancellationToken);
@@ -157,7 +154,7 @@ public async Task DeleteCredentialAsync(byte[] id, CancellationToken cancellatio
///
public async Task GetUsersCountAsync(CancellationToken cancellationToken = default)
{
- return (await _client.GetFromJsonAsync(
+ return (await _http.GetFromJsonAsync(
"users/count",
PasswordlessSerializerContext.Default.UsersCount,
cancellationToken))!;
@@ -166,17 +163,18 @@ public async Task GetUsersCountAsync(CancellationToken cancellationT
private string DebuggerToString()
{
var sb = new StringBuilder();
+
sb.Append("ApiUrl = ");
- sb.Append(_client.BaseAddress);
- if (_client.DefaultRequestHeaders.TryGetValues("ApiSecret", out var values))
+ sb.Append(_options.ApiUrl);
+
+ if (!string.IsNullOrEmpty(_options.ApiSecret))
{
- var apiSecret = values.First();
- if (apiSecret.Length > 5)
+ if (_options.ApiSecret.Length > 5)
{
sb.Append(' ');
sb.Append("ApiSecret = ");
sb.Append("***");
- sb.Append(apiSecret.Substring(apiSecret.Length - 4));
+ sb.Append(_options.ApiSecret.Substring(_options.ApiSecret.Length - 4));
}
}
else
@@ -200,9 +198,7 @@ public void Dispose()
///
protected virtual void Dispose(bool disposing)
{
- if (disposing && _disposeClient)
- {
- _client.Dispose();
- }
+ if (disposing)
+ _http.Dispose();
}
}
\ No newline at end of file
diff --git a/src/Passwordless/PasswordlessDelegatingHandler.cs b/src/Passwordless/PasswordlessDelegatingHandler.cs
deleted file mode 100644
index bdc0387..0000000
--- a/src/Passwordless/PasswordlessDelegatingHandler.cs
+++ /dev/null
@@ -1,31 +0,0 @@
-using System.Net.Http.Json;
-using Passwordless.Helpers;
-
-namespace Passwordless;
-
-internal class PasswordlessDelegatingHandler : DelegatingHandler
-{
- protected override async Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
- {
- var response = await base.SendAsync(request, cancellationToken);
-
- if (request.ShouldSkipErrorHandling())
- {
- return response;
- }
-
- if (!response.IsSuccessStatusCode
- && string.Equals(response.Content.Headers.ContentType?.MediaType, "application/problem+json", StringComparison.OrdinalIgnoreCase))
- {
- // Attempt to read problem details
- var problemDetails = await response.Content.ReadFromJsonAsync(
- PasswordlessSerializerContext.Default.PasswordlessProblemDetails,
- cancellationToken);
-
- // Throw exception
- throw new PasswordlessApiException(problemDetails!);
- }
-
- return response;
- }
-}
\ No newline at end of file
diff --git a/src/Passwordless/PasswordlessExtensions.cs b/src/Passwordless/PasswordlessExtensions.cs
deleted file mode 100644
index cfa479f..0000000
--- a/src/Passwordless/PasswordlessExtensions.cs
+++ /dev/null
@@ -1,9 +0,0 @@
-namespace Passwordless;
-
-public static class PasswordlessExtensions
-{
- public static string ToBase64Url(this byte[] bytes)
- {
- return Base64Url.Encode(bytes);
- }
-}
\ No newline at end of file
diff --git a/src/Passwordless/PasswordlessHttpHandler.cs b/src/Passwordless/PasswordlessHttpHandler.cs
new file mode 100644
index 0000000..fe05c1b
--- /dev/null
+++ b/src/Passwordless/PasswordlessHttpHandler.cs
@@ -0,0 +1,48 @@
+using System.Net.Http.Json;
+using Passwordless.Helpers;
+
+namespace Passwordless;
+
+internal class PasswordlessHttpHandler : HttpMessageHandler
+{
+ // Externally provided HTTP Client
+ private readonly HttpClient _http;
+ private readonly bool _disposeClient;
+
+ public PasswordlessHttpHandler(HttpClient http, bool disposeClient = false)
+ {
+ _http = http;
+ _disposeClient = disposeClient;
+ }
+
+ protected override async Task SendAsync(
+ HttpRequestMessage request,
+ CancellationToken cancellationToken)
+ {
+ var response = await _http.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, cancellationToken);
+
+ // On failed requests, check if responded with ProblemDetails and provide a nicer error if so
+ if (!request.ShouldSkipErrorHandling() &&
+ !response.IsSuccessStatusCode &&
+ string.Equals(response.Content.Headers.ContentType?.MediaType,
+ "application/problem+json",
+ StringComparison.OrdinalIgnoreCase))
+ {
+ var problemDetails = await response.Content.ReadFromJsonAsync(
+ PasswordlessSerializerContext.Default.PasswordlessProblemDetails,
+ cancellationToken
+ );
+
+ if (problemDetails is not null)
+ throw new PasswordlessApiException(problemDetails);
+ }
+
+ return response;
+ }
+
+ protected override void Dispose(bool disposing)
+ {
+ if (disposing && _disposeClient)
+ _http.Dispose();
+ }
+}
\ No newline at end of file
diff --git a/src/Passwordless/Polyfill.CodeAnalysis.cs b/src/Passwordless/Polyfill.CodeAnalysis.cs
deleted file mode 100644
index 559ed10..0000000
--- a/src/Passwordless/Polyfill.CodeAnalysis.cs
+++ /dev/null
@@ -1,144 +0,0 @@
-#if !NET6_0_OR_GREATER
-
-// Licensed to the .NET Foundation under one or more agreements.
-// The .NET Foundation licenses this file to you under the MIT license.
-
-namespace System.Diagnostics.CodeAnalysis;
-
-///
-/// Indicates that certain members on a specified are accessed dynamically,
-/// for example through .
-///
-///
-/// This allows tools to understand which members are being accessed during the execution
-/// of a program.
-///
-/// This attribute is valid on members whose type is or .
-///
-/// When this attribute is applied to a location of type , the assumption is
-/// that the string represents a fully qualified type name.
-///
-/// When this attribute is applied to a class, interface, or struct, the members specified
-/// can be accessed dynamically on instances returned from calling
-/// on instances of that class, interface, or struct.
-///
-/// If the attribute is applied to a method it's treated as a special case and it implies
-/// the attribute should be applied to the "this" parameter of the method. As such the attribute
-/// should only be used on instance methods of types assignable to System.Type (or string, but no methods
-/// will use it there).
-///
-[AttributeUsage(
- AttributeTargets.Field | AttributeTargets.ReturnValue | AttributeTargets.GenericParameter |
- AttributeTargets.Parameter | AttributeTargets.Property | AttributeTargets.Method |
- AttributeTargets.Class | AttributeTargets.Interface | AttributeTargets.Struct,
- Inherited = false)]
-internal sealed class DynamicallyAccessedMembersAttribute : Attribute
-{
- ///
- /// Initializes a new instance of the class
- /// with the specified member types.
- ///
- /// The types of members dynamically accessed.
- public DynamicallyAccessedMembersAttribute(DynamicallyAccessedMemberTypes memberTypes)
- {
- MemberTypes = memberTypes;
- }
-
- ///
- /// Gets the which specifies the type
- /// of members dynamically accessed.
- ///
- public DynamicallyAccessedMemberTypes MemberTypes { get; }
-}
-
-///
-/// Specifies the types of members that are dynamically accessed.
-///
-/// This enumeration has a attribute that allows a
-/// bitwise combination of its member values.
-///
-[Flags]
-internal enum DynamicallyAccessedMemberTypes
-{
- ///
- /// Specifies no members.
- ///
- None = 0,
-
- ///
- /// Specifies the default, parameterless public constructor.
- ///
- PublicParameterlessConstructor = 0x0001,
-
- ///
- /// Specifies all public constructors.
- ///
- PublicConstructors = 0x0002 | PublicParameterlessConstructor,
-
- ///
- /// Specifies all non-public constructors.
- ///
- NonPublicConstructors = 0x0004,
-
- ///
- /// Specifies all public methods.
- ///
- PublicMethods = 0x0008,
-
- ///
- /// Specifies all non-public methods.
- ///
- NonPublicMethods = 0x0010,
-
- ///
- /// Specifies all public fields.
- ///
- PublicFields = 0x0020,
-
- ///
- /// Specifies all non-public fields.
- ///
- NonPublicFields = 0x0040,
-
- ///
- /// Specifies all public nested types.
- ///
- PublicNestedTypes = 0x0080,
-
- ///
- /// Specifies all non-public nested types.
- ///
- NonPublicNestedTypes = 0x0100,
-
- ///
- /// Specifies all public properties.
- ///
- PublicProperties = 0x0200,
-
- ///
- /// Specifies all non-public properties.
- ///
- NonPublicProperties = 0x0400,
-
- ///
- /// Specifies all public events.
- ///
- PublicEvents = 0x0800,
-
- ///
- /// Specifies all non-public events.
- ///
- NonPublicEvents = 0x1000,
-
- ///
- /// Specifies all interfaces implemented by the type.
- ///
- Interfaces = 0x2000,
-
- ///
- /// Specifies all members.
- ///
- All = ~None
-}
-
-#endif
\ No newline at end of file
diff --git a/src/Passwordless/ServiceCollectionExtensions.cs b/src/Passwordless/ServiceCollectionExtensions.cs
index b228445..f2b3678 100644
--- a/src/Passwordless/ServiceCollectionExtensions.cs
+++ b/src/Passwordless/ServiceCollectionExtensions.cs
@@ -1,5 +1,3 @@
-using System.Diagnostics.CodeAnalysis;
-using Microsoft.Extensions.Options;
using Passwordless;
// This is a trick to always show up in a class when people are registering services
@@ -13,45 +11,20 @@ public static class ServiceCollectionExtensions
///
/// Adds and configures Passwordless-related services.
///
- public static IServiceCollection AddPasswordlessSdk(this IServiceCollection services, Action configureOptions)
+ public static IServiceCollection AddPasswordlessSdk(
+ this IServiceCollection services,
+ Action configureOptions)
{
services.AddOptions()
.Configure(configureOptions)
.PostConfigure(options => options.ApiUrl ??= PasswordlessOptions.CloudApiUrl)
.Validate(options => !string.IsNullOrEmpty(options.ApiSecret), "Passwordless: Missing ApiSecret");
- services.AddPasswordlessClientCore((sp, client) =>
- {
- var options = sp.GetRequiredService>().Value;
-
- client.BaseAddress = new Uri(options.ApiUrl);
- client.DefaultRequestHeaders.Add("ApiSecret", options.ApiSecret);
- });
+ services.AddHttpClient();
// TODO: Get rid of this service, all consumers should use the interface
services.AddTransient(sp => (PasswordlessClient)sp.GetRequiredService());
return services;
}
-
- ///
- /// Helper method for making custom typed HttpClient implementations that also have
- /// the inner handler for throwing fancy exceptions. Not intended for public use,
- /// hence the hiding of it in IDE's.
- ///
- ///
- /// This method signature is subject to change without major version bump/announcement.
- ///
- internal static IServiceCollection AddPasswordlessClientCore<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] TClient, [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] TImplementation>(this IServiceCollection services, Action configureClient)
- where TClient : class
- where TImplementation : class, TClient
- {
- services.AddTransient();
-
- services
- .AddHttpClient(configureClient)
- .AddHttpMessageHandler();
-
- return services;
- }
}
\ No newline at end of file