.NET IHostBuilder.ConfigureWebHostDefaults() 와 ApplicationPart
개요
IHostBuilder.ConfigureWebHostDefaults() 를 사용하여 WebHost 를 구성할 때 유의해야 하는 점을 소개합니다.
IHostBuilder.ConfigureWebHostDefaults()
| version | method | 
|---|---|
| ASP.NET Core 2.x | WebHost.CreateDefaultBuilder() | 
| ASP.NET Core 3.x | Host.CreateDefaultBuilder() | 
| .NET 5 | Host.CreateDefaultBuilder() | 
| .NET 6 | WebApplication.CreateBuilder() | 
ConfigureWebHostDefaults() 는 .NET 5 에서 Generic Host 를 지원하면서 추가된 메소드입니다.
.NET 6 의 WebApplication 은 ConfigureWebHostDefaults() 을 래핑한 간단 버전입니다.
ASP.NET Core 프로젝트 생성
dotnet new web -o WeatherForecast
기본 코드 작성
Program.cs
var hostBuilder = Host.CreateDefaultBuilder(args)
    .ConfigureWebHostDefaults(webBuilder =>
        {
            webBuilder.ConfigureServices((ctx, services) =>
            {
                services.AddControllers();
            });
            webBuilder.Configure((ctx, app) => 
            {
                app.UseRouting();
                app.UseEndpoints(endpoints =>
                {
                    endpoints.MapGet("/", () => ctx.HostingEnvironment.ApplicationName);
                    endpoints.MapControllers();
                });
            });
        });
var host = hostBuilder.Build();
host.Run();
WeatherForecast.cs
namespace WeatherForecast;
public class WeatherForecast
{
    public DateTime Date { get; set; }
    public int TemperatureC { get; set; }
    public int TemperatureF => 32 + (int)(TemperatureC / 0.5556);
    public string? Summary { get; set; }
}
Controllers/WeatherForecastController.cs
using Microsoft.AspNetCore.Mvc;
namespace WeatherForecast.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();
    }
}
실행
dotnet run
확인
[mumbi@home ~]$ curl localhost:5061/weatherforecast
[{"date":"2022-11-27T14:03:20.863756+09:00","temperatureC":14,"temperatureF":57,"summary":"Warm"},{"date":"2022-11-28T14:03:20.8637653+09:00","temperatureC":-10,"temperatureF":15,"summary":"Bracing"},{"date":"2022-11-29T14:03:20.8637657+09:00","temperatureC":17,"temperatureF":62,"summary":"Freezing"},{"date":"2022-11-30T14:03:20.863766+09:00","temperatureC":5,"temperatureF":40,"summary":"Bracing"},{"date":"2022-12-01T14:03:20.8637662+09:00","temperatureC":54,"temperatureF":129,"summary":"Scorching"}]
컨트롤러가 잘 동작하는 것을 확인할 수 있습니다.
라이브러리로 모듈 분리
그런데 ConfigureWebHostDefaults() 부분을 다른 애플리케이션에서도 사용하게 되었네요.
코드를 복사해서 사용할 수 있겠지만, 코드의 중복을 피하기 위해 라이브러리로 분리할 수 있습니다.
라이브러리 프로잭트 생성
dotnet new classlib -o Lib
ASP.NET Core 를 사용하기 위해 AspNetCore 프레임워크를 추가합니다.
Lib.csproj
<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <TargetFramework>net6.0</TargetFramework>
    <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>enable</Nullable>
  </PropertyGroup>
  <ItemGroup>
    <FrameworkReference Include="Microsoft.AspNetCore.App" />
  </ItemGroup>
</Project>
모듈 분리
ConfigureWebHostDefaults() 를 확장 메소드를 사용하여 제공합니다.
HostBuilderExtensions.cs
namespace Lib;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
public static class HostBuilderExtensions
{
    public static IHostBuilder ConfigureMyWebHost(this IHostBuilder hostBuilder)
    {
        return hostBuilder.ConfigureWebHostDefaults(webBuilder =>
        {
            webBuilder.ConfigureServices((ctx, services) =>
            {
                services.AddControllers();
            });
            webBuilder.Configure((ctx, app) => 
            {
                app.UseRouting();
                app.UseEndpoints(endpoints =>
                {
                    endpoints.MapGet("/", () => ctx.HostingEnvironment.ApplicationName);
                    endpoints.MapControllers();
                });
            });
        });
    }
}
라이브러리 참조
WeatherForecast 프로젝트에서 Lib 프로젝트를 참조합니다.
dotnet add reference ../Lib/Lib.csproj
라이브러리 함수 사용
WeatherForecast 에서 분리한 모듈의 함수를 사용합니다.
Program.cs
using Lib;
var hostBuilder = Host.CreateDefaultBuilder(args)
    .ConfigureMyWebHost();
