はじめに
これまで 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
- Microsoft.Extensions.Configuration ver.5.0.0-preview.7.20364.11
- Microsoft.Extensions.Configuration.FileExtensions ver.5.0.0-preview.7.20364.11
- Microsoft.Extensions.Hosting ver.5.0.0-preview.7.20364.11
- Microsoft.Extensions.Configuration.Json ver.5.0.0-preview.7.20364.11
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(); } } }
- ConfigurationBuilder クラス (Microsoft.Extensions.Configuration) | Microsoft Docs
- FileConfigurationExtensions.SetBasePath(IConfigurationBuilder, String) メソッド (Microsoft.Extensions.Configuration) | Microsoft Docs
なお 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
- Microsoft.EntityFrameworkCore ver.5.0.0-preview.7.20365.15
- Microsoft.EntityFrameworkCore.Relational ver.5.0.0-preview.7.20365.15
- Microsoft.EntityFrameworkCore.Abstractions ver.5.0.0-preview.7.20365.15
- Npgsql.EntityFrameworkCore.PostgreSQL ver.5.0.0-preview7-ci.20200722t163648
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 などは相当大きいとは思いますが)が組み合わさって作られているのだ、というまぁ当たり前といえば当たり前のことに気づいたというか。
今後それぞれもう少し突っ込んでみていきたいところ。