ASP.NET Core - ファイルを扱う
ASP.NET Coreでファイルを扱うサンプルです。
ドキュメントはこのあたり。 docs.microsoft.com
任意の物理ファイルを扱うには、IFileProviderを実装したPhysicalFileProviderを使います。
// ルートフォルダ(=スコープ)を指定してファイルプロバイダを生成 var fileProvider = new PhysicalFileProvider(@"c:\temp"); // IFileInfoを取得して var foundFile = fileProvider.GetFileInfo("test.txt"); // ファイルの存在チェックができたり Debug.WriteLine(foundFile.Exists); // True // ファイルの内容を読み込んだり if (foundFile.Exists) { using var stream = foundFile.CreateReadStream(); using var reader = new StreamReader(stream); var text = await reader.ReadToEndAsync(); }
PhysicalFileProviderはインスタンス生成時に指定したフォルダ(スコープと呼ぶみたい)以外にアクセスできないようになっています。セキュアですね。
var fileProvider = new PhysicalFileProvider(@"c:\temp"); // スコープ外のファイルには存在していてもアクセスできない var notFoundFile = fileProvider.GetFileInfo("c:\test.txt"); Debug.WriteLine(notFoundFile.Exists); // False // 戻り値はNotFoundFileInfoというクラス Debug.WriteLine(notFoundFile.GetType()); // Microsoft.Extensions.FileProviders.NotFoundFileInfo
プロジェクト内のファイルにアクセスするファイルプロバイダは用意されています。
- プロジェクト直下のwwwrootフォルダ - IWebHostEnvironmentのWebRootFileProviderプロパティ
- プロジェクト直下のフォルダ(Startup.csなどがあるフォルダ) - IWebHostEnvironmentのContentRootFileProviderプロパティ
コントローラやビューでファイルを参照する場合はIWebHostEnvironmentをDIで解決する感じですね。
// コントローラでファイルを参照する場合 public class SampleController : Controller { // wwwrootフォルダのファイルプロバイダ private readonly IFileProvider _webRootFileProvider; // プロジェクト直下のフォルダのファイルプロバイダ private readonly IFileProvider _contentRootFileProvider; public SampleController(IWebHostEnvironment env) { _webRootFileProvider = env.WebRootFileProvider; _contentRootFileProvider = env.ContentRootFileProvider; } }
ASP.NET Core MVC - 時間がかかるアクションメソッドをキャンセルできるようにする
ASP.NET Core 2.2からサポートされているインプロセスホスティングモデル。その特徴の1つをDocsから引用します。
クライアントの切断が検出されます。
クライアントが切断されると、HttpContext.RequestAbortedキャンセルトークンが取り消されます。
今回はこの機能を使って時間がかかるアクションメソッドのキャンセルを試していきます。
「クライアントの切断」というのはChromeやFirefoxの×ボタンやEscキーで発生するようです。ウィンドウやタブの×ボタンではなく、通信中に更新ボタンと同じ位置に現れる×ボタンのことを言っています。
Edgeでも同じことを試してみましたが、どうも発生しない様子でした。
確認した各ブラウザのバージョンです。OSはWindows 10です。
ブラウザのウィンドウやタブを閉じたりとか、XMLHttpRequestの通信なども試していませんがどうなんでしょう。 このあたりの仕様はどこかにあるのかな。(調べずに言ってます。)
すべてのブラウザでキャンセルできるものではないという前提で話を進めていきます。
CancellationTokenをバインドする
アクションをキャンセルできるようにするには、アクションメソッドの引数にCancellationTokenを追加します。CancellationTokenModelBinderクラスがHttpContext.RequestAbortedをバインドしてくれるようです。
後はそのCancellationTokenをチェックするだけです。非同期メソッドの引数に渡していくというか。
public class SampleController : Controller { private readonly ILogger _logger; public SampleController(ILogger<SampleController> logger) { _logger = logger; } // キャンセルしたいアクション // CancellationTokenをバインドする public async Task<IActionResult> CancelTest(CancellationToken token) { try { _logger.LogInformation("Heavy task started."); // 何か時間がかかる処理 // バインドしたCancellationTokenを引数に渡す await Task.Delay(TimeSpan.FromSeconds(10), token); _logger.LogInformation("Heavy task completed."); } catch (TaskCanceledException _) { // ブラウザのEscキーでキャンセルされる _logger.LogInformation("Heavy task canceled."); } return new EmptyResult(); } }
Chrome、FirefoxでアクションのURLにアクセスしてみましょう。
そして途中でEscキーを押すと、アクションメソッドの中のTaskがキャンセルされることを確認できます。
// 出力されるログ
Heavy task started.
Heavy task canceled.
参考
ホスティングモデル自体についてはこのあたりが参考になると思います。
ASP.NET Core MVC - Serilogを使ってファイルにログ出力する
ASP.NET Core MVCでSerilogを使ってファイルにログ出力するサンプルを書いてみました。
Serilog — simple .NET logging with fully-structured events
サンプルでは次のことを実現しています。
それではコードを見ていきましょう。
Serilogをインストールする
まずはNuGetで必要なパッケージをインストールします。
Install-Package Serilog Install-Package Serilog.AspNetCore Install-Package Serilog.Formatting.Compact Install-Package Serilog.Settings.Configuration Install-Package Serilog.Sinks.File
パッケージ名から想像できるかもしれませんが、それぞれのパッケージの役割は次のような感じです。
パッケージ | 説明 |
---|---|
Serilog | 本体 |
Serilog.AspNetCore | ASP.NET Core用(WebHostBuilderのUseSerilog) |
Serilog.Formatting.Compact | ログのJSON形式出力 |
Serilog.Settings.Configuration | appsettings.jsonから設定を読み込む |
Serilog.Sinks.File | ログのファイル出力(日付ごとのログファイル作成もこれ) |
今回のサンプルでは使っていませんが以下もよく使いそうですね。
パッケージ | 説明 |
---|---|
Serilog.Sinks.Console | ログのコンソール出力 |
Serilog.Sinks.Debug | ログのデバッグ出力 |
ログ出力の準備をする
一旦appsettings.jsonのことは忘れて、まずはコードでログ出力の準備をしたいと思います。
公式のサンプルなどを見ているとWebHostを構築する前にロガーを準備するといいようで、Programクラスを次のような感じにします。
public class Program { public static void Main(string[] args) { // ロガーを構築する Log.Logger = new LoggerConfiguration() // ファイルに書き込む .WriteTo.File( // JSON形式で出力 formatter: new CompactJsonFormatter(), path: @".\log\webapp.txt", restrictedToMinimumLevel: LogEventLevel.Information, // 日付ごとに新しいファイルを作る rollingInterval: RollingInterval.Day) .CreateLogger(); try { Log.Information("Starting Web Host"); CreateWebHostBuilder(args) .Build() .Run(); } catch (Exception exception) { Log.Fatal(exception, "Host terminated unexpectedly"); } finally { Log.CloseAndFlush(); } } public static IWebHostBuilder CreateWebHostBuilder(string[] args) => WebHost.CreateDefaultBuilder(args) .UseStartup<Startup>() // ログプロバイダーとしてSerilogを使う .UseSerilog(); }
コントローラでログを出力する
コントローラのアクションでログを出力するコードを書き足します。
public class HomeController : Controller { private readonly ILogger _logger; public HomeController(ILogger<HomeController> logger) { _logger = logger; } public IActionResult Index() { // ログを出力 _logger.LogInformation( "Log {@param}", new { controller = "Home", action = "Index" }); return Content("Home.Index"); } }
これでアクションを呼び出すとログが出力されるようになりました。
指定した出力先にwebapp20181205.txt
といった日付を含んだ名前のファイルが作成されます。また出力されるログの中身は整形すると次のようなものです。
{ "@t": "2018-12-05T02:18:12.3417756Z", "@mt": "Log {@param}", "param": { "controller": "Home", "action": "Index" }, "SourceContext": "WebApp.Controllers.HomeController", "ActionId": "", "ActionName": "WebApp.Controllers.HomeController.Index (WebApp)", "RequestId": "", "RequestPath": "", "CorrelationId": null, "ConnectionId": "" }
appsettings.jsonを参照する
今度はappsettings.jsonから設定を読み込んで上記と同じロガーを構築したいと思います。
appsettings.jsonを作成して、公式を参考にしながら設定を書いていきます。formatterを指定する方法に悩みましたが多分これでいいはずです。
{ "Serilog": { "Using": [ "Serilog.Sinks.File", "Serilog.Formatting.Compact" ], "WriteTo": [ { "Name": "File", "Args": { "formatter": "Serilog.Formatting.Compact.CompactJsonFormatter, Serilog.Formatting.Compact", "path": ".\\log\\webapp.txt", "restrictedToMinimumLevel": "Information", "rollingInterval": "Day" } } ] } }
Programクラスのロガーを構築する部分は、設定ファイルを読み込むように変更します。
// appsettings.jsonを読み込む準備 var config = new ConfigurationBuilder() .SetBasePath(Directory.GetCurrentDirectory()) .AddJsonFile("appsettings.json") .Build(); // ロガーを構築する Log.Logger = new LoggerConfiguration() .ReadFrom.Configuration(config) .CreateLogger();
これでアクションを呼び出すとさっきと同じようなログが出力されます。
おしまい。
AutoMapper - ポリモーフィズムを使ったマッピングを試す
マッピング元はポリモーフィズムもいけるよね?と確認したくてサンプルを書いてみました。
いつものごとくモデルとプロファイルを用意します。
// マッピング元の親クラス public abstract class SampleSrcBase { public int Id { get; set; } public string Value1 { get; set; } } // マッピング元 public class SampleSrc : SampleSrcBase { public string Value2 { get; set; } } // マッピング先 public class SampleDst { public int Id { get; set; } public string Value1 { get; set; } public string Value2 { get; set; } } // マッピングのプロファイル public class SampleProfile : Profile { public SampleProfile() { CreateMap<SampleSrc, SampleDst>(); } }
SampleSrcBase(SampleSrc)からSampleDstにマッピングします。
// 準備 Mapper.Initialize(config => { config.AddProfile<SampleProfile>(); }); Mapper.AssertConfigurationIsValid(); // マッピング元のSampleSrcBaseを作る関数 SampleSrcBase getSrc() => new SampleSrc { Id = 1, Value1 = "x", Value2 = "y", }; // 変数srcはSampleSrcBase型(中身はSampleSrc) var src = getSrc(); // マップ var dst = Mapper.Map<SampleDst>(src); // 確認 // Value2もマッピングされている Console.WriteLine(dst.Id); // 1 Console.WriteLine(dst.Value1); // x Console.WriteLine(dst.Value2); // y
ちゃんとマッピングできると。
参考
AutoMapper - コンストラクタを使ってマッピングする
ちょっと発見だったのでメモ。
AutoMapperでは、マッピング先のコンストラクタを呼び出してマッピングすることができます。マッピング先にはpublicなsetterプロパティも必要ありません。
ということでサンプルコード。
モデルとプロファイルを用意して、
// マッピング元 public class SampleSrc { public int Id { get; set; } public string Value { get; set; } } // マッピング先 public class SampleDst { // このコンストラクタを使ってマッピングする public SampleDst(int id, string value) { Console.WriteLine($"{nameof(SampleDst)} constructor({id}, {value})"); // SampleDst constructor(1, x) Id = id; Value = value; } // getterプロパティのみ public int Id { get; } public string Value { get; } } // マッピングのプロファイル public class SampleProfile : Profile { public SampleProfile() { CreateMap<SampleSrc, SampleDst>(); } }
マッピングします。
// 準備 Mapper.Initialize(config => { config.AddProfile<SampleProfile>(); }); Mapper.AssertConfigurationIsValid(); // マッピング元 var src = new SampleSrc { Id = 1, Value = "x", }; // SampleSrc => SampleDst var dst = Mapper.Map<SampleDst>(src); // 確認 Console.WriteLine(dst.Id); // 1 Console.WriteLine(dst.Value); // x