-
Notifications
You must be signed in to change notification settings - Fork 4.8k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Fire diagnostic source events from IHostBuilder.Build #53757
Conversation
- We want to enable getting access to the IHostBuilder and IHost during the call to IHostBuilder.Build so that we can access the application's IServiceProvider from various tools. Usually, this is enabled by exposing a different entry point from the application but this technique allows us to let the application execute normally and hook important events via a diagnostic source.
Tagging subscribers to this area: @eerhardt, @maryamariyan Issue Details
|
- Added support for resolving an IServiceProvider using the new diagnostics source pattern (which doesn't require a different entrypoint)
|
||
public void OnNext(DiagnosticListener value) | ||
{ | ||
if (value.Name == "Microsoft.Extensions.Hosting") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is where we wire up to the new event to intercept calls to build.
src/libraries/Microsoft.Extensions.HostFactoryResolver/src/HostFactoryResolver.cs
Outdated
Show resolved
Hide resolved
src/libraries/Microsoft.Extensions.HostFactoryResolver/src/HostFactoryResolver.cs
Outdated
Show resolved
Hide resolved
- DiagnosticSources will publish events globally and we need to make sure there's no cross talk between tests
Would this light-up the ability to use |
Yes that's the intent:
Etc |
Nice 👍 - I've been holding off in using it in a bunch of stuff as so much of our tests use WebApplicationFactory to do integration-level tests. |
} | ||
|
||
[UnconditionalSuppressMessage("ReflectionAnalysis", "IL2026:UnrecognizedReflectionPattern", | ||
Justification = "The values being passed into Write are being consumed by the application already.")] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It isn't the "The values being passed into Write" that are the issue, but instead the recursive properties of the values.
DiagnosticSource has an EventSource bridge that allows end-users to set up "filter specs" that specify which properties of the objects passed here should be written to the EventSource. If those properties aren't used in the app, they could be trimmed.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think that's true in general but in the use case for these events, I'm not worried:
- There won't be any trimming happening in development. You don't publish an application trimmed and then run tools in it to grab the IHost and IHostBuilder.
- In the 90% case, the relevant properties/methods are going to be used as part of application bootstrapping.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Of course, if we don't expect people to be using the EventSource bridge, that could be a "good enough" reason to suppress.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There won't be any trimming happening in development. You don't publish an application trimmed and then run tools in it to grab the IHost and IHostBuilder.
What about someone else who takes a dependency on this new DiagnosticSource?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Of course, if we don't expect people to be using the EventSource bridge, that could be a "good enough" reason to suppress.
Yes this isn't a use case. There's nothing serializable on there that can be sent out of process.
What about someone else who takes a dependency on this new DiagnosticSource?
Then we'll learn about the scenario then because it won't work for that scenario we don't currently understand.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think anyone else who used the DiagnosticSource would either:
a) cast the received object to its correct type which would make the linker aware of the usage - everything works nicely here
b) private reflection - I trust this puts the onus on the new DiagnosticSource consumer to ensure the fields aren't removed
src/libraries/Microsoft.Extensions.HostFactoryResolver/src/HostFactoryResolver.cs
Outdated
Show resolved
Hide resolved
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks fine to me. Just some random nits.
src/libraries/Microsoft.Extensions.HostFactoryResolver/src/HostFactoryResolver.cs
Outdated
Show resolved
Hide resolved
src/libraries/Microsoft.Extensions.HostFactoryResolver/src/HostFactoryResolver.cs
Outdated
Show resolved
Hide resolved
src/libraries/Microsoft.Extensions.HostFactoryResolver/src/HostFactoryResolver.cs
Outdated
Show resolved
Hide resolved
// Wait before throwing an exception | ||
if (!_hostTcs.Task.Wait(_waitTimeout)) | ||
{ | ||
throw new InvalidOperationException("Unable to build IHost"); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe use a different message here - specifically call out that it timed out. That way we can tell the difference between the entrypoint returning gracefully vs. timing out.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I want the timeout to be an implementation detail. I'm also thinking this could also return null.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Who do you expect to see the message? If the message is primarily shown to people who are trying to resolve the issue (or users will relay it to someone else who resolves the issue) then it might help to make it more descriptive.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I need to check the callers but its possible null is a better return than throwing.
.../Microsoft.Extensions.HostFactoryResolver/tests/CreateHostBuilderInvalidSignature/Program.cs
Show resolved
Hide resolved
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM. I really like this approach--it's the magic I've been looking for since the beginning of ASP.NET Core.
As discussed offline, we should try hard to avoid the compile-time dependency on Microsoft.Extensions.Hosting
. We had a lot of pushback against this in the 1.1 days since it adds it to all app models (see dotnet/efcore#7835 from a WPF user for example)
/cc @ajcvickers
src/libraries/Microsoft.Extensions.HostFactoryResolver/src/HostFactoryResolver.cs
Show resolved
Hide resolved
… events to fire - We want to fail fast if we know this version of hosting will never fire events of if the hosting assembly fails to load. Added a version check. - Allow the caller to specify a wait timeout.
try | ||
{ | ||
// Attempt to load hosting and check the version to make sure the events | ||
// even have a change of firing (they were adding in .NET >= 6) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit:
// even have a change of firing (they were adding in .NET >= 6) | |
// even have a chance of firing (they were added in .NET >= 6) |
// Wait before throwing an exception | ||
if (!_hostTcs.Task.Wait(_waitTimeout)) | ||
{ | ||
throw new InvalidOperationException("Unable to build IHost"); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Who do you expect to see the message? If the message is primarily shown to people who are trying to resolve the issue (or users will relay it to someone else who resolves the issue) then it might help to make it more descriptive.
if (_stopApplication) | ||
{ | ||
// Stop the host from running further | ||
throw new StopTheHostException(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The prize for dirty hackery this week : D
Is it important to guarantee that the thread really does stop? It would be easy enough for an app to throw a try/catch around the part of their code where this gets raised so that it never gets back to the entrypoint. Your tool code will be running in parallel with the user's unknown error handling app code. It doesn't seem obviously harmful to me but figured I mention it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Disposal is one of the things I'm worried about and why I think throwing to stop execution makes the most sense here. The main app never gets a handle on the application here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It would be easy enough for an app to throw a try/catch around the part of their code where this gets raised so that it never gets back to the entrypoint.
The thing that stops this from happening in the throw case is the fact that the application never gets access to the IHost instance. They can't call build again on it because double building throws. Even if they catch the exception and do something else, the IHost isntance that came out of Build is never observed by the application.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, I wasn't imagining they continue normally, more like they have some complex error handling code that will be running in parallel.
the application never gets access to the IHost instance
[Joking] What do you mean? This PR just added the official mechanism that lets everyone access IHost without the pesky inconvenience of needing Build() to return it to you ;p
@@ -31,6 +40,35 @@ internal sealed class HostFactoryResolver | |||
return ResolveFactory<THostBuilder>(assembly, CreateHostBuilder); | |||
} | |||
|
|||
public static Func<string[], object>? ResolveHostFactory(Assembly assembly, TimeSpan? waitTimeout = null, bool stopApplication = true, Action<object>? configureHostBuilder = null) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't see a test that uses configureHostBuilder
. Do we need this parameter?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ok, then it would be great if we had a test ensuring it doesn't break.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
2 things:
- As of right now, ASP.NET Core will need to use it so it kinda has to work or tests will fail regardless 😄.
- I agree with you and will add a test.
IHostBuilder
andIHost
during the call toIHostBuilder.Build
so that we can access the application'sIServiceProvider
from various tools (like the EntityFramework migrations tool or scaffolding). Usually, this is enabled by exposing a different entry point from the application (CreateHostBuilder
) but this technique allows us to let the application execute normally and hook important events via a diagnostic source.TODO: I need to update theHostFactoryResolver
to support this new pattern.TODO: Add tests for various scenarios:Exception thrown before IHostBuilder.Build is called where the exception propagatesException thrown before IHostBuilder.Build is called where the exception does not propagatecc @halter73