Muster: Hideable Entities
In einigen Anwendungsfällen dürfen oder sollen Daten nicht vollständig gelöscht werden, sondern nur durch Ausblendung unsichtbar gemacht werden. Eine ausgeblendete Entität nennen wir Hideable Entity. Die Ausblendung einer Hideable Entity bewirkt, dass diese für die weitere Verarbeitung nicht mehr sichtbar ist. Sie liegt jedoch physisch weiterhin vor und kann wenn notwendig weiterhin genutzt werden (dies widerspricht natürlich dem Ziel und ist für Sonderfälle gedacht).
Folgende Anforderungen werden mit diesem Muster und der zugehörigen Implementierung umgesetzt:
- Unterstützer der unterschiedlichen expliziten Verwendung in Usecase Controllern:
- Aufruf von
Delete()
im Repository, um Daten physisch zu löschen - Aufruf von
Hide()
auf einer Entität, um diese aus Abfrageergebnissen im Default auszublenden
- Aufruf von
- Einsetzbarkeit sowohl für AggregateRoot als auch für AggregateChildren (siehe unbedingt Anmerkung unten!)
- Automatisches Ausfiltern ausgeblendeter Entitäten aus Abfrageergebnissen
- Unterstützung des korrekten erwarteten Verhaltens bei Event-Benachrichtigungen:
- Beim Ausblenden der Entität mittels
Hide()
wird ebenso wie beim physischen Löschen mittelsDelete()
das Deleted-Event ausgelöst - Beim Ausblenden von AggregateChildren wird ein Updated-Event auf der AggregateRoot ausgelöst
- Beim Ausblenden der Entität mittels
Design
In folgendem Diagramm sind die im Kontext eines Repositorys mit Hideable Entities relevanten Klassen und Schnittstellen dargestellt. Dabei sind die grün hinterlegten Klassen die im folgenden zu erstellenden bzw. betroffenen Stellen:

