How to write dynamic select expression - c#

I need to write some dynamic select expression on entity framework something like in the example.
var list = db.Article
.GroupBy(x => x.CategoryId)
.Select(x => new ArtDto
{
No = x.Select(c => c.NUMBER).FirstOrDefault(),
UserName = x.Key,
Count = x.Count()
})
.ToList();
I can write group by with expression like this:
Expression<Func<Article, int>> groupByExp;
groupByExp = (x) => x.CategoryId;
So I can replace actual expression with groupByExp.
var list = db.Article
.GroupBy(groupByExp)
.Select(x => new ArtDto
{
No = x.Select(c => c.NUMBER).FirstOrDefault(),
UserName = x.Key,
Count = x.Count()
})
.ToList();
I also want to write another expression for select. So I can send it to another function and it will be dynamic on that function.
Expression<Func<Article, bool>> selectExp;
selectExp = (x) => new ArtDto { ... };
Is it possible? Do you have any idea or tutorial for that?

Yes it is possible,
before start you need to:
Create the new object for selected properties
Map your model to the new object
lets consider that you have your model Article and you need to return the new model ArticleSummary as below
public class Article {
public int id { get; set; }
public string Title { get; set; }
public string Introduction { get; set; }
public string AuthorId { get; set; }
public AppUser Author { get; set; }
public DateTime PublishDate { get; set; }
}
public class ArticleSummary {
public int Id { get; set; }
public string Title { get; set; }
public string Introduction { get; set; }
}
and here is the mapping :
Expression<Func<Article, ArticleSummary>> mapArticle = x => new ArticleSummary {
Id = x.Id,
Title = x.Title,
Introduction = x.Introduction
};
and here is the "simplified" data function :
// T is Article model
// U is ArticleSummary model
public async Task<ICollection<U>> SelectListAsync<T, U>(
Expression<Func<T, bool>> search,
Expression<Func<T, U>> select) where T : class
{
var query =
_context.Set<T>()
.Where(search)
.Select(select);
return await query.ToListAsync();
}
you can call it by passing mapping expression to select property.

Your expression should take IIGrouping<T, Article> as first argument (where T is a type of CategoryId). Assuming that CategoryId is int expression can be written like
public static Expression<Func<IGrouping<int, Article>, ArtDto>> SelectExpression()
{
return x => new ArtDto
{
No = x.Select(c => c.NUMBER).FirstOrDefault(),
UserName = x.Key,
Count = x.Count()
};
}

Related

Error when using Select() instead of Include() in a query

I have the following query:
var catInclude = _db.Cat
.Where(x => x.ProvId == request.ProvId)
.Include(x => x.CatItems)
.SingleOrDefault(p => p.Id == request.ProvId
cancellationToken: cancellationToken);
As I don't want to get all properties from CatItems with Include(), I have created the following query:
var catSelect = _db.Cat
.Where(x => x.ProvId == request.ProvId)
.Select(p ==> new
{ Provider = p,
Items = p.CatItems.Select(x => new List<CatItems> { new CatItems
{ Id = x.Id, Name = x.Name, Price = x.Price } }
})})
SingleOrDefault(cancellationToken: cancellationToken);
But something is wrong in the 2nd query because here return _mapper.ProjectTo<CatDto>(cat) I get the following error:
Argument 1: cannot convert from '<anonymous type: Db.Entities.Cat Prov, System.Colletions.Generic.IEnumerable<System.Colletions.Generic.List<Models.CatItems> > Items>' to 'System.Linq.IQueryable'
Here is my CatDto:
public class CatDto
{
public int ProvId { get; set; }
public List<CatItems> CatItems { get; set; }
}
Here are my entities:
public class Prov
{
public int Id { get; set; }
public Cat Cat { get; set; }
}
public class Cat
{
public int Id { get; set; }
public int ProvId { get; set; }
public List<CatItems> CatItems { get; set; }
}
public class CatItems
{
public int Id { get; set; }
public int CatId { get; set; }
public DateTime CreatedOn { get; set; }
}
Is there a way to recreate the 2nd query and use it?
Main difference that instead of returning List of CatItems, your code returns IEnumerable<List<CatItems>> for property Items.
So, just correct your query to project to List:
var catSelect = await _db.Cat
.Where(x => x.ProvId == request.ProvId)
.Select(p => new CatDto
{
ProvId = p.ProvId,
Items = p.CatItems.Select(x => new CatItems
{
Id = x.Id,
Name = x.Name,
Price = x.Price
})
.ToList()
})
.SingleOrDefaultAsync(cancellationToken: cancellationToken);
I mean, even the exception is pretty self-explanatory. Nevertheless:
You are performing a .Select(...). It returns an Anonymous type. So, your catSelect is an anonymous type, thus the AutoMapper fails.
The quickest fix is to just cast (Cat)catSelect before mapping.
Or, you can dig deeper into how does AutoMapper play with anonymous types.
I feel like you can make most of the classes inherent Id and why is public cat CAT {get; set;} i thought you were supposed to initialize some kind of value

