Dependencyinjection: Umgebungsbereiche werden nicht von allen DIs unterstützt

Erstellt am 4. Dez. 2015  ·  27Kommentare  ·  Quelle: aspnet/DependencyInjection

Ich diskutiere also mit @dotnetjunkie über die Implementierung eines DNS-Adapters für SimpleInjector (https://github.com/simpleinjector/SimpleInjector/issues/156).

Er ist kein Fan davon, eine Abstraktion zu implementieren, es ist ein konformes Container-Anti-Pattern. Ich habe argumentiert, dass SimpleInjector bei ASP.NET 5-Projekten für Benutzer einfach keine Wahl sein wird, wenn er keinen DNS-Adapter erstellt. Ich glaube (und er kann mich korrigieren, wenn ich falsch liege), dass das ultimative Problem auf Microsofts Verwendung von Umgebungsbereichen zurückzuführen ist .

Meine Frage ist also diese. Wenn man an den gemeinsamen Nenner von Features zwischen Containern denkt, wird dieser tatsächlich benötigt?

Werden Bereiche im ASP.NET-Stack nur im Kontext von Webanforderungen verwendet?

Wie wäre es mit dem Hinzufügen von zwei Arten von Bereichen:

  • Lokal/Umgebung – Erfordert einen Verweis auf das tatsächliche IServiceScope , um einen Dienst aufzulösen. So macht es die ASP.NET DI derzeit.
  • Global/non-ambiant - Der Geltungsbereich wird im Container selbst festgelegt. Es überprüft intern HttpContext.Current oder etwas Ähnliches. Vielleicht könnte es einfach als "WebRequestScope" bezeichnet und hart codiert werden, wenn die Abstraktion über DIs ein Problem wird.

Wenn die ASP.NET-Anforderungspipeline derzeit die _einzige_ Sache ist, die Bereiche verwendet, kann sie möglicherweise geändert werden, um globale Bereiche zu verwenden, wobei der lokale Ansatz intakt bleibt, aber nicht verwendet wird. Wenn wir dies tun, kann SimpleInjector Webanfragen unterstützen, wobei explizit erklärt wird, dass sie keine lokalen Bereiche unterstützen. Der Betreuer von SimpleInjector hat nicht die Absicht, jemals lokale Bereiche zu unterstützen, und es könnte einfach eine bekannte Einschränkung bei der Verwendung eines SimpleInjector-DNX-Adapters sein. Natürlich vorausgesetzt, dass die Kernbibliotheken für ASP.NET nicht von lokalen Bereichen abhängen. Wir möchten nicht, dass der SimpleInjector-DNX-Adapter sofort kaputt geht.

Irgendwelche Gedanken? Ich glaube nicht, dass @dotnetjunkie sich bewegt ;) SimpleInjector ist einer der schnellsten DIs, und es wird mir das Herz brechen, wenn er nicht unterstützt wird.

Hilfreichster Kommentar

Obwohl David Fowler die Verwendung von Ambient State nicht mag, ist es aus der vorherigen Diskussion wichtig zu betonen, dass an der Verwendung von Ambient State, die in der Kompositionswurzel gekapselt ist, nichts von Natur aus falsch ist . Umgebungszustand, wenn in der Kompositionswurzel gekapselt:

  • Ist komplett prüfbar.
  • Verletzt nicht SOLID.
  • Leckt nicht.
  • Wird nicht dazu führen, dass wir "die Fähigkeit verlieren, diesen Zustand als Teil des Vertrags auszudrücken".

Ich werde jeden der oben genannten Punkte im Folgenden genauer besprechen.

### Umgebungszustand, wenn in der Kompositionswurzel gekapselt: Ist vollständig testbar.

Wie bereits erwähnt und sogar von David als Antwort auf @TheBigRic bestätigt , kann die Verwendung des Umgebungszustands getestet werden, und wenn der Umgebungszustand hinter SOLID-Abstraktionen verborgen ist, können die Verbraucher dieser Abstraktionen getestet werden, ohne dass Instanzen des Umgebungszustands erforderlich sind.

### Umgebungszustand, wenn in der Kompositionswurzel gekapselt: Verletzt nicht SOLID

Umgebungszustand ist Zustand. State kann (und sollte) hinter Adaptern gekapselt werden, die den SOLID-Prinzipien folgen.

### Umgebungszustand, wenn in der Kompositionswurzel gekapselt: Leckt nicht.

Die Übergabe des Laufzeitzustands durch verschiedene Methodenaufrufe des Objektgraphen - wie David bereits vorgeschlagen hat - führt dazu, dass Abstraktionen Implementierungsdetails verlieren. Schauen Sie sich zum Beispiel die folgende Abstraktion mit einer einfachen Implementierung an:

``` c#
öffentliche Schnittstelle ICustomerRepository
{
void Speichern(Kundeneinheit);
}

öffentliche Klasse SqlCustomerRepository : ICustomerRepository
{
private readonly IUnitOfWork uow;

public CustomerRepository(IUnitOfWork uow) {
    this.uow = uow;
}

public void Save(Customer entity) {
    this.uow.Save(entity);
}

}

Imagine that we need to alter this `SqlCustomerRepository` in such way that when a `Customer` is saved we should append this action to the audit trail. The audit trail should contain the `Id` of the current user. Such an implementation may look like this:

``` c#
public class SqlCustomerRepository : ICustomerRepository
{
    public void Save(Customer entity) {
        this.uow.Save(entity);
        this.uow.Save(new AuditTrail {
            EntityId = entity.Id,
            Type = "Save",
            CreatedOn = DateTime.Now,
            CreatedBy = ???
        });
    }
}

Das Problem dabei ist, dass Customer keine Informationen über den aktuellen Benutzer enthält und da Customer eine Entität ist, würde es nicht viel Sinn machen, Details des aktuellen Benutzers hinzuzufügen. Wenn wir Davids Rat befolgen, würden wir die ICustomerRepository Benutzeroberfläche wie folgt ändern:

``` c#
öffentliche Schnittstelle ICustomerRepository
{
void Save(Kundenentität, Guid currentUserId);
}

This minor alteration has caused sweeping changes to ripple through the call stack, since all direct and indirect consumers will need to pass this value on. This causes many other application abstractions to be altered in favor of this 'minor change'. In SOLID terminology: **this is both an Open/Closed Principle violation and a Dependency Inversion Principle violation.**

Now, imagine that we did go through the painful process of making these sweeping changes through the application and then another request comes in: a change that asks us to store the `TenantId` in the audit table! We again have to change our `ICustomerRepository` abstraction. This approach is clearly leaking implementation details and is unmaintainable in the end.

If, however, we were to inject an `IUserContext` into the `SqlCustomerRepository`, these problems go away completely:

``` c#
class CustomerRepository : ICustomerRepository
{
    private readonly IUnitOfWork uow;
    private readonly IUserContext userContext;

    public CustomerRepository(IUnitOfWork uow, IUserContext userContext) {
        this.uow = uow;
        this.userContext = userContext;
    }

    public void Save(Customer entity) {
        this.uow.Save(entity);
        this.uow.Save(new AuditTrail {
            EntityId = entity.Id,
            Type = "Save",
            CreatedOn = DateTime.Now,
            CreatedBy = this.userContext.UserId
        });
    }
} 

Die Verwendung von IUserContext verhindert, dass sich Änderungen durch das System verbreiten und ob IUserContext.UserId mithilfe des Umgebungszustands abgerufen wird oder nicht, ist ein Implementierungsdetail der zugrunde liegenden IUserContext Implementierung, die von kein Interesse an den SqlCustomerRepository .

### Umgebungszustand, wenn er in der Kompositionswurzel gekapselt ist: Wird nicht dazu führen, dass wir "die Fähigkeit verlieren, diesen Zustand als Teil des Vertrags auszudrücken".

Die Aussage, dass wir dies nicht zum Vertragsinhalt machen können, ist unbegründet. Andererseits; wir bekommen mehr optionen. Falls wir feststellen, dass Kontextinformationen ein Implementierungsdetail sind, können wir sie hinter Abstraktionen kapseln, um zu verhindern, dass Änderungen durch das System übertragen werden. Sollten wir jedoch feststellen, dass diese Laufzeitdaten Bestandteil des öffentlichen Auftrags der Anwendung sein sollten, können wir diese dennoch so gestalten. Niemand hat gesagt, dass alle Informationen im Umgebungszustand gespeichert werden sollten und nichts durch Methodenaufrufe weitergegeben werden sollte.

Eines der Argumente, die David verwendete, war, dass "es alle zusammengesetzten Roots im System dazu zwingt, sich des Umgebungscontainers bewusst zu sein". Eine Kompositionswurzel ist der Einstiegspunkt der Anwendung und kennt daher per Definition jeden Aspekt der Anwendung. Es verfügt über intrinsische Kenntnisse über den ausgewählten DI-Container, das Anwendungsframework (zB ASP.NET Core MVC) und die Plattform, auf der die Anwendung ausgeführt wird. Es ist kein Problem für die Kompositionswurzel, sich des Umgebungszustands bewusst zu sein.

Es kann sein, dass es bei der Verwendung von _ambient scoping_ schwieriger wird, auf den übergeordneten Bereich zuzugreifen, während Sie sich im Kontext eines verschachtelten Bereichs befinden, wie Davids konstruiertes Beispiel zeigt. Es ist jedoch ein sehr ungewöhnlicher Anwendungsfall und es ist eigentlich ziemlich schwierig, ein gültiges Szenario für so etwas zu entwickeln. Obwohl der .NET Core DI-Container dieses Szenario zulässt, ist es sehr unwahrscheinlich, dass derzeit jemand dieses "Feature" verwendet. David zeigte 4 Beispiele, die alle der „normalen“ Arbeitsweise folgen; ein Weg, den Simple Injector und Castle Windsor ebenfalls unterstützen. Ich würde sagen, dass diese „Einschränkung“ nicht existiert. Das versehentliche Auflösen von Diensten aus einem übergeordneten Bereich ist sogar eine sehr häufige Fehlerquelle in Anwendungen, die DI-Container verwenden.

## Abschluss

Sie sollten den Anwendungscode auf keinen Fall direkt vom Umgebungszustand abhängen lassen, genauso wie Sie den Anwendungscode nicht direkt vom DI-Container abhängen lassen sollten. Der Umgebungszustand ist jedoch, wenn er in der Kompositionswurzel gekapselt ist, testbar, wartbar und verletzt keines der SOLID-Prinzipien und ist daher nicht "im Allgemeinen schlecht".

Alle 27 Kommentare

Gibt es keine Möglichkeit, dies einfach in die Abstraktion zu hacken, anstatt zu versuchen, die aktuelle Abstraktion zu ändern? Wir sind in RC2 und tbh ich sehe nicht, dass wir etwas so Grundlegendes ändern, vielleicht können wir einfach den vorhandenen Container in dieser Implementierung zurückgeben. Es ist nicht genau, aber besser als nichts, funktioniert im Standardfall und kann ausgelöst werden, wenn Sie mehr als einen Bereich erstellen (in diesem Impl).

Was ist damit?

@davidfowl Ich habe mich nicht mit jeder Verwendung von DI im ASP.NET-Stack befasst, aber wird es nur für

SimpleInjector veröffentlicht kein Paket, das nicht zu 100% funktioniert.

Damit werde ich wahrscheinlich einen machen.

Welchen statischen Accesor gibt es für die aktuelle Webanfrage? Ähnlich wie HttpContet.Current.Items["scope"] ?

Ich werde den Bereich dort speichern und ihn innerhalb der ctor und Dispose der erstellten IServiceScope . Ich werde eine Ausnahme auslösen, wenn in diesem statischen Speicher bereits ein Bereich vorhanden ist (verschachtelte Bereiche werden verhindert). Ich werde auch eine Ausnahme auslösen, wenn Sie versuchen, einen Bereich zu erstellen, der sich nicht in einem Webanforderungsthread befindet.

Ich hoffe, dass dies die Out-of-the-Box-Erfahrung für SimpleInjector bei der Arbeit mit ASP.NET 5 löst.

@davidfowl Ich habe mich nicht mit jeder Verwendung von DI im ASP.NET-Stack befasst, aber wird es nur für

Nö.

Welchen statischen Accesor gibt es für die aktuelle Webanfrage? Ähnlich wie HttpContet.Current.Items["scope"]?

Nö. Wir haben keine solche Statik mehr.

Nö. Wir haben keine solche Statik mehr.

:golf: :klatsch:

Ich glaube (und er kann mich korrigieren, wenn ich falsch liege), dass das ultimative Problem auf Microsofts Verwendung von Umgebungsbereichen zurückzuführen ist.

Leider ist die Inkompatibilität in den Umgebungsbereichen zwischen DNX und Simple Injector nur eine von vielen Inkompatibilitäten, und ich würde sagen, eine der am wenigsten problematischen, da diese durch einige kleine, nicht brechende Änderungen an Simple Injector behoben werden könnte. Es gibt jedoch noch andere - aus meiner Sicht unlösbare - Inkompatibilitäten. Neben meinem Kommentar zur alten Diskussion 41 habe ich kürzlich in diesem Kommentar die Inkompatibilitäten zwischen den beiden APIs beschrieben.

Verschieben Sie dies in das Backlog, da wir derzeit keine Änderungen daran planen.

Ich denke, dass Castle Windsor aus dem gleichen Grund mit dieser Bibliothek mehr oder weniger inkompatibel ist. Ich habe versucht, einen Adapter dafür zu bauen, und es ist mir nicht gelungen, ihn vollständig kompatibel zu machen (ich weiß jedoch nicht viel über die Windsor-Interna).

Da es eines der Hauptziele dieser Bibliothek ist, durch unseren 'bevorzugten' Container ersetzt zu werden , denke ich wirklich, dass Sie vor RTM mehr Zeit damit

Sie sperren einige große DI-Frameworks aus.

Ich sehe nicht, dass wir das ändern. Der Umgebungszustand ist im Allgemeinen schlecht und wir verwenden ihn in asp.net Core (Logging-Bereiche) sparsam. Wir haben keinen Code, der den Umgebungszustand vertieft, der irgendwo fließt (weil dies mit Leistungskosten verbunden ist).

Nach v1 können wir uns unsere Nutzung ansehen und sehen, ob es eine gleiche Möglichkeit gibt, diese Container in asp.net-Szenarien funktionsfähig zu halten.

@davidfowl , erkläre bitte, warum es eine schlechte Sache ist, einen Umgebungszustand in der Infrastruktur zu haben?

Fragen Sie mich, warum der Umgebungszustand im Allgemeinen schlecht ist oder warum ein Container mit Umgebungsanforderungsumfang schlecht ist? Der Umgebungszustand ist so schlecht wie der statische Zustand. .NET bietet Möglichkeiten, es auf einen Thread oder eine asynchrone Aufrufkette oder über den logischen Aufrufkontext zu beschränken. Sie sind alle nur Möglichkeiten, um zu vermeiden, dass Dinge herumgereicht werden. Insbesondere ASP.NET verfügt bereits über den http-Kontext, und es gibt keinen guten Grund, den Umgebungszustand einzuführen, wenn wir nur Objekte mit einer logischen HTTP-Anforderung verknüpfen können. Sie zahlen auch die Kosten für das Fließen des Umgebungszustands, wenn Sie es in einigen Fällen möglicherweise nicht möchten (aus diesem Grund verwendet ASP.NET 4.x den unlogischen Aufrufkontext). Es ist im Allgemeinen nur ein schlechtes Muster, und obwohl es manchmal nicht vermieden werden kann, tun wir unser Bestes, um es nach Möglichkeit zu vermeiden.

Wenn Sie einen Umgebungszustand wünschen, können Sie jederzeit einen asynchronen lokalen (oder einen beliebigen Umgebungsgrad) erstellen und ihn verwenden. Aber wir werden sicherlich nicht den gesamten Code in unseren Frameworks ändern, um davon auszugehen, dass ein Umgebungsbereich verfügbar ist.

Hallo David,

Ich versuche hier nur ein oder zwei Dinge zu lernen. Können Sie erklären, warum der Umgebungszustand eigentlich schlecht ist? Ihre vorherigen Aussagen enthielten keine Erklärung dafür, warum der Umgebungskontext tatsächlich schlecht ist. Um Ihre aktuelle Argumentation zusammenzufassen:

  • Der Umgebungszustand ist so schlecht wie der statische Zustand.
  • Es ist im Allgemeinen nur ein schlechtes Muster
  • Sie sind alle nur Möglichkeiten, um zu vermeiden, dass Dinge herumgereicht werden.
  • Es gibt keinen guten Grund, den Umgebungszustand einzuführen, wenn wir nur Objekte mit einer logischen http-Anfrage verknüpfen können.

Können Sie erklären, was das Problem bei der "Vermeidung von Dingen herum" ist, insbesondere wenn der Umgebungszustand ausschließlich im Kompositionsstamm der Anwendung (der Infrastruktur) verwendet wird?

Der Umgebungszustand erschwert das Testen und Isolieren von Dingen. Nehmen wir an, Sie wollten 2 Methoden mit einer anderen Containerinstanz aufrufen, weil Sie 2 Bereiche erstellt haben (innerhalb derselben asynchronen Aufrufkette). Wenn dieser Bereich Umgebungslicht wäre und in die Infrastruktur integriert wäre, wäre dies unmöglich. Es erschwert auch das Testen, da der Aufrufer wissen muss, dass der Umgebungsbereich für das Funktionieren der Dinge erforderlich ist, diese Abhängigkeit nicht explizit angegeben wird.

Können Sie erklären, was das Problem bei der "Vermeidung von Dingen herum" ist, insbesondere wenn der Umgebungszustand nur im Kompositionsstamm der Anwendung (der Infrastruktur) verwendet wird?

Ich muss Ihnen das nicht erklären, Sie haben einen Dependency-Injection-Container geschrieben und Sie zitieren immer SOLID-Prinzipien bei allem, was Sie schreiben. Ich bin mir nicht sicher, warum Sie den Umgebungszustand in alles (auch in die Kompositionswurzel) einbacken möchten, wenn dies einfach nicht erforderlich ist.

Wie ich schon sagte, haben , was wir umgesetzt ist das flexibelste Muster , die keine Annahme auf jedem Umgebungs scoped Behälter macht. Wenn Sie ein ServiceProvider.Current freigeben möchten, können Sie dies gerne tun, aber unsere Infrastruktur wird es nicht verwenden.

Nur damit wir uns darüber im Klaren sind, was wir beide über den Unterschied sagen zwischen:

```C#
public void CompositionRoot()
{
var entryPoint = ServiceProvider.Current.GetService();
EntryPoint.Invoke();
}

using(var scopedProvider = MakeScopeProvider())
{
ServiceProvider.Current = scopedProvider;
ZusammensetzungRoot();
ServiceProvider.Current = null;
}

and:

```C#
public void CompositionRoot(IServiceProvider provider)
{
   var entryPoint = provider.GetService<EntryPoint>();
   entryPoint.Invoke();
}

using (var scopedProvider = MakeScope())
{
    CompositionRoot(scopedProvider);
}

PS: Ich denke auch, dass es generell ein schlechtes Muster ist.

Ich muss Ihnen das nicht erklären, Sie haben einen Dependency-Injection-Container geschrieben und Sie zitieren immer SOLID-Prinzipien bei allem, was Sie schreiben. Ich bin mir nicht sicher, warum Sie den Umgebungszustand in irgendetwas backen möchten

Sie scheinen zu implizieren, dass die Verwendung des Ambient State nicht SOLID ist, was IMO falsch ist. Solange wir den Zugriff auf den Umgebungszustand mit Adaptern kapseln und diesen Teil des Kompositionsstamms der Anwendung machen, kann die Verwendung des Umgebungszustands sehr wohl SOLID sein.

Du behauptest auch, dass ich den Ambient-Zustand zu allem machen möchte, aber ich würde dies niemals vorschlagen oder fördern. Den Umgebungszustand in der Kompositionswurzel gekapselt und isoliert zu halten, ist etwas völlig anderes, als „Umgebungszustand in irgendetwas zu backen“.

Darüber hinaus kann die Lösung bei Anwendung der SOLID-Prinzipien sehr gut testbar bleiben. Der Anrufer nicht über die Existenz jeglicher Umgebungs Umfang überhaupt wissen müssen und wir gefälschte Adapter implementieren , die verschiedene Umgebungszustände während des Tests zur Verfügung stellen.

Sie wiederholen Ihre Behauptung, dass der Umgebungszustand ein "schlechtes Muster im Allgemeinen" ist, müssen jedoch noch vernünftige Argumente liefern, um diese Behauptung zu stützen. Zusammenfassen:

  • Sie geben an, dass der Umgebungszustand "nur eine Möglichkeit ist, das Herumreichen von Dingen zu vermeiden", ohne zu erklären, welche negativen Folgen es hat, wenn man nicht "Dinge herumreicht".
  • Sie geben an, dass dies die Testbarkeit behindert, aber diese Aussage ist falsch, wenn wir beispielsweise den Zugriff auf den Umgebungszustand hinter SOLID-Abstraktionen verstecken, da SOLID Dinge intrinsisch testbar macht.
  • Sie scheinen zu implizieren, dass der Umgebungszustand nicht SOLID ist, und dies scheint ein falscher Vergleich zu sein. Umgebungszustand ist Zustand; es kann hinter SOLID-Abstraktionen gekapselt werden.

Daraus müssen wir schließen, dass am Umgebungszustand _im Allgemeinen_ nichts auszusetzen ist. Der Umgebungszustand kann wie bei jedem Muster falsch verwendet werden, aber ich habe noch keine Argumente gesehen, die zuverlässig zeigen, dass der Umgebungszustand, der in der Kompositionswurzel gekapselt ist, schlecht ist.

Dies bringt mich zu dem Punkt, den ich hier zu sagen versuche: Es ist vernünftig, dass Sie sich dafür entscheiden, Umgebungskontexte in Ihrem eigenen DI-Container nicht zu unterstützen, aber bitte hören Sie auf zu sagen, dass der Umgebungszustand ein "schlechtes Muster im Allgemeinen" ist, ohne der Community zur Verfügung zu stellen alle vernünftigen Argumente, um Ihren Anspruch zu bestätigen. Sie sind eine führende Persönlichkeit in unserer Branche und viele werden Ihr Wort für bare Münze nehmen. Indem Sie unausgegorene Behauptungen ohne stichhaltige Argumente aufstellen, schaden Sie unserer Branche.

Es ist eine undichte Abstraktion. Sie haben Recht, Sie können die Auswirkungen reduzieren, indem Sie sie sicher aus einigen der Schichten ausblenden. Aber alles, was die Umgebungsdaten berücksichtigen muss, ist nicht testbar. Nur weil Sie es in einigen Szenarien ausblenden können, heißt das nicht, dass das Muster SOLID ist. Es zwingt alle zusammengesetzten Wurzeln im System, sich des Umgebungscontainers bewusst zu sein. Darüber hinaus kommt es je nach Ambiente zu seltsamen Macken wie diesen:

_Warnung: Ein Gültigkeitsbereich ist threadspezifisch. Ein einzelner Bereich sollte nicht über mehrere Threads hinweg verwendet werden. Übergeben Sie keinen Bereich zwischen Threads und umschließen Sie eine ASP.NET-HTTP-Anforderung nicht mit einem Lifetime-Bereich, da ASP.NET eine Webanforderung in einem anderen Thread als dem Thread beenden kann, auf dem die Anforderung gestartet wird._

_Aus den einfachen Injektor-Dokumenten._

Mein Punkt ist, selbst wenn Sie es innerhalb der Kompositionswurzel verstecken und die Exposition begrenzen können, folgt normalerweise ein skurriles Verhalten, da Sie mit dem Umgebungszustand die Fähigkeit verlieren, diesen Zustand als Teil des Vertrags auszudrücken, und dies hat manchmal unbeabsichtigte Konsequenzen.

Als erfundenes Beispiel:

```C#
mit (container.BeginScope())
{
var scope1Foo = container.Resolve();
mit (container.BeginScope())
{
var scope2Foo = container.Resolve();
// Wie kann ich hier aus dem äußeren Bereich auflösen?
}
}

```C#
using (var scope1 = container.BeginScope())
{
     var scope1Foo = scope1.Container.Resolve<IFoo>();
     using (var scope2 = container.BeginScope())
     {
          var scope2Foo = scope2.Container.Resolve<IFoo>();
          // How do I resolve from the outer scope here?
         var scope1Foo2 = scope1.Container.Resolve<IFoo>();
     }
}

Es ist viel klarer, was hier passiert, da der Objektverweis auf den bereichsbezogenen Container direkt verfügbar gemacht und nicht aus dem Code ausgeblendet wird.

Hallo David,

Wir können zustimmen, nicht zuzustimmen. Es ist eine undichte Abstraktion. Sie können es sicher aus einigen der Ebenen ausblenden. Aber alles, was die Umgebungsdaten berücksichtigen muss, ist nicht testbar. Nur weil Sie es in einigen Szenarien "verstecken" können, bedeutet dies nicht, dass das Muster in der Praxis SOLIDE ist.

Natürlich können wir einfach „nicht einverstanden“ sein, aber ich würde viel lieber eine Diskussion entwickeln, die zu einem sinnvollen Verständnis führt, basierend auf „soliden“ Argumenten, anstatt dass Sie – wieder – vorschnelle Aussagen machen.

  • Sie behaupten, dass der Umgebungszustand eine undichte Abstraktion ist. Der Umgebungszustand ist nur ein Zustand, er ist keine Abstraktion und kann daher keine Implementierungsdetails _lecken_. Wir können den Zustand hinter einer Abstraktion verstecken, wie ich vorschlage, aber eine solche Abstraktion verliert in keiner Weise Implementierungsdetails über die Existenz des zugrunde liegenden Umgebungszustands. Ein solcher Adapter kann mit oder ohne Umgebungszustand erstellt werden und der Verbraucher wird den Unterschied nie erkennen.
  • Sie behaupten – wieder –, dass alles, was den Umgebungszustand berührt, nicht testbar ist, aber selbst ein Adapter, der den Umgebungszustand verwendet, ist vollständig testbar. Nehmen wir zum Beispiel einen Adapter, der AsyncLocal<T> als Container für einen Umgebungszustand verwendet. Das isolierte Testen eines solchen Adapters ist trivial. Das gleiche gilt für DI-Container, die den Umgebungszustand verwenden; auch sie können getestet werden; Werfen Sie einfach einen Blick auf den Simple Injector-Quellcode und sehen Sie, wie sogar Teile, die den Umgebungszustand verwenden, vollständig getestet werden.
  • Ihre Behauptung, dass Umgebungsdaten hinter einer Abstraktion versteckt werden, ist nicht SOLID. Ich glaube, Sie haben die SOLID-Prinzipien missverstanden. Das ist nichts, wofür man sich schämen sollte, es gibt viele Entwickler, die sich ihrer nicht bewusst sind oder sie falsch interpretieren. Ich kann Ihnen wärmstens raten, Robert Martins ausgezeichnetes Buch Agile Principles, Patterns and Practices zu lesen. Nachdem Sie es gelesen haben, werden Sie vielleicht verstehen, dass das Verstecken von Implementierungsdetails hinter anwendungsspezifischen Abstraktionen genau das ist, was das Dependency Inversion Principle fördert.

Ihre Behauptung, dass Umgebungsdaten hinter einer Abstraktion versteckt werden, ist nicht SOLID. Ich glaube, Sie haben die SOLID-Prinzipien missverstanden. Das ist nichts, wofür man sich schämen sollte, es gibt viele Entwickler, die sich ihrer nicht bewusst sind oder sie falsch interpretieren. Ich kann Ihnen wärmstens raten, Robert Martins ausgezeichnetes Buch Agile Principles, Patterns and Practices zu lesen. Nachdem Sie es gelesen haben, werden Sie vielleicht verstehen, dass das Verstecken von Implementierungsdetails hinter anwendungsspezifischen Abstraktionen genau das ist, was das Dependency Inversion Principle fördert.

Danke für den Tipp.

@davidfowler

Ich brauche eine Klarstellung zu den Aussagen in diesem Thread. Bei den Bewerbungen, die ich schreibe, versuche ich, den SOLID-Prinzipien zu folgen. Ihre Aussagen in diesem Beitrag scheinen darauf hinzuweisen, dass ich etwas falsch mache. Ich hoffe, Sie können mir helfen, mein Anwendungsdesign zu verbessern.

Ich habe mehrere Beispiele für den Umgebungszustand in den von mir verwalteten Anwendungen. Ich dachte immer, dass ich SOLID folge, weil ich diesen Zustand hinter Abstraktionen verstecke. Ich hoffe, Sie können über mein Design nachdenken und mir zeigen, wo meine Abstraktionen undicht sind und wo dieser gebackene Zustand Probleme mit beispielsweise Unit-Tests verursacht.

Eines der einfachsten Beispiele, die ich in meiner Codebasis finden konnte, die auch in mehreren Teilen meiner Anwendungen weit verbreitet ist, ist ein Dienst, ein Adapter oder wie auch immer Sie es nennen möchten, um Informationen über den aktuellen Benutzer der Anfrage bereitzustellen.

Ich verwende eine Abstraktion, die der folgenden ähnelt:

``` c#
öffentliche Schnittstelle IUserContext
{
Zeichenfolge CurrentUser { get; }
}

Depending on the environment, I have these implementations amongst others:

``` c#
private sealed class WindowsUserContext : IUserContext
{
    public string CurrentUser => WindowsIdentity.GetCurrent().Name;
}

private sealed class HttpUserContext : IUserContext
{
    public string CurrentUser => HttpContext.Current.User.Identity.Name;
}

Tief unten im Objektgraphen brauche ich oft Zugriff auf den aktuellen Benutzer, zum Beispiel bei einer (Datenbank-)Operation. Dies sind Dinge wie Berechtigungsprüfung, Audit-Trail usw. Was Sie implizieren, zumindest ist das meine Interpretation, ist, dass der Laufzeitzustand durch das System fließen sollte, was meiner Meinung nach bedeutet, dass alle öffentlichen Methoden ein CurrentUser benötigen.

Aber ist die Weitergabe von CurrentUser durch den Aufrufgraphen nicht allein eine undichte Abstraktion? Es scheint jetzt, dass ein Teil meiner Anwendung, der vorher nichts über den aktuellen Benutzer wissen musste, jetzt plötzlich weiß.

Ich mache mir auch Sorgen über die weitreichenden Änderungen, die ich dadurch in meinem gesamten System vornehmen würde, jedes Mal, wenn eine Komponente auf niedriger Ebene plötzlich die Verwendung des aktuellen Benutzers erfordert. Wie würde ich diese weitreichenden Veränderungen verhindern?

