✓
Home›Tutorials›.NET & ASP.NET Core›ASP.NET Core 10 Configuration and Dependency Injection: A Practical Tutorial
.NET & ASP.NET Core6 min read

ASP.NET Core 10 Configuration and Dependency Injection: A Practical Tutorial

Learn ASP.NET Core 10 configuration and dependency injection with strongly typed options, startup validation, service lifetimes, and a testable Minimal API.

Configuration and dependency injection are two of the habits that turn a small ASP.NET Core API into an application you can safely change. In this second lesson of the CodeWithChai .NET 10 series, you will move settings out of endpoint code, bind them to a strongly typed options class, validate them when the app starts, and inject a service into a Minimal API endpoint.

Prerequisite: complete Lesson 1: Build Your First ASP.NET Core Minimal API. You should have a project targeting net10.0 that can run locally.

What you will build

We will improve the Greetings API from Lesson 1. Its welcome message will come from configuration rather than a hard-coded value. An IGreetingService will own the business operation, and the endpoint will only translate HTTP into a service call.

  • Store a welcome-message setting in appsettings.json.
  • Bind the setting to a strongly typed options class.
  • Fail fast when required configuration is missing or invalid.
  • Register and inject an application service.
  • Choose an appropriate service lifetime.

Why configuration should not live inside endpoints

Hard-coded values look harmless until an application has different local, test, staging, and production environments. A welcome message may be simple, but the same pattern applies to API base URLs, feature settings, database connections, and timeouts.

ASP.NET Core reads configuration from an ordered set of providers. In a standard application, JSON settings, environment variables, and command-line values are included; later providers win when they use the same key. Environment variables therefore override values in appsettings.json. For portable hierarchical environment-variable names, use double underscores, such as Greeting__WelcomeMessage. See Microsoft’s ASP.NET Core configuration guidance for the full provider order and deployment options.

1. Add an options class

Create a GreetingOptions.cs file. This class gives the configuration section a single, discoverable home. The data-annotation rule also makes the expectation explicit.

using System.ComponentModel.DataAnnotations;

public sealed class GreetingOptions
{
    public const string SectionName = "Greeting";

    [Required]
    [StringLength(120)]
    public string WelcomeMessage { get; init; } = string.Empty;
}

The class deliberately has no secret values. Keep passwords, tokens, and production connection strings out of source control. For development, use Secret Manager; for production, use your platform’s secret store.

2. Add the setting to appsettings.json

Add a Greeting section to your project’s appsettings.json. The property names match the options class, which lets the configuration binder map the values.

{
  "Greeting": {
    "WelcomeMessage": "Welcome to the CodeWithChai .NET 10 API"
  },
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft.AspNetCore": "Warning"
    }
  }
}

To override this value in a container or deployment environment, set Greeting__WelcomeMessage. Do not use a colon in environment-variable names when you need cross-platform compatibility.

3. Bind and validate options at startup

Open Program.cs. Register the options before building the application. ValidateOnStart makes an invalid deployment fail during startup instead of waiting until the first request reaches an endpoint.

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddOptions<GreetingOptions>()
    .Bind(builder.Configuration.GetSection(GreetingOptions.SectionName))
    .ValidateDataAnnotations()
    .ValidateOnStart();

var app = builder.Build();

app.Run();

This is safer than reading a raw string through IConfiguration["Greeting:WelcomeMessage"] throughout the application. The options pattern groups related settings, makes them easier to test, and centralizes validation. Microsoft documents this pattern and ValidateOnStart in its options-pattern guidance.

4. Create a service with one responsibility

Create IGreetingService.cs and GreetingService.cs. The service depends on IOptions<GreetingOptions>, not on the whole configuration system. That keeps its dependency small and makes tests straightforward.

using Microsoft.Extensions.Options;

public interface IGreetingService
{
    GreetingResponse Create(string? name);
}

public sealed class GreetingService : IGreetingService
{
    private readonly GreetingOptions _options;

    public GreetingService(IOptions<GreetingOptions> options)
    {
        _options = options.Value;
    }

    public GreetingResponse Create(string? name)
    {
        var displayName = string.IsNullOrWhiteSpace(name)
            ? "developer"
            : name.Trim();

        return new GreetingResponse(
            $"{_options.WelcomeMessage}, {displayName}!",
            DateTimeOffset.UtcNow);
    }
}

public sealed record GreetingResponse(string Message, DateTimeOffset CreatedAt);

5. Register the service and keep the endpoint thin

Now register the service and map an endpoint that receives it through dependency injection. Replace the earlier sample endpoint with this complete Program.cs example.

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddOptions<GreetingOptions>()
    .Bind(builder.Configuration.GetSection(GreetingOptions.SectionName))
    .ValidateDataAnnotations()
    .ValidateOnStart();

builder.Services.AddScoped<IGreetingService, GreetingService>();

var app = builder.Build();

app.MapGet("/health", () => Results.Ok(new { status = "ok" }));

app.MapGet("/api/greeting", (
    string? name,
    IGreetingService greetingService) =>
{
    return Results.Ok(greetingService.Create(name));
});

app.Run();

Run the app and open /api/greeting?name=Jay. You should receive JSON containing the configured message and a UTC timestamp. Change the setting in appsettings.json, restart the application, and confirm the response changes without editing endpoint logic.

Choosing transient, scoped, and singleton lifetimes

Service lifetime is a design decision, not a default to memorize:

  • Transient: a new instance every time it is requested. Use it for lightweight, stateless services.
  • Scoped: one instance per HTTP request in a web application. This is often right for application services and database contexts.
  • Singleton: one instance for the life of the application. Use it only when the service is thread-safe and safe to share.

We used AddScoped because future versions of GreetingService may depend on request-scoped services such as an EF Core database context. Do not inject a scoped service into a singleton: that creates a captive dependency and can lead to incorrect behavior. Microsoft’s DI guidelines explain disposal and lifetime pitfalls in more detail.

Common mistakes to avoid

  • Reading configuration everywhere: bind related settings once to an options class.
  • Skipping startup validation: configuration mistakes should fail a deployment early, not become a production request failure.
  • Putting secrets in appsettings.json: treat the file as non-secret application configuration.
  • Calling BuildServiceProvider during registration: let ASP.NET Core build the container; creating another container causes duplicate singletons and disposal issues.
  • Registering everything as a singleton: singleton services must be thread-safe and cannot safely depend on scoped services.

Production checklist

  • Validate required options with ValidateDataAnnotations and ValidateOnStart.
  • Use environment variables or a secret manager for production secrets.
  • Choose lifetimes based on ownership and thread safety.
  • Keep endpoints focused on HTTP concerns; put application behavior in services.
  • Write a test for invalid configuration and another for the service’s behavior.
  • Log configuration failures without logging sensitive values.

Next lesson

Next, we will replace the in-memory data with Entity Framework Core and a database, while preserving the clean endpoint-to-service boundary you created here.

Quick recap

You moved a setting out of code, bound it to a typed class, validated it at startup, and injected a service into a .NET 10 Minimal API. These are small changes, but they establish a structure that makes later work—data access, tests, authentication, and deployment—much safer.

Continue learning

If you skipped the first lesson, start with building your first ASP.NET Core Minimal API.

For production operations, read how to build ASP.NET Core readiness and liveness probes.

See all lessons in the .NET and ASP.NET Core tutorial track.