Exploring .NET Aspire and Adding it to my existing boilerplate
Bervianto Leo Pratama

Bervianto Leo Pratama @berviantoleo

About: AWS Community Builder | Software Engineer | Focus on topics: Microservices, Cloud Computing, and Cyber Security.

Location:
Bandung, Indonesia
Joined:
Aug 9, 2018

Exploring .NET Aspire and Adding it to my existing boilerplate

Publish Date: Apr 5
0 0

Hi everyone!

Glad to see you again!

I'm coming up with a new topic, which is still regarding DevOps. Yes, as your guest, it's .NET Aspire!

Happy GIF

I don't want to take too long for the introduction. Let's get started!

Prerequisite

Note:

  • I will use JetBrains Rider. So, I will install the .NET Aspire Plugin for JetBrains Rider.
  • Another reference for adding .NET Aspire to the existing project.

Setup .NET Aspire Project

Ready GIF

Ensure you already have .NET Aspire templates; if not, please install them using this command.

dotnet new install Aspire.ProjectTemplates
Enter fullscreen mode Exit fullscreen mode

Create a .NET Aspire AppHost project through .NET Aspire templates.

dotnet new aspire-apphost -f net9.0 --name BervProject.WebApi.Boilerplate.AppHost
Enter fullscreen mode Exit fullscreen mode

.NET Aspire AppHost Successfully created

Now, create a .NET Aspire Service Defaults project through .NET Aspire templates.

dotnet new aspire-servicedefaults -f net9.0 --name BervProject.WebApi.Boilerplate.ServiceDefaults
Enter fullscreen mode Exit fullscreen mode

.NET Aspire Service Defaults Successfully created

Let's add the projects to our solutions.

dotnet sln add BervProject.WebApi.Boilerplate.AppHost/BervProject.WebApi.Boilerplate.AppHost.csproj BervProject.WebApi.Boilerplate.ServiceDefaults/BervProject.WebApi.Boilerplate.ServiceDefaults.csproj
Enter fullscreen mode Exit fullscreen mode

Our project solutions will be like this.

Project Solutions

Well done! Now, let's integrate the existing Web API and add some required services.

Integrating the Web API

  • Add the Web API as a reference in our AppHost.
 dotnet add BervProject.WebApi.Boilerplate.AppHost reference BervProject.WebApi.Boilerplate
Enter fullscreen mode Exit fullscreen mode
  • Add Redis package to AppHost.
dotnet add BervProject.WebApi.Boilerplate.AppHost package Aspire.Hosting.Redis
Enter fullscreen mode Exit fullscreen mode
  • Add Postgres package to AppHost.
dotnet add BervProject.WebApi.Boilerplate.AppHost package Aspire.Hosting.PostgreSQL
Enter fullscreen mode Exit fullscreen mode
  • Add Azure Storage package to AppHost.
dotnet add BervProject.WebApi.Boilerplate.AppHost package Aspire.Hosting.Azure.Storage
Enter fullscreen mode Exit fullscreen mode
  • Add Azure Service Bus package to AppHost.
dotnet add BervProject.WebApi.Boilerplate.AppHost package Aspire.Hosting.Azure.ServiceBus
Enter fullscreen mode Exit fullscreen mode

Adding Migration Service

  • Creating the migration service.
dotnet new worker -n BervProject.WebApi.Boilerplate.MigrationService -f "net9.0"
Enter fullscreen mode Exit fullscreen mode
  • Add to the solution.
dotnet sln add BervProject.WebApi.Boilerplate.MigrationService
Enter fullscreen mode Exit fullscreen mode
  • Add reference to the API (the source of the migration data)
dotnet add BervProject.WebApi.Boilerplate.MigrationService reference BervProject.WebApi.Boilerplate
Enter fullscreen mode Exit fullscreen mode
  • Add reference to the service defaults.
