Ü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?
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.