Skip to content

Instantly share code, notes, and snippets.

@rkttu
Created April 21, 2026 23:54
Show Gist options
  • Select an option

  • Save rkttu/ebabfa7c0aeaac59e4de3603e4a3f232 to your computer and use it in GitHub Desktop.

Select an option

Save rkttu/ebabfa7c0aeaac59e4de3603e4a3f232 to your computer and use it in GitHub Desktop.
File-based App 기반 Aspire 테스트 프로젝트 샘플

Aspire 테스트 프로젝트

이 저장소는 .NET 파일 기반 앱(File-based apps).NET Aspire AppHost를 이용해, 아래 3개 컴포넌트를 함께 실행하는 간단한 예제입니다.

  • apphost.cs: 전체 분산 애플리케이션 오케스트레이션(AppHost)
  • worker.cs: Redis(Garnet)에 주기적으로 시간을 기록하는 백그라운드 워커
  • minapi.cs: Redis(Garnet)에 저장된 값을 읽어 HTTP API로 반환하는 Minimal API

구성 파일 설명

apphost.cs

  • DistributedApplication을 생성하고 실행합니다.
  • apphost.json과 사용자 시크릿(apphosttest), 환경 변수를 설정 소스로 사용합니다.
  • cache라는 이름의 Garnet(Redis 호환) 리소스를 추가합니다.
  • worker.cs, minapi.cs를 C# 앱으로 등록하고 cache 리소스를 참조하도록 연결합니다.
  • minapi에는 HTTP/HTTPS 엔드포인트를 노출합니다.

worker.cs

  • Worker SDK 기반 백그라운드 서비스입니다.
  • Aspire의 Redis 클라이언트 연결(cache)을 주입받습니다.
  • 1초마다 LastUpdated 키를 Redis에 갱신합니다.
  • OpenTelemetry(로그/메트릭/트레이스) OTLP 내보내기를 설정합니다.

minapi.cs

  • Web SDK 기반 Minimal API입니다.
  • Aspire의 Redis 클라이언트 연결(cache)을 주입받습니다.
  • / 엔드포인트에서:
    • Message 키가 없으면 Hello, World!를 저장
    • LastUpdated 키가 있으면 함께 응답 문자열에 포함
  • OpenTelemetry(로그/메트릭/트레이스) OTLP 내보내기를 설정합니다.

apphost.json

