Skip to content

Commit

Permalink
Add AddParameter overloads that take a constant and a ParameterDefault
Browse files Browse the repository at this point in the history
  • Loading branch information
davidebbo committed Sep 3, 2024
1 parent 6db273b commit dfba0ba
Show file tree
Hide file tree
Showing 3 changed files with 143 additions and 1 deletion.
41 changes: 40 additions & 1 deletion src/Aspire.Hosting/ParameterResourceBuilderExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,43 @@ public static IResourceBuilder<ParameterResource> AddParameter(this IDistributed
return builder.AddParameter(name, parameterDefault => GetParameterValue(builder.Configuration, name, parameterDefault), secret: secret);
}

/// <summary>
/// Adds a parameter resource to the application, providing a default value to beused as a fallback.
/// </summary>
/// <param name="builder">Distributed application builder</param>
/// <param name="name">Name of parameter resource</param>
/// <param name="defaultValue"></param>
/// <param name="secret">Optional flag indicating whether the parameter should be regarded as secret.</param>
/// <returns></returns>
[System.Diagnostics.CodeAnalysis.SuppressMessage("ApiDesign", "RS0026:Do not add multiple public overloads with optional parameters",
Justification = "third parameters are mutually exclusive.")]
public static IResourceBuilder<ParameterResource> AddParameter(this IDistributedApplicationBuilder builder, string name, string defaultValue, bool secret = false)
{
// An alternate implementation is to use some ConstantParameterDefault implementation that always returns the default value.
// However, doing this causes a "default": {} to be written to the manifest, which is not valid.
// And note that we ignore parameterDefault in the callback, because it can never be non-null, and we want our own default.
return builder.AddParameter(name, parameterDefault => builder.Configuration[$"Parameters:{name}"] ?? defaultValue, secret: secret);
}

/// <summary>
/// Adds a parameter resource to the application, providing a ParameterDefault to be used as a fallback.
/// </summary>
/// <param name="builder">Distributed application builder</param>
/// <param name="name">Name of parameter resource</param>
/// <param name="defaultValue"></param>
/// <param name="secret">Optional flag indicating whether the parameter should be regarded as secret.</param>
/// <returns></returns>
[System.Diagnostics.CodeAnalysis.SuppressMessage("ApiDesign", "RS0026:Do not add multiple public overloads with optional parameters",
Justification = "third parameters are mutually exclusive.")]
public static IResourceBuilder<ParameterResource> AddParameter(this IDistributedApplicationBuilder builder, string name, ParameterDefault defaultValue, bool secret = false)
{
return builder.AddParameter(
name,
parameterDefault => GetParameterValue(builder.Configuration, name, parameterDefault),
secret: secret,
parameterDefault: defaultValue);
}

