web-dev-qa-db-fra.com

Unité avec ASP.NET Core et MVC6 (Core)

Mise à jour 09.08.2018
Unity est en cours de développement ici mais je n'ai pas eu le temps de tester son fonctionnement avec le framework ASP.NET Core.

Mise à jour 15.03.2018
Cette solution est destinée au problème spécifique de l'utilisation d'ASP.NET Core v1 avec Unity lors de l'utilisation du .NET Framework 4.5.2 NE PAS le .NET Core Framework. J'ai dû utiliser cette configuration car j'avais besoin de certaines DLL .Net 4.5.2 mais pour tous ceux qui recommenceraient, je ne recommanderais pas cette approche. De plus, Unity n'est pas développé davantage (à ma connaissance), donc je recommanderais d'utiliser le cadre Autofac pour de nouveaux projets. Voir ceci Post pour plus d'informations sur la façon de le faire.

Intro
Je crée une application Web en utilisant ASP.NET avec MVC. Cette application dépend de certains services (un service WCF, un service de banque de données, etc.). Maintenant, pour garder les choses agréables et découplées, je veux utiliser un cadre DI (Dependecy Injection), en particulier Unity.

Recherche initiale
J'ai trouvé ça article de blog mais malheureusement ça ne marche pas. L'idée est pourtant Nice.
Il indique essentiellement que vous ne devez pas enregistrer tous les services enregistrés dans ServiceCollection dans votre propre conteneur, mais plutôt référencer le fournisseur de services par défaut.
Alors. si quelque chose doit être résolu, le ServiceProvider par défaut est appelé et s'il n'a pas de résolution, le type sera résolu en utilisant votre UnityContainer personnalisé.

Les problèmes
MVC essaie toujours de résoudre le contrôleur avec le ServiceProvider par défaut.
De plus, j'ai remarqué que même si le contrôleur était résolu correctement, je ne pourrai jamais "mélanger" les dépendances. Maintenant, si je veux utiliser l'un de mes services mais aussi une interface IOptions de ASP la classe ne peut jamais être résolue car aucun de ces deux conteneurs n'a de résolutions pour les deux types.

Ce dont j'ai besoin
Donc, pour récapituler, j'ai besoin des choses suivantes:

  • Une configuration où je n'ai pas besoin de copier les dépendances ASP.NET dans mon UnityContainer
  • Un conteneur qui peut résoudre mes contrôleurs MVC
  • Un conteneur qui peut résoudre les dépendances "mixtes"

MODIFIER:
La question est donc de savoir comment puis-je atteindre ces points?

Environnement
project.json:
enter image description here

20
D4rth B4n3

Donc, après quelques recherches, j'ai trouvé les solutions suivantes à mes problèmes:

Utilisez Unity avec ASP
Pour pouvoir utiliser Unity avec ASP j'avais besoin d'un IServiceProvider personnalisé ( documentation ASP ) j'ai donc écrit un wrapper pour IUnityContainer qui ressemble à ceci

public class UnityServiceProvider : IServiceProvider
{
    private IUnityContainer _container;

    public IUnityContainer UnityContainer => _container;

    public UnityServiceProvider()
    {
        _container = new UnityContainer();
    }

    #region Implementation of IServiceProvider

    /// <summary>Gets the service object of the specified type.</summary>
    /// <returns>A service object of type <paramref name="serviceType" />.-or- null if there is no service object of type <paramref name="serviceType" />.</returns>
    /// <param name="serviceType">An object that specifies the type of service object to get. </param>
    public object GetService(Type serviceType)
    {
        //Delegates the GetService to the Containers Resolve method
        return _container.Resolve(serviceType);
    }

    #endregion
}

J'ai également dû changer la signature de la méthode ConfigureServices dans ma classe de démarrage à partir de ceci:

public void ConfigureServices(IServiceCollection services)

pour ça:

public IServiceProvider ConfigureServices(IServiceCollection services)