dotnet add BervProject.WebApi.Boilerplate.MigrationService reference BervProject.WebApi.Boilerplate.ServiceDefaults
Enter fullscreen mode Exit fullscreen mode
  • Add Aspire.Npgsql.EntityFrameworkCore.PostgreSQL package to the migration service.
dotnet add BervProject.WebApi.Boilerplate.MigrationService package Aspire.Npgsql.EntityFrameworkCore.PostgreSQL
Enter fullscreen mode Exit fullscreen mode
  • Update the Program.cs in the migration service.
using BervProject.WebApi.Boilerplate.EntityFramework;
using BervProject.WebApi.Boilerplate.MigrationService;

var builder = Host.CreateApplicationBuilder(args);

builder.AddServiceDefaults();

builder.Services.AddHostedService<Worker>();

builder.Services.AddOpenTelemetry()
    .WithTracing(tracing => tracing.AddSource(Worker.ActivitySourceName));
builder.AddNpgsqlDbContext<BoilerplateDbContext>("BoilerplateConnectionString");

var host = builder.Build();
host.Run();
Enter fullscreen mode Exit fullscreen mode
  • Update the Worker.cs in the migration service.
using System.Diagnostics;
using BervProject.WebApi.Boilerplate.EntityFramework;
using Microsoft.EntityFrameworkCore;

namespace BervProject.WebApi.Boilerplate.MigrationService;

public class Worker : BackgroundService
{
    public const string ActivitySourceName = "Migrations";
    private static readonly ActivitySource SActivitySource = new(ActivitySourceName);

    private readonly IServiceProvider _serviceProvider;
    private readonly IHostApplicationLifetime _hostApplicationLifetime;

    public Worker(IServiceProvider serviceProvider,
        IHostApplicationLifetime hostApplicationLifetime)
    {
        _serviceProvider = serviceProvider;
        _hostApplicationLifetime = hostApplicationLifetime;
    }

    protected override async Task ExecuteAsync(CancellationToken cancellationToken)
    {
        using var activity = SActivitySource.StartActivity("Migrating database", ActivityKind.Client);

        try
        {
            using var scope = _serviceProvider.CreateScope();
            var dbContext = scope.ServiceProvider.GetRequiredService<BoilerplateDbContext>();

            await RunMigrationAsync(dbContext, cancellationToken);
        }
        catch (Exception ex)
        {
            activity?.AddException(ex);
            throw;
        }

        _hostApplicationLifetime.StopApplication();
    }

    private static async Task RunMigrationAsync(BoilerplateDbContext dbContext, CancellationToken cancellationToken)
    {
        var strategy = dbContext.Database.CreateExecutionStrategy();
        await strategy.ExecuteAsync(async () =>
        {
            // Run migration in a transaction to avoid partial migration if it fails.
            await dbContext.Database.MigrateAsync(cancellationToken);
        });
    }

}
Enter fullscreen mode Exit fullscreen mode
  • Add the Migration Service to the AppHost.
dotnet add BervProject.WebApi.Boilerplate.AppHost reference BervProject.WebApi.Boilerplate.MigrationService
Enter fullscreen mode Exit fullscreen mode
  • Add Microsoft.EntityFrameworkCore to both BervProject.WebApi.Boilerplate.MigrationService and BervProject.WebApi.Boilerplate projects to avoid package version conflicts.
    <PackageReference Include="Microsoft.EntityFrameworkCore" Version="9.0.3" />

Enter fullscreen mode Exit fullscreen mode

Final Changes

Finally GIF

  • Update the Program.cs in our AppHost.
var builder = DistributedApplication.CreateBuilder(args);

var cache = builder.AddRedis("cache").WithRedisInsight();
var postgres = builder.AddPostgres("postgres").WithPgAdmin();
var postgresdb = postgres.AddDatabase("postgresdb");
var serviceBus = builder.AddAzureServiceBus("messaging").RunAsEmulator();
var storage = builder.AddAzureStorage("storage").RunAsEmulator();
var blobs = storage.AddBlobs("blobs");
var queues = storage.AddQueues("queues");
var tables = storage.AddTables("tables");

