3 분 소요

개요

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 6WebApplicationConfigureWebHostDefaults() 을 래핑한 간단 버전입니다.

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 이 버전업을 하면서 사용법은 더욱 간단해지고 있지만, 범용성은 점점 줄고 있는 것 같습니다.

라이브러리를 개발하면서 이것 저것 삽질하면서 내부를 알아 가고 있습니다.

같은 문제를 겪으시는 분들께 도움이 되었으면 좋겠습니다.

감사합니다.

댓글남기기