yotiky Tech Blog

とあるエンジニアの備忘録

Azure SendGrid のアカウント

目次

Username と Password

Azure から SendGrid のアカウントを作成すると、Username は自動採番される。 sendgrid.com で Account Details から ~@azure.com を確認することができる。 パスワードはたぶんリソース作成時のもの。(確認できなくなったのでリセット方法探す羽目に)

f:id:yotiky:20210330135806p:plain

もうひとつの Username の確認方法は、Azure CLI を使う方法。

Azure Portal の SendGrid Account のプロパティからリソースIDを確認して、次のコマンドを実行する。

az resource show --ids <your resource id>

properties の中に username が出力される。

f:id:yotiky:20210330151500p:plain

ログイン

Azure Portal から

Azure Portal からは、SendGrid Account の Manage ボタンを押下すると自動でログインする。 SendGrid 側で Sign out してるとたまに失敗するが、もう一度 Manage ボタンを押して開き直すと成功する。

f:id:yotiky:20210330135451p:plain

sendgrid.com から

sendgrid.com からログインする場合、Username は ~@azure.com を使用する。

f:id:yotiky:20210330151823p:plain

パスワードのリセット

sendgrid.com から Sign out をしてログイン画面に遷移する。

f:id:yotiky:20210330151924p:plain

「Forgot your password?」リンクをクリックして、Username に ~@azure.com な値を入力して送信。

f:id:yotiky:20210330152244p:plain

パスワードの変更

sendgrid.com の Account Details からパスワードの変更ができる。 Azure Portal から作成する時は8桁以上を求められるが、こちらは16桁なので要注意。

f:id:yotiky:20210330152525p:plain

Billing と Products

Azure から作成するとどちらも権限がないので見れないっぽい。

f:id:yotiky:20210330152800p:plain

sendgrid.com

アカウントの削除?

Azure から作成した場合はリソースを削除する。(sendgrid.com では権限がない)

ただし、一度削除した後同月中にもう一度 SendGrid Account を作成しようとした所、以下のエラーが出て作成できなくなった。翌月に作成できるかは継続調査中。

  • 状態:InternalServerError
  • 状態メッセージ:課金サービスと通信中に問題が発生しました。診断情報~

f:id:yotiky:20210330154043p:plain

sendgrid.com で作成した場合は、Account Details の Your Products からアカウントをキャンセルできる。キャンセルした場合は月末に停止されるようだ。

f:id:yotiky:20210330153717p:plain
sendgrid.com でのキャンセル画面

Cancellations take full effect on the first day of the next calendar month, at which point your account will still be usable, but will be automatically downgraded to the Free package level.

このページには上記のように書かれており、翌月から Free パッケージレベルにダウングレードされるとなっていてアカウント自体が削除できるのか謎である。。

ASP.NET Core と Azure App Service のアップロードのサイズ制限

目次

