web-dev-qa-db-ja.com

ASP.NET Coreでの汎用コントローラーの検出

私はこのような一般的なコントローラーを作成しようとしています:

[Route("api/[controller]")]
public class OrdersController<T> : Controller where T : IOrder
{
    [HttpPost("{orderType}")]
    public async Task<IActionResult> Create(
        [FromBody] Order<T> order)
    {
       //....
    }
}

{orderType} URIセグメント変数がコントローラーのジェネリック型を制御することを意図しています。私はカスタムIControllerFactoryIControllerActivatorの両方を実験していますが、何も機能していません。リクエストを送信しようとするたびに、404応答が返されます。カスタムコントローラーファクトリー(およびアクティベーター)のコードが実行されません。

明らかに問題は、ASP.NETコアが有効なコントローラーが「コントローラー」という接尾辞で終わることを期待していることですが、私の汎用コントローラーには代わりに(リフレクションに基づく)サフィックス「コントローラー」があります。したがって、それが宣言する属性ベースのルートは気付かれなくなります。

ASP.NET MVCでは、少なくとも初期の段階では、 DefaultControllerFactoryはすべての利用可能なコントローラーを検出する責任がありました 。 「Controller」サフィックスをテストしました。

MVCフレームワークは、IControllerを実装し、名前が「Controller」で終わるすべてのタイプを探すアプリドメイン内のすべてのアセンブリを検索するデフォルトのコントローラーファクトリ(DefaultControllerFactoryという名前)を提供します。

どうやら、ASP.NET Coreでは、コントローラーファクトリはこの責任を負いません。前述したように、カスタムコントローラーファクトリは「通常の」コントローラーに対して実行されますが、汎用コントローラーに対しては呼び出されません。したがって、評価プロセスの初期の段階で、コントローラーの検出を管理する何か他のものがあります。

誰がその「サービス」インターフェースがその発見の原因であるか知っていますか?カスタマイズインターフェイスや「フック」ポイントがわかりません。

また、ASP.NET Coreが発見したすべてのコントローラーの名前を "ダンプ"する方法を知っている人はいますか?私が期待するすべてのカスタムコントローラー検出が実際に機能していることを確認する単体テストを作成することは素晴らしいことです。

ちなみに、一般的なコントローラー名を検出できる「フック」がある場合、ルート置換も正規化する必要があることを意味します。

[Route("api/[controller]")]
public class OrdersController<T> : Controller { }

Tにどのような値が指定されているかに関係なく、[controller]の名前は単純な基本汎用名のままである必要があります。上記のコードを例にとると、[controller]の値は「Orders」になります。 「Orders`1」や「OrdersOfSomething」にはなりません。

注意

この問題は、実行時に生成する代わりに、閉じたジェネリック型を明示的に宣言することでも解決できます。

public class VanityOrdersController : OrdersController<Vanity> { }
public class ExistingOrdersController : OrdersController<Existing> { }

上記は機能しますが、私が好きではないURIパスが生成されます。

~/api/VanityOrders
~/api/ExistingOrders

私が実際に欲しかったのはこれです:

~/api/Orders/Vanity
~/api/Orders/Existing

別の調整により、私が探しているURIがわかります。

[Route("api/Orders/Vanity", Name ="VanityLink")]
public class VanityOrdersController : OrdersController<Vanity> { }
[Route("api/Orders/Existing", Name = "ExistingLink")]
public class ExistingOrdersController : OrdersController<Existing> { }

しかし、これはうまくいくように見えますが、実際には私の質問に答えるものではありません。コンパイル時に間接的に(手動コーディングを介して)ではなく、実行時に直接汎用コントローラーを使用したいと思います。基本的に、これは、実行時リフレクション名が予期される「コントローラー」サフィックスで終わっていないにもかかわらず、汎用コントローラーを「表示」または「検出」できるようにするためにASP.NETコアが必要であることを意味します。

20
Brent Arias

簡潔な答え

実装 _IApplicationFeatureProvider<ControllerFeature>_

質問と回答

[利用可能なすべてのコントローラーを検出する]ための「サービス」インターフェースの役割を知っている人はいますか?

ControllerFeatureProvider がその原因です。

また、ASP.NET Coreが発見したすべてのコントローラーの名前を "ダンプ"する方法を知っている人はいますか?

ControllerFeatureProvider.IsController(TypeInfo typeInfo) 内で行います。

MyControllerFeatureProvider.cs

_using System;
using System.Linq;
using System.Reflection;
using Microsoft.AspNetCore.Mvc.Controllers;

namespace CustomControllerNames 
{
    public class MyControllerFeatureProvider : ControllerFeatureProvider 
    {
        protected override bool IsController(TypeInfo typeInfo)
        {
            var isController = base.IsController(typeInfo);

            if (!isController)
            {
                string[] validEndings = new[] { "Foobar", "Controller`1" };

                isController = validEndings.Any(x => 
                    typeInfo.Name.EndsWith(x, StringComparison.OrdinalIgnoreCase));
            }

            Console.WriteLine($"{typeInfo.Name} IsController: {isController}.");

            return isController;
        }
    }
}
_

