Diese Anwendung demonstriert, wie man atomare Erstellungs-, Aktualisierungs- und Löschvorgänge für typische Kunden- und Kundentransaktionstabellen in einer Eins-zu-viele-Beziehung unter Verwendung des Dapper-Mikro-ORM und FluentMap sowie eines RepositoryUnit Of Work-Musters durchführt. Die Datenbank besteht aus DBF-Dateien und der Zugriff erfolgt über den Visual FoxPro OleDb-Treiber. Auf Datenbankebene sind keine Regeln, Trigger oder ähnliches implementiert.
Als Anwendungsbeispiele werden im Projekt „Tests“ einige Integrationstests mit XUnit bereitgestellt, außerdem gibt es im Projekt „SimpleExample“ eine einfache Konsolenanwendung.
Dies basiert auf dem Ansatz, den Ian Rufus in diesem Blogeintrag detailliert beschreibt.
Wichtig! Diese Anwendung erfordert den Microsoft Visual FoxPro 9.0 OleDb Provider. Dies ist ein reiner 32-Bit-Anbieter, es gibt keine 64-Bit-Version. Daher muss diese Anwendung nur für x86 kompiliert werden.
Dapper ist ein beliebter Micro-ORM-Objekt-Mapper, der IDbConnection um praktische Methoden erweitert, die Datenbankergebnisse zurückgeben, die Entitätstypen zugeordnet sind.
Hier ist eine Datenbanktabelle im DBF-Format:
Field Field Name Type Width
1 CU_CODE Character 10
2 CU_NAME Character 50
3 CU_ADDR1 Character 50
4 CU_ADDR2 Character 50
5 CU_POSTCODE Character 10
6 CU_BALANCE Numeric 12
und hier ist die C#-Entität, die es darstellt.
public class Customer
{
[ Key ]
public string Code { get ; set ; } = string . Empty ;
public string Name { get ; set ; } = string . Empty ;
public string Address1 { get ; set ; } = string . Empty ;
public string ? Address2 { get ; set ; } = string . Empty ;
public string ? Postcode { get ; set ; }
///
/// Gets or sets the customer balance. Not writable. It can only
/// be updated by inserting, deleting or updating a
/// transaction or transactions for this customer.
///
[ Write ( false ) ]
public float Balance { get ; set ; }
public override string ToString ( )
{
return $ " { Code } { Name } " ;
}
}
Dapper bietet dann die Möglichkeit, Dinge zu tun wie:
public Customer GetByCode ( string code )
{
var cmd = @"select cu_code, cu_name, cu_addr1, cu_addr2, cu_postcode, cu_balance " ;
cmd += "from Customers where cu_code = ?" ;
return _connection . QueryFirstOrDefault < Customer > ( cmd , param : new { c = code } , transaction ) ;
}
Beachten Sie die Art und Weise, wie Abfrageparameter implementiert werden – OleDB unterstützt keine benannten Parameter, sondern nur Positionsparameter. Wenn also mehrere Parameter verwendet werden, ist die Reihenfolge von entscheidender Bedeutung:
public void Update ( Customer customer )
{
var cmd = @"update Customers set cu_name=?, cu_addr1=?, cu_addr2=?, cu_postcode=? where cu_code=?" ;
_connection . ExecuteScalar ( cmd , param : new
{
n = customer . Name ,
add1 = customer . Address1 ,
add2 = customer . Address2 ,
pc = customer . Postcode ,
acc = customer . Code ,
} ,
transaction ) ;
}
FluentMap ist eine Dapper-Erweiterung, mit der die Zuordnung zwischen C#-Entitätseigenschaften und den zugehörigen Datenbanktabellenfeldern explizit deklariert werden kann.
public class CustomerEntityMap : EntityMap < Customer >
{
public CustomerEntityMap ( )
{
Map ( c => c . Code ) . ToColumn ( "cu_code" , caseSensitive : false ) ;
Map ( c => c . Name ) . ToColumn ( "cu_name" , caseSensitive : false ) ;
Map ( c => c . Address1 ) . ToColumn ( "cu_addr1" , caseSensitive : false ) ;
Map ( c => c . Address2 ) . ToColumn ( "cu_addr2" , caseSensitive : false ) ;
Map ( c => c . Postcode ) . ToColumn ( "cu_postcode" , caseSensitive : false ) ;
Map ( c => c . Balance ) . ToColumn ( "cu_balance" , caseSensitive : false ) ;
}
}
Das Unit-Of-Work-Muster ermöglicht die Durchführung oder das Rollback von Datenbankerstellungs-, -aktualisierungs- und -löschvorgängen als eine einzige Transaktion, wodurch eine Datenbank-„Atomarität“ ermöglicht wird, bei der alle Aktualisierungen oder keine durchgeführt werden.
Ein Repository-Muster isoliert Datenbankoperationen von der Benutzeroberfläche und ermöglicht die Ausführung von Datenbankoperationen durch Hinzufügen, Aktualisieren oder Löschen von Elementen aus einer Objektsammlung.
In der Anwendung gibt es zwei Repository-Klassen: CustomerRepositoty
und CustomerTransactionRepository
. Jedem wird über den Konstruktor ein Parameter vom Typ IDbConnection übergeben. Die zu verwendende Datenbankverbindung wird dann aus diesem Parameter abgerufen:
private IDbConnection _connection { get => databaseTransaction . Connection ! ; }
Beachten Sie das nullverzeihende „!“ Operator. Diese Abhängigkeitsinjektion macht den Klassendatenbankanbieter natürlich unabhängig. Die Klasse enthält dann verschiedene Methoden für die Datenbank-CRUD-Operationen, wie zum Beispiel die folgende Methode, die eine Liste von Kundenobjekten zurückgibt:
public List < Customer > GetAll ( )
{
var cmd = @"select cu_code, cu_name, cu_addr1, cu_addr2, cu_postcode, cu_balance " ;
cmd += "from Customers " ;
return _connection . Query < Customer > ( cmd , transaction : transaction ) . ToList ( ) ;
}
In dieser Anwendung wird die Arbeitseinheit durch die Klasse DapperUnitOfWork
dargestellt. Dies ist eine Klasse, die IDisposable implementiert. Es gibt Instanzen beider Arten von Repositorys. Der Konstruktor verwendet die Verbindungszeichenfolge als Parameter und konfiguriert die FluentMap-Zuordnung, sofern dies noch nicht geschehen ist. Anschließend wird eine OleDb-Verbindung geöffnet und eine neue Transaktion gestartet.
public class DapperUnitOfWork : IDisposable
{
private readonly IDbConnection databaseConnection ;
private IDbTransaction databaseTransaction ;
///
/// Initializes a new instance of the class.
/// Sets up the unit of work and configures the FluentMap mappings.
/// Opens the OleDb connection.
///
/// The OleDb connection string.
public DapperUnitOfWork ( string connString )
{
ConnectionString = connString ;
if ( ! FluentMapper . EntityMaps . Any ( m => m . Key == typeof ( Entities . Customer ) ) )
{
FluentMapper . Initialize ( config =>
{
config . AddMap ( new CustomerEntityMap ( ) ) ;
config . AddMap ( new CustomerTransactionEntityMap ( ) ) ;
} ) ;
}
databaseConnection = new OleDbConnection ( ConnectionString ) ;
databaseConnection . Open ( ) ;
// Some default setup items for the connection.
// 'Set null off' - any inserts will insert the relevant empty value for the database field type instead of a null
// where a value is not supplied.
// 'set exclusive off' - tables will be opened in shared mode.
// 'set deleted on' - unintuitively this means that table rows marked as deleted will be ignored in SELECTs.
var cmd = $ "set null off { Environment . NewLine } set exclusive off { Environment . NewLine } set deleted on { Environment . NewLine } " ;
databaseConnection . Execute ( cmd ) ;
databaseTransaction = databaseConnection . BeginTransaction ( ) ;
}
Die Repository-Eigenschaften der Klasse erhalten die Datenbanktransaktion, die in ihren Getter eingefügt wird. Der folgende Code gibt entweder ein vorhandenes Repository zurück oder erstellt je nach Bedarf ein neues:
public CustomerRepository ? CustomerRepository
{
get
{
return customerRepository ??= new CustomerRepository ( dbTransaction ) ;
}
}
Die Commit()
-Methode der Klasse versucht, die aktuelle Transaktion festzuschreiben. Jede Ausnahme führt zu einem Rollback und die Ausnahme wird ausgelöst. Es gibt auch eine Rollback()
Methode, mit der die Transaktion explizit zurückgesetzt werden kann. In allen Fällen wird die aktuelle Transaktion verworfen, eine neue erstellt und die Repository-Mitglieder zurückgesetzt.
public void Commit ( )
{
try
{
databaseTransaction . Commit ( ) ;
}
catch
{
databaseTransaction . Rollback ( ) ;
throw ;
}
finally
{
databaseTransaction . Dispose ( ) ;
databaseTransaction = databaseConnection . BeginTransaction ( ) ;
ResetRepositories ( ) ;
}
}
Da sowohl die Repository-Objekte Customer
als auch CustomerTransaction
dieselbe Transaktion verwenden, sind Commit oder Rollback atomar und stellen eine Arbeitseinheit dar.
Sowohl Commit()
als auch Rollback()
Methode rufen explizit die Dispose()
Methode der Klasse auf. Diese Methode kümmert sich darum, die aktuelle Transaktion und Verbindung zu verwerfen und die Repository-Mitglieder zurückzusetzen.
public void Dispose ( )
{
dbTransaction ? . Dispose ( ) ;
dbConnection ? . Dispose ( ) ;
GC . SuppressFinalize ( this ) ;
}
Wichtig In einer dateibasierten Datenbank wie DBF ist es äußerst wichtig, die Transaktion und die Verbindung immer nach Abschluss zu entsorgen. Alle in der Festplattendatei geöffneten Dateihandles können Probleme für andere Anwendungen verursachen./
Dies ist ein einfaches Beispiel – die Einheit der Arbeitsklasse hier ist eine Art „Gottobjekt“, da sie immer eine Instanz jedes Typs von Repository-Klasse enthalten muss. Es ist also ein Kandidat für eine weitere Abstraktion.