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に認可ハンドラの設定
例：

```C#
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"));
            });
        });
    }
    -------------------------中略----------------------------
}
```

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

```C#
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を継承したクラスを作成します。

```C#
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を作成する必要があります。

```C#
[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を実装したクラスを作成します

```C#
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を設定します

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

##Exceptionフィルターのテスト

```C#
[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で実装しています。