WPF Over Data mit Caliburn.Micro – die Daten

 

Prolog

Als erstes gilt es an dieser Stelle eine Entschuldigung auszusprechen: im letzten Beitrag habe ich die ArrayDependencyFacility sowie den ArraySubDependencyResolver benutzt, ohne die Angabe zu machen, dass die Quellen aus der Feder von Alexander Groß stammen. Mea culpa und Asche auf mein Haupt.

Um es wieder gut zu machen, stammen viele der folgenden Vorgehensweisen, Methoden und Klassen auch von ihm.

Gesteckte Ziele

Was soll erreicht werden? Wir haben Daten. Meistens. Wie im ersten Teil schon erwähnt, soll mittels NHibernate darauf zugegriffen werden. Um der Konfiguration durch XML-Dateien zu entgehen, wird für das Mapping FluentNHibernate eingesetzt. Es sollte allerdings kein statisches Mapping sein, sondern die Module sollen ihre Mappings selbst mitbringen. Die Konfiguration der Datenverbindung soll auch irgendwie erfolgen. Tests sind nötig, sie sollten gegen SQLite gefahren werden (Speicher-Datenbanken), das Programm selbst wird durch einen Connection-String konfiguriert. Man bräuchte auch eine allgemeine Möglichkeit, mir der Datenbank zu sprechen, CRUD-Befehle abzusetzen – womöglich innerhalb von Transaktionen – aber auch Queries werden benötigt.

Eine lange Liste… fangen wir einfach mit dem an, was wir immer speichern wollen: einen Teil unserer Domäne.

 

Eine Domänen-Entität

Ungeachtet vieler Diskussionen, ob die ID eines Datenbank-Objektes nun besser durch eine GUID oder einen Integer abgebildet werden sollte, ist der allgemeine Konsens doch der, dass die ID selbst nichts mit der Businesslogik zu tun haben sollte. Die DomainEntity stellt sie in unserem Fall als int zur Verfügung.

Ebenfalls wird im Allgemeinen als unschön angesehen, wenn eine Änderung eine andere überschreibt. Hierfür bzw hiergegen wird ein Versionsfeld eingesetzt.

Damit wir 2 Entities miteinander vergleichen können, implementieren wir noch IEquatable und mappen im wesentlichen die Gleichheit auf die Gleichheit der Ids.

Insgesamt sieht unsere Basisklasse also so aus:

public class DomainEntity : IEquatable<DomainEntity>
{
  int _id;

  protected DomainEntity()
  {
  }

  protected DomainEntity(int id)
  {    _id = id;
  }

  public virtual int Id
  {
    get { return _id; }
    protected set { _id = value; }
  }

  public virtual int Version { get; protected set; }

  public virtual bool Equals(DomainEntity other)
  {
    if (ReferenceEquals(null, other)) return false;
    if (ReferenceEquals(this, other)) return true;
    return Id.Equals(default(int)) ? base.Equals(other) : other.Id.Equals(Id);
  }

  public override bool Equals(object obj)
  {
    return Equals(obj as DomainEntity);
  }

  public override int GetHashCode()
  {
    return Id.Equals(default(int)) ? base.GetHashCode() : Id.GetHashCode();
  }

  public static bool operator ==(DomainEntity left, DomainEntity right)
  {
    return Equals(left, right);
  }

  public static bool operator !=(DomainEntity left, DomainEntity right)
  {    return !Equals(left, right);
  }
}

Datenbank-Zugriffe

Wir wissen nun also ungefähr, welche Daten wir mit unserer Datenquelle austauschen wollen, aber noch nicht, wie genau das funktionieren soll. Uns fehlt ein Gespräch mit dem Server, also nehmen wir doch genau das als Interface: IDbConversation. Die Gesprächsoptionen sind ja vorgegeben, wir nehmen InsertOnCommit als Create- und Update-Funktion, DeleteOnCommit als Delete-, sowie GetById als Read-Funktion. Zusätzlich untersützen wir Transaktionen und Queries:

public interface IDbConversation : IDisposable
{
  TResult Query<TResult>(IDomainQuery<TResult> query);
  void UsingTransaction(Action action);

  TResult GetById<TResult>(object key);
  void InsertOnCommit(object instance);
  void DeleteOnCommit(object instance);
}

Aber Moment – was ist denn nun ein IDomainQuery? Sicherlich eine Abfrage, die ein irgendwie geartetes Ergebnis liefert, wie sich unschwer am Interface ablesen läßt:

public interface IDomainQuery<TResult>
{
  TResult Execute(ISession session);
}

