C#
.NETFramework
ServiceFabric
ASP.NET_Core

Azure ServiceFabric - Asp.Net WebApiをCore化した時のメモ

概要

ServiceFabricのWeb系プロジェクトのテンプレートがCoreのみになったので、ServiceFabricのAsp.Net WebApiのプロジェクトをCore化してみた。
ServiceFabric向けなので、.NetFramework上で動作するAsp.Net Coreです。

環境

ServiceFabric - Stateless Service
.NetFramework 4.5.2
Asp.Net Core 1.1.2
Asp.Net Mvc Core 1.1.3
MsTest(Unit Test用)
Mock(Unit Test用)

web hostはWebListner(試してないですが、kestrelでもたぶん動きます。)

変わったところ

主にフィルタ回り。他はほぼそのまま移行できました。

認可(Policyベース)

認可フィルタの実装

① IAuthorizationRequirementを実装した条件設定用のクラスを作成します。
例:Permissionを設定できるRequirementを作成

public class PermissionRequirement : IAuthorizationRequirement
{
    public string Permission { get; set; }

    public PermissionRequirement(string permission)
    {
        Permission = permission;
    }
}

② 型パラメータに先ほどのPermissionRequirementを指定したAuthorizationHandlerを継承したエラーハンドラを作成しHandleRequirementAsyncメソッドを実装します。
例:HttpContextのHeaderから"x-access-token"を取得しIsValid()メソッドで認可する想定です。(IsValid()の実装例は省略)

public class ApiAuthorizationHandler : AuthorizationHandler<ApiCodeRequirement>
{
    protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, PermissionRequirement requirement)
    {
        if (context.Resource is Microsoft.AspNetCore.Mvc.Filters.AuthorizationFilterContext mvcContext)
        {
            string accessToken = mvcContext.HttpContext.Headers["x-access-token"].FirstOrDefault();
            if(IsValid(requirement.Permission, accessToken))
            {
                context.Succeed(requirement);
            }
        }
        Task.FromResult(0);
    }
}

context.Succeedメソッドを実行すると認可されます。
context.Succeedメソッドを実行しないと後述するエラーハンドラにInvalidOperationExceptionが渡されます。
認可エラー時にInvalidOerationException以外のExceptionを投げたい時は認可エラー時に任意のExceptionを投げることで
エラーハンドラ側で取得することができます。
また、Service Fabric以外のアプリケーションでIISにバインドしているときはHandleRequirementAsync内で認可エラー用の
レスポンスを作成すればIIS側でよしなにやってくれます。IISにバインドしない場合はエラーとなるので、
エラーハンドラを実装し認可エラー用のレスポンスを作成する必要があります。

③ Startupに認可ハンドラの設定
例:

public class Startup
{
    -------------------------中略----------------------------
    public void ConfigureServices(IServiceCollection services)
    {
        services.AddMvc();
        services.AddSingleton<IAuthorizationHandler, ApiAuthorizationHandler>().AddAuthorization(options =>
        {
            options.AddPolicy("Admin", policy =>
            {
                policy.Requirements.Add(new ApiCodeRequirement("Admin"));
            });
        });
    }
    -------------------------中略----------------------------
}

④ エラー時のレスポンスを返すエラーハンドラの設定
例:

public class Startup
{
    -------------------------中略----------------------------
    public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
    {
        app.UseExceptionHandler(appBuilder =>
        {
            appBuilder.Use(async (context, next) =>
            {
                var error = context.Features[typeof(IExceptionHandlerFeature)] as IExceptionHandlerFeature;
                if(error?.Exception as InvalidOperationException)
                {
                    var result =
                            Encoding.UTF8.GetBytes(
                                JsonConvert.SerializeObject(new {ID = "99999", Messages = "認可エラー"}));
                    context.Response.ContentType = "application/json; charset=utf-8";
                    await context.Response.Body.WriteAsync(result, 0, result.Length);
                }
                else
                {
                    await next();
                }
            });
        });
        app.UseMvc();
    }
    -------------------------中略----------------------------
}

app.UseMvcより後ろにapp.UseExceptionHandlerを実装するとエラーがハンドルできないので注意
認可処理の時に独自のExceptionを投げてる場合はここで、そのExceptionに関する処理を追加してください。

アクションフィルタ

.Net Coreのアクションフィルタは抽象クラス/インタフェース、同期/非同期、DI等用途に応じてさまざまパターンがあります。
今回は抽象クラスを使って実装します。

アクションフィルタの実装

① ActionFilterAttributeを継承したクラスを作成します。

public sealed class MyActionFilterAttribute : ActionFilterAttribute
{
    public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next)
    {
        string token = context.HttpContext.Request.Headers["x-access-token"].FirstOrDefault();
            .FirstOrDefault();

        // アクションが実行される前の処理
        Debug.WriteLine($"Start Action: Token = {token}");
        await next();
        // アクションが実行された後の処理
        Debug.WriteLine("End Action: Token = {token}");
    }
}

