Orderby lambda expression on anonymous type - c#

I refactored a linq to entities query to speed it up and broke my orderby lambda feature.
Is there any way to get this to working again since the query is now a join and creating an anonymous type?
Refactored code that is broken because of the orderBy:
public List<UserProductRating> GetUserProductRatings<TKey>(int userId, IPager pager, Func<UserProductRating, TKey> orderBy)
{
var result = _userProductRatingRepo.Table.Where(a => a.UserId == userId)
.Join(_productRepo.Table, outer => outer.ProductId, inner => inner.ProductId,
(outer, inner) => new { UserProductRating = outer, Product = inner })
.OrderByDescending(o => orderBy) // won't work because the query creates an anonymous type above that doesn't match the Func<> definition
.Skip(pager.Skip).Take(pager.PageSize)
.Select(a => new
{
a.UserProductRating.UserId,
a.UserProductRating.ProductId,
a.UserProductRating.VoteCount,
a.UserProductRating.TotalViews,
a.UserProductRating.Rating,
a.Product.Name
}).ToList();
}
Old code that works with orderBy:
public List<UserProductRating> GetUserProductRatings<TKey>(int userId, IPager pager, Func<UserProductRating, TKey> orderBy)
{
return _userProductRatingRepo.Table
.Include(a => a.Product)
.Where(a => a.UserId == userId)
.OrderByDescending(orderBy)
.Skip(pager.Skip)
.Take(pager.PageSize)
.ToList();
}

Since your OrderBy parameter takes a UserProductRating and you include it as one of the anonymous type's properties, you should be able to do this:
public List<UserProductRating> GetUserProductRatings<TKey>(int userId, IPager pager, Func<UserProductRating, TKey> orderBy)
{
var result = _userProductRatingRepo.Table.Where(a => a.UserId == userId)
.Join(_productRepo.Table, outer => outer.ProductId, inner => inner.ProductId,
(outer, inner) => new { UserProductRating = outer, Product = inner })
.OrderByDescending(o => orderBy(o.UserProductRating)) // <-- pass the joined property to the order function
.Skip(pager.Skip).Take(pager.PageSize)
.Select(a => new
{
a.UserProductRating.UserId,
a.UserProductRating.ProductId,
a.UserProductRating.VoteCount,
a.UserProductRating.TotalViews,
a.UserProductRating.Rating,
a.Product.Name
}).ToList();
}

Related

Converting SQL with LEFT JOIN to Linq (Method Syntax)

It's my first post here, so if I get anything wrong let me know and I'll fix it.
I'm struggling to convert a simple SQL statement with a left join, to a LINQ statement (Method syntax). I cannot use Linquer since this is a .Net Core 5.0 MVC project.
Consider that I have two tables:
dbo.OrganisationChannel (Id, OrganisationId, ChannelId)
dbo.Channel (Id, ChannelName, ChannelUrl)
I want to show all channels that an organisation DOESN'T currently have.
Here is the correct SQL query
SELECT c.Id, c.ChannelName, c.ChannelUrl
FROM dbo.Channel c
LEFT JOIN dbo.OrganisationChannel oc ON c.Id = oc.ChannelId
WHERE oc.ChannelId IS NULL OR oc.OrganisationId <> 1
However, the corresponding .GroupJoin and .SelectMany is perplexing me.. I can't find the right place to add the WHERE clauses:
var groupItems = db.Channel
.GroupJoin(
db.OrganisationChannel,
c => c.Id,
oc => oc.ChannelId,
(c, oc) => new { c, oc })
.SelectMany(
x => x.oc.DefaultIfEmpty(),
(chan, orgChan) => new
{
Id = chan.c.Id,
ChannelName = chan.c.ChannelName,
ChannelUrl = chan.c.ChannelUrl,
IsActive = chan.c.IsActive,
}
);
I'd be grateful for any help here, thanks!
Si
Method syntax with LEFT JOIN is a nightmare. If you really want method syntax install Reshaper and click "convert to method chain". But I do not recommend to do that - query become unmaintainable.
Your query is simple with query syntax
var query =
from c in db.Channel
join oc in db.OrganisationChannel on c.Id equals oc.ChannelId into gj
from oc in gj.DefaultIfempty()
where (int?)oc.ChannelId == null || oc.OrganisationId != 1
select new
{
c.Id,
c.ChannelName,
c.ChannelUrl
};
You can use the LeftJoin extension method :
public static IQueryable<TResult> LeftJoin<TResult, Ta, Tb, TKey>(this IQueryable<Ta> TableA, IEnumerable<Tb> TableB, Expression<Func<Ta, TKey>> outerKeySelector, Expression<Func<Tb, TKey>> innerKeySelector, Expression<Func<JoinIntermediate<Ta, Tb>, Tb, TResult>> resultSelector)
{
return TableA.GroupJoin(TableB, outerKeySelector, innerKeySelector, (a, b) => new JoinIntermediate<Ta, Tb> { Value = a, ManyB = b }).SelectMany(intermediate => intermediate.ManyB.DefaultIfEmpty(), resultSelector);
}
public class JoinIntermediate<Ta, Tb>
{
public Ta Value { get; set; }
internal IEnumerable<Tb> ManyB { get; set; }
}
It's usage is similar to the Join extension method but will perform a left join instead of a regular join. Then you can add your call to the Where method right after the call to LeftJoin.
Use the following query instead of lambda expressions
from left in lefts
join right in rights on left equals right.Left into leftRights
from leftRight in leftRights.DefaultIfEmpty()
select new { }
check this url https://dotnettutorials.net/lesson/left-outer-join-in-linq/
also my working code example:
UserApiKeys
.Where(w => w.AppID == AppID && w.IsActive)
.Join(
UserApiApplications,
keys => keys.AppID,
apps => apps.AppID,
(keys, apps) => new { UserApiKeys = keys, UserApiApplications = apps}
)
.OrderByDescending(d => (d.UserApiKeys.ExpirationDate ?? DateTime.MaxValue))
.Select(s => new {
ApiKey = s.UserApiKeys.ApiKey,
IsActive = s.UserApiKeys.IsActive,
SystemName = s.UserApiKeys.SystemName,
ExpirationDate = (s.UserApiKeys.ExpirationDate == null)
? "Newer Expires"
: s.UserApiKeys.ExpirationDate.ToString(),
s.UserApiApplications
})
.ToList()
in addition, to refer #nalka post about extension method usage:
NotificationEvents
.Where(w => w.ID == 123)
.LeftJoin(
Events,
events => events.EventID, ev => ev.EventID,
(events, ev) => new { NotificationEvents = events, Events = ev }
);