Nun mappen wir unsere IDbConversation auf unsere NHibernate-Session unter der Annahme, wir hätten eine INHibernateSessionFactory, die uns eine solche Session erzeugen kann:

public class DbConversation : IDbConversation { readonly ISession _session; public DbConversation(INHibernateSessionFactory sessionFactory) { _session = sessionFactory.CreateSession(); } public void Dispose() { if (_session != null) _session.Dispose(); } public TResult Query<TResult>(IDomainQuery<TResult> query) { return query.Execute(_session); } public void UsingTransaction(Action action) { using( var transaction=_session.BeginTransaction()) { try { action(); transaction.Commit(); } catch (Exception) { transaction.Rollback(); throw; } } } public TResult GetById<TResult>(object key) { return _session.Load<TResult>(key); } public void InsertOnCommit(object instance) { _session.SaveOrUpdate(instance); } public void DeleteOnCommit(object instance) { _session.Delete(instance); }

}

Der Code dürfte wenig Raum für Überraschungen lassen, und das Interface INHibernateSessionFactory wurde ja quasi schon verweggenommen:

public interface INHibernateSessionFactory : IDisposable
{
  ISession CreateSession();
}

Zwischenstand: wir haben einen Weg gefunden mit der Datenbank zu kommunizieren, wir haben ein Basis-Objekt, das wir abrufen und abspeichern können. Jetzt fehlt noch die Konfiguration des Datenbankzugriffs.

 

Datenbank-Konfiguration

Was kann es an einer Datenbank-Verbindung schon viel zu konfigurieren geben? Man nehme einen Connection-String, übergebe ihn NHibernate, und fertig. Nun ja, fast fertig. Man könnte zum Beispiel noch ein paar Module mitkonfigurieren wollen, an die man vorher nicht gedacht hat. Aber nur im Debug-Modus. Oder den Aufbau zur Datenbank mitprotokollieren. Und wo kommen eigentlich die Tabellendefinitionen ins Spiel?

Fangen wir mit dem Connection-String an. Die Persistenz sollte konfigurierbar sein, durch eine IPersistenceConfiguration:

public interface IPersistenceConfiguration
{
  IPersistenceConfigurer GetConfiguration();
}

Im Falle eines SQL-Servers könnte die Implementierung der Schnittstelle etwa so aussehen:

public class SqlServerPersistenceConfiguration : IPersistenceConfiguration
{
  readonly string _connectionString;
  public bool ShowSql { get; set; }

  public SqlServerPersistenceConfiguration(string connectionString)
  {
    _connectionString = connectionString;
  }

  public IPersistenceConfigurer GetConfiguration()
  {
    var configuration = MsSqlConfiguration
      .MsSql2005
      .ConnectionString(c => c.Is(_connectionString))
      .ProxyFactoryFactory(typeof(ProxyFactoryFactory).AssemblyQualifiedName)
      .AdoNetBatchSize(10)
      .UseReflectionOptimizer()
      .UseOuterJoin();

    if (ShowSql)
    {
      configuration.ShowSql();
    }

    return configuration;
  }
}

Auf die selbe Art und Weise soll das Persistenzmodell selbst konfigurierbar sein. Der Unterschied ist, dass es im Gegensatz zur Persistenzkonfiguration mehrere Mappings gibt, die es zu berücksichtigen gilt. Nachdem diese allerdings gesammelt worden sind, sollen sie zusammen angewandt werden. Was zu folgendem Interface führt:

public interface INHibernatePersistenceModel
{
  void AddMappings(MappingConfiguration configuration); 
}

und wie oben bereits angesprochen sammelt die Implementierung lediglich alle vorkommenden Mappings:

public IMappingContributor[] MappingContributors
{
  get;
  set;
}

public void AddMappings(MappingConfiguration configuration)
{
  MappingContributors.Each(x => x.Apply(configuration));
}

Ein MappingContributor hat nur eine Aufgabe, nämlich seine eigenen Mappings der aktuellen Konfiguration hinzuzufügen:

public interface IMappingContributor
{
  void Apply(MappingConfiguration configuration);
}

Jetzt wird es allerdings so sein, daß alle weiteren MappingContributors in externen Assemblies hinzugefügt werden. Um nicht alle Mappings – also ClassMap<Type> – einzeln angeben zu müssen, lassen wir sie uns einfach hereinreichen: wir lassen FluentNHibernate selbst danach suchen:

public class FluentMappingFromAssembly : IMappingContributor
{
  readonly Assembly _assembly;

  public FluentMappingFromAssembly(string assembly)
  {
    _assembly = Assembly.LoadFrom(assembly);
  }

  public void Apply(MappingConfiguration configuration)
  {
    configuration.FluentMappings.AddFromAssembly(_assembly);
  }
}

Es fehlt noch der letzte Punkt: wie könnte man mitprotokollieren, wie weit die Konfiguration der Datenbankverbindung fortgeschritten ist? Man könnte die Fortschrittspunkte als Zustände definieren, und für jeden Zustand eine eigene Methode aufrufen. In etwa:

public interface INHibernateInitializationAware
{
  void BeforeInitialization();
  void Configuring(NHibernate.Cfg.Configuration configuration);
  void Configured(NHibernate.Cfg.Configuration configuration);
  void Initialized(NHibernate.Cfg.Configuration configuration, ISessionFactory sessionFactory);
}

Mit Hilfe dieses Interface könnte man den NHProfiler im Debug-Modus aktivieren, sobald die Initialisierung abgeschlossen ist:

public class NhProfilerInitializer : INHibernateInitializationAware
{
  public bool Enabled { get; set; }

  public NhProfilerInitializer()
  {
    EnableProfiler();
  }

  [Conditional("DEBUG")]
  void EnableProfiler()
  {
    Enabled = true;
  }

  public void BeforeInitialization() { }
  public void Configuring(NHibernate.Cfg.Configuration configuration) { }
  public void Configured(NHibernate.Cfg.Configuration configuration) { }

  public void Initialized(NHibernate.Cfg.Configuration configuration, ISessionFactory sessionFactory)
  {
    if (!Enabled)
      return;
    NHibernateProfiler.Initialize();
  }       
}

Mit Hilfe dieser Vorbereitungen können wir nun endlich die Konfiguration der Datenbank selbst angehen. Wir konfigurieren die Datenbankverbindung, fügen die Tabellendefinitionen an, lassen uns im Debug-Modus die Tabelle nach neuester Definition erzeugen, und erhalten als Gegenleistung eine INHibernateSessionFactory, die uns ISessions erzeugen kann:

public class NHibernateSessionFactory : INHibernateSessionFactory
{
  static readonly object InitializationSynchronization = new object();
  readonly IPersistenceConfiguration _persistenceConfiguration;
  readonly INHibernatePersistenceModel _persistenceModel;
  ISessionFactory _sessionFactory;

  public NHibernateSessionFactory(IPersistenceConfiguration persistenceConfiguration,
                                        INHibernatePersistenceModel persistenceModel)
  {    _persistenceConfiguration = persistenceConfiguration;
    _persistenceModel = persistenceModel;
  }

  public INHibernateInitializationAware[] Initializers
  {
    get;
    set;
  }

  ISessionFactory CreateSessionFactory()
  {
    Initializers.Each(x => x.BeforeInitialization());

    var configuration = Fluently.Configure()
                .Database(_persistenceConfiguration.GetConfiguration())
                .Mappings(_persistenceModel.AddMappings);

    configuration.ExposeConfiguration(c => Initializers.Each(x => x.Configuring(c)));

    var actualConfiguration = configuration.BuildConfiguration();
    Initializers.Each(x => x.Configured(actualConfiguration));
    CreateDatabaseWhenDebug(configuration);

    _sessionFactory = configuration.BuildSessionFactory();

    Initializers.Each(x => x.Initialized(actualConfiguration, _sessionFactory));

    return _sessionFactory;
  }

  ~NHibernateSessionFactory()
  {
    Dispose(false);
  }

  protected virtual void Dispose(bool @explicit)
  {
    if (@explicit)
    {
      if (_sessionFactory != null)
      {
        _sessionFactory.Dispose();
        _sessionFactory = null;
      }
    }
  }

  public ISession CreateSession()
  {
    if (_sessionFactory == null)
    {
      lock (InitializationSynchronization)
      {
        if (_sessionFactory == null)
        {
          _sessionFactory = CreateSessionFactory();
        }
      }
    }

    var session = _sessionFactory.OpenSession();
    session.FlushMode = FlushMode.Commit;
    return session;
  }

  [Conditional("DEBUG")]
  static void CreateDatabaseWhenDebug(FluentConfiguration configuration)
  {
    if (configuration == null) throw new ArgumentNullException("configuration");
    configuration.ExposeConfiguration(
    config => new SchemaUpdate(config).Execute(false, true));
  }

  public void Dispose()
  {
     Dispose(true);
     GC.SuppressFinalize(this);
  }

