Skip to content

Initial bits for the interaction service #4045

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

Open
wants to merge 10 commits into
base: release-9.4
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
368 changes: 368 additions & 0 deletions docs/extensibility/interaction-service.md

Large diffs are not rendered by default.

Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
using Microsoft.Extensions.DependencyInjection;

partial class Program
{
public static async Task<ExecuteCommandResult> ShowConfirmationExample(ExecuteCommandContext context)
{
// <example>
var interactionService = context.ServiceProvider.GetRequiredService<IInteractionService>();

// Prompt for confirmation before resetting database
var resetConfirmation = await interactionService.PromptConfirmationAsync(
title: "Confirm Reset",
message: "Are you sure you want to reset the `development-database`? This action **cannot** be undone.",
options: new MessageBoxInteractionOptions
{
Intent = MessageIntent.Confirmation,
PrimaryButtonText = "Reset",
SecondaryButtonText = "Cancel",
ShowSecondaryButton = true,
EnableMessageMarkdown = true
});

if (resetConfirmation.Data is true)
{
// Perform the reset operation...

return CommandResults.Success();
}
else
{
return CommandResults.Failure("Database reset canceled by user.");
}
// </example>
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
using Microsoft.Extensions.DependencyInjection;

partial class Program
{
public static async Task<ExecuteCommandResult> ShowMessageBoxExample(ExecuteCommandContext context)
{
// <example>
var interactionService = context.ServiceProvider.GetRequiredService<IInteractionService>();

var result = await interactionService.PromptMessageBoxAsync(
"Simple Message Box: Example",
"""
##### 🤓 Nice!

It's worth noting that **Markdown** is _supported_
(and demonstrated here) in the message body. Simply
configure the options as:

```csharp
var options = new MessageBoxInteractionOptions
{
EnableMessageMarkdown = true,
// Other options...
};
```

Cool, [📖 learn more](https://learn.microsoft.com/dotnet/aspire/extensibility/interaction-service)...
""",
new MessageBoxInteractionOptions
{
EnableMessageMarkdown = true,
PrimaryButtonText = "Awesome"
}
);

if (result.Canceled)
{
return CommandResults.Failure("User cancalled.");
}

return result.Data
? CommandResults.Success()
: CommandResults.Failure("The user doesn't like the example");
// </example>
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;

partial class Program
{
public static async Task<ExecuteCommandResult> ShowMultipleInputExample(
ExecuteCommandContext context, FakeResource fakeResource)
{
// <example>
var interactionService = context.ServiceProvider.GetRequiredService<IInteractionService>();
var loggerService = context.ServiceProvider.GetRequiredService<ResourceLoggerService>();
var logger = loggerService.GetLogger(fakeResource);

var inputs = new List<InteractionInput>
{
new()
{
Label = "Application Name",
InputType = InputType.Text,
Required = true,
Placeholder = "my-app"
},
new()
{
Label = "Environment",
InputType = InputType.Choice,
Required = true,
Options =
[
new("dev", "Development"),
new("staging", "Staging"),
new("test", "Testing")
]
},
new()
{
Label = "Instance Count",
InputType = InputType.Number,
Required = true,
Placeholder = "1"
},
new()
{
Label = "Enable Monitoring",
InputType = InputType.Boolean,
Required = false
}
};

var appConfigurationInput = await interactionService.PromptInputsAsync(
title: "Application Configuration",
message: "Configure your application deployment settings:",
inputs: inputs);

if (!appConfigurationInput.Canceled)
{
// Process the collected input values
var appName = appConfigurationInput.Data[0].Value;
var environment = appConfigurationInput.Data[1].Value;
var instanceCount = int.Parse(appConfigurationInput.Data[2].Value ?? "1");
var enableMonitoring = bool.Parse(appConfigurationInput.Data[3].Value ?? "false");

logger.LogInformation("""
Application Name: {AppName}
Environment: {Environment}
Instance Count: {InstanceCount}
Monitoring Enabled: {EnableMonitoring}
""",
appName, environment, instanceCount, enableMonitoring);

// Use the collected values as needed
return CommandResults.Success();
}
else
{
return CommandResults.Failure("User canceled application configuration input.");
}
// </example>
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
using Microsoft.Extensions.DependencyInjection;

partial class Program
{
public static async Task<ExecuteCommandResult> ShowNotificationExample(ExecuteCommandContext context)
{
// <example>
var interactionService = context.ServiceProvider.GetRequiredService<IInteractionService>();

// Demonstrating various notification types with different intents
var tasks = new List<Task>
{
interactionService.PromptNotificationAsync(
title: "Confirmation",
message: "Are you sure you want to proceed?",
options: new NotificationInteractionOptions
{
Intent = MessageIntent.Confirmation
}),
interactionService.PromptNotificationAsync(
title: "Success",
message: "Your operation completed successfully.",
options: new NotificationInteractionOptions
{
Intent = MessageIntent.Success,
LinkText = "View Details",
LinkUrl = "https://learn.microsoft.com/dotnet/aspire/success"
}),
interactionService.PromptNotificationAsync(
title: "Warning",
message: "Your SSL certificate will expire soon.",
options: new NotificationInteractionOptions
{
Intent = MessageIntent.Warning,
LinkText = "Renew Certificate",
LinkUrl = "https://portal.azure.com/certificates"
}),
interactionService.PromptNotificationAsync(
title: "Information",
message: "There is an update available for your application.",
options: new NotificationInteractionOptions
{
Intent = MessageIntent.Information,
LinkText = "Update Now",
LinkUrl = "https://learn.microsoft.com/dotnet/aspire"
}),
interactionService.PromptNotificationAsync(
title: "Error",
message: "An error occurred while processing your request.",
options: new NotificationInteractionOptions
{
Intent = MessageIntent.Error,
LinkText = "Troubleshoot",
LinkUrl = "https://learn.microsoft.com/dotnet/aspire/troubleshoot"
})
};

await Task.WhenAll(tasks);

return CommandResults.Success();
// </example>
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
using Microsoft.Extensions.DependencyInjection;

partial class Program
{
public static async Task<ExecuteCommandResult> ShowSingleInputExample(ExecuteCommandContext context)
{
// <example>
var interactionService = context.ServiceProvider.GetRequiredService<IInteractionService>();

var apiKeyInput = await interactionService.PromptInputAsync(
title: "API Configuration",
message: "Enter your third-party service API key:",
input: new InteractionInput
{
Label = "API Key",
InputType = InputType.SecretText,
Required = true,
Placeholder = "Enter your API key"
});

if (!apiKeyInput.Canceled)
{
var apiKey = apiKeyInput.Data?.Value ?? "";

// Store the API key securely

return CommandResults.Success();
}
else
{
return CommandResults.Failure("User canceled API key input.");
}
// </example>
}
}
65 changes: 65 additions & 0 deletions docs/extensibility/snippets/InteractionService/AppHost.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
using Microsoft.Extensions.DependencyInjection;

var builder = DistributedApplication.CreateBuilder(args);

var fakeResource = builder.AddFakeResource("fake-resource-01")
.WithCommand("msg-dialog", "Example Message Dialog", ShowMessageBoxExample)
.WithCommand("msg-bar", "Example Message Bar", ShowNotificationExample)
.WithCommand("confirm", "Confirmation Example", ShowConfirmationExample)
.WithCommand("single-input", "Single Input Example", ShowSingleInputExample);

fakeResource.WithCommand(
"multi-input",
"Multi Input Example",
context => ShowMultipleInputExample(context, fakeResource.Resource));


builder.AddFakeResource("fake-resource-02")
.WithDeployment(async context =>
{
var interactionService = context.Services.GetRequiredService<IInteractionService>();
var logger = context.Logger;

var inputs = new List<InteractionInput>
{
new()
{
Label = "Application Name",
InputType = InputType.Text,
Required = true,
Placeholder = "my-app"
},
new()
{
Label = "Environment",
InputType = InputType.Choice,
Required = true,
Options =
[
new("dev", "Development"),
new("staging", "Staging"),
new("test", "Testing")
]
},
new()
{
Label = "Instance Count",
InputType = InputType.Number,
Required = true,
Placeholder = "1"
},
new()
{
Label = "Enable Monitoring",
InputType = InputType.Boolean,
Required = false
}
};

var appConfigurationInput = await interactionService.PromptInputsAsync(
title: "Application Configuration",
message: "Configure your application deployment settings:",
inputs: inputs);
});

builder.Build().Run();
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
public sealed class FakeResource(string name) : IResource
{
string IResource.Name => name;

ResourceAnnotationCollection IResource.Annotations { get; } = [];
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
namespace Aspire.Hosting;

public static class FakeResourceExtensions
{
public static IResourceBuilder<FakeResource> AddFakeResource(
this IDistributedApplicationBuilder builder,
[ResourceName] string name)
{
var fakeResource = new FakeResource(name);

return builder.AddResource(fakeResource)
.WithInitialState(new()
{
ResourceType = "Fake Resource",
State = KnownResourceStates.Running,
Properties =
[
new(CustomResourceKnownProperties.Source, "Fake")
]
})
.ExcludeFromManifest();
}

public static IResourceBuilder<FakeResource> WithDeployment(
this IResourceBuilder<FakeResource> builder,
Func<DeployingContext, Task> callback)
{
builder.WithAnnotation(new DeployingCallbackAnnotation(callback));

return builder;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<Project Sdk="Microsoft.NET.Sdk">

<Sdk Name="Aspire.AppHost.Sdk" Version="9.3.0" />

<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<OutputType>Exe</OutputType>
<NoWarn>$(NoWarn);ASPIREINTERACTION001;ASPIREPUBLISHERS001</NoWarn>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Aspire.Hosting.AppHost" Version="9.4.0-preview.1.25366.2" />
</ItemGroup>

</Project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
<Solution>
<Project Path="InteractionService.AppHost.csproj" />
</Solution>
Loading