Tipp Sitzung erstellen | Mehrbenutzer / Mehrmandanten

Finn

Neues Mitglied
Guten Tag zusammen,

wir alle wissen, dass die Arbeit mit Sage-Schnittstellen eine steile Lernkurve bedeuten kann – besonders wenn man, wie ich, aus einer anderen Programmiersprache kommt. Da hilfreiche Ressourcen oft schwer zu finden sind, habe ich mich intensiv in die Materie eingearbeitet und eigene Lösungen entwickelt.

Um anderen die zeitintensive Suche und die damit verbundene Frustration zu ersparen, stelle ich meine Ergebnisse gerne zur Verfügung. Ich hoffe, dass diese Impulse für eure Projekte hilfreich sind und freue mich auf den Austausch.

Der nachfolgende Code zeigt eine Helper-Klasse, die den Aufbau einer neuen Sage-Session zentralisiert. Mit dieser Grundlage könnt ihr – wie im Anschluss an das Code-Beispiel detailliert beschrieben – den entsprechenden Mandanten initialisieren und ansteuern:

C#:
using Sagede.Core.Tools;
using Sagede.OfficeLine.Engine;
using Sagede.OfficeLine.Shared;
using System;

namespace HinriFlow.Sage.Core.Services.Infrastructure
{
    /// <summary>
    /// Eine versiegelte Singleton-Klasse zur zentralen Erzeugung und Verwaltung
    /// von Sage 100 Engine-Sessions.
    /// </summary>
    public sealed class SageSessionFactory : IDisposable
    {
        // Thread-sichere Lazy-Initialisierung der Singleton-Instanz.
        // Das 'true' im Konstruktor stellt sicher, dass die Erzeugung thread-safe ist.
        private static readonly Lazy<SageSessionFactory> _instance =
            new(() => new SageSessionFactory(), true);

        private Session _session;
        private readonly object _sync = new();

        // Statische Felder zur Speicherung der Verbindungsdaten aus der ENV-Konfiguration.
        // Diese werden einmalig beim Laden der Klasse initialisiert.
        private static readonly string MandantDbName = ENV.Current.MandantDBName;
        private static readonly string SageUsername = ENV.Current.SageUsername;
        private static readonly string SagePassword = ENV.Current.SagePassword;

        /// <summary>
        /// Privater Konstruktor verhindert die manuelle Instanziierung außerhalb der Klasse.
        /// Initialisiert die interne Session direkt beim Erstellen der Factory.
        /// </summary>
        private SageSessionFactory()
        {
            _session = Create();
        }

        /// <summary>
        /// Zugriff auf die globale Singleton-Instanz der Factory.
        /// </summary>
        public static SageSessionFactory Instance => _instance.Value;

        /// <summary>
        /// Erstellt eine neue Instanz einer Sage <see cref="Session"/> mit den konfigurierten Anmeldedaten.
        /// </summary>
        /// <returns>Eine aktive Sage 100 Session.</returns>
        /// <exception cref="InvalidOperationException">
        /// Wird ausgelöst, wenn die Verbindung zur Sage Engine (z. B. wegen falscher Credentials) fehlschlägt.
        /// </exception>
        public static Session Create()
        {
            try
            {
                // Aufruf der Sage Office Line Engine zur Session-Erstellung.
                // Parameter: Mandant, App-Token (Wawi/Abf), Trace-File, Credentials, Verhalten.
                return ApplicationEngine.CreateSession(
                    MandantDbName,
                    ApplicationToken.Abf,
                    null,
                    new NamePasswordCredential(SageUsername, SagePassword),
                    SessionBehavior.Client
                );
            }
            catch (Exception ex)
            {
                // Protokollierung des Fehlers im Standard-Error-Stream
                Console.Error.WriteLine("[SageSessionFactory] Sage 100 Login fehlgeschlagen: " + ex.Message);

                // Weitergabe als InvalidOperationException, um den Aufrufer über das Scheitern zu informieren
                throw new InvalidOperationException("Sage 100 Login fehlgeschlagen", ex);
            }
        }

        /// <summary>
        /// Gibt die von der Factory gehaltenen Ressourcen (die interne Session) frei.
        /// Implementiert das <see cref="IDisposable"/> Pattern.
        /// </summary>
        public void Dispose()
        {
            // Lock verhindert Race-Conditions während des Dispose-Vorgangs
            lock (_sync)
            {
                if (_session != null)
                {
                    _session.Dispose();
                    _session = null;
                }
            }
        }
    }
}

Um auf Basis dieser Klasse eine neue Sitzung zu instanziieren und den gewünschten Mandanten anzusprechen, könnt ihr die folgende Logik in euren Code einbinden:

