Your First Worker With .Net
This guide walks you through creating a custom .Net Milvaion worker from scratch. By the end, you'll have a working worker with a custom job that you can deploy.
Prerequisites
- .NET 10 SDK installed
- Milvaion stack running (see Quick Start)
- Basic C# knowledge
Step 1: Install the Worker Template
Milvaion provides project templates for quick setup:
dotnet new install Milvasoft.Templates.Milvaion
Verify installation:
dotnet new list milvaion
You should see:
Template Name Short Name Language Tags
----------------------- ----------------------- -------- -----------------------
Milvaion Api Worker milvaion-api-worker [C#] Api/Worker/Milvaion
Milvaion Console Worker milvaion-console-worker [C#] Console/Worker/Milvaion
Step 2: Create a New Worker Project
dotnet new milvaion-console-worker -n MyCompany.BillingWorker
cd MyCompany.BillingWorker
This creates:
MyCompany.BillingWorker/
├── Program.cs # Entry point
├── appsettings.json # Configuration
├── appsettings.Development.json # Dev config
|
├── Jobs/
| └── SampleJob.cs # Example job
|
├── Dockerfile # Container build
└── MyCompany.BillingWorker.csproj
Step 3: Configure the Worker
Edit appsettings.json to point to your Milvaion infrastructure:
{
"Logging": {
"LogLevel": {
"Default": "Debug",
"Microsoft": "Debug",
"System": "Debug"
}
},
"Worker": {
"WorkerId": "sample-worker-01",
"MaxParallelJobs": 128,
"MaxParallelJobsPerWorker": 128,
"ExecutionTimeoutSeconds": 300,
"RabbitMQ": {
"Host": "rabbitmq",
"Port": 5672,
"Username": "guest",
"Password": "guest",
"VirtualHost": "/"
},
"Redis": {
"ConnectionString": "redis:6379",
"Password": "",
"Database": 0,
"CancellationChannel": "Milvaion:JobScheduler:cancellation_channel"
},
"Heartbeat": {
"Enabled": true,
"IntervalSeconds": 5
},
"OfflineResilience": {
"Enabled": true,
"LocalStoragePath": "./worker_data",
"SyncIntervalSeconds": 30,
"MaxSyncRetries": 3,
"CleanupIntervalHours": 1,
"RecordRetentionDays": 1
}
},
"JobConsumers": {
"SimpleJob": {
"ConsumerId": "simple-consumer",
"MaxParallelJobs": 32,
"MaxParallelJobsPerWorker": 128,
"ExecutionTimeoutSeconds": 120,
"MaxRetries": 3,
"BaseRetryDelaySeconds": 5,
"LogUserFriendlyLogsViaLogger": true
},
"SendEmailJob": {
"ConsumerId": "email-consumer",
"MaxParallelJobs": 16,
"MaxParallelJobsPerWorker": 128,
"ExecutionTimeoutSeconds": 600,
"MaxRetries": 3,
"BaseRetryDelaySeconds": 5,
"LogUserFriendlyLogsViaLogger": true
}
}
}
Note
For Docker, use container names (rabbitmq, redis) instead of localhost.
Step 4: Create Your First Job
Create Jobs/GenerateInvoiceJob.cs:
using System.Text.Json;
using Milvasoft.Milvaion.Sdk.Worker.Abstractions;
namespace MyCompany.BillingWorker.Jobs;
public class GenerateInvoiceJob : IAsyncJob<InvoiceJobData>
{
public async Task ExecuteAsync(IJobContext context)
{
// 1. Log start
context.LogInformation("Starting invoice generation job");
// 2. Get job data
var data = context.GetData<InvoiceJobData>();
if (data == null || data.OrderId <= 0)
{
context.LogError("Invalid OrderId");
throw new ArgumentException("OrderId is required");
}
context.LogInformation($"Generating invoice for OrderId: {data.OrderId}");
// 3. Cancellation check
context.CancellationToken.ThrowIfCancellationRequested();
// 4. Simulate invoice generation
await Task.Delay(3000, context.CancellationToken);
// 5. Finish
context.LogInformation($"Invoice successfully generated for OrderId: {data.OrderId}");
}
}
/// <summary>
/// Invoice job data definition.
/// This schema is automatically discovered and displayed in the dashboard.
/// </summary>
public class InvoiceJobData
{
/// <summary>
/// The order ID to generate invoice for.
/// </summary>
[Required]
[Description("The order identifier to generate an invoice for")]
public int OrderId { get; set; }
/// <summary>
/// Currency code for the invoice.
/// </summary>
[DefaultValue("USD")]
[Description("The currency code (e.g., 'USD', 'EUR', 'TRY')")]
public string Currency { get; set; } = "USD";
}
Step 5: Register the Job
Add configuration for your new job in appsettings.json:
{
"JobConsumers": {
"GenerateInvoiceJob": {
"ConsumerId": "invoice-consumer",
"MaxParallelJobs": 8,
"MaxParallelJobsPerWorker": 128,
"ExecutionTimeoutSeconds": 300,
"MaxRetries": 5,
"BaseRetryDelaySeconds": 10,
"LogUserFriendlyLogsViaLogger": true
}
}
}
The SDK automatically discovers jobs that implement any of the job interfaces:
- Non-generic:
IJob,IAsyncJob,IJobWithResult,IAsyncJobWithResult - Generic (typed data):
IJob<TJobData>,IAsyncJob<TJobData>,IJobWithResult<TJobData>,IAsyncJobWithResult<TJobData>
Cancellation
Synchronous jobs (IJob, IJobWithResult) do not support cancellation. For cancellation support, use the async variants.
Step 6: Run the Worker
Locally
dotnet run
Expected output:
info: Milvaion.Sdk.Worker[0]
Registered job: GenerateInvoiceJob → MyCompany.BillingWorker.Jobs.GenerateInvoiceJob
info: Milvaion.Worker.Job[0]
Starting invoice generation job
info: Milvaion.Worker.Job[0]
Generating invoice for OrderId: 12345
info: Milvaion.Worker.Job[0]
Invoice successfully generated for OrderId: 12345
With Docker
Build and run:
Find existing Milvaion network name;
docker inspect milvaion-api --format='{{json .NetworkSettings.Networks}}'
docker build -t my-billing-worker .
docker run -d --name billing-worker \
--network milvaion-quick_milvaion-network \
my-billing-worker