get_pages filter:0.000371 msecclass="wp-singular page-template page-template-track page-template-track-php page page-id-216168 page-child parent-pageid-219449 wp-custom-logo wp-embed-responsive wp-theme-blocksy wp-child-theme-Blocksy-Child-Theme stk--is-blocksy-theme" data-link="type-2" data-prefix="single_page" data-header="type-1:sticky" data-footer="type-1" itemscope="itemscope" itemtype="https://schema.org/WebPage">

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
  • 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 mittels Delete() das Deleted-Event ausgelöst
    • Beim Ausblenden von AggregateChildren wird ein Updated-Event auf der AggregateRoot ausgelöst

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 Typ HideableEntityAdorner 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

Die für die Implementierung der Hideable Entity-Funktionalität in Fachanwendungen benötigten Typen werden in folgenden Framework-Pakete bereitgestellt:

  • Entity-Implementierung: IHideableEntity und HideableEntityAdorner in Schleupen.CS.PI.Framework.HideableEntities
  • Repository-Implementierung: HideableEntityExtender und HideableEntityPublishAggregateChangesExtender 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 des HideableEntityExtenders 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 einen HideableEntityPublishAggregateChangesExtender 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
get_pages filter:0.082905 msec
Cookie Consent mit Real Cookie Banner