IT業界のすみっこ暮らし

ふと気がついたときの記録



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呼出前後の共通ロジックの参考」と同じです。

参考ページ

[asp.net-mvc] MVC 4でのAuthorizeAttributeのオーバーライド [asp.net-mvc-3] [asp.net-mvc-4] | CODE Q&A 問題解決 [日本語]

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


以上





プライバシーポリシー