Demystifying .NET Core apps, a quick walkthrough (.NET 6)
This is just an overview of what makes .NET Core apps work with links pointing to a much more in-depth coverage of these concepts. There are several types of .NET Core apps but this only covers the common ones, or at least those I have worked with. Hopefully this will give you a much better understanding of the scope involved in developing .NET Core apps. Let’s go straight to it then.
.NET Generic Host
.NET Core apps uses this .NET Generic Host to handle the apps’ resources such as:
- Dependency Injection (DI)
- Logging
- Configuration
- App shutdown
They are 2 versions of this host and are created by these 2 builders:
.NET Core apps such as the Worker Services and even Console apps use the HostBuilder
version while ASP.NET Core apps such as Web APIs and MVC apps use the WebApplicationBuilder
version.
Historically, this host was originally developed for ASP.NET Core apps but was extended for use with other types of .NET Core apps.
Let’s look at the different .NET Core apps generated from templates provided in the Visual Studio 2022 Community Edition and see what type of host they use and how it is used in it’s basic setup.
Console App
Here is the generated code from the Console App
template with the usual startup code or entry point for any .NET Core app found in Program.cs
file.
/* Program.cs */
// See https://aka.ms/new-console-template for more information
Console.WriteLine("Hello, World!");
It does not actually contain code that uses a host. You will have to add it yourself which is not that difficult once you looked at the generated Worker Service
code later on.
What you will notice in the code is the missing Main
method. This is the new form in .NET 6. See .NET 6 C# console app template generates top-level statements for more information on this.
Worker Service
Here is the generated code from the Worker Service
template:
/* Program.cs */
using WorkerService1;
IHost host = Host.CreateDefaultBuilder(args)
.ConfigureServices(services =>
{
services.AddHostedService<Worker>();
})
.Build();
await host.RunAsync();
/* Worker.cs */
namespace WorkerService1
{
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);
}
}
}
}
In the Program.cs
file,
- the Host.CreateDefaultBuilder() returns an instance of the HostBuilder class.
- From this host you can then add services to the (dependency injection) container via ConfigureServices().
- Then you register your own implementation of an IHostedService which in this case is the
Worker
in theWorker.cs
file via AddHostedService<T>(). - Then you Build() your host.
- And lastly, call Run() or RunAsync() on the host to run all registered hosted services, because you can have multiple hosted services running and not just one.
The Worker.cs
file, on the otherhand, contains the Worker
class which inherits from BackgroundService which is an implementation of an IHostedService.
Note that in the constructor method of the Worker
class, Worker()
, is passed, or shall we say is where an ILogger<T> implementation is injected. This is an example of how .NET Generic Host provides dependency injection for services such as the basic logging provided by .NET Core.
Next the function ExecuteAsync() is called when the hosted service starts. It accepts a CancellationToken which means this long running operation should be cancellable.
ASP.NET Core Web API
Here is the generated code from the ASP.NET Core Web API
template:
/* Program.cs */
var builder = WebApplication.CreateBuilder(args);
// Add services to the container.
builder.Services.AddControllers();
// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
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();
/* WeatherForecastController.cs */
using Microsoft.AspNetCore.Mvc;
namespace ControllerWebAPI1.Controllers
{
[ApiController]
[Route("[controller]")]
public class WeatherForecastController : ControllerBase
{
private static readonly string[] Summaries = new[]
{
"Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching"
};
private readonly ILogger<WeatherForecastController> _logger;
public WeatherForecastController(ILogger<WeatherForecastController> logger)
{
_logger = logger;
}
[HttpGet(Name = "GetWeatherForecast")]
public IEnumerable<WeatherForecast> Get()
{
return Enumerable.Range(1, 5).Select(index => new WeatherForecast
{
Date = DateTime.Now.AddDays(index),
TemperatureC = Random.Shared.Next(-20, 55),
Summary = Summaries[Random.Shared.Next(Summaries.Length)]
})
.ToArray();
}
}
}
In the Program.cs
file of an ASP.NET Core app such as Web API app,
- the WebApplication.CreateBuilder() is called to create an instance of the WebApplicationBuilder class.
- Then the following services are added into it’s DI container, the WebApplicationBuilder.Services:
- Adds MVC services for controllers via AddControllers()
- Configures ApiExplorer using Endpoint’s Metadata via AddEndpointsApiExplorer()
- Adds the Swagger generator via
AddSwaggerGen()
. See Get started with Swashbuckle and ASP.NET Core.
- Then you Build() the host, the WebApplication.
- Next you configure the HTTP request pipeline or the middleware with the following calls:
- Register the Swagger middleware via
UseSwagger()
- Register the SwaggerUI middleware via
UseSwaggerUI()
- Adds middleware for redirecting HTTP requests to HTTPS vi UseHttpsRedirection()
- Enables authorization capabilities via UseAuthorization()
- Adds endpoints to controller actions via MapControllers()
See ASP.NET Core Middleware for more information of what middleware is and what other built-in middleware components came with ASP.NET Core. Briefly, they replace the HTTP handlers and modules in previous ASP.NET versions and come with Use, Map, or Run extension methods.
- Register the Swagger middleware via
- And lastly, runs the application via Run() which blocks the calling thread until host shutdown
The Old Startup.cs
You will notice that all the configuration and setup is done in the Program.cs
file. Before .NET 6, it used to be in 2 files: Program.cs
and Startup.cs
and it looks something like this:
/* Program.cs */
public class Program
{
public static void Main(string[] args)
{
CreateWebHostBuilder(args).Build().Run();
}
public static IWebHostBuilder CreateWebHostBuilder(string[] args)
{
IConfiguration config = Configuration.CreateConfigurationContainer();
return WebHost.CreateDefaultBuilder(args)
.UseConfiguration(config)
.ConfigureLogging(logging =>
{
logging.ClearProviders();
logging.AddConsole();
})
.UseStartup<Startup>();
}
}
/* Startup.cs */
public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
public IConfiguration Configuration { get; }
// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc()
.SetCompatibilityVersion(CompatibilityVersion.Version_2_1);
// etc.
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseMvc();
}
}
You have the Program.cs
file where you create the host and call UseStartup<TStartup>() to use a separate file containing the startup code.
In the Startup.cs
file, you implement the following 2 methods that are called by the runtime:
- ConfigureServices(IServiceCollection) - to add services to the container
- Configure(IApplicationBuilder app, IHostingEnvironment env) - to configure the HTTP request pipeline
As you can see there is more boilerplate code you need to add compared to the new minimal hosting model in .NET 6.
Minimal API
Minimal API is a new type of .NET Core application in .NET 6. You can generate the code from the same ASP.NET Core Web API
template but unselecting the checkbox besides Use controllers (uncheck to use minimal APIs)
. Below is the generated code:
/* Program.cs */
var builder = WebApplication.CreateBuilder(args);
// Add services to the container.
// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
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();
var summaries = new[]
{
"Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching"
};
app.MapGet("/weatherforecast", () =>
{
var forecast = 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 forecast;
})
.WithName("GetWeatherForecast");
app.Run();
internal record WeatherForecast(DateTime Date, int TemperatureC, string? Summary)
{
public int TemperatureF => 32 + (int)(TemperatureC / 0.5556);
}
So it does not have controller code like in the regular Web API but it makes a call to MapGet(). I won’t go over too much on this but feel free to look at Minimal APIs overview for more information. Also Routing basics in ASP.NET Core.
ASP.NET Core Empty
And just to show you how many lines of code is required to get yourself a bare minimum, no content ASP.NET Core web application up and running, you can use the ASP.NET Core Empty
template to generate this code for you:
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.MapGet("/", () => "Hello World!");
app.Run();
Adding Host to a Console App
As previously mentioned, you need to add the .NET Generic Host to the console app because the generated code does not expose it. You will need to add a NuGet package for Microsoft.Hosting.Extensions before you can use the host. See this video showing you how to do this.
And below is the code to use host in a console app after adding the NuGet package:
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
var host = Host.CreateDefaultBuilder(args).Build();
// See https://aka.ms/new-console-template for more information
Console.WriteLine("Hello, World!");
var logger = host.Services.GetRequiredService<ILogger<Program>>();
logger.LogInformation("Host created.");
await host.RunAsync();
And as an added bonus, it also shows you how to get the host’s default logger service to log messages.
What’s Next
There’s still a lot more to cover but this is only part 1. In the next part or so, I like to get more into .NET Generic Host’s features such as dependency injection, logging, and configuration; some of the framework provided sevices; some of ASP.NET Core’s built-in HTTP middleware like authentication and authorization; and how to create a custom HTTP middleware.