Skip to content

Conversation

@Jaben
Copy link
Member

@Jaben Jaben commented Oct 17, 2025

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 (like .Bind()).

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 (all 9 tests passing ✅)
  • README.md: Fixed all configuration examples to show correct patterns

Usage Example

// Hybrid configuration (appsettings.json + 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
  • ✅ Solution builds without errors
  • ✅ New tests cover hybrid configuration scenarios
  • ✅ Tests validate PostConfigure overrides work correctly
  • ✅ Tests validate incomplete auth fails as expected

Breaking Changes

None - this is a bug fix that makes the existing hybrid configuration pattern work correctly.

Checklist

  • Tests added/updated
  • Documentation updated (README.md)
  • All tests passing
  • Code builds successfully
  • Follows gitflow (feature branch from develop)

Summary by CodeRabbit

  • Documentation

    • README updated with hybrid configuration examples showing binding + PostConfigure usage for flexible setup.
  • New Features

    • Added multiple DI registration overloads for the client, including generic options-based and service-provider HttpClient customization.
    • Configuration now supports programmatic overrides applied after binding (hybrid Configure + PostConfigure pattern).
  • Tests

    • Added tests covering hybrid configuration precedence, authentication validation, and error handling.

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
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Oct 17, 2025

Caution

Review failed

The pull request is closed.

Walkthrough

Refactors client registration to use the Options pattern and DI-resolved options (IOptions) with new AddGotenbergSharpClient overloads (parameterless, generic, and configureClient variants). README examples and tests updated to show binding + PostConfigure hybrid configuration and to validate credential/override behaviors.

Changes

Cohort / File(s) Summary
Documentation
README.md
Replaced inline AddGotenbergSharpClient(options => ...) examples with AddOptions<GotenbergSharpClientOptions>().Configure(...)/PostConfigure(...) and a parameterless AddGotenbergSharpClient() usage; updated retry type reference to RetryOptions.
Registration Extension API
src/Gotenberg.Sharp.Api.Client/Extensions/TypedClientServiceCollectionExtensions.cs
Replaced old overload with four new overloads: AddGotenbergSharpClient(), AddGotenbergSharpClient<TOptions>(), and both variants accepting Action<IServiceProvider, HttpClient> configureClient. Options are now resolved from DI via IOptions<TOptions>; removed local GetOptions helper and prior inline configure path.
Tests
test/GotenbergSharpClient.Tests/BasicAuthTests.cs
Added tests for hybrid configuration overrides, live-auth hybrid scenario, failure on incomplete BasicAuth, and programmatic-only configuration to validate precedence and option binding behavior.

Sequence Diagram(s)

sequenceDiagram
    autonumber
    participant Startup as Startup
    participant DI as DI Container
    participant Options as IOptions<TOptions>
    participant Builder as IHttpClientBuilder
    participant Handler as HttpMessageHandler
    participant Client as GotenbergSharpClient

    Startup->>DI: AddOptions<TOptions>().Configure(config)
    Note right of DI #DFF3E0: appsettings binding
    Startup->>DI: AddOptions<TOptions>().PostConfigure(override)
    Note right of DI #FFF4D6: programmatic overrides applied
    Startup->>Builder: AddGotenbergSharpClient() / AddGotenbergSharpClient<TOptions>()
    Builder->>DI: Resolve IOptions<TOptions>
    DI-->>Builder: return combined options
    Builder->>Handler: create/configure handler (BasicAuth, Timeout, Retry)
    Handler->>Client: attach handler
    Client-->>Startup: Typed client registered
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Poem

🐰
I hopped through options, bindings spun,
PostConfigure fixed what's undone,
DI now holds each overridden part,
BasicAuth sings — no broken heart,
Cheers from a rabbit, code done! 🥕

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 44.44% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title Check ✅ Passed The pull request title "Fix hybrid configuration for basic authentication credentials" is specific and clearly describes the primary change. The changeset demonstrates a comprehensive fix for issue #67, where the service registration API has been refactored to use standard .NET options patterns with PostConfigure support, the README has been updated with correct hybrid configuration examples, and new tests validate the fix. The title accurately captures the main objective and is concise without unnecessary noise.
Linked Issues Check ✅ Passed The PR successfully addresses all objectives from issue #67. It enables hybrid configuration by refactoring TypedClientServiceCollectionExtensions.cs to use the standard .NET options pattern with IOptions and PostConfigure support instead of the previous custom helper [#67]. The README.md is updated with examples demonstrating the correct hybrid configuration pattern using AddOptions<>().Configure(...) followed by PostConfigure(...) [#67]. BasicAuthTests.cs is expanded with four new tests validating hybrid configuration scenarios, programmatic-only configuration, and error handling, ensuring BasicAuthUsername and BasicAuthPassword overrides work correctly [#67]. The changes ensure the service provider's options reflect overridden values through standard .NET patterns.
Out of Scope Changes Check ✅ Passed All changes in the PR are directly scoped to fixing hybrid configuration for basic authentication as defined in issue #67. The RefactoringTypeRegistry in TypedClientServiceCollectionExtensions.cs to support the options pattern and new AddGotenbergSharpClient overloads are necessary architectural changes to enable PostConfigure-based overrides. The README updates provide required documentation, and the four new test methods in BasicAuthTests.cs validate the fix directly. No unrelated improvements, cosmetic refactorings, or out-of-scope features appear to have been mixed into this changeset.

📜 Recent review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 39ec673 and c357e54.

📒 Files selected for processing (1)
  • README.md (2 hunks)

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (5)
README.md (1)

133-145: Guard env-var overrides to avoid incomplete auth exceptions

If either GOTENBERG_USER or GOTENBERG_PASS is missing, the runtime validation will throw InvalidOperationException. Suggest guarding to only set both when present.

