Skip to content

Commit 39ec673

Browse files
committed
Fix hybrid configuration for basic authentication credentials
Fixes #67 ## Problem When using hybrid configuration (binding from appsettings.json combined with programmatic overrides), basic authentication credentials set programmatically were not being applied to requests. Users expected to be able to override appsettings.json values with programmatic configuration, particularly for sensitive credentials like BasicAuthUsername and BasicAuthPassword. ## Root Cause The service registration API didn't properly support the .NET options pattern for applying configuration overrides. Options need to be configured using PostConfigure to ensure overrides run after all other configuration sources. ## Solution Simplified the API to use standard .NET options configuration patterns: 1. **Removed custom options helper** - Users now use standard `AddOptions<T>()` 2. **Added non-generic overloads** - `AddGotenbergSharpClient()` defaults to `GotenbergSharpClientOptions` for simpler API 3. **Use `GetRequiredService`** - Changed from nullable `GetService` to fail fast if options aren't registered 4. **Updated documentation** - All examples now show proper use of `PostConfigure` for overrides ## Changes - **TypedClientServiceCollectionExtensions.cs**: Simplified API, added non-generic overloads, updated XML docs - **BasicAuthTests.cs**: Updated tests to use standard options patterns, added hybrid config tests - **README.md**: Fixed all configuration examples to show correct patterns - **DIExample/Program.cs**: Updated to use simplified non-generic API ## Usage ```csharp // Hybrid configuration (appsettings + programmatic overrides) services.AddOptions<GotenbergSharpClientOptions>() .Bind(Configuration.GetSection("GotenbergSharpClient")) .PostConfigure(options => { // These run AFTER binding, so they override appsettings values options.BasicAuthUsername = Environment.GetEnvironmentVariable("GOTENBERG_USER"); options.BasicAuthPassword = Environment.GetEnvironmentVariable("GOTENBERG_PASS"); }); services.AddGotenbergSharpClient(); ``` ## Testing All 9 BasicAuthTests pass, including new tests for: - Hybrid configuration with PostConfigure overrides - Programmatic-only configuration - Incomplete auth validation
1 parent 5a409f0 commit 39ec673

File tree

3 files changed

+304
-46
lines changed

3 files changed

+304
-46
lines changed

README.md

Lines changed: 26 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -102,21 +102,24 @@ public void ConfigureServices(IServiceCollection services)
102102
{
103103
.....
104104
// Configure with an action
105-
services.AddGotenbergSharpClient(options =>
106-
{
107-
options.ServiceUrl = new Uri("http://localhost:3000");
108-
options.TimeOut = TimeSpan.FromMinutes(5);
109-
options.BasicAuthUsername = "username";
110-
options.BasicAuthPassword = "password";
111-
// Configure retry policy
112-
options.RetryPolicy = new RetryPolicyOptions
105+
services.AddOptions<GotenbergSharpClientOptions>()
106+
.Configure(options =>
113107
{
114-
Enabled = true,
115-
RetryCount = 4,
116-
BackoffPower = 1.5,
117-
LoggingEnabled = true
118-
};
119-
});
108+
options.ServiceUrl = new Uri("http://localhost:3000");
109+
options.TimeOut = TimeSpan.FromMinutes(5);
110+
options.BasicAuthUsername = "username";
111+
options.BasicAuthPassword = "password";
112+
// Configure retry policy
113+
options.RetryPolicy = new RetryOptions
114+
{
115+
Enabled = true,
116+
RetryCount = 4,
117+
BackoffPower = 1.5,
118+
LoggingEnabled = true
119+
};
120+
});
121+
122+
services.AddGotenbergSharpClient();
120123
.....
121124
}
122125
```
@@ -127,15 +130,16 @@ public void ConfigureServices(IServiceCollection services)
127130
{
128131
.....
129132
services.AddOptions<GotenbergSharpClientOptions>()
130-
.Bind(Configuration.GetSection(nameof(GotenbergSharpClient)));
133+
.Bind(Configuration.GetSection(nameof(GotenbergSharpClient)))
134+
.PostConfigure(options =>
135+
{
136+
// Override or add settings programmatically (runs after binding)
137+
options.TimeOut = TimeSpan.FromMinutes(10); // Override timeout
138+
options.BasicAuthUsername = Environment.GetEnvironmentVariable("GOTENBERG_USER");
139+
options.BasicAuthPassword = Environment.GetEnvironmentVariable("GOTENBERG_PASS");
140+
});
131141

132-
// Override or add settings programmatically
133-
services.AddGotenbergSharpClient(options =>
134-
{
135-
options.TimeOut = TimeSpan.FromMinutes(10); // Override timeout
136-
options.BasicAuthUsername = Environment.GetEnvironmentVariable("GOTENBERG_USER");
137-
options.BasicAuthPassword = Environment.GetEnvironmentVariable("GOTENBERG_PASS");
138-
});
142+
services.AddGotenbergSharpClient();
139143
.....
140144
}
141145
```

src/Gotenberg.Sharp.Api.Client/Extensions/TypedClientServiceCollectionExtensions.cs

