aspire icon indicating copy to clipboard operation
aspire copied to clipboard

API Proposal: builder.When(predicate, callback)

Open mitchdenny opened this issue 1 year ago • 4 comments

Background and Motivation

When using the fluent-like API in the .NET Aspire app model developers sometimes find themselves wanting to conditionally apply a method to a resource builder. For example, you might want to only add a reference to a resource in publish mode, or for a particular environment.

Today to do this you need to break out of the fluent pattern to add conditional blocks to do this because C# doesn't have an inline conditional method invocation (as far as I know). Example of current approach:

var builder = DistributedApplication.CreateBuilder(args);
var db = builder.AddPostgres("pgsql").AddDatabase("db");
var myapp = builder.AddProject<Projects.MyApp>("myapp")
                   .WithReference(db);

if (builder.ExecutionContext.Operation = DistributedApplicationOperation.Publish)
{
    var ai = builder.AddAzureApplicationInsights("ai");
    myapp.WithReference(ai);
}

This makes the code look a lot less concise. Here is an example which inspired this API proposal:

https://github.com/dotnet/eShop/pull/464

Proposed API

namespace Aspire.Hosting;

public static class ResourceBuilderExtensions
{
+    public static IResourceBuilder<T> When<T>(
+        this IResourceBuilder<T> builder,
+        Func<bool> predicate,
+        Action<IResourceBuilder<T>> callback) where T: IResource
}

Usage Examples

Applying the example above to this new API design:

var builder = DistributedApplication.CreateBuilder(args);
var db = builder.AddPostgres("pgsql").AddDatabase("db");
var myapp = builder.AddProject<Projects.MyApp>("myapp")
                   .WithReference(db);
                   .When(builder.Execution.Operation == DistributedApplicationOperation.Publish, b => {
                       var ai = builder.AddAzureApplicationInsights("ai");
                       b.WithReference(ai);
                   });

To help make things even more concise we could add reusable conditional functions:

var builder = DistributedApplication.CreateBuilder(args);
var db = builder.AddPostgres("pgsql").AddDatabase("db");
var myapp = builder.AddProject<Projects.MyApp>("myapp")
                   .WithReference(db);
                   .When(builder.Execution.PublishCondition, b => {
                       var ai = builder.AddAzureApplicationInsights("ai");
                       b.WithReference(ai);
                   });

Alternative Designs

We could add conditional logic to methods like WithReference and WithEnvironment. However this would further complicate the set of overloads on these methods so this general purpose helper method is probably the most flexible building block that doesn't require additional ongoing work to implement.

Risks

One day we might get a more elaborate method invocation syntax for C# which makes this redundant.

mitchdenny avatar Aug 06 '24 00:08 mitchdenny

@samsp-msft @mattmccleary @eerhardt @davidfowl @maddymontaquila @DamianEdwards

mitchdenny avatar Aug 06 '24 00:08 mitchdenny

@rajkumar-rangaraj @TimothyMothra @vishweshbankwar, FYI, feedback welcome.

mattmccleary avatar Aug 06 '24 02:08 mattmccleary

I’d love to see a more convincing example. Something that goes from ugly ifs to clean When calls.

davidfowl avatar Aug 09 '24 01:08 davidfowl

Another example:

var nodeApp = builder.AddJavaScriptApp("node-joke-api", "../NodeApp", "start")
    .WithPnpm()
    If([System.OperatingSystem]::IsWindows(), builder => builder.WithEnvironment("PATH", Environment.GetEnvironmentVariable("PATH") + ";" + Environment.ExpandEnvironmentVariables(@"%USERPROFILE%\AppData\Roaming\fnm\aliases\default")))
    If(![System.OperatingSystem]::IsWindows(), builder => builder.WithEnvironment("PATH", Environment.GetEnvironmentVariable("PATH") + ";" + "/tmp/directory/"))
    .WithHttpEndpoint(env: "PORT")

flcdrg avatar Nov 14 '25 23:11 flcdrg