Maintenant, je peux retourner mon IServiceProvider personnalisé et il sera utilisé à la place de celui par défaut.
La méthode ConfigureServices complète est indiquée dans la section Câblage en bas.

Résolution des contrôleurs
J'ai trouvé ce billet de blog . J'en ai appris que MVC utilise une interface IControllerActivator pour gérer l'instanciation du contrôleur. J'ai donc écrit le mien qui ressemble à ceci:

public class UnityControllerActivator : IControllerActivator
{
    private IUnityContainer _unityContainer;

    public UnityControllerActivator(IUnityContainer container)
    {
        _unityContainer = container;
    }

    #region Implementation of IControllerActivator

    public object Create(ControllerContext context)
    {
        return _unityContainer.Resolve(context.ActionDescriptor.ControllerTypeInfo.AsType());
    }


    public void Release(ControllerContext context, object controller)
    {
        //ignored
    }

    #endregion
}

Maintenant, si une classe Controller est activée, elle sera instanciée avec mon UnityContainer. Par conséquent, mon UnityContainer doit savoir comment résoudre n'importe quel contrôleur!

Problème suivant: utilisez le fournisseur IServiceProvider par défaut
Maintenant, si j'enregistre des services tels que Mvc dans ASP.NET, je le ferais normalement comme ceci:

services.AddMvc();

Maintenant, si j'utilise un UnityContainer, toutes les dépendances MVC ne pourront pas être résolues car elles ne sont pas enregistrées. Je peux donc soit les enregistrer (comme AutoFac), soit créer un UnityContainerExtension. J'ai opté pour l'extension et j'ai trouvé les deux classes suivantes:
UnityFallbackProviderExtension

public class UnityFallbackProviderExtension : UnityContainerExtension
{
    #region Const

    ///Used for Resolving the Default Container inside the UnityFallbackProviderStrategy class
    public const string FALLBACK_PROVIDER_NAME = "UnityFallbackProvider";

    #endregion

    #region Vars

    // The default Service Provider so I can Register it to the IUnityContainer
    private IServiceProvider _defaultServiceProvider;

    #endregion

    #region Constructors

    /// <summary>
    /// Creates a new instance of the UnityFallbackProviderExtension class
    /// </summary>
    /// <param name="defaultServiceProvider">The default Provider used to fall back to</param>
    public UnityFallbackProviderExtension(IServiceProvider defaultServiceProvider)
    {
        _defaultServiceProvider = defaultServiceProvider;
    }

    #endregion

    #region Overrides of UnityContainerExtension

    /// <summary>
    /// Initializes the container with this extension's functionality.
    /// </summary>
    /// <remarks>
    /// When overridden in a derived class, this method will modify the given
    /// <see cref="T:Microsoft.Practices.Unity.ExtensionContext" /> by adding strategies, policies, etc. to
    /// install it's functions into the container.</remarks>
    protected override void Initialize()
    {
        // Register the default IServiceProvider with a name.
        // Now the UnityFallbackProviderStrategy can Resolve the default Provider if needed
        Context.Container.RegisterInstance(FALLBACK_PROVIDER_NAME, _defaultServiceProvider);

        // Create the UnityFallbackProviderStrategy with our UnityContainer
        var strategy = new UnityFallbackProviderStrategy(Context.Container);

        // Adding the UnityFallbackProviderStrategy to be executed with the PreCreation LifeCycleHook
        // PreCreation because if it isnt registerd with the IUnityContainer there will be an Exception
        // Now if the IUnityContainer "magically" gets a Instance of a Type it will accept it and move on
        Context.Strategies.Add(strategy, UnityBuildStage.PreCreation);
    }

    #endregion
}


UnityFallbackProviderStrategy :

public class UnityFallbackProviderStrategy : BuilderStrategy
{
    private IUnityContainer _container;

    public UnityFallbackProviderStrategy(IUnityContainer container)
    {
        _container = container;
    }

    #region Overrides of BuilderStrategy

