Skip to content

optimze crontab identity#1378

Open
yileicn wants to merge 3 commits into
SciSharp:masterfrom
yileicn:master
Open

optimze crontab identity#1378
yileicn wants to merge 3 commits into
SciSharp:masterfrom
yileicn:master

Conversation

@yileicn

@yileicn yileicn commented Jul 14, 2026

Copy link
Copy Markdown
Member

No description provided.

@qodo-code-review

Copy link
Copy Markdown
Contributor

PR Summary by Qodo

Fix crontab hook identity propagation across HookEmitter async boundaries

🐞 Bug fix 🧪 Tests ⚙️ Configuration changes 🕐 40+ Minutes

Grey Divider

AI Description

• Split crontab hook execution into sync auth + async work to preserve ambient identity.
• Add unit tests reproducing the AsyncLocal/ExecutionContext identity-loss bug and guarding the fix.
• Move/rehook the unit test project under /tests and add required ASP.NET framework reference.
Diagram

graph TD
  A["CrontabService"] --> B["HookEmitter.Emit (sync)"] --> C["Identity hook"] --> F[("Ambient identity\nAsyncLocal")]
  A --> D["HookEmitter.Emit (async)"] --> E["Worker hooks"] --> F
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Make HookEmitter support pre-auth + async-work in one API
  • ➕ Encapsulates the two-phase pattern so callers can’t accidentally regress it
  • ➕ Keeps scheduling code simpler and consistent across emitters
  • ➖ Bigger refactor; risks changing semantics for other hook consumers
  • ➖ Still relies on ambient AsyncLocal identity implicitly
2. Pass identity explicitly to hooks (no ambient HttpContextAccessor)
  • ➕ Eliminates ExecutionContext/AsyncLocal coupling and identity leakage risks
  • ➕ More testable and deterministic background execution
  • ➖ Breaking change across hook interfaces and call sites
  • ➖ More plumbing (additional parameters/context objects)
3. Use an explicit scoped service/context for cron runs
  • ➕ Keeps identity scoped to a DI scope rather than process-wide async locals
  • ➕ Reduces risk of cross-request contamination
  • ➖ Requires reworking identity resolution and any code reading IHttpContextAccessor
  • ➖ May be difficult if downstream libraries assume HttpContextAccessor

Recommendation: The current two-phase approach in CrontabService is a pragmatic, low-risk fix that directly addresses the ExecutionContext restore behavior in the async emitter. Keep it, and consider a follow-up to centralize this pattern (or move toward explicit identity/context passing) to prevent future regressions and reduce reliance on ambient AsyncLocal identity.

Files changed (6) +273 / -17

Bug fix (1) +16 / -1
CrontabService.csTwo-phase hook emission to preserve crontab identity across async boundaries +16/-1

Two-phase hook emission to preserve crontab identity across async boundaries

• Splits ScheduledTimeArrived hook execution into a synchronous authentication phase (OnAuthenticate) followed by an async execution phase (OnTaskExecuting/OnCronTriggered/OnTaskExecuted). This prevents AsyncLocal-backed identity from being lost due to async state-machine ExecutionContext restore between hooks.

src/Infrastructure/BotSharp.Core.Crontab/Services/CrontabService.cs

Tests (3) +237 / -0
CrontabIdentityFlowTests.csAdd regression tests for crontab identity propagation and reset semantics +237/-0

Add regression tests for crontab identity propagation and reset semantics

• Introduces a dedicated test suite that reproduces the pre-fix identity loss when OnAuthenticate runs inside per-hook awaited lambdas. Validates the two-phase (sync auth + async work) fix and adds targeted tests documenting the behavior of the isResetHttpContext flag during live requests vs background flows.

tests/BotSharp.Core.UnitTests/Crontab/CrontabIdentityFlowTests.cs

SettingServiceTests.csNo diff hunk provided in snippet (verify full PR for actual changes) +0/-0

No diff hunk provided in snippet (verify full PR for actual changes)

• This file is listed in the provided diff header, but no content changes were included in the snippet. Review the full PR diff to confirm whether this is a rename/formatting-only change (e.g., line endings) or contains functional test updates.

tests/BotSharp.Core.UnitTests/Infrastructures/SettingServiceTests.cs

RoutingRuleTests.csNo diff hunk provided in snippet (verify full PR for actual changes) +0/-0

No diff hunk provided in snippet (verify full PR for actual changes)

• This file is listed in the provided diff header, but no content changes were included in the snippet. Review the full PR diff to confirm whether this is a rename/formatting-only change (e.g., line endings) or contains functional test updates.

