---
description: Create a new .NET Aspire worker project with Kafka, RavenDB, and MinIO integration (project)
---
# Add Aspire Worker Project Skill
Create a new .NET Aspire worker project with messaging, database, and storage integration for NovaTune.
## Project Context
- Workers location: `src/NovaTuneApp/NovaTuneApp.Workers.{Name}/`
- AppHost: `src/NovaTuneApp/NovaTuneApp.AppHost/`
- Solution: `src/NovaTuneApp/NovaTuneApp.sln`
- Naming convention: `NovaTuneApp.Workers.{PurposeName}`
## Steps
### 1. Create Project Directory
```bash
mkdir -p src/NovaTuneApp/NovaTuneApp.Workers.Lifecycle
```
### 2. Create Project File
Location: `src/NovaTuneApp/NovaTuneApp.Workers.Lifecycle/NovaTuneApp.Workers.Lifecycle.csproj`
```xml
net9.0
enable
enable
dotnet-NovaTuneApp.Workers.Lifecycle
```
### 3. Create Program.cs
Location: `src/NovaTuneApp/NovaTuneApp.Workers.Lifecycle/Program.cs`
```csharp
using Confluent.Kafka;
using KafkaFlow;
using KafkaFlow.Serializer;
using Microsoft.Extensions.Options;
using Minio;
using NovaTuneApp.ApiService.Infrastructure.Configuration;
using NovaTuneApp.Workers.Lifecycle.Handlers;
using NovaTuneApp.Workers.Lifecycle.Services;
using Raven.Client.Documents;
using Serilog;
using Serilog.Events;
using Serilog.Formatting.Compact;
// Bootstrap logging
Log.Logger = new LoggerConfiguration()
.MinimumLevel.Override("Microsoft", LogEventLevel.Warning)
.Enrich.FromLogContext()
.WriteTo.Console(new RenderedCompactJsonFormatter())
.CreateBootstrapLogger();
try
{
var builder = Host.CreateApplicationBuilder(args);
// Add service defaults (OpenTelemetry, health checks, etc.)
builder.AddServiceDefaults();
// Serilog
builder.Services.AddSerilog((services, configuration) => configuration
.ReadFrom.Configuration(builder.Configuration)
.ReadFrom.Services(services)
.MinimumLevel.Information()
.MinimumLevel.Override("Microsoft", LogEventLevel.Warning)
.MinimumLevel.Override("KafkaFlow", LogEventLevel.Information)
.Enrich.FromLogContext()
.Enrich.WithEnvironmentName()
.Enrich.WithMachineName()
.WriteTo.Console(new RenderedCompactJsonFormatter()));
// Configuration
builder.Services.Configure(
builder.Configuration.GetSection(NovaTuneOptions.SectionName));
builder.Services.Configure(
builder.Configuration.GetSection(LifecycleOptions.SectionName));
var topicPrefix = builder.Configuration["NovaTune:TopicPrefix"] ?? "dev";
var bootstrapServers = builder.Configuration.GetConnectionString("messaging")
?? "localhost:9092";
// RavenDB
var ravenConnectionString = builder.Configuration.GetConnectionString("novatune");
string ravenDbUrl;
string ravenDbDatabase;
if (ravenConnectionString != null && ravenConnectionString.Contains(';'))
{
var parts = ravenConnectionString.Split(';')
.Select(p => p.Split('=', 2))
.Where(p => p.Length == 2)
.ToDictionary(p => p[0], p => p[1]);
ravenDbUrl = parts.GetValueOrDefault("URL") ?? "http://localhost:8080";
ravenDbDatabase = parts.GetValueOrDefault("Database") ?? "NovaTune";
}
else
{
ravenDbUrl = ravenConnectionString ?? "http://localhost:8080";
ravenDbDatabase = builder.Configuration["RavenDb:Database"] ?? "NovaTune";
}
builder.Services.AddSingleton(sp =>
{
var store = new DocumentStore
{
Urls = [ravenDbUrl],
Database = ravenDbDatabase
};
store.Initialize();
return store;
});
// MinIO
var minioEndpoint = builder.Configuration.GetConnectionString("storage")
?? "http://localhost:9000";
var minioAccessKey = builder.Configuration["MinIO:AccessKey"] ?? "minioadmin";
var minioSecretKey = builder.Configuration["MinIO:SecretKey"] ?? "minioadmin";
var minioHost = minioEndpoint.Replace("http://", "").Replace("https://", "");
var useSSL = minioEndpoint.StartsWith("https://");
builder.Services.AddSingleton(_ =>
new MinioClient()
.WithEndpoint(minioHost)
.WithCredentials(minioAccessKey, minioSecretKey)
.WithSSL(useSSL)
.Build());
// Health Checks
builder.Services.AddHealthChecks()
.AddRavenDB(
setup => setup.Urls = [ravenDbUrl],
name: "ravendb",
timeout: TimeSpan.FromSeconds(5))
.AddKafka(
new ProducerConfig { BootstrapServers = bootstrapServers },
name: "kafka",
timeout: TimeSpan.FromSeconds(5))
.AddUrlGroup(
new Uri($"{minioEndpoint}/minio/health/live"),
name: "minio",
timeout: TimeSpan.FromSeconds(5));
// KafkaFlow Consumer (track deletions)
builder.Services.AddKafka(kafka => kafka
.UseMicrosoftLog()
.AddCluster(cluster =>
{
cluster.WithBrokers([bootstrapServers]);
cluster.AddConsumer(consumer => consumer
.Topic($"{topicPrefix}-track-deletions")
.WithGroupId($"{topicPrefix}-lifecycle-worker")
.WithBufferSize(100)
.WithWorkersCount(2)
.WithAutoOffsetReset(KafkaFlow.AutoOffsetReset.Earliest)
.AddMiddlewares(m => m
.AddDeserializer()
.AddTypedHandlers(h => h.AddHandler())
)
);
})
);
// Services
builder.Services.AddTransient();
builder.Services.AddScoped();
// Background Services
builder.Services.AddHostedService();
builder.Services.AddHostedService();
var host = builder.Build();
await host.RunAsync();
}
catch (Exception ex)
{
Log.Fatal(ex, "Lifecycle worker terminated unexpectedly");
}
finally
{
Log.CloseAndFlush();
}
```
### 4. Add to Solution
```bash
cd src/NovaTuneApp
dotnet sln add NovaTuneApp.Workers.Lifecycle/NovaTuneApp.Workers.Lifecycle.csproj
```
### 5. Register in AppHost
Location: `src/NovaTuneApp/NovaTuneApp.AppHost/AppHost.cs`
Add project reference to `NovaTuneApp.AppHost.csproj`:
```xml
```
Add to AppHost.cs (in the non-testing block):
```csharp
// Lifecycle Worker - handles physical deletion of soft-deleted tracks
builder.AddProject("lifecycle-worker")
.WithReference(messaging)
.WaitFor(messaging)
.WithReference(database)
.WaitFor(database)
.WithReference(storage.GetEndpoint("api"))
.WaitFor(storage)
.WithEnvironment("NovaTune__TopicPrefix", "dev");
```
### 6. Create Configuration Class
Location: `src/NovaTuneApp/NovaTuneApp.Workers.Lifecycle/Configuration/LifecycleOptions.cs`
```csharp
namespace NovaTuneApp.Workers.Lifecycle.Configuration;
public class LifecycleOptions
{
public const string SectionName = "Lifecycle";
///
/// Interval between physical deletion polling.
/// Default: 5 minutes.
///
public TimeSpan PollingInterval { get; set; } = TimeSpan.FromMinutes(5);
///
/// Maximum tracks to process per cycle.
/// Default: 50.
///
public int BatchSize { get; set; } = 50;
///
/// Whether physical deletion is enabled.
/// Default: true.
///
public bool Enabled { get; set; } = true;
}
```
### 7. Create appsettings.json
Location: `src/NovaTuneApp/NovaTuneApp.Workers.Lifecycle/appsettings.json`
```json
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.Hosting.Lifetime": "Information"
}
},
"NovaTune": {
"TopicPrefix": "dev"
},
"Lifecycle": {
"PollingInterval": "00:05:00",
"BatchSize": 50,
"Enabled": true
}
}
```
## Project Structure
```
NovaTuneApp.Workers.Lifecycle/
├── Configuration/
│ └── LifecycleOptions.cs
├── Handlers/
│ └── TrackDeletedHandler.cs
├── Services/
│ ├── IPhysicalDeletionService.cs
│ └── PhysicalDeletionService.cs
├── Program.cs
├── appsettings.json
├── appsettings.Development.json
└── NovaTuneApp.Workers.Lifecycle.csproj
```
## Dependencies Pattern
Workers typically depend on:
| Dependency | Purpose |
|------------|---------|
| `NovaTuneApp.ServiceDefaults` | OpenTelemetry, health checks, service discovery |
| `NovaTuneApp.ApiService` | Shared models, configuration, services |
| `KafkaFlow` | Kafka/Redpanda messaging |
| `RavenDB.Client` | Document database |
| `Minio` | Object storage |
| `Serilog` | Structured logging |
## Verification
After creating the project:
```bash
# Build solution
dotnet build src/NovaTuneApp/NovaTuneApp.sln
# Run the worker standalone
dotnet run --project src/NovaTuneApp/NovaTuneApp.Workers.Lifecycle
# Run with Aspire orchestration
dotnet run --project src/NovaTuneApp/NovaTuneApp.AppHost
```