Skip to content

Allow HostFactoryResolver to listen on arbitrary events#127743

Draft
Youssef1313 wants to merge 6 commits into
mainfrom
dev/ygerges/hostfactoryresolver
Draft

Allow HostFactoryResolver to listen on arbitrary events#127743
Youssef1313 wants to merge 6 commits into
mainfrom
dev/ygerges/hostfactoryresolver

Conversation

@Youssef1313
Copy link
Copy Markdown
Member

@Youssef1313 Youssef1313 commented May 4, 2026

I needed this to prototype the change in dotnet/aspnetcore#66527.

Whether or not we will need to use that in aspnet is to be decided when dotnet/aspnetcore#66574 is reviewed.

The change here is in internal class which is consumed via an internal source-only package (that package isn't shipped to nuget.org). The PR doesn't introduce any behavior changes to runtime. It will only allow aspnetcore to hook additional delegates, which will allow us to fire HostApplicationBuilderCreated event when the user calls WebApplication.CreateBuilder, and gives us the opportunity in the testing library to add early configuration sources that users expect to be available before Build() is called.

@dotnet-policy-service
Copy link
Copy Markdown
Contributor

Tagging subscribers to this area: @dotnet/area-extensions-hosting
See info in area-owners.md if you want to be subscribed.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR extends HostFactoryResolver.ResolveHostFactory to allow callers to attach callbacks for additional DiagnosticSource event names emitted by Microsoft.Extensions.Hosting, enabling tooling scenarios that need to observe more than just the existing HostBuilding / HostBuilt flow.

Changes:

  • Adds an optional arbitraryActions map to ResolveHostFactory and threads it into HostingListener.
  • Invokes a registered callback when an observed hosting DiagnosticSource event key matches an entry in arbitraryActions.
  • Adds a new using Microsoft.Extensions.Configuration; directive (currently unused).

Comment thread src/libraries/Microsoft.Extensions.HostFactoryResolver/src/HostFactoryResolver.cs Outdated
Comment on lines 67 to 68
Dictionary<string, Action<object?>>? arbitraryActions = null,
Action<Exception?>? entrypointCompleted = null)
Comment on lines 64 to 68
TimeSpan? waitTimeout = null,
bool stopApplication = true,
Action<object>? configureHostBuilder = null,
Dictionary<string, Action<object?>>? arbitraryActions = null,
Action<Exception?>? entrypointCompleted = null)
Comment on lines +349 to +352
else if (_arbitraryActions?.TryGetValue(value.Key, out var arbitraryAction) == true)
{
arbitraryAction.Invoke(value.Value);
}
Comment on lines 335 to +352
if (value.Key == "HostBuilding")
{
_configure?.Invoke(value.Value!);
}

if (value.Key == "HostBuilt")
else if (value.Key == "HostBuilt")
{
_hostTcs.TrySetResult(value.Value!);

if (_stopApplication)
{
// Stop the host from running further
ThrowHostAborted();
}
}
else if (_arbitraryActions?.TryGetValue(value.Key, out var arbitraryAction) == true)
{
arbitraryAction.Invoke(value.Value);
}
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

I don't think it's worth making _arbitraryActions apply to HostBuilding/HostBuilt. There is no need and this is all internal stuff, so it's easy to change in future if needed.

Copilot AI review requested due to automatic review settings May 4, 2026 12:27
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 1 out of 1 changed files in this pull request and generated 3 comments.

Comment thread src/libraries/Microsoft.Extensions.HostFactoryResolver/src/HostFactoryResolver.cs Outdated
Comment on lines +204 to +218
private readonly TaskCompletionSource<object> _hostTcs = new();
private IDisposable? _disposable;
private readonly Action<object>? _configure;
private readonly Dictionary<string, Action<object?>>? _arbitraryActions;
private readonly Action<Exception?>? _entrypointCompleted;
private static readonly AsyncLocal<HostingListener> _currentListener = new();

public HostingListener(string[] args, MethodInfo entryPoint, TimeSpan waitTimeout, bool stopApplication, Action<object>? configure, Action<Exception?>? entrypointCompleted)
public HostingListener(string[] args, MethodInfo entryPoint, TimeSpan waitTimeout, bool stopApplication, Action<object>? configure, Dictionary<string, Action<object?>>? arbitraryActions, Action<Exception?>? entrypointCompleted)
{
_args = args;
_entryPoint = entryPoint;
_waitTimeout = waitTimeout;
_stopApplication = stopApplication;
_configure = configure;
_arbitraryActions = arbitraryActions;
}
else if (_arbitraryActions?.TryGetValue(value.Key, out var arbitraryAction) == true)
{
arbitraryAction.Invoke(value.Value);
@github-actions

This comment has been minimized.

Copilot AI review requested due to automatic review settings May 4, 2026 14:04
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 1 out of 1 changed files in this pull request and generated 3 comments.

Comment on lines 62 to 68
public static Func<string[], object>? ResolveHostFactory(Assembly assembly,
TimeSpan? waitTimeout = null,
bool stopApplication = true,
Action<object>? configureHostBuilder = null,
Action<Exception?>? entrypointCompleted = null)
Action<Exception?>? entrypointCompleted = null,
Dictionary<string, Action<object?>>? arbitraryActions = null)
{
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Not worth it. All internal stuff and we won't mutate the dictionary.

Comment on lines +355 to +358
else if (_arbitraryActions?.TryGetValue(value.Key, out var arbitraryAction) == true)
{
arbitraryAction.Invoke(value.Value);
}
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

@copilot apply changes based on this feedback

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.

Implemented this in 08df3ae: added coverage for arbitraryActions with custom event callbacks (including both stopApplication modes) and a test verifying exceptions from arbitrary callbacks propagate through ResolveHostFactory.

@svick
Copy link
Copy Markdown
Member

svick commented May 7, 2026

Mostly repeating what copilot said, but:

  1. I think there should be tests for this.
  2. You should make sure that exceptions from arbitraryActions don't escape to the thread pool. I.e. if you're using this in a test and an arbitraryAction throws, it should fail the test, not crash the test process. Maybe it alwady works that way, I don't know.

@svick
Copy link
Copy Markdown
Member

svick commented May 14, 2026

@Youssef1313 If you're not planning on getting this PR merged soon, can you turn it into a draft? We don't want to have PRs that are open for a long time. Thanks.

@Youssef1313
Copy link
Copy Markdown
Member Author

@svick With regards to exceptions, how is the new arbitraryActions different from the existing configureHostBuilder which is just called as-is?

Copilot AI review requested due to automatic review settings May 15, 2026 12:52
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 1 out of 1 changed files in this pull request and generated 1 comment.

Comment on lines +355 to +358
else if (_arbitraryActions?.TryGetValue(value.Key, out var arbitraryAction) == true)
{
arbitraryAction.Invoke(value.Value);
}
@github-actions
Copy link
Copy Markdown
Contributor

Caution

Security scanning requires review for Code Review

Details

The threat detection results could not be parsed. The workflow output should be reviewed before merging.

Review the workflow run logs for details.

🤖 Copilot Code Review — PR #127743

Holistic Assessment

Motivation: The PR adds a hook mechanism to allow consumers (ASP.NET Core) to listen on arbitrary diagnostic source events during host resolution. The use case (firing HostApplicationBuilderCreated for testing libraries) is reasonable, though the aspnetcore design is still under review.

Approach: Adding an IDictionary<string, Action<object?>> parameter to an internal API is a straightforward extension. The approach is minimal and consistent with how configureHostBuilder already works.

Summary: ⚠️ Needs Human Review. The code change is small and mechanically correct, but there are two concerns that a human reviewer should weigh: (1) missing tests, and (2) a subtle behavioral change in the if/else if restructuring.


Detailed Findings

⚠️ Missing Tests

The reviewer @svick already flagged this. This PR adds new functionality with no test coverage. Even though this is an internal source-only package, the HostFactoryResolver has existing tests (in Microsoft.Extensions.Hosting.Tests or similar). A test verifying that arbitrary actions are invoked for matching diagnostic keys would be appropriate.

Classification: Advisory — the aspnetcore consumer will implicitly test this, but in-repo tests are standard practice.

⚠️ Behavioral Change: ifelse if for "HostBuilding"/"HostBuilt"

The original code had:

if (value.Key == "HostBuilding") { ... }

if (value.Key == "HostBuilt") { ... }

This was changed to if / else if / else if. Since value.Key can only be one string at a time, this is semantically equivalent for these two cases — only one branch could ever match per call. However, it's worth noting explicitly that this is safe because OnNext is called once per event, not with a key that could match both.

The else if for _arbitraryActions correctly ensures that if a consumer passes "HostBuilding" or "HostBuilt" as keys in the dictionary, they will not fire (the built-in handling takes precedence). This is reasonable but should be documented — a consumer could be surprised that these keys are silently ignored.

Classification: Advisory — the behavior is correct but the precedence rule should be called out in a doc comment or the method's XML docs.

✅ Exception Handling

The author's point in the comments is valid: arbitraryAction.Invoke(value.Value) has the same exception propagation characteristics as _configure?.Invoke(value.Value!). Both are called on the entry point thread inside a try/catch that captures exceptions into _hostTcs. Exceptions from arbitrary actions will propagate to the TrySetException path and surface to the caller, not crash the process.

💡 Naming: arbitraryActions

The name "arbitrary" is vague. Consider eventActions, diagnosticEventHandlers, or additionalEventListeners to better convey that these are keyed by diagnostic event names.


Generated by Copilot code review

Generated by Code Review for issue #127743 · ● 1.3M ·

Agent-Logs-Url: https://github.com/dotnet/runtime/sessions/c2f33a8c-2e5f-41cc-87fb-11636c3780e9

Co-authored-by: Youssef1313 <31348972+Youssef1313@users.noreply.github.com>
@Youssef1313 Youssef1313 marked this pull request as draft May 15, 2026 13:29
Copilot AI review requested due to automatic review settings May 15, 2026 13:31
@Youssef1313 Youssef1313 review requested due to automatic review settings May 15, 2026 13:31
Copy link
Copy Markdown
Contributor

Copilot AI commented May 15, 2026

Mostly repeating what copilot said, but:

  1. I think there should be tests for this.
    ...

Addressed in 08df3ae: added tests for arbitraryActions callback invocation and explicit exception propagation behavior so thrown callback exceptions fail the test path instead of escaping unchecked.

@svick
Copy link
Copy Markdown
Member

svick commented May 18, 2026

With regards to exceptions, how is the new arbitraryActions different from the existing configureHostBuilder which is just called as-is?

It seems that with how it's used, an exception in arbitraryActions (or configureHostBuilder) bubbles up through the invoked Main and ends up failing the test. So I think it already behaves the way it should and you don't need to change anything.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants