Übersetzungen

"ba98d63f-8b15-4b68-852f-102cbeb70d6e";"Service hinzufügen (Massenverarbeitung)";"Add service (Mass worker)";"";"";"";"False";"0"
"281e4152-6d03-40b4-9971-10728e673a00";"Services hinzufügen";"Add services";"";"";"";"False";"0"
"61bca157-96a6-436b-8719-0521b98462b9";"Starte Prozess";"Start process";"";"";"";"False";"0"
"227b79dd-31f2-4f6d-a255-94eb8aca0e89";"Neustart Prozess";"Restart process";"";"";"";"False";"0"
"03a9ae68-226f-4f19-91cd-47fa4370230b";"{0} zu verarbeiten";"{0} to process";"";"";"";"False";"0"
"e3d32867-83eb-4514-abec-c7e596ae192b";"{0} von {1} verarbeitet";"{0} of {1} processed";"";"";"";"False";"0"
"1cba5efa-7842-4764-abb8-05e2fab5bde3";"Maschine {0} wird verarbeitet.";"Engine {0} processing";"";"";"";"False";"0"
"0dd299df-c558-44fd-86ec-b3b2acd47530";"Keine Bearbeitungrechte für {0}";"No edit rights for {0}";"";"";"";"False";"0"
"cf6889ab-e880-42c9-8755-bfb15f9f01af";"Temporärer Datensatz wurde nicht gefunden: {0} / {1}";"Temporary record not found: {0} / {1}";"";"";"";"False";"0"
"160c2db4-e013-4569-8ab1-a37bdf776707";"Meine Ausnahme";"My exception";"";"";"";"False";"0"

Hintergrundprozess

Worker

namespace BA.Training.Worker
{
    /// <summary>
    /// Dieser Hintergrundprozess verarbeitet die selektierten Datensätze. Diese wurden vom Igniter in die temporäre Tabelle geschrieben. 
    /// Über die <see cref="TaskExecutionId"/> können die Datensätze ermittelt werden.
    /// 
    /// Wenn eine Verarbeitung von Datensätzen selbst implementiert wird. Muss beim Konzept beachtet werden, das der Prozess auf Fehler stoßen 
    /// und die Verarbeitung beispielsweise durch Herunterfahren der Anwendung unterbrochen werden kann.
    /// 
    /// In diesem Beispiel ist die zu verarbeitende Menge durch die Einträge in der temporären Tabelle definiert. Wenn ein Datensatz verarbeitet
    /// worden ist, wird der enstprechende Eintrag in der temporären Tabelle gelöscht.
    /// 
    /// Durch das Verwenden der <see cref="UnitOfWork"/> wird sichergestellt, das das Löschen des Eintrags und die Verarbeitung eine Transaktion ist.
    /// </summary>
    public class AddServiceWorker : WorkItemBase
    {
        // Id der Datensätze in der temporären Tabelle
        public Guid TaskExecutionId { get; set; }

        // Die zu setzende Bemerkung
        public string Remark { get; set; }

        // Das zu setzende Datum
        public DateTime Date { get; set; }

        // Zähler für Datensätze bei denen die Rechte fehlen
        public int MissingRights { get; set; }

        public AddServiceWorker() : base()
        {
            // Die erwartete Laufzeit ist land. Damit die entsprechende Queue genutzt wird.
            ExpectedRunTime = EnumExpectedRunTime.Long;
        }

        /// <summary>
        /// Implementierung des Prozesses. Wird bei Jedem Start aufgerufen
        /// </summary>
        protected override void Run()
        {
            // Abfrage auf die temporäre Tabelle
            IQueryable<OrmTempSelectedRecords> query = Api.ORM.GetQuery<OrmTempSelectedRecords>(UnitOfWork);
            query = query.Where(ff => ff.TaskExecutionId == TaskExecutionId);

            // Maximale Anzahl setzen. Wenn sie größer als 0 ist. Wird der Prozess neugestartet und der Count der Abfrage enthählt nicht 
            // mehr die schon verarbeiteten Datensätze.
            if (MaxProgress == 0)
                MaxProgress = query.Count();

            // Abfrage der zu verarbeitenden Maschinen
            IQueryable<Guid> oids = query.Select(ff => ff.SelectedRecordOid);
            IQueryable<OrmEngine> engines = Api.ORM.GetQuery<OrmEngine>(UnitOfWork).Where(ff => oids.Contains(ff.Oid));
            foreach (OrmEngine engine in engines)
            {
                // Abprüfung der Rechte
                if (engine.IsAllowed(EnumTableOperations.Edit))
                {
                    // Verarbeitung des aktuellen Datensatzes
                    OrmSubEngineServices service = engine.Services.AddNewObject();
                    service.SortOrder = engine.Services.Count() - 1;
                    service.ServiceDate = Date;
                    service.Remark = Remark;
                    engine.Save();
                }
                else
                    MissingRights++;

                // Löschen des temporären Eintrages
                query.Where(ff => ff.SelectedRecordOid == engine.Oid).FirstOrDefault()?.Delete();

                // Exception, wenn der dritte Datensatz im ersten Durchlauf verarbeitet wird.
                if (CurrentProgress == 2 && NumberOfStarts == 1)
                    throw new Exception(Logger.Translate("160C2DB4-E013-4569-8AB1-A37BDF776707"));

                // Zähler für die verarbeiteten Datensätze
                CurrentProgress++;
                Save();
            }
        }