検証環境

  • APS.NET Core 5.0 Web アプリ(MVC
  • Azure App Service

ASP.NET Core

APS.NET Core では、.NET Core 2.0 以降、クロスプラットフォーム向け Web サーバーとして Kestrel が提供され、ASP.NET Core のプロジェクトテンプレートに既定で含まれ有効になっている。 .NET Framework の方は従来の IIS をベースにした動きとなる。

ASP.NET Core のテンプレートで生成される Program のうち ConfigureWebHostDefaults メソッドで UseKestrel が呼ばれる。

f:id:yotiky:20210325193650p:plain

ただし、ASP.NET Core であっても実行環境によって Kestrel、IIS のどちらかで動くことになる。

ローカルでデバッグ実行する時はどちらで動かすか選択できるようになっている。 IIS Express の場合はhttps://localhost:44393/、Kestrel の場合はhttps://localhost:5001/ とポートが異なる。

f:id:yotiky:20210325184052p:plain

App Service

ホスト

Visual Studio の 発行で 「Azure App Service(Windows)」を選択した場合や、ポータルで公開が「コード」の場合、ASP.NET Core のプロジェクトは IIS でホストされる。

f:id:yotiky:20210325194223p:plain

f:id:yotiky:20210325194159p:plain

Visual Studio の 発行で 「Azure App Service(Linux)」または「Azure App Service コンテナー」を選択した場合や、ポータルで公開が「Docker」でOSが「Linux」の場合、ASP.NET Core のプロジェクトは Kestrelでホストされる。

f:id:yotiky:20210325194223p:plain

f:id:yotiky:20210325194517p:plain

※Web App for Containers の Windows コンテナは2020年9月に一般提供され、料金プランは「Premium V3」から選択できる。お高いのでスキップ。*1

リクエスト本文のサイズ制限

IIS

IIS でホストされる場合、リクエスト本文のデフォルトの制限は3000万バイト(約28.6MB)となる。 制限を変更するには、web.config を追加して maxAllowedContentLength を設定する。 以下は1億バイト(約95.4MB)になる。

<system.webServer>
  <security>
    <requestFiltering>
      <requestLimits maxAllowedContentLength="100000000" />
    </requestFiltering>
  </security>
</system.webServer>

9500万バイトのファイルをアップロードすると成功してファイルサイズが返ってくる。

f:id:yotiky:20210325195002p:plain
結果
f:id:yotiky:20210325195014p:plain

1億バイトのファイルをアップロードするとエラーメッセージが表示される。

f:id:yotiky:20210325194908p:plain

f:id:yotiky:20210325195707p:plain
The page was not displayed because the request entity is too large.

Kestrel

Kestrel のリクエスト本文も最大サイズは3000万バイト(約28.6MB)がデフォルトである。 Bodyなので添付ファイルのみではなく送信するリクエスト全文のサイズになる。

アプリ全体でグローバルに制限を変更するには、Program 内で MaxRequestBodySize を設定する。 以下は1億バイト(約95.4MB)になる。

public static IHostBuilder CreateHostBuilder(string[] args) =>
    Host.CreateDefaultBuilder(args)
        .ConfigureWebHostDefaults(webBuilder =>
        {
            webBuilder.ConfigureKestrel((context, options) =>
            {
                options.Limits.MaxRequestBodySize = 100000000;
            })
            .UseStartup<Startup>();
        });

9500万バイトのファイルをアップロードすると成功してファイルサイズが返ってくる。

f:id:yotiky:20210325195002p:plain
結果
f:id:yotiky:20210325195014p:plain

1億バイトのファイルをアップロードすると 400 が返ってくる。

f:id:yotiky:20210325194908p:plain
f:id:yotiky:20210325194858p:plain

グローバル設定の他に、コントローラーやアクション単位でも制限を設定できる。どちらも RequestSizeLimitAttribute を付与して設定する。無制限にする場合は DisableRequestSizeLimitAttribute も用意されている。

[AutoValidateAntiforgeryToken]
[RequestSizeLimit(100000000)]
public class FileController : Controller
{
    [HttpPost]
    [RequestSizeLimit(100000000)]
    public IActionResult Upload(IFormFile file)
    {
        return Ok(new { file.Length });
    }
    [HttpPost]
    [DisableRequestSizeLimit]
    public IActionResult UploadUnlimited(IFormFile file)
    {
        return Ok(new { file.Length });
    }
}

マルチパート本文のサイズ制限

リクエスト本文のサイズ制限の他に Form セクションのマルチパート本文にもサイズ制限がある。 こちらは IIS と Kestrel で動作も設定も同じである。 デフォルトの上限は 128MB(134,217,728バイト)となる。

ファイルをアップロードするだけの Form の場合128MBのファイルであれば成功する。

f:id:yotiky:20210325202024p:plain
結果
f:id:yotiky:20210325202032p:plain

1バイト足したファイルだと 400 が返ってくる。 f:id:yotiky:20210325202056p:plain
f:id:yotiky:20210325194858p:plain

アプリ全体でグローバルに制限を変更するには、Startup 内で MultipartBodyLengthLimit を設定する。 以下の例は 256 MB が上限になる。

public void ConfigureServices(IServiceCollection services)
{
    services.AddControllersWithViews();
    services.Configure<FormOptions>(options =>
    {
        options.MultipartBodyLengthLimit = 268435456; // 256 MB
    });
}

先程失敗した「128MB1byte.zip」のアップロードが成功するようになる。

結果
f:id:yotiky:20210325203256p:plain

グローバル設定の他に、コントローラーやアクション単位でも制限を設定できる。どちらも RequestFormLimitsAttributeMultipartBodyLengthLimit を設定して付与する。 以下の例は 256 MB が上限になる。

[AutoValidateAntiforgeryToken]
[RequestFormLimits(MultipartBodyLengthLimit = 268435456)]
public class FileController : Controller
{
    [HttpPost]
    [RequestFormLimits(MultipartBodyLengthLimit = 268435456)]
    public IActionResult Upload(IFormFile file)
    {
        return Ok(new { file.Length });
    }
}

おまけ (Azure Functions)

Azure Functions は リクエスト本文の上限が 100 MB になっている。これは変更できない。 あと httpRuntime 要素じゃなく requestLimits 要素で指定されてる。

HTTP 要求の長さは 100 MB (104,857,600 バイト) に、URL の長さは 4 KB (4,096 バイト) バイトに制限されています。 これらの制限は、ランタイムの Web.config ファイルの httpRuntime 要素で指定されています。

参考

C# - Path を操作する

目次

検証環境

  • .NET Core 5.0

Path を操作する

エスケープと逐語的文字列リテラル

Console.WriteLine("C:\\Program Files (x86)\\Microsoft Visual Studio\\2019\\Professional\\Common7\\IDE");
Console.WriteLine(@"C:\Program Files (x86)\Microsoft Visual Studio\2019\Professional\Common7\IDE");

// 同じ文字列
// C:\Program Files (x86)\Microsoft Visual Studio\2019\Professional\Common7\IDE
// C:\Program Files (x86)\Microsoft Visual Studio\2019\Professional\Common7\IDE

セパレーター

Console.WriteLine($"Path.DirectorySeparatorChar: '{Path.DirectorySeparatorChar}'");
Console.WriteLine($"Path.AltDirectorySeparatorChar: '{Path.AltDirectorySeparatorChar}'");
Console.WriteLine($"Path.PathSeparator: '{Path.PathSeparator}'");
Console.WriteLine($"Path.VolumeSeparatorChar: '{Path.VolumeSeparatorChar}'");

// Windows:
//    Path.DirectorySeparatorChar: '\'
//    Path.AltDirectorySeparatorChar: '/'
//    Path.PathSeparator: ';'
//    Path.VolumeSeparatorChar: ':'
//    Path.GetInvalidPathChars:

// Linux:
//    Path.DirectorySeparatorChar: '/'
//    Path.AltDirectorySeparatorChar: '/'
//    Path.PathSeparator: ':'
//    Path.VolumeSeparatorChar: '/'

ディレクトリとファイル

var vs = @"C:\Program Files (x86)\Microsoft Visual Studio\2019\Professional\Common7\IDE\devenv.exe";
Console.WriteLine($"Path.GetDirectoryname: {Path.GetDirectoryName(vs)}");
Console.WriteLine($"Path.GetExtension: {Path.GetExtension(vs)}");
Console.WriteLine($"Path.GetFileName: {Path.GetFileName(vs)}");
Console.WriteLine($"Path.GetFileNameWithoutExtension: {Path.GetFileNameWithoutExtension(vs)}");
Console.WriteLine($"Path.GetPathRoot: {Path.GetPathRoot(vs)}");

// Path.GetDirectoryname: C:\Program Files (x86)\Microsoft Visual Studio\2019\Professional\Common7\IDE
// Path.GetExtension: .exe
// Path.GetFileName: devenv.exe
// Path.GetFileNameWithoutExtension: devenv
// Path.GetPathRoot: C:\

GetRelativePath .NET Standard 2.1、 .NET Core 2.0 以降で使用可能。

Console.WriteLine($"Path.GetFullPath: {Path.GetFullPath(vs)}");
Console.WriteLine($"Path.GetFullPath: {Path.GetFullPath(@".\..\Tools\spyxx.exe", Path.GetDirectoryName(vs))}");
Console.WriteLine($"Path.GetRelativePath: {Path.GetRelativePath(@"C:\Program Files (x86)\Microsoft Visual Studio\", vs)}");
Console.WriteLine($"Path.ChangeExtension: {Path.ChangeExtension(vs, ".bin")}");

// Path.GetFullPath: C:\Program Files (x86)\Microsoft Visual Studio\2019\Professional\Common7\IDE\devenv.exe
// Path.GetFullPath: C:\Program Files (x86)\Microsoft Visual Studio\2019\Professional\Common7\Tools\spyxx.exe
// Path.GetRelativePath: 2019\Professional\Common7\IDE\devenv.exe
// Path.ChangeExtension: C:\Program Files (x86)\Microsoft Visual Studio\2019\Professional\Common7\IDE\devenv.bin
var download = @"C:\Downloads\";
Console.WriteLine($"Path.EndsInDirectorySeparator: {Path.EndsInDirectorySeparator(download )}");
Console.WriteLine($"Path.TrimEndingDirectorySeparator: {Path.TrimEndingDirectorySeparator(download )}");

// Path.EndsInDirectorySeparator: True
// Path.TrimEndingDirectorySeparator: C:\Downloads

連結と結合

var unity = new[] {"C:", @"Program Files\Unity", @"Hub\Editor\", @"2019.4.3f1/Editor/Unity.exe"};
Console.WriteLine($"Path.Join: {Path.Join(unity)}");
Console.WriteLine($"Path.Combine: {Path.Combine(unity)}");
Console.WriteLine($"Path.GetFullPath(Mix separator): {Path.GetFullPath(Path.Combine(unity))}");
Console.WriteLine($"Path.Join(2root): {Path.Join("C:", @"1\2\3", "D:", @"4\5\6")}");
Console.WriteLine($"Path.Combine(2root): {Path.Combine("C:", @"1\2\3", "D:", @"4\5\6")}");

// Path.Join: C:\Program Files\Unity\Hub\Editor\2019.4.3f1/Editor/Unity.exe
// Path.Combine: C:\Program Files\Unity\Hub\Editor\2019.4.3f1/Editor/Unity.exe
// Path.GetFullPath(Mix separator): C:\Program Files\Unity\Hub\Editor\2019.4.3f1\Editor\Unity.exe
// Path.Join(2root): C:\1\2\3\D:\4\5\6
// Path.Combine(2root): D:\4\5\6

使用できない文字

char convert(char x) => char.IsControl(x) ? (char)(UnicodeRanges.ControlPictures.FirstCodePoint + (int)x) : x;
Console.WriteLine($"Path.GetInvalidPathChars: {string.Join(" ", Path.GetInvalidPathChars().Select(convert))}");
Console.WriteLine($"Path.GetInvalidFileNameChars: {string.Join(" ", Path.GetInvalidFileNameChars().Select(convert))}");

// Path.GetInvalidPathChars: | ␀ ␁ ␂ ␃ ␄ ␅ ␆ ␇ ␈ ␉ ␊ ␋ ␌ ␍ ␎ ␏ ␐ ␑ ␒ ␓ ␔ ␕ ␖ ␗ ␘ ␙ ␚ ␛ ␜ ␝ ␞ ␟
// Path.GetInvalidFileNameChars: " < > | ␀ ␁ ␂ ␃ ␄ ␅ ␆ ␇ ␈ ␉ ␊ ␋ ␌ ␍ ␎ ␏ ␐ ␑ ␒ ␓ ␔ ␕ ␖ ␗ ␘ ␙ ␚ ␛ ␜ ␝ ␞ ␟ : * ? \ /

ファイル名の自動生成

Console.WriteLine($"Path.GetRandomFileName: {Path.GetRandomFileName()}");
// snxcaq4r.ddm, 3gwra5ri.ixv, w4hcste4.gol, ...

絶対パス相対パス

Path クラス

GetRelativePath .NET Standard 2.1、 .NET Core 2.0 以降で使用可能。

// 絶対パス
var vs = @"C:\Program Files (x86)\Microsoft Visual Studio\2019\Professional\Common7\IDE\devenv.exe";
// 相対パスを使って対象の絶対パスを取得
var currentEnv = Path.GetFullPath(@".\..\..\..\..", Path.GetDirectoryName(vs));
// 基準のパスから対象の相対パスを取得
var relative = Path.GetRelativePath(currentEnv, vs);

Console.WriteLine($"FullPath: {vs}");
Console.WriteLine($"CurrentEnv: {currentEnv}");
Console.WriteLine($"Path.GetRelativePath: {relative}");

// FullPath: C:\Program Files (x86)\Microsoft Visual Studio\2019\Professional\Common7\IDE\devenv.exe
// CurrentEnv: C:\Program Files (x86)\Microsoft Visual Studio
// Path.GetRelativePath: 2019\Professional\Common7\IDE\devenv.exe

Uri クラス

Uri クラス

Uri クラスを使用する場合は、基本的にセパレーターが「\」ではなく「/」なのとURLエンコード/デコードされる点に注意する。URLエンコードの対象文字列がパスに含まれている場合は誤変換してしまうの置換処理などが必要。

var uri = new Uri(vs);
Console.WriteLine($"AbsolutePath: {uri.AbsolutePath}");
Console.WriteLine($"AbsoluteUri: {uri.AbsoluteUri}");
Console.WriteLine($"DnsSafeHost: {uri.DnsSafeHost}");
Console.WriteLine($"Fragment: {uri.Fragment}");
Console.WriteLine($"Host: {uri.Host}");
Console.WriteLine($"HostNameType: {uri.HostNameType}");
Console.WriteLine($"IdnHost: {uri.IdnHost}");
Console.WriteLine($"IsAbsoluteUri: {uri.IsAbsoluteUri}");
Console.WriteLine($"IsDefaultPort: {uri.IsDefaultPort}");
Console.WriteLine($"IsFile: {uri.IsFile}");
Console.WriteLine($"IsLoopback: {uri.IsLoopback}");
Console.WriteLine($"IsUnc: {uri.IsUnc}");
Console.WriteLine($"LocalPath: {uri.LocalPath}");
Console.WriteLine($"OriginalString: {uri.OriginalString}");
Console.WriteLine($"PathAndQuery: {uri.PathAndQuery}");
Console.WriteLine($"Port: {uri.Port}");
Console.WriteLine($"Query: {uri.Query}");
Console.WriteLine($"Scheme: {uri.Scheme}");
Console.WriteLine($"Segments: {string.Join(", ", uri.Segments)}");
Console.WriteLine($"UserEscaped: {uri.UserEscaped}");
Console.WriteLine($"UserInfo: {uri.UserInfo}");

// AbsolutePath: C:/Program%20Files%20(x86)/Microsoft%20Visual%20Studio/2019/Professional/Common7/IDE/devenv.exe
// AbsoluteUri: file:///C:/Program%20Files%20(x86)/Microsoft%20Visual%20Studio/2019/Professional/Common7/IDE/devenv.exe
// DnsSafeHost: 
// Fragment: 
// Host: 
// HostNameType: Basic
// IdnHost: 
// IsAbsoluteUri: True
// IsDefaultPort: True
// IsFile: True
// IsLoopback: True
// IsUnc: False
// LocalPath: C:\Program Files (x86)\Microsoft Visual Studio\2019\Professional\Common7\IDE\devenv.exe
// OriginalString: C:\Program Files (x86)\Microsoft Visual Studio\2019\Professional\Common7\IDE\devenv.exe
// PathAndQuery: C:/Program%20Files%20(x86)/Microsoft%20Visual%20Studio/2019/Professional/Common7/IDE/devenv.exe
// Port: -1
// Query: 
// Scheme: file
// Segments: /, C:/, Program%20Files%20(x86)/, Microsoft%20Visual%20Studio/, 2019/, Professional/, Common7/, IDE/, devenv.exe
// UserEscaped: False
// UserInfo: 

f:id:yotiky:20210325140809p:plain

AbsolutePath は「/」で区切られ、URLエンコードされる。
AbsoluteUriAbsolutePath に file スキームが付く。
LocalPathWindows なら「\」で区切られ、URLデコードされる。

絶対パス
Console.WriteLine($"LocalPath: {uri.LocalPath}");

// LocalPath: C:\Program Files (x86)\Microsoft Visual Studio\2019\Professional\Common7\IDE\devenv.exe
相対パスを使って対象の絶対パスを取得
var uri2 = new Uri(uri, @".\..\..\..\..");

Console.WriteLine($"AbsolutePath: {uri.AbsolutePath}");
Console.WriteLine($"AbsolutePath: {uri2.AbsolutePath}");

// AbsolutePath: C:/Program%20Files%20(x86)/Microsoft%20Visual%20Studio/2019/Professional/Common7/IDE/devenv.exe
// AbsolutePath: C:/Program%20Files%20(x86)/Microsoft%20Visual%20Studio/

Console.WriteLine($"LocalPath: {uri.LocalPath}");
Console.WriteLine($"LocalPath: {uri2.LocalPath}");

// LocalPath: C:\Program Files (x86)\Microsoft Visual Studio\2019\Professional\Common7\IDE\devenv.exe
// LocalPath: C:\Program Files (x86)\Microsoft Visual Studio\

Console.WriteLine($"AbsoluteUri: {uri.AbsoluteUri}");
Console.WriteLine($"AbsoluteUri: {uri2.AbsoluteUri}");

// AbsoluteUri: file:///C:/Program%20Files%20(x86)/Microsoft%20Visual%20Studio/2019/Professional/Common7/IDE/devenv.exe
// AbsoluteUri: file:///C:/Program%20Files%20(x86)/Microsoft%20Visual%20Studio/

f:id:yotiky:20210325140830p:plain

基準のパスから対象の相対パスを取得
var relativeUri = uri2.MakeRelativeUri(uri);
Console.WriteLine($"MakeRelativeUri: {relativeUri}");

// MakeRelativeUri: 2019/Professional/Common7/IDE/devenv.exe

f:id:yotiky:20210325140924p:plain

参考

Application Insights に出力された例外を検知する

目次

検証環境

  • Azure Functions v3
  • Application Insights

概要

Azure Monitor はクラウドおよびオンプレミス環境のアプリケーションとサービスのテレメトリを収集、分析し、問題に対応するための包括的なソリューションで、複数の機能を提供している。 Application Insights もアラートも Azure Monitor の一機能となる。

docs.microsoft.com

docs.microsoft.com

今回は、Azure Function の Application Insights で、ログのクエリを元に例外が発生していた場合にメールで通知するアラートを作成する。

設定

Azure Functions で Application Insights を設定した場合、未処理の例外は exceptions にスタックされる。

監視 > ログ から exceptions を検索するクエリを実行すると次のような結果が表示される。

f:id:yotiky:20210316222406p:plain

クエリを条件にルールを作成する場合、タブ内右上の「新しいアラートルール」からアラートのルールを作成する。

条件に警告が出るので内容を修正する。

f:id:yotiky:20210316222823p:plain

直近5分でクエリ結果が1件以上あった場合をトリガーに、5分間隔で実行されるように設定する。

f:id:yotiky:20210316223152p:plain

アクショングループの追加から、アクショングループを作成する。

f:id:yotiky:20210316223608p:plain

f:id:yotiky:20210316223740p:plain

電子メールを送信するように設定。

f:id:yotiky:20210316223753p:plain

Application Insights のログを使って5分間隔で評価する場合、$1.5 / 月(価格ページだと¥168 表記) の料金が発生する。間隔を長くすると少し安くなる。条件を追加すると条件毎に料金が加算される。

f:id:yotiky:20210316223830p:plain

作成したルールは、アラートルールの管理から確認できる。

f:id:yotiky:20210316224831p:plain

通知

クエリの条件に一致した場合、つまり例外が発生していた場合は次のようなメールが届く。 View をクリックすると、Application Insights のログでクエリを実行したページが開く。

f:id:yotiky:20210316225605p:plain

また、アラートのページにはサマリが表示されるようになる。

f:id:yotiky:20210316225801p:plain

参考

Azure Functions で FunctionExceptionFilter を使って例外を処理する

目次

検証環境

  • Azure Functions v3

概要

Azure Functions で使えるフィルターには、FunctionInvocationFilterFunctionExceptionFilter がある。

それぞれ Function のクラス毎にインターフェイスとして実装する IFunctionInvocationFilterIFunctionExceptionFilter、クラスに属性として付与する汎用的な抽象クラス FunctionExceptionFilterAttributeFunctionInvocationFilterAttribute が存在する。

FunctionInvocationFilter は関数の実行前、実行後に処理を追加することができる。

FunctionExceptionFilter は未処理例外が発生した場合の処理を追加することができる。

いずれのインターフェイス、抽象クラスも プレビューという名の Obsolete でマークされており、アップデートにより破壊的な変更が入る可能性があることには注意が必要である。(すでに5年くらいプレビューっぽい)

実装

FunctionExceptionFilter

IFunctionExceptionFilter

インターフェイスを使って関数クラス自体に実装する場合は次の通り。

この場合 DI で Storage などのクライアントのインスタンスを取得していると、ハンドラーメソッド内でそれらを使えるので、例外が起きた時にログ以外にテーブルやキューに情報を登録することができる。

public class ErrorFunction : IFunctionExceptionFilter
{
    private readonly NanikanoClient _client;
    public ErrorFunction(NanikanoClient client)
    {
        _client = client;
    }

    [FunctionName("ErrorFunction")]
    public async Task<IActionResult> Run(
        [HttpTrigger(AuthorizationLevel.Function, "get", "post", Route = null)] HttpRequest req,
        ILogger log)
    {
        log.LogInformation("C# HTTP trigger function processed a request.");
    
        throw new System.Exception("Exception test.");
    
        return new OkObjectResult("OK");
    }

    public Task OnExceptionAsync(FunctionExceptionContext exceptionContext, CancellationToken cancellationToken)
    {
        exceptionContext.Logger.LogError($"OnExceptionAsync called.");

        // Table や Queue などにエラー情報を登録できる
        // _client.Add("エラーが発生したョ");

        return Task.CompletedTask;
    }
}

FunctionExceptionFilterAttribute

FunctionExceptionFilterAttribute は抽象クラスなので継承したクラスを定義する。 こっちは DI ができそうにないのでログ出力が基本になりそう。

public class ErrorHandlerAttribute : FunctionExceptionFilterAttribute
{
    public override Task OnExceptionAsync(FunctionExceptionContext exceptionContext, CancellationToken cancellationToken)
    {
        exceptionContext.Logger.LogError($"ErrorHandler called. {exceptionContext.FunctionName}");

        return Task.CompletedTask;
    }
}

利用する場合は Function のクラスに属性として付ける。

[ErrorHandler]
public class ErrorFunction
{
    // 省略
}

FunctionInvocationFilter

IFunctionInvocationFilter

インターフェイスを使って関数クラス自体に実装する場合は次の通り。

こちらも Exception 同様、Function のメンバ変数にアクセスできるので自由度が高い。

public class ErrorFunction : IFunctionInvocationFilter
{
    private readonly NanikanoClient _client;
    public ErrorFunction(NanikanoClient client)
    {
        _client = client;
    }

    [FunctionName("ErrorFunction")]
    public async Task<IActionResult> Run(
        [HttpTrigger(AuthorizationLevel.Function, "get", "post", Route = null)] HttpRequest req,
        ILogger log)
    {
        log.LogInformation("C# HTTP trigger function processed a request.");

        throw new System.Exception("Exception test.");

        return new OkObjectResult("OK");
    }

    public Task OnExecutingAsync(FunctionExecutingContext executingContext, CancellationToken cancellationToken)
    {
        executingContext.Logger.LogInformation($"OnExecutingAsync called.");
        // _client.Add("実行されたョ");
        return Task.CompletedTask;
    }

    public Task OnExecutedAsync(FunctionExecutedContext executedContext, CancellationToken cancellationToken)
    {
        executedContext.Logger.LogInformation($"OnExecutedAsync called.");
        // _client.Add("Doneしたョ");
        return Task.CompletedTask;
    }
}

FunctionInvocationFilterAttribute

FunctionInvocationFilterAttribute は抽象クラスなので継承したクラスを定義する。 Exception との違いは、Context からサービスを取得できるので DI で登録したクラスのインスタンスなどを使うことができる。そのため、自由度高めで汎用的な処理を行える。

public class AspectFilterAttribute : FunctionInvocationFilterAttribute
{
    public override Task OnExecutingAsync(FunctionExecutingContext executingContext, CancellationToken cancellationToken)
    {
        executingContext.Logger.LogInformation($"AspectFilter.OnExecutingAsync called. {executingContext.FunctionName}");

        var req = executingContext.Arguments["req"] as HttpRequest;
        var svc = req.HttpContext.RequestServices;
        var client = svc.GetService(typeof(NanikanoClient)) as NanikanoClient;
        client.Add("取り出せるョ");

        return base.OnExecutingAsync(executingContext, cancellationToken);
    }
    public override Task OnExecutedAsync(FunctionExecutedContext executedContext, CancellationToken cancellationToken)
    {
        executedContext.Logger.LogInformation($"AspectFilter.OnExecutedAsync called. {executedContext.FunctionName}");

        return base.OnExecutedAsync(executedContext, cancellationToken);
    }
}

利用する場合は Function のクラスに属性として付ける。

[AspectFilter]
public class ErrorFunction
{
    // 省略
}

実行結果

これまでの機能をすべて実装した状態で Function を呼び出した結果は次の通り。

[2021-03-16T10:38:16.851Z] OnExecutingAsync called.
[2021-03-16T10:38:16.852Z] AspectFilter.OnExecutingAsync called. ErrorFunction
[2021-03-16T10:38:16.855Z] C# HTTP trigger function processed a request.
[2021-03-16T10:38:16.871Z] AspectFilter.OnExecutedAsync called. ErrorFunction
[2021-03-16T10:38:16.872Z] OnExecutedAsync called.
[2021-03-16T10:38:16.918Z] OnExceptionAsync called.
[2021-03-16T10:38:16.920Z] ErrorHandler called. ErrorFunction

f:id:yotiky:20210316194021p:plain

実行順序は次のようになる。

  1. 関数の OnExecutingAsync
  2. フィルタの OnExecutingAsync
  3. (例外発生)
  4. フィルタの OnExecutedAsync
  5. 関数の OnExecutedAsync
  6. 関数の OnExceptionAsync
  7. フィルタの OnExceptionAsync

参考

Azure SendGrid を使ってメールを送信する

目次

アカウントの作成

無料枠を使いたい場合は、SendGridのサイトからアカウントを作成する必要がある。 2021年1月時点*1で、Azure Marketplace から作ろうとした場合に無料プランは選択できなくなっている。 どちらから作るかによって選べるプランが違うようなので、比較した上で目的にあったプランを選択する。

また、SendGrid は sendgrid.com を元にリセラーパートナーがあり、それぞれ別の契約になっている様子。 sendgrid.com、構造計画研究所(sendgrid.kke.co.jp)、Azure Marketplace などそれぞれ別のアカウント、契約&サポートになるようなので注意が必要。

support.sendgrid.kke.co.jp

Azure Marketplace で SendGrid のアカウント作成する場合、1つのサブスクリプションにつき1つしか作れない。*2


Azure のサービスから「SendGrid」で検索して、SendGrid Accounts を開いて「作成」からアカウントを作成する。

f:id:yotiky:20210313170101p:plain

Azure 上から作成する場合は、 Bronze が一番下のプラン。 f:id:yotiky:20210313170502p:plain

f:id:yotiky:20210313171136p:plain

こちらの記事を参考にすると、SendGrid の昔の料金体系のまま、上位のプランが増えた感じ。

blog.shibayan.jp

Azure を使わず sendgrid.com からアカウントを作成する場合はこちらも参考に。

yotiky.hatenablog.com

SendGrid のセットアップ

作成したリソースの概要から Manage を選択して、sendgrid.com のページを開く。

f:id:yotiky:20210313171654p:plain

Single Sender (差出人) の登録かドメインの認証が必要になる。

【重要】APIキー認証および二要素認証が必須に変更されます – サポート

今回は Single Sender を選択した。

f:id:yotiky:20210313171929p:plain

f:id:yotiky:20210313172341p:plain

作成したらメールが飛んでくるので verify する。

f:id:yotiky:20210313172644p:plain

Single Sender は、Settings > Sender Authentication で管理される。

f:id:yotiky:20210313172936p:plain

「Verify a Single Sender」で作成したり編集したりできる。

f:id:yotiky:20210313173058p:plain

f:id:yotiky:20210313173316p:plain

API Key の作成

Settings の API Keys からキーを作成することができる。

f:id:yotiky:20210313163025p:plain

f:id:yotiky:20210313173918p:plain

権限は必要に合わせて調整する。送信するには [Mail Send] が必要。

f:id:yotiky:20210313174039p:plain

作成するとキーが表示されるのでコピーしておく。

f:id:yotiky:20210313174128p:plain

実装

NuGet で Sendgrid をインストールする。

Single Sender 認証を行った場合、From のメールアドレスが認証に使用したメールアドレスと一致していないと Forbiddenステータスコードが返ってくるので注意。

シンプルなシナリオでは以下の通り。

var apiKey = "API KEY";
var client = new SendGridClient(apiKey);
var from = new EmailAddress("登録した送信者のメールアドレス@example.com", "fromの名前");
var subject = "SendGridを使ったメール送信";
var to = new EmailAddress("to@example.com", "toの名前");
var plainTextContent = "テキストの内容です。";
var htmlContent = "<strong>HTMLの内容です。</strong>";
var msg = MailHelper.CreateSingleEmail(from, to, subject, plainTextContent, htmlContent);

var response = await client.SendEmailAsync(msg).ConfigureAwait(false);
Console.WriteLine(response.StatusCode.ToString());

複数の宛先に送信する場合は CreateSingleEmailToMultipleRecipients() を使用する。

var tos = new List<EmailAddress>
{
    new EmailAddress("to1@example.comt"),
    new EmailAddress("to2@example.com"),
};

var msg = MailHelper.CreateSingleEmailToMultipleRecipients(from, tos, subject, plainTextContent, htmlContent);

リポジトリには設定ファイルから読み込む、.NET Core のサンプル.NET Frameworkサンプルもあるので参考になる。

github.com

参考

SendGrid を使ってメールを送信する (Not Azure)

目次

アカウントの作成

無料枠を使いたい場合は、SendGridのサイトからアカウントを作成する必要がある。 2021年1月時点*1で、Azure Marketplace から作ろうとした場合に無料プランは選択できなくなっている。 どちらから作るかによって選べるプランが違うようなので、比較した上で目的にあったプランを選択する。

また、SendGrid は sendgrid.com を元にリセラーパートナーがあり、それぞれ別の契約になっている様子。 sendgrid.com、構造計画研究所(sendgrid.kke.co.jp)、Azure Marketplace などそれぞれ別のアカウント、契約&サポートになるようなので注意が必要。

support.sendgrid.kke.co.jp

sendgrid.com での作成手順はこちらで詳しく書かれている。

www.tama-negi.com

SendGrid のセットアップ

アカウント作成後は、二段階認証と Single Sender (差出人) の登録かドメインの認証が必要になる。

二段階認証を組織などで1アカウントを複数名で利用する場合は以下の方法が提示されている。

  1. Teammatesを利用する
  2. Authyの「Multi-Device」機能を利用する

【重要】APIキー認証および二要素認証が必須に変更されます – サポート


今回は Single Sender を選択した。

f:id:yotiky:20210313171910p:plain

f:id:yotiky:20210313172341p:plain

作成したらメールが飛んでくるので verify する。

f:id:yotiky:20210313172644p:plain

Single Sender は、Settings > Sender Authentication で管理される。

f:id:yotiky:20210313172936p:plain

「Verify a Single Sender」で作成したり編集したりできる。

f:id:yotiky:20210313173058p:plain

f:id:yotiky:20210313173316p:plain

API Key の作成

Email API の Integration Guide から Web API を選択。

f:id:yotiky:20210313162628p:plain

続いて言語に C# を選択する。

f:id:yotiky:20210313162755p:plain

Key の名前を入力したら「Create Key」を押すとキーが表示される。 後半にはサンプルコードが提示されているので参考にする。

f:id:yotiky:20210313162833p:plain

作成した API Key は Settings の API Keys で名前や権限の変更、削除などが行える。

f:id:yotiky:20210313163025p:plain

また、ここから Key を作成することもできる。

f:id:yotiky:20210313163145p:plain

実装

NuGet で Sendgrid をインストールする。

Single Sender 認証を行った場合、From のメールアドレスが認証に使用したメールアドレスと一致していないと Forbiddenステータスコードが返ってくるので注意。

シンプルなシナリオでは以下の通り。

var apiKey = "API KEY";
var client = new SendGridClient(apiKey);
var from = new EmailAddress("登録した送信者のメールアドレス@example.com", "fromの名前");
var subject = "SendGridを使ったメール送信";
var to = new EmailAddress("to@example.com", "toの名前");
var plainTextContent = "テキストの内容です。";
var htmlContent = "<strong>HTMLの内容です。</strong>";
var msg = MailHelper.CreateSingleEmail(from, to, subject, plainTextContent, htmlContent);

var response = await client.SendEmailAsync(msg).ConfigureAwait(false);
Console.WriteLine(response.StatusCode.ToString());

複数の宛先に送信する場合は CreateSingleEmailToMultipleRecipients() を使用する。

var tos = new List<EmailAddress>
{
    new EmailAddress("to1@example.comt"),
    new EmailAddress("to2@example.com"),
};

var msg = MailHelper.CreateSingleEmailToMultipleRecipients(from, tos, subject, plainTextContent, htmlContent);

リポジトリには設定ファイルから読み込む、.NET Core のサンプル.NET Frameworkサンプルもあるので参考になる。

github.com

参考