Kom igang med .NET Aspire

Jan Sviland
Systek
Published in
10 min readAug 14, 2024

Formålet med skytjenester var at man raskt kunne komme i gang med utviklingen. Man trengte ikke lenger å sette opp egne servere og vedlikeholde disse. Kostnaden for å sette opp servere ble redusert, man kunne enkelt skalere opp tjenestene sine for å håndtere mer trafikk, om man lagde den neste Facebook eller Twitter og fikk et stort antall brukere på kort tid var det bare å trykke på en knapp for å øke kapasiteten. Utviklere kunne fokusere på det som var viktig — Business logikk.

Men, med domene drevet arkitektur (DDD), mikrotjenester og gamle tjenester som skal migreres til skyen blir det stadig flere tjenester som skal settes opp. Hver tjeneste skal ha sin back-end, front-end, caching, database, kanskje load-balancing. Alt skal kobles sammen via APIer og vi må duplisere alt et par ganger for å ha dev, test og prod-miljø i tillegg.

Eksempel-oppsett av en restaurant sitt system, med 8 forskjellige micro-tjenester, alle med hver sine databaser og REST API’er knyttet sammen.

Dette gjør at oppsettet i skyen blir stadig mer komplekst. Mange tjenester skal settes opp og kobles sammen og back-end utviklere ender opp med å bruke mye tid på oppsett og vedlikehold av sky-tjenester. Akkurat det skyen skulle forhindre til å begynne med.

Kompleksitet != Kvalitet

Et viktig prinsipp i utvikling er “the KISS principle”, oversatt “Keep it simple, stupid”, eller en finere versjon “Simplicity is the ultimate sophistication”. Ideen er at alt skal være så lagd så enkelt som mulig for å oppnå det ønskete resultat. En enklere løsning er enklere å forstå, implementere, vedlikeholde og bruke. Kompleksitet introduserer bugs, sikkerhetshull og gjør det vanskelig å utvikle.

Microsoft har nok også innsett at vi trenger en måte å forenkle oppsettet i skyen på, vi må gå tilbake til det originale formålet med sky-tjenester, gjøre det lettere for utviklere å komme i gang med et prosjekt.

Det er her .NET Aspire kommer inn. .NET Aspire brukes for å lage “cloud-native” applikasjoner, alt du skal ha i skyen, setter du opp lokalt med en gang. Alt oppsett mellom tjenester går automatisk, passord og ConnectionStrings, som ikke bør deles, lagres automatisk i app secrets lokalt og KeyVault i skyen, koblinger mellom hver tjeneste blir satt opp korrekt av seg selv. Dette forhindrer både feil og sikkerhetshull.

Du kan enkelt sette opp alle tjenestene du ønsker, front-end, back-end, caching, databaser etc. I et vanlig .Net prosjekt, så blir alt sømløst koblet sammen. Når du kjører lokalt blir alt spunnet opp akkurat som i skyen, med hver sin docker-container, du kan teste load-balancing, caching og databasen lokalt og enkelt publisere det i skyen med ett klikk.

I tillegg kommer Aspire også med et veldig fint dashboard hvor du lett kan få en oversikt over alle tjenester, se live data med logger og statistikk for hver enkelt tjeneste, samt trace mellom hver tjeneste så du kan se et kall treffe front-end -> back-end -> cache -> database, og følge alt som skjer. Uten noe ekstra oppsett.

Hvordan komme i gang

Målet med denne artikkelen er å vise hvordan man, med Aspire, enkelt kan sette opp front-end, back-end, cache, database etc. Med minimalt oppsett. Vi skal også deploye alt til skyen med kun ett klikk!

Installer .NET Aspire

Installer siste versjon av Aspire

dotnet workload update
dotnet workload install aspire

Sjekk at .NET Aspire er installert

dotnet workload list

Installed Workload Id Manifest Version Installation Source
------------------------------------------------------------------------------------------------
aspire 9.0.0-preview.3.24210.17/9.0.100-preview.1 SDK 9.0.100-preview.4

Sample Project oppsett

Vi starter med et sample Aspire project som utgangpunkt.

dotnet new aspire-starter --use-redis-cache --output AspireSample

Etter å ha kjørt dette vil flere prosjekter automatisk bli opprettet. Aspire prosjektet som har blitt generert har en front-end, back-end og redis caching ferdig satt opp.

var builder = DistributedApplication.CreateBuilder(args);

var cache = builder.AddRedis("cache");

var apiService = builder.AddProject<Projects.AspireSample_ApiService>("apiservice");

builder.AddProject<Projects.AspireSample_Web>("webfrontend")
.WithExternalHttpEndpoints()
.WithReference(cache)
.WithReference(apiService);

builder.Build().Run();

Her kan vi se hvordan Aspire prosjektet ser ut, cache og back-end er koblet til en front-end applikasjon.

Når vi kjører “AspireSample.AppHost: http” så får vi opp:

Her kan vi se at 3 tjenester kjører, front-end, back-end og redis cache. Hvis vi starter front-end så er det en enkel side som viser været.

Du kan lese en mer detaljert oversikt over Aspire her:

All koden jeg har skrevet kan du finne her:

Caching, logging og trace

Etter å ha vært inne på vår enkle vær app, så kan vi se i oversikten hvordan front-end kaller vår redis cache for å se om vær-data er lagret fra før, deretter sjekkes back-end tjenesten for data.

Deploy til skyen!

Nå som vi alt fungerer lokalt, la oss deploye til skyen. Først trenger vi et verktøy for å deploye via kommando-linja.

På windows trenger du bare å skrive:

winget install microsoft.azd

(mulig du må re-starte etter å ha installert)

Deretter lager vi et Azure prosjekt

azd init

Så er det bare å logge inn og deploye

azd auth login
azd up

PS. Hvis du hare flere kontoer, så kan du velge riktig konto slik:
az account set --subscription <your subscription id>

Slik ser det ut:

Azure CLI (azd)
Azure Portal tjenester

Vips så er alt satt opp i skyen!

Application Insights

La oss fortsette meg å legge til en ny tjeneste, application insights.

Vi har allerede et prosjekt som heter AspireSample.ServiceDefaults, alle tjenestene gjenbruker oppsettet her. Så ved å legge til Application insights konfigurasjonen her, så får alle prosjektene dette.

Alt man trenger å gjøre er å installere Azure.Monitor.OpenTelemetry.AspNetCore, og skrive:

if (!string.IsNullOrEmpty(builder.Configuration["APPLICATIONINSIGHTS_CONNECTION_STRING"]))
{
builder.Services.AddOpenTelemetry()
.UseAzureMonitor();
}

Dette vil legge til application insights i alle tjenestene våre.

Vi vil at Application insights skal bli satt opp automatisk i skyen og koblet med våre eksisterende tjenester, så vi oppdater Aspire Program.cs slik

var builder = DistributedApplication.CreateBuilder(args);

var insights = builder.AddAzureApplicationInsights("applicationInsights");

var cache = builder.AddRedis("cache");

var apiService = builder.AddProject<Projects.AspireSample_ApiService>("apiservice")
.WithReference(insights);

builder.AddProject<Projects.AspireSample_Web>("webfrontend")
.WithExternalHttpEndpoints()
.WithReference(cache)
.WithReference(apiService)
.WithReference(insights);

builder.Build().Run();

Nå har vi front-end, back-end, cache og application insights.

Hvis vi deployer på nytt så har vi fått en ny tjeneste! (kjør azd up)

Og alt er koblet sammen automatisk, hvis vi går inn på vår back-end i Azure og ser på secrets så har en kobling til application insights blitt lagt til.

Hvis vi går inn på et enkelt kall i Application insights kan vi se trace mellom alle applikasjonene på samme måte som i dashbordet vi kjørte lokalt.

trace mellom de forskjellige tjenestene vist i Application insights

Vi må gjøre et lite oppsett for å lagre kobling i prosjektet vårt, i appsettings.json så må man legge til Azure koblingen slik:

  "Azure": {
"SubscriptionId": "<Your subscription id>",
"AllowResourceGroupCreation": true,
"ResourceGroup": "<Valid resource group name>",
"Location": "<Valid Azure location>"
}

Det er bare å kopiere det som akkurat har blitt publisert i Azure.

Hvis vi kjører tjenestene våre lokalt så kan vi også se at connection strings til både redis cache og application insights har blitt lagt til automatisk.

Database

Akkurat nå har vi en back-end som bare generer noen tilfeldige verdier, la oss gjøre applikasjon litt mer realistisk. Vi legger til en database med vær-data.

Vi starter med å legge til pakken Aspire.Hosting.SqlServer i AppHost. Vårt nye oppsett i Aspire blir nå

var builder = DistributedApplication.CreateBuilder(args);

var insights = builder.AddAzureApplicationInsights("applicationInsights");

var cache = builder
.AddRedis("cache")
.WithRedisCommander();

var sql = builder.AddSqlServer("sql")
.AddDatabase("sqldata");

builder.AddProject<Projects.AspireSample_Database>("migration")
.WithReference(sql);

var apiService = builder.AddProject<Projects.AspireSample_ApiService>("apiservice")
.WithReference(cache)
.WithReference(sql)
.WithReference(insights);

builder.AddProject<Projects.AspireSample_Web>("webfrontend")
.WithExternalHttpEndpoints()
.WithReference(cache)
.WithReference(apiService)
.WithReference(insights);

builder.Build().Run();

Enda en tjeneste er lagt til, en SQL database, det krever litt mer oppsett for databasen, vi vil ha EntityFramework, migrasjoner etc. Får å se nøyaktig hvordan dette settes opp kan du lese her:

Her lages et eget “migrations” prosjekt som kjører først og setter opp databasen, evt. gjør endringer på databasen.

Vi kan legge til en migrasjon som oppretter en tabell “weather” og lagrer noen verdier med vær-data:

        protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "Weather",
columns: table => new
{
Id = table.Column<int>(type: "int", nullable: false).Annotation("SqlServer:Identity", "1, 1"),
Date = table.Column<DateTime>(type: "datetime2", nullable: false),
TemperatureC = table.Column<int>(type: "int", nullable: false),
Summary = table.Column<string>(type: "nvarchar(max)", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_Weather", x => x.Id);
});

migrationBuilder.InsertData(
table: "Weather",
columns: new[] { "Date", "TemperatureC", "Summary" },
values: new object[,]
{
{ new DateTime(2024, 7, 6), 25, "Hot" },
{ new DateTime(2024, 7, 7), 24, "Warm" },
{ new DateTime(2024, 7, 8), 23, "Cold" },
{ new DateTime(2024, 7, 9), 22, "Freezing" },
});
}

Dermed har vi ikke lenger tilfeldige verdier, men data fra en SQL database.

Caching

Vi har allerede caching satt opp i vår applikasjon, vår Blazor side med vær-data ser slik ut:

@page "/weather"
@attribute [StreamRendering(true)]
@attribute [OutputCache(Duration = 5)]

@inject WeatherApiClient WeatherApi
(...)

Her har vi OutputCache satt til 5 sekunder, hele siden blir lagret i cache og hentet derfra om det har gått under 5 sekunder siden sist.

Men vi kan forbedre dette, for eksempel, hvis vær-data ikke har endret seg siden sist, så er det ingen grunn til å ikke hente fra cache. Selv når det har gått flere timer eller dager. Vi bør ha mer kontroll over hva som er lagret i cache og når man skal gå videre til back-end databasen.

Vi starter med å legge til Aspire.StackExchange.Redis i API prosjektet vårt. Deretter kan vi legge til builder.AddRedisClient("cache"); i Program.cs.

Her er en oppdatert tjeneste som først sjekker cache, deretter henter data fra SQL databasen, og til slutt oppdaterer cache.

public class WeatherCachedRepository : IWeatherRepository
{
private const string WeatherForecastCacheKey = "weather_forecast";

private readonly WeatherDatabaseRepository _database;
private readonly IConnectionMultiplexer _cache;

public WeatherCachedRepository(WeatherDatabaseRepository database, IConnectionMultiplexer cache)
{
_database = database;
_cache = cache;
}

public async Task<IEnumerable<WeatherForecastDb>> GetWeatherForecastAsync()
{
// 1. Check if the data is in cache
var cacheDb = _cache.GetDatabase();
var cachedData = await cacheDb.StringGetAsync(WeatherForecastCacheKey);
if (cachedData.HasValue)
{
var forecast = JsonSerializer.Deserialize<IEnumerable<WeatherForecastDb>>(cachedData.ToString());
if (forecast is not null)
{
// 2. If it is, return the data
return forecast;
}
}

// 3. If it is not, get the data from the database
var sqlData = await _database.GetWeatherForecastAsync();

// 4. Save the data to the cache
var json = JsonSerializer.Serialize(sqlData);
await cacheDb.StringSetAsync(WeatherForecastCacheKey, json);

return sqlData;
}
}

I praksis så ser det slik ut:

Request 1:

Her ser vi at vi kaller cache, vi finner ingenting, deretter kaller vi SQL databasen og oppdaterer cache. Dette tar 800ms.

Request 2:

Her kan vi set at vi går rett til cache og rører ikke SQL databasen. Kallet tar kun 15ms.

Vi kan også gå å sjekke hva som ligger i cache

Her kan vi se at vi har ett element, “weather_forecast”, med data fra SQL databasen.

Load Balancing

Nå har vi cache, både på selve siden og API kall, men denne siden kommer sikkert til å få enormt med trafikk så kan vi forberede oss ytterligere med load-balancing.

Det er veldig enkelt å sette opp med Aspire, alt man trenger å legge til er .WithReplicas(5) for å spinne opp 5 instanser.

builder.AddProject<Projects.AspireSample_Web>("webfrontend")
.WithExternalHttpEndpoints()
.WithReplicas(5)
.WithReference(cache)
.WithReference(apiService)
.WithReference(insights);

Her har vi satt front-end til å kjøre med 5 instanser. Når vi kjører prosjektet nå så ser det slik ut:

5 instanser av front-end

Og hvis jeg åpner flere nettlesere for å gå inn på siden:

Her kan vi se at trafikk blir fordelt mellom de forskjellige instansene av front-end prosjektet.

Oppsummering

Med veldig lite kode og kun en kommando til Azure så har vi nå satt opp:

  • Front-end
  • Back-end
  • Redis Cache
  • SQL Database
  • Load Balancing
  • Application Insights
  • Key vault
  • App Secrets

Jeg har kun skrevet tre linjer i appsettings.json ellers har jeg gjort null konfigurering og jeg har kun åpnet Azure for å ta screenshot. Vi kunne fortsatt å lagt til flere tjenester, men den generelle formelen er det samme.

Det er ikke sikkert man bør gjøre det fullt så enkelt når man deployer til produksjon, men dette viser hvor enkelt man kan komme igang. Med Aspire kan du også veldig enkelt spinne opp alt lokalt, helt likt som oppsettet vil bli i skyen, dette gjør at du kan teste caching, load-balancing etc. lokalt før du deployer.

Håper du fant denne artikkelen nyttig. Du kan finne all koden som er brukt her:

Ønsker du å vite mer om Systek kan du titte innom vår hjemmeside og ta kontakt → www.systek.no

--

--