-
Notifications
You must be signed in to change notification settings - Fork 531
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
Allow specifying an Event Hub to use as the default EntityPath for namespace connection string #7105
base: main
Are you sure you want to change the base?
Conversation
If we can agree on tihs, I'll send a PR for the same thing in Azure Service Bus for queue/topic in EntityPath, or I can put it in this one |
Co-authored-by: David Fowler <[email protected]>
Co-authored-by: David Fowler <[email protected]>
Does this also work when deployed? |
It's funny you should ask -- I had just deployed the playground project to test, and I found a bug. I'll report back when fixed. |
Well, sh*t. It turns out that there's no way to officially pass the event hub name when using the FQNS + TokenCredential for a real Event Hub. It's fine for the emulator, because the non-tokencredential connection string allows you to pass an EntityPath. I had mistakely believed you could include the hub name as part of the Uri in the non-emulator case. One way to do it would be to add Aspire-specific handling of a FQNS that includes the hub name as a query string, e.g. The other way might be to emit an environment variable, but then this needs even more thought for scoping/namespacing it. |
We can use our own connection string format that the client understands. This is what openai does |
What about setting the ENV that will be bound to the Another comment on this PR, late one sorry, I personally prefer calling a helper like |
Hi @sebastienros - I had thought of that in an earlier point in time, but given that there are five different clients for eventhub, we'd have to inject five env vars to bind to all client settings. Also, they would take precedence over client appsettings which might be at best surprising and at worst possibly breaking, right? If we use a custom connection string then it's just a hint and the regular configuration should take precedence, if provided. As for the WithEntityPath idea, I guess so? What does everyone else think? |
I prefer a single connection string… This brings back our connection info discussion 🙂 |
Alrighty -- so I'll add a query path hint in the FQNS. And are we good with having WithEntityPath(string) ? It would just set IsDefaultEntity on the model, and I'd drop the visibility to internal on the field. I take it we don't want to start down the "two ways to do everything" path. |
before making the change, can we compare both API samples? |
Avar eventHub = builder.AddAzureEventHubs("eventhubns")
.RunAsEmulator()
.WithHub("hub1")
.WithHub("hub2", h => h.IsDefaultEntity = true); vs Bvar eventHub = builder.AddAzureEventHubs("eventhubns")
.RunAsEmulator()
.WithHub("hub1")
.WithHub("hub2")
.WithDefaultEntity("hub2"); // WithEntityPath ? At first you might think why not |
I am fine with both approaches, my preference being an extension method (whatever name makes more sense), or an argument on the Does my suggestion to generate a separate ENV to flow to the settings make sense? It would be prefixed with the resource name to bind to the correct settings. |
Yes, I thought I had mentioned this above. It does make sense from a mechanical perspective, but like I had said, there are five different clients that ship in the integration and you'd have to inject an ENV var for all of them since you cannot know in the hosting side which clients will be used. Personally, I prefer to inline a hint in the connectionstring. It keeps the scope narrower. Although injecting environment variables means we could probably get away without modifying the clients, it might cause unexpected behaviour when someone tries to specify/override the event hub in appsetting.json -- the env var will always win for configuration. |
Isn't it of higher priority than ENVs? And then the configure callback has the most priority.
I checked the code as I am not familiar with it, I see the issue is each component has its own settings class, but the Would something like this work: protected override void BindSettingsToConfiguration(AzureMessagingEventHubsBufferedProducerSettings settings,
IConfiguration configuration)
{
configuration.Bind((AzureMessagingEventHubsSettings)settings);
configuration.Bind(settings);
} If not set in component specific section, it would take the one from the apphost config. |
Nope. AppSettings are loaded first, then environment. Last one wins. Priority is effectively: https://learn.microsoft.com/en-us/dotnet/core/extensions/configuration |
What happens when you use this feature with azure functions, none of this applies right? |
It only improves the scenario for Functions that use the SASKey style connection string that officially supports EntityPath. Existing Function bindings that are explicit about the Event Hub name will override this, so I don't forsee any breaking behaviour there. For those bound via an FQNS + managed identity, I don't touch it since we don't have control over the client to parse out the hint. |
Ooops, I accidentally checked in overrides in the playground to use AzureCliCredential for my local testing. I'll remove them in the next commit. |
update note for README about WithDefaultEntity
Ready for review @davidfowl I've updated the PR description with more details to help with the review. |
src/Components/Aspire.Azure.Messaging.EventHubs/EventHubsComponent.cs
Outdated
Show resolved
Hide resolved
….com/oising/aspire into add-isdefaultentity-eventhub-hosting
/// <param name="builder">The Azure Event Hubs resource builder.</param> | ||
/// <param name="name">The name of the Event Hub.</param> | ||
/// <returns>A reference to the <see cref="IResourceBuilder{T}"/>.</returns> | ||
public static IResourceBuilder<AzureEventHubsResource> WithDefaultEntity(this IResourceBuilder<AzureEventHubsResource> builder, [ResourceName] string name) |
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.
Is there a reason we'd want to use this without having WithHub? Example: connecting to an existing azure resource without the need to call WithHub
for each of the existing ones.
After all WithHub("foo")
doesn't ensure it actually exists on the resource either. Almost like we are asking users twice in that case.
And related to this, as a user it might be nice to be able to call it twice, the second being one winning. That would mean store the hub name in the resource itself, instead of having a bool on each resource to verify the integrity (might be simpler too).
Is "Default" in DefaultEntity
something concrete? Why not called WithEntityPath()
? (ignore me if I already asked ;) )
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.
Is there a reason we'd want to use this without having WithHub? Example: connecting to an existing azure resource without the need to call
WithHub
for each of the existing ones.
When you mentioned this first, I had to think about it and I don't believe there's a scenario where we'd want to set a default entity path without having any associated hubs in the resource. If we want to use a pre-existing resource that already has hubs, we would use AddConnectionString. Anything that uses AddAzureEventHubs needs to provide at least one hub, or health checks will fail. I understand it is possible to want to create a namespace without hubs and have hubs created dynamically (i.e. some dapr patterns encourage dthis but you have to grant it management permissions.)
After all
WithHub("foo")
doesn't ensure it actually exists on the resource either. Almost like we are asking users twice in that case.
The resource itself doesn't neccessarily exist either -- all this does is mutate the model. At runtime, it's either projected into the emulator config, or emitted in the manifest. I don't think this is our concern?
And related to this, as a user it might be nice to be able to call it twice, the second being one winning. That would mean store the hub name in the resource itself, instead of having a bool on each resource to verify the integrity (might be simpler too).
I'm not sure how this scenario ("nice to be able to call it twice") would arise beyond a mistake?
Is "Default" in
DefaultEntity
something concrete? Why not calledWithEntityPath()
? (ignore me if I already asked ;) )
I say "default" because you can override it in the client settings.
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.
@sebastienros -- you're right -- it does make sense to allow someone to call it multiple times with different hubs, with last one winning. I've had a head cold all week and for some reason I didn't quite get it, lol. I'll fix that now.
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'm renaming WithDefaultEntity
to WithEntityPath
since this now opens up:
var ns = builder.AddAzureEventHubs("eventhubns")
.RunAsEmulator()
.WithHub("hub")
.WithHub("hub2");
builder.AddProject<Projects.EventHubsConsumerA>("consumera")
.WithReference(ns.WithEntityPath("hub"));
builder.AddProject<Projects.EventHubsConsumerB>("consumerb")
.WithReference(ns.WithEntityPath("hub2"));
// ...
@davidfowl @eerhardt ^ ?
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.
Although ... this approach could be troublesome due to it mutating the parent. Example:
ar ns = builder.AddAzureEventHubs("eventhubns")
.RunAsEmulator()
.WithHub("hub")
.WithHub("hub2");
var hub = ns.WithEntityPath("hub");
var hub2 = ns.WithEntityPath("hub2");
builder.AddProject<Projects.EventHubsConsumerA>("consumera")
.WithReference(hub);
builder.AddProject<Projects.EventHubsConsumerB>("consumerb")
.WithReference(hub2);
So here, both get pointed at hub2. Oops. What are your thoughts? I like that With vs Add has well defined semantics. Is it clear enough though to dissuade people doing things like this? Maybe this isn't the only API that could fall foul to this, so it's an accepted tradeoff?
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 more I think about it, I think it's okay. There have to be many places where people might assign results from With* extensions and expect different semantics. This is why the distinction exists, right?
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 this is getting at the comment @davidfowl made here: #7105 (comment). The problem we are running into is that EventHub "Hub" instances aren't a Resource
in Aspire. Which means they aren't referencable/addressable. So that's the struggle with the above code. If instead it looked more like a database in typical DBMS systems (like SqlServer or Postgres) the above code would be:
IResourceBuilder<AzureEventHubsResource> ns = builder.AddAzureEventHubs("eventhubns")
.RunAsEmulator();
IResourceBuilder<AzureEventHubsHubResource> hub = ns.AddHub("hub");
IResourceBuilder<AzureEventHubsHubResource> hub2 = ns.AddHub("hub2");
builder.AddProject<Projects.EventHubsConsumerA>("consumera")
.WithReference(hub);
builder.AddProject<Projects.EventHubsConsumerB>("consumerb")
.WithReference(hub2);
ConsumerA would get a connection string for eventhubns?EntityPath=hub
and ConsumerB would get a connection string for eventhubns?EntityPath=hub2
.
This feels more natural and consistent with the rest of Aspire.
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.
Absolutely, and I completely agree. I had another PR open where I was creating child resources that could be deep-linked in this style, but it stalled because of refactoring work by the team in main. The thing is that we have obsolete APIs for AddEventHub that mutate the parent, so a stop-gap solution would be nice until the obsoleted API can be removed and then an AddHub added that does what you show. I guess the question is, do we go with this intermediary solution or do we throw it away and target 10.0 with AddHub. That's for you guys to decide.
The thing that feels strange about this to be is that fact that we would mutate the original resource to have a default entity instead of making a child resource (like we had before), which could be a way to "deep link" (to use @mitchdenny's words) into an event hub instance. That way you aren't limited to one and there's no strange default behavior to implement. |
I already started that PR before you guys got entangled in internal debate and switched to this model, so then I spent my time on this instead. This is frustrating as a contributor who isn't paid to do this. This is just a hint to the model which gets projected to emulator config, or into bicep via the manifest. I don't see a problem with the namespace (the primary resource) having a "default entity path" hint, since it can be overridden later. In your own words, "With" mutates the resource model, and "Add" creates new child resources. Just go with this now, you're already deprecated v1 of the AddEventHub calls, remove them in v10, then add them back in v11 with child resources for event hubs. Deep linking can still happen without interfering with having a default entity path on the namespace resource. update: "...making a child resource (like we had before)" -- you guys never had child resources for azure event hub (nor service bus.) You had AddEventHub calls that mutated the namespace. This (welcome) distinction between With and Add is good, but it's new. |
src/Components/Aspire.Azure.Messaging.EventHubs/EventProcessorClientComponent.cs
Outdated
Show resolved
Hide resolved
src/Components/Aspire.Azure.Messaging.EventHubs/EventHubsComponent.cs
Outdated
Show resolved
Hide resolved
src/Components/Aspire.Azure.Messaging.EventHubs/EventHubsComponent.cs
Outdated
Show resolved
Hide resolved
…ClientComponent.cs Co-authored-by: Eric Erhardt <[email protected]>
…nent.cs Co-authored-by: Eric Erhardt <[email protected]>
…nent.cs Co-authored-by: Eric Erhardt <[email protected]>
Co-authored-by: Jesse Squire <[email protected]>
kill whitespace
Description
Only one EventHub can be specified as the default entity path. The entity path can still be overridden by settings in integrations. This method can be called multiple times with the same name.
An exception is thrown early if WithDefaultEntity sees that a hub has already been flagged.
Manual testing (playground project):
azd up
full deployment to azureNotes
I cleaned up the validation logic and tried to simplify it in the base client shared component
EventHubsComponent.cs
in the methodEnsureConnectionStringOrNamespaceProvided
. I also fixed an edge case bug that already existed for parsing. I added#define
clauses to the playground for disabling the emulator to make it easier to test. There are also some#define
conditionals in the projects to allow using AzureCli credential source, but functionally the playground project as it stands is unchanged.As the FQNS & token credential method doesn't work with docker and the emulator, I couldn't add local unit testing. That said, all scenarios were fully tested, and the code that extracts the event hub name hint for the FQNS method is shared among all five client types, so I'm confident it's solid.
For those not super familiar with the quirks of event hub connection strings, this PR ensures that the SAS-key style connection string has the
EntityPath
keyword set ifWithDefaultEntity(string hubName)
is called:i.e.
Endpoint=sb://localhost:57195;SharedAccessKeyName=RootManageSharedAccessKey;SharedAccessKey=SAS_KEY_VALUE;UseDevelopmentEmulator=true;EntityPath=hub
For the FQNS (Uri) & Token Credential connection, this PR stashes a hint in the FQNS (fully qualified namespace) Uri via a QueryString parameter:
https://foobar.servicebus.azure.net:443/?EntityPath=hub
The client integration packages will extract this hint from the Url and assign it to the client's
settings.EventHubName
if it is found to be unset, thus using the prior tested code path of the settings callback.Fixes #7093
Checklist
<remarks />
and<code />
elements on your triple slash comments?breaking-change
template):doc-idea
template):