앱 실행 시 사용할 환경 변수 기본값이 들어 있습니다.

  • ASPNETCORE_URLS: Minimal API 바인딩 URL (http://localhost:18888)
  • ASPIRE_DASHBOARD_OTLP_ENDPOINT_URL: 대시보드 OTLP gRPC 엔드포인트
  • ASPIRE_DASHBOARD_OTLP_HTTP_ENDPOINT_URL: 대시보드 OTLP HTTP 엔드포인트
  • ASPIRE_ALLOW_UNSECURED_TRANSPORT: 비보안 전송 허용
  • ASPIRE_CONTAINER_RUNTIME: 컨테이너 런타임 (podman)

aspire.config.json

Aspire 설정 파일로, AppHost 진입 파일 경로(apphost.cs)를 지정합니다.

사전 요구사항

  1. .NET SDK 10 이상
  2. Aspire CLI 13.2 이상
  3. 컨테이너 런타임
    • 현재 설정은 podman 기준입니다.
    • Docker를 사용하려면 ASPIRE_CONTAINER_RUNTIME=docker로 변경하세요.
  4. (선택) 사용자 시크릿 사용 시:
    • apphost.cs에서 AddUserSecrets("apphosttest")를 사용하므로, 필요 시 해당 ID로 시크릿을 등록하세요.

실행 방법

프로젝트 루트(이 README가 있는 폴더)에서 아래 명령을 실행하세요.

dotnet run apphost.cs

실행되면 AppHost가 다음을 함께 구동합니다.

  • Garnet(cache)
  • Worker 앱
  • Minimal API 앱

동작 확인

  1. API 호출:

    curl http://localhost:18888/
  2. 예상 응답 예시:

    Hello, World! / Last updated: 2026-04-22 오전 10:00:00 +09:00
    

Worker가 1초마다 LastUpdated 값을 바꾸므로, 연속 호출 시 시간이 갱신됩니다.

문제 해결

  • 포트 충돌이 있으면 apphost.json의 URL/포트를 변경하세요.
  • Podman 대신 Docker를 쓰려면 ASPIRE_CONTAINER_RUNTIME 값을 docker로 바꾸세요.
  • 컨테이너 실행 문제가 있으면 컨테이너 런타임이 실행 중인지 먼저 확인하세요.

라이선스

이 저장소의 코드는 MIT 라이선스를 따릅니다.

#:sdk Aspire.AppHost.Sdk@13.2.0
#:package Aspire.Hosting.Garnet@13.*
#pragma warning disable ASPIRECSHARPAPPS001
using Microsoft.Extensions.Configuration;
var builder = DistributedApplication.CreateBuilder(args);
builder.Configuration
.AddJsonFile("apphost.json")
.AddUserSecrets("apphosttest")
.AddEnvironmentVariables();
var garnet = builder.AddGarnet("cache");
_ = builder.AddCSharpApp("worker", "worker.cs")
.WithReference(garnet)
.WaitFor(garnet);
_ = builder.AddCSharpApp("minapi", "minapi.cs")
.WithReference(garnet)
.WaitFor(garnet)
.WithHttpsEndpoint()
.WithHttpEndpoint();
using var app = builder.Build();
app.Run();
{
"ASPNETCORE_URLS": "http://localhost:18888",
"ASPIRE_DASHBOARD_OTLP_ENDPOINT_URL": "http://localhost:18889",
"ASPIRE_DASHBOARD_OTLP_HTTP_ENDPOINT_URL": "http://localhost:18890",
"ASPIRE_ALLOW_UNSECURED_TRANSPORT": "true",
"ASPIRE_CONTAINER_RUNTIME": "podman"
}
#!/usr/bin/env dotnet
#:sdk Microsoft.NET.Sdk.Web
#:property PublishAot=false
#:package Aspire.StackExchange.Redis@13.*
#:package OpenTelemetry.Exporter.OpenTelemetryProtocol@1.*
#:package OpenTelemetry.Extensions.Hosting@1.*
#:package OpenTelemetry.Instrumentation.AspNetCore@1.*
#:package OpenTelemetry.Instrumentation.Http@1.*
#:package OpenTelemetry.Instrumentation.Runtime@1.*
using Microsoft.AspNetCore.Mvc;
using OpenTelemetry;
using OpenTelemetry.Metrics;
using OpenTelemetry.Trace;
using StackExchange.Redis;
var builder = WebApplication.CreateBuilder(args);
builder.AddRedisClient("cache");
builder.Logging.AddOpenTelemetry(l =>
{
l.IncludeFormattedMessage = true;
l.IncludeScopes = true;
});
builder.Services.AddOpenTelemetry()
.WithMetrics(m => m.AddAspNetCoreInstrumentation().AddHttpClientInstrumentation().AddRuntimeInstrumentation())
.WithTracing(t => t.AddSource(builder.Environment.ApplicationName).AddAspNetCoreInstrumentation().AddHttpClientInstrumentation())
.UseOtlpExporter();
using var app = builder.Build();
app.MapGet("/", async ([FromServices] IConnectionMultiplexer cache) =>
{
var database = cache.GetDatabase();
var messageKey = new RedisKey("Message");
var lastUpdatedKey = new RedisKey("LastUpdated");
if (!await database.KeyExistsAsync(messageKey))
await database.StringSetAsync(messageKey, new RedisValue("Hello, World!"));
var message = (await database.StringGetAsync(messageKey)).ToString();
if (await database.KeyExistsAsync(lastUpdatedKey))
message += " / " + (await database.StringGetAsync(lastUpdatedKey)).ToString();
return Results.Ok(message);
});
app.Run();
#!/usr/bin/env dotnet
#:sdk Microsoft.NET.Sdk.Worker
#:property PublishAot=false
#:package Microsoft.Extensions.Hosting@10.*
#:package Aspire.StackExchange.Redis@13.*
#:package OpenTelemetry.Exporter.OpenTelemetryProtocol@1.*
#:package OpenTelemetry.Extensions.Hosting@1.*
#:package OpenTelemetry.Instrumentation.Http@1.*
#:package OpenTelemetry.Instrumentation.Runtime@1.*
using OpenTelemetry;
using OpenTelemetry.Metrics;
using OpenTelemetry.Trace;
using StackExchange.Redis;
var builder = Host.CreateApplicationBuilder(args);
builder.Services.AddHostedService<Worker>();
builder.AddRedisClient("cache");
builder.Logging.AddOpenTelemetry(l =>
{
l.IncludeFormattedMessage = true;
l.IncludeScopes = true;
});
builder.Services.AddOpenTelemetry()
.WithMetrics(m => m.AddHttpClientInstrumentation().AddRuntimeInstrumentation())
.WithTracing(t => t.AddSource(builder.Environment.ApplicationName).AddHttpClientInstrumentation())
.UseOtlpExporter();
using var app = builder.Build();
app.Run();
public sealed class Worker(
ILogger<Worker> logger, IConnectionMultiplexer cache
) : BackgroundService
{
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
var key = new RedisKey("LastUpdated");
var database = cache.GetDatabase();
while (true)
{
var current = DateTimeOffset.Now;
await database.StringSetAsync(key, "Last updated: " + current.ToString());
logger.LogInformation("Worker running at: {Time}", current);
await Task.Delay(TimeSpan.FromSeconds(1d), stoppingToken);
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment