Skip to main content

.NET Service Hosting

Introduction

Service hosting in .NET refers to the process of running and managing services within a .NET application. A service is a component designed to perform specific tasks or provide functionality to other parts of your application or to external clients. In the .NET ecosystem, services can take various forms including web services, background services, Windows services, and more.

Understanding service hosting is crucial for building robust, scalable applications that can reliably perform operations over time. Whether you're building a web API, a background job processor, or a system service, the way you host your services significantly impacts their performance, reliability, and maintainability.

This guide will introduce you to the fundamental concepts of service hosting in .NET, explore different hosting models, and provide practical examples to help you get started.

Basic Concepts of .NET Service Hosting

Before diving into the specifics, let's understand some core concepts:

What is a Service?

In .NET, a service is typically a class that implements specific functionality, often following certain patterns or interfaces:

  • Web Services: APIs that respond to HTTP requests
  • Background Services: Long-running operations that work in the background
  • Worker Services: Standalone services that can run continuously
  • Windows Services: Services that run in the Windows operating system background

Hosting Models

.NET offers several ways to host services:

  1. ASP.NET Core Hosting: For web services and APIs
  2. Generic Host: For non-web scenarios like background services
  3. Windows Service Hosting: For system-level services on Windows
  4. Console Application Hosting: Simple hosting for development or lightweight scenarios

Getting Started with .NET Service Hosting

1. ASP.NET Core Web Service Hosting

ASP.NET Core provides a robust hosting model for web services. Let's create a simple web API:

csharp
// Program.cs
var builder = WebApplication.CreateBuilder(args);

// Add services to the container
builder.Services.AddControllers();
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();

var app = builder.Build();

// Configure the HTTP request pipeline
if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI();
}

app.UseHttpsRedirection();
app.UseAuthorization();
app.MapControllers();

app.Run();

This code sets up a web host for your API controllers. Now let's create a simple controller:

csharp
// WeatherController.cs
using Microsoft.AspNetCore.Mvc;

namespace MyApiService.Controllers;

[ApiController]
[Route("[controller]")]
public class WeatherController : ControllerBase
{
private static readonly string[] Summaries = new[]
{
"Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching"
};

[HttpGet]
public IEnumerable<WeatherForecast> Get()
{
var forecasts = Enumerable.Range(1, 5).Select(index =>
new WeatherForecast
(
DateTime.Now.AddDays(index),
Random.Shared.Next(-20, 55),
Summaries[Random.Shared.Next(Summaries.Length)]
))
.ToArray();

return forecasts;
}
}

public record WeatherForecast(DateTime Date, int TemperatureC, string? Summary)
{
public int TemperatureF => 32 + (int)(TemperatureC / 0.5556);
}

This creates a web service that returns weather forecasts when you access the /weather endpoint.

2. Background Service Hosting with Worker Service

For long-running background tasks, .NET provides the Worker Service template. Here's how to create a basic worker service:

csharp
// Program.cs
using MyWorkerService;

IHost host = Host.CreateDefaultBuilder(args)
.ConfigureServices(services =>
{
services.AddHostedService<Worker>();
})
.Build();

await host.RunAsync();

And the Worker implementation:

csharp
// Worker.cs
namespace MyWorkerService;

public class Worker : BackgroundService
{
private readonly ILogger<Worker> _logger;

public Worker(ILogger<Worker> logger)
{
_logger = logger;
}

protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
while (!stoppingToken.IsCancellationRequested)
{
_logger.LogInformation("Worker running at: {time}", DateTimeOffset.Now);
await Task.Delay(1000, stoppingToken);
}
}
}

This creates a background service that logs a message every second.

3. Generic Host for Console Applications

You can use the .NET Generic Host in console applications to benefit from dependency injection, configuration, and logging:

csharp
// Program.cs
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;

var host = Host.CreateDefaultBuilder(args)
.ConfigureServices((hostContext, services) =>
{
services.AddTransient<IMyService, MyService>();
services.AddHostedService<MyBackgroundService>();
})
.Build();

// Access a service from the service provider
using (var scope = host.Services.CreateScope())
{
var service = scope.ServiceProvider.GetRequiredService<IMyService>();
service.DoSomething();
}

await host.RunAsync();
csharp
// MyService.cs
public interface IMyService
{
void DoSomething();
}

public class MyService : IMyService
{
private readonly ILogger<MyService> _logger;

public MyService(ILogger<MyService> logger)
{
_logger = logger;
}

public void DoSomething()
{
_logger.LogInformation("Doing something at: {time}", DateTimeOffset.Now);
}
}

