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