|  | 
|  | 1 | +--- | 
|  | 2 | +description: "This chat mode is to create a new .NET Aspire hosting integration, using the design in the Community Toolkit repo." | 
|  | 3 | +tools: [ | 
|  | 4 | +        "changes", | 
|  | 5 | +        "codebase", | 
|  | 6 | +        "editFiles", | 
|  | 7 | +        "fetch", | 
|  | 8 | +        "new", | 
|  | 9 | +        "problems", | 
|  | 10 | +        "runCommands", | 
|  | 11 | +        "runTasks", | 
|  | 12 | +        "search", | 
|  | 13 | +        "searchResults", | 
|  | 14 | +        "usages", | 
|  | 15 | +    ] | 
|  | 16 | +--- | 
|  | 17 | + | 
|  | 18 | +# New Aspire Hosting Integration | 
|  | 19 | + | 
|  | 20 | +You are going to develop a new .NET Aspire hosting integration. The following in the process in which you need to go through to complete the task. | 
|  | 21 | + | 
|  | 22 | +## 1. Collect Requirements | 
|  | 23 | + | 
|  | 24 | +You are going to need to know: | 
|  | 25 | + | 
|  | 26 | +-   The name of the hosting integration. | 
|  | 27 | +-   The description of the hosting integration. | 
|  | 28 | + | 
|  | 29 | +This will be important to ensure that the hosting integration is created correctly. | 
|  | 30 | + | 
|  | 31 | +Ideally, the user should provide a URL for the docs on how to run the tool that the hosting integration is for, using the `fetch` tool, so that you can use it to understand how the tool works and how to implement the hosting integration. | 
|  | 32 | + | 
|  | 33 | +## 2. Scaffold the C# project | 
|  | 34 | + | 
|  | 35 | +Start by creating a new C# class library project in the `src` folder. The project should be named `CommunityToolkit.Aspire.Hosting.<HostingIntegrationName>`, where `<HostingIntegrationName>` is the name of the hosting integration you are creating. | 
|  | 36 | + | 
|  | 37 | +It can be created using the following command: | 
|  | 38 | + | 
|  | 39 | +```bash | 
|  | 40 | +dotnet new classlib -n CommunityToolkit.Aspire.Hosting.<HostingIntegrationName> -o src/CommunityToolkit.Aspire.Hosting.<HostingIntegrationName> | 
|  | 41 | +``` | 
|  | 42 | + | 
|  | 43 | +Once created the `csproj` file can be stripped back to just the following minimal starting point: | 
|  | 44 | + | 
|  | 45 | +```xml | 
|  | 46 | +<Project Sdk="Microsoft.NET.Sdk"> | 
|  | 47 | + | 
|  | 48 | +  <PropertyGroup> | 
|  | 49 | +    <AdditionalPackageTags>hosting $HostingIntegrationName</AdditionalPackageTags> | 
|  | 50 | +    <Description>$HostingIntegrationDescription.</Description> | 
|  | 51 | +  </PropertyGroup> | 
|  | 52 | + | 
|  | 53 | +</Project> | 
|  | 54 | +``` | 
|  | 55 | + | 
|  | 56 | +Also create an empty `README.md` file in the root of the project with the following content: | 
|  | 57 | + | 
|  | 58 | +```markdown | 
|  | 59 | +# CommunityToolkit.Aspire.Hosting.<HostingIntegrationName> | 
|  | 60 | + | 
|  | 61 | +TODO: Add a description of the hosting integration. | 
|  | 62 | +``` | 
|  | 63 | + | 
|  | 64 | +Ensure that the project is added to the solution file, which is in the repo root, and can be done using: | 
|  | 65 | + | 
|  | 66 | +```bash | 
|  | 67 | +dotnet sln add src/CommunityToolkit.Aspire.Hosting.<HostingIntegrationName> | 
|  | 68 | +``` | 
|  | 69 | + | 
|  | 70 | +## 3. Implement the Hosting Integration | 
|  | 71 | + | 
|  | 72 | +An integration consists of two main parts, a `Resource` implementation, and extension methods for adding it to the `IDistributedApplicationBuilder`. | 
|  | 73 | + | 
|  | 74 | +### 3.1 Create the Resource | 
|  | 75 | + | 
|  | 76 | +There are multiple choices for the `Resource` implementation. Using the knowledge of the thing to be hosted, you can choose the most appropriate from the list: | 
|  | 77 | + | 
|  | 78 | +-   `ExecutableResource` - For running a local executable (e.g. Node.js, Python, Rust, etc.) | 
|  | 79 | +-   `ContainerResource` - For running a container image using Docker. | 
|  | 80 | +-   `Resource` - For running a generic resource that does not fit into the above categories. | 
|  | 81 | + | 
|  | 82 | +Here is an example of how to implement a `ContainerResource`: | 
|  | 83 | + | 
|  | 84 | +```csharp | 
|  | 85 | +namespace Aspire.Hosting.ApplicationModel; | 
|  | 86 | + | 
|  | 87 | +/// <summary> | 
|  | 88 | +/// A resource that represents an Ollama container. | 
|  | 89 | +/// </summary> | 
|  | 90 | +/// <remarks> | 
|  | 91 | +/// Constructs an <see cref="OllamaResource"/>. | 
|  | 92 | +/// </remarks> | 
|  | 93 | +/// <param name="name">The name for the resource.</param> | 
|  | 94 | +public class OllamaResource(string name) : ContainerResource(name), IResourceWithConnectionString | 
|  | 95 | +{ | 
|  | 96 | +    internal const string OllamaEndpointName = "http"; | 
|  | 97 | + | 
|  | 98 | +    private readonly List<string> _models = []; | 
|  | 99 | + | 
|  | 100 | +    private EndpointReference? _primaryEndpointReference; | 
|  | 101 | + | 
|  | 102 | +    /// <summary> | 
|  | 103 | +    /// Adds a model to the list of models to download on initial startup. | 
|  | 104 | +    /// </summary> | 
|  | 105 | +    public IReadOnlyList<string> Models => _models; | 
|  | 106 | + | 
|  | 107 | +    /// <summary> | 
|  | 108 | +    /// Gets the endpoint for the Ollama server. | 
|  | 109 | +    /// </summary> | 
|  | 110 | +    public EndpointReference PrimaryEndpoint => _primaryEndpointReference ??= new(this, OllamaEndpointName); | 
|  | 111 | + | 
|  | 112 | +    /// <summary> | 
|  | 113 | +    /// Gets the connection string expression for the Ollama server. | 
|  | 114 | +    /// </summary> | 
|  | 115 | +    public ReferenceExpression ConnectionStringExpression => | 
|  | 116 | +      ReferenceExpression.Create( | 
|  | 117 | +        $"Endpoint={PrimaryEndpoint.Property(EndpointProperty.Scheme)}://{PrimaryEndpoint.Property(EndpointProperty.Host)}:{PrimaryEndpoint.Property(EndpointProperty.Port)}" | 
|  | 118 | +      ); | 
|  | 119 | + | 
|  | 120 | +    /// <summary> | 
|  | 121 | +    ///     Adds a model to the list of models to download on initial startup. | 
|  | 122 | +    /// </summary> | 
|  | 123 | +    /// <param name="modelName">The name of the model</param> | 
|  | 124 | +    public void AddModel(string modelName) | 
|  | 125 | +    { | 
|  | 126 | +        ArgumentException.ThrowIfNullOrEmpty(modelName, nameof(modelName)); | 
|  | 127 | +        if (!_models.Contains(modelName)) | 
|  | 128 | +        { | 
|  | 129 | +            _models.Add(modelName); | 
|  | 130 | +        } | 
|  | 131 | +    } | 
|  | 132 | +} | 
|  | 133 | +``` | 
|  | 134 | + | 
|  | 135 | +Resources can also have endpoints, which are used to connect to the resource, they can also expose a connection string expression, which is used to connect to the resource in a more generic way. | 
|  | 136 | + | 
|  | 137 | +The following requirements **must** be met when implementing a resource: | 
|  | 138 | + | 
|  | 139 | +-   Namespace: `Aspire.Hosting.ApplicationModel`. | 
|  | 140 | +-   Class name: `<HostingIntegrationName>Resource`. | 
|  | 141 | +-   Inherits from `Resource`, `ContainerResource`, or `ExecutableResource`. | 
|  | 142 | +-   Public constructor that takes a `string name` parameter. | 
|  | 143 | +-   If the resource has a connection string, it must implement `IResourceWithConnectionString`. | 
|  | 144 | +-   If the resource has endpoints, it must implement `IResourceWithEndpoints`. | 
|  | 145 | +-   Public methods, properties, and events should be documented with XML comments. | 
|  | 146 | + | 
|  | 147 | +### 3.2 Create the Extension Methods | 
|  | 148 | + | 
|  | 149 | +Next, you need to create extension methods for the `IDistributedApplicationBuilder` to add the resource to the application. This is done by creating a static class with a method that takes an `IDistributedApplicationBuilder` and returns an `IResourceBuilder<T>`. | 
|  | 150 | + | 
|  | 151 | +Here is an example of how to implement the extension method for the `OllamaResource`: | 
|  | 152 | + | 
|  | 153 | +```csharp | 
|  | 154 | +namespace Aspire.Hosting; | 
|  | 155 | + | 
|  | 156 | +/// <summary> | 
|  | 157 | +/// Extension methods for adding a Bun app to a <see cref="IDistributedApplicationBuilder"/>. | 
|  | 158 | +/// </summary> | 
|  | 159 | +public static class BunAppExtensions | 
|  | 160 | +{ | 
|  | 161 | +    /// <summary> | 
|  | 162 | +    /// Adds a Bun app to the builder. | 
|  | 163 | +    /// </summary> | 
|  | 164 | +    /// <param name="builder">The <see cref="IDistributedApplicationBuilder"/> to add the resource to.</param> | 
|  | 165 | +    /// <param name="name">The name of the resource.</param> | 
|  | 166 | +    /// <param name="workingDirectory">The working directory.</param> | 
|  | 167 | +    /// <param name="entryPoint">The entry point, either a file or package.json script name.</param> | 
|  | 168 | +    /// <param name="watch">Whether to watch for changes.</param> | 
|  | 169 | +    /// <returns>A reference to the <see cref="IResourceBuilder{T}"/>.</returns> | 
|  | 170 | +    public static IResourceBuilder<BunAppResource> AddBunApp( | 
|  | 171 | +        this IDistributedApplicationBuilder builder, | 
|  | 172 | +        [ResourceName] string name, | 
|  | 173 | +        string? workingDirectory = null, | 
|  | 174 | +        string entryPoint = "index.ts", | 
|  | 175 | +        bool watch = false) | 
|  | 176 | +    { | 
|  | 177 | +        ArgumentNullException.ThrowIfNull(builder, nameof(builder)); | 
|  | 178 | +        ArgumentException.ThrowIfNullOrEmpty(name, nameof(name)); | 
|  | 179 | +        ArgumentException.ThrowIfNullOrEmpty(entryPoint, nameof(entryPoint)); | 
|  | 180 | + | 
|  | 181 | +        workingDirectory ??= Path.Combine("..", name); | 
|  | 182 | + | 
|  | 183 | +        var resource = new BunAppResource(name, PathNormalizer.NormalizePathForCurrentPlatform(Path.Combine(builder.AppHostDirectory, workingDirectory))); | 
|  | 184 | + | 
|  | 185 | +        string[] args = watch ? ["--watch", "run", entryPoint] : ["run", entryPoint]; | 
|  | 186 | + | 
|  | 187 | +        return builder.AddResource(resource) | 
|  | 188 | +            .WithBunDefaults() | 
|  | 189 | +            .WithArgs(args); | 
|  | 190 | +    } | 
|  | 191 | +} | 
|  | 192 | +``` | 
|  | 193 | + | 
|  | 194 | +The extension method should meet the following requirements: | 
|  | 195 | + | 
|  | 196 | +-   Namespace: `Aspire.Hosting`. | 
|  | 197 | +-   Class name: `<HostingIntegrationName>Extensions`. | 
|  | 198 | +-   Static class with a static method. | 
|  | 199 | +-   Method name: `Add<HostingIntegrationName>`. | 
|  | 200 | +-   Method parameters: | 
|  | 201 | +    -   `IDistributedApplicationBuilder builder` - The builder to add the resource to. | 
|  | 202 | +    -   `string name` - The name of the resource, decorated with `[ResourceName]`. | 
|  | 203 | +    -   Additional parameters as required by the resource. | 
|  | 204 | +-   Returns an `IResourceBuilder<T>` where `T` is the resource type. | 
|  | 205 | +-   The method should call `builder.AddResource(resource)` to add the resource to the builder. | 
|  | 206 | +-   Perform `ArgumentNullException.ThrowIfNull` and `ArgumentException.ThrowIfNullOrEmpty` checks on the parameters. | 
|  | 207 | + | 
|  | 208 | +## 4. Sample Usage | 
|  | 209 | + | 
|  | 210 | +You need to create a sample usage of the hosting integration in the `examples` folder. This should be a minimal example that demonstrates how to use the hosting integration in a .NET Aspire application. | 
|  | 211 | + | 
|  | 212 | +Start by scaffolding a new .NET Aspire App Host project in the `examples` folder. This can be done using the following command: | 
|  | 213 | + | 
|  | 214 | +```bash | 
|  | 215 | +dotnet new aspire-apphost -n CommunityToolkit.Aspire.Hosting.<HostingIntegrationName>.AppHost -o examples/CommunityToolkit.Aspire.Hosting.<HostingIntegrationName>.AppHost | 
|  | 216 | +``` | 
|  | 217 | + | 
|  | 218 | +Make sure to add the project to the solution with `dotnet sln add examples/CommunityToolkit.Aspire.Hosting.<HostingIntegrationName>.AppHost`. | 
|  | 219 | + | 
|  | 220 | +Once created, refer to existing AppHost `csproj` files to ensure that the right packages are referenced, such as `CommunityToolkit.Aspire.Hosting.<HostingIntegrationName>`. For the `Sdk`, ensure the version is `$(AspireAppHostSdkVersion)`, since we use a MSBuild variable to ensure that a consistent version is used across all App Host projects. Any `PackageReference` elements should **not** have a version specified. | 
|  | 221 | + | 
|  | 222 | +Next, edit the `AppHost.cs` file that the template created to use the hosting integration. | 
|  | 223 | + | 
|  | 224 | +Here is an example of how to use the `BunResource` in the `AppHost.cs` file: | 
|  | 225 | + | 
|  | 226 | +```csharp | 
|  | 227 | +var builder = DistributedApplication.CreateBuilder(args); | 
|  | 228 | + | 
|  | 229 | +var api = builder.AddBunApp("api") | 
|  | 230 | +    .WithBunPackageInstallation() | 
|  | 231 | +    .WithHttpEndpoint(env: "PORT") | 
|  | 232 | +    .WithHttpHealthCheck("/"); | 
|  | 233 | + | 
|  | 234 | +builder.Build().Run(); | 
|  | 235 | +``` | 
|  | 236 | + | 
|  | 237 | +Ensure that the example is a minimal working example that can be run using the `dotnet run` command. | 
|  | 238 | + | 
|  | 239 | +## 5. Tests | 
|  | 240 | + | 
|  | 241 | +You need to create tests for the hosting integration. This should include unit tests for the resource implementation and integration tests for the extension methods. | 
|  | 242 | + | 
|  | 243 | +The tests should be placed in a new test project in the `tests` folder. The project should be named `CommunityToolkit.Aspire.Hosting.<HostingIntegrationName>.Tests`, and can be created using the following command: | 
|  | 244 | + | 
|  | 245 | +```bash | 
|  | 246 | +dotnet new xunit -n CommunityToolkit.Aspire.Hosting.<HostingIntegrationName>.Tests -o tests/CommunityToolkit.Aspire.Hosting.<HostingIntegrationName>.Tests | 
|  | 247 | +``` | 
|  | 248 | + | 
|  | 249 | +Make sure to add the project to the solution with `dotnet sln add examples/CommunityToolkit.Aspire.Hosting.<HostingIntegrationName>.AppHost`. | 
|  | 250 | + | 
|  | 251 | +Ensure that the test project references the hosting integration project and any necessary Aspire packages. Refer to other test projects in the `tests` folder for guidance on how to set up the project file. | 
|  | 252 | + | 
|  | 253 | +Once the project is created, you can start writing tests for the resource implementation and extension methods. Ensure that the tests cover all public methods and properties of the resource, as well as the extension methods. | 
|  | 254 | + | 
|  | 255 | +## 6. Documentation | 
|  | 256 | + | 
|  | 257 | +Once the integration is implemented, you need to update the `README.md` file in the hosting integration project to include a description of the hosting integration, how to use it, and any other relevant information. | 
|  | 258 | + | 
|  | 259 | +Also, update the root `README.md` file of the repo to include a link to the new hosting integration in the table that exists. | 
0 commit comments