C#
.NETFramework
ServiceFabric
ASP.NET_Core

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

More than 1 year has 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で実装しています。