tests/BotSharp.Core.UnitTests/Routing/RoutingRuleTests.cs

Other (2) +20 / -16
BotSharp.slnRelocate unit test project to /tests and update solution mappings +15/-15

Relocate unit test project to /tests and update solution mappings

• Removes the old BotSharp.Core.UnitTests project entry and adds the test project from tests/BotSharp.Core.UnitTests. Updates configuration/platform mappings and solution folder nesting accordingly.

BotSharp.sln

BotSharp.Core.UnitTests.csprojAdd ASP.NET framework reference and fix project reference path +5/-1

Add ASP.NET framework reference and fix project reference path

• Adds FrameworkReference to Microsoft.AspNetCore.App so tests can use HttpContextAccessor/DefaultHttpContext. Updates the BotSharp.Core project reference to reflect the test project’s new location under /tests.

tests/BotSharp.Core.UnitTests/BotSharp.Core.UnitTests.csproj

@qodo-code-review

Copy link
Copy Markdown
Contributor

Code Review by Qodo

🐞 Bugs (1) 📘 Rule violations (1) 📜 Skill insights (0)

Grey Divider


Action required

1. Case-sensitive Triggers.Contains match 📘 Rule violation ≡ Correctness
Description
hook.Triggers.Contains(item.Title) uses default (case-sensitive/culture-sensitive) string
equality, which can break identifier-like trigger matching when casing differs. This violates the
requirement to use ordinal, case-insensitive comparisons/collections for identifier lookups.
Code

src/Infrastructure/BotSharp.Core.Crontab/Services/CrontabService.cs[R136-148]

