Skip to content

Remove TransactionLocal and SessionSearchPath modes; use QualifiedNames only - #88

Merged
yilmaztayfun merged 2 commits into
masterfrom
claude/npgsql-connection-timeout-mgzobn
Jul 22, 2026
Merged

Remove TransactionLocal and SessionSearchPath modes; use QualifiedNames only#88
yilmaztayfun merged 2 commits into
masterfrom
claude/npgsql-connection-timeout-mgzobn

Conversation

@yilmaztayfun

@yilmaztayfun yilmaztayfun commented Jul 22, 2026

Copy link
Copy Markdown
Contributor

Summary

Simplifies the multi-schema implementation by removing the TransactionLocal and SessionSearchPath switching modes and their associated search_path manipulation logic. Schema targeting now exclusively uses SchemaSwitchingMode.QualifiedNames, where SQL is rewritten to fully-qualified "schema"."table" names at command execution time. This eliminates connection-level state dependencies, making the system compatible with any connection pool topology (including PgBouncer transaction pooling) and allowing non-transactional units of work to leave connection management entirely to EF Core.

Key Changes

  • Removed SearchPathCommandInterceptor: The 217-line interceptor that issued SET LOCAL search_path (TransactionLocal) or session-level SET search_path (SessionSearchPath) is gone.
  • Added QualifiedNamesCommandInterceptor: A simpler 90-line interceptor that only rewrites model placeholders and raw SQL {{schema}} tokens to the quoted bound schema; no async overhead since rewriting is synchronous.
  • Simplified NpgsqlAetherProvider: Removed the SchemaSwitchingMode constructor parameter (now parameterless); always applies qualified-names binding via ApplySchemaBinding().
  • Updated SchemaSwitchingMode enum: Now contains only QualifiedNames as the sole member; the parameter on AddAetherNpgsql is optional and kept for signature compatibility.
  • Enhanced CompositeUnitOfWork documentation: Clarified that transactional roots open a shared connection/transaction lazily, while non-transactional roots never open a connection (EF Core rents pooled connections per operation).
  • Added BuildOwnedOptions() to IAetherDatabaseProvider: Enables non-transactional contexts to bind UseNpgsql(connectionString) directly, letting EF Core manage the connection lifecycle.
  • Updated all documentation: Removed mode-selection guidance; emphasized that qualified names is the only strategy and works with any pool topology.
  • Updated tests: Removed SessionSearchPath test provider; simplified interceptor instantiation in unit tests; added assertions that non-transactional contexts leave connection state to EF Core.

Implementation Details

  • No connection state leakage: Because every command carries its fully-qualified relation names, no search_path state exists on the connection. This is safe on any pooled connection and with PgBouncer transaction pooling.
  • Reduced overhead for non-transactional work: Non-transactional UoWs no longer pin a shared connection; contexts bind the connection string and EF Core rents/returns pooled connections per operation, reducing pool contention.
  • Backward compatibility: The SchemaSwitchingMode parameter on AddAetherNpgsql remains optional for signature compatibility, but only QualifiedNames is supported.

https://claude.ai/code/session_01BKsqFuUQTnEJaJKygkEh2c

Summary by Sourcery

Simplify PostgreSQL multi-schema support to rely solely on qualified-name SQL rewriting and decouple non-transactional units of work from shared connections.

Enhancements:

  • Remove TransactionLocal and SessionSearchPath schema switching modes and the SearchPathCommandInterceptor in favor of a new QualifiedNamesCommandInterceptor that always rewrites SQL to fully-qualified schema names.
  • Update NpgsqlAetherProvider, schema switching configuration, and unit-of-work connection handling so transactional units share a single connection/transaction while non-transactional units let EF Core manage pooled connections per operation.
  • Extend the database provider and DbContext configurator abstractions with BuildOwnedOptions / ApplyOwned to support non-transactional, connection-string-based contexts without changing existing providers.
  • Revise multi-schema, unit-of-work, and adoption documentation to reflect the qualified-names-only strategy, simplified configuration, and pooling-safe behavior under PgBouncer and native pools.

Documentation:

  • Clarify how qualified-name schema targeting works, how transactional vs non-transactional units of work manage connections, and how to configure Aether Npgsql now that schema switching modes and search_path manipulation have been removed.

Tests:

  • Adjust PostgreSQL and infrastructure tests to use QualifiedNamesCommandInterceptor, remove mode-specific SessionSearchPath/TransactionLocal expectations, and add coverage that non-transactional units of work do not hold open connections and that schema binding does not leak across units of work.

Summary by CodeRabbit

  • New Features

    • PostgreSQL multi-schema support now uses qualified names exclusively, improving compatibility with transaction pooling.
    • Non-transactional units of work allow EF Core to manage pooled connections per operation.
    • Runtime schema tokens are safely rewritten and validated during database operations.
  • Documentation

    • Updated multi-schema and unit-of-work guides with the new configuration, usage examples, pooling guidance, and schema-safety behavior.
  • Bug Fixes

    • Prevented schema state from leaking between units of work.
    • Made schema setup resilient when schemas already exist.

claude and others added 2 commits July 22, 2026 14:44
…; reduce SchemaSwitchingMode to QualifiedNames

Non-transactional CompositeUnitOfWork roots no longer create/open a shared
DbConnection. Contexts are built via the new BuildOwnedOptions/ApplyOwned
path bound to the connection string, so EF Core rents a pooled connection
per operation and returns it immediately. This removes hold-for-request
connection pinning that inflated pool demand (Npgsql connect timeouts under
long-polling load). Transactional roots keep the shared connection +
transaction semantics unchanged.

