Skip to content
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

Merged
merged 17 commits into from
Jun 8, 2021

Conversation

davidfowl
Copy link
Member

@davidfowl davidfowl commented Jun 5, 2021

  • 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 (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.
  • Updated HostFactoryResolver and added new APIs for resolving the IHostBuilder from an assembly with an entry point.
  • Added tests

TODO: I need to update the HostFactoryResolver to support this new pattern.

TODO: Add tests for various scenarios:

  • Exception thrown before IHostBuilder.Build is called where the exception propagates
  • Exception thrown before IHostBuilder.Build is called where the exception does not propagate
  • Crazy scenarios where multiple host builders are fired up at during the call to main (not sure this is important)

cc @halter73

- 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.
@ghost
Copy link

ghost commented Jun 5, 2021

Tagging subscribers to this area: @eerhardt, @maryamariyan
See info in area-owners.md if you want to be subscribed.

Issue Details
  • 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.
Author: davidfowl
Assignees: -
Labels:

area-Extensions-Hosting

Milestone: -

- 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")
Copy link
Member Author

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.

@davidfowl davidfowl marked this pull request as ready for review June 6, 2021 07:36
- DiagnosticSources will publish events globally and we need to make sure there's no cross talk between tests
@davidfowl davidfowl requested a review from halter73 June 7, 2021 04:29
@martincostello
Copy link
Member

Would this light-up the ability to use WebApplicationFactory<T> (and maybe top-level programs) with the new minimal hosting APIs?

@davidfowl
Copy link
Member Author

Yes that's the intent:

  • EF migrations
  • WebApplicationFactory
  • NSwag
  • Scaffolding

Etc

@martincostello
Copy link
Member

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.")]
Copy link
Member

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.

Copy link
Member Author

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.

Copy link
Member

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.

Copy link
Member

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?

Copy link
Member Author

@davidfowl davidfowl Jun 7, 2021

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.

Copy link
Member

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

Copy link
Member

@eerhardt eerhardt left a 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.

// Wait before throwing an exception
if (!_hostTcs.Task.Wait(_waitTimeout))
{
throw new InvalidOperationException("Unable to build IHost");
Copy link
Member

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.

Copy link
Member Author

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.

Copy link
Member

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.

Copy link
Member Author

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.

Copy link
Contributor

@bricelam bricelam left a 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

… 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)
Copy link
Member

Choose a reason for hiding this comment

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

Nit:

Suggested change
// 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");
Copy link
Member

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();
Copy link
Member

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.

Copy link
Member Author

@davidfowl davidfowl Jun 8, 2021

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.

Copy link
Member Author

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.

Copy link
Member

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)
Copy link
Member

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?

Copy link
Member Author

Choose a reason for hiding this comment

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

Copy link
Member

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.

Copy link
Member Author

Choose a reason for hiding this comment

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

2 things:

  1. As of right now, ASP.NET Core will need to use it so it kinda has to work or tests will fail regardless 😄.
  2. I agree with you and will add a test.

@davidfowl davidfowl merged commit 543a983 into main Jun 8, 2021
@davidfowl
Copy link
Member Author

@eerhardt and @noahfalk I need to get this merged so I can react in several places down stream. I will react to your feedback in another PR so keep commenting.

@eerhardt eerhardt deleted the davidfowl/fire-hosting-events branch June 8, 2021 15:52
@davidfowl davidfowl added this to the 6.0.0 milestone Jun 9, 2021
@ghost ghost locked as resolved and limited conversation to collaborators Jul 11, 2021
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants