Table of Contents

FSServiceProvider

(siehe auch FSServiceProvider)

Im Folgenden werden spezielle Situationen für den Einsatz des FSServiceProvider beschrieben.

Zugriff auf Services

In jedem Request und jeder Service-Methode wird ein Service-Scope erstellt. Auf diesen kann mit FSServiceProvider.Current zugegriffen werden.

if (FSServiceProvider.Current.GetRequiredService<IFSAuthorization>().Granted(myAccessUnit))
{
   ...

Wird ein Service in einem Code mehrfach verwendet, ist es sinnvoll die Instanz in eine lokale Variable zu speichern.

var printApi = FSServiceProvider.Current.GetRequiredService<IPrintServerApi>();
foreach(var printServer in printApi.GetPrintServers())
{
    foreach(var printer in printApi.GetPrinters(printServer.PrintServerId))
    {
        // ...
    }
}
Caution

Ein ermittelter Service darf niemals in einem Property gespeichert werden.

Die längerfristige Speicherung eines Service-Objekts untergräbt das DI-Konzept und kann schwerwiegende Fehler verursachen, wie z.B. falsche Lebensdauern, Memory-Leaks oder inkonsistente Zustände. Services sollten immer nur lokal und für die unmittelbare Verwendung abgefragt werden.

Eigener Scope

Jeder (normale) FS-Code läuft standardmäßig in einem DI-Scope, der durch den Broker-Request oder die Service-Methode erstellt wird. Dieser Scope organisiert unter anderem Datenbankverbindungen und die Benutzer-Authentifizierung.

Soll Code in einem separaten Scope ausgeführt werden, kann dies mit dem FSServiceProvider umgesetzt werden.

Anmerkung: Früher wurde hierfür der GlobalContext verwendet. Das Konzept des FSServiceProvider-Scopes baut darauf auf und erweitert es.

Ein eigener Scope wird mit FSServiceProvider.CreateScope() erzeugt:

using (FSServiceProvider.CreateScope())
{
    // In diesem Block steht ein eigener DI-Scope zur Verfügung.
}

Die Verwendung in einem using-Block ist zwingend erforderlich, damit der Scope korrekt bereinigt wird und keine Memory-Leaks entstehen.

Caution

Innerhalb des using-Blocks dürfen keine Tasks oder Threads gestartet werden, die über die Lebensdauer des Blocks hinaus aktiv sind.

Hintergrund / Exkurs:

Mit der Methode CreateScope() wird ein neuer ServiceProvider-Scope erzeugt und dieser mithilfe eines AsyncLocal über FSServiceProvider.Current bereitgestellt.

Das AsyncLocal stellt sicher, dass auch Tasks und Threads, die im aktuellen Ausführungskontext gestartet werden, auf diesen Scope über FSServiceProvider.Current zugreifen können. Dieser Mechanismus entspricht dem, der bereits im GlobalContext verwendet wird.

CreateScope() gibt ein IDisposable-Objekt zurück, sodass die Verwendung in einem using-Block möglich ist. Beim Bereinigen des Objekts am Ende des Blocks werden sowohl der ServiceProvider-Scope als auch der Wert von AsyncLocal bzw. FSServiceProvider.Current zurückgesetzt.

Beides zusammen hat zur Folge, dass ein Task / Thread zwar auf das AsyncLocal auf den ServiceProvider-Scope zugreifen kann, der Ende des using-Blocks diesen aber zerstört.

Wie diese Mechanismen unabhängig voneinander gesteuert werden können, wird weiter unten im Abschnitt Asynchrone Operationen erläutert.

Eigener Scope mit Global

CreateScope() kann auch mit einem GlobalObject aufgerufen werden. Das hat denselben Effekt wie using(GlobalContext.Context(global)).

using (var globalObj = FS.Hosting.Broker.Base.GlobalObjectManager.CreateGlobalObject(guid.NewGuid().Value))
using (FSServiceProvider.CreateScope(globalObj))
{
    // FSServiceProvider.Current....
    // GlobalContext.Current...
}

Das folgende Beispiel zeigt eine Umstellung von GlobalContext auf FSServiceProvider:

    using (var globalObj = FS.Hosting.Broker.Base.GlobalObjectManager.CreateGlobalObject(guid.NewGuid().Value))
-   using (GlobalContext.Context(globalObj))
+   using (FSServiceProvider.CreateScope(globalObj))
 {
    //...

Asynchrone Operationen

Important

Das hier beschriebene Vorgehen sollte nur in Ausnahmefällen angewendet werden. Vorher muss intensiv ein Refactoring geprüft werden.

Der Abschnitt baut auf den oben beschriebenen eigenen Scope auf.

Im folgenden Beispiel wird ein Worst-Case-Szenario aufgezeigt:

var globalObj = FS.Hosting.Broker.Base.GlobalObjectManager.CreateGlobalObject(guid.NewGuid().Value);
IUserControlledScope ucServiceScope = FSServiceProvider.CreateUserControlledScope();
bool ucScopeDisposedByThread = false;
try
{
    using (FSServiceProvider.UseScope(ucServiceScope, globalObj))
    {
        // Aktionen im aktuellen ServiceProvider-Scope / GlobalContext
        GlobalContext.Current.ocGlobal.SetBusinessUnit(oBusinessUnitP.sUnitID);
        IcReplicationOrderProcessor processor = cReplicationOrderProcessorFactory.Create();
        // ...

        if (processor.ThreadInit())
        {
            System.Threading.Thread thread = new(() =>
            {
                // das using stellt sicher, dass am Ende des Threads - auch bei Exceptions - ein Dispose erfolgt.
                using(globalObj)
                using (ucServiceScope)
                {
                    processor.Process();
                }
            });
            thread.IsBackground = true;
            thread.Start();
            ucScopeDisposedByThread = true;
        }
    }
}
finally
{
    // Falls der Thread nicht gestartet wurde, muss manuell ein Dispose ausgeführt werden.
    // Das trifft auch im Falle eine Exception zu.
    if (!ucScopeDisposedByThread)
    {
        ucServiceScope.Dispose();
        globalObj?.Dispose();
    }
}

In diesem Beispiel werden sowohl ein Scope als auch ein GlobalObject explizit für einen Thread verwaltet. Die Lebensdauer dieser Objekte muss manuell gesteuert werden, um Memory-Leaks und inkonsistente Zustände zu vermeiden. Solche Konstrukte sollten nur verwendet werden, wenn keine Alternative besteht und die Auswirkungen vollständig verstanden sind.

Zunächst wird mit FSServiceProvider.CreateUserControlledScope() ein benutzerkontrollierter Scope (IUserControlledScope) erzeugt.

Mit FSServiceProvider.UseScope(IUserControlledScope, IGlobalObjects) wird innerhalb des using-Blocks sichergestellt, dass FSServiceProvider.Current und der GlobalContext auf die korrekten Instanzen gesetzt werden. Im Gegensatz zum normalen Scope erfolgt am Ende des Blocks keine automatische Bereinigung des Scopes, sondern lediglich ein Zurücksetzen von FSServiceProvider.Current und GlobalContext auf die ursprünglichen Werte.

Die Bereinigung des benutzerkontrollierten ServiceProvider-Scopes und des GlobalObjects muss explizit durch einen Aufruf von Dispose() am jeweiligen Objekt erfolgen. Dies sollte immer durch einen try-finally-Block oder ein using(...) sichergestellt werden, damit Ressourcen zuverlässig freigegeben werden und keine Memory-Leaks entstehen.