dotnet / aspire

An opinionated, cloud ready stack for building observable, production ready, distributed applications in .NET
https://learn.microsoft.com/dotnet/aspire
MIT License
3.8k stars 450 forks source link

On Demand Startup of Resources #5851

Open afscrome opened 1 month ago

afscrome commented 1 month ago

Is there an existing issue for this?

Is your feature request related to a problem? Please describe the problem.

With the addition of being able to start/stop resources in the dashboard, it would be useful to be able to support resources which are started on demand rather than automatically on startup. A few use cases I can think of:

  1. Optional Local Dev dependencies - maybe your app has an expensive dependency that is only needed a small percentage of the time. It would be helpful to have this in your local dev dashboard, but only pay the cost of starting it up when you actually needed.
  2. CronJob resources deployed to Kubernetes - We have a few resources we deploy as nightly cron jobs to kuberentes. In local dev, it would be helpful to have these available
  3. Optional Second instance - most of the time, testing with a single . (Although perhaps this use case is better served by some kind of "Scale Replicas" command)

Describe the solution you'd like

Some kind of way to mark a resource so that Aspire doesn't automatically start the resource, instead waiting for the user to click the start button in the UI.

An alternative approach would be to provide a way for commands to dynamically add new resources, which would allow you to use a command to spawn up a new resource. This work well for options 2 & 3 above, but doesn't fit 1 so well.

Additional context

No response

davidlahuta commented 1 week ago

CRUD interface to manage resources at runtime from application code is really needed.

We are working on software which deployes services (from docker images) as our users request them. The ablity to implement this with Aspire for local development would be awesome.

davidfowl commented 1 week ago

Played with this a little based on what works today:

using Microsoft.Extensions.DependencyInjection;

var builder = DistributedApplication.CreateBuilder(args);

builder.AddContainer("redis", "redis").WithExplicitStart();

builder.Build().Run();

public static class ExplicitStartupExtensions
{
    public static IResourceBuilder<T> WithExplicitStart<T>(this IResourceBuilder<T> builder)
        where T : IResource
    {
        builder.ApplicationBuilder.Eventing.Subscribe<BeforeResourceStartedEvent>(builder.Resource, async (evt, ct) =>
        {
            var rns = evt.Services.GetRequiredService<ResourceNotificationService>();

            // This is possibly the last safe place to update the resource's annotations
            // we need to do it this late because the built in lifecycle annotations are added *very* late
            var startCommand = evt.Resource.Annotations.OfType<ResourceCommandAnnotation>().FirstOrDefault(c => c.Type == "resource-start");

            if (startCommand is null)
            {
                return;
            }

            evt.Resource.Annotations.Remove(startCommand);

            // This will block the resource from starting until the "resource-start" command is executed
            var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);

            // Create a new command that clones the start command
            var newCommand = new ResourceCommandAnnotation(
                startCommand.Type,
                startCommand.DisplayName,
                context =>
                {
                    if (!tcs.Task.IsCompleted)
                    {
                        return ResourceCommandState.Enabled;
                    }

                    return startCommand.UpdateState(context);
                },
                context =>
                {
                    if (!tcs.Task.IsCompleted)
                    {
                        tcs.SetResult();
                        return Task.FromResult(CommandResults.Success());
                    }

                    return startCommand.ExecuteCommand(context);
                },
                startCommand.DisplayDescription,
                startCommand.Parameter,
                startCommand.ConfirmationMessage,
                startCommand.IconName,
                startCommand.IconVariant,
                startCommand.IsHighlighted);

            evt.Resource.Annotations.Add(newCommand);

            await rns.PublishUpdateAsync(evt.Resource, s => s with { State = new(KnownResourceStates.Waiting, KnownResourceStateStyles.Info) });

            await tcs.Task.WaitAsync(ct);
        });

        return builder;
    }
}
davidlahuta commented 1 week ago

Thank you for the insight, but it's not quite what I have in mind.

I can envision an interface to manage Aspire resources, with its instance accessible from a running project in Aspire. Let’s write some pseudocode.

IAspireResourceManager  
{
  IAspireResource[] GetResources();
  IAspireResource GetResource(name);
  IAspireResource AddResource(name, ...)
}

IAspireResouce 
{
  Task<string> GetState();
  Task WaitFor();
  Task Remove();
  ....
  string GetConnectionString()
}

// from running Aspire application project, let's say Blazor App
IAspireResourceManager resourceManager = .... get instance from DI

resourceManager.AddResource("worker-user-1", ...);
var worker = resourceManager.GetResource("worker-user-1");
await worker.WaitFor();
...
}
davidfowl commented 1 week ago

Sorry, I wasn't replying to your request for an api to dynamically add resources, I think that's a different, harder and longer term feature request. The other features in this issue are quite doable with existing APIs.