起動時に登録してください。

_public void ConfigureServices(IServiceCollection services)
{
    services
        .AddMvcCore()
        .ConfigureApplicationPartManager(manager => 
        {
            manager.FeatureProviders.Add(new MyControllerFeatureProvider());
        });
}
_

次に出力例を示します。

_MyControllerFeatureProvider IsController: False.
OrdersFoobar IsController: True.
OrdersFoobarController`1 IsController: True.
Program IsController: False.
<>c__DisplayClass0_0 IsController: False.
<>c IsController: False.
_

これはGitHubのデモです 。幸運を祈ります。

編集-バージョンの追加

.NETバージョン

_> dnvm install "1.0.0-rc2-20221" -runtime coreclr -architecture x64 -os win -unstable
_

NuGet.Config

_<?xml version="1.0" encoding="utf-8"?>
<configuration>
  <packageSources>
    <clear/>
    <add key="AspNetCore" 
         value="https://www.myget.org/F/aspnetvnext/api/v3/index.json" />  
  </packageSources>
</configuration>
_

.NET CLI

_> dotnet --info
.NET Command Line Tools (1.0.0-rc2-002429)

Product Information:
 Version:     1.0.0-rc2-002429
 Commit Sha:  612088cfa8

Runtime Environment:
 OS Name:     Windows
 OS Version:  10.0.10586
 OS Platform: Windows
 RID:         win10-x64
_

復元、ビルド、実行

_> dotnet restore
> dotnet build
> dotnet run
_

編集-RC1とRC2に関するメモ

DefaultControllerTypeProvider.IsController()internalとしてマークされているため、これはRC1では不可能かもしれません。

16
Shaun Luttin

デフォルトで何が起こるか

コントローラーの検出プロセス中、開いているジェネリック_Controller<T>_クラスが候補の型に含まれます。ただし、_IApplicationFeatureProvider<ControllerFeature>_インターフェースのデフォルト実装であるDefaultControllerTypeProviderは、オープンジェネリックパラメーターを持つクラスを除外するため、_Controller<T>_を排除します。

IsController()のオーバーライドが機能しない理由

_IApplicationFeatureProvider<ControllerFeature>_インターフェースのデフォルト実装を置き換えて、DefaultControllerTypeProvider.IsController()をオーバーライドすることはできません。なぜなら、実際にディスカバリー・プロセスがオープン・ジェネリック・コントローラー(_Controller<T>_)を有効なコントローラーとして受け入れる必要はないからです。それはnotそれ自体が有効なコントローラーであり、コントローラーファクトリーはTが何であるかを知らないため、それをインスタンス化する方法を知りません。

なすべきこと

1.閉じたコントローラータイプを生成する

コントローラーの検出プロセスが始まる前に、リフレクションを使用して、開いているジェネリックコントローラーからクローズされたジェネリック型を生成する必要があります。ここでは、AccountおよびContactという名前の2つのサンプルエンティティタイプを使用しています。

_Type[] entityTypes = new[] { typeof(Account), typeof(Contact) };
TypeInfo[] closedControllerTypes = entityTypes
    .Select(et => typeof(Controller<>).MakeGenericType(et))
    .Select(cct => cct.GetTypeInfo())
    .ToArray();
_

_Controller<Account>_および_Controller<Contact>_のTypeInfosをクローズしました。

2.それらをアプリケーションパーツに追加して登録する

通常、アプリケーションパーツはCLRアセンブリでラップされますが、実行時に生成されるタイプのコレクションを提供するカスタムアプリケーションパーツを実装することもできます。 IApplicationPartTypeProviderインターフェースを実装する必要があるだけです。したがって、ランタイムで生成されたコントローラータイプは、他の組み込みタイプと同様にコントローラー検出プロセスに入ります。

カスタムアプリケーション部分:

_public class GenericControllerApplicationPart : ApplicationPart, IApplicationPartTypeProvider
{
    public GenericControllerApplicationPart(IEnumerable<TypeInfo> typeInfos)
    {
        Types = typeInfos;
    }

    public override string Name => "GenericController";
    public IEnumerable<TypeInfo> Types { get; }
}
_

MVCサービスへの登録(_Startup.cs_):

_services.AddMvc()
    .ConfigureApplicationPartManager(apm =>
        apm.ApplicationParts.Add(new GenericControllerApplicationPart(closedControllerTypes)));
_

コントローラーが組み込みのControllerクラスから派生している限り、IsControllerControllerFeatureProviderメソッドをオーバーライドする必要はありません。ジェネリックコントローラーはControllerBaseから_[Controller]_属性を継承するため、多少奇妙な名前( "Controller`1")に関係なく、検出プロセスのコントローラーとして受け入れられます。

3.アプリケーションモデルのコントローラー名を上書きする

それでも、 "Controller`1"はルーティングの目的には適していません。閉じた各汎用コントローラーに独立したRouteValuesを持たせたい。ここでは、コントローラーの名前をエンティティタイプの名前に置き換え、2つの独立した「AccountController」タイプと「ContactController」タイプで何が起こるかを一致させます。

モデル規約属性:

_public class GenericControllerAttribute : Attribute, IControllerModelConvention
{
    public void Apply(ControllerModel controller)
    {
        Type entityType = controller.ControllerType.GetGenericArguments()[0];

        controller.ControllerName = entityType.Name;
    }
}
_

コントローラクラスに適用:

_[GenericController]
public class Controller<T> : Controller
{
}
_

結論

このソリューションは、全体的なASP.NET Coreアーキテクチャに近いままであり、特に、APIエクスプローラーを介してコントローラーの完全な可視性を維持します( "Swagger"と考えてください)。

従来のルーティングと属性ベースのルーティングの両方で正常にテストされています。

11
Mathieu Renda

アプリケーション機能プロバイダーは、アプリケーションパーツを調べ、それらのパーツに機能を提供します。次のMVC機能の組み込み機能プロバイダーがあります。

  • コントローラー
  • メタデータリファレンス
  • タグヘルパー
  • コンポーネントを表示

機能プロバイダーはIApplicationFeatureProviderから継承します。Tは機能のタイプです。上記のMVCの機能タイプのいずれかに対して独自の機能プロバイダーを実装できます。 ApplicationPartManager.FeatureProvidersコレクション内の機能プロバイダーの順序は重要です。これは、後のプロバイダーが前のプロバイダーによって実行されたアクションに対応できるためです。

デフォルトでは、ASP.NET Core MVCは汎用コントローラー(SomeControllerなど)を無視します。このサンプルは、デフォルトのプロバイダーの後に実行されるコントローラー機能プロバイダーを使用し、指定されたタイプのリスト(EntityTypes.Typesで定義)の汎用コントローラーインスタンスを追加します。

public class GenericControllerFeatureProvider : IApplicationFeatureProvider<ControllerFeature>
{
    public void PopulateFeature(IEnumerable<ApplicationPart> parts, ControllerFeature feature)
    {
        // This is designed to run after the default ControllerTypeProvider, 
        // so the list of 'real' controllers has already been populated.
        foreach (var entityType in EntityTypes.Types)
        {
            var typeName = entityType.Name + "Controller";
            if (!feature.Controllers.Any(t => t.Name == typeName))
            {
                // There's no 'real' controller for this entity, so add the generic version.
                var controllerType = typeof(GenericController<>)
                    .MakeGenericType(entityType.AsType()).GetTypeInfo();
                feature.Controllers.Add(controllerType);
            }
        }
    }
}

エンティティタイプ:

public static class EntityTypes
{
    public static IReadOnlyList<TypeInfo> Types => new List<TypeInfo>()
    {
        typeof(Sprocket).GetTypeInfo(),
        typeof(Widget).GetTypeInfo(),
    };

    public class Sprocket { }
    public class Widget { }
}

機能プロバイダーがスタートアップに追加されます。

services.AddMvc()
    .ConfigureApplicationPartManager(p => 
        p.FeatureProviders.Add(new GenericControllerFeatureProvider()));

デフォルトでは、ルーティングに使用される汎用コントローラー名は、ウィジェットではなくGenericController`1 [Widget]の形式になります。次の属性は、コントローラーが使用するジェネリック型に対応するように名前を変更するために使用されます。

microsoft.AspNetCore.Mvc.ApplicationModelsを使用します。システムの使用;

namespace AppPartsSample
{
    // Used to set the controller name for routing purposes. Without this convention the
    // names would be like 'GenericController`1[Widget]' instead of 'Widget'.
    //
    // Conventions can be applied as attributes or added to MvcOptions.Conventions.
    [AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = true)]
    public class GenericControllerNameConvention : Attribute, IControllerModelConvention
    {
        public void Apply(ControllerModel controller)
        {
            if (controller.ControllerType.GetGenericTypeDefinition() != 
                typeof(GenericController<>))
            {
                // Not a GenericController, ignore.
                return;
            }

            var entityType = controller.ControllerType.GenericTypeArguments[0];
            controller.ControllerName = entityType.Name;
        }
    }
}

GenericControllerクラス:

using Microsoft.AspNetCore.Mvc;

namespace AppPartsSample
{
    [GenericControllerNameConvention] // Sets the controller name based on typeof(T).Name
    public class GenericController<T> : Controller
    {
        public IActionResult Index()
        {
            return Content($"Hello from a generic {typeof(T).Name} controller.");
        }
    }
}

サンプル:汎用コントローラー機能

1
Mohammad Akbari

RC2のコントローラーのリストを取得するには、DependencyInjectionからApplicationPartManagerを取得し、次のようにします。

    ApplicationPartManager appManager = <FROM DI>;

    var controllerFeature = new ControllerFeature();
    appManager.PopulateFeature(controllerFeature);

    foreach(var controller in controllerFeature.Controllers)
    {
        ...
    }
0
bang