Help us understand the problem. What is going on with this article?

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

More than 3 years have passed since last update.

概要

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

msomini
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away