Mapping private attributes of domain entities with Dapper dot net

那年仲夏 提交于 2021-02-06 11:53:01

问题


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

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!