aspire
aspire copied to clipboard
Consider providing an external endpoint resource
For external endpoint resources, I'm using this code:
public class ResourceWithServiceDiscovery : IResourceWithServiceDiscovery
{
public required string Name { get; init; }
public required ResourceMetadataCollection Annotations { get; init; }
}
And using it like this:
var remoteResource = new ResourceWithServiceDiscovery
{
Name = "someremoteresource",
Annotations = [
new AllocatedEndpointAnnotation(
"http",
ProtocolType.Tcp,
remoteHubConfig.GetValue<string>("Address")!,
remoteHubConfig.GetValue<int>("Port"),
remoteHubConfig.GetValue<string>("Scheme")!)
]
};
It would be nice to have something like this built in and showing up in the dashboard.
https://github.com/dotnet/aspire/pull/2390
@paulomorgado if you would like to send a PR for this, that would be great 😄. I think we'd want to model it similarly to how we did AddConnectionString
(it's just a parameter). Do you want to mock up an API in the issue?
@davidfowl, AddConnectionString
is a preview 4 thing only, right?
And it creates a resource builder behind the scenes, right?
How does that behave if that connection string is added to more than one resource? How would that behave?
AddConnectionString
adds a parameter resource configured to be accessed like a connection string.
How does that behave if that connection string is added to more than one resource? How would that behave?
It's a resource by itself, it's not added to a resource.
Is this preview 4 only, or can I use it on preview 3?
Preview 4
@davidfowl how would adding an external endpoint like AddConnectionString
be any different from using
public static IResourceBuilder<TDestination> WithReference<TDestination>(this IResourceBuilder<TDestination> builder, string name, Uri uri)
where TDestination : IResourceWithEnvironment
@davidfowl, does this look like what you're looking for?
/// <summary>
/// Adds a parameter to the distributed application but wrapped in a resource with a URL for use with <see cref="ResourceBuilderExtensions.WithReference{TDestination}(IResourceBuilder{TDestination}, IResourceBuilder{IResourceWithServiceDiscovery})"/>
/// </summary>
/// <param name="builder">Distributed application builder</param>
/// <param name="name">Name of parameter resource.</param>
/// <param name="uri">The uri of the service.</param>
/// <returns>Resource builder for the parameter.</returns>
/// <exception cref="DistributedApplicationException"></exception>
public static IResourceBuilder<IResourceWithServiceDiscovery> AddEndpoint(this IDistributedApplicationBuilder builder, string name, Uri uri)
{
if (!uri.IsAbsoluteUri)
{
throw new InvalidOperationException("The uri for service reference must be absolute.");
}
if (uri.AbsolutePath != "/")
{
throw new InvalidOperationException("The uri absolute path must be \"/\".");
}
var parameterBuilder = builder.AddParameter(
name: name,
callback: uri.ToString,
secret: false,
connectionString: false);
parameterBuilder.WithAnnotation(
new AllocatedEndpointAnnotation(
name: "http",
protocol: ProtocolType.Tcp,
address: uri.Host,
port: uri.Port,
scheme: uri.Scheme));
var surrogate = new ResourceWithServiceDiscoverySurrogate(parameterBuilder.Resource);
return builder.CreateResourceBuilder(surrogate);
}
where
internal class ResourceWithServiceDiscoverySurrogate(IResource innerResource) : IResourceWithServiceDiscovery
{
public string Name => innerResource.Name;
public ResourceAnnotationCollection Annotations => innerResource.Annotations;
}
It kind of looks odd.
I'd like it to be an EndpointReference
, but it doesn't look like a good idea.
What's odd?
I'm still trying to wrap my head around it.
Is this what you were suggesting?
Without the AllocatedEndpointAnnotation
part. I think we would handle this resource type specially in WithReference.
I have configuration in one of my Aspire projects to choose between project, container and service hosted somewhere.
I think it would be valuable to get a EndpointReference
from that. It should, at least, be a IResourceWithEndpoints
.
How would I get an EndpointReference
without an AllocatedEndpointAnnotation
?
I guess need the full picture. Show me how you plan to use this resource.
This is kind of what I have now:
var dependencyEndpointReference = CreateDependencyEndpointReference();
dependent1.WithReference(dependencyEndpointReference);
dependent2.WithReference(dependencyEndpointReference);
dependent3.WithReference(dependencyEndpointReference);
EndpointReference CreateDependencyEndpointReference()
{
if (builder.Configuration.GetSection("VisionBox:ConneXt:Demo:Settings:Dependency") is { } dependencyConifg && dependencyConifg.Exists())
{
switch (DependencyConifg.GetValue<string>("$mode"))
{
case "Container":
if (DependencyConifg.GetSection("Container") is { } DependencyContainerConfig && DependencyContainerConfig.Exists())
{
var DependencyContainer = builder.AddContainer(
"Dependency",
DependencyContainerConfig.GetValue<string>("Image")!,
DependencyContainerConfig.GetValue<string>("Tag") ?? "latest")
.WithEndpoint(containerPort: 8080, hostPort: 5012, name: "http", scheme: "http")
.WithEnvironment("OTEL_DOTNET_EXPERIMENTAL_OTLP_EMIT_EVENT_LOG_ATTRIBUTES", "true")
.WithEnvironment("OTEL_DOTNET_EXPERIMENTAL_OTLP_EMIT_EXCEPTION_LOG_ATTRIBUTES", "true")
.WithOtlpExporter()
.WithEnvironment("OTEL_SERVICE_NAME", "Dependency")
.WithReference(DependencyIdpEndpointReference)
.ConfigureHttpServerAuth()
.ConfigureLogging()
;
var DependencyContainerResource = new ResourceWithServiceDiscovery { Name = DependencyDependencyName, Annotations = DependencyContainer.Resource.Annotations };
var DependencyContainerEndpointReference = new EndpointReference(DependencyContainerResource, "http");
return DependencyContainerEndpointReference;
}
break;
case "Remote":
if (DependencyConifg.GetSection("Remote") is { } remoteDependencyConfig && remoteDependencyConfig.Exists())
{
var remoteDependencyResource = new ResourceWithServiceDiscovery
{
Name = DependencyDependencyName,
Annotations = [
new AllocatedEndpointAnnotation(
"http",
ProtocolType.Tcp,
remoteDependencyConfig.GetValue<string>("Address")!,
remoteDependencyConfig.GetValue<int>("Port"),
remoteDependencyConfig.GetValue<string>("Scheme")!)
]
};
var remoteDependencyEndpointReference = new EndpointReference(remoteDependencyResource, "http");
return remoteDependencyEndpointReference;
}
break;
}
}
throw new InvalidOperationException("Missing Dependency configuration.");
}
Seems like to doesn’t need to be an EndpointReference. We just need a common interface between those 2 resource types.
What would that be?
Maybe we need an AllocatedEndpointReference
that is the current EndpointReference
and allow for other types of EndpointReference
.
Thinking about this more you might be right that supporting EndpointReference
might be a nice thing to do.
var remote = builder.AddUrl("foo", "http://foo.remote.service");
builder.AddProject<Projects.Api>("api")
.WithReference(remote); // Service discovery behavior
builder.AddProject<Projects.Frontend>("fe")
.WithEnvironment("REMOTE_URL", remote.Endpoint); // This is an endpoint reference.
The only question in my mind is do we want this to be a parameter resource for publishing or does the same url get used verbatim ?
It might make sense to be a parameter resource for publishing.
If it's not, it can be excluded from publishing.
Hi @davidfowl, how are we going to progress with this?
At the moment I-m using this:
internal class ExternalHostResource(string name, string host) : Resource(name), IResourceWithEndpoints
{
public string Host { get; } = host;
}
internal static class ExternalHostResourceBuilderExtensions
{
public static IResourceBuilder<ExternalHostResource> AddExternalHost(
this IDistributedApplicationBuilder builder,
string name,
string hostname)
{
builder.Services.TryAddLifecycleHook<ExternalHostResourceLifecycleHook>();
return builder.AddResource(new ExternalHostResource(name, hostname))
.WithInitialState(new CustomResourceSnapshot
{
ResourceType = "Endpoint",
State = "Running",
Properties = [
new (CustomResourceKnownProperties.Source, hostname),
new ("Running", KnownResourceStateStyles.Success)
]
});
}
public static IResourceBuilder<ExternalHostResource> AddExternalHost(
this IDistributedApplicationBuilder builder,
string name,
Uri uri,
bool isProxied = false,
bool isExternal = true)
=> builder.AddExternalHost(name, uri.Host)
.WithEndpoint(targetPort: uri.Port, scheme: uri.Scheme, name: uri.Scheme, isProxied: isProxied, isExternal: isExternal);
}
internal sealed class ExternalHostResourceLifecycleHook : IDistributedApplicationLifecycleHook
{
private readonly ResourceNotificationService notificationService;
private readonly ResourceLoggerService loggerService;
public ExternalHostResourceLifecycleHook(ResourceNotificationService notificationService, ResourceLoggerService loggerService)
{
this.notificationService = notificationService;
this.loggerService = loggerService;
}
internal sealed class ExternalHostResourceLifecycleHook : IDistributedApplicationLifecycleHook
{
private readonly ResourceNotificationService notificationService;
private readonly ResourceLoggerService loggerService;
public ExternalHostResourceLifecycleHook(ResourceNotificationService notificationService, ResourceLoggerService loggerService)
{
this.notificationService = notificationService;
this.loggerService = loggerService;
}
public async Task BeforeStartAsync(DistributedApplicationModel appModel, CancellationToken cancellationToken = default)
{
foreach (var resource in appModel.Resources)
{
if (resource is ExternalHostResource externalEndpointResource)
{
var uris = new List<UrlSnapshot>();
foreach (var annotation in externalEndpointResource.Annotations)
{
if (annotation is EndpointAnnotation endpointAnnotation)
{
endpointAnnotation.IsProxied = false;
endpointAnnotation.IsExternal = true;
if (endpointAnnotation.AllocatedEndpoint is null)
{
endpointAnnotation.AllocatedEndpoint = new(
endpointAnnotation,
externalEndpointResource.Host,
endpointAnnotation.TargetPort.HasValue
? endpointAnnotation.TargetPort.GetValueOrDefault()
: new UriBuilder(endpointAnnotation.UriScheme, externalEndpointResource.Host).Uri.Port);
}
uris.Add(new UrlSnapshot(endpointAnnotation.Name, externalEndpointResource.GetEndpoint(endpointAnnotation.Name).Url, false));
}
}
if (uris.Count != 0)
{
await notificationService.PublishUpdateAsync(resource, state => state with
{
Urls = [.. state.Urls, .. uris]
});
}
}
}
}
}
...
var resource = builder.AddExternalHost(name: "hub-service", new Uri("https://host:port/")
;
Thanks @paulomorgado!
For the scenario I'm just having I changed ExternalHostResource
to implement IResourceWithServiceDiscovery
which allows to reference the resource using WithReference
.
@christiannagel,
Thanks @paulomorgado!
For the scenario I'm just having I changed
ExternalHostResource
to implementIResourceWithServiceDiscovery
which allows to reference the resource usingWithReference
.
I'm starting to think that a parameter implementing IResourceWithServiceDiscovery
, like ResourceWithConnectionStringSurrogate
returned by ParameterResourceBuilderExtensions.AddConnectionString
implements IResourceWithConnectionString
.
I've created a NuGet package to provide the functionality outlined in this issue: ExternalEndpoint.Aspire.Hosting.
The source can be found here.
I've created a NuGet package to provide the functionality outlined in this issue: ExternalEndpoint.Aspire.Hosting.
The source can be found here.
That's pretty much what I have myself, but I'm thinking this should come from a parameter, like AddConnectionString
.
I can add another method to add it from a parameter. Then everyone is free to choose.
1.1.0 now includes another extension Method to add an External Endpoint from a parameter.