ASP.NET 5&MVC 6シリーズの解読(12):Lamda式に基づく強いタイプRouting実装

10913 ワード

原文:
ASP.NET 5&MVC 6シリーズの解読(12):Lamda式に基づく強いタイプRouting実装
前述のRouting章では,MVCにおいて,デフォルトのASP.NET 5を用いたルーティング登録方式に加えて,Attributeに基づく特性(RouteとHttpXXXシリーズ手法)を用いて定義することも可能である.この章では、Lambda式に基づく強いタイプについて説明します.
この方式の基本的な使用例は以下の通りである.
services.Configure<MvcOptions>(opt =>

{

    opt.EnableTypedRouting();



    opt.GetRoute("homepage", c => c.Action<ProductsController>(x => x.Index()));

    opt.GetRoute("aboutpage/{name}", c => c.Action<ProductsController>(x => x.About(Param<string>.Any)));

    opt.PostRoute("sendcontact", c => c.Action<ProductsController>(x => x.Contact()));

});

例から分かるように、我々はGetRouteやPostRouteなどの拡張方法でrouteを定義することができ、後にLambda式を用いてControllerのタイプとActionの方法を決定することができる.
なお、ここでActionを取得するメソッド名は、そのActionメソッドの実行を依頼することによって実現される(実際には実行されず、これに基づいてそのActionを取得するMethodInfo).
実装の原理Stratup.csConfigureServicesメソッドでサービスを構成する場合、MVCサイトで使用するコアプロファイルMvcOptionsを構成することができます.このクラスにはApplicationModelConventions属性(List<IApplicationModelConvention>)が1つIApplicationModelConventionインタフェースの集合を保存し、インタフェースを変更することでMVCプログラムのプログラムモデルをパイプライン処理することができます.インタフェースの定義は次のとおりです.
public interface IApplicationModelConvention

{

   void Apply(ApplicationModel application);

}

インタフェースのApplyメソッドが受信するパラメータのタイプはApplicationModelであり、ApplicationModel2つの極めて重要な内容が操作できる.1つはControllerモデルの集合であり、1つは各種Filterの集合であり、このクラスの定義は以下の通りである.
public class ApplicationModel

{

    public ApplicationModel();



    public IList<ControllerModel> Controllers { get; }

    public IList<IFilter> Filters { get; }

}

ここで最も重要なのはControllerModelクラスであり、このクラスと関連アクション上のルーティング定義データ、API記述情報、ルーティング制約など、様々な重要で操作可能な情報が格納されている.
新しいIApplicationModelConventionの登録方法は以下の通りです.
services.Configure<MvcOptions>(opt =>

{

   opts.ApplicationModelConventions.Add(new MyApplicationModelConvention());

});

そこで,この手法を用いて,MVC全体のプログラムモデルに適切なタイミングで応答する調整と修正を行うことができ,この章の強いタイプのルーティングはこの特性を利用して実現される.
実装手順
まず、強いタイプのルーティングモデルTypedRouteModelクラスを定義します.このクラスはAttributeRouteModelに継承されます.AttributeRouteModelクラスはAttributeルーティングに基づく基本モデルです.TypedRouteModelクラスのコードは以下の通りです.
public class TypedRouteModel : AttributeRouteModel

{

    public TypedRouteModel(string template)

    {

        Template = template;

        HttpMethods = new string[0];

    }



    public TypeInfo ControllerType { get; private set; }



    public MethodInfo ActionMember { get; private set; }



    public IEnumerable<string> HttpMethods { get; private set; }



    public TypedRouteModel Controller<TController>()

    {

        ControllerType = typeof(TController).GetTypeInfo();

        return this;

    }



    public TypedRouteModel Action<T, U>(Expression<Func<T, U>> expression)

    {

        ActionMember = GetMethodInfoInternal(expression);

        ControllerType = ActionMember.DeclaringType.GetTypeInfo();

        return this;

    }



    public TypedRouteModel Action<T>(Expression<Action<T>> expression)

    {

        ActionMember = GetMethodInfoInternal(expression);

        ControllerType = ActionMember.DeclaringType.GetTypeInfo();

        return this;

    }



    private static MethodInfo GetMethodInfoInternal(dynamic expression)

    {

        var method = expression.Body as MethodCallExpression;

        if (method != null)

            return method.Method;



        throw new ArgumentException("Expression is incorrect!");

    }



    public TypedRouteModel WithName(string name)

    {

        Name = name;

        return this;

    }



    public TypedRouteModel ForHttpMethods(params string[] methods)

    {

        HttpMethods = methods;

        return this;

    }

}

このクラスの主な機能は、転送をサポートするControllerタイプを定義し、チェーン呼び出しをサポートすることです.
次に、継承IApplicationModelConventionインタフェースのTypedRoutingApplicationModelConventionクラスを定義します.コードは次のとおりです.
public class TypedRoutingApplicationModelConvention : IApplicationModelConvention

{

    internal static readonly Dictionary<TypeInfo, List<TypedRouteModel>> Routes = new Dictionary<TypeInfo, List<TypedRouteModel>>();



    public void Apply(ApplicationModel application)

    {

        foreach (var controller in application.Controllers)

        {

            if (Routes.ContainsKey(controller.ControllerType))

            {

                var typedRoutes = Routes[controller.ControllerType];

                foreach (var route in typedRoutes)

                {

                    var action = controller.Actions.FirstOrDefault(x => x.ActionMethod == route.ActionMember);

                    if (action != null)

                    {

                        action.AttributeRouteModel = route;

                        //         ,     Controller  Route       

                        foreach (var method in route.HttpMethods)

                        {

                            action.HttpMethods.Add(method);

                        }

                    }

                }

            }

        }

    }

}

このクラスには、Lamda式で宣言されたすべてのルーティングを保存し、既存のControllersコレクションで検索および変更した後、AttributeRouteModelプロパティを置き換え、応答するHttp Methodを設定する静的変数Routesが保存されています(設定しない場合は、デフォルトではすべての方法が許可されます).
ここでは、単純に置き換えるだけaction.AttributeRouteModelですので、いくつかの欠陥(例えば、1つのアクションは1つのルーティングパスしかサポートできず、最後のパスに準じて)を招き、皆さんは自分の能力に応じて最適化することができます.
最適化の際には、Controller上のRoute集合がcontroller.Attributes属性に保存され、Action上のRoute集合がaction.Attributes属性に保存されていることに注意して最適化することができる.
次に、MvcOptionsでは、TypeRouteModelに拡張方法を追加して使用しやすくします.コードは次のとおりです.
public static class MvcOptionsExtensions

{

    public static TypedRouteModel GetRoute(this MvcOptions opts, string template, Action<TypedRouteModel> configSetup)

    {

        return AddRoute(template, configSetup).ForHttpMethods("GET");

    }



    public static TypedRouteModel PostRoute(this MvcOptions opts, string template, Action<TypedRouteModel> configSetup)

    {

        return AddRoute(template, configSetup).ForHttpMethods("POST");

    }



    public static TypedRouteModel PutRoute(this MvcOptions opts, string template, Action<TypedRouteModel> configSetup)

    {

        return AddRoute(template, configSetup).ForHttpMethods("PUT");

    }



    public static TypedRouteModel DeleteRoute(this MvcOptions opts, string template, Action<TypedRouteModel> configSetup)

    {

        return AddRoute(template, configSetup).ForHttpMethods("DELETE");

    }



    public static TypedRouteModel TypedRoute(this MvcOptions opts, string template, Action<TypedRouteModel> configSetup)

    {

        return AddRoute(template, configSetup);

    }



    private static TypedRouteModel AddRoute(string template, Action<TypedRouteModel> configSetup)

    {

        var route = new TypedRouteModel(template);

        configSetup(route);



        if (TypedRoutingApplicationModelConvention.Routes.ContainsKey(route.ControllerType))

        {

            var controllerActions = TypedRoutingApplicationModelConvention.Routes[route.ControllerType];

            controllerActions.Add(route);

        }

        else

        {

            var controllerActions = new List<TypedRouteModel> { route };

            TypedRoutingApplicationModelConvention.Routes.Add(route.ControllerType, controllerActions);

        }



        return route;

    }



    public static void EnableTypedRouting(this MvcOptions opts)

    {

        opts.ApplicationModelConventions.Add(new TypedRoutingApplicationModelConvention());

    }

}

上記のコードでは、EnableTypedRouting拡張メソッドを追加し、MvcOptions.ApplicationModelConventions属性に新しいTypedRoutingApplicationModelConventionタイプ例を追加しました.
他の拡張メソッドは、関連するrouteを宣言するために使用されます.最初の例では、action情報を取得する方法は、このactionメソッドを呼び出すように依頼することですが(実際に呼び出されていません)、パラメータがある方法があります.どうすればいいですか.このため、パラメータを無視したParamクラスを次のように指定します.
public static class Param<TValue>

{

    public static TValue Any

    {

        get { return default(TValue); }

    }

}

これにより,パラメータを含むAboutメソッドをルーティングする際に,コードを以下のように定義することができる.
opt.GetRoute("aboutpage/{name}", c => c.Action<HomeController>(x => x.About(Param<string>.Any)));

また、TypeRouteModelではチェーン呼び出しが可能な方法が多いため、routeの名前を指定することもできます.サンプルコードは以下の通りです.
opt.GetRoute("homepage", c => c.Action<HomeController>(x => x.Index())).WithName("foo");

これで、強いタイプのルーティング全体の機能が実現し、皆さんが使用するときに、もう一つの選択肢が増えました.
弊害(またはBug)
上記では、IApplicationModelConventionインタフェースを実装する場合、単純にaction.AttributeRouteModelを置き換えるだけです.つまり、ActionですでにRouteプロパティを持っている場合、彼はあなたの情報を上書きし、routeが失効することになります.たとえば、このようなカスタムルーティングを定義した場合:
public class ProductsController : Controller

{

    [Route("index")]

    public IActionResult Index()

    {

        return Content("Index");

    }

}

次に、Lamda式によって、次のような強いタイプのルーティングが定義されます.
opt.GetRoute("homepage", c => c.Action<ProductsController>(x => x.Index()));

では、あなたは/homepageを通じてしかアクセスできません./indexを通じてアクセスできません.それはあなたのRouteをカバーしているからです.
ただし、前述のLamda式では、Controllerで定義されているRouteプロパティ定義は上書きされていませんので、ProductsControllerでRouteプロパティを定義すると、次のように組み合わせられます.
[Route("products")]

public class ProductsController : Controller

{    

    public IActionResult Index()

    {

        return Content("Index");

    }

}

では、あなたのアクセス先は/product/homepageではなく/homepageですが、Lamda式のコードであれば、次のようになります.
opt.GetRoute("/homepage", c => c.Action<ProductsController>(x => x.Index()));

あなたのアクセス先は/homepageです.このルート文字は絶対パスですから/homepageではなくhomepageです.
参考:http://www.strathweb.com/2015/03/strongly-typed-routing-asp-net-mvc-6-iapplicationmodelconvention/
同期と推奨
この文書はディレクトリインデックスに同期済み:ASP.NET 5&MVC 6シリーズの解読