private static string GetParameterValue(IConfiguration configuration, string name, ParameterDefault? parameterDefault)
{
var configurationKey = $"Parameters:{name}";
Expand All @@ -39,10 +76,12 @@ internal static IResourceBuilder<ParameterResource> AddParameter(this IDistribut
string name,
Func<ParameterDefault?, string> callback,
bool secret = false,
bool connectionString = false)
bool connectionString = false,
ParameterDefault? parameterDefault = null)
{
var resource = new ParameterResource(name, callback, secret);
resource.IsConnectionString = connectionString;
resource.Default = parameterDefault;

var state = new CustomResourceSnapshot()
{
Expand Down
2 changes: 2 additions & 0 deletions src/Aspire.Hosting/PublicAPI.Unshipped.txt
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,8 @@ static Aspire.Hosting.ApplicationModel.ResourceExtensions.GetEnvironmentVariable
Aspire.Hosting.ApplicationModel.ResourceNotificationService.WaitForResourceAsync(string! resourceName, string? targetState = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task!
Aspire.Hosting.ApplicationModel.ResourceNotificationService.WaitForResourceAsync(string! resourceName, System.Collections.Generic.IEnumerable<string!>! targetStates, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task<string!>!
static Aspire.Hosting.ContainerResourceBuilderExtensions.WithContainerLifetime<T>(this Aspire.Hosting.ApplicationModel.IResourceBuilder<T!>! builder, Aspire.Hosting.ApplicationModel.ContainerLifetimeType lifetimeType) -> Aspire.Hosting.ApplicationModel.IResourceBuilder<T!>!
static Aspire.Hosting.ParameterResourceBuilderExtensions.AddParameter(this Aspire.Hosting.IDistributedApplicationBuilder! builder, string! name, Aspire.Hosting.ApplicationModel.ParameterDefault! defaultValue, bool secret = false) -> Aspire.Hosting.ApplicationModel.IResourceBuilder<Aspire.Hosting.ApplicationModel.ParameterResource!>!
static Aspire.Hosting.ParameterResourceBuilderExtensions.AddParameter(this Aspire.Hosting.IDistributedApplicationBuilder! builder, string! name, string! defaultValue, bool secret = false) -> Aspire.Hosting.ApplicationModel.IResourceBuilder<Aspire.Hosting.ApplicationModel.ParameterResource!>!
static Aspire.Hosting.ProjectResourceBuilderExtensions.WithEndpointsInEnvironment(this Aspire.Hosting.ApplicationModel.IResourceBuilder<Aspire.Hosting.ApplicationModel.ProjectResource!>! builder, System.Func<Aspire.Hosting.ApplicationModel.EndpointAnnotation!, bool>! filter) -> Aspire.Hosting.ApplicationModel.IResourceBuilder<Aspire.Hosting.ApplicationModel.ProjectResource!>!
Aspire.Hosting.DistributedApplicationExecutionContext.DistributedApplicationExecutionContext(Aspire.Hosting.DistributedApplicationExecutionContextOptions! options) -> void
Aspire.Hosting.DistributedApplicationExecutionContext.ServiceProvider.get -> System.IServiceProvider!
Expand Down
101 changes: 101 additions & 0 deletions tests/Aspire.Hosting.Tests/AddParameterTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

using Aspire.Hosting.Lifecycle;
using Aspire.Hosting.Publishing;
using Aspire.Hosting.Utils;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Xunit;
Expand Down Expand Up @@ -111,6 +112,106 @@ public void ParametersWithConfigurationValueDoNotGetDefaultValue()
Assert.Equal("ValueFromConfiguration", parameterResource.Value);
}

[Fact]
public async Task ParametersWithDefaultValueStringOverloadOnlyUsedIfNoConfigurationValue()
{
var appBuilder = DistributedApplication.CreateBuilder();

appBuilder.Configuration.AddInMemoryCollection(new Dictionary<string, string?>
{
["Parameters:val1"] = "ValueFromConfiguration",
});

// We have 2 params, one with a config value and one without. Both get a default value.
var parameter1 = appBuilder.AddParameter("val1", "DefaultValue1");
var parameter2 = appBuilder.AddParameter("val2", "DefaultValue2");

using var app = appBuilder.Build();

var appModel = app.Services.GetRequiredService<DistributedApplicationModel>();

// Make sure the config value is used for the first parameter
var parameterResource1 = Assert.Single(appModel.Resources.OfType<ParameterResource>(), r => r.Name == "val1");
Assert.Equal("ValueFromConfiguration", parameterResource1.Value);

// Make sure the default value is used for the second parameter, since there is no config value
var parameterResource2 = Assert.Single(appModel.Resources.OfType<ParameterResource>(), r => r.Name == "val2");
Assert.Equal("DefaultValue2", parameterResource2.Value);

// Note that the manifest should not include anything about the default value
var param1Manifest = await ManifestUtils.GetManifest(parameter1.Resource);
var expectedManifest = $$"""
{
"type": "parameter.v0",
"value": "{val1.inputs.value}",
"inputs": {
"value": {
"type": "string"
}
}
}
""";
Assert.Equal(expectedManifest, param1Manifest.ToString());
}

[Fact]
public async Task ParametersWithDefaultValueObjectOverloadOnlyUsedIfNoConfigurationValue()
{
var appBuilder = DistributedApplication.CreateBuilder();

appBuilder.Configuration.AddInMemoryCollection(new Dictionary<string, string?>
{
["Parameters:val1"] = "ValueFromConfiguration",
});

var genParam = new GenerateParameterDefault
{
MinLength = 10,
// just use letters for the username since it can't start with a number
Numeric = false,
Special = false
};

// We have 2 params, one with a config value and one without. Both get a generated param default value.
var parameter1 = appBuilder.AddParameter("val1", genParam);
var parameter2 = appBuilder.AddParameter("val2", genParam);

using var app = appBuilder.Build();

var appModel = app.Services.GetRequiredService<DistributedApplicationModel>();

// Make sure the config value is used for the first parameter
var parameterResource1 = Assert.Single(appModel.Resources.OfType<ParameterResource>(), r => r.Name == "val1");
Assert.Equal("ValueFromConfiguration", parameterResource1.Value);

// Make sure the generated default value is used for the second parameter, since there is no config value
// We can't test the exact value since it's random, but we can test the length
var parameterResource2 = Assert.Single(appModel.Resources.OfType<ParameterResource>(), r => r.Name == "val2");
Assert.Equal(10, parameterResource2.Value.Length);

// The manifest should include the fields for the generated default value
var param1Manifest = await ManifestUtils.GetManifest(parameter1.Resource);
var expectedManifest = $$"""
{
"type": "parameter.v0",
"value": "{val1.inputs.value}",
"inputs": {
"value": {
"type": "string",
"default": {
"generate": {
"minLength": 10,
"numeric": false,
"special": false
}
}
}
}
}
""";
Assert.Equal(expectedManifest, param1Manifest.ToString());
}

private sealed class TestParameterDefault(string defaultValue) : ParameterDefault
{
public override string GetDefaultValue() => defaultValue;
Expand Down

0 comments on commit dfba0ba

Please sign in to comment.