        /// <summary>
        /// Prozess ist beendet und dem Anwender wird eine Nachricht zugestellt.
        /// </summary>
        protected override void WorkItemFinished()
        {
            if (State == EnumWorkItemState.Finished)
                if (MissingRights == 0)
                    // Der Anwender hatte auf alle Datensätze ausreichend Rechte
                    Api.ClientCommunication.CreateSuccess(Api.User.CurrentUserGuid(), "dd1ceadc-6808-4583-b6b7-dda73188b5a8".Translate(CurrentProgress));
                else
                    // Auf eineige Datensätze hatte der Anwender nicht genügend Rechte
                    Api.ClientCommunication.CreateSuccess(Api.User.CurrentUserGuid(), "e3f652e7-8019-4444-9e6c-f3c30528e862".Translate(CurrentProgress - MissingRights, MissingRights));

            base.WorkItemFinished();
        }
    }
}

Igniter-Änderung

Die temporären Datensätze müssen angelegt werden.

CreateTemporaryRecordsIndicator = true;

Anstatt der Verarbeitung, wird nun der Hintergrundprozess erstellt.

public override ActionResult Ignite()
{
    if (SomethingSelected)
    {
        if (IgnitionModel.Parameters.TryGetValue("RemarkMessage", out object remarkObj) && remarkObj is string remark && !string.IsNullOrWhiteSpace(remark))
        {
            DateTime date;
            if (IgnitionModel.Parameters.TryGetValue("ServiceDate", out object dateObj) && dateObj is DateTime)
                date = (DateTime)dateObj;
            else
                date = DateTime.Now;

            Api.Worker.CreateOrUpdate(new AddServiceWorker
            {
                TaskExecutionId = TaskExecutionId,
                Remark = remark,
                Date = date
            });
        }
        else
            Api.ClientCommunication.CreateError(Api.User.CurrentUserGuid(), "d49ef787-3ad1-4f66-bb12-b2660fd71738".Translate());
    }

    return null;
}

Antworten

Warum wurde ein Anwendungsprotokoll geschrieben? Und wie kann man es verhindern?
Der Vorgabewert von LoggingMode ist WorkerLoggingMode.Auto, damit ist dem System gestattet im Zweifelsfall ein Protokoll zu erstellen.
LoggingMode = WorkerLoggingMode.Never würde dies verhindern.

Wurden alle Datensätze korrekt verarbeitet?
Ja. Da der Arbeitsvorrat (die temporäre Tabelle) immer aktuell gehalten wurde. Beim Neustart wurde damit genau an dem Datensatz fortgesetzt bei dem der Fehler auftrat.

Massenverarbeitung

Aktion

namespace BA.Training.Configuration.Navigation
{
    /// <summary>
    /// Aktion mit einem Hintergrundprozess. Die Funktionalität wird asynchron ausgeführt und der Benutzer muss NICHT warten.
    /// </summary>
    [Serializable]
    [Toolbox(EnumConfigurationType.NavigationConfigurationGuid, true)]
    [ControlFilter("NavigationConfigurationType", ExpressionType.Equal, EnumNavigationConfigurationType.RibbonNavigationGuid, EnumControlFilterApplyState.IfPositive)]
    public class ClientActionAddServiceMassWorker : ClientActionGridMassOperationBase
    {
        [DisplayName("41ABA083-E37A-4709-98D4-1D685496459C")]
        public string RemarkMessage { get; set; }

        [TokenboxControl]
        [CDPRolesProviderProperties(dataSources: new[] { EnumDataSource.RoleGuid })]
        [DisplayName("BD0D11A9-9E71-4980-9265-C4A037432D48")]
        public RoleSet DialogRoles { get; set; }

        public ClientActionAddServiceMassWorker() : base()
        {
            ToolboxName = "BA98D63F-8B15-4B68-852F-102CBEB70D6E";
            Caption = "BA98D63F-8B15-4B68-852F-102CBEB70D6E";
            ControlInitName = "TrainingActionAddServiceMassWorker";
            ToolboxGroupName = "C007681C-8644-4BB0-A4A0-4A643265EABD";
            Id = "D6F15F40-5A3F-4D02-AF5B-491C8705B7BC".ToGuid();
            Icon = "wrench";
            IconName = Icon;

            // Action ist nur für Ansichten
            VisibilityForParentTypes.Add(EnumActionVisibleForParentType.Grid);

            // Igniter zum Erstellen des Workers setzen
            MassOperationIgniter = typeof(AddServiceWorkerIgniter).AssemblyQualifiedName;

            // Es muss etwas selektiert sein. Auf false setzen, wenn die Aktion auch funktioniert, wenn nichts gesetzt ist.
            // Siehe Eigenschaft AllRecordsIfNothingIsSelectedIndicator im Igniter
            SomethingMustBeSelected = true;

            // Sichtbarkeitssteuerung bei welcher von Selektionen die Aktion sichtbar ist
            // Weitere Möglichkeiten: OneSelected, OneOrNothingSelectedGuid, NothingSelected, ManySelected
            DynamicClientVisibility.Add(EnumActionVisibility.SomethingSelected);

            // Wird in CanHandleOrm mit true oder false belegt
            DynamicClientVisibility.Add(EnumActionVisibility.IfUserHasRole);

            // Clientseitige Aktion, die ausgeführt wird
            ActionMethodId = "BA.Training.ClientActionAddService";
        }

        /// <inheritdoc/>
        public override void AdditionalRibbonButtonAssignment(RibbonButtonItem ribbonItem, Dictionary<string, object> additionalClientData, EnumActionVisibleForParentType parentType, DevExUIModelBase uiModel, OrmBABase orm)
        {
            // Die Basemethode sollte immer als erste ausgeführt werden
            base.AdditionalRibbonButtonAssignment(ribbonItem, additionalClientData, parentType, uiModel, orm);

            // Übertragung der konfigurierten Werte zum Client
            additionalClientData.AddOrUpdate("RemarkMessage", RemarkMessage);
            additionalClientData.AddOrUpdate("DialogAuthorized", Api.User.CurrentUserIsInRole(DialogRoles, false));

            // Ermittlung ob die Aktion ausgeführt werden kann
            bool canHandle;
            if (orm != null)
                // Überprüfung eines konkreten Orms, ob der Benutzer es bearbeiten kann. Für Aktionen in Masken.
                // Hinweis: Oben wurde die Aktion nur für Ansichten definiert. Daher wird dieser Abschnitt
                // nicht durchlaufen und dient nur als Beispiel
                canHandle = orm.IsAllowed(EnumTableOperations.Edit);
            else
            {
                // In einer Ansicht kann nicht ein konkretes Orm angefragt werden, daher wird nur geprüft ob der Benutzer theoritisch
                // Datensätze dieses Typs bearbeiten kann. Dazu wird die Datentabellenkonfiguration geladen.
                OrmEntityConfiguration entityConfig = Api.Config.OrmEntity(EnumDataSourceExtension.Engine.ValueGuid);
                canHandle = entityConfig.IsAllowed(EnumTableOperations.Edit) != EnumTableOperations.Denied;
            }

            // Ergebnis zum Client übertragen. Siehe dazu im Konstruktor DynamicClientVisibility.Add(EnumActionVisibility.IfUserHasRole);
            additionalClientData.AddOrUpdate("UserHasRole", canHandle);
        }
    }
}

Erweiterung von EnumLogProcess

namespace BA.Training.Enums.Extensions
{
    [EnumExtension(typeof(EnumLogProcesses))]
    public static class EnumLogProcessesExtension
    {
        public const string AddServiceGuid = "FBF67FBA-9E64-4254-B168-A0C8DF806C63";
        public static readonly EnumLogProcesses AddService = new EnumLogProcesses(AddServiceGuid, 1000, "281E4152-6D03-40B4-9971-10728E673A00");
    }
}

Worker

namespace BA.Training.Worker
{
    /// <summary>
    /// Die Verarbeitung der Einträge der temporären Tabelle wird von dieser Basisklasse automatisch sicher gestellt. 
    /// Daher ist die Komplexität viel geringer.
    /// </summary>
    public class AddServiceMassWorker : TemporaryRecordMassWorkItem<OrmEngine>
    {
        public string Remark { get; set; }
        public DateTime Date { get; set; }
        public int MissingRights { get; set; }

