diff --git a/.editorconfig b/.editorconfig
index eb80665..ced06b5 100644
--- a/.editorconfig
+++ b/.editorconfig
@@ -10,10 +10,11 @@ indent_style = space
file_header_template = Copyright (c) David Pine. All rights reserved.\nLicensed under the MIT License.
# Code files
-[*.cs]
+[*.cs]
indent_size = 4
tab_width = 4
trim_trailing_whitespace = true
+end_of_line = lf
###############################
# .NET Coding Conventions #
diff --git a/samples/HaveIBeenPwned.MinimalApi/Program.cs b/samples/HaveIBeenPwned.MinimalApi/Program.cs
index 5d82b0f..4d75ab6 100644
--- a/samples/HaveIBeenPwned.MinimalApi/Program.cs
+++ b/samples/HaveIBeenPwned.MinimalApi/Program.cs
@@ -1,37 +1,44 @@
-// Copyright (c) David Pine. All rights reserved.
-// Licensed under the MIT License.
-
-var builder = WebApplication.CreateBuilder(args);
-
-builder.Services.AddPwnedServices(
- builder.Configuration.GetSection(nameof(HibpOptions)));
-
-builder.Services.AddEndpointsApiExplorer();
-builder.Services.AddSwaggerGen(options =>
- options.SwaggerDoc("v1", new() { Title = "HaveIBeenPwned.MinimalApi", Version = "v1" }));
-
-using var app = builder.Build();
-
-if (builder.Environment.IsDevelopment())
-{
- app.UseDeveloperExceptionPage();
- app.UseSwagger();
- app.UseSwaggerUI(options =>
- options.SwaggerEndpoint("/swagger/v1/swagger.json", "HaveIBeenPwned.MinimalApi v1"));
-}
-
-app.UseHttpsRedirection();
-
-// Map "have i been pwned" breaches.
-app.MapGroup("api/breaches")
- .MapPwnedBreachesApi();
-
-// Map "have i been pwned" passwords.
-app.MapGet("api/passwords/{plainTextPassword}",
- (string plainTextPassword, IPwnedPasswordsClient client) => client.GetPwnedPasswordAsync(plainTextPassword));
-
-// Map "have i been pwned" pastes.
-app.MapGet("api/pastes/{account}",
- (string account, IPwnedPastesClient client) => client.GetPastesAsync(account));
-
-await app.RunAsync();
+// Copyright (c) David Pine. All rights reserved.
+// Licensed under the MIT License.
+
+var builder = WebApplication.CreateBuilder(args);
+
+builder.Services.AddPwnedServices(
+ builder.Configuration.GetSection(nameof(HibpOptions)));
+
+builder.Services.AddEndpointsApiExplorer();
+builder.Services.AddSwaggerGen(options =>
+ options.SwaggerDoc("v1", new()
+ {
+ Title = "HaveIBeenPwned.MinimalApi",
+ Version = "v1"
+ }));
+
+using var app = builder.Build();
+
+if (builder.Environment.IsDevelopment())
+{
+ app.UseDeveloperExceptionPage();
+ app.UseSwagger();
+ app.UseSwaggerUI(options =>
+ options.SwaggerEndpoint(
+ "/swagger/v1/swagger.json", "HaveIBeenPwned.MinimalApi v1"));
+}
+
+app.UseHttpsRedirection();
+
+// Map "have i been pwned" breaches.
+app.MapGroup("api/breaches")
+ .MapPwnedBreachesApi();
+
+// Map "have i been pwned" passwords.
+app.MapGet("api/passwords/{plainTextPassword}",
+ static (string plainTextPassword, IPwnedPasswordsClient client) =>
+ client.GetPwnedPasswordAsync(plainTextPassword));
+
+// Map "have i been pwned" pastes.
+app.MapGet("api/pastes/{account}",
+ static (string account, IPwnedPastesClient client) =>
+ client.GetPastesAsync(account));
+
+await app.RunAsync();
diff --git a/src/HaveIBeenPwned.Client.Abstractions/GlobalUsings.cs b/src/HaveIBeenPwned.Client.Abstractions/GlobalUsings.cs
index 05f224b..d4a1b5f 100644
--- a/src/HaveIBeenPwned.Client.Abstractions/GlobalUsings.cs
+++ b/src/HaveIBeenPwned.Client.Abstractions/GlobalUsings.cs
@@ -1,4 +1,7 @@
// Copyright (c) David Pine. All rights reserved.
// Licensed under the MIT License.
+global using System.Text.Json;
+global using System.Text.Json.Serialization;
+
global using HaveIBeenPwned.Client.Abstractions;
\ No newline at end of file
diff --git a/src/HaveIBeenPwned.Client.Abstractions/Internals/Visibility.cs b/src/HaveIBeenPwned.Client.Abstractions/Internals/Visibility.cs
index 8dbffad..8205a1e 100644
--- a/src/HaveIBeenPwned.Client.Abstractions/Internals/Visibility.cs
+++ b/src/HaveIBeenPwned.Client.Abstractions/Internals/Visibility.cs
@@ -1,7 +1,7 @@
-// Copyright (c) David Pine. All rights reserved.
-// Licensed under the MIT License.
-
-using System.Runtime.CompilerServices;
-
-[assembly: InternalsVisibleTo("HaveIBeenPwned.Client")]
+// Copyright (c) David Pine. All rights reserved.
+// Licensed under the MIT License.
+
+using System.Runtime.CompilerServices;
+
+[assembly: InternalsVisibleTo("HaveIBeenPwned.Client")]
[assembly: InternalsVisibleTo("HaveIBeenPwned.Client.AbstractionsTests")]
\ No newline at end of file
diff --git a/src/HaveIBeenPwned.Client.Abstractions/Models/BreachDetails.cs b/src/HaveIBeenPwned.Client.Abstractions/Models/BreachDetails.cs
index a728ecb..de2a888 100644
--- a/src/HaveIBeenPwned.Client.Abstractions/Models/BreachDetails.cs
+++ b/src/HaveIBeenPwned.Client.Abstractions/Models/BreachDetails.cs
@@ -1,80 +1,80 @@
-// Copyright (c) David Pine. All rights reserved.
-// Licensed under the MIT License.
-
-namespace HaveIBeenPwned.Client.Abstractions;
-
-///
-/// See
-///
-public sealed class BreachDetails : BreachHeader
-{
- ///
- /// A descriptive title for the breach suitable for displaying to end users. It's unique across all breaches but individual values may change in the future (i.e. if another breach occurs against an organisation already in the system). If a stable value is required to reference the breach, refer to the "Name" attribute instead.
- ///
- public string Title { get; set; } = null!;
-
- ///
- /// The domain of the primary website the breach occurred on. This may be used for identifying other assets external systems may have for the site.
- ///
- public string Domain { get; set; } = null!;
-
- ///
- /// The date (with no time) the breach originally occurred on in ISO 8601 format. This is not always accurate — frequently breaches are discovered and reported long after the original incident. Use this attribute as a guide only.
- ///
- public DateTime BreachDate { get; set; }
-
- ///
- /// The date and time (precision to the minute) the breach was added to the system in ISO 8601 format.
- ///
- public DateTime AddedDate { get; set; }
-
- ///
- /// The date and time (precision to the minute) the breach was modified in ISO 8601 format. This will only differ from the AddedDate attribute if other attributes represented here are changed or data in the breach itself is changed (i.e. additional data is identified and loaded). It is always either equal to or greater then the AddedDate attribute, never less than.
- ///
- public DateTime ModifiedDate { get; set; }
-
- ///
- /// The total number of accounts loaded into the system. This is usually less than the total number reported by the media due to duplication or other data integrity issues in the source data.
- ///
- public int PwnCount { get; set; }
-
- ///
- /// Contains an overview of the breach represented in HTML markup. The description may include markup such as emphasis and strong tags as well as hyperlinks.
- ///
- public string Description { get; set; } = null!;
-
- ///
- /// This attribute describes the nature of the data compromised in the breach and contains an alphabetically ordered string array of impacted data classes.
- ///
- public string[] DataClasses { get; set; } = [];
-
- ///
- /// Indicates that the breach is considered unverified. An unverified breach may not have been hacked from the indicated website. An unverified breach is still loaded into HIBP when there's sufficient confidence that a significant portion of the data is legitimate.
- ///
- public bool IsVerified { get; set; }
-
- ///
- /// Indicates that the breach is considered fabricated. A fabricated breach is unlikely to have been hacked from the indicated website and usually contains a large amount of manufactured data. However, it still contains legitimate email addresses and asserts that the account owners were compromised in the alleged breach.
- ///
- public bool IsFabricated { get; set; }
-
- ///
- /// Indicates if the breach is considered sensitive. The public API will not return any accounts for a breach flagged as sensitive.
- ///
- public bool IsSensitive { get; set; }
-
- ///
- /// Indicates if the breach has been retired. This data has been permanently removed and will not be returned by the API.
- ///
- public bool IsRetired { get; set; }
-
- ///
- /// Indicates if the breach is considered a spam list. This flag has no impact on any other attributes but it means that the data has not come as a result of a security compromise.
- ///
- public bool IsSpamList { get; set; }
-
- ///
- /// A URI that specifies where a logo for the breached service can be found. Logos are always in PNG format.
- ///
- public string LogoPath { get; set; } = null!;
-}
+// Copyright (c) David Pine. All rights reserved.
+// Licensed under the MIT License.
+
+namespace HaveIBeenPwned.Client.Abstractions;
+
+///
+/// See
+///
+public sealed class BreachDetails : BreachHeader
+{
+ ///
+ /// A descriptive title for the breach suitable for displaying to end users. It's unique across all breaches but individual values may change in the future (i.e. if another breach occurs against an organisation already in the system). If a stable value is required to reference the breach, refer to the "Name" attribute instead.
+ ///
+ public string Title { get; set; } = null!;
+
+ ///
+ /// The domain of the primary website the breach occurred on. This may be used for identifying other assets external systems may have for the site.
+ ///
+ public string Domain { get; set; } = null!;
+
+ ///
+ /// The date (with no time) the breach originally occurred on in ISO 8601 format. This is not always accurate — frequently breaches are discovered and reported long after the original incident. Use this attribute as a guide only.
+ ///
+ public DateTime BreachDate { get; set; }
+
+ ///
+ /// The date and time (precision to the minute) the breach was added to the system in ISO 8601 format.
+ ///
+ public DateTime AddedDate { get; set; }
+
+ ///
+ /// The date and time (precision to the minute) the breach was modified in ISO 8601 format. This will only differ from the AddedDate attribute if other attributes represented here are changed or data in the breach itself is changed (i.e. additional data is identified and loaded). It is always either equal to or greater then the AddedDate attribute, never less than.
+ ///
+ public DateTime ModifiedDate { get; set; }
+
+ ///
+ /// The total number of accounts loaded into the system. This is usually less than the total number reported by the media due to duplication or other data integrity issues in the source data.
+ ///
+ public int PwnCount { get; set; }
+
+ ///
+ /// Contains an overview of the breach represented in HTML markup. The description may include markup such as emphasis and strong tags as well as hyperlinks.
+ ///
+ public string Description { get; set; } = null!;
+
+ ///
+ /// This attribute describes the nature of the data compromised in the breach and contains an alphabetically ordered string array of impacted data classes.
+ ///
+ public string[] DataClasses { get; set; } = [];
+
+ ///
+ /// Indicates that the breach is considered unverified. An unverified breach may not have been hacked from the indicated website. An unverified breach is still loaded into HIBP when there's sufficient confidence that a significant portion of the data is legitimate.
+ ///
+ public bool IsVerified { get; set; }
+
+ ///
+ /// Indicates that the breach is considered fabricated. A fabricated breach is unlikely to have been hacked from the indicated website and usually contains a large amount of manufactured data. However, it still contains legitimate email addresses and asserts that the account owners were compromised in the alleged breach.
+ ///
+ public bool IsFabricated { get; set; }
+
+ ///
+ /// Indicates if the breach is considered sensitive. The public API will not return any accounts for a breach flagged as sensitive.
+ ///
+ public bool IsSensitive { get; set; }
+
+ ///
+ /// Indicates if the breach has been retired. This data has been permanently removed and will not be returned by the API.
+ ///
+ public bool IsRetired { get; set; }
+
+ ///
+ /// Indicates if the breach is considered a spam list. This flag has no impact on any other attributes but it means that the data has not come as a result of a security compromise.
+ ///
+ public bool IsSpamList { get; set; }
+
+ ///
+ /// A URI that specifies where a logo for the breached service can be found. Logos are always in PNG format.
+ ///
+ public string LogoPath { get; set; } = null!;
+}
diff --git a/src/HaveIBeenPwned.Client.Abstractions/Models/BreachHeader.cs b/src/HaveIBeenPwned.Client.Abstractions/Models/BreachHeader.cs
index 89fd652..8b430e6 100644
--- a/src/HaveIBeenPwned.Client.Abstractions/Models/BreachHeader.cs
+++ b/src/HaveIBeenPwned.Client.Abstractions/Models/BreachHeader.cs
@@ -1,17 +1,17 @@
-// Copyright (c) David Pine. All rights reserved.
-// Licensed under the MIT License.
-
-namespace HaveIBeenPwned.Client.Abstractions;
-
-///
-/// See
-///
-public class BreachHeader
-{
- ///
- /// A Pascal-cased name representing the breach which is unique across all other breaches.
- /// This value never changes and may be used to name dependent assets (such as images)
- /// but should not be shown directly to end users (see the "Title" attribute instead).
- ///
- public string Name { get; set; } = null!;
-}
+// Copyright (c) David Pine. All rights reserved.
+// Licensed under the MIT License.
+
+namespace HaveIBeenPwned.Client.Abstractions;
+
+///
+/// See
+///
+public class BreachHeader
+{
+ ///
+ /// A Pascal-cased name representing the breach which is unique across all other breaches.
+ /// This value never changes and may be used to name dependent assets (such as images)
+ /// but should not be shown directly to end users (see the "Title" attribute instead).
+ ///
+ public string Name { get; set; } = null!;
+}
diff --git a/src/HaveIBeenPwned.Client.Abstractions/Models/Pastes.cs b/src/HaveIBeenPwned.Client.Abstractions/Models/Pastes.cs
index d2c44a3..675dac9 100644
--- a/src/HaveIBeenPwned.Client.Abstractions/Models/Pastes.cs
+++ b/src/HaveIBeenPwned.Client.Abstractions/Models/Pastes.cs
@@ -1,41 +1,41 @@
-// Copyright (c) David Pine. All rights reserved.
-// Licensed under the MIT License.
-
-namespace HaveIBeenPwned.Client.Abstractions;
-
-///
-/// See
-///
-public sealed class Pastes
-{
- ///
- /// The paste service the record was retrieved from.
- /// Current values are:
- /// Pastebin, Pastie, Slexy, Ghostbin, QuickLeak, JustPaste, AdHocUrl, PermanentOptOut, OptOut.
- ///
- public string Source { get; set; } = null!;
-
- ///
- /// The ID of the paste as it was given at the source service.
- /// Combined with the attribute, this can be used to resolve the URL of the paste.
- ///
- public string Id { get; set; } = null!;
-
- ///
- /// The title of the paste as observed on the source site.
- /// This may be and if so will be omitted from the response.
- ///
- public string? Title { get; set; } = null!;
-
- ///
- /// The date and time (precision to the second) that the paste was posted.
- /// This is taken directly from the paste site when this information is available but may be null if no date is published.
- ///
- public DateTime Date { get; set; }
-
- ///
- /// The number of emails that were found when processing the paste.
- /// Emails are extracted by using the regular expression \b[a-zA-Z0-9\.\-_\+]+@[a-zA-Z0-9\.\-_]+\.[a-zA-Z]+\b.
- ///
- public int EmailCount { get; set; }
-}
+// Copyright (c) David Pine. All rights reserved.
+// Licensed under the MIT License.
+
+namespace HaveIBeenPwned.Client.Abstractions;
+
+///
+/// See
+///
+public sealed class Pastes
+{
+ ///
+ /// The paste service the record was retrieved from.
+ /// Current values are:
+ /// Pastebin, Pastie, Slexy, Ghostbin, QuickLeak, JustPaste, AdHocUrl, PermanentOptOut, OptOut.
+ ///
+ public string Source { get; set; } = null!;
+
+ ///
+ /// The ID of the paste as it was given at the source service.
+ /// Combined with the attribute, this can be used to resolve the URL of the paste.
+ ///
+ public string Id { get; set; } = null!;
+
+ ///
+ /// The title of the paste as observed on the source site.
+ /// This may be and if so will be omitted from the response.
+ ///
+ public string? Title { get; set; } = null!;
+
+ ///
+ /// The date and time (precision to the second) that the paste was posted.
+ /// This is taken directly from the paste site when this information is available but may be null if no date is published.
+ ///
+ public DateTime Date { get; set; }
+
+ ///
+ /// The number of emails that were found when processing the paste.
+ /// Emails are extracted by using the regular expression \b[a-zA-Z0-9\.\-_\+]+@[a-zA-Z0-9\.\-_]+\.[a-zA-Z]+\b.
+ ///
+ public int EmailCount { get; set; }
+}
diff --git a/src/HaveIBeenPwned.Client.Abstractions/Models/PwnedPassword.cs b/src/HaveIBeenPwned.Client.Abstractions/Models/PwnedPassword.cs
index 8005aae..c38d407 100644
--- a/src/HaveIBeenPwned.Client.Abstractions/Models/PwnedPassword.cs
+++ b/src/HaveIBeenPwned.Client.Abstractions/Models/PwnedPassword.cs
@@ -1,38 +1,38 @@
-// Copyright (c) David Pine. All rights reserved.
-// Licensed under the MIT License.
-
-namespace HaveIBeenPwned.Client.Abstractions;
-
-///
-/// An object used to represent the plain-text password, and corresponding
-/// hashed password. As well as whether the password is considered "pwned",
-/// and if so, how many times.
-///
-public sealed record class PwnedPassword
-{
- ///
- /// The plain text password used for the lookup.
- ///
- public string? PlainTextPassword { get; set; }
-
- ///
- /// Whether or not the current
- /// instance is considered to be "pwned".
- ///
- public bool? IsPwned { get; set; }
-
- ///
- /// When is true, this will be a non-zero number.
- /// It represents the number of times the given
- /// has been found in the "have i been pwned" passwords database.
- ///
- public long PwnedCount { get; set; }
-
- ///
- /// The hashed representation of the given .
- ///
- public string? HashedPassword { get; set; }
-
- internal bool IsInvalid() =>
- PlainTextPassword is null or { Length: 0 };
-}
+// Copyright (c) David Pine. All rights reserved.
+// Licensed under the MIT License.
+
+namespace HaveIBeenPwned.Client.Abstractions;
+
+///
+/// An object used to represent the plain-text password, and corresponding
+/// hashed password. As well as whether the password is considered "pwned",
+/// and if so, how many times.
+///
+public sealed record class PwnedPassword
+{
+ ///
+ /// The plain text password used for the lookup.
+ ///
+ public string? PlainTextPassword { get; set; }
+
+ ///
+ /// Whether or not the current
+ /// instance is considered to be "pwned".
+ ///
+ public bool? IsPwned { get; set; }
+
+ ///
+ /// When is true, this will be a non-zero number.
+ /// It represents the number of times the given
+ /// has been found in the "have i been pwned" passwords database.
+ ///
+ public long PwnedCount { get; set; }
+
+ ///
+ /// The hashed representation of the given .
+ ///
+ public string? HashedPassword { get; set; }
+
+ internal bool IsInvalid() =>
+ PlainTextPassword is null or { Length: 0 };
+}
diff --git a/src/HaveIBeenPwned.Client.Abstractions/Serialization/SourceGeneratorContext.cs b/src/HaveIBeenPwned.Client.Abstractions/Serialization/SourceGeneratorContext.cs
index 9159c9d..cdcbee3 100644
--- a/src/HaveIBeenPwned.Client.Abstractions/Serialization/SourceGeneratorContext.cs
+++ b/src/HaveIBeenPwned.Client.Abstractions/Serialization/SourceGeneratorContext.cs
@@ -1,9 +1,6 @@
// Copyright (c) David Pine. All rights reserved.
// Licensed under the MIT License.
-using System.Text.Json;
-using System.Text.Json.Serialization;
-
using BreachDetailsModel = HaveIBeenPwned.Client.Abstractions.BreachDetails;
using BreachHeaderModel = HaveIBeenPwned.Client.Abstractions.BreachHeader;
using PastesModel = HaveIBeenPwned.Client.Abstractions.Pastes;
diff --git a/src/HaveIBeenPwned.Client.PollyExtensions/Extensions/PwnedClientServiceCollectionExtensions.cs b/src/HaveIBeenPwned.Client.PollyExtensions/Extensions/PwnedClientServiceCollectionExtensions.cs
index 5a30585..0269762 100644
--- a/src/HaveIBeenPwned.Client.PollyExtensions/Extensions/PwnedClientServiceCollectionExtensions.cs
+++ b/src/HaveIBeenPwned.Client.PollyExtensions/Extensions/PwnedClientServiceCollectionExtensions.cs
@@ -1,115 +1,115 @@
-// Copyright (c) David Pine. All rights reserved.
-// Licensed under the MIT License.
-
-namespace Microsoft.Extensions.DependencyInjection;
-
-///
-/// Extension methods for setting up pwned client related services in an .
-///
-public static class PwnedClientServiceCollectionExtensions
-{
- ///
- /// Adds all of the necessary Pwned service functionality to
- /// the collection for dependency injection.
- ///
- /// The service collection to add services to.
- /// The name configuration section to bind options from.
- /// The rety policy configuration function, when provided adds transient HTTP error policy.
- /// The same instance with other services added.
- ///
- /// If either the or are .
- ///
- public static IServiceCollection AddPwnedServices(
- this IServiceCollection services,
- IConfiguration namedConfigurationSection,
- Action configureResilienceOptions)
- {
- ArgumentNullException.ThrowIfNull(services);
- ArgumentNullException.ThrowIfNull(namedConfigurationSection);
- ArgumentNullException.ThrowIfNull(configureResilienceOptions);
-
- services.Configure(namedConfigurationSection);
-
- return AddPwnedServices(services, configureResilienceOptions);
- }
-
- ///
- /// Adds all of the necessary Pwned service functionality to
- /// the collection for dependency injection.
- ///
- /// The service collection to add services to.
- /// The action used to configure options.
- /// The retry policy configuration function, when provided adds transient HTTP error policy.
- /// The same instance with other services added.
- ///
- /// If either the or are .
- ///
- public static IServiceCollection AddPwnedServices(
- this IServiceCollection services,
- Action configureOptions,
- Action configureResilienceOptions)
- {
- ArgumentNullException.ThrowIfNull(services);
- ArgumentNullException.ThrowIfNull(configureOptions);
- ArgumentNullException.ThrowIfNull(configureResilienceOptions);
-
- services.Configure(configureOptions);
-
- return AddPwnedServices(services, configureResilienceOptions);
- }
-
- static IServiceCollection AddPwnedServices(
- IServiceCollection services,
- Action? configureResilienceOptions)
- {
- services.AddLogging();
- services.AddOptions();
-
- var builder = AddPwnedHttpClient(
- services,
- HttpClientNames.HibpClient,
- HttpClientUrls.HibpApiUrl);
-
- builder.ConfigureHttpResilience(configureResilienceOptions);
-
- builder = AddPwnedHttpClient(
- services,
- HttpClientNames.PasswordsClient,
- HttpClientUrls.PasswordsApiUrl,
- isPlainText: true);
-
- builder.ConfigureHttpResilience(configureResilienceOptions);
-
- services.AddSingleton();
- services.AddSingleton();
- services.AddSingleton();
- services.AddSingleton();
-
- return services;
- }
-
- static IHttpClientBuilder AddPwnedHttpClient(
- IServiceCollection services,
- string httpClientName,
- string baseAddress,
- bool isPlainText = false) =>
- services.AddHttpClient(
- httpClientName,
- (serviceProvider, client) =>
- {
- var options = serviceProvider.GetRequiredService>();
- var (apiKey, userAgent, _) = options?.Value
- ?? throw new InvalidOperationException(
- "The 'Have I Been Pwned' options object cannot be null.");
-
- client.BaseAddress = new(baseAddress);
- client.DefaultRequestHeaders.Add("hibp-api-key", apiKey);
- client.DefaultRequestHeaders.UserAgent.ParseAdd(userAgent);
-
- if (isPlainText)
- {
- client.DefaultRequestHeaders.Accept.Add(
- new(MediaTypeNames.Text.Plain));
- }
- });
-}
+// Copyright (c) David Pine. All rights reserved.
+// Licensed under the MIT License.
+
+namespace Microsoft.Extensions.DependencyInjection;
+
+///
+/// Extension methods for setting up pwned client related services in an .
+///
+public static class PwnedClientServiceCollectionExtensions
+{
+ ///
+ /// Adds all of the necessary Pwned service functionality to
+ /// the collection for dependency injection.
+ ///
+ /// The service collection to add services to.
+ /// The name configuration section to bind options from.
+ /// The rety policy configuration function, when provided adds transient HTTP error policy.
+ /// The same instance with other services added.
+ ///
+ /// If either the or are .
+ ///
+ public static IServiceCollection AddPwnedServices(
+ this IServiceCollection services,
+ IConfiguration namedConfigurationSection,
+ Action configureResilienceOptions)
+ {
+ ArgumentNullException.ThrowIfNull(services);
+ ArgumentNullException.ThrowIfNull(namedConfigurationSection);
+ ArgumentNullException.ThrowIfNull(configureResilienceOptions);
+
+ services.Configure(namedConfigurationSection);
+
+ return AddPwnedServices(services, configureResilienceOptions);
+ }
+
+ ///
+ /// Adds all of the necessary Pwned service functionality to
+ /// the collection for dependency injection.
+ ///
+ /// The service collection to add services to.
+ /// The action used to configure options.
+ /// The retry policy configuration function, when provided adds transient HTTP error policy.
+ /// The same instance with other services added.
+ ///
+ /// If either the or are .
+ ///
+ public static IServiceCollection AddPwnedServices(
+ this IServiceCollection services,
+ Action configureOptions,
+ Action configureResilienceOptions)
+ {
+ ArgumentNullException.ThrowIfNull(services);
+ ArgumentNullException.ThrowIfNull(configureOptions);
+ ArgumentNullException.ThrowIfNull(configureResilienceOptions);
+
+ services.Configure(configureOptions);
+
+ return AddPwnedServices(services, configureResilienceOptions);
+ }
+
+ static IServiceCollection AddPwnedServices(
+ IServiceCollection services,
+ Action? configureResilienceOptions)
+ {
+ services.AddLogging();
+ services.AddOptions();
+
+ var builder = AddPwnedHttpClient(
+ services,
+ HttpClientNames.HibpClient,
+ HttpClientUrls.HibpApiUrl);
+
+ builder.ConfigureHttpResilience(configureResilienceOptions);
+
+ builder = AddPwnedHttpClient(
+ services,
+ HttpClientNames.PasswordsClient,
+ HttpClientUrls.PasswordsApiUrl,
+ isPlainText: true);
+
+ builder.ConfigureHttpResilience(configureResilienceOptions);
+
+ services.AddSingleton();
+ services.AddSingleton();
+ services.AddSingleton();
+ services.AddSingleton();
+
+ return services;
+ }
+
+ static IHttpClientBuilder AddPwnedHttpClient(
+ IServiceCollection services,
+ string httpClientName,
+ string baseAddress,
+ bool isPlainText = false) =>
+ services.AddHttpClient(
+ httpClientName,
+ (serviceProvider, client) =>
+ {
+ var options = serviceProvider.GetRequiredService>();
+ var (apiKey, userAgent, _) = options?.Value
+ ?? throw new InvalidOperationException(
+ "The 'Have I Been Pwned' options object cannot be null.");
+
+ client.BaseAddress = new(baseAddress);
+ client.DefaultRequestHeaders.Add("hibp-api-key", apiKey);
+ client.DefaultRequestHeaders.UserAgent.ParseAdd(userAgent);
+
+ if (isPlainText)
+ {
+ client.DefaultRequestHeaders.Accept.Add(
+ new(MediaTypeNames.Text.Plain));
+ }
+ });
+}
diff --git a/src/HaveIBeenPwned.Client.PollyExtensions/GlobalUsings.cs b/src/HaveIBeenPwned.Client.PollyExtensions/GlobalUsings.cs
index 4f6012b..33784ff 100644
--- a/src/HaveIBeenPwned.Client.PollyExtensions/GlobalUsings.cs
+++ b/src/HaveIBeenPwned.Client.PollyExtensions/GlobalUsings.cs
@@ -1,17 +1,14 @@
-// Copyright (c) David Pine. All rights reserved.
-// Licensed under the MIT License.
-
-global using System.Net;
-global using System.Net.Mime;
-
-global using Polly;
-global using Polly.RateLimiting;
-
-global using HaveIBeenPwned.Client;
-global using HaveIBeenPwned.Client.Http;
-global using HaveIBeenPwned.Client.Options;
-
-global using Microsoft.Extensions.Configuration;
-global using Microsoft.Extensions.DependencyInjection;
-global using Microsoft.Extensions.Http.Resilience;
-global using Microsoft.Extensions.Options;
+// Copyright (c) David Pine. All rights reserved.
+// Licensed under the MIT License.
+
+global using System.Net;
+global using System.Net.Mime;
+
+global using HaveIBeenPwned.Client;
+global using HaveIBeenPwned.Client.Http;
+global using HaveIBeenPwned.Client.Options;
+
+global using Microsoft.Extensions.Configuration;
+global using Microsoft.Extensions.DependencyInjection;
+global using Microsoft.Extensions.Http.Resilience;
+global using Microsoft.Extensions.Options;
diff --git a/src/HaveIBeenPwned.Client/DefaultPwnedClient.Passwords.cs b/src/HaveIBeenPwned.Client/DefaultPwnedClient.Passwords.cs
index ce74ab1..731133d 100644
--- a/src/HaveIBeenPwned.Client/DefaultPwnedClient.Passwords.cs
+++ b/src/HaveIBeenPwned.Client/DefaultPwnedClient.Passwords.cs
@@ -1,100 +1,98 @@
-// Copyright (c) David Pine. All rights reserved.
-// Licensed under the MIT License.
-
-using System.Collections.Frozen;
-
-namespace HaveIBeenPwned.Client;
-
-internal sealed partial class DefaultPwnedClient : IPwnedClient
-{
- ///
- async Task IPwnedPasswordsClient.GetPwnedPasswordAsync(string plainTextPassword)
- {
- if (plainTextPassword is null or { Length: 0 })
- {
- throw new ArgumentException(
- "The plainTextPassword cannot be either null, or empty.", nameof(plainTextPassword));
- }
-
- var pwnedPassword = new PwnedPassword()
- {
- PlainTextPassword = plainTextPassword
- };
-
- if (pwnedPassword.IsInvalid())
- {
- return pwnedPassword;
- }
-
- try
- {
- var passwordHash = plainTextPassword.ToSha1Hash()!;
- var firstFiveChars = passwordHash[..5];
- var client = httpClientFactory.CreateClient(PasswordsClient);
- var passwordHashesInRange =
- await client.GetStringAsync($"range/{firstFiveChars}");
-
- pwnedPassword =
- ParsePasswordRangeResponseText(pwnedPassword, passwordHashesInRange, passwordHash);
- }
- catch (Exception ex)
- {
- logger.LogError(ex, "{ExceptionMessage}", ex.Message);
- }
-
- return pwnedPassword;
- }
-
- internal static readonly char[] s_newLineSeparator = ['\n'];
- internal static readonly char[] s_colonSeparator = [':'];
-
- internal static PwnedPassword ParsePasswordRangeResponseText(
- PwnedPassword pwnedPassword, string passwordRangeResponseText, string passwordHash)
- {
- pwnedPassword = pwnedPassword with
- {
- HashedPassword = passwordHash
- };
-
- if (passwordRangeResponseText is not null)
- {
- // Example passwordRangeResponseText
- // The remaining hash characters, less the first five separated by a : with the corresponding count.
- // :
-
- // 0018A45C4D1DEF81644B54AB7F969B88D65:10
- // 00D4F6E8FA6EECAD2A3AA415EEC418D38EC:2
- // 011053FD0102E94D6AE2F8B83D76FAF94F6:701
- // 012A7CA357541F0AC487871FEEC1891C49C:2
- // 0136E006E24E7D152139815FB0FC6A50B15:2
-
- var hashCountMap =
- passwordRangeResponseText
- .Split(s_newLineSeparator, StringSplitOptions.RemoveEmptyEntries)
- .Select(hashCountPair =>
- {
- var pair = hashCountPair
- .Replace('\r', '\0')
- .Split(s_colonSeparator, StringSplitOptions.RemoveEmptyEntries);
-
- return pair?.Length != 2 || !long.TryParse(pair[1], out var count)
- ? (Hash: "", Count: 0L, IsValid: false)
- : (Hash: pair[0], Count: count, IsValid: true);
- })
- .Where(t => t.IsValid)
- .ToFrozenDictionary(t => t.Hash, t => t.Count, StringComparer.OrdinalIgnoreCase);
-
- var hashSuffix = passwordHash[5..];
- if (hashCountMap.TryGetValue(hashSuffix, out var count))
- {
- pwnedPassword = pwnedPassword with
- {
- PwnedCount = count,
- IsPwned = count > 0,
- };
- }
- }
-
- return pwnedPassword;
- }
-}
+// Copyright (c) David Pine. All rights reserved.
+// Licensed under the MIT License.
+
+namespace HaveIBeenPwned.Client;
+
+internal sealed partial class DefaultPwnedClient : IPwnedClient
+{
+ ///
+ async Task IPwnedPasswordsClient.GetPwnedPasswordAsync(string plainTextPassword)
+ {
+ if (plainTextPassword is null or { Length: 0 })
+ {
+ throw new ArgumentException(
+ "The plainTextPassword cannot be either null, or empty.", nameof(plainTextPassword));
+ }
+
+ var pwnedPassword = new PwnedPassword()
+ {
+ PlainTextPassword = plainTextPassword
+ };
+
+ if (pwnedPassword.IsInvalid())
+ {
+ return pwnedPassword;
+ }
+
+ try
+ {
+ var passwordHash = plainTextPassword.ToSha1Hash()!;
+ var firstFiveChars = passwordHash[..5];
+ var client = httpClientFactory.CreateClient(PasswordsClient);
+ var passwordHashesInRange =
+ await client.GetStringAsync($"range/{firstFiveChars}");
+
+ pwnedPassword =
+ ParsePasswordRangeResponseText(pwnedPassword, passwordHashesInRange, passwordHash);
+ }
+ catch (Exception ex)
+ {
+ logger.LogError(ex, "{ExceptionMessage}", ex.Message);
+ }
+
+ return pwnedPassword;
+ }
+
+ internal static readonly char[] s_newLineSeparator = ['\n'];
+ internal static readonly char[] s_colonSeparator = [':'];
+
+ internal static PwnedPassword ParsePasswordRangeResponseText(
+ PwnedPassword pwnedPassword, string passwordRangeResponseText, string passwordHash)
+ {
+ pwnedPassword = pwnedPassword with
+ {
+ HashedPassword = passwordHash
+ };
+
+ if (passwordRangeResponseText is not null)
+ {
+ // Example passwordRangeResponseText
+ // The remaining hash characters, less the first five separated by a : with the corresponding count.
+ // :
+
+ // 0018A45C4D1DEF81644B54AB7F969B88D65:10
+ // 00D4F6E8FA6EECAD2A3AA415EEC418D38EC:2
+ // 011053FD0102E94D6AE2F8B83D76FAF94F6:701
+ // 012A7CA357541F0AC487871FEEC1891C49C:2
+ // 0136E006E24E7D152139815FB0FC6A50B15:2
+
+ var hashCountMap =
+ passwordRangeResponseText
+ .Split(s_newLineSeparator, StringSplitOptions.RemoveEmptyEntries)
+ .Select(hashCountPair =>
+ {
+ var pair = hashCountPair
+ .Replace('\r', '\0')
+ .Split(s_colonSeparator, StringSplitOptions.RemoveEmptyEntries);
+
+ return pair?.Length != 2 || !long.TryParse(pair[1], out var count)
+ ? (Hash: "", Count: 0L, IsValid: false)
+ : (Hash: pair[0], Count: count, IsValid: true);
+ })
+ .Where(t => t.IsValid)
+ .ToFrozenDictionary(t => t.Hash, t => t.Count, StringComparer.OrdinalIgnoreCase);
+
+ var hashSuffix = passwordHash[5..];
+ if (hashCountMap.TryGetValue(hashSuffix, out var count))
+ {
+ pwnedPassword = pwnedPassword with
+ {
+ PwnedCount = count,
+ IsPwned = count > 0,
+ };
+ }
+ }
+
+ return pwnedPassword;
+ }
+}
diff --git a/src/HaveIBeenPwned.Client/DefaultPwnedClient.Pastes.cs b/src/HaveIBeenPwned.Client/DefaultPwnedClient.Pastes.cs
index 5a0e137..c242430 100644
--- a/src/HaveIBeenPwned.Client/DefaultPwnedClient.Pastes.cs
+++ b/src/HaveIBeenPwned.Client/DefaultPwnedClient.Pastes.cs
@@ -1,61 +1,61 @@
-// Copyright (c) David Pine. All rights reserved.
-// Licensed under the MIT License.
-
-namespace HaveIBeenPwned.Client;
-
-internal sealed partial class DefaultPwnedClient : IPwnedClient
-{
- ///
- async Task IPwnedPastesClient.GetPastesAsync(string account)
- {
- if (string.IsNullOrWhiteSpace(account))
- {
- throw new ArgumentException(
- "The account cannot be either null, empty or whitespace.", nameof(account));
- }
-
- try
- {
- var client = httpClientFactory.CreateClient(HibpClient);
- var pastes =
- await client.GetFromJsonAsync(
- $"pasteaccount/{HttpUtility.UrlEncode(account)}",
- SourceGeneratorContext.Default.PastesArray);
-
- return pastes ?? [];
- }
- catch (Exception ex)
- {
- logger.LogError(ex, "{ExceptionMessage}", ex.Message);
-
- return [];
- }
- }
-
- ///
- IAsyncEnumerable IPwnedPastesClient.GetPastesAsAsyncEnumerable(
- string account, CancellationToken cancellationToken)
- {
- if (string.IsNullOrWhiteSpace(account))
- {
- throw new ArgumentException(
- "The account cannot be either null, empty or whitespace.", nameof(account));
- }
-
- try
- {
- var client = httpClientFactory.CreateClient(HibpClient);
-
- return client.GetFromJsonAsAsyncEnumerable(
- $"pasteaccount/{HttpUtility.UrlEncode(account)}",
- SourceGeneratorContext.Default.Pastes,
- cancellationToken: cancellationToken);
- }
- catch (Exception ex)
- {
- logger.LogError(ex, "{ExceptionMessage}", ex.Message);
-
- return AsyncEnumerable.Empty();
- }
- }
-}
+// Copyright (c) David Pine. All rights reserved.
+// Licensed under the MIT License.
+
+namespace HaveIBeenPwned.Client;
+
+internal sealed partial class DefaultPwnedClient : IPwnedClient
+{
+ ///
+ async Task IPwnedPastesClient.GetPastesAsync(string account)
+ {
+ if (string.IsNullOrWhiteSpace(account))
+ {
+ throw new ArgumentException(
+ "The account cannot be either null, empty or whitespace.", nameof(account));
+ }
+
+ try
+ {
+ var client = httpClientFactory.CreateClient(HibpClient);
+ var pastes =
+ await client.GetFromJsonAsync(
+ $"pasteaccount/{HttpUtility.UrlEncode(account)}",
+ SourceGeneratorContext.Default.PastesArray);
+
+ return pastes ?? [];
+ }
+ catch (Exception ex)
+ {
+ logger.LogError(ex, "{ExceptionMessage}", ex.Message);
+
+ return [];
+ }
+ }
+
+ ///
+ IAsyncEnumerable IPwnedPastesClient.GetPastesAsAsyncEnumerable(
+ string account, CancellationToken cancellationToken)
+ {
+ if (string.IsNullOrWhiteSpace(account))
+ {
+ throw new ArgumentException(
+ "The account cannot be either null, empty or whitespace.", nameof(account));
+ }
+
+ try
+ {
+ var client = httpClientFactory.CreateClient(HibpClient);
+
+ return client.GetFromJsonAsAsyncEnumerable(
+ $"pasteaccount/{HttpUtility.UrlEncode(account)}",
+ SourceGeneratorContext.Default.Pastes,
+ cancellationToken: cancellationToken);
+ }
+ catch (Exception ex)
+ {
+ logger.LogError(ex, "{ExceptionMessage}", ex.Message);
+
+ return AsyncEnumerable.Empty();
+ }
+ }
+}
diff --git a/src/HaveIBeenPwned.Client/DefaultPwnedClient.cs b/src/HaveIBeenPwned.Client/DefaultPwnedClient.cs
index ff3ab7a..a544e1b 100644
--- a/src/HaveIBeenPwned.Client/DefaultPwnedClient.cs
+++ b/src/HaveIBeenPwned.Client/DefaultPwnedClient.cs
@@ -1,11 +1,11 @@
-// Copyright (c) David Pine. All rights reserved.
-// Licensed under the MIT License.
-
-namespace HaveIBeenPwned.Client;
-
-///
-internal sealed partial class DefaultPwnedClient(
- IHttpClientFactory httpClientFactory,
- ILogger logger) : IPwnedClient
-{
-}
+// Copyright (c) David Pine. All rights reserved.
+// Licensed under the MIT License.
+
+namespace HaveIBeenPwned.Client;
+
+///
+internal sealed partial class DefaultPwnedClient(
+ IHttpClientFactory httpClientFactory,
+ ILogger logger) : IPwnedClient
+{
+}
diff --git a/src/HaveIBeenPwned.Client/Extensions/PwnedClientExtensions.cs b/src/HaveIBeenPwned.Client/Extensions/PwnedClientExtensions.cs
index 43ad276..0744aec 100644
--- a/src/HaveIBeenPwned.Client/Extensions/PwnedClientExtensions.cs
+++ b/src/HaveIBeenPwned.Client/Extensions/PwnedClientExtensions.cs
@@ -1,76 +1,76 @@
-// Copyright (c) David Pine. All rights reserved.
-// Licensed under the MIT License.
-
-namespace HaveIBeenPwned.Client;
-
-///
-public static class PwnedClientExtensions
-{
- ///
- /// An extension method that evaluates whether the is "pwned".
- /// When true, the Count is at least 1.
- ///
- ///
- ///
- ///
- ///
- /// -
- /// When the given is "pwned", returns (true, 3) when "pwned" three times.
- ///
- /// -
- /// When the given isn't "pwned", this could return (false, 0).
- ///
- /// -
- /// When unable to determine, returns (null, null).
- ///
- ///
- ///
- public static async Task<(bool? IsPwned, long? Count)> IsPasswordPwnedAsync(
- this IPwnedPasswordsClient pwnedPasswordsClient, string plainTextPassword)
- {
- var pwnedPassword = await pwnedPasswordsClient.GetPwnedPasswordAsync(plainTextPassword);
-
- return
- (
- IsPwned: pwnedPassword.IsPwned ?? false,
- Count: pwnedPassword.PwnedCount
- );
- }
-
- ///
- /// An extension method that evaluates whether the is part of a breach.
- /// When true, the Breaches has at least one breach name.
- ///
- ///
- ///
- ///
- ///
- /// -
- /// When the given is part of a breach, returns
- /// (true, ["Adobe", "LinkedIn"]) when the found in the Adobe and LinkedIn breaches.
- ///
- /// -
- /// When the given isn't part of a breach, returns (false, []).
- ///
- /// -
- /// When unable to determine, returns (null, null).
- ///
- ///
- ///
- public static async Task<(bool? IsBreached, string[]? Breaches)> IsBreachedAccountAsync(
- this IPwnedBreachesClient pwnedBreachesClient, string account)
- {
- if (string.IsNullOrWhiteSpace(account))
- {
- return (null, null);
- }
-
- var breaches = await pwnedBreachesClient.GetBreachHeadersForAccountAsync(account);
-
- return
- (
- IsBreached: breaches is { Length: > 0 },
- Breaches: breaches?.Select(breach => breach.Name)?.ToArray() ?? []
- );
- }
-}
+// Copyright (c) David Pine. All rights reserved.
+// Licensed under the MIT License.
+
+namespace HaveIBeenPwned.Client;
+
+///
+public static class PwnedClientExtensions
+{
+ ///
+ /// An extension method that evaluates whether the is "pwned".
+ /// When true, the Count is at least 1.
+ ///
+ ///
+ ///
+ ///
+ ///
+ /// -
+ /// When the given is "pwned", returns (true, 3) when "pwned" three times.
+ ///
+ /// -
+ /// When the given isn't "pwned", this could return (false, 0).
+ ///
+ /// -
+ /// When unable to determine, returns (null, null).
+ ///
+ ///
+ ///
+ public static async Task<(bool? IsPwned, long? Count)> IsPasswordPwnedAsync(
+ this IPwnedPasswordsClient pwnedPasswordsClient, string plainTextPassword)
+ {
+ var pwnedPassword = await pwnedPasswordsClient.GetPwnedPasswordAsync(plainTextPassword);
+
+ return
+ (
+ IsPwned: pwnedPassword.IsPwned ?? false,
+ Count: pwnedPassword.PwnedCount
+ );
+ }
+
+ ///
+ /// An extension method that evaluates whether the is part of a breach.
+ /// When true, the Breaches has at least one breach name.
+ ///
+ ///
+ ///
+ ///
+ ///
+ /// -
+ /// When the given is part of a breach, returns
+ /// (true, ["Adobe", "LinkedIn"]) when the found in the Adobe and LinkedIn breaches.
+ ///
+ /// -
+ /// When the given isn't part of a breach, returns (false, []).
+ ///
+ /// -
+ /// When unable to determine, returns (null, null).
+ ///
+ ///
+ ///
+ public static async Task<(bool? IsBreached, string[]? Breaches)> IsBreachedAccountAsync(
+ this IPwnedBreachesClient pwnedBreachesClient, string account)
+ {
+ if (string.IsNullOrWhiteSpace(account))
+ {
+ return (null, null);
+ }
+
+ var breaches = await pwnedBreachesClient.GetBreachHeadersForAccountAsync(account);
+
+ return
+ (
+ IsBreached: breaches is { Length: > 0 },
+ Breaches: breaches?.Select(breach => breach.Name)?.ToArray() ?? []
+ );
+ }
+}
diff --git a/src/HaveIBeenPwned.Client/Extensions/PwnedClientServiceCollectionExtensions.cs b/src/HaveIBeenPwned.Client/Extensions/PwnedClientServiceCollectionExtensions.cs
index 3d66631..2555329 100644
--- a/src/HaveIBeenPwned.Client/Extensions/PwnedClientServiceCollectionExtensions.cs
+++ b/src/HaveIBeenPwned.Client/Extensions/PwnedClientServiceCollectionExtensions.cs
@@ -1,106 +1,104 @@
-// Copyright (c) David Pine. All rights reserved.
-// Licensed under the MIT License.
-
-using System.Diagnostics.CodeAnalysis;
-
-namespace Microsoft.Extensions.DependencyInjection;
-
-///
-/// Extension methods for setting up pwned client related services in an .
-///
-public static class PwnedClientServiceCollectionExtensions
-{
- ///
- /// Adds all of the necessary Pwned service functionality to
- /// the collection for dependency injection.
- ///
- /// The service collection to add services to.
- /// The name configuration section to bind options from.
- /// The same instance with other services added.
- ///
- /// If either the or are .
- ///
- public static IServiceCollection AddPwnedServices(
- this IServiceCollection services,
- IConfiguration namedConfigurationSection)
- {
- ArgumentNullException.ThrowIfNull(services);
- ArgumentNullException.ThrowIfNull(namedConfigurationSection);
-
- services.Configure(namedConfigurationSection);
-
- return AddPwnedServices(services);
- }
-
- ///
- /// Adds all of the necessary Pwned service functionality to
- /// the collection for dependency injection.
- ///
- /// The service collection to add services to.
- /// The action used to configure options.
- /// The same instance with other services added.
- ///
- /// If either the or are .
- ///
- public static IServiceCollection AddPwnedServices(
- this IServiceCollection services,
- Action configureOptions)
- {
- ArgumentNullException.ThrowIfNull(services);
- ArgumentNullException.ThrowIfNull(configureOptions);
-
- services.Configure(configureOptions);
-
- return AddPwnedServices(services);
- }
-
- static IServiceCollection AddPwnedServices(IServiceCollection services)
- {
- services.AddLogging();
- services.AddOptions();
-
- _ = AddPwnedHttpClient(
- services,
- HttpClientNames.HibpClient,
- HttpClientUrls.HibpApiUrl);
-
- _ = AddPwnedHttpClient(
- services,
- HttpClientNames.PasswordsClient,
- HttpClientUrls.PasswordsApiUrl,
- isPlainText: true);
-
- services.AddSingleton();
- services.AddSingleton();
- services.AddSingleton();
- services.AddSingleton();
-
- return services;
- }
-
- static IHttpClientBuilder AddPwnedHttpClient(
- IServiceCollection services,
- string httpClientName,
- string baseAddress,
- bool isPlainText = false) =>
- services.AddHttpClient(
- httpClientName,
- (serviceProvider, client) =>
- {
- var options = serviceProvider.GetRequiredService>();
-
- var (apiKey, userAgent, _) = options?.CurrentValue
- ?? throw new InvalidOperationException(
- "The 'Have I Been Pwned' options object cannot be null.");
-
- client.BaseAddress = new(baseAddress);
- client.DefaultRequestHeaders.Add(HttpHeaderNames.HibpApiKey, apiKey);
- client.DefaultRequestHeaders.UserAgent.ParseAdd(userAgent);
-
- if (isPlainText)
- {
- client.DefaultRequestHeaders.Accept.Add(
- new(MediaTypeNames.Text.Plain));
- }
- });
-}
+// Copyright (c) David Pine. All rights reserved.
+// Licensed under the MIT License.
+
+namespace Microsoft.Extensions.DependencyInjection;
+
+///
+/// Extension methods for setting up pwned client related services in an .
+///
+public static class PwnedClientServiceCollectionExtensions
+{
+ ///
+ /// Adds all of the necessary Pwned service functionality to
+ /// the collection for dependency injection.
+ ///
+ /// The service collection to add services to.
+ /// The name configuration section to bind options from.
+ /// The same instance with other services added.
+ ///
+ /// If either the or are .
+ ///
+ public static IServiceCollection AddPwnedServices(
+ this IServiceCollection services,
+ IConfiguration namedConfigurationSection)
+ {
+ ArgumentNullException.ThrowIfNull(services);
+ ArgumentNullException.ThrowIfNull(namedConfigurationSection);
+
+ services.Configure(namedConfigurationSection);
+
+ return AddPwnedServices(services);
+ }
+
+ ///
+ /// Adds all of the necessary Pwned service functionality to
+ /// the collection for dependency injection.
+ ///
+ /// The service collection to add services to.
+ /// The action used to configure options.
+ /// The same instance with other services added.
+ ///
+ /// If either the or are .
+ ///
+ public static IServiceCollection AddPwnedServices(
+ this IServiceCollection services,
+ Action configureOptions)
+ {
+ ArgumentNullException.ThrowIfNull(services);
+ ArgumentNullException.ThrowIfNull(configureOptions);
+
+ services.Configure(configureOptions);
+
+ return AddPwnedServices(services);
+ }
+
+ static IServiceCollection AddPwnedServices(IServiceCollection services)
+ {
+ services.AddLogging();
+ services.AddOptions();
+
+ _ = AddPwnedHttpClient(
+ services,
+ HttpClientNames.HibpClient,
+ HttpClientUrls.HibpApiUrl);
+
+ _ = AddPwnedHttpClient(
+ services,
+ HttpClientNames.PasswordsClient,
+ HttpClientUrls.PasswordsApiUrl,
+ isPlainText: true);
+
+ services.AddSingleton();
+ services.AddSingleton();
+ services.AddSingleton();
+ services.AddSingleton();
+
+ return services;
+ }
+
+ static IHttpClientBuilder AddPwnedHttpClient(
+ IServiceCollection services,
+ string httpClientName,
+ string baseAddress,
+ bool isPlainText = false) =>
+ services.AddHttpClient(
+ httpClientName,
+ (serviceProvider, client) =>
+ {
+ var options = serviceProvider.GetRequiredService>();
+
+ var (apiKey, userAgent, _) = options?.CurrentValue
+ ?? throw new InvalidOperationException(
+ "The 'Have I Been Pwned' options object cannot be null.");
+
+ client.BaseAddress = new(baseAddress);
+ client.DefaultRequestHeaders.Add(HttpHeaderNames.HibpApiKey, apiKey);
+ client.DefaultRequestHeaders.UserAgent.ParseAdd(userAgent);
+
+ if (isPlainText)
+ {
+ client.DefaultRequestHeaders.Accept.Add(
+ new(MediaTypeNames.Text.Plain));
+ }
+ });
+}
diff --git a/src/HaveIBeenPwned.Client/Extensions/StringExtensions.cs b/src/HaveIBeenPwned.Client/Extensions/StringExtensions.cs
index 42862c0..fd86fa9 100644
--- a/src/HaveIBeenPwned.Client/Extensions/StringExtensions.cs
+++ b/src/HaveIBeenPwned.Client/Extensions/StringExtensions.cs
@@ -1,29 +1,29 @@
-// Copyright (c) David Pine. All rights reserved.
-// Licensed under the MIT License.
-
-namespace HaveIBeenPwned.Client.Extensions;
-
-static class StringExtensions
-{
- ///
- /// Converts the string into a
- /// computed equivalent.
- ///
- internal static string? ToSha1Hash(this string? value)
- {
- if (value is null or { Length: 0 })
- {
- return value;
- }
-
- var hash = SHA1.HashData(Encoding.UTF8.GetBytes(value));
- StringBuilder stringBuilder = new(hash.Length * 2);
-
- foreach (var b in hash)
- {
- stringBuilder.Append(b.ToString("x2"));
- }
-
- return stringBuilder.ToString();
- }
-}
+// Copyright (c) David Pine. All rights reserved.
+// Licensed under the MIT License.
+
+namespace HaveIBeenPwned.Client.Extensions;
+
+static class StringExtensions
+{
+ ///
+ /// Converts the string into a
+ /// computed equivalent.
+ ///
+ internal static string? ToSha1Hash(this string? value)
+ {
+ if (value is null or { Length: 0 })
+ {
+ return value;
+ }
+
+ var hash = SHA1.HashData(Encoding.UTF8.GetBytes(value));
+ StringBuilder stringBuilder = new(hash.Length * 2);
+
+ foreach (var b in hash)
+ {
+ stringBuilder.Append(b.ToString("x2"));
+ }
+
+ return stringBuilder.ToString();
+ }
+}
diff --git a/src/HaveIBeenPwned.Client/GlobalUsings.cs b/src/HaveIBeenPwned.Client/GlobalUsings.cs
index 6176e7c..df1339d 100644
--- a/src/HaveIBeenPwned.Client/GlobalUsings.cs
+++ b/src/HaveIBeenPwned.Client/GlobalUsings.cs
@@ -1,26 +1,27 @@
-// Copyright (c) David Pine. All rights reserved.
-// Licensed under the MIT License.
-
-global using System.Net.Http.Json;
-global using System.Net.Mime;
-global using System.Reflection;
-global using System.Security.Cryptography;
-global using System.Text;
-global using System.Web;
-
-global using HaveIBeenPwned.Client;
-global using HaveIBeenPwned.Client.Abstractions;
-global using HaveIBeenPwned.Client.Abstractions.Serialization;
-global using HaveIBeenPwned.Client.Extensions;
-global using HaveIBeenPwned.Client.Factories;
-global using HaveIBeenPwned.Client.Http;
-global using HaveIBeenPwned.Client.Internals;
-global using HaveIBeenPwned.Client.Options;
-
-global using Microsoft.Extensions.Configuration;
-global using Microsoft.Extensions.DependencyInjection;
-global using Microsoft.Extensions.Logging;
-global using Microsoft.Extensions.Logging.Abstractions;
-global using Microsoft.Extensions.Options;
-
+// Copyright (c) David Pine. All rights reserved.
+// Licensed under the MIT License.
+
+global using System.Collections.Frozen;
+global using System.Net.Http.Json;
+global using System.Net.Mime;
+global using System.Reflection;
+global using System.Security.Cryptography;
+global using System.Text;
+global using System.Web;
+
+global using HaveIBeenPwned.Client;
+global using HaveIBeenPwned.Client.Abstractions;
+global using HaveIBeenPwned.Client.Abstractions.Serialization;
+global using HaveIBeenPwned.Client.Extensions;
+global using HaveIBeenPwned.Client.Factories;
+global using HaveIBeenPwned.Client.Http;
+global using HaveIBeenPwned.Client.Internals;
+global using HaveIBeenPwned.Client.Options;
+
+global using Microsoft.Extensions.Configuration;
+global using Microsoft.Extensions.DependencyInjection;
+global using Microsoft.Extensions.Logging;
+global using Microsoft.Extensions.Logging.Abstractions;
+global using Microsoft.Extensions.Options;
+
global using static HaveIBeenPwned.Client.Http.HttpClientNames;
\ No newline at end of file
diff --git a/src/HaveIBeenPwned.Client/Http/HttpClientNames.cs b/src/HaveIBeenPwned.Client/Http/HttpClientNames.cs
index 44bedf2..d1c1680 100644
--- a/src/HaveIBeenPwned.Client/Http/HttpClientNames.cs
+++ b/src/HaveIBeenPwned.Client/Http/HttpClientNames.cs
@@ -1,17 +1,17 @@
-// Copyright (c) David Pine. All rights reserved.
-// Licensed under the MIT License.
-
-namespace HaveIBeenPwned.Client.Http;
-
-static class HttpClientNames
-{
- ///
- /// HTTP client name corresponding to the .
- ///
- internal const string HibpClient = nameof(HibpClient);
-
- ///
- /// HTTP client name corresponding to the .
- ///
- internal const string PasswordsClient = nameof(PasswordsClient);
-}
+// Copyright (c) David Pine. All rights reserved.
+// Licensed under the MIT License.
+
+namespace HaveIBeenPwned.Client.Http;
+
+static class HttpClientNames
+{
+ ///
+ /// HTTP client name corresponding to the .
+ ///
+ internal const string HibpClient = nameof(HibpClient);
+
+ ///
+ /// HTTP client name corresponding to the .
+ ///
+ internal const string PasswordsClient = nameof(PasswordsClient);
+}
diff --git a/src/HaveIBeenPwned.Client/Http/HttpClientUrls.cs b/src/HaveIBeenPwned.Client/Http/HttpClientUrls.cs
index 018c17d..48b2c27 100644
--- a/src/HaveIBeenPwned.Client/Http/HttpClientUrls.cs
+++ b/src/HaveIBeenPwned.Client/Http/HttpClientUrls.cs
@@ -1,11 +1,11 @@
-// Copyright (c) David Pine. All rights reserved.
-// Licensed under the MIT License.
-
-namespace HaveIBeenPwned.Client.Http;
-
-static class HttpClientUrls
-{
- internal const string HibpApiUrl = "https://haveibeenpwned.com/api/v3/";
-
- internal const string PasswordsApiUrl = "https://api.pwnedpasswords.com/";
-}
+// Copyright (c) David Pine. All rights reserved.
+// Licensed under the MIT License.
+
+namespace HaveIBeenPwned.Client.Http;
+
+static class HttpClientUrls
+{
+ internal const string HibpApiUrl = "https://haveibeenpwned.com/api/v3/";
+
+ internal const string PasswordsApiUrl = "https://api.pwnedpasswords.com/";
+}
diff --git a/src/HaveIBeenPwned.Client/Internals/Visibility.cs b/src/HaveIBeenPwned.Client/Internals/Visibility.cs
index 73b83d6..f67fb18 100644
--- a/src/HaveIBeenPwned.Client/Internals/Visibility.cs
+++ b/src/HaveIBeenPwned.Client/Internals/Visibility.cs
@@ -1,8 +1,8 @@
-// Copyright (c) David Pine. All rights reserved.
-// Licensed under the MIT License.
-
-using System.Runtime.CompilerServices;
-
-[assembly: InternalsVisibleTo("HaveIBeenPwned.ClientTests")]
-[assembly: InternalsVisibleTo("HaveIBeenPwned.Client.PollyExtensions")]
+// Copyright (c) David Pine. All rights reserved.
+// Licensed under the MIT License.
+
+using System.Runtime.CompilerServices;
+
+[assembly: InternalsVisibleTo("HaveIBeenPwned.ClientTests")]
+[assembly: InternalsVisibleTo("HaveIBeenPwned.Client.PollyExtensions")]
[assembly: InternalsVisibleTo("HaveIBeenPwned.Client.PollyExtensionsTests")]
\ No newline at end of file
diff --git a/src/HaveIBeenPwned.Client/Options/HibpOptions.cs b/src/HaveIBeenPwned.Client/Options/HibpOptions.cs
index 3404f4b..05f148e 100644
--- a/src/HaveIBeenPwned.Client/Options/HibpOptions.cs
+++ b/src/HaveIBeenPwned.Client/Options/HibpOptions.cs
@@ -1,51 +1,51 @@
-// Copyright (c) David Pine. All rights reserved.
-// Licensed under the MIT License.
-
-namespace HaveIBeenPwned.Client.Options;
-
-///
-/// The "Have I Been Pwned" API options object.
-/// See
-///
-public sealed class HibpOptions : IOptions
-{
- static readonly string LibraryVersion =
- typeof(HibpOptions).Assembly
- .GetCustomAttribute()
- ?.Version ?? "1.0";
-
- internal static readonly string DefaultUserAgent =
- $".NET HIBP Client/{LibraryVersion}";
-
- private string? _userAgent = DefaultUserAgent;
-
- ///
- /// Gets or sets the API key, used to authorize HTTP calls to HIBP.
- /// See
- ///
- public string ApiKey { get; set; } = null!;
-
- ///
- /// Gets or sets the HTTP header value for User-Agent. Defaults to .NET HIBP Client/1.0.
- /// See
- ///
- public string UserAgent
- {
- get => _userAgent ?? DefaultUserAgent;
- set => _userAgent = value ?? DefaultUserAgent;
- }
-
- ///
- /// Gets or sets the subscription level for the "Have I Been Pwned" API.
- /// The subscription level impacts rate limiting, and this setting can be
- /// used to employ a rate-limit aware HTTP resilience strategy when using:
- /// .
- ///
- public HibpSubscriptionLevel? SubscriptionLevel { get; set;}
-
- ///
- HibpOptions IOptions.Value => this;
-
- internal void Deconstruct(out string apiKey, out string userAgent, out HibpSubscriptionLevel? subscriptionLevel) =>
- (apiKey, userAgent, subscriptionLevel) = (ApiKey, UserAgent, SubscriptionLevel);
-}
+// Copyright (c) David Pine. All rights reserved.
+// Licensed under the MIT License.
+
+namespace HaveIBeenPwned.Client.Options;
+
+///
+/// The "Have I Been Pwned" API options object.
+/// See
+///
+public sealed class HibpOptions : IOptions
+{
+ static readonly string LibraryVersion =
+ typeof(HibpOptions).Assembly
+ .GetCustomAttribute()
+ ?.Version ?? "1.0";
+
+ internal static readonly string DefaultUserAgent =
+ $".NET HIBP Client/{LibraryVersion}";
+
+ private string? _userAgent = DefaultUserAgent;
+
+ ///
+ /// Gets or sets the API key, used to authorize HTTP calls to HIBP.
+ /// See
+ ///
+ public string ApiKey { get; set; } = null!;
+
+ ///
+ /// Gets or sets the HTTP header value for User-Agent. Defaults to .NET HIBP Client/1.0.
+ /// See
+ ///
+ public string UserAgent
+ {
+ get => _userAgent ?? DefaultUserAgent;
+ set => _userAgent = value ?? DefaultUserAgent;
+ }
+
+ ///
+ /// Gets or sets the subscription level for the "Have I Been Pwned" API.
+ /// The subscription level impacts rate limiting, and this setting can be
+ /// used to employ a rate-limit aware HTTP resilience strategy when using:
+ /// .
+ ///
+ public HibpSubscriptionLevel? SubscriptionLevel { get; set; }
+
+ ///
+ HibpOptions IOptions.Value => this;
+
+ internal void Deconstruct(out string apiKey, out string userAgent, out HibpSubscriptionLevel? subscriptionLevel) =>
+ (apiKey, userAgent, subscriptionLevel) = (ApiKey, UserAgent, SubscriptionLevel);
+}