  public void Configure()
  {
    CreateSessionFactory();
  }
}

Die Persistenz-Konfiguration sowie das Persistenz-Modell werden per Construktor-Injektion vom IoC-Container hineingereicht, die optionalen Initialisierer hingegen per Property-Injection. Schön zu sehen an dieser Stelle, wie beim Erzeugen der Session Factory die einzelnen Stadien durchgereicht werden.

 

Und wofür das alles?

Jetzt haben wir diese Unmengen an Code erzeugt, und können noch nicht einmal eine einfache Tabellenabfrage generieren – kann das gut sein?

Fügen wir erst einmal die in der CastleBootstrapper.cs fehlende Funktion hinzu, die im letzten Teil noch auskommentiert blieb:

static IEnumerable<IRegistration> GetPersistenceRegistrations() {

yield return Component

.For<IPersistenceConfiguration>()

.ImplementedBy<SqlServerPersistenceConfiguration>()

.Parameters(Parameter.ForKey(„connectionString“).Eq(ConfigurationManager.AppSettings[„DbConnection“]));

 

yield return AllTypes

.FromAssemblyContaining(typeof (IMappingContributor)) .BasedOn(typeof (IMappingContributor)) .WithService.Base(); yield return Component .For<INHibernatePersistenceModel>() .ImplementedBy<NHibernatePersistenceModel>(); yield return AllTypes

.FromAssemblyContaining(typeof (INHibernateInitializationAware)) .BasedOn(typeof (INHibernateInitializationAware)) .WithService.Base(); yield return Component .For<INHibernateSessionFactory>() .ImplementedBy<NHibernateSessionFactory>(); yield return Component .For<IDbConversation>() .ImplementedBy<DbConversation>() .LifeStyle.Transient; }

Man sieht auf den ersten Blick, wie die einzelnen Module der Datenbankanbindung getreu der Separation of Concerns aufgeteilt wurden. Jede Komponente ist für sich testbar, und das Gesamtpaket ist offen für Erweiterungen.

Bereits oben haben wir gesehen, wie einfach wir das NHibernate-Profiling aktivieren können, ohne die Software ändern zu müssen. Und es stimmt, mangels Tabellendefinitionen können wir nicht einmal eine Tabelle ausgeben. Das soll sich nun ändern.

 

Ein Beispielmodul

Wir fügen jetzt eine neue Tabellendefinition hinzu, ohne dass die bisherigen Projekte davon betroffen sind.

Beginnen wir zB mit einer Benutzerverwaltung für die einzelnen Module des im ersten Teil angedeuteten Backends, dem sogenannten User Management System. Wir fügen im Projekt Lucifer.Ums.Model die beiden Entities User und UserRole hinzu:

public class User : DomainEntity
{
  public virtual string Name { get; set; }
  public virtual UserRole UserRole { get; set; }

}
public class UserRole : DomainEntity
{
  public virtual string Name { get; set; }
}

Da wir wahrscheinlich auch Listenansichten befüllen möchten, erzeugen wir auch die beiden Queries hierfür:

public class AllUsersQuery : IDomainQuery<IEnumerable<User>>
{
  public IEnumerable<User> Execute(ISession session)
  {
    return session.Query<User>();
  }
}
public class AllUserRolesQuery : IDomainQuery<IEnumerable<UserRole>>
{
  public IEnumerable<UserRole> Execute(ISession session)
  {
    return session.Query<UserRole>();
  }
}

Nun machen wir im Projekt Lucifer.Ums.Mapping der Datenbank die beiden Tabellen per FluentNHibernate bekannt:

public class UserMap : ClassMap<User>
{
  public UserMap()
  {
    Id(d => d.Id).GeneratedBy.HiLo("10");
    Map(d => d.Name).Length(40);
    References(d => d.UserRole).Not.Nullable();

    Version(d => d.Version);
  }
}
public class UserRoleMap : ClassMap<UserRole>
{
  public UserRoleMap()
  {
    Id(d => d.Id).GeneratedBy.HiLo("10");
    Map(d => d.Name).Length(40);

    Version(d => d.Version);
  }
}

Und damit der IoC-Container unsere Implementierungen auch findet, registrieren wir unsere MappingContributors:

public class UmsWindsorInstaller : IWindsorInstaller
{
  public void Install(IWindsorContainer container, IConfigurationStore store)
  {    GetRegistrations().ForEach(x => container.Register(x));
  }