var migration = builder.AddProject<Projects.BervProject_WebApi_Boilerplate_MigrationService>("migrations")
    .WithReference(postgresdb, connectionName: "BoilerplateConnectionString")
    .WithExplicitStart();

builder.AddProject<Projects.BervProject_WebApi_Boilerplate>("apiservice")
    .WithHttpEndpoint()
    .WithReference(cache, connectionName: "Redis")
    .WithReference(postgresdb, connectionName: "BoilerplateConnectionString")
    .WithReference(blobs, connectionName: "AzureStorageBlob")
    .WithReference(queues, connectionName: "AzureStorageQueue")
    .WithReference(tables, connectionName: "AzureStorageTable")
    .WithReference(serviceBus, connectionName: "AzureServiceBus")
    .WaitFor(cache)
    .WaitFor(postgresdb)
    .WaitFor(blobs)
    .WaitFor(queues)
    .WaitFor(tables)
    .WaitFor(serviceBus)
    .WaitForCompletion(migration);

builder.Build().Run();
Enter fullscreen mode Exit fullscreen mode

Migrating Connection Strings in the existing API

  • Update Program.cs in BervProject.WebApi.Boilerplate.
using System;
using System.IO;
using System.Reflection;
using Autofac.Extensions.DependencyInjection;
using BervProject.WebApi.Boilerplate.ConfigModel;
using BervProject.WebApi.Boilerplate.EntityFramework;
using BervProject.WebApi.Boilerplate.Extenstions;
using BervProject.WebApi.Boilerplate.Services;
using BervProject.WebApi.Boilerplate.Services.Azure;
using Hangfire;
using Hangfire.PostgreSql;
using Microsoft.AspNetCore.Builder;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using NLog.Web;

var builder = WebApplication.CreateBuilder(args);
builder.Host.UseServiceProviderFactory(new AutofacServiceProviderFactory());
builder.Logging.ClearProviders();
builder.Logging.SetMinimumLevel(LogLevel.Trace);
builder.Logging.AddNLog("Nlog.config");
builder.Logging.AddNLogWeb();
builder.Host.UseNLog();

// settings injection
var awsConfig = builder.Configuration.GetSection("AWS").Get<AWSConfiguration>();
builder.Services.AddSingleton(awsConfig);

var azureConfig = builder.Configuration.GetSection("Azure").Get<AzureConfiguration>();
builder.Services.AddSingleton(azureConfig);

// aws services
builder.Services.SetupAWS();

// azure services
builder.Services.SetupAzure(builder.Configuration);

// cron services
builder.Services.AddScoped<ICronService, CronService>();
builder.Services.AddHangfire(x => x.UsePostgreSqlStorage(opt =>
{
    opt.UseNpgsqlConnection(builder.Configuration.GetConnectionString("BoilerplateConnectionString"));
}));
builder.Services.AddHangfireServer();

// essential services
builder.Services.AddStackExchangeRedisCache(options =>
{
    options.Configuration = builder.Configuration.GetConnectionString("Redis");
});
builder.Services.AddDbContext<BoilerplateDbContext>(options => options.UseNpgsql(builder.Configuration.GetConnectionString("BoilerplateConnectionString")));

builder.Services.AddHealthChecks();
builder.Services.AddControllers();
builder.Services.AddApiVersioning();
builder.Services.AddSwaggerGen(options =>
{
    var xmlFilename = $"{Assembly.GetExecutingAssembly().GetName().Name}.xml";
    options.IncludeXmlComments(Path.Combine(AppContext.BaseDirectory, xmlFilename));
});

var app = builder.Build();

