سؤال

I'm working with the Entity Framework Code First and trying to map from my entity classes to my DTO classes. But I'm having difficult figuring out how to write the Selector.

In this small example, I have created a Person class and a Address class.

In the DTO classes I have created a Selector, which maps from my Entity to my DTO, but is it not possible to use AddressDto.Selector inside the PersonDto.Selector?

public class Person
    {
        public int Id { get; set; }
        public string Name { get; set; }
        public Address Address { get; set; }
    }

    public class Address
    {
        public int Id { get; set; }
        public string Street { get; set; }
    }

Now I'm trying to map this to a DTO classes.

    public class PersonDto
        {
            public static Expression<Func<Person, PersonDto>> Selector = 
            entity => new PersonDto
                {
                     Id = entity.Id,
                     Name = entity.Name,
                     Address = ??? AddressDTO.Selector
                };

            public int Id { get; set; }
            public string Name { get; set; }
            public AddressDto Address { get; set; }
        }

        public class AddressDto
        {
            public static Expression<Func<Address, AddressDto>> Selector = 
            entity => new AddressDto
                 {
                     Id = entity.Id,
                     Street = entity.Street
                 };

            public int Id { get; set; }
            public string Street { get; set; }
        }

I know that I could just write this inside PersonDto.Selector

Address = new AddressDto
                     {
                         Id = entity.Address.Id,
                         Street = entity.Address.Street
                     };

But I'm looking for a way to reuse the Selector from the AddressDto class. To keep the code clean and separate responsibility between classes.

هل كانت مفيدة؟

المحلول

So, we're going to need several helper methods here, but once we have them, things should be fairly simple.

We'll start out with this class that can replace all instance of one expression with another:

internal class ReplaceVisitor : ExpressionVisitor
{
    private readonly Expression from, to;
    public ReplaceVisitor(Expression from, Expression to)
    {
        this.from = from;
        this.to = to;
    }
    public override Expression Visit(Expression node)
    {
        return node == from ? to : base.Visit(node);
    }
}

Then an extension method to make calling it easier:

public static Expression Replace(this Expression expression,
    Expression searchEx, Expression replaceEx)
{
    return new ReplaceVisitor(searchEx, replaceEx).Visit(expression);
}

Next we'll write a compose extension method. This will take a lambda that computes an intermediate result, and then another lambda that computes a final result based on the intermediate result and returns a new lambda that takes what the initial lambda returns and returns the output of the final lambda. In effect it calls one function, and then calls another on the result of the first, but with expressions, not methods.

public static Expression<Func<TFirstParam, TResult>>
    Compose<TFirstParam, TIntermediate, TResult>(
    this Expression<Func<TFirstParam, TIntermediate>> first,
    Expression<Func<TIntermediate, TResult>> second)
{
    var param = Expression.Parameter(typeof(TFirstParam), "param");

    var newFirst = first.Body.Replace(first.Parameters[0], param);
    var newSecond = second.Body.Replace(second.Parameters[0], newFirst);

    return Expression.Lambda<Func<TFirstParam, TResult>>(newSecond, param);
}

Now we'll create a Combine method. This will be similar, but subtly different. It will take a lambda that computes an intermediate result, and a function that uses both the initial input, and the intermediate input, to compute a final result. It's basically the same as the Compose method, but the second function also gets to know about the first parameter:

public static Expression<Func<TFirstParam, TResult>>
    Combine<TFirstParam, TIntermediate, TResult>(
    this Expression<Func<TFirstParam, TIntermediate>> first,
    Expression<Func<TFirstParam, TIntermediate, TResult>> second)
{
    var param = Expression.Parameter(typeof(TFirstParam), "param");

    var newFirst = first.Body.Replace(first.Parameters[0], param);
    var newSecond = second.Body.Replace(second.Parameters[0], param)
        .Replace(second.Parameters[1], newFirst);

    return Expression.Lambda<Func<TFirstParam, TResult>>(newSecond, param);
}

Okay, now that we have all of this, we get to use it. First thing we'll do is create a static constructor; we won't be able to inline all we have to do into the field initializer. (The other option is to make a static method that computes this, and have the initializer call it.)

After that we'll create an expression that take a person and returns it's address. It's one of the missing puzzle pieces in the expressions that you have. Using that, we'll compose that address selector with the AddressDto selector, and then use Combine on that. Using that we have a lambda that takes a Person and an AddressDTO and returns a PersonDTO. So in there we have basically what you wanted to have, but with an address parameter given to us to assign to the address:

static PersonDto()
{
    Expression<Func<Person, Address>> addressSelector =
        person => person.Address;

    Selector = addressSelector.Compose(AddressDto.Selector)
            .Combine((entity, address) => new PersonDto
            {
                Id = entity.Id,
                Name = entity.Name,
                Address = address,
            });
}

نصائح أخرى

First off, I would use only a single class to do this. No need for DTO class that does the same thing.

If you insist on having secondary DTO classes, then I would simply create extension methods for them. Ex: PersonDTO personDto = myPerson.ToDTO()

مرخصة بموجب: CC-BY-SA مع الإسناد
لا تنتمي إلى StackOverflow
scroll top