var host = hostBuilder.Build();
host.Run();
실행 및 확인
[mumbi@home ~]$ curl -i localhost:5061/weatherforecast
HTTP/1.1 404 Not Found
Content-Length: 0
Date: Sat, 26 Nov 2022 05:44:03 GMT
Server: Kestrel
????
해당 URL 을 찾을 수 없다고 합니다.
즉, WeatherForecastController 가 라우터에 등록되지 않은 것입니다.
원인은 AddControllers() 가 코드가 포함된 어셈블리에 있는 컨트롤러들만 추가하기 때문입니다.
해결책
등록되어야 할 컨틑롤러와 AddControllers() 의 어셈블리가 다르면 컨트롤러를 찾을 수 없기 때문에 찾을 수 있도록 정보를 제공해줘야 합니다.
ApplicationPart
AddApplicationPart() 는 IMvcBuilder 가 컨트롤러를 찾을 때 추가로 찾을 어셈블리를 추가합니다.
Lib 프로젝트의 HostBuilderExtensions.cs
namespace Lib;
using System.Reflection;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
public static class HostBuilderExtensions
{
    public static IHostBuilder ConfigureMyWebHost(this IHostBuilder hostBuilder, Assembly assembly)
    {
        return hostBuilder.ConfigureWebHostDefaults(webBuilder =>
        {
            webBuilder.ConfigureServices((ctx, services) =>
            {
                services.AddControllers()
                    .AddApplicationPart(assembly);
            });
            webBuilder.Configure((ctx, app) => 
            {
                app.UseRouting();
                app.UseEndpoints(endpoints =>
                {
                    endpoints.MapGet("/", () => ctx.HostingEnvironment.ApplicationName);
                    endpoints.MapControllers();
                });
            });
        });
    }
}
WeatherForecast 프로젝트의 Program.cs
using Lib;
using System.Reflection;
var hostBuilder = Host.CreateDefaultBuilder(args)
    .ConfigureMyWebHost(Assembly.GetExecutingAssembly());
var host = hostBuilder.Build();
host.Run();
실행 및 확인
[mumbi@home ~]$ curl -i localhost:5061/weatherforecast
HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
Date: Sat, 26 Nov 2022 05:59:58 GMT
Server: Kestrel
Transfer-Encoding: chunked
[{"date":"2022-11-27T14:59:58.485063+09:00","temperatureC":8,"temperatureF":46,"summary":"Sweltering"},{"date":"2022-11-28T14:59:58.4850736+09:00","temperatureC":26,"temperatureF":78,"summary":"Cool"},{"date":"2022-11-29T14:59:58.4850744+09:00","temperatureC":8,"temperatureF":46,"summary":"Cool"},{"date":"2022-11-30T14:59:58.4850748+09:00","temperatureC":15,"temperatureF":58,"summary":"Bracing"},{"date":"2022-12-01T14:59:58.4850752+09:00","temperatureC":1,"temperatureF":33,"summary":"Freezing"}]
HostEnvironment.ApplicationName
하나의 문제가 더 있습니다.
WeatherForecast 프로젝트의 Program.cs
using System.Reflection;
using Microsoft.Extensions.Logging;
using Lib;
var loggerFactory = LoggerFactory.Create(loggingBuilder => loggingBuilder.AddConsole());
var logger = loggerFactory.CreateLogger<Program>();
var hostBuilder = Host.CreateDefaultBuilder(args)
    .ConfigureMyWebHost(Assembly.GetExecutingAssembly())
    .ConfigureAppConfiguration((hostBuilderContext, configurationBuilder) =>
    {
        logger.LogInformation($"Application Name: {hostBuilderContext.HostingEnvironment.ApplicationName}");
    });
var host = hostBuilder.Build();
host.Run();
이렇게 로그로 ApplicationName 을 확인보겠습니다.
[mumbi@home WeatherForecast]$ dotnet run
info: Program[0]
      Application Name: Lib
ConfigureWebHostDefaults() 가 ApplicationName 을 포함된 어셈블리의 이름으로 설정합니다.
ApplicationName 을 실행 중인 어셈블리 또는 원하는 이름으로 바꾸기 위해서 IWebHostBuilder.UseSetting(webHostDefaults.ApplicationKey, ...) 를 사용합니다.
Lib 프로젝트의 HostBuilderExtensions.cs
namespace Lib;
using System.Reflection;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
public static class HostBuilderExtensions
{
    public static IHostBuilder ConfigureMyWebHost(this IHostBuilder hostBuilder, Assembly assembly)
    {
        return hostBuilder.ConfigureWebHostDefaults(webBuilder =>
        {
            webBuilder.ConfigureServices((ctx, services) =>
            {
                services.AddControllers()
                    .AddApplicationPart(assembly);
            });
            webBuilder.Configure((ctx, app) => 
            {
                app.UseRouting();
                app.UseEndpoints(endpoints =>
                {
                    endpoints.MapGet("/", () => ctx.HostingEnvironment.ApplicationName);
                    endpoints.MapControllers();
                });
            });
            webBuilder.UseSetting(WebHostDefaults.ApplicationKey, assembly.GetName().Name);
        });
    }
}
실행 및 확인
[mumbi@home WeatherForecast]$ dotnet run
info: Program[0]
      Application Name: WeatherForecast
마무리
ASP.NET 이 버전업을 하면서 사용법은 더욱 간단해지고 있지만, 범용성은 점점 줄고 있는 것 같습니다.
라이브러리를 개발하면서 이것 저것 삽질하면서 내부를 알아 가고 있습니다.
같은 문제를 겪으시는 분들께 도움이 되었으면 좋겠습니다.
감사합니다.
댓글남기기