// register Consumer
var connectionString = builder.Configuration.GetConnectionString("AzureServiceBus");
var queueName = azureConfig.ServiceBus.QueueName;
var topicName = azureConfig.ServiceBus.TopicName;
if (!string.IsNullOrWhiteSpace(queueName) && !string.IsNullOrWhiteSpace(connectionString))
{
    var bus = app.Services.GetService<IServiceBusQueueConsumer>();
    bus.RegisterOnMessageHandlerAndReceiveMessages();
}
if (!string.IsNullOrWhiteSpace(topicName) && !string.IsNullOrWhiteSpace(connectionString))
{
    var bus = app.Services.GetService<IServiceBusTopicSubscription>();
    bus.RegisterOnMessageHandlerAndReceiveMessages();
}

// register essential things
if (app.Environment.IsDevelopment())
{
    app.UseDeveloperExceptionPage();
}
else
{
    app.UseExceptionHandler("/error");
    app.UseHttpsRedirection();
}

app.UseRouting();

app.UseAuthorization();

app.MapHealthChecks("/healthz");

app.UseSwagger(c =>
{
    c.RouteTemplate = "api/docs/{documentName}/swagger.json";
});

app.UseSwaggerUI(c =>
{
    c.SwaggerEndpoint("/api/docs/v1/swagger.json", "My API V1");
    c.RoutePrefix = "api/docs";
});

app.MapControllers();
app.MapHangfireDashboard();

app.Run();

public partial class Program { }
Enter fullscreen mode Exit fullscreen mode
  • Update SetupAzureExtension.cs in BervProject.WebApi.Boilerplate.
using Microsoft.Extensions.Configuration;

namespace BervProject.WebApi.Boilerplate.Extenstions;

using Entities;
using BervProject.WebApi.Boilerplate.Services.Azure;
using Services;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Azure;


public static class SetupAzureExtension
{
    public static void SetupAzure(this IServiceCollection services, ConfigurationManager config)
    {
        services.AddAzureClients(builder =>
        {
                        builder.AddBlobServiceClient(config.GetConnectionString("AzureStorageBlob"));
            builder.AddQueueServiceClient(config.GetConnectionString("AzureStorageQueue"));
            builder.AddServiceBusClient(config.GetConnectionString("AzureServiceBus"));
            builder.AddTableServiceClient(config.GetConnectionString("AzureStorageTable"));
        });
        services.AddScoped<IAzureQueueServices, AzureQueueServices>();
        services.AddScoped<ITopicServices, TopicServices>();
        services.AddScoped<IAzureStorageQueueService, AzureStorageQueueService>();
        services.AddScoped<IBlobService, BlobService>();
        // add each tables
        services.AddScoped<IAzureTableStorageService<Note>, AzureTableStorageService<Note>>();
        // service bus
        services.AddSingleton<IServiceBusQueueConsumer, ServiceBusQueueConsumer>();
        services.AddSingleton<IServiceBusTopicSubscription, ServiceBusTopicSubscription>();
        services.AddTransient<IProcessData, ProcessData>();
        services.AddApplicationInsightsTelemetry();
    }
}
Enter fullscreen mode Exit fullscreen mode

Running the AppHost

Sweat GIF

Let's run the AppHost!

dotnet run --project .\BervProject.WebApi.Boilerplate.AppHost
Enter fullscreen mode Exit fullscreen mode

Please wait until all services are running. It may take a long time, depending on your network.

Successfully Running Services

Before starting the API, please run the migration service first after the postgres service is running. When there are no errors, your API will run successfully.

Let's Compare It with the PR

Try the API

You may try the API with a health check endpoint /healthz.

Health Endpoint

Conclusion

Tired GIF

Gosh! So tired, it's quite many things that I need to set up for the first time. However, it's satisfying! I notice some unexpected behaviour, for example, the migrating service won't stop after it's finished migrating, especially when running automatically. So, I use a workaround by starting it manually.

That's it. If you have any feedback, please let me know!

Cheers!

Love GIF

Comments 0 total

    Add comment