vaguely

和歌山に戻りました。ふらふらと色々なものに手を出す毎日。

【.NET】Console Appを試す

はじめに

これまで ASP.NET Core のアプリは作ってきたものの、よく考えると Console アプリってあまり作ったことないよな~。と思ったので試してみることにしました。

気になるところとしては、 ASP.NET Core のように DI やログ出力などできるのか、というところなので、順に試していくことにします。

Environments

  • .NET Core ver.5.0.100-preview.7.20366.6

元のプロジェクト

とりあえず Console アプリを作ります。

Program.cs

using System;

namespace ConsoleSample
{
    class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine("Hello World!");
        }
    }
}

設定ファイルの読み込み

Database の接続文字列などは設定ファイルを使って読み込みたいところ。

当然ながら StreamReader などを使って通常のファイル読み込みのように JSON ファイルを読み込むことはできるのですが、 ASP.NET Core のようにもう少しシンプルな方法はないでしょうか。

Startup.cs

...
    private readonly IConfiguration configuration;
    public Startup(IHostEnvironment env)
    {
        var builder = new ConfigurationBuilder()
            .SetBasePath(env.ContentRootPath)
            .AddJsonFile("appsettings.json", false, true)
            .AddJsonFile($"appsettings.{env.EnvironmentName}.json", false, true)
            .AddEnvironmentVariables();
        configuration = builder.Build();
    }
...

と思っていたら、 ConfigurationBuilder がそのまま使えるそうです。

Install

Samples

NuGet でインストールができたら、読み込むための JSON ファイルと、読み込む機能を追加します。

appsettings.json

{
    "ConnectionStrings": "Host=localhost;Port=5432;Database=Example;Username=postgres;Password=example"
}

appsettings.Development.json

{
    "Message": "Hello Development"
}

appsettings.Production.json

{
    "Message": "Hello Production"
}

Program.cs

using System;
using Microsoft.Extensions.Configuration;

namespace ConsoleSample
{
    class Program
    {
        static void Main(string[] args)
        {
            var config = GetConfiguration();
            Console.WriteLine(config["ConnectionStrings"]);
            Console.WriteLine("Hello World!");
        }
        private static IConfiguration GetConfiguration()
        {
            var builder = new ConfigurationBuilder()
            .SetBasePath(AppDomain.CurrentDomain.BaseDirectory)
            .AddJsonFile("appsettings.json", false, true)
            .AddEnvironmentVariables();
            return builder.Build();
        }
    }
}

なお Base Path に「System.IO.Directory.GetCurrentDirectory()」を設定することもできます。

EnvironmentName を取得する

「appsettings.json」については読み込みができましたが、デバッグ時には「appsettings.Development.json」を、リリースビルドでは「appsettings.Production.json」を読み込みたい。

ASP.NET Core であれば、 IHostEnvironment の EnvironmentName から受け取ることができます。
この値はどうやら Visual Studio などで登録しておけば同じようなことができるようです。

var environmentName = Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT");

が、プロジェクト作成のたびに登録するのはなぁ。。。(もっとシンプルな方法があるのかもしれませんが)

ということで、 #if ディレクティブを使うことにしました。

        private static IConfiguration GetConfiguration()
        {
            var builder = new ConfigurationBuilder()
                .SetBasePath(AppDomain.CurrentDomain.BaseDirectory)
                .AddJsonFile("appsettings.json", false, true)
#if DEBUG
                .AddJsonFile($"appsettings.Development.json", false, true)
#else
                .AddJsonFile($"appsettings.Production.json", false, true)
#endif
                .AddEnvironmentVariables();
            return builder.Build();
        }

正直いまいち感がないではないのですが、とにかく環境別で切り替えることができました。

DI(Dependency Injection)

DI も ASP.NET Core と同じものが使えます。

Install

Samples

IProductService.cs

namespace Products
{
    public interface IProductService
    {       
    }
}

ProductService.cs

namespace Products
{
    public class ProductService: IProductService
    {       
    }
}

MainController.cs

using System;
using System.Threading.Tasks;
using Products;

namespace Controllers
{
    public class MainController
    {
        private readonly IProductService _product;
        public MainController(IProductService product)
        {
            _product = product;
        }
        public async Task StartAsync()
        {
            await Task.Run(() => Console.WriteLine(_product == null));
        }
    }
}

Program.cs

using System;
using System.Threading.Tasks;
using Controllers;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Products;

namespace ConsoleSample
{
    class Program
    {
        static async Task Main(string[] args)
        {
...
            var servicesProvider = BuildDi();
            using (servicesProvider as IDisposable)
            {
                var mainController = servicesProvider.GetRequiredService<MainController>();
                await mainController.StartAsync();
            }
        }
...
        private static IServiceProvider BuildDi()
        {
            var services = new ServiceCollection();
            services.AddTransient<MainController>();
            services.AddScoped<IProductService, ProductService>();
            return services.BuildServiceProvider();
        }
    }
}

IConfiguration のインジェクション

ASP.NET Core であれば、Controller クラスからでも Configuration の値を参照できます。

これは Microsoft.AspNetCore.Mvc.Controller を継承しているからだと思いますが、 Console アプリの場合は自分で IServiceProvider に渡す必要があります。

Program.cs

...
        private static IServiceProvider BuildDi()
        {
            var services = new ServiceCollection();

            services.AddSingleton<IConfiguration>(GetConfiguration());
            
            services.AddTransient<MainController>();
            services.AddScoped<IProductService, ProductService>();
            return services.BuildServiceProvider();
        }
...

ということで Configuration の値を参照できるようになりました。

MainController.cs

using System;
using System.Threading.Tasks;
using Microsoft.Extensions.Configuration;
using Products;

namespace Controllers
{
    public class MainController
    {
        private readonly IProductService _product;
        public MainController(IConfiguration config,
            IProductService product)
        {

            Console.WriteLine(config["Message"]); // <- print "Hello Development"
            _product = product;
        }

Entity Framework Core を使う

DI が同じなので当然といえば当然ですが、 Entity Framework Core も ASP.NET Core と同じように使うことができます。

Install

ConsoleSampleContext.cs

using Microsoft.EntityFrameworkCore;

namespace Models
{
    public class ConsoleSampleContext: DbContext
    {
        public ConsoleSampleContext(DbContextOptions<ConsoleSampleContext> options)
            :base(options)
        {
        }
    }
}

Program.cs

...
        private static IServiceProvider BuildDi()
        {
            var config = GetConfiguration();
            var services = new ServiceCollection();
            services.AddSingleton<IConfiguration>(config);
            
            services.AddDbContext<ConsoleSampleContext>(options =>
                options.UseNpgsql(config["ConnectionStrings"]));
            
            services.AddTransient<MainController>();
            services.AddScoped<IProductService, ProductService>();
            return services.BuildServiceProvider();
        }
...

ログ出力(NLog)

NLog を使ってログ出力します。

Install

  • NLog ver.4.7.3
  • NLog.Extensions.Logging ver.1.6.4

Samples

nlog.config

<?xml version="1.0" encoding="utf-8" ?>
<nlog xmlns="http://www.nlog-project.org/schemas/NLog.xsd"
      xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
      autoReload="true">

    <targets>
        <target xsi:type="Console" name="outputconsole"
            layout="${longdate}|${event-properties:item=EventId_Id}|${uppercase:${level}}|${logger}|${message} ${exception:format=tostring}" />

        <target xsi:type="File" name="outputfile" fileName="C:\tmp\logs\ConsoleSample\${date:format=yyyy}\${date:format=MM}\${shortdate}.log"
            layout="${longdate}|${event-properties:item=EventId_Id}|${uppercase:${level}}|${logger}|${message} ${exception:format=tostring}|url: ${aspnet-request-url}|action: ${aspnet-mvc-action}" />
    </targets>

    <rules>
        <logger name="*" minlevel="Debug" writeTo="outputconsole" />
        <!--Microsoft.* のクラスの Info レベル以下のログはスキップ-->
        <logger name="Microsoft.*" maxLevel="Info" final="true" />
        <logger name="*" minlevel="Debug" writeTo="outputfile" />
    </rules>
</nlog>

ConsoleSample.csproj

...
  <ItemGroup>
    <Content Include="nlog.config">
      <CopyToOutputDirectory>Always</CopyToOutputDirectory>
    </Content>
    <Content Include="appsettings.json">
      <CopyToOutputDirectory>Always</CopyToOutputDirectory>
    </Content>
    <Content Include="appsettings.Development.json">
      <CopyToOutputDirectory>Always</CopyToOutputDirectory>
    </Content>
    <Content Include="appsettings.Production.json">
      <CopyToOutputDirectory>Always</CopyToOutputDirectory>
    </Content>
  </ItemGroup>
</Project>

NLog ver.5.0.0-beta11

ほぼドキュメント通りなのですが、一点注意が。

NLog ver.5.0.0-beta11 で試したところ、依存関係の解決に失敗してエラーになりました。

Attempt by method 'NLog.Extensions.Logging.ConfigureExtensions.CreateNLogLoggerProvider(System.IServiceProvider, Microsoft.Extensions.Configuration.IConfiguration, NLog.Extensions.Logging.NLogProviderOptions, NLog.LogFactory)' to access method 'NLog.LogManager.get_LogFactory()' failed.

おそらく ver.5.0.0 で何か変わったのかなぁ、と思っているのですが、少なくとも今(2020-08-09)、上記のコードを試す場合は ver.4.7.3 を使うのがよいと思います。

おわりに

ASP.NET Core は色々特別に作られた機能があるために DI などが実現できているのだろうな~と思っていたのですが、結構 Console アプリでもそのまま使えるのだとわかったのが今回の収穫です。

Angular などもそうだと思うのですが、「複雑でよくわからない一つの塊」ではなく、一つ一つの小さな機能(それでも DI などは相当大きいとは思いますが)が組み合わさって作られているのだ、というまぁ当たり前といえば当たり前のことに気づいたというか。

今後それぞれもう少し突っ込んでみていきたいところ。