Convert IEnumerable to Sum item Linq

I need to convert an IEnumerable to something else to be able to use Sum. What can I do to achieve this?
CsTotCommit = h.Sum(x => x.CSTotCommit)
I receive an error stating that:
CapStackTrancheDTO does not contain a definition for 'Sum' accepting a first argument of type CapStackTrancheDTO.
IEnumerable<CapStackTrancheDTO> debtRank = debt.AsEnumerable()
.Select((g,index) =>
new CapStackTrancheDTO
{
Rankid = index + 1,
CsTrancheId = g.CsTrancheId,
CsId = g.CsId,
CsTotCommit = g.CsTotCommit,
});
IEnumerable<CapStackTrancheDTO> debtSum = debtRank
.Select(h =>
new
{
CsId = h.CsId,
CsTotCommit = h.Sum(x => x.CSTotCommit)
});
Here are the class defintions:
public class CapStackTrancheDTO
{
public int? Rankid { get; set; }
public int? CsTrancheId { get; set; }
public int? CsId { get; set; }
public decimal? CsTotCommit { get; set; }
}
There are multiple records which I want to group by CsId and SUM.
From the comments, you've said that you want to group by CsId and then sum.
Currently, you're not applying any grouping.
Use the .GroupBy method, like this:
IEnumerable<CapStackTrancheDTO> debtSum = debtRank
.GroupBy(h => h.CsId)
.Select(h =>
new CapStackTrancheDTO
{
CsId = h.Key, // the value we grouped on above is now available in `Key`
CsTotCommit = h.Sum(x => x.CSTotCommit)
}
);

LINQ multiple keyword search to PagedList

I'm a bit lost here and I've tried a few different ways to tackle it. So far I'm having a hard time writing out the LINQ to do what I want.
I want to take the user input string which can be multiple keywords split either by whitespace or ",".
This here works grabs the whole search term and compares it to the title in the Post or any tag I may have. I want the user to type in "HTML Preview" which would match a post called, "Preview the World" with the tags "HTML", "CSS", etc....
This query won't work...but I'm trying to modify it so that it does work.
public IPagedList<Post> SearchResultList(string searchTerm, int resultsPerPage, int page)
{
string[] terms = searchTerm.Split(null);
TNDbContext context = DataContext;
return context.Posts
.Include(a => a.Tags)
.Include(b => b.Comments)
.Where(c => (c.Title.Contains(searchTerm) || c.Tags.Any(d => d.Name.StartsWith(searchTerm))) || searchTerm == null)
.OrderByDescending(x => x.Views)
.ToPagedList(page, resultsPerPage);
}
I tried writing this instead of the other "Where" statement
.Where(x => (terms.All(y => x.Title.Contains(y))) || terms == null)
but it keeps throwing this error
Cannot compare elements of type 'System.String[]'. Only primitive types, enumeration types and entity types are supported.
FOR REFERENCE:
public class Post
{
public Post()
{
Tags = new HashSet<Tag>();
Comments = new HashSet<Comment>();
}
public int Id { get; set; }
public string Title { get; set; }
public string UrlTitle { get; set; }
public DateTime Date { get; set; }
public DateTime DateEdited { get; set; }
public string Body { get; set; }
public string Preview { get; set; }
public string PhotoPath { get; set; }
public int Views { get; set; }
//Navigational
public ICollection<Tag> Tags { get; set; }
public ICollection<Comment> Comments { get; set; }
}
public class Tag
{
public Tag()
{
Post = new HashSet<Post>();
}
public int Id { get; set; }
public string Name { get; set; }
public int TimesTagWasUsed { get; set; }
//Navigational
public ICollection<Post> Post { get; set; }
}
You need to start with a base query, and then keep adding where clauses to it for each search term. Try this:
TNDbContext context = DataContext;
//Create the base query:
var query = context.Posts
.Include(a => a.Tags)
.Include(b => b.Comments)
.OrderByDescending(x => x.Views);
//Refine this query by adding "where" filters for each search term:
if(!string.IsNullOrWhitespace(searchTerm))
{
string[] terms = searchTerm.Split(" ,".ToCharArray(),
StringSplitOptions.RemoveEmptyEntries);
foreach(var x in terms)
{
string term = x;
query = query.Where(post => (post.Title.Contains(term) ||
post.Tags.Any(tag => tag.Name.StartsWith(term))));
}
}
//Run the final query to get some results:
var result = query.ToPagedList(page, resultsPerPage);
return result;
You can nest queries with additional 'from' statements, so something like this should work:
var list = (from post in context.Posts.Include(a => a.Tags).Include(b => b.Comments)
from term in terms
where post.Title.Contains(term) || post.Tags.Any(d => d.Name.StartsWith(term))
select post).OrderByDescending(x => x.Views);