Append Filtered Include to Expression

In our project we have texts for multiple languages stored in our database.
I want to create a helper function that includes a text in a query.
This would be useful because this include happens a lot in the application and I want have the include code in one place.
The include should use the new filtered includes from Entity Framework Core 5.
This is what I want to replace:
.Include(c => c.NameTexts.Where(t => t.LanguageId == langId))
Replace it for:
.IncludeText(e => e.NameTexts, langId)
The function I want to write:
// The helper function:
public static IQueryable<T> IncludeText<T>(this IQueryable<T> originalQuery, Expression<Func<T, IEnumerable<UserText>>> textToInclude, int langId) where T : class
{
var textWhere = textToInclude.Where(e => e.LanguageId == langId);
originalQuery.Include(textWhere);
return originalQuery;
}
// And call it from a query like this:
var result = await _context.SomeEntity
.IncludeText(e => e.NameTexts, langId)
// Instead of
// .Include(c => c.NameTexts.Where(t => t.LanguageId == langId))
.Where(c => c.Id == request.Id)
.SingleOrDefaultAsync(cancellationToken);
I tried doing the following but I get an error because the types don't match.
Expression<Func<UserText, bool>> newPred = t => t.LanguageId == langId;
var textWhere = Expression.Lambda<Func<T, IList<UserText>>>(Expression.AndAlso(textToInclude, newPred), textToInclude.Parameters);
originalQuery.Include(textWhere);
return originalQuery;
Maybe this can work :
public static IQueryable<T> IncludeText<T>(this IQueryable<T> originalQuery, Expression<Func<T, IEnumerable<UserText>>> textToInclude, int langId) where T : class
{
var methodWhere = typeof(Enumerable)
.GetMethods()
.First(m => m.ToString() == "System.Collections.Generic.IEnumerable`1[TSource] Where[TSource](System.Collections.Generic.IEnumerable`1[TSource], System.Func`2[TSource,System.Boolean])")
.MakeGenericMethod(typeof(UserText));
Expression<Func<UserText, bool>> predicate = t => t.LanguageId == langId;
var navigationWhere = Expression.Call(methodWhere, textToInclude.Body, predicate);
var lambda = Expression.Lambda<Func<T, IEnumerable<UserText>>>(navigationWhere, Expression.Parameter(typeof(T)));
return originalQuery.Include(lambda);
}
I dislike how Where MethodInfo is recuperated. You can improve by inspiring from this other question.

LINQ ordering with multiple joins