await next()の前
  アクション実行前の処理
await next()の後
  アクション実行後の処理

アクションフィルタのテスト

アクションフィルタのテストをするにはActionExecutionContextとActionExecutionDelegateを作成する必要があります。

[TestClass]
public class MyActionFilterAttributeTests
{
    public await Task OnActionExecutionAsync_正常系()
    {
        // Setup
        Mock<HttpContext> httpContextMock = new Mock<HttpContext>();
        httpContextMock.Setup(m => m.Request.Headers["x-access-token"])
                .Returns(new StringValues("aaaaaaaaaaa"));
        ActionContext actionContext = ActionContext(httpContextMock.Object, new RouteData(), new ActionDescriptor());
        ActionExecutingContext actionExecutingContext = new ActionExecutingContext(actionContext, new IFilterMetadata[] { }, new Dictionary<string, object>(), controller: new object());
        ActionExecutedContext actionExecutedContext = new ActionExecutedContext(actionExecutingContext, context.Filters, context.Controller)
        {
            Result = context.Result,
        };
        ActionExecutionDelegate next = new ActionExecutionDelegate(() => Task.FromResult(actionExecutedContext));
        MyActionFilterAttribute target = new MyActionFilterAttribute();

        // Act
        await target.OnActionExecutionAsync(actionExecutingContext, next);

        // Assert
        -------------------------中略----------------------------
    }
}

Exceptionフィルター

エラーフィルタもアクションフィルタ同様さまざまなパターンがあります。
今回はインタフェースを使ってグローバルフィルタとして設定します。
また、上記の認可NGの際のExceptionはこちらでは扱えませんので注意が必要です。

Exceptionフィルターの実装

① IExceptionFilterを実装したクラスを作成します

public class WebApiExceptionFilter : IExceptionFilter
{
    public void OnException(ExceptionContext context)
    {
        Exception exception = context.Exception;
        int statusCode = (int)HttpStatusCode.InternalServerError;

        HogeResult result = new HogeResult
        {
            Code = "99999",
            Message = "想定できないエラー"
        };
        if(exception as HogeException)
        {
            HogeException hogeException = exeption as HogeException;
            statusCode = (int)HttpStatusCode.InternalServerError;
            result.Code = hogeException.Code;
            result.Message = hogeException.Message;
        }

            context.Result = new JsonResult(result)
            {
                StatusCode = (int)statusCode
            };
    }
}

public class HogeResult
{
    public Code { get; set; }
    public Message { get; set; }
    public override string ToString()
    {
        return JsonConvert.SerializeObject(this);
    }
}

② グローバルフィルタに作成したExceptionFilterを設定します

public class Startup
{
    -------------------------中略----------------------------
    public void ConfigureServices(IServiceCollection services)
    {
        services.AddMvc();
        services.AddMvcCore().AddMvcOptions(options =>
        {
            options.Filters.Add(new WebApiExceptionFilter());
        });
    }
    -------------------------中略----------------------------
}

Exceptionフィルターのテスト

[TestClass]
public class WebApiExceptionFilterTests
{
    public void OnException_アプリケーションエラー()
    {
        // Setup
        Mock<HttpContext> httpContextMock = new Mock<HttpContext>();
        httpContextMock.Setup(m => m.Request.Headers["x-access-token"])
                .Returns(new StringValues("aaaaaaaaa-bbbbbbbbb-cccccc-dddddd"));
        HogeException exception = new HogeException();
        ActionContext actionContext = new ActionContext(httpContextMock.Object, new RouteData(), new ActionDescriptor());
        ExceptionContext context =
                new ExceptionContext(actionContextnew List<IFilterMetadata>()) { Exception = exception };
        WebApiExceptionFilter target = new WebApiExceptionFilter();

        // Act
        target.OnExecute(context);
        JsonResult result = context.Result as JsonResult;
        HogeResult hoge = resultJson.Value as HogeResult;

        // Assert
        Assert.AreEqual((int)HttpStatusCode.InternalServerError, result.StatusCode);
        Assert.AreEqual("11111", hoge.Code);
        Assert.AreEqual("アプリケーションエラー", hoge.Message);
    }
}

OnExecuteの引数に渡すExceptionContextのオブジェクトを作成します。
ExceptionContextのオブジェクトを作成する際にHttpContextのMockを作成する必要があります。
テスト対象の処理でHttpContextを利用する場合はSetupでMockの設定をします。

その他

.NetFrameworkで動くAsp.Net Coreなので、それほどインパクトはないように思えます。
エラーについてExceptionHandlerにすべてのパターンを実装すればExceptionフィルターの必要はありません。
なんとなくですが(まだ結論が見えてません)私が実装したアプリケーションでは今までIIS側でやってたであろうエラー処理はapp.UseExceptionHandlerで、.Net Mvc側でやってたであろうエラー処理はExceptionFilterで実装しています。