LINQ - append MemberBinding expression into exist MemberInit expression

Basic idea is similar to Merging Expression Trees to Reuse in Linq Queries.
In my situation, I have two models and DTOs:
public class User
{
public int Id { get; set; }
public string Name { get; set; }
public Extra Extra { get; set; }
}
public class Extra
{
public int Id { get; set; }
public string Text { get; set; }
}
public class UserDto
{
public int Id { get; set; }
public string Name { get; set; }
public ExtraDto Extra { get; set; }
}
public class ExtraDto
{
public int Id { get; set; }
public string Text { get; set; }
}
and expressions:
Expression<Func<Extra, ExtraDto>> extraSelector = o => new ExtraDto
{
Id = o.Id,
Text = o.Text
};
Expression<Func<User, UserDto>> userSelector = o => new UserDto
{
Id = o.Id,
Name = o.Name
};
Now, I'd like to 'append' extraSelector into userSelector. The pseudo code is like:
var selectorExpression = userSelector.Append(user => user.Extra, extraSelector);
Context.Users.Select(selectorExpression).ToList();
The final expression would be like this:
Expression<Func<User, UserDto>> userSelector = o => new UserDto
{
Id = o.Id,
Name = o.Name,
Extra = new ExtraDto
{
Id = o.Extra.Id,
Text = o.Extra.Text
}
};
I've tried using ExpressionVisitor, but no luck.
Apart from the "merge" of the two selectors, you have to insert the "path" o => o.Extra into the extraSelector and create a new "bind expression" for the property Extra of UserDto.
In fact, i'm playing around with such scenarios within this project, where i've tried to abstract this kind of expression plumbing. Your "merge" would then look like that:
userSelector = extraSelector.Translate()
.Cross<User>(o => o.Extra)
.Apply(o => o.Extra, userSelector);
The Translate extension method is just a little helper to make use of type inference, Cross inserts o => o.Extra into the extraSelector, Apply creates the "bind expression" for the property Extra of UserDto and finally merges the result with userSelector.

How can I compare two classes base their methods?

I want to find out the missing properties in one class by comparing the other class
public class User
{
public int UserID { get; set; }
public string UserName { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
}
public class UserDTO
{
public string UserName { get; set; }
public string FirstName { get; set; }
}
Above I should get the output like "UserID, "LastName" properties are missing in UserDTO.
var list = typeof(User).GetProperties().Select(x => x.Name)
.Except(typeof(UserDTO).GetProperties().Select(y => y.Name))
.ToList();
EDIT
Including suggestions in comments and public Fields
public static IEnumerable<string> Diff(Type t1, Type t2)
{
return t1.GetProperties().Select(p1 => new { Name = p1.Name, Type = p1.PropertyType })
.Concat(t1.GetFields().Select(f1 => new { Name = f1.Name, Type = f1.FieldType }))
.Except(t2.GetProperties().Select(p2 => new { Name = p2.Name, Type = p2.PropertyType })
.Concat(t2.GetFields().Select(f2 => new { Name = f2.Name, Type = f2.FieldType })))
.Select(a => a.Name);
}
Use reflection to get the properties, see Type.GetProperties. Then compare both property lists to find the missing ones.
var UserProperties = typeof(User).GetProperties().Select(p => p.Name);
var UserDTOProperties = typeof(UserDTO).GetProperties().Select(p => p.Name);
var missingProperties = UserProperties.Except(UserDTOProperties);
Take into account that all inherited properties will also be present in these lists, unless yous specify BindingFlags.DeclaredOnly to the GetProperties() method, see BindingFlags.

Categories