Advanced Service Hosting Concepts

Configuring Services

Configuration is a crucial aspect of service hosting. Here's how to configure your services using different approaches:

csharp
var builder = WebApplication.CreateBuilder(args);

// From appsettings.json
var connectionString = builder.Configuration.GetConnectionString("DefaultConnection");

// From environment variables
var apiKey = builder.Configuration["ApiKey"];

// Typed configuration
builder.Services.Configure<MyServiceOptions>(
builder.Configuration.GetSection("MyService"));

Example appsettings.json:

json
{
"ConnectionStrings": {
"DefaultConnection": "Server=myserver;Database=mydb;User Id=myuser;Password=mypassword;"
},
"MyService": {
"Option1": "Value1",
"Option2": 42,
"Timeout": "00:00:30"
}
}

Dependency Injection

.NET's built-in dependency injection makes it easy to manage service dependencies:

csharp
// Register services
builder.Services.AddSingleton<IGlobalService, GlobalService>();
builder.Services.AddScoped<IPerRequestService, PerRequestService>();
builder.Services.AddTransient<ITransientService, TransientService>();

// Use services in a controller
public class MyController : ControllerBase
{
private readonly IGlobalService _globalService;
private readonly IPerRequestService _perRequestService;

public MyController(IGlobalService globalService, IPerRequestService perRequestService)
{
_globalService = globalService;
_perRequestService = perRequestService;
}

[HttpGet]
public IActionResult Get()
{
var result = _globalService.GetData();
_perRequestService.ProcessData(result);
return Ok(result);
}
}

Lifetime Management

Service lifetimes determine how long a service instance exists:

  1. Singleton: One instance for the entire application lifetime
  2. Scoped: One instance per scope (typically per HTTP request in web apps)
  3. Transient: A new instance every time it's requested

Health Checks

Adding health checks to your services helps monitor their status:

csharp
builder.Services.AddHealthChecks()
.AddCheck("database", () => {
// Check if database is available
return HealthCheckResult.Healthy("Database is up and running");
})
.AddCheck<ExternalApiHealthCheck>("externalApi");

// In the request pipeline
app.MapHealthChecks("/health");

Real-world Examples

Example 1: Microservice with API and Background Processing

Let's build a more complete example of a microservice that combines a web API with background processing:

csharp
// Program.cs
var builder = WebApplication.CreateBuilder(args);

// Add services
builder.Services.AddControllers();
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();

// Register our custom services
builder.Services.AddSingleton<IDataRepository, DataRepository>();
builder.Services.AddScoped<IOrderProcessor, OrderProcessor>();

// Add a background service
builder.Services.AddHostedService<OrderProcessingService>();

// Add health checks
builder.Services.AddHealthChecks()
.AddCheck<DatabaseHealthCheck>("database")
.AddCheck<MessageQueueHealthCheck>("message-queue");

var app = builder.Build();

if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI();
}

app.UseHttpsRedirection();
app.UseAuthorization();
app.MapControllers();
app.MapHealthChecks("/health");

app.Run();
csharp
// OrderProcessingService.cs
public class OrderProcessingService : BackgroundService
{
private readonly IServiceScopeFactory _scopeFactory;
private readonly ILogger<OrderProcessingService> _logger;

public OrderProcessingService(
IServiceScopeFactory scopeFactory,
ILogger<OrderProcessingService> logger)
{
_scopeFactory = scopeFactory;
_logger = logger;
}

protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
while (!stoppingToken.IsCancellationRequested)
{
try
{
_logger.LogInformation("Processing pending orders at {time}", DateTimeOffset.Now);

// Create a scope to resolve scoped services
using (var scope = _scopeFactory.CreateScope())
{
var processor = scope.ServiceProvider.GetRequiredService<IOrderProcessor>();
await processor.ProcessPendingOrdersAsync();
}
}
catch (Exception ex)
{
_logger.LogError(ex, "Error processing orders");
}

await Task.Delay(TimeSpan.FromMinutes(1), stoppingToken);
}
}
}
csharp
// OrdersController.cs
[ApiController]
[Route("[controller]")]
public class OrdersController : ControllerBase
{
private readonly IOrderProcessor _orderProcessor;
private readonly ILogger<OrdersController> _logger;

public OrdersController(
IOrderProcessor orderProcessor,
ILogger<OrdersController> logger)
{
_orderProcessor = orderProcessor;
_logger = logger;
}

[HttpPost]
public async Task<IActionResult> CreateOrder(OrderRequest request)
{
_logger.LogInformation("Creating new order for customer {CustomerId}", request.CustomerId);

var orderId = await _orderProcessor.CreateOrderAsync(request);

return Ok(new { OrderId = orderId });
}

[HttpGet("{id}")]
public async Task<IActionResult> GetOrder(string id)
{
var order = await _orderProcessor.GetOrderAsync(id);

if (order == null)
return NotFound();

return Ok(order);
}
}