Ich teste meine Anwendung auch gründlich, indem ich sowohl Unit- als auch Integrationstests verwende, und hatte nie zuvor Probleme, eine vollständige Abdeckung sowohl in den Verbrauchern dieser IUserContext Abstraktion als auch in den Adapterimplementierungen selbst zu erreichen. Können Sie mir helfen, wo die Verwendung einer solchen Abstraktion zu versagen beginnt und Probleme mit der Testbarkeit verursacht?

Ihre Gedanken und Kommentare interessieren mich sehr.

Tief unten im Objektgraphen brauche ich oft Zugriff auf den aktuellen Benutzer, zum Beispiel bei einer (Datenbank-)Operation. Dies sind Dinge wie Berechtigungsprüfung, Audit-Trail usw. Was Sie implizieren, zumindest ist das meine Interpretation, ist, dass der Laufzeitzustand durch das System fließen sollte, was meiner Meinung nach bedeutet, dass alle öffentlichen Methoden einen CurrentUser-Parameter benötigen.

Das ist super gemeinsame und das Muster , das Sie oben haben funktioniert und ist prüfbar. Aber lassen Sie mich Devils Advocate spielen, warum ist der aktuelle Benutzer etwas Besonderes? Warum erstellen Sie nicht eine Reihe anderer Dinge in Ihrem Systemambiente, damit Sie sich keine Sorgen machen müssen, sie herumzugeben? Sie können jedes dieser Dinge immer hinter einer anderen Schnittstelle verstecken, so dass der Konsum die Tatsache ignoriert, dass Sie Dinge über diesen Umgebungszustand übergeben, oder?

Aber ist die Übergabe des CurrentUser durch das Aufrufdiagramm nicht allein eine undichte Abstraktion? Es scheint jetzt, dass ein Teil meiner Anwendung, der vorher nichts über den aktuellen Benutzer wissen musste, jetzt plötzlich weiß.

Argumente an Dinge weiterzugeben, die diese Argumente erfordern, ist das Gegenteil von undicht. Es ist, als würde man sagen, dass Stornierungstoken undicht sind, wenn Sie sie an etwas weitergeben müssen, das abgebrochen werden muss (sie sind nervig, aber nicht undicht, sie sind ehrlich 😄).

Aber! Wie @dotnetjunkie sagt, sind Ihre Komponenten in Ordnung, da Sie sie ordnungsgemäß vom Umgebungszustand abstrahieren. Diese Komponenten selbst sind sehr gut testbar, da der Umgebungszustand ein Implementierungsdetail ist .

Das Argument , das ich machte war , dass es bestimmte Komponenten , die Notwendigkeit , mit der Tatsache zu tun , dass Umgebungszustand vorliegt (die gesamte Diskussion aus der Tatsache ergab sich, dass Umgebungs Bereiche der Standard in einfachen Injektor sind). Die "Kompositionswurzel" oder vielmehr alles, was selbst einen Bereich erstellen, übergeben oder verwalten muss, müsste direkt auf den Umgebungsbereich zugreifen. Schlimmer noch, es gibt Fälle, in denen Sie möglicherweise auf einen anderen Bereich zugreifen müssen (Anforderungsbereich vs. Transaktionsbereich vs. Threadbereich), und ohne Zugriff auf das Objekt können Sie nicht mehr auswählen, welcher Status Ihnen wichtig ist.

Jetzt sind wir vielleicht im Unkraut, wenn wir über "Infrastruktur" sprechen, aber jeder Code, der explizit GetService aufrufen muss, um einen Graphen von Abhängigkeiten zu erstellen, hat dieses Problem.

PS: Ich würde vorab von @dotnetjunkie über mich WRT diesen Bereich nehmen. Ich schreibe nur Frameworks 😉 , keinen echten Code.

Vielen Dank, dass Sie sich die Zeit genommen haben, meine Fragen so schnell zu beantworten.

Es gibt Fälle, in denen Sie möglicherweise auf einen anderen Bereich zugreifen müssen (Anforderungsbereich vs. Transaktionsbereich vs. Threadbereich) und ohne Zugriff auf das Objekt die Möglichkeit verloren haben, den gewünschten Status auszuwählen.

Es fällt mir schwer, mir konkrete Fälle vorzustellen, in denen ein Zugriff auf einen anderen Umfang erforderlich ist. Können Sie ein konkretes Beispiel geben, in dem der Zugang zu einem anderen Anwendungsbereich erforderlich ist?

Es fällt mir schwer, mir konkrete Fälle vorzustellen, in denen ein Zugriff auf einen anderen Umfang erforderlich ist. Können Sie ein konkretes Beispiel geben, in dem der Zugang zu einem anderen Anwendungsbereich erforderlich ist?

Überall, wo GetService aka im Kompositionsstamm aufgerufen werden muss. Ein gutes Framework erledigt dies normalerweise für Sie, irgendwo, damit Sie es nicht selbst tun müssen.

https://github.com/aspnet/Hosting/blob/dev/src/Microsoft.AspNetCore.Hosting/Internal/RequestServicesFeature.cs#L30

https://github.com/aspnet/SignalR/blob/dev/src/Microsoft.AspNetCore.SignalR/HubEndPoint.cs#L91

https://github.com/OrchardCMS/Orchard2/blob/85c9f7ecf1ca11dd58a1c58f92345d370e725889/src/Orchard.Environment.Shell.Abstractions/Builders/ShellContext.cs#L30

https://github.com/OrchardCMS/Orchard2/blob/982e149ff5e6e10e6e3af5725619754654adfb03/src/Orchard.Environment.Shell/Builders/ShellContainerFactory.cs#L137

@davidfowl

Ich habe die Codebasen, auf die sich Ihre Links beziehen, gründlich durchsucht, aber ich denke, ich vermisse hier etwas, da keines Ihrer verlinkten Beispiele zu zeigen scheint, wie auf einen anderen (äußeren oder übergeordneten) Bereich zugegriffen wird, während er in einem (inneren) ausgeführt wird ) Umfang.

Das wiederkehrende Muster scheint zu sein, dass ein ScopeFactory aus dem übergeordneten Bereich aufgelöst wird, verwendet wird, um ein neues Scope zu erstellen, und das resultierende Scope verwendet wird, um eine Komponente aufzulösen. Von diesem Punkt an kann ich keinen Zugriff auf einen anderen Bereich finden, bis der gerade erstellte Bereich freigegeben wird. Ich sehe keine problematischen Dinge, die von einem bestimmten spezifischen oder äußeren / übergeordneten Bereich gelöst werden müssen.

