ASP.NET MVCの開発がはじめての人向けのアドバイス
最近、職場でASP.NET MVCを初めて触りながら開発してる人に対し、その人の作ったコードを見ながらコードレビューというより、ASP.NET MVCではこういうことが出来ます。とアドバイスをする機会があったので、その内容を簡単にまとめてみました。
1、アプリケーション設定は環境ごとに分ける
検証環境と本番環境でキーは同じだけど、値は違うものを使いたい場合、以下の内容を追加して環境ごとに値を上書きします。
xdt:Transform="Replace" xdt:Locator="Match(key)"
【Web.config】環境ごとに異なる値の設定(見本)
Web.config (例) 開発環境)
<appSettings> <add key="env" value="Local"/> </appSettings>
Web.Debug.config (例) 検証環境)
<appSettings> <add key="env" value="Test" xdt:Transform="Replace" xdt:Locator="Match(key)" /> </appSettings>
Web.Release.config (例) 本番環境)
<appSettings> <add key="env" value="Live" xdt:Transform="Replace" xdt:Locator="Match(key)" /> </appSettings>
2、Log4netの設定
Log4netの設定はWeb.configのConfiguration(Debug/Release)ごとに設定を分けます。
Properties\AssemblyInfo.cs
[assembly: AssemblyVersion("1.0.0.0")] [assembly: AssemblyFileVersion("1.0.0.0")] [assembly: log4net.Config.XmlConfigurator(Watch = true)]★追加
Web.config (例) 開発環境)
<log4net debug="true"> <appender name="RollingLogFileAppender" type="log4net.Appender.RollingFileAppender"> <param name="File" value="C:\logs\" /> <param name="AppendToFile" value="true" /> <param name="MaxSizeRollBackups" value="10" /> <param name="RollingStyle" value="date" /> <param name="StaticLogFileName" value="false" /> <param name="DatePattern" value='yyyy-MM-dd".log"' /> <lockingModel type="log4net.Appender.FileAppender+MinimalLock" /> <layout type="log4net.Layout.PatternLayout"> <param name="ConversionPattern" value="%d [%t] %-5p - %m%n" /> </layout> </appender> <root> <level value="DEBUG" /> <appender-ref ref="RollingLogFileAppender" /> </root> </log4net>
Web.Debug.config/Web.Release.config ※環境に合わせて設定する
<log4net debug="true" xdt:Transform="Replace"> <appender name="RollingLogFileAppender" type="log4net.Appender.RollingFileAppender"> <param name="File" value="C:\logs\Test\" /> <param name="AppendToFile" value="true" /> <param name="MaxSizeRollBackups" value="10" /> <param name="RollingStyle" value="date" /> <param name="StaticLogFileName" value="false" /> <param name="DatePattern" value='yyyy-MM-dd".log"' /> <lockingModel type="log4net.Appender.FileAppender+MinimalLock" /> <layout type="log4net.Layout.PatternLayout"> <param name="ConversionPattern" value="%d [%t] %-5p - %m%n" /> </layout> </appender> <root> <level value="DEBUG" /> <appender-ref ref="RollingLogFileAppender" /> </root> </log4net>
3、Action呼出前後の共通ロジックと共通認証
MVCではActionFilterAttributeを継承したカスタムAttributeの中でActionが呼ばれる前後のタイミングで共通処理を行うことができます。
3-1、Action呼出前後の共通ロジックの参考
/Filters/KariiInitAttribute.cs (仮)
using System; using System.Collections.Generic; using System.Linq; using System.Web; using System.Web.Mvc; namespace Pj.Kari.Filters { // カスタム アクション フィルターの作成 // https://msdn.microsoft.com/ja-jp/library/dd381609(v=vs.98).aspx public class KariInitAttribute : ActionFilterAttribute { public override void OnActionExecuting(ActionExecutingContext filterContext) { } public override void OnActionExecuted(ActionExecutedContext filterContext) { base.OnActionExecuted(filterContext); } } }
KariInitAttribute適用例
※Controller/Action単位で適用されます。
[KariInit] public class HonyararaController : BaseController
もし特定のController/Actionのときはフィルターの適用をしたくない場合(Controller単位でカスタムAttributeを適用させた場合など)、下記の方法で現在のController/Actionを取得し、対象外にすることも出来ます。
var currentController = httpContext.Request.RequestContext.RouteData.Values["controller"].ToString(); var currentAction = httpContext.Request.RequestContext.RouteData.Values["action"].ToString();
3-2、共通認証の参考
Actionが呼ばれる前に認証をさせる場合(ログインユーザーか、権限を持っているかなどのチェック)はAuthorizeAttributeを継承したカスタムAttributeの使用がおすすめです。適用例は「3-1、Action呼出前後の共通ロジックの参考」と同じです。
参考ページ
MyAuthorizeAttribute
public class MyAuthorizeAttribute: AuthorizeAttribute { protected override bool AuthorizeCore(HttpContextBase httpContext) { var authorized = base.AuthorizeCore(httpContext); if (!authorized) { // The user is not authorized => no need to go any further return false; } // We have an authenticated user, let's get his username string authenticatedUser = httpContext.User.Identity.Name; // and check if he has completed his profile if (!this.IsProfileCompleted(authenticatedUser)) { // we store some key into the current HttpContext so that // the HandleUnauthorizedRequest method would know whether it // should redirect to the Login or CompleteProfile page httpContext.Items["redirectToCompleteProfile"] = true; return false; } return true; } protected override void HandleUnauthorizedRequest(AuthorizationContext filterContext) { if (filterContext.HttpContext.Items.Contains("redirectToCompleteProfile")) { var routeValues = new RouteValueDictionary(new { controller = "someController", action = "someAction", }); filterContext.Result = new RedirectToRouteResult(routeValues); } else { base.HandleUnauthorizedRequest(filterContext); } } private bool IsProfileCompleted(string user) { // You know what to do here => go hit your database to verify if the // current user has already completed his profile by checking // the corresponding field throw new NotImplementedException(); } }
4、例外の集約
ASP.NET MVCでは例外を集約して処理可能です。また、ユーザーの動向をその都度ログに出力することも出来ます。
参考ページ
ASP.NET MVCの集約例外処理
https://qiita.com/mocha/items/6928870b2d02d4c1ac37
サンプル
Global.asax.cs
protected void Application_Start() { AreaRegistration.RegisterAllAreas(); FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters);//activeにする★ RouteConfig.RegisterRoutes(RouteTable.Routes); } // Application_Errorを使っている場合はコメントアウトにする //protected void Application_Error(object sender, EventArgs e) //{ // var exception = Server.GetLastError(); // if (exception == null) // { // return; // } // Log.Err(exception.Message, exception); //}
App_Start/FilterConfig.cs
public static void RegisterGlobalFilters(GlobalFilterCollection filters) { //filters.Add(new HandleErrorAttribute());//MVC Defaultはコメントアウト filters.Add(new LogAttribute()); //ユーザーの動向をログ出力 filters.Add(new ExceptionHandleAttribute()); //集約例外処理 }
Filters/ExceptionHandleAttribute.cs ※新規追加
using System; using System.Collections.Generic; using System.Linq; using System.Web; using System.Web.Mvc; using System.Web.Routing; namespace Pj.Kari.Filters { public class ExceptionHandleAttribute : HandleErrorAttribute { readonly log4net.ILog logger = log4net.LogManager.GetLogger(System.Reflection.MethodBase.GetCurrentMethod().DeclaringType); public override void OnException(ExceptionContext exceptionContext) { var controllerName = exceptionContext.RouteData.Values["controller"].ToString(); var actionName = exceptionContext.RouteData.Values["action"].ToString(); var exceptionMsg = exceptionContext.Exception.Message; var exMessage = string.Format("{0}", exceptionMsg); // リファレンスエラー if (exceptionContext.Exception.GetType() == typeof(NullReferenceException)) { // } // 認証エラー if (exceptionContext.Exception.GetType() == typeof(HttpAntiForgeryException)) { exceptionContext.ExceptionHandled = true; exceptionContext.Result = new RedirectToRouteResult( new RouteValueDictionary( new { controller = "Home", action = "Index" }) ); return; } if (exceptionContext.ExceptionHandled) { return; } // Ajaxリクエストエラー if (exceptionContext.HttpContext.Request.IsAjaxRequest()) { var text = string.Empty; text = HttpUtility.JavaScriptStringEncode(exceptionMsg); exceptionContext.Result = new JavaScriptResult() { // jsを返すことが可能 //Script = "alert('エラーが発生しました。');" }; } else { exceptionContext.Result = new ViewResult() { ViewName = "../Error/Error", // ★表示したい共通のエラー画面 ViewData = new ViewDataDictionary { Model = new HandleErrorInfo(exceptionContext.Exception, controllerName, actionName) } }; } var errorMsgList = new List<string>(); var innerExection = exceptionContext.Exception; while (innerExection != null) { errorMsgList.Add(innerExection.Message); innerExection = innerExection.InnerException; } logger.Fatal(string.Format("{1}{0}{2}", Environment.NewLine, exMessage, string.Join(Environment.NewLine, errorMsgList)), exceptionContext.Exception); exceptionContext.ExceptionHandled = true; exceptionContext.HttpContext.Response.StatusCode = 500; HttpContext.Current.Response.TrySkipIisCustomErrors = true; } } }
Filters/LogAttribute.cs ※新規追加
using log4net; using System; using System.Collections.Generic; using System.Diagnostics; using System.Linq; using System.Security.Claims; using System.Web; using System.Web.Mvc; namespace Pj.Kari.Filters { public class LogAttribute : ActionFilterAttribute { private ILog log = LogManager.GetLogger(System.Reflection.MethodBase.GetCurrentMethod().DeclaringType); private Stopwatch stopwatch = new Stopwatch(); public override void OnActionExecuting(ActionExecutingContext filterContext) { stopwatch.Reset(); stopwatch.Start(); // ユーザー情報を取得して、現在あるユーザーがどのような操作をしているかをログに記録することができる log.Debug(string.Format("▼Start...{0}.{1}", // ユーザーID // ユーザー名 filterContext.RouteData.Values["controller"], filterContext.RouteData.Values["action"])); } public override void OnActionExecuted(ActionExecutedContext filterContext) { //filterContext.Exception // レスポンスヘッダーにキャッシュ無効化を追加 var response = filterContext.HttpContext.Response; response.Cache.SetCacheability(HttpCacheability.NoCache); base.OnActionExecuted(filterContext); } public override void OnResultExecuting(ResultExecutingContext filterContext) { base.OnResultExecuting(filterContext); } public override void OnResultExecuted(ResultExecutedContext filterContext) { // ユーザー情報を取得して、現在あるユーザーがどのような操作をしているかをログに記録することができる stopwatch.Stop(); log.Debug(string.Format("▲End.....{0}.{1}..........took {2}ms", // ユーザーID // ユーザー名 filterContext.RouteData.Values["controller"], filterContext.RouteData.Values["action"], stopwatch.ElapsedMilliseconds)); } } }
5、セキュリティ対策
下記記事の内容と同様。 pie001.hatenablog.com
以上