-            .PostConfigure(options =>
-            {
-                // Override or add settings programmatically (runs after binding)
-                options.TimeOut = TimeSpan.FromMinutes(10); // Override timeout
-                options.BasicAuthUsername = Environment.GetEnvironmentVariable("GOTENBERG_USER");
-                options.BasicAuthPassword = Environment.GetEnvironmentVariable("GOTENBERG_PASS");
-            });
+            .PostConfigure(options =>
+            {
+                // Override or add settings programmatically (runs after binding)
+                options.TimeOut = TimeSpan.FromMinutes(10);
+                var user = Environment.GetEnvironmentVariable("GOTENBERG_USER");
+                var pass = Environment.GetEnvironmentVariable("GOTENBERG_PASS");
+                if (!string.IsNullOrWhiteSpace(user) && !string.IsNullOrWhiteSpace(pass))
+                {
+                    options.BasicAuthUsername = user;
+                    options.BasicAuthPassword = pass;
+                }
+            });
test/GotenbergSharpClient.Tests/BasicAuthTests.cs (1)

169-207: Gate integration test to avoid CI flakiness

This requires a running Gotenberg with basic auth. Consider marking with [Explicit], a category (e.g., [Category("Integration")]), or a conditional assume/ignore to skip when the service isn’t available.

src/Gotenberg.Sharp.Api.Client/Extensions/TypedClientServiceCollectionExtensions.cs (3)

93-110: Type reference may need namespace import

This file references GotenbergSharpClient (nameof and AddTypedClient) without a using for Gotenberg.Sharp.API.Client. If no global using exists, add:

+using Gotenberg.Sharp.API.Client;

Alternatively, fully qualify the type in nameof and AddTypedClient.


150-156: Add null check for services for consistency

Other overloads guard services; mirror that here.

 public static IHttpClientBuilder AddGotenbergSharpClient(
-    this IServiceCollection services,
+    this IServiceCollection services,
     Action<IServiceProvider, HttpClient> configureClient)
 {
-    return services.AddGotenbergSharpClient<GotenbergSharpClientOptions>(configureClient);
+    if (services == null) throw new ArgumentNullException(nameof(services));
+    return services.AddGotenbergSharpClient<GotenbergSharpClientOptions>(configureClient);
 }

197-244: Consider moving auth validation to options validation

Current validation occurs in the handler factory. You could also add options validation so failures surface earlier and consistently:

services.AddOptions<TOptions>()
    .Validate(o => string.IsNullOrWhiteSpace(o.BasicAuthUsername) == string.IsNullOrWhiteSpace(o.BasicAuthPassword),
              "BasicAuth configuration is incomplete. Both BasicAuthUsername and BasicAuthPassword must be set, or neither should be set.");

Handler-level validation can remain as defense-in-depth.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 5a409f0 and 39ec673.

📒 Files selected for processing (3)
  • README.md (2 hunks)
  • src/Gotenberg.Sharp.Api.Client/Extensions/TypedClientServiceCollectionExtensions.cs (3 hunks)
  • test/GotenbergSharpClient.Tests/BasicAuthTests.cs (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (2)
test/GotenbergSharpClient.Tests/BasicAuthTests.cs (1)
src/Gotenberg.Sharp.Api.Client/Domain/Settings/GotenbergSharpClientOptions.cs (1)
  • GotenbergSharpClientOptions (25-64)
src/Gotenberg.Sharp.Api.Client/Extensions/TypedClientServiceCollectionExtensions.cs (1)
src/Gotenberg.Sharp.Api.Client/Domain/Settings/GotenbergSharpClientOptions.cs (1)
  • GotenbergSharpClientOptions (25-64)
🪛 markdownlint-cli2 (0.18.1)
README.md

133-133: Hard tabs
Column: 1

(MD010, no-hard-tabs)

🔇 Additional comments (5)
README.md (1)

105-123: Programmatic options sample looks correct

Good illustration of options pattern and RetryOptions usage. No issues.

test/GotenbergSharpClient.Tests/BasicAuthTests.cs (3)

131-167: Hybrid PostConfigure test is solid

Validates override precedence correctly via IOptions. LGTM.


209-239: Good negative test for incomplete auth

Asserts the new validation path. Message match is specific; keep it synced with the thrown text.

Ensure the exception message remains unchanged if refactoring; otherwise loosen to Contains("BasicAuth configuration is incomplete").


241-269: Programmatic-only configuration test looks good

Covers non-hybrid path; assertions align with expected values.

src/Gotenberg.Sharp.Api.Client/Extensions/TypedClientServiceCollectionExtensions.cs (1)

56-66: Non-generic overload delegating to default options is clean

Reads options from DI and fails fast if missing. LGTM.

README.md Outdated
.....
services.AddOptions<GotenbergSharpClientOptions>()
.Bind(Configuration.GetSection(nameof(GotenbergSharpClient)));
.Bind(Configuration.GetSection(nameof(GotenbergSharpClient)))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Fix markdownlint MD010: replace hard tabs with spaces

Line 133 uses a hard tab; replace with spaces to satisfy markdownlint.

Based on static analysis hints

🧰 Tools
🪛 markdownlint-cli2 (0.18.1)

133-133: Hard tabs
Column: 1

(MD010, no-hard-tabs)

🤖 Prompt for AI Agents
README.md around line 133: the line contains a hard tab character which violates
markdownlint MD010; replace the tab with spaces (preferably 2 or 4 spaces
consistent with the file) so the indentation uses spaces instead of tabs and
re-run linting to confirm the MD010 issue is resolved.

@Jaben Jaben merged commit a3a05ef into develop Oct 18, 2025
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Hybrid configuration for basic auth not working

2 participants