SchemaSwitchingMode now has a single member, QualifiedNames; the
TransactionLocal and SessionSearchPath modes and all search_path
manipulation are removed. SearchPathCommandInterceptor is replaced by
QualifiedNamesCommandInterceptor(schema, currentSchema). NpgsqlAetherProvider
is parameterless; AddAetherNpgsql keeps the optional mode parameter for
signature compatibility and defaults to QualifiedNames.

Tests updated for the new topology (owned connections, qualified-names DDL
placeholder rewriting) and multi-schema/unit-of-work docs rewritten
accordingly.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BKsqFuUQTnEJaJKygkEh2c
@yilmaztayfun
yilmaztayfun requested review from a team July 22, 2026 20:36
@gemini-code-assist

Copy link
Copy Markdown

Caution

The consumer version of Gemini Code Assist on GitHub has been sunset. All code review activity has officially ceased.

@sourcery-ai

sourcery-ai Bot commented Jul 22, 2026

Copy link
Copy Markdown

Reviewer's Guide

Consolidates multi-schema support to a single QualifiedNames strategy, removing all search_path-based modes and interceptors, changing the Npgsql provider and UoW to support either a shared transactional connection or EF Core-owned pooled connections, and updating docs/tests to reflect the new behavior and non-transactional connection management.

Sequence diagram for command execution with QualifiedNamesCommandInterceptor

sequenceDiagram
    participant CS as ICurrentSchema
    participant Ctx as DbContext
    participant INT as QualifiedNamesCommandInterceptor
    participant PG as PostgreSQL

    Ctx->>INT: ReaderExecuting(command,eventData,result)
    INT->>CS: read Name
    INT->>INT: ApplyQualifiedNames(command)
    INT->>INT: PostgreSqlRawSchemaTokenRewriter.RewriteModelPlaceholder(commandText,quotedSchema)
    INT->>INT: PostgreSqlRawSchemaTokenRewriter.Rewrite(commandText,quotedSchema)
    INT-->>Ctx: InterceptionResult
    Ctx->>PG: Execute command.CommandText

    Ctx->>INT: NonQueryExecuting/ScalarExecuting(...)
    INT->>CS: read Name
    INT->>INT: ApplyQualifiedNames(command)
    INT-->>Ctx: InterceptionResult
    Ctx->>PG: Execute command.CommandText
Loading

File-Level Changes

Change Details Files
Replace search_path-based schema switching with a single QualifiedNames interceptor and adjust provider wiring accordingly.
  • Delete SearchPathCommandInterceptor and introduce QualifiedNamesCommandInterceptor that rewrites the model placeholder and {{schema}} tokens to a quoted schema while enforcing current-schema/context alignment.
  • Refactor NpgsqlAetherProvider to be parameterless, always apply the qualified-names model extension, and use the new interceptor for both shared and owned-connection DbContexts.
  • Simplify SchemaSwitchingMode enum to only QualifiedNames and adjust AddAetherNpgsql overload to default/accept only that mode while preserving the parameter for signature compatibility.
framework/src/BBT.Aether.Npgsql/SearchPathCommandInterceptor.cs
framework/src/BBT.Aether.Npgsql/QualifiedNamesCommandInterceptor.cs
framework/src/BBT.Aether.Npgsql/NpgsqlAetherProvider.cs
framework/src/BBT.Aether.Npgsql/BBT/Aether/Uow/EntityFrameworkCore/SchemaSwitchingMode.cs
framework/src/BBT.Aether.Npgsql/Microsoft/Extensions/DependencyInjection/AetherNpgsqlServiceCollectionExtensions.cs
Teach the UnitOfWork and DbContext configurator to support non-transactional contexts that let EF Core own connections instead of using a shared connection.
  • Extend IAetherDbContextConfigurator/IAetherDatabaseProvider with BuildOwnedOptions/ApplyOwned so non-transactional flows bind UseNpgsql(connectionString) and schema-aware options without a shared connection.
  • Modify CompositeUnitOfWork.GetDbContextAsync to branch on effective IsTransactional: transactional roots open and reuse a shared connection/transaction; non-transactional roots never open a connection and instead use BuildOwnedOptions.
  • Clarify XML docs and comments in CompositeUnitOfWork and UoW docs to describe transactional vs non-transactional connection lifecycles and when connections are opened.
framework/src/BBT.Aether.Infrastructure/BBT/Aether/Uow/CompositeUnitOfWork.cs
framework/src/BBT.Aether.Infrastructure/BBT/Aether/Uow/EntityFrameworkCore/IAetherDbContextConfigurator.cs
framework/src/BBT.Aether.Infrastructure/BBT/Aether/Uow/EntityFrameworkCore/AetherDbContextConfigurator.cs
framework/src/BBT.Aether.Infrastructure/BBT/Aether/Uow/EntityFrameworkCore/IAetherDatabaseProvider.cs
framework/docs/unit-of-work/README.md
Align docs with the new single-mode QualifiedNames design and revised connection behavior.
  • Rewrite multi-schema README and IMPLEMENTATION_NOTES to drop TransactionLocal/SessionSearchPath sections, describe QualifiedNames as the only strategy, and document transactional vs non-transactional UoW connection topologies.
  • Update the multi-schema adoption guide (including Turkish text) to remove mode selection guidance, describe that NpgsqlAetherProvider is parameterless, and explain that QualifiedNames is pool-topology-agnostic.
  • Adjust unit-of-work docs to show only AddAetherNpgsql(connectionString) usage and to explain that non-transactional roots hold no physical connection and rely on EF Core for pooling.
