Related
I have AutoMapper configured in my .NET 5.0 project with a map between an entity (Setting) and its DTO (SettingByProfileDto). The said entity has a child collection of another entity (SettingValue) (one to many). The child collection (SettingValues) of the first entity is mapped to a single item (another DTO : SettingValueDto) inside the DTO because I only need a specific item from this list.
For the mapping configuration, I use the following lines :
int profileId = default;
profile.CreateMap<Setting, SettingByProfileDto>()
.ForMember(dto => dto.SettingValue, opt =>
{
opt.MapFrom(ss =>
ss.SettingValues
.FirstOrDefault(ssv => ssv.ProfileId == profileId)
);
});
When I want to retrieve the first entity I use the AutoMapper ProjectTo method in order to only request the fields the DTO has. I give to the ProjectTo method the value of the profileId parameter so the mapping can know on which Id the filter has to be done :
// ...
.Where(ss => ss.Id == request.Id)
.ProjectTo<SettingByProfileDto>(
_mapper.ConfigurationProvider,
new { profileId = request.ProfileId },
dest => dest.SettingValue
)
// ...
The result of the query and the mapping are both correct. However, the query sent to the database to fetch the results seems to be poorly optimized.
Here is the resulting query :
SELECT [s8].[Description], [s8].[DisplayName], [s8].[Id], [s8].[Name], CASE
WHEN (
SELECT TOP(1) [s].[Id]
FROM [SettingValue] AS [s]
WHERE ([s8].[Id] = [s].[SettingId]) AND ([s].[ProfileId] = #__profileId_1)) IS NULL THEN CAST(1 AS bit)
ELSE CAST(0 AS bit)
END, (
SELECT TOP(1) [s2].[Id]
FROM [SettingValue] AS [s2]
WHERE ([s8].[Id] = [s2].[SettingId]) AND ([s2].[ProfileId] = #__profileId_1)), (
SELECT TOP(1) [s3].[ProfileId]
FROM [SettingValue] AS [s3]
WHERE ([s8].[Id] = [s3].[SettingId]) AND ([s3].[ProfileId] = #__profileId_1)), (
SELECT TOP(1) [s4].[SettingId]
FROM [SettingValue] AS [s4]
WHERE ([s8].[Id] = [s4].[SettingId]) AND ([s4].[ProfileId] = #__profileId_1)), (
SELECT TOP(1) [s7].[Value]
FROM [SettingValue] AS [s7]
WHERE ([s8].[Id] = [s7].[SettingId]) AND ([s7].[ProfileId] = #__profileId_1))
FROM [Setting] AS [s8]
WHERE [s8].[Id] = #__request_Id_0
ORDER BY (SELECT 1)
OFFSET #__p_2 ROWS FETCH NEXT #__p_3 ROWS ONLY
And here is the query expression that is generated by AutoMapper and then transformed to SQL :
DbSet<Setting>()
.AsNoTracking()
.Where(ss => ss.Id == __request_Id_0)
.Select(dtoSetting => new Object_1800281414___SettingValue_Description_DisplayName_Id_Name{
__SettingValue = dtoSetting.SettingValues
.FirstOrDefault(ssv => ssv.ProfileId == __profileId_1),
Description = dtoSetting.Description,
DisplayName = dtoSetting.DisplayName,
Id = dtoSetting.Id,
Name = dtoSetting.Name,
}
)
.Select(dtoLet => new SettingByProfileDto{
Description = dtoLet.Description,
DisplayName = dtoLet.DisplayName,
Id = dtoLet.Id,
Name = dtoLet.Name,
SettingValue = dtoLet.__SettingValue == null ? null : new SettingValueDto{
Id = dtoLet.__SettingValue.Id,
ProfileId = dtoLet.__SettingValue.ProfileId,
SettingId = dtoLet.__SettingValue.SettingId,
Value = dtoLet.__SettingValue.Value
}
}
)
.Skip(__p_2)
.Take(__p_3)
I tried to replace the FirstOrDefault clause in the mapping configuration by a Where (with the same condition) and in this case, the query generated will use a LEFT JOIN which avoid repeating one WHERE per field. However, with this way, I can't map the child collection to a single item but only another collection (of dto).
My questions are the following :
Is there a better (more efficient?) way to achieve what I want (only keep one item of the child collection) ?
Is the above query considered optimized ? If so, I would be able to continue with my current way of doing
Thanks for your help !
AutoMapper projections work by injecting Select calls with generated mapping selectors, or just selectors in the query expression tree where the destination type doesn't match the source type.
The problem is where are these injected. For instance, in your example it generates something like this (pseudo code, MapTo just marks the mapping injection point)
SettingValue = source.SettingValues
.FirstOrDefault(ssv => ssv.ProfileId == profileId)
.MapTo<SettingByProfileDto>()
Here with the predicate version of FirstOrDefault it has no choice (well, relatively, keep reading), but even if you rewrite it to the equivalent Where(predicate) + FirstOrDefault() chain, it still injects the mapping at the end
SettingValue = source.SettingValues
.Where(ssv => ssv.ProfileId == profileId)
.FirstOrDefault()
.MapTo<SettingByProfileDto>()
which is too late, and that's why EF Core generates inefficient query.
Now, one may consider this to be EF Core query translation defect. But if the mapping is injected before the FirstOrDefault() call
SettingValue = source.SettingValues
.Where(ssv => ssv.ProfileId == profileId)
.MapTo<SettingByProfileDto>()
.FirstOrDefault()
then Core produces optimal translation.
I didn't find a way to force AM to do that, and also it is good all that to happen transparently. So I wrote a little custom extension. What it does is to to plug into AutoMapper pipeline and transform appropriately the following Enumerable extension methods (both predicate and non predicate overloads) - First, FirstOrDefault, Last, LastOrDefault, Single, SingleOrDefault.
Here is the source code
using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using AutoMapper.Internal;
using AutoMapper.QueryableExtensions;
using AutoMapper.QueryableExtensions.Impl;
namespace AutoMapper
{
public static class SingeResultQueryMapper
{
public static IMapperConfigurationExpression AddSingleResultQueryMapping(this IMapperConfigurationExpression config)
{
config.Advanced.QueryableBinders.Insert(0, new Binder());
config.Advanced.QueryableResultConverters.Insert(0, new ResultConverter());
return config;
}
static string[] TargetMethodNames => new[]
{
nameof(Enumerable.First),
nameof(Enumerable.FirstOrDefault),
nameof(Enumerable.Last),
nameof(Enumerable.LastOrDefault),
nameof(Enumerable.Single),
nameof(Enumerable.SingleOrDefault),
};
static HashSet<MethodInfo> TargetMethods { get; } =
(from method in typeof(Enumerable).GetTypeInfo().DeclaredMethods
join name in TargetMethodNames
on method.Name equals name
select method).ToHashSet();
static bool IsTarget(IMemberMap propertyMap) =>
propertyMap.SourceType != propertyMap.DestinationType &&
propertyMap.ProjectToCustomSource is null &&
propertyMap.CustomMapExpression?.Body is MethodCallExpression call &&
call.Method.IsGenericMethod &&
TargetMethods.Contains(call.Method.GetGenericMethodDefinition());
class ResultConverter : IExpressionResultConverter
{
public bool CanGetExpressionResolutionResult(ExpressionResolutionResult expressionResolutionResult, IMemberMap propertyMap)
=> IsTarget(propertyMap);
public ExpressionResolutionResult GetExpressionResolutionResult(ExpressionResolutionResult expressionResolutionResult, IMemberMap propertyMap, LetPropertyMaps letPropertyMaps)
=> new(propertyMap.CustomMapExpression.ReplaceParameters(propertyMap.CheckCustomSource(expressionResolutionResult, letPropertyMaps)));
}
class Binder : IExpressionBinder
{
public bool IsMatch(PropertyMap propertyMap, TypeMap propertyTypeMap, ExpressionResolutionResult result)
=> IsTarget(propertyMap);
public MemberAssignment Build(IConfigurationProvider configuration, PropertyMap propertyMap, TypeMap propertyTypeMap, ExpressionRequest request, ExpressionResolutionResult result, IDictionary<ExpressionRequest, int> typePairCount, LetPropertyMaps letPropertyMaps)
{
var call = (MethodCallExpression)result.ResolutionExpression;
var selectors = configuration.ExpressionBuilder.CreateMapExpression(
new(propertyMap.SourceType, propertyMap.DestinationType, request.MembersToExpand, request),
typePairCount, letPropertyMaps.New());
if (selectors == null) return null;
var query = call.Arguments[0];
var method = call.Method.GetGenericMethodDefinition();
if (call.Arguments.Count > 1)
{
// Predicate version of the method
// Convert query.Method(predicate) to query.Where(predicate).Method()
query = Expression.Call(typeof(Enumerable), nameof(Enumerable.Where), new[] { propertyMap.SourceType }, query, call.Arguments[1]);
method = TargetMethods.First(m => m.Name == method.Name && m.GetParameters().Length == 1);
}
method = method.MakeGenericMethod(propertyMap.DestinationType);
foreach (var selector in selectors)
query = Expression.Call(typeof(Enumerable), nameof(Enumerable.Select), new[] { selector.Parameters[0].Type, selector.ReturnType }, query, selector);
call = Expression.Call(method, query);
return Expression.Bind(propertyMap.DestinationMember, call);
}
}
}
}
Just put it in a code file inside the project where you configure AutoMapper, and then enable it with the provided configuration helper extension method (similar to Expression Mapping AutoMapper extension) like this
var mapper = new MapperConfiguration(cfg => {
cfg.AddSingleResultQueryMapping();
// The rest ...
}).CreateMapper();
or with DI
services.AddAutoMapper(cfg => {
cfg.AddSingleResultQueryMapping();
}, /* assemblies with profiles */);
And that's all. Now your original DTO, mapping and ProjectTo will produce optimal SQL query translation (single LEFT OUTER JOIN).
I have 3 tables linked by Foreign Keys: ChangeSet, ObjectChanges, PropertyChanges. These tables have 1-To-Many Relationships with each other. I need to join and project and flatten the results into an anonymous type.
We use Entity Framework at the data layer and I essentially need to make the following Query with linq.
select c.Id as ChangeSetId,
c.Timestamp,
c.Author_Id,
u.Name as [User],
o.id as ObjectChangeId,
o.TypeName,
o.ObjectReference as EntityId,
o.DisplayName,
p.Id as PropertyChangeId,
p.PropertyName,
p.ChangeType,
p.OriginalValue,
p.Value
from ChangeSets c
inner join ObjectChanges o
on c.Id = o.ChangeSetId
left join PropertyChanges p
on p.ObjectChangeId = o.Id
inner join Users u
on u.Id = c.Author_Id
order by c.id desc
The Method in question however looks like this:
GetAllWhereExpression(Expression<Func<ChangeSet, bool>> expression)
The expression in this case is likely to be a Where o.EntityId = [Some Value] and c.TimeStamp > X and < Y.
I got very close I felt in linq with the following but couldn't figure out how to inject the expression: (The .GetRepository().Entities is basically DbSet)
var foo = from c in _uow.GetRepository<ChangeSet>().Entities
join o in _uow.GetRepository<ObjectChange>().Entities on c.Id equals o.ChangeSetId
join p in _uow.GetRepository<PropertyChange>().Entities on o.Id equals p.ObjectChangeId
where expression // This Line Not Valid
select new
{
ChangeSetId = c.Id,
Timestamp = c.Timestamp,
User = c.User.DisplayName,
EntityType = o.TypeName,
EntityValue = o.DisplayName,
Property = p.PropertyName,
OldValue = p.OriginalValue,
NewValue = p.Value
};
I'd prefer to use Lambda syntax but I can't figure out how to construct it. I know I need SelectMany to project and flatten the results but I can't figure out how to use them within the anonymous type for the subcollections:
var queryable = _uow.GetRepository<ChangeSet>().Entities // This is basically the DbSet<ChangeSet>()
.Where(expression)
.SelectMany(c => new
{
ChangeSetId = c.Id,
Timestamp = c.Timestamp,
User = c.User.DisplayName,
EntityType = c.ObjectChanges.SelectMany(o => o.TypeName), //Doesn't work, turns string into char array
//PropertyName = c. this would be a 1 to many on the entity
}
)
How do I craft the linq to produce basically the same results as the sql query?
Here is how it may look like in method syntax.
_uow.GetRepository<ChangeSet>().Entities
.Where(expression)
.Join(_uow.GetRepository<ObjectChanges>().Entities, cs => cs.Id, oc => oc.ChangeSetId,
(cs, oc) => new { cs, oc })
.Join(_uow.GetRepository<PropertyChanges>().Entities, outer => outer.oc.Id, pc => pc.ObjectChangeId,
(outer, pc) => new { cs = outer.cs, oc = outer.cs, pc })
.Join(_uow.GetRepository<User>().Entities, outer => outer.cs.Author_Id, u => u.Id,
(outer, u) => new {
ChangeSetId = outer.cs.Id,
Timestamp = outer.cs.Timestamp,
User = u.DisplayName,
EntityType = outer.oc.TypeName,
EntityValue = outer.oc.DisplayName,
Property = outer.pc.PropertyName,
OldValue = outer.pc.OriginalValue,
NewValue = outer.pc.Value
})
Please follow the given demonstration, Four entities joined by LINQ extension methods.
N.B: .Join use for Inner Join
var foo=_uow.GetRepository<ChangeSet>().Entities
.Join(_uow.GetRepository<ObjectChange>().Entities,
c=>c.Id,
o=>o.ChangeSetId,
(c,o)=>new{ChangeSet=c,ObjectChange=o})
.Join(_uow.GetRepository<PropertyChange>().Entities,
o=>o.ObjectChange.Id,
p=>p.ObjectChangeId,
(o,p)=>new{o.ChangeSet,o.ObjectChange,PropertyChange=p})
.Join(_uow.GetRepository<Users>().Entities,
c=>c.ChangeSet.Author_Id,
u=>u.Id,
(c,u)=>new{c.ChangeSet,c.ObjectChange,c.PropertyChange,User=u})
.Select(x=>new
{
ChangeSetId=x.ChangeSet.Id,
x.ChangeSet.Timestamp,
x.ChangeSet.Author_Id,
User=x.User.Name,
ObjectChangeId=x.ObjectChange.id,
x.ObjectChange.TypeName,
EntityId=x.ObjectChange.ObjectReference,
x.ObjectChange.DisplayName,
PropertyChangeId=x.PropertyChange.Id,
x.PropertyChange.PropertyName,
x.PropertyChange.ChangeType,
x.PropertyChange.OriginalValue,
x.PropertyChange.Value
}).OrderByDescending(x=>x.ChangeSetId).ToList() //ChangeSetId=c.Id
To keep it simple imagine each table has a column named Description and we want to retrieve a flat result like this:
-------------------------------------------------------------------------------------
ChangeSetDescription | ObjectChangeDescription | PropertyChangeDescription |
-------------------------------------------------------------------------------------
Short Answer
You do not need to do a join because EF will figure out the joining for you based on your data model. You can just do this. Imagine ctx is an instance of a class which derives DbContext :
var query = ctx.ChangeSets
.SelectMany(x => x.ObjectChanges, (a, oc) => new
{
ChangeSetDescription = a.Description,
ObjectChangeDescription = oc.Description
,
Pcs = oc.PropertyChanges
})
.SelectMany(x => x.Pcs, (a, pc) => new
{
ChangeSetDescription = a.ChangeSetDescription,
ObjectChangeDescription = a.ObjectChangeDescription,
PropertyChangeDesription = pc.Description
});
var result = query.ToList();
Long Answer
Why do I not need the join?
When you create your EF model, regardless of whether you are using code first, database first or a hybrid approach, if you create the relationships correctly then EF will figure out the joins for you. In a database query, we need to tell the database engine how to join, but in EF we do not need to do this. EF will figure out the relationship based on your model and do the joins using the navigation properties. The only time we should be using joins is if we are joining on some arbitrary property. In other words, we should seldom use joins in EF queries.
Your EF queries should not be written as a direct translation of SQL queries. Take advantage of EF and let it do the work for you.
Can you explain the query in the short answer?
It is the SelectMany method which will throw readers off a little. But it is pretty easy. Let's use a simple class as example:
public class Student
{
public string Name { get; private set; }
public IEnumerable<string> Courses { get; private set; }
public Student(string name, params string[] courses)
{
this.Name = name;
this.Courses = courses;
}
}
Let's create 2 students and add them to a generic list:
var s1 = new Student("George", "Math");
var s2 = new Student("Jerry", "English", "History");
var students = new List<Student> { s1, s2 };
Now let's imagine we need to get the name of the students and the classes they are taking as flat result set. In other words, we do not want Jerry to have a list with 2 subjects but instead we want 2 records for Jerry. Naturally, we will think of SelectMany and do this:
var courses = students.SelectMany(x => new { Name = x.Name, Courses = x.Courses });
But that will not work and cause a compilation error:
The type arguments for method 'Enumerable.SelectMany<TSource, TResult>
(IEnumerable<TSource>, Func<TSource, IEnumerable<TResult>>)'
cannot be inferred >from the usage. Try specifying the type arguments explicitly.
The SelectMany method takes a Func which will return IEnumerable<TResult> NOT TResult so that will not work. Luckily, it has an overload that we can use. But the overload has a, well, intimidating signature:
public static IEnumerable<TResult> SelectMany<TSource, TCollection, TResult>
(
this IEnumerable<TSource> source,
Func<TSource, IEnumerable<TCollection>> collectionSelector,
Func<TSource, TCollection, TResult> resultSelector);
Basically what that is saying is the first Func should return an IEnumerable<TCollection> and the second will receive the TSource and TCollection as input and we can return a TResult as output. Ok so let's do that:
var courses = students.SelectMany(x => x.Courses,
(a, b) => new { Name = a.Name, Course = b });
So in the above line, a is the source which is our students list and b is the string yielded to us one by one from the first Func which we said will return x.Courses. The TResult we are returning is an anonymous type.
Hopefully this example will clarify what is going on in the short answer.
Or what you can do is, since your where expression is for changeSet table, you can filter it when you select from it within your linq
var foo = from c in _uow.GetRepository<ChangeSet>().Entities.Where(expression) //Notice lambda exp here
join o in _uow.GetRepository<ObjectChange>().Entities on c.Id equals o.ChangeSetId
join p in _uow.GetRepository<PropertyChange>().Entities on o.Id equals p.ObjectChangeId
select new
{
ChangeSetId = c.Id,
Timestamp = c.Timestamp,
User = c.User.DisplayName,
EntityType = o.TypeName,
EntityValue = o.DisplayName,
Property = p.PropertyName,
OldValue = p.OriginalValue,
NewValue = p.Value
};
I've some problems with a LINQ query in C#.
I have in the database the same tables that have the same structure.
So, today, I've been troubling with my LINQ query.
More details, I want to join some tables using predicates.
I have a function that has two parameters.
The first parameter is some kind of Context (For example, it may be ProductContext, CarContext, CatContext and etc).
The second parameter is a List<something> that I will join with my first parameter - Context.
I do not want a set of methods.
I've added the sample:
public Element[] GetByIds( MyPredicateContext, Guid[] ids)
{
return
from id in ids
join element in MyPredicateContext on id equals element.Id
select
new Element
{
Id = element.Id,
Description = element.JobDescription,
};
}
If the query is correct, one basic issue that I can see is the return type is Element array whereas you are trying to return IEnumerable. Maybe doing a .ToArray() on the result set might solve the problem.
Why not
return MyPredicateContext.Where(element=>ids.Contains(element.Id))
.Select(e=>new Element()
{
Id = element.Id,
Description = element.JobDescription
}).ToArray();
First of all you can't create a new IQueryable from an array this will revert to pulling everything in memory and filtering there. You are working with expressions and not c# code when you do LINQ with SQL, this will only work on in memory stuff (IEnumerable).
Your query will work in SQL if you do it like this
from element in MyPredicateContext
where ids.Contains(element.Id)
select new Element
{
Id = element.Id,
Description = element.JobDescription,
}
Given that the type of IQueryable where T is an Interface or class.
The end method will look something like this
public interface IElement
{
Guid Id { get; }
string JobDescription { get; }
}
public Element[] GetByIds<T>(IQueryable<T> myPredicateContext, Guid[] ids) where T:IElement
{
return (from element in myPredicateContext
where ids.Contains(element.Id)
select new Element
{
Id = element.Id,
Description = element.JobDescription,
}).ToArray();
}
There are ways to do it with no Generics but they are a bit more advanced and will be hard to maintain.
Here is a method that will work on all T types and proper IQueryable will produce good sql just as I pointed out is a bit more advanced and you will need to lookup how expression work.
public static Element[] GetById<T, Tkey>(IQueryable<T> items,Tkey[] ids)
{
var type = typeof(T);
ParameterExpression param = Expression.Parameter(type);
var list = Expression.Constant(ids);
//The names of the properties you need to get if all models have them and are named the same and are the same type this will work
var idProp = Expression.Property(param, "Id");
var descriptionProp = Expression.Property(param, "JobDescription");
var contains = typeof(Enumerable).GetMethods().First(m => m.Name == "Contains" && m.GetParameters().Count() == 2).MakeGenericMethod(typeof(Tkey));
var where = Expression.Lambda<Func<T, bool>>(Expression.Call(contains, list, idProp), param);
return (items.
Where(where).
Select(Expression.Lambda<Func<T, Element>>(
Expression.MemberInit(
Expression.New(typeof(Element)),
Expression.Bind(typeof(Element).GetProperty("Id"), idProp),
Expression.Bind(typeof(Element).GetProperty("Description"), descriptionProp)),
param))).ToArray();
}
Call GetById(items, new Guid[] { Guid.NewGuid() })
I'm looking for a way to dynamically create a select list from a iQueryable object.
Concrete example, i want to do something like the following:
public void CreateSelectList(IQueryable(of EntityModel.Core.User entities), string[] columns)
{
foreach(var columnID in columns)
{
switch(columnID)
{
case "Type":
SelectList.add(e => e.UserType);
break;
case "Name":
SelectList.add(e => e.Name);
break;
etc....
}
}
var selectResult = (from u in entities select objSelectList);
}
So all properties are known, i however don't know beforehand what properties are to be selected. That will be passed via the columns parameter.
I know i'm going to run into issues with the type of the selectResult type, because when the select list is dynamic, the compiler doesn't know what the properties of the anonymous type needs to be.
If the above is not possible: The scenario I need it for is the following:
I'm trying to create a class that can be implemented to display a paged/filtered list of data. This data can be anything (depends on the implementations).The linq used is linq to entities. So they are directly linked to sql data. Now i want to only select the columns of the entities that i am actually showing in the list. Therefore i want the select to be dynamic. My entity might have a hundred properties, but if only 3 of them are shown in the list, i don't want to generate a query that selects the data of all 100 columns and then only uses 3 of them. If there is a different approach that I haven't thought of, I'm open to ideas
Edit:
Some clarifications on the contraints:
- The query needs to work with linq to entities (see question subject)
- an entity might contain 100 columns, so selecting ALL columns and then only reading the ones i need is not an option.
- The end user decides what columns to show, so the columns to select are determined at run time
- i need to create a SINGLE select, having multiple select statements means having multiple queries on the database, which i don't want
Dynamic select expression to a compile time known type can easily be build using Expression.MemberInit method with MemberBindings created using the Expression.Bind method.
Here is a custom extension method that does that:
public static class QueryableExtensions
{
public static IQueryable<TResult> Select<TResult>(this IQueryable source, string[] columns)
{
var sourceType = source.ElementType;
var resultType = typeof(TResult);
var parameter = Expression.Parameter(sourceType, "e");
var bindings = columns.Select(column => Expression.Bind(
resultType.GetProperty(column), Expression.PropertyOrField(parameter, column)));
var body = Expression.MemberInit(Expression.New(resultType), bindings);
var selector = Expression.Lambda(body, parameter);
return source.Provider.CreateQuery<TResult>(
Expression.Call(typeof(Queryable), "Select", new Type[] { sourceType, resultType },
source.Expression, Expression.Quote(selector)));
}
}
The only problem is what is the TResult type. In EF Core you can pass the entity type (like EntityModel.Core.User in your example) and it will work. In EF 6 and earlier, you need a separate non entity type because otherwise you'll get NotSupportedException - The entity or complex type cannot be constructed in a LINQ to Entities query.
UPDATE: If you want a to get rid of the string columns, I can suggest you replacing the extension method with the following class:
public class SelectList<TSource>
{
private List<MemberInfo> members = new List<MemberInfo>();
public SelectList<TSource> Add<TValue>(Expression<Func<TSource, TValue>> selector)
{
var member = ((MemberExpression)selector.Body).Member;
members.Add(member);
return this;
}
public IQueryable<TResult> Select<TResult>(IQueryable<TSource> source)
{
var sourceType = typeof(TSource);
var resultType = typeof(TResult);
var parameter = Expression.Parameter(sourceType, "e");
var bindings = members.Select(member => Expression.Bind(
resultType.GetProperty(member.Name), Expression.MakeMemberAccess(parameter, member)));
var body = Expression.MemberInit(Expression.New(resultType), bindings);
var selector = Expression.Lambda<Func<TSource, TResult>>(body, parameter);
return source.Select(selector);
}
}
with sample usage:
var selectList = new SelectList<EntityModel.Core.User>();
selectList.Add(e => e.UserType);
selectList.Add(e => e.Name);
var selectResult = selectList.Select<UserDto>(entities);
What you are going for is possible, but it's not simple. You can dynamically build EF queries using the methods and classes in the System.Linq.Expressions namespace.
See this question for a good example of how you can dynamically build your Select expression.
I believe this is what you need:
var entities = new List<User>();
entities.Add(new User { Name = "First", Type = "TypeA" });
entities.Add(new User { Name = "Second", Type = "TypeB" });
string[] columns = { "Name", "Type" };
var selectResult = new List<string>();
foreach (var columnID in columns)
{
selectResult.AddRange(entities.Select(e => e.GetType().GetProperty(columnID).GetValue(e, null).ToString()));
}
foreach (var result in selectResult)
{
Console.WriteLine(result);
}
This code outputs:
First
Second
TypeA
TypeB
UPDATE (according to comments)
// initialize alist of entities (User)
var entities = new List<User>();
entities.Add(new User { Name = "First", Type = "TypeA", SomeOtherField="abc" });
entities.Add(new User { Name = "Second", Type = "TypeB", SomeOtherField = "xyz" });
// set the wanted fields
string[] columns = { "Name", "Type" };
// create a set of properties of the User class by the set of wanted fields
var properties = typeof(User).GetProperties()
.Where(p => columns.Contains(p.Name))
.ToList();
// Get it with a single select (by use of the Dynamic object)
var selectResult = entities.Select(e =>
{
dynamic x = new ExpandoObject();
var temp = x as IDictionary<string, Object>;
foreach (var property in properties)
temp.Add(property.Name, property.GetValue(e));
return x;
});
// itterate the results
foreach (var result in selectResult)
{
Console.WriteLine(result.Name);
Console.WriteLine(result.Type);
}
This code outputs:
First
TypeA
Second
TypeB
I understand that LINQ can't use properties that are not mapped to a database column, though I don't understand why one LINQ statement works inside a non static method but I get this error when attempting within one.
Here's my working method:
private TemplatesAPIContext db = new TemplatesAPIContext();
// GET api/Template
public IQueryable<TemplateDto> GetTemplates()
{
return db.TemplateModels.Include(t => t.Categories).Select(
x => new TemplateDto
{
TemplateID = x.TemplateID,
Name = x.Name,
HTMLShowcase = x.HTMLShowcase,
ShortDescription = x.ShortDescription,
CreationDate = x.CreationDate,
Downloads = x.Downloads,
Tags = x.Tags,
Categories = db.CategoryModels
.Where(c => x.Categories.Where(a => a.TemplateID == x.TemplateID)
.Select(a => a.CategoryID).Contains(c.CategoryID))
}
);
}
I don't want to repeat myself with this complex building of a DTO (I actually still need to add some other relationships still to it and it will get much more complex) and type this out on every method in the controller so I wanted to make a lambda expression and pass it to the methods.
So I did this:
private static readonly Expression<Func<TemplateModel, TemplateDto>> AsTemplateDto =
x => new TemplateDto
{
TemplateID = x.TemplateID,
Name = x.Name,
HTMLShowcase = x.HTMLShowcase,
ShortDescription = x.ShortDescription,
CreationDate = x.CreationDate,
Downloads = x.Downloads,
Tags = x.Tags,
Categories = new TemplatesAPIContext().CategoryModels
.Where(c => x.Categories.Where(a => a.TemplateID == x.TemplateID)
.Select(a => a.CategoryID).Contains(c.CategoryID))
};
In the hopes of calling:
// GET api/Template
public IQueryable<TemplateDto> GetTemplates()
{
return db.TemplateModels.Include(t => t.Categories).Select(AsTemplateDto);
}
But this returns this error, which doesn't make sense to me since its the exact same query, only difference being that I need to instantiate the dbContext in the lambda since I can't use the one instantiated in the controller as the lambda expression is static.
Error
The specified type member 'CategoryModels' is not supported in LINQ to Entities. Only initializers, entity members, and entity navigation properties are supported.
It's important that the same context be used within the query as the one that's making the query, for the query provider to understand what you're trying to do. So all you need is a way of making a copy of that expression that's specific to a given context, which isn't that hard, you've done almost all of the work.
//TODO rename method as appropriate
private static Expression<Func<TemplateModel, TemplateDto>>
CreateTemplateDTO(TemplatesAPIContext context)
{
return x => new TemplateDto
{
TemplateID = x.TemplateID,
Name = x.Name,
HTMLShowcase = x.HTMLShowcase,
ShortDescription = x.ShortDescription,
CreationDate = x.CreationDate,
Downloads = x.Downloads,
Tags = x.Tags,
Categories = context.CategoryModels
.Where(c => x.Categories.Where(a => a.TemplateID == x.TemplateID)
.Select(a => a.CategoryID).Contains(c.CategoryID))
};
}
Now you can write:
public IQueryable<TemplateDto> GetTemplates()
{
return db.TemplateModels.Include(t => t.Categories)
.Select(CreateTemplateDTO(db));
}
Your first method is a simple expression tree that contains only simple operations in tree nodes (like assign A to B) thus it can easily be compiled into SQL query.
The other method contains instantiation of TemplatesAPIContext. It's not possible for database query.