Repository Pattern mit MongoDB und C#
MongoDB unterstützt reichhaltige Datentypen und kann .NET-Objekte oft 1:1 in der Datenbank ablegen. Das verleitet dazu, auf eine Trennung zwischen der Geschäftslogik und dem Datenzugriffscode zu verzichten. Für kleinere Projekte kann das funktionieren, allerdings ist der Test der Business Logik anschließend nur sehr eingeschränkt möglich.
Hier kommt das Repository Pattern ins Spiel. Es zieht eine Schicht zwischen der Business Logik und dem Datenzugriffscode ein, so dass die Details des Datenzugriffs hinter dem Repository verborgen bleiben. Die Zugriffsschnittstelle und die Struktur der Datenobjekte, die das Repository anbietet, wird nach Inversion of Control durch die Business Logik definiert. Im Repository wird der Datenzugriff implementiert, so dass sich diese nach den Erfordernissen der Datenbank richten kann.
Ein beispielhaftes Repository für eine einfache Kundenklasse könnte also folgendermaßen definiert sein:
public class Customer
{
public string Id { get; set; }
public string Name { get; set; }
public string CountryCode { get; set; }
}
public interface ICustomerRepository
{
Task AddAsync(Customer c);
Task UpdateNameAsync(string id, string name);
Task<IEnumerable<Customer>> GetByCountryCodeAsync(string countryCode);
}
Da wir mit MongoDB arbeiten, wollen wir eine ObjectId
als Id des Dokuments verwenden. Außerdem wollen wir folgende Dokumentenstruktur erreichen:
{
_id: <ObjectId>,
n: "Customer Name",
c: "CountryCode",
}
Dazu müssen ein paar Anpassungen am Mapping vorgenommen werden. Ein einfacher, gern genutzter Weg ist die deklarative ClassMap durch das Hinzufügen von Attributen zur Klasse:
[BsonIgnoreExtraElements]
public class Customer
{
[BsonRepresentation(BsonType.ObjectId)]
public string Id { get; set; }
[BsonElement("n")]
public string Name { get; set; }
[BsonElement("c")]
public string CountryCode { get; set; }
}
Damit wäre zweifellos die passende Dokumentenstruktur erreicht - allerdings zu dem Preis, dass der Code in der Business Logik nun MongoDB-Attribute enthält. Um dies aus Sicht der Softwarearchitektur sauber zu lösen, bieten sich zwei Wege an:
Eigene Daten-DTOs in der Datenzugriffsschicht
Dies bedeutet im Wesentlichen, dedizierte Daten-Klassen in der Datenzugriffsschicht anzulegen und ein Mapping zwischen den DTOs aus der Business Logik und den DTOs in der Datenzugriffsschicht herzustellen. Für dieses Mapping kann beispielsweise der AutoMapper genutzt werden. Die Attribute werden dann an den DTOs in der Datenzugriffsschicht hinzugefügt.
Der Vorteil dieser Lösung ist, dass durch die Attribute sehr einfach erkannt werden kann, welche Mapping-Einstellungen für eine Klasse verwendet werden. Außerdem eröffnet das Mappen auf eigene Klassen später die Möglichkeit einer Entkopplung von der Struktur der Business Logik. Ein Nachteil ist die erhöhte Komplexität durch das Mapping, die in vielen Fällen unnötig ist, da die Objekte mit wenigen Anpassungen in MongoDB abgelegt werden.
Imperative ClassMaps
Insbesondere, wenn nur wenige Anpassungen beim Mappen der Klassen vorgenommen werden, ist die imperative Umsetzung der ClassMaps ein Weg, um auf dedizierte Daten-DTOs verzichten zu können. Hierbei wird das Mapping über Befehle definiert und nicht über die Angabe von Attributen an der Klasse selbst.
Wichtig ist nur, dass das Mapping möglichst frühzeitig erfolgt, damit MongoDB beim Serialisieren auf die richtigen Einstellungen zurückgreift. Ein geeigneter Platz zur Definition der ClassMap ist beispielsweise der statische Konstruktor der Repository-Implementierung:
public class CustomerRepository : ICustomerRepository
{
static CustomerRepository()
{
BsonClassMap.TryRegisterClassMap<Customer>(map =>
{
map.AutoMap();
map.SetIgnoreExtraElements(true);
map
.MapIdField(x => x.Id)
.SetIdGenerator(StringObjectIdGenerator.Instance)
.SetSerializer(new StringSerializer(BsonType.ObjectId));
map.MapMember(x => x.Name).SetElementName("n");
map.MapMember(x => x.CountryCode).SetElementName("c");
});
// ...
}
}
In obigem Beispiel wird über AutoMap
zuerst das Default-Mapping hergestellt. Anschließend wird über SetIgnoreExtraElements
abgesichert, dass der Driver bei überzähligen Eigenschaften nicht aus dem Tritt gerät. Das Mapping des Id-Felds ist insgesamt etwas aufwändiger als über die Attribute, kommt jedoch zum gleichen Ziel. Zu guter Letzt werden die Element-Namen der beiden Properties angepasst.
Ein Vorteil dieser Lösung ist der Verzicht auf ein Mapping, das an einigen Stellen problematisch werden kann, beispielsweise, wenn man einen Zugriff über IQueryable<T>
bieten will. Im Vergleich zur Definition über Attribute ist die Lesbarkeit zwar etwas schlechter, aber das kann mit einer passenden Basisklassenstruktur und Hilfsmethoden gut gelöst werden.
Fazit
Durch die Auslagerung des Datenzugriffscodes in Repositories ist es möglich, die Business Logik frei von Datenbank-Spezifika zu halten. Wie die obigen Beispiele zeigen, gibt es einige Möglichkeiten, um dies mit dem MongoDB C# Driver umzusetzen. Weitere Informationen zu ClassMaps sind in der Dokumentation des Drivers zu finden.