web-dev-qa-db-ger.com

Einheit mit ASP.NET Core und MVC6 (Core)

Update 09.08.2018
Unity wird entwickelt hier aber ich hatte keine Zeit zu testen, wie es mit dem ASP.NET Core Framework funktioniert.

Update 15.03.2018
Diese Lösung ist für das spezielle Problem der Verwendung von ASP.NET Core v1 mit Unity unter .NET Framework 4.5.2 vorgesehen NICHT das .NET Core Framework. Ich musste dieses Setup verwenden, da ich einige .Net 4.5.2-DLLs benötigte, aber für jeden, der neu startet, würde ich diesen Ansatz nicht empfehlen. Unity wird meines Wissens auch nicht weiterentwickelt, daher würde ich empfehlen, das Autofac Framework für neue Projekte zu verwenden. Siehe this Post für weitere Informationen dazu.

Intro
Ich erstelle eine Webanwendung mit ASP.NET und MVC. Diese Anwendung hängt von bestimmten Diensten ab (einem WCF-Dienst, einem Datenspeicherdienst usw.). Um die Dinge nun schön und entkoppelt zu halten, möchte ich ein DI-Framework (Dependecy Injection) verwenden, insbesondere Unity.

Erste Recherche
Ich fand das Blogpost aber leider funktioniert es nicht. Die Idee ist aber schön.
Grundsätzlich heißt es, dass Sie nicht alle in der ServiceCollection registrierten Services in Ihrem eigenen Container registrieren sollten, sondern auf den Standard-ServiceProvider verweisen sollten.
So. Wenn etwas aufgelöst werden muss, wird der Standard-ServiceProvider aufgerufen. Wenn keine Auflösung vorliegt, wird der Typ mit Ihrem benutzerdefinierten UnityContainer aufgelöst.

Die Probleme
MVC versucht immer, den Controller mit dem Standard-ServiceProvider aufzulösen.
Außerdem habe ich festgestellt, dass ich Abhängigkeiten niemals "mischen" kann, selbst wenn der Controller korrekt aufgelöst würde. Wenn ich nun einen meiner Services, aber auch eine IOptions-Schnittstelle von ASP) verwenden möchte, kann die Klasse niemals aufgelöst werden, da keiner dieser beiden Container Auflösungen für beide Typen hat.

Was ich brauche
Um es zusammenzufassen, brauche ich folgende Dinge:

  • Ein Setup, bei dem ich keine ASP.NET-Abhängigkeiten in meinen UnityContainer kopieren muss
  • Ein Container, der meine MVC-Controller auflösen kann
  • Ein Container, der "gemischte" Abhängigkeiten auflösen kann

EDIT:
Die Frage ist also, wie ich diese Punkte erreichen kann?

Umgebung
project.json:
enter image description here

20
D4rth B4n3

Nach einigen Recherchen habe ich folgende Lösungen für meine Probleme gefunden:

Verwenden Sie Unity mit ASP
Um Unity mit ASP) verwenden zu können, benötigte ich einen benutzerdefinierten IServiceProvider ( ASP Documentation ), also schrieb ich einen Wrapper für den IUnityContainer, der so aussieht

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
}

Außerdem musste ich die Signatur der ConfigureServices-Methode in meiner Startup-Klasse folgendermaßen ändern:

public void ConfigureServices(IServiceCollection services)

dazu:

public IServiceProvider ConfigureServices(IServiceCollection services)

Jetzt kann ich meinen benutzerdefinierten IServiceProvider zurückgeben und er wird anstelle des Standard-IServiceProviders verwendet.
Die vollständige ConfigureServices-Methode wird im Abschnitt "Verdrahten" unten angezeigt.

Auflösen von Controllern
Ich fand diesen Blogeintrag . Daraus habe ich erfahren, dass MVC eine IControllerActivator-Schnittstelle verwendet, um die Controller-Instanziierung zu handhaben. Also habe ich meine eigene geschrieben, die so aussieht:

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
}

Wenn jetzt eine Controller-Klasse aktiviert ist, wird sie mit meinem UnityContainer installiert. Deshalb muss mein UnityContainer wissen, wie man einen Controller auflöst!

Nächstes Problem: Verwenden Sie den Standard-IServiceProvider
Wenn ich jetzt Dienste wie Mvc in ASP.NET registriere, würde ich das normalerweise so machen:

services.AddMvc();

Wenn ich jetzt einen UnityContainer verwende, können alle MVC-Abhängigkeiten nicht aufgelöst werden, da sie nicht registriert sind. So kann ich sie entweder registrieren (wie AutoFac) oder eine UnityContainerExtension erstellen. Ich habe mich für die Erweiterung entschieden und mir folgende zwei Klassen ausgedacht:
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
}

Wenn mein UnityContainer keine Registrierung für etwas hat, frage einfach den Standardanbieter danach.
All das habe ich aus verschiedenen Artikeln gelernt

Das Schöne an diesem Ansatz ist, dass ich jetzt auch Abhängigkeiten "mischen" kann. Wenn ich Dienste und ein IOptions-Interface von ASP) benötige, löst mein UnityContainer alle diese Abhängigkeiten auf und injiziert sie in meinen Controller !!!
Wenn ich eine meiner eigenen Abhängigkeiten verwende, muss ich meine Controller-Klasse bei Unity registrieren, da der Standard-IServiceProvider meine Controller-Abhängigkeiten nicht mehr auflösen kann.

Zum Schluss: Verdrahten
Jetzt verwende ich in meinem Projekt verschiedene Dienste (ASP-Optionen, MVC mit Optionen). Damit alles funktioniert, sieht meine ConfigureServices-Methode jetzt so aus:

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;
    }

Da ich in der letzten Woche das meiste über DI gelernt habe, hoffe ich, dass ich kein großes Pricipal/Pattern gebrochen habe, wenn ja, sag es mir bitte!

37
D4rth B4n3

Für ASP.Net Core 2.0, 2.1, 2.2 und Unity ist eine offizielle Lösung von Unity-Autoren als NuGet-Paket hier verfügbar: NuGetPackage

Hier ist das Git-Repository mit Beispielen: Git-Repo

Die Benutzung ist sehr einfach (von der Git Repo Homepage):

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

Und hier ist ein Beispiel mit Unity DI für ASP.Net Core.

Ich benutze diese Lösung in meiner ASP.Net Core-Anwendung und funktioniert gut.

19
Karel Kral