        public AddServiceMassWorker() : base()
        {
            // Der Prozess wird um eine Minute verzögert gestartet.
            ScheduledStartTime = DateTime.UtcNow.AddMinutes(1);

            // Der Prozess kann von dem Benutzer in der UI unterbrochen werden.
            IsCancellable = true;

            // Beschreibende Informationen
            LoggingProcess = EnumLogProcessesExtension.AddService;
            Caption = "281e4152-6d03-40b4-9971-10728e673a00";
            Title = "281e4152-6d03-40b4-9971-10728e673a00";

            // Die ChunkSize gibt an in welchen Paketen die zu verarbeitenden Datensätze geladen werden
            // Default ist 200
            ChunkSize = 200;
        }

        protected override void BeforeProcessing(bool resume)
        {
            if (NumberOfStarts == 1)
            {
                // Erster Start des Prozesses
                Logger.AddInfo("61BCA157-96A6-436B-8719-0521B98462B9");
                Logger.AddInfo("03A9AE68-226F-4F19-91CD-47FA4370230B", MaxProgress);

                // Exception zum Testen des Verhaltens
                throw new Exception(Logger.Translate("160C2DB4-E013-4569-8AB1-A37BDF776707"));
            }
            else
            {
                // Neustart des Prozesses
                Logger.AddInfo("227B79DD-31F2-4F6D-A255-94EB8ACA0E89");
                Logger.AddInfo("E3D32867-83EB-4514-ABEC-C7E596AE192B", CurrentProgress, MaxProgress);
            }
        }

        /// <summary>
        /// Verarbeitung eines Datensatzes
        /// </summary>
        /// <param name="engine"></param>
        protected override void ProcessSingleOrm(OrmEngine engine)
        {
            Logger.AddInfo("1CBA5EFA-7842-4764-ABB8-05E2FAB5BDE3", engine.Name);

            // Exception zum Testen des Verhaltens
            if (CurrentProgress == 2)
                throw new Exception(Logger.Translate("160C2DB4-E013-4569-8AB1-A37BDF776707"));

            // Abprüfung der Rechte
            if (engine.IsAllowed(EnumTableOperations.Edit))
            {
                // Verarbeitung des aktuellen Datensatzes
                OrmSubEngineServices service = engine.Services.AddNewObject();
                service.SortOrder = engine.Services.Count() - 1;
                service.ServiceDate = Date;
                service.Remark = Remark;
                engine.Save();
            }
            else
            {
                Logger.AddWarning("0DD299DF-C558-44FD-86EC-B3B2ACD47530", engine.Name);
                MissingRights++;
            }
        }

        protected override void AfterProcessing(Exception exception)
        {
            if (MissingRights == 0)
            {
                Api.ClientCommunication.CreateSuccess(Api.User.CurrentUserGuid(), "dd1ceadc-6808-4583-b6b7-dda73188b5a8".Translate(CurrentProgress));
                Logger.AddInfo("dd1ceadc-6808-4583-b6b7-dda73188b5a8", CurrentProgress);
            }
            else
            {
                Api.ClientCommunication.CreateError(Api.User.CurrentUserGuid(), "e3f652e7-8019-4444-9e6c-f3c30528e862".Translate(CurrentProgress - MissingRights, MissingRights));
                Logger.AddInfo("e3f652e7-8019-4444-9e6c-f3c30528e862", CurrentProgress - MissingRights, MissingRights);
            }
        }
    }
}

Antworten

Was steht im Anwendungsprotokoll?

Mass Worker Exception

Wurden alle Datensätze verarbeitet?
Nein, der Datensatz mit der Exception wurde übersprungen.

Warum gibt es einen Unterschied zum vorherigen Hintergrundprozess, und was müsste man tun, um fehlerhafte Datensätze im zweiten Hintergrundprozess nochmal zu vearbeiten?
Im ersten Worker wurde durch die Ausnahme der gesamte Prozess abgebrochen. Dies geschah bevor der Eintrag aus der temporären Tabelle entfernt wurde. Nach einer Ausnahme wird ein Prozess neu eingeplant und beim Neustart wurde bei dem Datensatz fortgefahren.

Der TemporaryRecordMassWorkItem überpringt Datensätze bei denen Ausnahmen auftreten und der Prozess wird nicht abgebrochen. Um dies zu ändern, muss in OnError eine Ausnahme geworfen werden.