Search code examples
c#linqentity-frameworklinq-expressions

Dynamic Selector Linq To Entities


I have a dynamic selector expression that produces anonymous type. It's working fine in linq to objects, but in linq to entities, it throws:

Attempt 1

NotSupportedException

Only parameterless constructors and initializers are supported in LINQ to Entities.

Expression<Func<User, T>> DynamicSelect<T>(T obj, ParameterExpression userParam)
{
    var newExpression = Expression.New(
        typeof(T).GetConstructor(typeof(T).GenericTypeArguments),
        userParam,
        Expression.Constant("X"),
        Expression.Constant("Y")
    );
    return Expression.Lambda<Func<User, T>>(newExpression, userParam);
}

var userParam = Expression.Parameter(typeof(User), "u");
var obj = new { User = new User(), Address = string.Empty, Fax = string.Empty };
var arr = context.Set<T>()
    .Select(DynamicSelect(obj, userParam))
    .ToArray();

Attempt 2, If I create a custom type, it's working, but I don't want to, because I want to reuse this helper method without creating additional custom type for each entity, I want to be able to pass the type based on consumer.

public class Container
{
    public User User { get; set; }
    public string Address { get; set; }
    public string Fax { get; set; }
}
Expression<Func<User, T>> DynamicSelect<T>(T obj, ParameterExpression userParam)
{
    var initExpression = Expression.MemberInit(
        Expression.New(typeof(T)),
        Expression.Bind(typeof(T).GetProperty("User"), userParam),
        Expression.Bind(typeof(T).GetProperty("Address"), Expression.Constant("X")),
        Expression.Bind(typeof(T).GetProperty("Fax"), Expression.Constant("Y"))
    );
    return Expression.Lambda<Func<User, T>>(initExpression, userParam);
}

var userParam = Expression.Parameter(typeof(User), "u");
var arr = context.Set<T>()
    .Select(DynamicSelect<Container>(null, userParam))
    .ToArray();

Attempt 3, I also tried using Tuple<User, string, string>, but it's not supported too.

NotSupportedException

LINQ to Entities does not recognize the method 'System.Tuple`3[User,System.String,System.String] Create[User,String,String](User, System.String, System.String)' method, and this method cannot be translated into a store expression.

Expression<Func<User, T>> DynamicSelect<T>(T obj, ParameterExpression userParam)
{
    var createExpression = Expression.Call(
        typeof(Tuple), 
        "Create", 
        typeof(T).GenericTypeArguments,
        userParam,
        Expression.Constant("X"), 
        Expression.Constant("Y"));
    return Expression.Lambda<Func<User, T>>(createExpression, userParam);
}

var userParam = Expression.Parameter(typeof(User), "u");
var arr = context.Set<User>()
    .Select(DynamicSelect<Tuple<User, string, string>>(null, userParam))
    .ToArray();

Please help.

update

I was trying to reuse this helper method in any consumer (User, Customer, Associate, etc) without having specific implementation to each consumer.

The class structure look like.

public class User
{
    public int Id { get; set; }
    public string UserName { get; set; }
    public virtual ICollection<Contact> Contacts { get; set; }
}
public class Customer
{
    public int Id { get; set; }
    public string CompanyName { get; set; }
    public virtual ICollection<Contact> Contacts { get; set; }
}
public class Contact
{
    public int Id { get; set; }
    public string Type { get; set; }
    public string Content { get; set; }
}
public class UserDto
{
    public int Id { get; set; }
    public string UserName { get; set; }
    public ContactDto Contact { get; set; }
}
public class CustomerDto
{
    public int Id { get; set; }
    public string CompanyName { get; set; }
    public ContactDto Contact { get; set; }
}
public class ContactDto
{
    public string Email { get; set; }
    public string Address { get; set; }
    public string Fax { get; set; }
    // other contact informations
}

And I have many contacts that could be different for each consumer.

var users = context.Set<User>()
    .Select(x => new UserDto
    {
        Id = x.Id,
        UserName = x.UserName,
        Contact = new ContactDto
        {
            Email = x.Contacts.Where(c => c.Type == "Email").Select(c => c.Value).FirstOrDefault()
        }
    })
    .ToArray();

var customers = context.Set<Customer>()
    .Select(x => new CustomerDto
    {
        Id = x.Id,
        CompanyName = x.CompanyName,
        Contact = new ContactDto
        {
            Address = x.Contacts.Where(c => c.Type == "Address").Select(c => c.Value).FirstOrDefault(),
            Fax = x.Contacts.Where(c => c.Type == "Fax").Select(c => c.Value).FirstOrDefault(),
        }
    })
    .ToArray();

And have refactored the x.Contacts.Where(c => c.Type == "Address").Select(c => c.Value).FirstOrDefault() into expression, but I can't use it directly inside the method like:

var users = context.Set<User>()
    .Select(x => new UserDto
    {
        Id = x.Id,
        UserName = x.UserName,
        Contact = new ContactDto
        {
            Email = GetContactExpression("Email").Compile()(x)
        }
    })
    .ToArray();

It will throw error because Invoke method is not supported in linq to expression, so that I need to refactored the whole Select expression, but I need to make it generic (User has UserName, but Customer has CompanyName, and any other information) and probably passing the contact type(s) too after this get solved. The expected result at the moment would be something lile:

var obj = new { User = new User(), Email = "" };
var users = context.Set<User>()
    .Select(x => DynamicSelect(obj))
    .Select(x => new UserDto
    {
        Id = x.User.Id,
        UserName = x.User.UserName,
        Contact = new ContactDto
        {
            Email = x.Email
        }
    })
    .ToArray();

Solution

  • When I compare your expression with the one created by the compiler for something like u => new { User = u, Address = "X", Fax = "Y" }, the difference is that the latter has filled Members.

    I don't quite understand what is the reason for Members to exist at all, but I would try to set it, my guess is that it will fix your problem. The code might look something like:

    var constructor = typeof(T).GetConstructor(typeof(T).GenericTypeArguments);
    
    var newExpression = Expression.New(
        constructor,
        new Expression[] { userParam, Expression.Constant("X"), Expression.Constant("Y") },
        constructor.GetParameters().Select(p => typeof(T).GetProperty(p.Name)));