Basic schema that I have inherited (no foreign keys):
Tasks (TaskID, Description, ElementID, UserID)
Elements (ElementID, Description)
Users (UserID, FirstName, LastName)
I need to order a List<Task>, first by Element.Description, then by User.FirstName, then by User.LastName
I've managed so far to order by Element.Description using the following:
List<Task> tasks = db.Tasks.ToList();
List<Element> elements = db.Elements.ToList();
List<User> users = db.Users.ToList();
tasks = tasks.Join(elements,
t => t.ElementID,
e => e.ElementID,
(t, e) => new { t, e })
.OrderBy(m => m.e.Description)
.Select(m => m.t).ToList();
How do I join/order with more than one other entity? (ie. I need to add users to the above query and order by its fields)
(I know query syntax might be more suitable for joins but I've become more familiar/comfortable with lambda syntax, so it would be preferred)
You first need to Join the result of your first Join with the list of User, then use ThenBy to order by the User fields :
tasks = tasks
.Join(elements,
t => t.ElementID,
e => e.ElementID,
(t, e) => new { t, e })
.Join(users,
x => x.t.UserID,
u => u.UserID,
(x, u) => new { x.t, x.e, u })
.OrderBy(m => m.e.Description)
.ThenBy(m => m.u.FirstName)
.ThenBy(m => m.u.LastName)
.Select(m => m.t)
.ToList();

Group by, Count and Lambda Expression

I am trying to translate the following query:
SELECT STATE, COUNT(*)
FROM MYTABLE
GROUP BY STATE;
Into a lambda expression. I am using C# and EntityFramework, however it doesnt seem I can make it work. Here is what I have on my respository so far:
public IEnumerable<object> PorcentajeState(Guid id)
{
return _context.Sates.Where(a => a.Id == id)
.GroupBy(a => a.State)
.Select(n => new { n.StateId , n.Count() });
}
Of course it doesnt compile and I am lost after googling for 2 hours . Could you please help me?
thanks in advance
There are two issues here:
The result of GroupBy will will be an enumerable of type IEnumerable<IGrouping<TKey, TSource>>. The IGrouping interface only has one property you can access, Key which is the key you specified in the GroupBy expression, and implements IEnumerable<T> so you can do other Linq operations on the result.
You need to specify a property name for the anonymous type if it cannot be inferred from a property or field expression. In this case, you're calling Count on the IGrouping, so you need to specify a name for that property.
Try this:
public IEnumerable<object> PorcentajeState(Guid id)
{
return _context.Sates.Where(a => a.Id == id)
.GroupBy(a => a.StateId)
.Select(g => new { g.Key, Count = g.Count() });
}
The equivalent in query syntax would be
public IEnumerable<object> PorcentajeState(Guid id)
{
return from a in _context.Sates
where a.Id == id
group a by a.StateId into g
select new { a.Key, Count = g.Count() };
}
In either case, if you want the first property to be named StateId instead of Key, just change that to
new { StateId = g.Key, Count = g.Count() }
This one is good
public IEnumerable<object> PorcentajeState(Guid id)
{
return _context.Sates.Where(a => a.Id == id)
.GroupBy(a => a.StateId)
.Select(g => new { g.Key, Count = g.Count() });
}
But try this.
public IEnumerable<object> PorcentajeState(Guid id)
{
return _context.Sates.Where(a => a.Id == id)
.GroupBy(a => a.StateId)
.Select(g => new { g.Key.StateId, Count = g.Count() });
}

LINQ query needs either ascending or descending in the same query

Is there anyway this code can be refactored? The only difference is the order by part.
Idealy I'd like to use a delegate/lambda expression so the code is reusable but I don't know how to conditionally add and remove the query operators OrderBy and OrderByDescending
var linq = new NorthwindDataContext();
var query1 = linq.Customers
.Where(c => c.ContactName.StartsWith("a"))
.SelectMany(cus=>cus.Orders)
.OrderBy(ord => ord.OrderDate)
.Select(ord => ord.CustomerID);
var query2 = linq.Customers
.Where(c => c.ContactName.StartsWith("a"))
.SelectMany(cus => cus.Orders)
.OrderByDescending(ord => ord.OrderDate)
.Select(ord => ord.CustomerID);
You can create your own reusable extension method which will do this:
public static IOrderedQueryable<TSource> OrderBy<TSource, TKey>
(this IQueryable<TSource> source,
Expression<Func<TSource, TKey>> keySelector,
bool ascending)
{
return ascending ? source.OrderBy(keySelector)
: source.OrderByDescending(keySelector);
}
and similarly for ThenBy:
public static IOrderedQueryable<TSource> ThenBy<TSource, TKey>
(this IOrderedQueryable<TSource> source,
Expression<Func<TSource, TKey>> keySelector,
bool ascending)
{
return ascending ? source.ThenBy(keySelector)
: source.ThenByDescending(keySelector);
}
You can split your query up into bits, and use control flow logic. LINQ to SQL will magically construct the correct query as if you had typed it all one line! The reason this works is that the query is not sent to the database until you request the data, but instead is stored as an expression.
var linq = new NorthwindDataContext();
var query = linq.Customers
.Where(c => c.ContactName.StartsWith("a"))
.SelectMany(cus=>cus.Orders);
IOrderedQueryable<Order> query2;
if (useAscending) {
query2 = query.OrderBy(ord => ord.OrderDate);
} else {
query2 = query.OrderByDescending(ord => ord.OrderDate);
}
var query3 = query2.Select(ord => ord.CustomerID);
return from T in bk.anbarsabts
where T.kalaname == str
orderby T.date descending
select new { T.date, T.kalaname, T.model, T.tedad };
With numbers, etc you can normally just negate the 'ordering variable'.
With DateTime, I am not so sure. You could try using a Timespan.
Well, If you have a condition where you decide if the order by is ascending or descending you can use this
var query1 = linq.Customers
.Where(c => c.ContactName.StartsWith("a"))
.SelectMany(cus=>cus.Orders)
if(SortAscending)
query1 = query1.OrderBy(ord => ord.OrderDate);
else
query1 = query1.OrderByDescending(ord => ord.OrderDate);
var query2 = query1.Select(ord => ord.CustomerID);

Categories