framework/docs/multi-schema/README.md
framework/docs/multi-schema/IMPLEMENTATION_NOTES.md
framework/docs/multi-schema/ADOPTION-GUIDE.md
framework/docs/unit-of-work/README.md
Update tests to use the new interceptor, validate qualified-names semantics, and assert non-transactional connection behavior and schema isolation.
  • Refactor QualifiedNamesTests to construct QualifiedNamesCommandInterceptor directly and remove tests specific to other SchemaSwitchingMode values or rejection of {{schema}} outside QualifiedNames.
  • Change UnitOfWorkDisposalTests and MultiSchemaUnitOfWorkTests to assert that non-transactional contexts keep DbConnection closed between operations, reuse the same context per schema, and that schema isolation now depends on QualifiedNames plus current-schema scopes.
  • Update PgBouncerSearchPathTests and various background job/outbox/non-transactional tests to reflect that search_path is never mutated, NpgsqlAetherProvider has no mode parameter, and DDL scripts must explicitly rewrite the placeholder schema to the test schema.
  • Adapt in-memory and lease store tests to implement BuildOwnedOptions and default SchemaSwitchingMode.QualifiedNames where needed.
framework/test/BBT.Aether.Postgres.Tests/QualifiedNamesTests.cs
framework/test/BBT.Aether.Postgres.Tests/UnitOfWorkDisposalTests.cs
framework/test/BBT.Aether.Postgres.Tests/MultiSchemaUnitOfWorkTests.cs
framework/test/BBT.Aether.Postgres.Tests/PgBouncerSearchPathTests.cs
framework/test/BBT.Aether.Postgres.Tests/NonTransactionalOutboxDispatchTests.cs
framework/test/BBT.Aether.Postgres.Tests/NpgsqlLeaseStoreTests.cs
framework/test/BBT.Aether.Postgres.Tests/DbContextConfiguratorTests.cs
framework/test/BBT.Aether.Postgres.Tests/BackgroundJob/*.cs
framework/test/BBT.Aether.Infrastructure.Tests/BBT/Aether/Uow/DomainEventFailureTests.cs
framework/test/BBT.Aether.Postgres.Tests/OutboxWithinSharedTransactionTests.cs
framework/test/BBT.Aether.Postgres.Tests/UnitOfWorkMiddlewareTests.cs

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@coderabbitai

coderabbitai Bot commented Jul 22, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

PostgreSQL multi-schema support now uses qualified relation names exclusively. The provider rewrites schema placeholders and runtime tokens, transactional and non-transactional Unit of Work connection handling is separated, and tests and documentation are updated accordingly.

Changes

Qualified-names multi-schema migration

Layer / File(s) Summary
Qualified-names provider path
framework/src/BBT.Aether.Npgsql/...
SchemaSwitchingMode retains only QualifiedNames; provider wiring uses QualifiedNamesCommandInterceptor for schema validation and SQL rewriting.
Owned connection lifecycle
framework/src/BBT.Aether.Infrastructure/...
Transactional UoWs use shared resources, while non-transactional UoWs build owned options and leave pooled connection management to EF Core.
Integration and lifecycle validation
framework/test/BBT.Aether.Postgres.Tests/..., framework/test/BBT.Aether.Infrastructure.Tests/...
Tests cover qualified schema access, scope changes, pooling behavior, transaction lifecycles, interceptor rewriting, and idempotent schema creation.
Documentation alignment
framework/docs/multi-schema/*, framework/docs/unit-of-work/README.md
Documentation removes search-path modes and explains qualified-name rewriting, connection ownership, raw SQL tokens, and pooling behavior.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Caller
  participant CompositeUnitOfWork
  participant AetherDbContextConfigurator
  participant NpgsqlAetherProvider
  participant QualifiedNamesCommandInterceptor
  participant PostgreSQL
  Caller->>CompositeUnitOfWork: resolve schema-bound DbContext
  CompositeUnitOfWork->>AetherDbContextConfigurator: build transactional or owned options
  AetherDbContextConfigurator->>NpgsqlAetherProvider: apply schema binding
  NpgsqlAetherProvider->>QualifiedNamesCommandInterceptor: register bound schema and current-schema guard
  Caller->>PostgreSQL: execute EF Core command
  QualifiedNamesCommandInterceptor->>PostgreSQL: rewrite placeholders and execute qualified SQL
Loading

Possibly related PRs

Suggested labels: enhancement

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 11.11% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: removing the old schema-switching modes and standardizing on QualifiedNames.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch claude/npgsql-connection-timeout-mgzobn

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.

@codacy-production

Copy link
Copy Markdown

Not up to standards ⛔

🔴 Issues 1 critical

Alerts:
⚠ 1 issue (≤ 0 issues of at least minor severity)

Results:
1 new issue

Category Results
Security 1 critical

View in Codacy

🟢 Metrics 8 complexity · 4 duplication

Metric Results
Complexity 8
Duplication 4

View in Codacy

NEW Get contextual insights on your PRs based on Codacy's metrics, along with PR and Jira context, without leaving GitHub. Enable AI reviewer
TIP This summary will be updated as you push new changes.

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Hey - I've found 1 security issue, 5 other issues, and left some high level feedback:

Security issues:

  • Detected a formatted string in a SQL statement. This could lead to SQL injection if variables in the SQL statement are not properly sanitized. Use a prepared statements instead. You can obtain a PreparedStatement using 'SqlCommand' and 'SqlParameter'. (link)

Fixed security issues:

  • Detected a formatted string in a SQL statement. This could lead to SQL injection if variables in the SQL statement are not properly sanitized. Use a prepared statements instead. You can obtain a PreparedStatement using 'SqlCommand' and 'SqlParameter'. (link)

General comments:

  • In CompositeUnitOfWork.GetDbContextAsync the MaxDbContextCount guardrail is now commented out; if this change is intentional it would be good either to remove it entirely or re-enable it, otherwise long-lived UoWs can silently grow unbounded numbers of contexts.
  • The SchemaSwitchingMode mode parameter on AddAetherNpgsql is kept for signature compatibility but any non-QualifiedNames value is silently ignored; consider either validating and throwing for unsupported values or marking the parameter obsolete to avoid confusing callers who still try to select a mode.
  • The various ArrangeSchemaAsync helpers now duplicate the same string replacement logic on GenerateCreateScript to inject the schema; consider extracting this into a shared utility to reduce repetition and keep the placeholder-rewrite behavior consistent across tests.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- In `CompositeUnitOfWork.GetDbContextAsync` the `MaxDbContextCount` guardrail is now commented out; if this change is intentional it would be good either to remove it entirely or re-enable it, otherwise long-lived UoWs can silently grow unbounded numbers of contexts.
- The `SchemaSwitchingMode mode` parameter on `AddAetherNpgsql` is kept for signature compatibility but any non-`QualifiedNames` value is silently ignored; consider either validating and throwing for unsupported values or marking the parameter obsolete to avoid confusing callers who still try to select a mode.
- The various `ArrangeSchemaAsync` helpers now duplicate the same string replacement logic on `GenerateCreateScript` to inject the schema; consider extracting this into a shared utility to reduce repetition and keep the placeholder-rewrite behavior consistent across tests.

## Individual Comments

### Comment 1
<location path="framework/src/BBT.Aether.Infrastructure/BBT/Aether/Uow/CompositeUnitOfWork.cs" line_range="166-168" />
<code_context>
-            throw new InvalidOperationException(
-                $"UnitOfWork DbContext limit exceeded. Limit: {_options.MaxDbContextCount}");
-        }
+        // if (_contexts.Count >= _options.MaxDbContextCount)
+        // {
+        //     throw new InvalidOperationException(
+        //         $"UnitOfWork DbContext limit exceeded. Limit: {_options.MaxDbContextCount}");
+        // }
</code_context>
<issue_to_address>
**issue (performance):** Removing the DbContext count guard may allow unbounded context creation and increased resource usage.

With this guard removed, a single unit of work can now create an arbitrary number of DbContext instances. While transactional roots still share one connection/transaction, long‑lived scopes or unusual call patterns can significantly increase memory use and EF Core tracking overhead. If the previous limit was intended to prevent this, consider reintroducing it (possibly with a higher threshold) or adding another safeguard/monitoring mechanism so misuse is caught before it causes performance degradation.
</issue_to_address>

### Comment 2
<location path="framework/src/BBT.Aether.Npgsql/Microsoft/Extensions/DependencyInjection/AetherNpgsqlServiceCollectionExtensions.cs" line_range="36" />
<code_context>
         this IServiceCollection services,
         string connectionString,
-        SchemaSwitchingMode mode = SchemaSwitchingMode.TransactionLocal,
+        SchemaSwitchingMode mode = SchemaSwitchingMode.QualifiedNames,
         Action<IServiceProvider, DbContextOptionsBuilder>? configure = null)
         where TDbContext : AetherDbContext<TDbContext>
</code_context>
<issue_to_address>
**issue (bug_risk):** The `mode` parameter is effectively ignored, which can hide misconfiguration.

Since `new NpgsqlAetherProvider()` ignores `mode` and `SchemaSwitchingMode` effectively only supports `QualifiedNames`, callers can still pass other values that are silently ignored. To prevent this, validate that `mode == SchemaSwitchingMode.QualifiedNames` and throw `ArgumentOutOfRangeException` (or log a warning) for other values, or mark the parameter as obsolete in a follow-up change.
</issue_to_address>

### Comment 3
<location path="framework/src/BBT.Aether.Infrastructure/BBT/Aether/Uow/EntityFrameworkCore/AetherDbContextConfigurator.cs" line_range="32-29" />
<code_context>
         return builder.Options;
     }
+
+    public DbContextOptions<TDbContext> BuildOwnedOptions(string schema)
+    {
+        var builder = new DbContextOptionsBuilder<TDbContext>();
+        configure(serviceProvider, builder);
+        provider.ApplyOwned(
+            builder,
+            connectionString,
+            schema,
+            serviceProvider.GetRequiredService<ICurrentSchema>());
+        return builder.Options;
+    }
 }
</code_context>
<issue_to_address>
**suggestion (bug_risk):** The configuration order in `BuildOwnedOptions` differs from `BuildOptions`, which may lead to inconsistent behavior.

Here `configure` runs before `provider.ApplyOwned(...)`, while in `BuildOptions` it runs after `provider.ApplyShared(...)`. This difference can change how `configure` behaves (e.g., if it expects provider-specific settings to be applied first). Please align the call order between the two methods, or, if the difference is intentional, document it clearly in the configurator’s contract.
</issue_to_address>

### Comment 4
<location path="framework/docs/multi-schema/IMPLEMENTATION_NOTES.md" line_range="7" />
<code_context>
+> Earlier revisions described the `TransactionLocal` and `SessionSearchPath` switching modes
+> (`SET LOCAL search_path` / session `SET search_path` + `RESET search_path`), a session-level
+> interceptor (`NpgsqlSchemaConnectionInterceptor`), plus an `ICurrentSchema.Set()` /
+> `IsResolved` accessor. Those are gone: qualified names is the only strategy, and no
+> `search_path` manipulation happens anywhere. See the corrected design below.

</code_context>
<issue_to_address>
**nitpick (typo):** Adjust subject–verb agreement for “qualified names” or treat it as the proper mode name.

Consider either keeping it as a proper mode name (`QualifiedNames is the only strategy`) or using plural agreement (`qualified names are the only strategy`) to fix the grammar.

```suggestion
> `IsResolved` accessor. Those are gone: QualifiedNames is the only strategy, and no
```
</issue_to_address>

### Comment 5
<location path="framework/docs/multi-schema/IMPLEMENTATION_NOTES.md" line_range="86" />
<code_context>
-6. **PgBouncer-safe choices.** `TransactionLocal` never leaks because PostgreSQL reverts
-   `SET LOCAL` with the transaction. `QualifiedNames` has no connection schema state at all.
-   `SessionSearchPath` remains limited to a session-pinned/native connection.
+6. **Safe under any pooling.** Qualified names has no connection schema state at all — nothing
+   is ever written to session or transaction state — so it is safe under PgBouncer transaction
+   or session pooling as well as the native Npgsql pool. Non-transactional UoWs additionally
</code_context>
<issue_to_address>
**nitpick (typo):** Fix subject–verb agreement in “Qualified names has no connection schema state at all”.

I’d suggest either using the mode name (`QualifiedNames has no connection schema state at all`) or keeping the phrase but fixing agreement (`Qualified names have no connection schema state at all`).

```suggestion
6. **Safe under any pooling.** `QualifiedNames` has no connection schema state at all — nothing
```
</issue_to_address>

### Comment 6
<location path="framework/src/BBT.Aether.Npgsql/QualifiedNamesCommandInterceptor.cs" line_range="86-88" />
<code_context>
        command.CommandText = PostgreSqlRawSchemaTokenRewriter
            .Rewrite(modelRewritten, _quotedSchema)
            .CommandText;
</code_context>
<issue_to_address>
**security (csharp.lang.security.sqli.csharp-sqli):** Detected a formatted string in a SQL statement. This could lead to SQL injection if variables in the SQL statement are not properly sanitized. Use a prepared statements instead. You can obtain a PreparedStatement using 'SqlCommand' and 'SqlParameter'.

*Source: opengrep*
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment on lines +166 to +168
// if (_contexts.Count >= _options.MaxDbContextCount)
// {
// throw new InvalidOperationException(

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

issue (performance): Removing the DbContext count guard may allow unbounded context creation and increased resource usage.

With this guard removed, a single unit of work can now create an arbitrary number of DbContext instances. While transactional roots still share one connection/transaction, long‑lived scopes or unusual call patterns can significantly increase memory use and EF Core tracking overhead. If the previous limit was intended to prevent this, consider reintroducing it (possibly with a higher threshold) or adding another safeguard/monitoring mechanism so misuse is caught before it causes performance degradation.

this IServiceCollection services,
string connectionString,
SchemaSwitchingMode mode = SchemaSwitchingMode.TransactionLocal,
SchemaSwitchingMode mode = SchemaSwitchingMode.QualifiedNames,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

issue (bug_risk): The mode parameter is effectively ignored, which can hide misconfiguration.

Since new NpgsqlAetherProvider() ignores mode and SchemaSwitchingMode effectively only supports QualifiedNames, callers can still pass other values that are silently ignored. To prevent this, validate that mode == SchemaSwitchingMode.QualifiedNames and throw ArgumentOutOfRangeException (or log a warning) for other values, or mark the parameter as obsolete in a follow-up change.

@@ -28,4 +28,16 @@ public DbContextOptions<TDbContext> BuildOptions(DbConnection sharedConnection,
serviceProvider.GetRequiredService<ICurrentSchema>());
return builder.Options;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

suggestion (bug_risk): The configuration order in BuildOwnedOptions differs from BuildOptions, which may lead to inconsistent behavior.

Here configure runs before provider.ApplyOwned(...), while in BuildOptions it runs after provider.ApplyShared(...). This difference can change how configure behaves (e.g., if it expects provider-specific settings to be applied first). Please align the call order between the two methods, or, if the difference is intentional, document it clearly in the configurator’s contract.

> Earlier revisions described the `TransactionLocal` and `SessionSearchPath` switching modes
> (`SET LOCAL search_path` / session `SET search_path` + `RESET search_path`), a session-level
> interceptor (`NpgsqlSchemaConnectionInterceptor`), plus an `ICurrentSchema.Set()` /
> `IsResolved` accessor. Those are gone: qualified names is the only strategy, and no

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

nitpick (typo): Adjust subject–verb agreement for “qualified names” or treat it as the proper mode name.

Consider either keeping it as a proper mode name (QualifiedNames is the only strategy) or using plural agreement (qualified names are the only strategy) to fix the grammar.

Suggested change
> `IsResolved` accessor. Those are gone: qualified names is the only strategy, and no
> `IsResolved` accessor. Those are gone: QualifiedNames is the only strategy, and no

6. **PgBouncer-safe choices.** `TransactionLocal` never leaks because PostgreSQL reverts
`SET LOCAL` with the transaction. `QualifiedNames` has no connection schema state at all.
`SessionSearchPath` remains limited to a session-pinned/native connection.
6. **Safe under any pooling.** Qualified names has no connection schema state at all — nothing

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

nitpick (typo): Fix subject–verb agreement in “Qualified names has no connection schema state at all”.

I’d suggest either using the mode name (QualifiedNames has no connection schema state at all) or keeping the phrase but fixing agreement (Qualified names have no connection schema state at all).

Suggested change
6. **Safe under any pooling.** Qualified names has no connection schema state at all — nothing
6. **Safe under any pooling.** `QualifiedNames` has no connection schema state at all — nothing

Comment on lines +86 to +88
command.CommandText = PostgreSqlRawSchemaTokenRewriter
.Rewrite(modelRewritten, _quotedSchema)
.CommandText;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

security (csharp.lang.security.sqli.csharp-sqli): Detected a formatted string in a SQL statement. This could lead to SQL injection if variables in the SQL statement are not properly sanitized. Use a prepared statements instead. You can obtain a PreparedStatement using 'SqlCommand' and 'SqlParameter'.

Source: opengrep

@yilmaztayfun
yilmaztayfun merged commit bdcaba6 into master Jul 22, 2026
3 of 6 checks passed
@sonarqubecloud

Copy link
Copy Markdown

Quality Gate Failed Quality Gate failed

Failed conditions
0.0% Coverage on New Code (required ≥ 80%)

See analysis details on SonarQube Cloud

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
framework/src/BBT.Aether.Npgsql/NpgsqlAetherProvider.cs (1)

19-21: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Use the ambient ICurrentSchema for the 3-arg overload.

Only tests pass the 5th argument directly; production config routes through the 3-arg overload, which is implemented as ApplyShared(..., new StaticCurrentSchema(schema)). That makes QualifiedNamesCommandInterceptor compare schema to currentSchema.Name, both of which are fixed to the same value, so any request-mutated DI ICurrentSchema cannot change or fail the check. Resolve/consume ICurrentSchema here or delegate to a 5-arg caller path that preserves the runtime schema value.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@framework/src/BBT.Aether.Npgsql/NpgsqlAetherProvider.cs` around lines 19 -
21, The 3-argument ApplyShared overload currently hardcodes a
StaticCurrentSchema, bypassing the runtime DI schema. Update
NpgsqlAetherProvider.ApplyShared to resolve and use the ambient ICurrentSchema,
or delegate through a caller path that preserves it, so
QualifiedNamesCommandInterceptor validates against the request-mutated schema.
🧹 Nitpick comments (2)
framework/test/BBT.Aether.Postgres.Tests/BackgroundJob/ArmingProcessorTests.cs (1)

289-292: 📐 Maintainability & Code Quality | 🔵 Trivial | 🏗️ Heavy lift

Centralize and validate the PostgreSQL schema-script binding. These eight tests duplicate exact string replacements against Npgsql-generated SQL, so provider formatting changes or partial updates can leave placeholder handling inconsistent.

  • framework/test/BBT.Aether.Postgres.Tests/BackgroundJob/ArmingProcessorTests.cs#L289-L292: make this the shared helper implementation and validate that schema placeholders are fully removed.
  • framework/test/BBT.Aether.Postgres.Tests/BackgroundJob/DaprBridgeTests.cs#L113-L116: replace the local transformation with the shared helper.
  • framework/test/BBT.Aether.Postgres.Tests/BackgroundJob/EndToEndJobLifecycleTests.cs#L206-L209: replace the local transformation with the shared helper.
  • framework/test/BBT.Aether.Postgres.Tests/BackgroundJob/EnqueueAtomicityTests.cs#L114-L117: replace the local transformation with the shared helper.
  • framework/test/BBT.Aether.Postgres.Tests/BackgroundJob/JobDispatcherTests.cs#L257-L260: replace the local transformation with the shared helper.
  • framework/test/BBT.Aether.Postgres.Tests/BackgroundJob/JobStoreCasTests.cs#L84-L87: replace the local transformation with the shared helper.
  • framework/test/BBT.Aether.Postgres.Tests/BackgroundJob/JobStoreClaimReaperTests.cs#L70-L73: replace the local transformation with the shared helper.
  • framework/test/BBT.Aether.Postgres.Tests/BackgroundJob/ReaperTests.cs#L125-L128: replace the local transformation with the shared helper.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@framework/test/BBT.Aether.Postgres.Tests/BackgroundJob/ArmingProcessorTests.cs`
around lines 289 - 292, Centralize PostgreSQL schema-script transformation in
the shared helper at ArmingProcessorTests.cs lines 289-292, preserving all
required replacements and validating that no schema placeholders remain after
binding. Replace the duplicated local transformations with this helper in
DaprBridgeTests.cs lines 113-116, EndToEndJobLifecycleTests.cs lines 206-209,
EnqueueAtomicityTests.cs lines 114-117, JobDispatcherTests.cs lines 257-260,
JobStoreCasTests.cs lines 84-87, JobStoreClaimReaperTests.cs lines 70-73, and
ReaperTests.cs lines 125-128.
framework/src/BBT.Aether.Npgsql/NpgsqlAetherProvider.cs (1)

19-28: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Remove the unused state parameter from the provider contract.

NpgsqlAetherProvider qualified-names binding uses ICurrentSchema, and SqlServerAetherProvider no longer honors state either, so keep removing state from IAetherDatabaseProvider.ApplyShared(...) and both provider overload signatures now that this API exists for QualifiedNames design.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@framework/src/BBT.Aether.Npgsql/NpgsqlAetherProvider.cs` around lines 19 -
28, Remove the unused state parameter from IAetherDatabaseProvider.ApplyShared
and both ApplyShared overloads in NpgsqlAetherProvider, updating all affected
call sites and the corresponding SqlServerAetherProvider signatures while
preserving the ICurrentSchema-based qualified-names binding.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@framework/docs/multi-schema/IMPLEMENTATION_NOTES.md`:
- Around line 58-70: Update the documentation sentences beginning with
“Qualified names is” to use correct grammatical agreement, such as “Qualified
names are” or “The qualified-names strategy is,” including the corresponding
sentences in the additionally referenced section. Preserve the existing
technical content.

In `@framework/docs/multi-schema/README.md`:
- Around line 93-96: Update the preceding paragraph’s connection-state wording
to remove the claim that the formatted schema ends up on the connection. State
instead that the formatted schema name is used in the bound SQL or
qualified-name rewriter, while preserving the surrounding explanation.

In
`@framework/src/BBT.Aether.Infrastructure/BBT/Aether/Uow/CompositeUnitOfWork.cs`:
- Around line 166-170: Restore the MaxDbContextCount guard in the
context-creation flow of CompositeUnitOfWork, using _contexts.Count and
_options.MaxDbContextCount to throw the existing InvalidOperationException when
the configured limit is reached or exceeded. Keep the current context creation
behavior unchanged below the guard.

---

Outside diff comments:
In `@framework/src/BBT.Aether.Npgsql/NpgsqlAetherProvider.cs`:
- Around line 19-21: The 3-argument ApplyShared overload currently hardcodes a
StaticCurrentSchema, bypassing the runtime DI schema. Update
NpgsqlAetherProvider.ApplyShared to resolve and use the ambient ICurrentSchema,
or delegate through a caller path that preserves it, so
QualifiedNamesCommandInterceptor validates against the request-mutated schema.

---

Nitpick comments:
In `@framework/src/BBT.Aether.Npgsql/NpgsqlAetherProvider.cs`:
- Around line 19-28: Remove the unused state parameter from
IAetherDatabaseProvider.ApplyShared and both ApplyShared overloads in
NpgsqlAetherProvider, updating all affected call sites and the corresponding
SqlServerAetherProvider signatures while preserving the ICurrentSchema-based
qualified-names binding.

In
`@framework/test/BBT.Aether.Postgres.Tests/BackgroundJob/ArmingProcessorTests.cs`:
- Around line 289-292: Centralize PostgreSQL schema-script transformation in the
shared helper at ArmingProcessorTests.cs lines 289-292, preserving all required
replacements and validating that no schema placeholders remain after binding.
Replace the duplicated local transformations with this helper in
DaprBridgeTests.cs lines 113-116, EndToEndJobLifecycleTests.cs lines 206-209,
EnqueueAtomicityTests.cs lines 114-117, JobDispatcherTests.cs lines 257-260,
JobStoreCasTests.cs lines 84-87, JobStoreClaimReaperTests.cs lines 70-73, and
ReaperTests.cs lines 125-128.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 4e0cc9c6-c072-4fb5-b4f9-3e1c2a495932

📥 Commits

Reviewing files that changed from the base of the PR and between f02e44b and 576e69d.

📒 Files selected for processing (31)
  • framework/docs/multi-schema/ADOPTION-GUIDE.md
  • framework/docs/multi-schema/IMPLEMENTATION_NOTES.md
  • framework/docs/multi-schema/README.md
  • framework/docs/unit-of-work/README.md
  • framework/src/BBT.Aether.Infrastructure/BBT/Aether/Uow/CompositeUnitOfWork.cs
  • framework/src/BBT.Aether.Infrastructure/BBT/Aether/Uow/EntityFrameworkCore/AetherDbContextConfigurator.cs
  • framework/src/BBT.Aether.Infrastructure/BBT/Aether/Uow/EntityFrameworkCore/IAetherDatabaseProvider.cs
  • framework/src/BBT.Aether.Infrastructure/BBT/Aether/Uow/EntityFrameworkCore/IAetherDbContextConfigurator.cs
  • framework/src/BBT.Aether.Npgsql/BBT/Aether/Uow/EntityFrameworkCore/SchemaSwitchingMode.cs
  • framework/src/BBT.Aether.Npgsql/Microsoft/Extensions/DependencyInjection/AetherNpgsqlServiceCollectionExtensions.cs
  • framework/src/BBT.Aether.Npgsql/NpgsqlAetherProvider.cs
  • framework/src/BBT.Aether.Npgsql/QualifiedNamesCommandInterceptor.cs
  • framework/src/BBT.Aether.Npgsql/SearchPathCommandInterceptor.cs
  • framework/test/BBT.Aether.Infrastructure.Tests/BBT/Aether/Uow/DomainEventFailureTests.cs
  • framework/test/BBT.Aether.Postgres.Tests/BackgroundJob/ArmingProcessorTests.cs
  • framework/test/BBT.Aether.Postgres.Tests/BackgroundJob/DaprBridgeTests.cs
  • framework/test/BBT.Aether.Postgres.Tests/BackgroundJob/EndToEndJobLifecycleTests.cs
  • framework/test/BBT.Aether.Postgres.Tests/BackgroundJob/EnqueueAtomicityTests.cs
  • framework/test/BBT.Aether.Postgres.Tests/BackgroundJob/JobDispatcherTests.cs
  • framework/test/BBT.Aether.Postgres.Tests/BackgroundJob/JobStoreCasTests.cs
  • framework/test/BBT.Aether.Postgres.Tests/BackgroundJob/JobStoreClaimReaperTests.cs
  • framework/test/BBT.Aether.Postgres.Tests/BackgroundJob/ReaperTests.cs
  • framework/test/BBT.Aether.Postgres.Tests/DbContextConfiguratorTests.cs
  • framework/test/BBT.Aether.Postgres.Tests/MultiSchemaUnitOfWorkTests.cs
  • framework/test/BBT.Aether.Postgres.Tests/NonTransactionalOutboxDispatchTests.cs
  • framework/test/BBT.Aether.Postgres.Tests/NpgsqlLeaseStoreTests.cs
  • framework/test/BBT.Aether.Postgres.Tests/OutboxWithinSharedTransactionTests.cs
  • framework/test/BBT.Aether.Postgres.Tests/PgBouncerSearchPathTests.cs
  • framework/test/BBT.Aether.Postgres.Tests/QualifiedNamesTests.cs
  • framework/test/BBT.Aether.Postgres.Tests/UnitOfWorkDisposalTests.cs
  • framework/test/BBT.Aether.Postgres.Tests/UnitOfWorkMiddlewareTests.cs
💤 Files with no reviewable changes (1)
  • framework/src/BBT.Aether.Npgsql/SearchPathCommandInterceptor.cs

Comment on lines +58 to +70
3. **Qualified names is the only isolation strategy.** The former `TransactionLocal` and
`SessionSearchPath` switching modes were removed, along with all `search_path` manipulation
(`SET LOCAL search_path`, session `SET search_path`, `RESET search_path` cleanup).
`SchemaSwitchingMode` now has the single member `QualifiedNames`, and isolation is enforced
by `QualifiedNamesCommandInterceptor(schema, currentSchema)`:

- Uses one tenant-independent model placeholder, then rewrites it to the validated schema
bound to the context immediately before execution. Schema-dependent
`FromSqlRaw`/`ExecuteSqlRaw` relations use the exact `{{schema}}` token, rewritten to the
quoted bound schema.
- Throws if `ICurrentSchema.Name` does not match the context's bound schema (guard against
a context leaking across schema scopes).
- Emits no `SET`, `SET LOCAL`, or `RESET search_path` and requires no transaction.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Fix the grammatical agreement for “qualified names”.

Use “Qualified names are/have…” or “The qualified-names strategy is/has…” in these sentences.

Also applies to: 86-89

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@framework/docs/multi-schema/IMPLEMENTATION_NOTES.md` around lines 58 - 70,
Update the documentation sentences beginning with “Qualified names is” to use
correct grammatical agreement, such as “Qualified names are” or “The
qualified-names strategy is,” including the corresponding sentences in the
additionally referenced section. Preserve the existing technical content.

Comment on lines +93 to 96
Before a name is interpolated into SQL, it is validated and quoted by
`PostgreSqlIdentifier.QuoteSchema(...)` (regex `^[a-zA-Z_][a-zA-Z0-9_]*$`). An invalid name
throws `InvalidOperationException: Invalid PostgreSQL identifier: <name>`. Schema names
cannot be passed as SQL parameters, so this validate-then-quote step is the injection guard.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Remove the stale connection-state wording.

The preceding paragraph says the formatted schema “ends up on the connection,” but qualified-name rewriting does not store schema state on connections. Reword that sentence to say the formatted name is used in the bound SQL/rewriter instead.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@framework/docs/multi-schema/README.md` around lines 93 - 96, Update the
preceding paragraph’s connection-state wording to remove the claim that the
formatted schema ends up on the connection. State instead that the formatted
schema name is used in the bound SQL or qualified-name rewriter, while
preserving the surrounding explanation.

Comment on lines +166 to +170
// if (_contexts.Count >= _options.MaxDbContextCount)
// {
// throw new InvalidOperationException(
// $"UnitOfWork DbContext limit exceeded. Limit: {_options.MaxDbContextCount}");
// }

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Restore MaxDbContextCount enforcement.

Commenting out this guard breaks the existing option contract: UnitOfWorkOptions still exposes it, the UoW docs list the error, and Exceeds_max_context_limit_throws expects it. A UoW can now retain unbounded contexts for distinct schemas/types.

Proposed fix
-        // if (_contexts.Count >= _options.MaxDbContextCount)
-        // {
-        //     throw new InvalidOperationException(
-        //         $"UnitOfWork DbContext limit exceeded. Limit: {_options.MaxDbContextCount}");
-        // }
+        if (_contexts.Count >= _options.MaxDbContextCount)
+        {
+            throw new InvalidOperationException(
+                $"UnitOfWork DbContext limit exceeded. Limit: {_options.MaxDbContextCount}");
+        }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// if (_contexts.Count >= _options.MaxDbContextCount)
// {
// throw new InvalidOperationException(
// $"UnitOfWork DbContext limit exceeded. Limit: {_options.MaxDbContextCount}");
// }
if (_contexts.Count >= _options.MaxDbContextCount)
{
throw new InvalidOperationException(
$"UnitOfWork DbContext limit exceeded. Limit: {_options.MaxDbContextCount}");
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@framework/src/BBT.Aether.Infrastructure/BBT/Aether/Uow/CompositeUnitOfWork.cs`
around lines 166 - 170, Restore the MaxDbContextCount guard in the
context-creation flow of CompositeUnitOfWork, using _contexts.Count and
_options.MaxDbContextCount to throw the existing InvalidOperationException when
the configured limit is reached or exceeded. Keep the current context creation
behavior unchanged below the guard.

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.

2 participants