Select Only Specific Fields From an Optional Navigation Property - c#

I have two entities - User and Plan
class Plan
{
public int Id { get; set; }
public string Name { get; set; }
public string FieldA { get; set; }
public string FieldB { get; set; }
...
}
class User
{
public int Id { get; set; }
public string Name { get; set; }
public int PlanId { get; set; }
public Plan Plan { get; set; }
}
I want to select a specific user and populate its Plan property, but only with specific fields (Id and Name).
I know it's supposed to be simple, I just need to write
db.Users
.Select(u => new User()
{
Id = u.Id,
Name = u.Name,
PlanId = u.PlanId,
Plan = new Plan()
{
Id = u.Plan.Id,
Name = u.Plan.Name
}
})
.SingleOrDefault(u => u.Id == 1);
The tricky part is that Plan is optional (not in the sense that PlanId can be null, but in the sense that PlanId can point to a Plan object that doesn't exist).
When Plan exists, everything works as expected, but when it doesn't, the Plan property is still populated, with an Id but with a null Name
Example:
{
"Id": 1,
"Name": "some name..",
"PlanId": 9, // There is no Plan object with Id = 9
"Plan": {
"Id": 9,
"Name": null
}
}
What I expect, is for Plan to be null.
How can I achieve it?
Thank you

Why not just put a condition on populating the Plan field?
db.Users
.Select(u => new User()
{
Id = u.Id,
Name = u.Name,
PlanId = u.PlanId,
Plan = u.Plan.Name == null ? null : new Plan()
{
Id = u.Plan.Id,
Name = u.Plan.Name
}
})
.SingleOrDefault(u => u.Id == 1);
After the discussion in the comments another approach would be:
db.Users
.Select(u => new User()
{
Id = u.Id,
Name = u.Name,
PlanId = u.PlanId,
Plan = db.Plans.Find(u.PlanId)
})
.SingleOrDefault(u => u.Id == 1);
Find should return null if the Plan with u.PlanId is not found (in the current context or store)

Use GroupJoin like this
var userPlanPair = db.Users
.GroupJoin(db.Plans, u => u.PlanId, p => p.Id, (user, plans) => new { user, plans })
.SelectMany(pair => pair.plans.DefaultIfEmpty(), (pair, plan) => new { pair.user, plan = plan != null ? new Plan() { Id = plan.Id, Name = plan.Name } : null })
.SingleOrDefault(pair => pair.user.Id == 1);
The combination of GroupJoin, SelectMany and DefaultIfEmpty translates to a left join in SQL form.
Inside the resultSelector of SelectMany you query the specific fields that you need from Plan.
To get the final result - A User object with a Plan property, you just select the user and plan from the pair.
var user = userPlanPair.user;
user.Plan = userPlanPair.plan;

Related

LINQ To Entities INNER JOIN with COUNT

I'm trying to do replicate the following query in LINQ using the comprehension method. My SQL is as follows:
SELECT COUNT(Post.Id), User.Id, User.Name
FROM Post
INNER JOIN User ON Post.ModeratorId = User.Id
WHERE Post.Status IN ("Live", "Pending", "Expired")
GROUP BY User.Id, User.Name
My LINQ query is as follows but its still returns a 0 count when no moderator has been assigned to the post. Note a Post.ModeratorId is a nullable value.
I only want a list of moderators and a count of post they are or have moderated.
How can I replicate the above SQL in LINQ?
public IEnumerable<ModeratorPostViewModel> GetModeratorPostCount()
{
var posts = _context.Post
.Include(p => p.Moderator)
.Include(p => p.Status)
.Where(p => p.ModeratorId != null && p.Status.Name IN ("Live", "Pending", "Expired"))
.OrderBy(p => p.Moderator.Name)
.Select(p => new ModeratorPostViewModel
{
Id = p.ModeratorId,
Name = p.Moderator.Name,
CountOfPosts = p.Moderator.ModeratorPosts.Count()
})
.ToList();
// Return list
return posts
}
My models are defined as follows:
public class Post
{
int Id { get; set; }
int StatusId { get; set; }
string ModeratorId { get; set; }
// Navigation properties
public Status Status { get; set; }
public ApplicationUser Moderator { get; set; }
// some other other properties
}
public class ApplicationUser : IdentityUser
{
public string Name { get; set; }
// Navigation property
public ICollection<Post> ModeratorPosts { get; set; }
}
I only want a list of moderators and a count of post they are or have moderated
Then base your query on Moderator (or whatever it's called) entity:
var statuses = new [] { "Live", "Pending", "Expired" };
var posts = _context.Moderator
.OrderBy(m => m.Name)
.Select(m => new ModeratorPostViewModel
{
Id = m.Id,
Name = m.Name,
CountOfPosts = m.ModeratorPosts.Count(p => statuses.Contains(p.Status.Name))
})
.Where(m => m.CountOfPosts != 0)
.ToList();
UPDATE: I have to admit that the above LINQ query does not produce a very good SQL, especially with the last Where condition. So you might resort to the exact LINQ equivalent of your SQL query (you missed the GROUP BY part):
var statuses = new [] { "Live", "Pending", "Expired" };
var posts = _context.Post
.Where(p => p.ModeratorId != null && statuses.Contains(p.Status.Name))
.GroupBy(p => new { Id = p.ModeratorId, Name = p.Moderator.Name })
.Select(g => new ModeratorPostViewModel
{
Id = g.Key.Id,
Name = g.Key.Name,
CountOfPosts = g.Count()
})
.OrderBy(m => m.Name)
.ToList();
You can do a group by after filtering the posts by the statuses you are looking for
var s = new List<string>() {"Live", "Pending", "Expired"};
var grouped = db.Post.Where(x => s.Contains(x.Status)) //Fitler for the statuses
.GroupBy(f => f.ModeratorId, po => po,
(k, items) => new ModeratorPostViewModel
{
Name = items.FirstOrDefault().User.Name,
Id=k,
CountOfPosts = items.Count()
}).ToList();

Merging 2 lists of different objects into a third list of a different type?

I'm building an API wrapper for a third party API that translates their objects into business domain objects that can be used for other processing. In this case, I need to take 2 different objects Contact and User and merge them into a single list of objects called UserContacts. I'm matching these objects based on their Email property, and if there is no matching elements, a new one is inserted.
Here are my current objects and methods, I'm just trying to figure out if there's a better/faster method.
public class ContactUser : IUser
{
public string SalesForceUserId { get; set; }
public string SalesForceContactId { get; set; }
public string ZendeskId { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
public string Name { get; set; }
public string Email { get; set; }
}
private List<IUser> MergeContactsAndUsers()
{
var sfContacts = SalesForceCache.Contacts.Data;
var sfUsers = SalesForceCache.Users.Data;
var newUsers = sfUsers.Select(user => new ContactUser
{
SalesForceUserId = user.Id,
Name = user.Name,
FirstName = user.FirstName,
LastName = user.LastName,
Email = user.Email
}).Cast<IUser>().ToList();
foreach (var contact in sfContacts)
{
var tmp = newUsers.FirstOrDefault(n => n.Email == contact.Email);
if (tmp != null)
{
tmp.SalesForceContactId = contact.Id;
}
else
{
var newUser = new ContactUser
{
SalesForceContactId = contact.Id,
Name = contact.Name,
FirstName = contact.FirstName,
LastName = contact.LastName,
Email = contact.Email
};
newUsers.Add(newUser);
}
}
return newUsers;
}
If you want to replace your current implementation with Join you can have something like this:
private List<IUser> MergeContactsAndUsers()
{
var sfContacts = SalesForceCache.Contacts.Data;
var sfUsers = SalesForceCache.Users.Data;
var leftJoinResults =
sfUsers.Join(
sfContacts,
u => u.Email,
c => c.Email,
(u, c) => new ContactUser()
{
SalesForceContactId = c.SalesForceContactId,
SalesForceUserId = u.Id,
Name = u.Name,
FirstName = u.FirstName,
LastName = u.LastName,
Email = u.Email
}).Cast<IUser>().ToList();
var rightJoinResults =
sfContacts
.Where(c => !leftJoinResults.Select(nu => nu.SalesForceContactId).Contains(c.Id))
.Select(c => new ContactUser
{
SalesForceContactId = c.Id,
Name = c.Name,
FirstName = c.FirstName,
LastName = c.LastName,
Email = c.Email
});
leftJoinResults.AddRange(rightJoinResults);
return leftJoinResults;
}
But because Join is only a left join (and you need right join as well) it still requires an additional query to get missing contacts (the query to get rightJoinResults).
It's more of an alternative implementation with use of Join. Without proper measurements it's hard to tell whether it's faster.

Issue Related to SelectMany function in LINQ

I have two tables in Database:
PostCalculationLine
PostCaluclationLineProduct
PostCalculationLineProduct(table2) contains Foriegn key of PostCalucationLineId(table1)
In C# code I have two different Models for these two tables as follows:
public class PostCalculationLine : BaseModel
{
public long Id{ get; set; }
public string Position { get; set; }
public virtual Order Order { get; set; }
public virtual Task Task { get; set; }
//some other properties go here
public virtual IList<PostCalculationLineProduct> PostCalculationLineProducts { get; set; }
}
and
public class PostCalculationLineProduct : BaseModel
{
public long Id {get;set;}
public string Description { get; set; }
//some other properties go here
}
Now in Entityframework code, I fetch data from PostCalculationLineProduct as follows:
PostCalculationLineRepository pclr = new PostCalculationLineRepository();
DataSourceResult dsrResult = pclr.Get()
.SelectMany(p => p.PostCalculationLineProducts)
.Where(c => c.Product.ProductType.Id == 1 && c.DeletedOn == null)
.Select(c => new HourGridViewModel()
{
Id = c.Id,
Date = c.From,
EmployeeName = c.Employee != null ?c.Employee.Name:string.Empty,
Description= c.Description,
ProductName = c.Product != null?c.Product.Name :string.Empty,
From = c.From,
To = c.Till,
Quantity = c.Amount,
LinkedTo = "OrderName",
Customer ="Customer"
PostCalculationLineId = ____________
})
.ToDataSourceResult(request);
In the above query I want to get PostCalculationLineId(from Table1) marked with underLine. How can I achieve this?
Thanks
You can use this overload of SelectMany to achieve this:-
DataSourceResult dsrResult = pclr.Get()
.SelectMany(p => p.PostCalculationLineProducts,
(PostCalculationLineProductObj,PostCalculationLineObj) =>
new { PostCalculationLineProductObj,PostCalculationLineObj })
.Where(c => c.PostCalculationLineProductObj.Product.ProductType.Id == 1
&& c.PostCalculationLineProductObj.DeletedOn == null)
.Select(c => new HourGridViewModel()
{
Id = c.PostCalculationLineProductObj.Id,
Date = c.PostCalculationLineProductObj.From,
//Other Columns here
PostCalculationLineId = c.PostCalculationLineObj.Id
};
This will flatten the PostCalculationLineProducts list and returns the flattened list combined with each PostCalculationLine element.

Select distinct records from list of keywords

I need to generate an autocomplete sugestions list of Keywords based on a search, where each keyword has a set of KeywordSearch:
Keyword class:
public class Keyword
{
public int Id { get; set; }
public string Name { get; set; }
}
public class KeywordSearch
{
// Primary properties
public int Id { get; set; }
public string Name { get; set; }
public Keyword Keyword { get; set; }
}
So If have a keyword like "Company Name", I will have the KeywordSearch "Company" and "Name".
The function I have now, that is not working well is:
public IList<KeywordDto> GetAllBySearch(string keywords, int numberOfRecords)
{
var splitKeywords = keywords.Split(new Char[] { ' ' });
var keywordQuery = _keywordRepository.Query.Where(p => p.IsActive == true);
var keywordSearchQuery = _keywordSearchRepository.Query;
var keywordIds = keywordSearchQuery
.GroupBy(k => k.Keyword.Id)
.Where(g => splitKeywords.All(w => g.Any(k => w.Contains(k.Name))))
.Select(g => g.Key);
IList<KeywordDto> keywordList = (from kw in keywordQuery
join kwids in keywordIds on kw.Id equals kwids
select new KeywordDto { Id = kw.Id, Name = kw.Name })
.Take(numberOfRecords)
.Distinct()
.OrderBy(p => p.Name).ToList();
return keywordList;
}
I need to build a KeywordList based on the keywords string, so if keywords = "Compa" I return "Company Name" with the part "Comp" with bold style , or if keywords = "Compa Nam" I return "Company Name" with "Compa Nam" with bold style etc...
Now what is happening is that it's not able to find the part "Comp" in the KeywordSearch.
Any Suggestion?
Thanks
If I'm not mistaken w.Contains(k.Name) is the key part.
w is "Compa", k.Name is you KeywordSearch "Company" and "Name". So you're asking whether "Compa" contains "Company" or "Name", which is false.
k.Name.Contains(w) (or k.Name.StartsWith(w, StringComparison.CurrentCultureIgnoreCase) if you don't want it to be case sensitive) should return the correct result.

A way around LINQ to Entities does not recognize the method?

I've got a method I've been using against IEnumerable no problem. However I want to start using it against IQueryable as well. However, the way I currently have it wont work as its trying to execute my method against the database.
The situation is as follows. I want a property on the object I'm selecting into to be be null if the value selecting from is null or the Id and Name of the property if it exists. For example:
var foos = FooRepository.All().Select(s => new FooBrief()
{
Id = s.Id,
Customer = SimpleData.To(s.Customer, m => m.Id, m => m.Name)
});
where SimpleData.To looks like:
public class SimpleData
{
public int Id { get; set; }
public string Name { get; set; }
public static SimpleData To<T>(T t, Func<T, int> id, Func<T, string> name) where T : class
{
if (t != null)
{
return new SimpleData { Id = id(t), Name = name(t) };
}
return null;
}
}
Is there someway I can get this behaviour whilst allowing it to execute against the database?
NOTE: Because of reasons elsewhere in my code I cannot use .ToList(). I may be adding additional filtering at a later point
The simplest approach is just to perform the selection outside the database, using AsEnumerable:
var foos = FooRepository.All()
.Select(x => new { Id = x.Id,
CustomerId = x.Customer.Id,
CustomerName = x.Name })
.AsEnumerable()
.Select(s => new FooBrief {
Id = s.Id,
Customer = new SimpleData {
Id = s.CustomerId,
Name = s.CustomerName
}
});
The first Select is just to make sure that the database query only pulls out the required fields. If you really still want to use your SimpleData.To method:
// Outdented to avoid scrolling
var foos = FooRepository.All()
.Select(x => new { Id = x.Id,
CustomerId = x.Customer.Id,
CustomerName = x.Name })
.AsEnumerable()
.Select(s => new FooBrief {
Id = s.Id,
Customer = SimpleData.To(s,
s => s.CustomerId,
s => s.CustomerName)
});

Categories

Resources