Lines changed: 138 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -30,31 +30,78 @@ public static class TypedClientServiceCollectionExtensions
3030
{
3131
/// <summary>
3232
/// Registers GotenbergSharpClient with dependency injection using configured options.
33-
/// Configure options via appsettings.json or by calling services.Configure&lt;GotenbergSharpClientOptions&gt;().
3433
/// </summary>
3534
/// <param name="services">The service collection.</param>
36-
/// <param name="configureClientOptions">Optional function to configure client options after they are retrieved.</param>
3735
/// <returns>An IHttpClientBuilder for further configuration.</returns>
3836
/// <exception cref="ArgumentNullException">Thrown when services is null.</exception>
3937
/// <remarks>
40-
/// This method configures the HttpClient with automatic compression, retry policies, and basic authentication if
41-
/// credentials are provided.
42-
/// Options should be configured in the "GotenbergSharpClient" section of appsettings.json or programmatically.
38+
/// <para>
39+
/// This method registers the GotenbergSharpClient with automatic compression, retry policies,
40+
/// and basic authentication if credentials are provided in the options.
41+
/// </para>
42+
/// <para>
43+
/// Options must be registered before calling this method using
44+
/// standard .NET options configuration methods.
45+
/// The client retrieves options from the DI container using <c>IOptions&lt;TOptions&gt;</c>.
46+
/// </para>
47+
/// <para>
48+
/// Example usage:
49+
/// <code>
50+
/// services.AddOptions&lt;GotenbergSharpClientOptions&gt;()
51+
/// .Bind(configuration.GetSection("GotenbergSharpClient"));
52+
/// services.AddGotenbergSharpClient();
53+
/// </code>
54+
/// </para>
4355
/// </remarks>
4456
public static IHttpClientBuilder AddGotenbergSharpClient(
45-
this IServiceCollection services,
46-
Action<GotenbergSharpClientOptions>? configureClientOptions = null)
57+
this IServiceCollection services)
4758
{
4859
if (services == null)
4960
{
5061
throw new ArgumentNullException(nameof(services));
5162
}
5263

53-
return services.AddGotenbergSharpClient((sp, client) =>
64+
return services.AddGotenbergSharpClient<GotenbergSharpClientOptions>();
65+
}
66+
67+
/// <summary>
68+
/// Registers GotenbergSharpClient with dependency injection using configured options.
69+
/// </summary>
70+
/// <typeparam name="TOptions">The options type, must inherit from GotenbergSharpClientOptions.</typeparam>
71+
/// <param name="services">The service collection.</param>
72+
/// <returns>An IHttpClientBuilder for further configuration.</returns>
73+
/// <exception cref="ArgumentNullException">Thrown when services is null.</exception>
74+
/// <remarks>
75+
/// <para>
76+
/// This method registers the GotenbergSharpClient with automatic compression, retry policies,
77+
/// and basic authentication if credentials are provided in the options.
78+
/// </para>
79+
/// <para>
80+
/// Options must be registered before calling this method using
81+
/// standard .NET options configuration methods.
82+
/// The client retrieves options from the DI container using <c>IOptions&lt;TOptions&gt;</c>.
83+
/// </para>
84+
/// <para>
85+
/// Example usage:
86+
/// <code>
87+
/// services.AddOptions&lt;GotenbergSharpClientOptions&gt;()
88+
/// .Bind(configuration.GetSection("GotenbergSharpClient"));
89+
/// services.AddGotenbergSharpClient();
90+
/// </code>
91+
/// </para>
92+
/// </remarks>
93+
public static IHttpClientBuilder AddGotenbergSharpClient<TOptions>(
94+
this IServiceCollection services)
95+
where TOptions : GotenbergSharpClientOptions, new()
96+
{
97+
if (services == null)
5498
{
55-
var ops = GetOptions(sp) ?? new GotenbergSharpClientOptions();
99+
throw new ArgumentNullException(nameof(services));
100+
}
56101

57-
configureClientOptions?.Invoke(ops);
102+
return services.AddGotenbergSharpClient<TOptions>((sp, client) =>
103+
{
104+
var ops = sp.GetRequiredService<IOptions<TOptions>>().Value;
58105

59106
client.Timeout = ops.TimeOut;
60107
client.BaseAddress = ops.ServiceUrl;
@@ -65,18 +112,92 @@ public static IHttpClientBuilder AddGotenbergSharpClient(
65112
/// Registers GotenbergSharpClient with dependency injection using a custom HttpClient configuration.
66113
/// </summary>
67114
/// <param name="services">The service collection.</param>
68-
/// <param name="configureClient">Action to configure the HttpClient instance.</param>
69-
/// <param name="configureClientOptions">Optional function to configure client options after they are retrieved.</param>
115+
/// <param name="configureClient">
116+
/// Action to configure the HttpClient instance. The action receives the service provider and HttpClient
117+
/// for custom configuration.
118+
/// </param>
70119
/// <returns>An IHttpClientBuilder for further configuration.</returns>
71120
/// <exception cref="ArgumentNullException">Thrown when configureClient is null.</exception>
72121
/// <remarks>
73-
/// This overload allows full control over HttpClient configuration. The client is configured with
74-
/// automatic compression, timeout handling, and exponential backoff retry policies.
122+
/// <para>
123+
/// This overload allows full control over HttpClient configuration while still using the options
124+
/// for basic authentication and retry policies. The client is configured with automatic compression,
125+
/// timeout handling, and exponential backoff retry policies based on the registered options.
126+
/// </para>
127+
/// <para>
128+
/// Options must be registered before calling this method using
129+
/// standard .NET options configuration methods.
130+
/// </para>
131+
/// <para>
132+
/// Example usage:
133+
/// <code>
134+
/// services.AddOptions&lt;GotenbergSharpClientOptions&gt;()
135+
/// .Bind(configuration.GetSection("GotenbergSharpClient"))
136+
/// .PostConfigure(options =>
137+
/// {
138+
/// options.BasicAuthUsername = "user";
139+
/// options.BasicAuthPassword = "pass";
140+
/// });
141+
///
142+
/// services.AddGotenbergSharpClient((sp, client) =>
143+
/// {
144+
/// // Custom HttpClient configuration
145+
/// client.DefaultRequestHeaders.Add("X-Custom-Header", "value");
146+
/// });
147+
/// </code>
148+
/// </para>
75149
/// </remarks>
76150
public static IHttpClientBuilder AddGotenbergSharpClient(
77151
this IServiceCollection services,
78-
Action<IServiceProvider, HttpClient> configureClient,
79-
Action<GotenbergSharpClientOptions>? configureClientOptions = null)
152+
Action<IServiceProvider, HttpClient> configureClient)
153+
{
154+
return services.AddGotenbergSharpClient<GotenbergSharpClientOptions>(configureClient);
155+
}
156+
157+
/// <summary>
158+
/// Registers GotenbergSharpClient with dependency injection using a custom HttpClient configuration.
159+
/// </summary>
160+
/// <typeparam name="TOptions">The options type, must inherit from GotenbergSharpClientOptions.</typeparam>
161+
/// <param name="services">The service collection.</param>
162+
/// <param name="configureClient">
163+
/// Action to configure the HttpClient instance. The action receives the service provider and HttpClient
164+
/// for custom configuration.
165+
/// </param>
166+
/// <returns>An IHttpClientBuilder for further configuration.</returns>
167+
/// <exception cref="ArgumentNullException">Thrown when configureClient is null.</exception>
168+
/// <remarks>
169+
/// <para>
170+
/// This overload allows full control over HttpClient configuration while still using the options
171+
/// for basic authentication and retry policies. The client is configured with automatic compression,
172+
/// timeout handling, and exponential backoff retry policies based on the registered options.
173+
/// </para>
174+
/// <para>
175+
/// Options must be registered before calling this method using
176+
/// standard .NET options configuration methods.
177+
/// </para>
178+
/// <para>
179+
/// Example usage:
180+
/// <code>
181+
/// services.AddOptions&lt;GotenbergSharpClientOptions&gt;()
182+
/// .Bind(configuration.GetSection("GotenbergSharpClient"))
183+
/// .PostConfigure(options =>
184+
/// {
185+
/// options.BasicAuthUsername = "user";
186+
/// options.BasicAuthPassword = "pass";
187+
/// });
188+
///
189+
/// services.AddGotenbergSharpClient((sp, client) =>
190+
/// {
191+
/// // Custom HttpClient configuration
192+
/// client.DefaultRequestHeaders.Add("X-Custom-Header", "value");
193+
/// });
194+
/// </code>
195+
/// </para>
196+
/// </remarks>
197+
public static IHttpClientBuilder AddGotenbergSharpClient<TOptions>(
198+
this IServiceCollection services,
199+
Action<IServiceProvider, HttpClient> configureClient)
200+
where TOptions : GotenbergSharpClientOptions, new()
80201
{
81202
if (configureClient == null)
82203
{
@@ -94,9 +215,7 @@ public static IHttpClientBuilder AddGotenbergSharpClient(
94215
}))
95216
.AddHttpMessageHandler(sp =>
96217
{
97-
var ops = GetOptions(sp) ?? new GotenbergSharpClientOptions();
98-
99-
configureClientOptions?.Invoke(ops);
218+
var ops = sp.GetRequiredService<IOptions<TOptions>>().Value;
100219

101220
var hasUsername = !string.IsNullOrWhiteSpace(ops.BasicAuthUsername);
102221
var hasPassword = !string.IsNullOrWhiteSpace(ops.BasicAuthPassword);
@@ -122,9 +241,4 @@ public static IHttpClientBuilder AddGotenbergSharpClient(
122241

123242
return builder;
124243
}
125-
126-
private static GotenbergSharpClientOptions? GetOptions(IServiceProvider sp)
127-
{
128-
return sp.GetService<IOptions<GotenbergSharpClientOptions>>()?.Value;
129-
}
130244
}

0 commit comments

Comments
 (0)