  static IEnumerable<IRegistration> GetRegistrations()
  {    yield return Component    .For<IMappingContributor>()
      .ImplementedBy<FluentMappingFromAssembly>()
      .Parameters(Parameter.ForKey("assembly").Eq(typeof(UserRoleMap).Assembly.CodeBase))
      .Named("UmsMappingsFromAssembly");
  }
}

Hier sehen wir den Anwendungsfall des oben erwähnten FluentMappingFromAssembly: FluentNHibernate wird sich durch diese Registrierung die ClassMaps UserMap und UserRoleMap selbst aus Lucifer.Ums.Mapping auflösen.

Und das war schon die Anbindung eines neuen Moduls an die Datenbank. Die geeigneten Initialisierungen vorausgesetzt, könnten wir nun eine Abfrage aller Benutzer in einer Konsole ausgeben:

IDbConversation dbConversation;
dbConversation
  .Query(new AllUsersQuery())
  .Each(x => 
    { 
      System.Console.WriteLine("Id {0}, Name {1}", x.Id, x.Name); 
    });

Nur bräuchte man hierfür kaum diesen Aufwand Smiley

 

Ausblick

Was noch fehlt sind weitere Module, damit die Benutzerverwaltung auch Sinn macht, der Ausbau der Benutzerverwaltung selbst, und endlich auch sichtbare Ergebnisse. So wären Dialoge und Listen etwas für einen nächsten Teil in dieser Reihe.

Wie bereits im letzten Teil erwähnt, ist der Quelltext auf GitHub verfügbar.

DotNetKicks-DE Image
Advertisements
Dieser Beitrag wurde unter .NET, C#, NHibernate veröffentlicht. Setze ein Lesezeichen auf den Permalink.

2 Antworten zu WPF Over Data mit Caliburn.Micro – die Daten

  1. Robert Mischke schreibt:

    Sehr schöner Post! Das ist auch ein sehr schönes Beispielprojekt, an dem sich eine Menge lernen lässt – auch wenn jeder sicherlich Dinge findet die er anders machen würde (s. Projektstruktur) 🙂

    Im Sample Projekt habe ich immer nur eine Abfrage gefunden. Wie groß können denn solche Business-Conversations werden? Ich hatte erwartet, dass da ein wenig mehr passiert. Wenn ich eine Maske aufmache, dann möchte ich meist unterschiedliche Daten haben, die nicht in Abfrage passen. Wie wird das gelöst?

    Was mir am Sample Projekt nicht gefält, das Modell und Queries getrennt sind. In einer normalen Anwendung quillt dann der Query Ordner über. Ich mag es wenn der Modelltyp und der ApplicatonServices, also auch Abfragen, nah beieinander sind. Und warum sind die Mappings in einem eigenen Projekt? Ist wohl aber nur eine Geschmacksfrage 😉

    (Ach so, nach nochmaliger Recherche wurde ich bekehrt und werde jetzt auch grundsätzlich auch Equals für NH überschreiben – sicher ist sicher. (Ich werde Deine DomainEntity verwenden, falls ich darf :-))

    • joergpreiss schreibt:

      Danke. Naja, mit der Projektstruktur bin ich im Allgemeinen noch nicht so konform. Was, wenn ich jetzt noch einen Importer hätte, wo sollte der dann hin usw… Außerdem widerspricht es dem Vorschlag von Ralf W (glaub ich), die Projekte so klein wie möglich zu halten. Was aber nicht anders geht wegen nuget.

      Bisher hab ich es noch nicht gebraucht, daß ich riesige Conversations hätte. Die benötigten Listen innerhalb der Edit-Masken werden in die selbe Transaktion gepackt und fertig. Vielleicht sollte man das Backend mal voll ausbauen um zu sehen, wo die Engpässe sind.

      Die Mappings sind – wie oben angesprochen – extra, falls ich sie an anderer Stelle auch noch bräuchte. Ist aber wie alles Geschmackssache. Ebenso die getrennten Query-Ordner. Allerdings: wenn der Query-Ordner überlauft, tut er das erst recht, wenn ich die Models auch noch dazupacke 😉 Von daher…

Kommentar verfassen

Trage deine Daten unten ein oder klicke ein Icon um dich einzuloggen:

WordPress.com-Logo

Du kommentierst mit Deinem WordPress.com-Konto. Abmelden /  Ändern )

Google+ Foto

Du kommentierst mit Deinem Google+-Konto. Abmelden /  Ändern )

Twitter-Bild

Du kommentierst mit Deinem Twitter-Konto. Abmelden /  Ändern )

Facebook-Foto

Du kommentierst mit Deinem Facebook-Konto. Abmelden /  Ändern )

Verbinde mit %s