Example 2: Windows Service Hosting

For Windows-specific services, you can create a worker that can be run as a Windows Service:

csharp
// Program.cs
using Microsoft.Extensions.Hosting.WindowsServices;

IHost host = Host.CreateDefaultBuilder(args)
.UseWindowsService(options =>
{
options.ServiceName = "MyCustomService";
})
.ConfigureServices(services =>
{
services.AddHostedService<WindowsBackgroundService>();
})
.Build();

await host.RunAsync();
csharp
// WindowsBackgroundService.cs
public class WindowsBackgroundService : BackgroundService
{
private readonly ILogger<WindowsBackgroundService> _logger;

public WindowsBackgroundService(ILogger<WindowsBackgroundService> logger)
{
_logger = logger;
}

protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
try
{
_logger.LogInformation("Windows Service started at: {time}", DateTimeOffset.Now);

// Register for system events like power events
SystemEvents.PowerModeChanged += OnPowerModeChanged;

while (!stoppingToken.IsCancellationRequested)
{
// Perform service operations
_logger.LogInformation("Service running at: {time}", DateTimeOffset.Now);
await Task.Delay(TimeSpan.FromMinutes(5), stoppingToken);
}
}
catch (Exception ex)
{
_logger.LogError(ex, "Error in Windows Service");
throw;
}
finally
{
SystemEvents.PowerModeChanged -= OnPowerModeChanged;
_logger.LogInformation("Windows Service stopping at: {time}", DateTimeOffset.Now);
}
}

private void OnPowerModeChanged(object sender, PowerModeChangedEventArgs e)
{
_logger.LogInformation("Power mode changed: {mode}", e.Mode);

if (e.Mode == PowerModes.Suspend)
{
// Save state before computer goes to sleep
}
else if (e.Mode == PowerModes.Resume)
{
// Restore state when computer wakes up
}
}

public override Task StartAsync(CancellationToken cancellationToken)
{
_logger.LogInformation("Starting Windows Service");
return base.StartAsync(cancellationToken);
}

public override Task StopAsync(CancellationToken cancellationToken)
{
_logger.LogInformation("Stopping Windows Service");
return base.StopAsync(cancellationToken);
}
}

Best Practices for .NET Service Hosting

  1. Use the Generic Host when possible: It provides a unified way to configure services, dependency injection, logging, and more.

  2. Separate concerns: Keep your service logic separate from the hosting code to make it easier to test and maintain.

  3. Handle exceptions properly: Services should be resilient to errors. Implement proper exception handling and logging.

  4. Implement graceful shutdown: Ensure your services can shut down cleanly when requested:

csharp
public override async Task StopAsync(CancellationToken cancellationToken)
{
_logger.LogInformation("Service is stopping...");

// Clean up resources, close connections, etc.
await _databaseConnection.CloseAsync(cancellationToken);

await base.StopAsync(cancellationToken);
}
  1. Configure service timeouts: Set appropriate timeouts for service startup and shutdown:
csharp
Host.CreateDefaultBuilder(args)
.ConfigureServices((hostContext, services) =>
{
services.Configure<HostOptions>(opts =>
{
opts.ShutdownTimeout = TimeSpan.FromSeconds(30);
});
})
  1. Use health checks: Implement health checks to monitor your services.

  2. Implement metrics: Add metrics collection to understand how your services are performing.

Summary

.NET Service Hosting offers a flexible and powerful way to run various types of services within your applications. We've covered:

  • The fundamental concepts of service hosting in .NET
  • Different hosting models for various scenarios
  • How to create and configure web services, background services, and Windows services
  • Best practices for reliable service hosting
  • Real-world examples combining multiple service types

By leveraging the built-in hosting capabilities of .NET, you can create robust, maintainable services that can run reliably in different environments, from development to production.

Additional Resources

Exercises

  1. Create a simple web API with at least two endpoints that perform CRUD operations on a collection of items.

  2. Implement a background service that processes items from a queue and logs the results.

  3. Combine a web API and background service in a single application, where the API adds items to a queue and the background service processes them.

  4. Add health checks to your service that verify external dependencies like databases or other APIs.

  5. Implement graceful shutdown handling in a background service that ensures work in progress is completed before the service stops.



If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)