    /// <summary>
    /// Called during the chain of responsibility for a build operation. The
    /// PreBuildUp method is called when the chain is being executed in the
    /// forward direction.
    /// </summary>
    /// <param name="context">Context of the build operation.</param>
    public override void PreBuildUp(IBuilderContext context)
    {
        NamedTypeBuildKey key = context.OriginalBuildKey;

        // Checking if the Type we are resolving is registered with the Container
        if (!_container.IsRegistered(key.Type))
        {
            // If not we first get our default IServiceProvider and then try to resolve the type with it
            // Then we save the Type in the Existing Property of IBuilderContext to tell Unity
            // that it doesnt need to resolve the Type
            context.Existing = _container.Resolve<IServiceProvider>(UnityFallbackProviderExtension.FALLBACK_PROVIDER_NAME).GetService(key.Type);
        }

        // Otherwise we do the default stuff
        base.PreBuildUp(context);
    }

    #endregion
}

Maintenant, si mon UnityContainer n'a pas d'enregistrement pour quelque chose, il suffit de le demander au fournisseur par défaut.
J'ai appris tout cela à partir de plusieurs articles différents

La bonne chose à propos de cette approche est que je peux également "mélanger" les dépendances maintenant. Si j'ai besoin de l'un de mes services ET d'une interface IOptions de ASP mon UnityContainer résoudra toutes ces dépendances et les injectera dans mon contrôleur !!!
.

Enfin: câbler
Maintenant, dans mon projet, j'utilise différents services (Options ASP, MVC avec options). Pour que tout fonctionne, ma méthode ConfigureServices ressemble à ceci maintenant:

public IServiceProvider ConfigureServices(IServiceCollection services)
    {
        // Add all the ASP services here
        // #region ASP
        services.AddOptions();
        services.Configure<WcfOptions>(Configuration.GetSection("wcfOptions"));

        var globalAuthFilter = new AuthorizationPolicyBuilder()
            .RequireAuthenticatedUser()
            .Build();

        services.AddMvc(options => { options.Filters.Add(new AuthorizeFilter(globalAuthFilter)); })
                .AddJsonOptions
            (
                options => options.SerializerSettings.ContractResolver = new DefaultContractResolver()
            );
        // #endregion ASP

        // Creating the UnityServiceProvider
        var unityServiceProvider = new UnityServiceProvider();

        IUnityContainer container = unityServiceProvider.UnityContainer;

        // Adding the Controller Activator
        // Caution!!! Do this before you Build the ServiceProvider !!!
        services.AddSingleton<IControllerActivator>(new UnityControllerActivator(container));

        //Now build the Service Provider
        var defaultProvider = services.BuildServiceProvider();

        // Configure UnityContainer
        // #region Unity

        //Add the Fallback extension with the default provider
        container.AddExtension(new UnityFallbackProviderExtension(defaultProvider));

        // Register custom Types here

        container.RegisterType<ITest, Test>();

        container.RegisterType<HomeController>();
        container.RegisterType<AuthController>();

        // #endregion Unity

        return unityServiceProvider;
    }

Depuis que j'ai appris la plupart de ce que je sais sur DI au cours de la semaine dernière, j'espère que je n'ai pas cassé de gros Pricipal/Pattern si c'est le cas, dites-le moi!

37
D4rth B4n3

Pour ASP.Net Core 2.0, 2.1, 2.2 et Unity, il existe une solution officielle disponible auprès des auteurs Unity en tant que package NuGet ici: NuGetPackage

Voici le dépôt Git avec des exemples: Git repo

L'utilisation est très simple (depuis la page d'accueil de Git repo):

public static IWebHost BuildWebHost(string[] args) =>
WebHost.CreateDefaultBuilder(args)
       .UseUnityServiceProvider()   <---- Add this line
       .UseStartup<Startup>()
       .Build();

Et ici est un exemple avec Unity DI pour ASP.Net Core.

J'utilise cette solution dans mon application ASP.Net Core et fonctionne bien.

19
Karel Kral