问题
In most of my projects I use nHibernate + Fluent mapping and recently I've started to play around with Dapper to see if I can move read operations to it.
I follow DDD approach so my domain entities do not have any public setters. For example:
public class User
{
private int _id;
private string _name;
private IList<Car> _carList;
protected User(){} // Fluent Mapping
public User(string id, string name)
{
// validation
// ...
_id = id;
_name = name;
}
public int Id{ get {return _id;} }
public string Name { get {return _name;} }
public IList<Car> CarList { get {return _carList;}}
}
public class Car
{
private int _id;
private string _brand;
protected Car(){} // Fluent Mapping
public Car(string id, string brand)
{
// validation
// ...
_id = id;
_brand= brand;
}
public int Id{ get {return _id;} }
public string Brand { get {return _brand;} }
}
With Fluent nHibernate I'm able to reveal members for mapping:
Id(Reveal.Member<User>("_id")).Column("id");
Map(Reveal.Member<User>("_name")).Column("name");
Is there a way to map my domain entities with Dapper? If so, how?
回答1:
One option is to create a separate set of persistence classes to work with Dapper; for example: UserRecord and CarRecord. The record classes will match db table and will be encapsulate within persistence module. Dapper queries will run against this classes and then you can have a separate persistence factory which will assemble the domain entities and return them back to the client.
Small example:
var carRecord = DbConnection.Query<CarRecord>("select * from cars where id = @id", new {id = 1});
var carEntity = CarFactory.Build(carRecord);
This creates a nice separation and provides flexibility.
回答2:
For properties with private setters, Dapper
is smart enough to map them automatically, as long as their names match what you get from the database/queries.
Let' say your User
class is the Aggregate Root
public class User : AggregateRoot
{
public int Id { get; private set; }
public string Name { get; private set; }
...
}
and you have GetById
method from the repository to reconstruct the User
public class UserRepository : Repository<User>, IUserRepository
{
private UserRepository(IDbConnection dbConnection) : base(dbConnection) {}
...
public User GetById(int id)
{
const string sql = @"
SELECT *
FROM [User]
WHERE ID = @userId;
";
return base.DbConnection.QuerySingleOrDefault<User>(
sql: sql,
param: new { userId = id }
);
}
}
As long as the sql returns an Id and Name column, those will be automatically mapped to your User class matching properties, even when they have private setters. Nice and clean!
The problem comes when you have relationships
Everything becomes tricky when you have one-to-many objects you need to load up.
Let's say now the User class has a read-only car list that belongs to the User, and some methods you can use to add/remove cars:
public class User : AggregateRoot
{
public int Id { get; private set; }
public string Name { get; private set; }
private readonly IList<Car> _cars = new List<Car>();
public IEnumerable<Car> Cars => _cars;
public void PurchaseCar(Car car)
{
_cars.Add(car);
AddEvent(new CarPurchasedByUser { ... });
}
public void SellCar(Car car)
{
_cars.Remove(car);
AddEvent(new CarSoldByUser { ... });
}
}
public class Car : Entity
{
public int Id { get; private set; }
public string Brand { get; private set; }
}
Now how do you load up the car list when User class is constructed?
Some suggested to just run multiple queries and you construct the car list after you construct the user by calling the PurchaseCar
and SellCar
methods (or whatever methods available in the class) to add/remove cars:
public User GetById(int id)
{
const string sql = @"
SELECT *
FROM [User]
WHERE ID = @userId;
SELECT *
FROM [Car]
WHERE UserID = @userId;
";
using (var multi = base.DbConnection.QueryMultiple(sql, new { userId = id })
{
var user = multi.Read<User>()
.FirstOrDefault();
if (user != null)
{
var cars = multi.Read<Car>();
foreach (var car in cars)
{
user.PurchaseCar(car);
}
}
return user;
}
}
But you really can't do so if you're practicing Domain-Driven Design
as those methods usually would have additional events they will fire that might be subscribed by others to fire up other commands. You were just trying to initialize your User object.
Reflection!
The only thing that worked for me is to use System.Reflection
!
public User GetById(int id)
{
const string sql = @"
SELECT *
FROM [User]
WHERE ID = @userId;
SELECT *
FROM [Car]
WHERE UserID = @userId;
";
using (var multi = base.DbConnection.QueryMultiple(sql, new { userId = id })
{
var user = multi.Read<User>()
.FirstOrDefault();
if (user != null)
{
var cars = multi.Read<Car>();
// Load user car list using Reflection
var privateCarListField = user.GetType()
.GetField("_cars", BindingFlags.NonPublic | BindingFlags.Instance);
privateCarListField.SetValue(car, cars);
}
return user;
}
}
来源:https://stackoverflow.com/questions/15299946/mapping-private-attributes-of-domain-entities-with-dapper-dot-net