C#:
using (var session = SageSessionFactory.Create())
{
    /**
     * Die Sitzung wird automatisch nach dem durchlaufen wieder freigegeben.
     */
    var mandantenNr = mandantNr ?? _mandant;

    Mandant mandant = session.CreateMandant(mandantenNr);
}


C#:
var session = SageSessionFactory.Create()

var mandantenNr = mandantNr ?? _mandant;

Mandant mandant = session.CreateMandant(mandantenNr);

/**
 * Wichtiger Hinweis:
 * Nach Abschluss der Verarbeitung muss die Sitzung wieder
 * ordnungsgemäß freigegeben werden, um Systemressourcen zu schonen und die
 * Lizenzbelegung innerhalb der Sage 100 korrekt zu beenden.
 */
 
session.Dispose();

Oder wie wir es verwenden:

C#:
using HinriFlow.Sage.Core.Services.Infrastructure;
using Sagede.OfficeLine.Engine;
using Sagede.Shared.RealTimeData.Common;
using System;
using System.Threading.Tasks;

namespace HinriFlow.Sage.Core.Services.Bases
{
    /// <summary>
    /// Abstrakte Basisklasse für Dienste, die Operationen im Kontext eines Sage-Mandanten ausführen.
    /// Sie automatisiert das Erstellen von Sessions, die Initialisierung des MandantScopes
    /// und die anschließende Ressourcenfreigabe.
    /// </summary>
    public abstract class ExecuteMandantBase
    {
        protected readonly short _mandant;
        private static readonly object _sessionLock = new object();

        /// <summary>
        /// Initialisiert eine neue Instanz der Basisklasse mit einer Standard-Mandantennummer.
        /// </summary>
        /// <param name="mandant">Die Standard-Mandantennummer (z.B. aus der Konfiguration).</param>
        protected ExecuteMandantBase(short mandant)
        {
            _mandant = mandant;
        }

        /// <summary>
        /// Erstellt einen <see cref="MandantScope"/> für den übergebenen Mandanten.
        /// Wichtig für die korrekte Initialisierung von Kontext-Informationen in der Sage-Engine.
        /// </summary>
        protected static MandantScope BenutzeMandant(Mandant mandant)
        {
            return MandantScope.FromMandant(mandant);
        }

        /// <summary>
        /// Führt eine asynchrone Logik innerhalb einer isolierten Sage-Session und eines Mandanten-Scopes aus.
        /// Garantiert das Schließen der Session nach Abschluss (auch im Fehlerfall).
        /// </summary>
        /// <typeparam name="TResult">Der Rückgabetyp der auszuführenden Logik.</typeparam>
        /// <param name="mandantNr">Optionale Mandantennummer. Falls null, wird der Standard-Mandant verwendet.</param>
        /// <param name="work">Die eigentliche Geschäftslogik als Delegate.</param>
        protected async Task<TResult> ExecuteWithMandant<TResult>(
            short? mandantNr,
            Func<Mandant, ApplicationContext, Task<TResult>> work)
        {
            Session session;

            // Thread-sicheres Erzeugen einer neuen Session über die Factory
            lock (_sessionLock)
            {
                session = SageSessionFactory.Create();
            }

            try
            {
                // Mandanten-Instanz erzeugen
                var nr = mandantNr ?? _mandant;
                Mandant mandant = session.CreateMandant(nr);

                // MandantScope sicherstellen (wichtig für UI-Elemente und bestimmte Engine-Funktionen)
                using var scope = BenutzeMandant(mandant);

                // Ausführung der übergebenen Logik
                return await work(scope.Mandant, scope.AppContext).ConfigureAwait(false);
            }
            finally
            {
                // Lebenswichtig: Session freigeben, um Lizenzbelegung in Sage zu beenden
                session?.Dispose();
            }
        }
    }
}
 
Zuletzt bearbeitet:
In naher Zukunft plane ich, diesen Beitrag um weitere praxisrelevante Beispiele zu ergänzen. Dabei werde ich unter anderem auf folgende Themen im Detail eingehen:

  • Lagerbewegungen: Umsetzung von Prozessen wie Zugangsmeldungen (ZM), Entnahmemeldungen (EM) oder innerbetrieblichen Umbuchungen (IU).
  • Belegbuchungen: Erstellung von Belegen inklusive Positionserfassung für unterschiedliche Artikeltypen.
  • Bestandsführung: Handhabung von Standardartikeln sowie die Besonderheiten bei der Arbeit mit seriennummern- oder chargenpflichtigen Artikeln.
Ich hoffe, dass diese kommenden Leitfäden euch ebenfalls eine wertvolle Hilfestellung bei der Arbeit mit der Sage-Engine bieten.
 
Zurück
Oben