+        HookEmitter.Emit<ICrontabHook>(_services, hook =>
        {
            if (hook.Triggers == null || hook.Triggers.Contains(item.Title))
            {
                hook.OnAuthenticate(item);
+            }
+        }, item.AgentId);
+
+        // Phase 2 - Execute.
+        await HookEmitter.Emit<ICrontabHook>(_services, async hook =>
+        {
+            if (hook.Triggers == null || hook.Triggers.Contains(item.Title))
+            {
Evidence
PR Compliance ID 3 requires ordinal, case-insensitive matching for identifier-like strings and
appropriate comparers for lookups. The updated code filters hooks by trigger using
hook.Triggers.Contains(item.Title) in both Phase 1 and Phase 2, which relies on default
case-sensitive equality instead of
StringComparer.OrdinalIgnoreCase/StringComparison.OrdinalIgnoreCase.

src/Infrastructure/BotSharp.Core.Crontab/Services/CrontabService.cs[136-148]
Best Practice: Learned patterns

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`ScheduledTimeArrived` matches triggers using `hook.Triggers.Contains(item.Title)` which is case-sensitive by default. The compliance requirement is to use ordinal, case-insensitive comparisons for identifier-like strings.

## Issue Context
This PR introduced/rewrote the trigger-filtering logic in `ScheduledTimeArrived` (now executed in two phases), and the trigger match is performed in both phases.

## Fix Focus Areas
- src/Infrastructure/BotSharp.Core.Crontab/Services/CrontabService.cs[136-148]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Remediation recommended

2. Hook instances may differ 🐞 Bug ≡ Correctness
Description
ScheduledTimeArrived now resolves ICrontabHook implementations twice (once for Phase 1 auth, again
for Phase 2 execution); if any hook is registered as transient, OnAuthenticate can run on a
different instance than OnCronTriggered, breaking per-hook state that was initialized during
authentication.
Code

src/Infrastructure/BotSharp.Core.Crontab/Services/CrontabService.cs[R136-151]

+        HookEmitter.Emit<ICrontabHook>(_services, hook =>
        {
            if (hook.Triggers == null || hook.Triggers.Contains(item.Title))
            {
                hook.OnAuthenticate(item);
+            }
+        }, item.AgentId);
+
+        // Phase 2 - Execute.
+        await HookEmitter.Emit<ICrontabHook>(_services, async hook =>
+        {
+            if (hook.Triggers == null || hook.Triggers.Contains(item.Title))
+            {
                await hook.OnTaskExecuting(item);
                await hook.OnCronTriggered(item);
                await hook.OnTaskExecuted(item);
Evidence
The PR introduces two HookEmitter.Emit calls (Phase 1 and Phase 2). Each Emit call re-resolves
hooks via services.GetHooks<T>(agentId), and GetHooks uses GetServices<T>(), meaning
resolution can create new instances depending on DI lifetime; current in-repo crontab hooks are
registered as scoped, so the risk is conditional but real if a transient hook is added.

src/Infrastructure/BotSharp.Core.Crontab/Services/CrontabService.cs[118-154]
src/Infrastructure/BotSharp.Core/Infrastructures/HookEmitter.cs[7-53]
src/Infrastructure/BotSharp.Abstraction/Hooks/HookProvider.cs[6-12]
src/Infrastructure/BotSharp.OpenAPI/BotSharpOpenApiExtensions.cs[232-241]
src/Plugins/BotSharp.Plugin.SqlDriver/SqlDriverPlugin.cs[18-35]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`CrontabService.ScheduledTimeArrived` performs two separate `HookEmitter.Emit` calls, which re-resolve hooks from DI each time. This can produce different hook instances for transient lifetimes, causing `OnAuthenticate` state to be lost before `OnTaskExecuting/OnCronTriggered/OnTaskExecuted`.

## Issue Context
- Today, the known `ICrontabHook` registrations in-repo are `AddScoped`, so this may not reproduce right now, but the contract doesn’t enforce that and plugins can introduce transient hooks.

## Fix Focus Areas
- Resolve the hook list once and reuse it for both phases (or add an overload that accepts an already-resolved hook list).
- file: src/Infrastructure/BotSharp.Core.Crontab/Services/CrontabService.cs[129-153]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Qodo Logo

Comment on lines +136 to +148
HookEmitter.Emit<ICrontabHook>(_services, hook =>
{
if (hook.Triggers == null || hook.Triggers.Contains(item.Title))
{
hook.OnAuthenticate(item);
}
}, item.AgentId);

// Phase 2 - Execute.
await HookEmitter.Emit<ICrontabHook>(_services, async hook =>
{
if (hook.Triggers == null || hook.Triggers.Contains(item.Title))
{

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Action required

1. Case-sensitive triggers.contains match 📘 Rule violation ≡ Correctness

hook.Triggers.Contains(item.Title) uses default (case-sensitive/culture-sensitive) string
equality, which can break identifier-like trigger matching when casing differs. This violates the
requirement to use ordinal, case-insensitive comparisons/collections for identifier lookups.
Agent Prompt
## Issue description
`ScheduledTimeArrived` matches triggers using `hook.Triggers.Contains(item.Title)` which is case-sensitive by default. The compliance requirement is to use ordinal, case-insensitive comparisons for identifier-like strings.

## Issue Context
This PR introduced/rewrote the trigger-filtering logic in `ScheduledTimeArrived` (now executed in two phases), and the trigger match is performed in both phases.

## Fix Focus Areas
- src/Infrastructure/BotSharp.Core.Crontab/Services/CrontabService.cs[136-148]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment on lines +136 to 151
HookEmitter.Emit<ICrontabHook>(_services, hook =>
{
if (hook.Triggers == null || hook.Triggers.Contains(item.Title))
{
hook.OnAuthenticate(item);
}
}, item.AgentId);

// Phase 2 - Execute.
await HookEmitter.Emit<ICrontabHook>(_services, async hook =>
{
if (hook.Triggers == null || hook.Triggers.Contains(item.Title))
{
await hook.OnTaskExecuting(item);
await hook.OnCronTriggered(item);
await hook.OnTaskExecuted(item);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Remediation recommended

2. Hook instances may differ 🐞 Bug ≡ Correctness

ScheduledTimeArrived now resolves ICrontabHook implementations twice (once for Phase 1 auth, again
for Phase 2 execution); if any hook is registered as transient, OnAuthenticate can run on a
different instance than OnCronTriggered, breaking per-hook state that was initialized during
authentication.
Agent Prompt
## Issue description
`CrontabService.ScheduledTimeArrived` performs two separate `HookEmitter.Emit` calls, which re-resolve hooks from DI each time. This can produce different hook instances for transient lifetimes, causing `OnAuthenticate` state to be lost before `OnTaskExecuting/OnCronTriggered/OnTaskExecuted`.

## Issue Context
- Today, the known `ICrontabHook` registrations in-repo are `AddScoped`, so this may not reproduce right now, but the contract doesn’t enforce that and plugins can introduce transient hooks.

## Fix Focus Areas
- Resolve the hook list once and reuse it for both phases (or add an overload that accepts an already-resolved hook list).
- file: src/Infrastructure/BotSharp.Core.Crontab/Services/CrontabService.cs[129-153]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

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.

1 participant