Das verwirrt mich wirklich. Was fehlt mir hier? Also zu deiner früheren Aussage:

Es gibt Fälle, in denen Sie möglicherweise auf einen anderen Bereich zugreifen müssen (Anforderungsbereich vs. Transaktionsbereich vs. Threadbereich) und ohne Zugriff auf das Objekt die Möglichkeit verloren haben, den gewünschten Status auszuwählen.

Können Sie erläutern, wie die vorgestellten Beispiele das Problem zeigen, das Sie in dieser Aussage beschrieben haben?

Können Sie erläutern, wie die vorgestellten Beispiele das Problem zeigen, das Sie in dieser Aussage beschrieben haben?

Ich habe die Beispiele nicht durchgesehen, um zu zeigen, wie auf "äußere" Bereiche von inneren Bereichen zugegriffen wird. Ich bin mir nicht sicher, ob sie das in den obigen Beispielen tun. Mein Punkt war lediglich, dass der Umgebungszustand das viel schwieriger macht, da Sie den Zustand vollständig vor dem Verbraucher verborgen haben. Wenn der Bereich explizit ist, können Sie ihn einfach explizit weitergeben, ohne sich Gedanken darüber machen zu müssen, wohin er möglicherweise durchsickert oder nicht.

@davidfowl

Nachdem ich versucht habe, Ihre neueste Reaktion zu verdauen, kam ich zu dem Schluss, dass der Unterschied in unserem Arbeitsfeld (Framework-Entwickler vs. Anwendungsentwickler) wahrscheinlich zu groß ist, um die Probleme, Fragen und Designherausforderungen des anderen klar zu verstehen.

Wenn der Bereich explizit ist, können Sie ihn einfach explizit weitergeben, ohne sich Gedanken darüber machen zu müssen, wohin er möglicherweise durchsickert oder nicht.

Als Anwendungsentwickler würde ich dies mit folgenden Überlegungen bedenken: Ein Design, bei dem wir ein vom Framework definiertes Scope-Objekt explizit über den Anwendungscode weitergeben, würde die SOLID-Prinzipien verletzen und ist nur ein Fall einer fehlenden Abstraktion. Da ich aber selbst keine Erfahrung mit dem Bau von Frameworks habe, bin ich mir nicht sicher, ob eine solche Aussage beim Bauen von Frameworks gilt oder nicht.

Bis jetzt habe ich keine überzeugenden Argumente gelesen, die mich glauben lassen, dass Ambient State eigentlich „schlecht im Allgemeinen“ ist und nicht verwendet werden sollte, besonders wenn er _innerhalb_ der Kompositionswurzel gehalten wird. Es mag besondere Situationen geben, vielleicht gerade im Zusammenhang mit dem Schreiben von Frameworks, wo es bessere Lösungen im Gegensatz zur Verwendung des Ambient-Zustands gibt, aber an dieser Stelle komme ich zu dem Schluss, dass an meinem aktuellen _Anwendungs-Design schließlich nichts auszusetzen ist.

Ich glaube auch, dass an DI-Containern, die einen Umgebungszustand wie Castle Windsor und Simple Injector verwenden und bereitstellen, nichts auszusetzen ist. Beide Bibliotheken werden von zwei hochqualifizierten Fachleuten entworfen und unterstützt, die beide aktiv SOLID und gut gestalteten Code fördern, und es wäre eine große Disqualifikation zu sagen, dass ihre Bibliotheken Anwendungsentwickler zwingen, gegen gute Designprinzipien zu verstoßen. Ich sehe überhaupt kein Problem in der Verwendung eines Containers im Kompositionsstamm, der immer die Anleitung war.

Man sollte immer das beste Werkzeug für die jeweilige Aufgabe wählen und in meinem aktuellen Design leistet die Verwendung von Ambient State, gekapselt im Composition Root, einen wirklich guten Job.

Vielen Dank für Ihre Zeit und Geduld.

Kein Problem.

Obwohl David Fowler die Verwendung von Ambient State nicht mag, ist es aus der vorherigen Diskussion wichtig zu betonen, dass an der Verwendung von Ambient State, die in der Kompositionswurzel gekapselt ist, nichts von Natur aus falsch ist . Umgebungszustand, wenn in der Kompositionswurzel gekapselt:

  • Ist komplett prüfbar.
  • Verletzt nicht SOLID.
  • Leckt nicht.
  • Wird nicht dazu führen, dass wir "die Fähigkeit verlieren, diesen Zustand als Teil des Vertrags auszudrücken".

Ich werde jeden der oben genannten Punkte im Folgenden genauer besprechen.

### Umgebungszustand, wenn in der Kompositionswurzel gekapselt: Ist vollständig testbar.

Wie bereits erwähnt und sogar von David als Antwort auf @TheBigRic bestätigt , kann die Verwendung des Umgebungszustands getestet werden, und wenn der Umgebungszustand hinter SOLID-Abstraktionen verborgen ist, können die Verbraucher dieser Abstraktionen getestet werden, ohne dass Instanzen des Umgebungszustands erforderlich sind.

### Umgebungszustand, wenn in der Kompositionswurzel gekapselt: Verletzt nicht SOLID

Umgebungszustand ist Zustand. State kann (und sollte) hinter Adaptern gekapselt werden, die den SOLID-Prinzipien folgen.

### Umgebungszustand, wenn in der Kompositionswurzel gekapselt: Leckt nicht.

Die Übergabe des Laufzeitzustands durch verschiedene Methodenaufrufe des Objektgraphen - wie David bereits vorgeschlagen hat - führt dazu, dass Abstraktionen Implementierungsdetails verlieren. Schauen Sie sich zum Beispiel die folgende Abstraktion mit einer einfachen Implementierung an:

``` c#
öffentliche Schnittstelle ICustomerRepository
{
void Speichern(Kundeneinheit);
}

öffentliche Klasse SqlCustomerRepository : ICustomerRepository
{
private readonly IUnitOfWork uow;

public CustomerRepository(IUnitOfWork uow) {
    this.uow = uow;
}

public void Save(Customer entity) {
    this.uow.Save(entity);
}

}

Imagine that we need to alter this `SqlCustomerRepository` in such way that when a `Customer` is saved we should append this action to the audit trail. The audit trail should contain the `Id` of the current user. Such an implementation may look like this:

``` c#
public class SqlCustomerRepository : ICustomerRepository
{
    public void Save(Customer entity) {
        this.uow.Save(entity);
        this.uow.Save(new AuditTrail {
            EntityId = entity.Id,
            Type = "Save",
            CreatedOn = DateTime.Now,
            CreatedBy = ???
        });
    }
}