- Implementierung der Schnittstelle von
IHideableEntity
über den vom Framework bereitgestellten generischen TypHideableEntityAdorner
im Aggregate - Implementierung des Repositorys unter Verwendung eines NHibernateConfigurators, der folgende Schnittstellen
INHibernateExtender
- bzw. (nur 2nd Edition)INHibernateSessionExtender
-Varianten bereitstellt:- einen
HideableEntityExtender
für die (umschaltbare) Ausfilterung ausgeblendeter Entitäten aus Abfrageergebnissen - einen
HideableEntityPublishAggregateChangesExtender
für die Aufbereitung der Änderungsbenachrichtigungen für ausblendbare Entitäten
- einen
Die für die Implementierung der Hideable Entity-Funktionalität in Fachanwendungen benötigten Typen werden in folgenden Framework-Pakete bereitgestellt:
- Entity-Implementierung:
IHideableEntity
undHideableEntityAdorner
in Schleupen.CS.PI.Framework.HideableEntities - Repository-Implementierung:
HideableEntityExtender
undHideableEntityPublishAggregateChangesExtender
in- Schleupen.CS.PI.Framework.HideableEntities.Persistence.NHibernate51 (1st Edition)
- Schleupen.CS.PI.Framework.HideableEntities.Persistence.NHibernate51.2ndEdition (2nd Edition)
PersistenceInstaller
für die Bereitstellung desHideableEntityExtenders
per Dependency Injection in- Schleupen.CS.PI.Framework.HideableEntities.Persistence.NHibernate51.Castle3 (1st Edition) bzw.
- Schleupen.CS.PI.Framework.HideableEntities.Persistence.NHibernate51.2ndEdition.Castle3 (2nd Edition)
Implementierung
Hideable Entities bieten die Möglichkeit, beim Löschen von Entitäten diese - anstatt sie wirklich zu entfernen - nur logisch aus der Verarbeitung auszublenden, indem sie in der Persistenz mit einem Zeitstempel in der Datenbankspalte HiddenAt versehen werden. Im Framework werden die benötigten Funktionalitäten bereitgestellt, um dies mit NHibernate zu realisieren.
Um ein Repository, das ausblendbare Hideable Entities bereitstellt, nutzen zu können, sind folgende Schritte notwendig:
Änderungen an Entitäten
Ausblendbare Entitäten müssen die Schnittstelle IHideableEntity
implementieren.
public interface IHideableEntity { /// <summary> /// Gibt an, ob die Entität ausgeblendet ist. /// </summary> /// <returns><c>true</c>, falls die Entität ausgeblendet ist, sonst <c>false</c></returns> bool IsHidden { get; } /// <summary> /// Markiert die angegebene Entität als ausgeblendet. /// </summary> /// <returns><c>true</c>, falls ein neues Ausblendedatum gesetzt wurde, sonst <c>false</c></returns> /// <remarks>Das neue Ausblendedatum wird nur angewandt, falls auf der Entität noch kein zeitlich früheres Ausblendedatum gesetzt ist.</remarks> bool Hide(); /// <summary> /// Liest oder setzt den Zeitpunkt, zu dem die Entität ausgeblendet wurde/wird. /// </summary> /// <remarks> /// Diese Eigenschaft sollte i.d.R. nicht direkt verwendet werden, sondern es sollte /// <list type="bullet">die Methode <see cref="Hide"/> zum Ausblenden einer Entität und</list> /// <list type="bullet">die Eigenschaft <see cref="IsHidden"/> zur Überprüfung des Ausblendungsstatus einer Entität</list> /// verwendet werden. /// </remarks> [DebuggerNonUserCode] DateTimeOffset? HiddenAt { get; set; } }
Die einfachste Möglichkeit hierzu ist die Verwendung der vom Framework bereitgestellten generischen Klasse HideableEntityAdorner
und die Implementierung der IHideableEntity
über eine in die eigentliche Entität eingebettete Eigenschaft:
internal class Buch : ..., IRecoverableHideableEntity { private string titel; ... private readonly HideableEntityAdorner hideableEntityAdorner = new(); public Buch(BchId id, string titel, ...) : base(id) { this.titel = titel; ... } [ExcludeFromCodeCoverage] protected Buch() { } public virtual string Titel => titel; DateTimeOffset? IHideableEntity.HiddenAt { get => hideableEntityAdorner.HiddenAt; set => hideableEntityAdorner.HiddenAt = value; } bool IHideableEntity.IsHidden => hideableEntityAdorner.IsHidden; bool IHideableEntity.Hide() { return hideableEntityAdorner.Hide(); } void IRecoverableHideableEntity.Recover() { hideableEntityAdorner.Recover(); } ... }
Für ein Aggregat sollte im Standardfall das AggregateRoot die Schnittstelle IHideableEntity
erhalten!
Sollen in Ausnahmenfällen nur AggregateChild ausgeblendet werden, so müssen diese Typen die Schnittstelle IHideableEntity
erhalten! Auch hier reicht hierarchisch der höchste Typ. Beachte, dass dies auf einen falschen Aggregat-Schnitt hindeutet!
Umgekehrt formuliert: Entitäten, die die Schnittstelle IHideableEntity
nicht implementieren, werden nicht mit der Eigenschaft HiddenAt
versehen und generell von der Ausblendungs-Logik ignoriert.
Generelle Anbindung der Grundfunktionalität
Die generelle Funktionalität wird per Dependency Injection wie folgt angebunden.
public class ServiceHostFactoryConfigurator : WcfContainerConfigurator { ... protected override void OnContainerConfigured(IWindsorContainer windsorContainer) { if (windsorContainer == null) { throw new ArgumentNullException(nameof(windsorContainer)); } windsorContainer.Install(new Schleupen.CS.PI.Framework.HideableEntities.Persistence.NHibernate.PersistenceInstaller(IsInsideWcfOperation) { EnableHideableEntities = true }); ... } ... }
Änderungen am Repository
1st-Edition
Für die Persistierung ausblendbarer Entitäten muss das zugehörige Repository mithilfe des NHibernateConfigurator
konfiguriert werden. Hierbei müssen NHibernate-Extender entsprechend den nachfolgenden Bedingungen bereitgestellt werden:
public class BuchRepositoryConfigurator : NHibernateConfigurator, IBuchRepositoryConfigurator { private readonly IHideableEntityExtender hideableEntityExtender; ... public BuchRepositoryConfigurator( ..., IHideableEntityExtender hideableEntityExtender) { this.hideableEntityExtender = hideableEntityExtender ?? throw new ArgumentNullException(nameof(hideableEntityExtender)); ... } protected override IEnumerable<Type> MappingTypes { get { List<Type> mappingTypes = new(); mappingTypes.Add(typeof(BuchMap)); ... return mappingTypes; } } ... protected override IEnumerable<INHibernateExtender> Extenders { get { List<INHibernateExtender> nHibernateExtenders = new(); nHibernateExtenders.Add(hideableEntityExtender); ... return nHibernateExtenders; } } }
Der HideableEntityExtender wird auf der NHibernate SessionFactory angewendet. D.h. für die First-Edition darf dieser nicht getauscht werden. Es wird stets nur der erste verwendet.
2nd-Edition
In der 2nd-Edition muss der IHideableEntityExtender
nicht explizit angebunden werden, da diese Funktionalität out-of-the-box zur Verfügung steht.
Änderungen an Mappings
Mappings für ausblendbare Entitäten können komfortabel über die vom Framework bereitgestellte Erweiterungsmethode public static void ClassMapExtensions.ApplyHideableEntityMapping(this ClassMap classMap) where TEntity : IHideableEntity
angelegt werden. Diese ergänzt ein Mapping der Eigenschaft IHideableEntity.HiddenAt
auf die Datenbank-Spalte HiddenAt.
internal sealed class BuchMap : ClassMap<Buch> { public BuchMap() { ... Map(x => x.Name); ... this.ApplyHideableEntityMapping(); // alternativ: Map(x => ((IHideableEntity)x).HiddenAt); } }
Änderungen am Konfigurator
Der in einem Repository für ausblendbare Entitäten verwendete Konfigurator gibt muss Instanzen der folgenden INHibernateExtender
-Subtypen n der Liste der Extender zurückgeben:
- Ein
HideableEntityExtender
wird benötigt für die (umschaltbare) Ausfilterung ausgeblendeter Entitäten aus Abfrageergebnissen - Jeder dem Repository zugeordnete
PublishAggregateChangesExtender
muss in einenHideableEntityPublishAggregateChangesExtender
gekapselt werden, um Änderungsbenachrichtigungen für ausblendbare Entitäten korrekt aufzubereiten (z.B. Publizieren von OnDeleted-Events beim Ausblenden von Entitäten und zur Verhinderung weiterer Events auf bereits ausgeblendeten Entitäten)
Eine Implementierung von IHideableEntityExtender
kann durch den PersistenceInstaller
in den Paketen Schleupen.CS.PI.HideableEntities.Persistence.NHibernate51.Castle3 bzw. Schleupen.CS.PI.HideableEntities.Persistence.NHibernate51.2ndEdition.Castle3 für Dependency Injection in den Konfigurator bereitgestellt werden. Für den Typ HideableEntityPublishAggregateChangesExtender
wird durch denselben PersistenceInstaller eine Implementierung von IHideableEntityPublishAggregateChangesExtenderFactory
bereitgestellt, die aus gegebenen PublishAggregateChangesExtender
-Instanzen die für ausblendbare Entitäten benötigte Variante erzeugt.
Nur 2nd Edition: Ab den Framework-Versionen 3.29.1.23 in der HV21 und 3.28.1.52 in der SV21 ist die Klasse HideableEntityPublishAggregateChangesExtender
als abstrakte Basisklasse definiert. In diesem Fall ist es ausreichend, von der Basisklasse abzuleiten und die entsprechenden Methoden zu implementieren. Die Nutzung der Factory, um damit Instanzen der Klasse PublishAggregateChangesExtender
zu kapseln, entfällt ersatzlos. Über den Schleupen.CS.PI.Framework.HideableEntities.Persistence.NHibernate.PersistenceInstaller
wird sichergestellt, dass die Extender korrekt im DI-Container registriert werden. Damit können die Extender wie jede andere registrierte Komponente per Dependency Injection genutzt werden.
windsorContainer.Install(new Schleupen.CS.PI.Framework.HideableEntities.Persistence.NHibernate.PersistenceInstaller(IsInsideWcfOperation) { EnableHideableEntities = true });
Änderungen auf der Datenbank
Das HiddenAt
Feld muss in der Datenbank als DATETIMEOFFSET
definiert sein. Zusätzlich kann es nötig sein die referenzielle Integrität über Trigger sicherzustellen.
ALTER TABLE [mt_bib].Buch ADD [HiddenAt] [datetimeoffset](7) NULL;
Entities wieder einblenden
Wenn Entities nach dem Sie ausgeblendet wurden, wieder eingeblendet werden sollen, muss statt der Schnittstelle IHideableEntity
die Schnittstelle IRecoverableHideableEntity
implementiert werden:
/// <summary> /// Stellt Funktionalität für die Einblendung von Entitäten bereit. /// </summary> public interface IRecoverableHideableEntity : IHideableEntity { /// <summary> /// Markiert die angegebene Entität als eingeblendet. /// </summary> void Recover(); }
Die Methode Recover()
kann hier am Einfachsten genau so wie die Methode Hide()
über die generische Klasse HideableEntityAdorner
implementiert werden:
public virtual void Recover() => HideableEntityAdorner.Recover();
Rekursives Aus-/Einblenden
Manchmal ist es notwendig rekursiv AggregateChild aus-/einzublenden. Das Framewerk stellt hierzu die Erweiterungsmethode IHideableEntity.HideRecursively()
und IRecoverableHideableEntity.RecoverRecursively()
zur Verfügung.
Damit AggregateChild rekursive aus-/eingeblendet werden können, müssen diese auch das Interface IHideableEntity
bzw. IRecoverableHideableEntity
implementieren.
Dynamische Schaltbarkeit der Sichtbarkeit von HideableEntities
In einigen Anwendungsfällen möchte man möglichst zur Laufzeit die Sichtbarkeit von Hideable Entities schalten können. Der vom Framework bereitgestellte Typ HideableEntityExtender
stellt mit den Methoden IncludeHiddenItems()
und ExcludeHiddenItems()
eine komfortable Möglichkeit bereit, ausgeblendete Entitäten in Abfrageergebnisse dieses Repositories aufzunehmen oder sie daraus auszuschließen.
1st-Edition
public class BuchRepositoryConfigurator : NHibernateConfigurator, IBuchRepositoryConfigurator { private readonly IHideableEntityExtender hideableEntityExtender; ... public IHideableEntityExtender HideableEntityExtender => hideableEntityExtender; ... }
public sealed class BuchRepository : Repository<Buch>, IBuchRepository { private readonly IBuchRepositoryConfigurator configurator; public BuchRepository(..., IBuchRepositoryConfigurator configurator) : base(..., configurator) { this.configurator = configurator ?? throw new ArgumentNullException(nameof(configurator)); } public void IncludeHiddenEntities() { configurator.HideableEntityExtender.IncludeHiddenItems(NHibernateSession); // oder im Configurator delegieren, hier nur Demo } public void ExcludeHiddenEntities() { configurator.HideableEntityExtender.ExcludeHiddenItems(NHibernateSession); // oder im Configurator delegieren, hier nur Demo } ... }
Entsprechend kann der Controller dann IncludeHiddenEntities()
bzw. ExcludeHiddenEntities()
nutzen.
2nd-Edition
public sealed class BuchRepository : Repository<Buch>, IBuchRepository { private readonly IHideableEntityExtender hideableEntityExtender; public BuchRepository(..., IHideableEntityExtender hideableEntityExtender) : base(...) { this.hideableEntityExtender = hideableEntityExtender?? throw new ArgumentNullException(nameof(hideableEntityExtender)); ... } public void ExcludeHiddenItems() { hideableEntityExtender.ExcludeHiddenItems(NHibernateSession); } public void IncludeHiddenItems() { hideableEntityExtender.IncludeHiddenItems(NHibernateSession); } ... }
Entsprechend kann der Controller dann IncludeHiddenEntities()
bzw. ExcludeHiddenEntities()
nutzen.
Dynamische Schaltbarkeit der Sichtbarkeit von HideableEntities, V2
Mithilfe des folgendes Codes, kann man - solange der Scope aktiv ist - versteckte Entitäten einfach laden:
public sealed class BuchVerwaltenController : IBuchVerwaltenController { ... public IBuecher Query(BuchAggregateRestriction buchRestriction, bool? includeDeletedItems) { using (buchRepository.IncludeHiddenEntities()) { IBuecher queriedBuecher = buchRepository.QueryBy(new List<BuchAggregateRestriction> { buchRestriction }); return queriedBuecher; } } }
Hierzu muss die Repository-Schnittstelle von ISupportHideableEntities
erben:
public interface IReadOnlyBuchRepository : ISupportReadOnlyEntities, ISupportHideableEntities { ... }
und das Repository das Laden schaltbar anbieten:
public sealed class BuchRepository : Repository<Buch, BuchId>, IBuchRepository { ... public IIncludeHiddenEntitiesScope IncludeHiddenEntities() => new IncludeHiddenEntitiesScope(configurator.HideableEntityExtender, NHibernateSession); }
Anmerkungen
- Aggregates sollten immer vollständig oder gar nicht ausgeblendet werden. Ansonsten wird das Aggregate-Konzept verletzt somit das Prinzip der kleinsten Verwunderung (Principle of Least Astonishment (POLA)) verletzt.
Abgrenzung
Dieser Mechanismus greift nicht bei ISession.Get()
und ISession.Load()
von NHibernate! Da die Implementierung von Repository.QueryById()
diesen verwendet, greift auch hier der Mechnismus des Versteckens nicht!
Benutzte Muster
Stärken und Schwächen
Stärken
- Berücksichtigt alle Standards der anderen Muster (Mapping wie sonst auch etc.)
- Basiert auf Schnittstellen
Schwächen
- Implementierungsaufwand