Das Problem dabei ist, dass Customer keine Informationen über den aktuellen Benutzer enthält und da Customer eine Entität ist, würde es nicht viel Sinn machen, Details des aktuellen Benutzers hinzuzufügen. Wenn wir Davids Rat befolgen, würden wir die ICustomerRepository Benutzeroberfläche wie folgt ändern:

``` c#
öffentliche Schnittstelle ICustomerRepository
{
void Save(Kundenentität, Guid currentUserId);
}

This minor alteration has caused sweeping changes to ripple through the call stack, since all direct and indirect consumers will need to pass this value on. This causes many other application abstractions to be altered in favor of this 'minor change'. In SOLID terminology: **this is both an Open/Closed Principle violation and a Dependency Inversion Principle violation.**

Now, imagine that we did go through the painful process of making these sweeping changes through the application and then another request comes in: a change that asks us to store the `TenantId` in the audit table! We again have to change our `ICustomerRepository` abstraction. This approach is clearly leaking implementation details and is unmaintainable in the end.

If, however, we were to inject an `IUserContext` into the `SqlCustomerRepository`, these problems go away completely:

``` c#
class CustomerRepository : ICustomerRepository
{
    private readonly IUnitOfWork uow;
    private readonly IUserContext userContext;

    public CustomerRepository(IUnitOfWork uow, IUserContext userContext) {
        this.uow = uow;
        this.userContext = userContext;
    }

    public void Save(Customer entity) {
        this.uow.Save(entity);
        this.uow.Save(new AuditTrail {
            EntityId = entity.Id,
            Type = "Save",
            CreatedOn = DateTime.Now,
            CreatedBy = this.userContext.UserId
        });
    }
} 

Die Verwendung von IUserContext verhindert, dass sich Änderungen durch das System verbreiten und ob IUserContext.UserId mithilfe des Umgebungszustands abgerufen wird oder nicht, ist ein Implementierungsdetail der zugrunde liegenden IUserContext Implementierung, die von kein Interesse an den SqlCustomerRepository .

### Umgebungszustand, wenn er in der Kompositionswurzel gekapselt ist: Wird nicht dazu führen, dass wir "die Fähigkeit verlieren, diesen Zustand als Teil des Vertrags auszudrücken".

Die Aussage, dass wir dies nicht zum Vertragsinhalt machen können, ist unbegründet. Andererseits; wir bekommen mehr optionen. Falls wir feststellen, dass Kontextinformationen ein Implementierungsdetail sind, können wir sie hinter Abstraktionen kapseln, um zu verhindern, dass Änderungen durch das System übertragen werden. Sollten wir jedoch feststellen, dass diese Laufzeitdaten Bestandteil des öffentlichen Auftrags der Anwendung sein sollten, können wir diese dennoch so gestalten. Niemand hat gesagt, dass alle Informationen im Umgebungszustand gespeichert werden sollten und nichts durch Methodenaufrufe weitergegeben werden sollte.

Eines der Argumente, die David verwendete, war, dass "es alle zusammengesetzten Roots im System dazu zwingt, sich des Umgebungscontainers bewusst zu sein". Eine Kompositionswurzel ist der Einstiegspunkt der Anwendung und kennt daher per Definition jeden Aspekt der Anwendung. Es verfügt über intrinsische Kenntnisse über den ausgewählten DI-Container, das Anwendungsframework (zB ASP.NET Core MVC) und die Plattform, auf der die Anwendung ausgeführt wird. Es ist kein Problem für die Kompositionswurzel, sich des Umgebungszustands bewusst zu sein.

Es kann sein, dass es bei der Verwendung von _ambient scoping_ schwieriger wird, auf den übergeordneten Bereich zuzugreifen, während Sie sich im Kontext eines verschachtelten Bereichs befinden, wie Davids konstruiertes Beispiel zeigt. Es ist jedoch ein sehr ungewöhnlicher Anwendungsfall und es ist eigentlich ziemlich schwierig, ein gültiges Szenario für so etwas zu entwickeln. Obwohl der .NET Core DI-Container dieses Szenario zulässt, ist es sehr unwahrscheinlich, dass derzeit jemand dieses "Feature" verwendet. David zeigte 4 Beispiele, die alle der „normalen“ Arbeitsweise folgen; ein Weg, den Simple Injector und Castle Windsor ebenfalls unterstützen. Ich würde sagen, dass diese „Einschränkung“ nicht existiert. Das versehentliche Auflösen von Diensten aus einem übergeordneten Bereich ist sogar eine sehr häufige Fehlerquelle in Anwendungen, die DI-Container verwenden.

## Abschluss

Sie sollten den Anwendungscode auf keinen Fall direkt vom Umgebungszustand abhängen lassen, genauso wie Sie den Anwendungscode nicht direkt vom DI-Container abhängen lassen sollten. Der Umgebungszustand ist jedoch, wenn er in der Kompositionswurzel gekapselt ist, testbar, wartbar und verletzt keines der SOLID-Prinzipien und ist daher nicht "im Allgemeinen schlecht".

Eine Umwegschicht löst alles in der Informatik 🤣. Sie haben Recht, wir können den Umgebungszustand hinter einer Abstraktion verstecken, mit Ausnahme der Dinge, die sich darum kümmern müssen, aber TBH, das ist keine Erkenntnis.

Danke für die Diskussion, meine Herren.

Bearbeiten:

IMO sollte der Umgebungszustand vom Container selbst entkoppelt werden und die Framework-Infrastruktur sollte entscheiden, wie Objekte affiniert werden. Bei den meisten Containern können Sie entweder die Funktionsweise des internen Bereichs über eine Scope-Provider-Schnittstelle ändern oder noch einfacher, indem Sie jede Vorstellung von Umgebung vermeiden und dies dem Aufrufer überlassen, was IMO die einfachere Sache ist.

War diese Seite hilfreich?
0 / 5 - 0 Bewertungen