LINQ to SQL big query tuple - c#

I didn't plan project correctly from beginning so I ended up having many SQL Queries scatterd around page.
So to avoid catching Sql exceptions in every other method or missing half of page due to exception in one of methods I want to pull all the data I need so :
This is method from dll file, I need to return it so I can work with objects after context dispose but I am doing something awfully wrong (first query finalproduct returns empty):
public static Tuple<Product, IEnumerable<Tag>, IEnumerable<ProductComment>, IEnumerable<ProductVote>, IEnumerable<Review>, IEnumerable<ReviewComment>, IEnumerable<ReviewVote>> GetBigProduct(int productID)
{
using (ProductClassesDataContext context = new ProductClassesDataContext())
{
var outproduct = from product in context.Products
where product.ID == productID
join producttag in context.ProductTags on product.ID equals producttag.productID
join tag in context.Tags on producttag.TagID equals tag.ID
join productComment in context.ProductComments on product.ID equals productComment.productID
join productVote in context.ProductVotes on product.ID equals productVote.productID
join review in context.Reviews on product.ID equals review.productID
join reviewComment in context.ReviewComments on review.ID equals reviewComment.reviewID
join reviewVote in context.ReviewVotes on review.ID equals reviewVote.reviewID
select new Tuple<Product, Tag, ProductComment, ProductVote, Review, ReviewComment, ReviewVote>
(product, tag, productComment, productVote, review, reviewComment, reviewVote);
var finalProduct = (from t in outproduct select t.Item1).Single();
var finalTags = (from t in outproduct select t.Item2).ToList();
var finalProductComments = (from t in outproduct select t.Item3).ToList();
var finalProductVotes = (from t in outproduct select t.Item4).ToList();
var finalReviews = (from t in outproduct select t.Item5).ToList();
var finalReviewsComments = (from t in outproduct select t.Item6).ToList();
var finalReviewsVotes = (from t in outproduct select t.Item7).ToList();
return new Tuple<Product, IEnumerable<Tag>, IEnumerable<ProductComment>, IEnumerable<ProductVote>, IEnumerable<Review>, IEnumerable<ReviewComment>, IEnumerable<ReviewVote>>
(finalProduct, finalTags, finalProductComments, finalProductVotes, finalReviews, finalReviewsComments, finalReviewsVotes);
}
}

Are you sure there is only one element returned by from t in outproduct select t.Item1 query?
According to documentation, Single():
Returns the only element of a sequence, and throws an exception if
there is not exactly one element in the sequence.
You may need First() or FirstOrDefault() instead.
Update after comments
I think the problem is with your joins. Actually, I don't think you want to do joins here. What you need, I guess, is something like following:
var outproduct = from product in context.Products
where product.ID == productID
select new Tuple<Product, ProductComment, Tag>(
product, // product itself
context.ProductComments.Where(p => p.productID == product.ID).ToList(), // list of comments for this product
(from pt in context.ProductTags
join tag in context.Tags on pt.TagID equals tag.ID
where pt.productID = product.ID
select tag).ToList() // list of tags for this product
// ... more dimensions in tuple
);
Then you will get, out of outproduct query, a collection of of tuples containing <Product, List of ProductComments, List of Tags>.
You can easily add rest of your tuple dimensions. I removed them for simplicity.
You can now returns this tuple directly from your function doing just
return outproduct.FirstOfDefault()
There is no need for your var finalProduct =-style lines and rebuilding the final tuple.

If you want to find the source of the problem,
start with the query without any joins, comment out the results lines except for the first line
var outproduct = from product in context.Products
where product.ID == productID
select new Tuple<Product>(product);
if this gives you some results, add the next join and try again. keep on adding joins, one by one, until your query fails. then you should have and idea where the problem is.

If you use FirstOrDefault() instead of Single() that wouldn't return null.
Hope it helps.

Related

How do I group one to many in a nested fashion with linq to entities?

I have three tables, Chart, ChartQuestion, ChartAnswer
A Chart has many ChartQuestions, a ChartQuestion has many ChartAnswers.
I'd like to do a linq query that gives me an object containing all of this data, so it'd be a ChartObject containing List<ChartQuestion> and each ChartQuestion contains a List<ChartAnswer>
I started with this:
(from chart in db.Chart
join chartQuestion in db.chartQuestion on chart.ChartId equals chartQuestion.ChartId into chartQuestions)
This seems to be the first step. However I want to include the ChartAnswers now, so I have to do another join to pull back the ChartAnswers but I don't know how to do this.
I can't do a join, and while I can do a from, I am not sure of the exact syntax.
(from chart in db.Chart
join chartQuestion in db.chartQuestion on chart.ChartId equals chartQuestion.ChartId into chartQuestions
from chartQuestionsSelection in chartQuestions
join chartAnswer in context.ChartAnswers on chartQuestions.ChartAnswerId equals chartAnswer.ChartAnswerId into chartAnswers // This is wrong
)
With that code above you end up as chartAnswers being separate to the chartQuestions rather than belonging to them, so I don't think it is correct.
Any idea?
joining a table destructors it i.e. you now have the row as a variable if you need it in a nested fashion then select in like this
(from chart in db.Chart
select new { //can also be your own class/record, perhaps a DTO
chart.Id,
chart.Name,
questions = (from chartQuestion in db.chartQuestion
where chart.ChartId == chartQuestion.ChartId
select new { //perhaps a dto
chartQuestion.Id,
chartQuestion.Question,
Answers =
(from chartAnswer in context.ChartAnswers
where chartAnswer.ChartAnswerId == chartQuestion.ChartAnswerId
select chartAnswer).ToList() //this is translated and not evaluated
}).ToList() //this is translated not evaluated
}).ToListAsync(cancellationToken) //this will evaluate the expression
If you need it by joins then you can group join it like this:
from m in _context.Chart
join d in _context.ChartQuestions
on m.ID equals d.ID into mdJoin
select new
{
chartId = m.ID,
chartName = "m.name",
quess = from d in mdJoin
join dd in _context.ChartAnswer
on d.Iddomain equals dd.DomainId into anJoin
select new
{
quesId = d.ID,
quesName = d.Question,
anss = anJoin
}
}
A better way: If you edit your DbConfigurations to include navigation properties of each then the code will become way simpler.
example:
db.Chart
.Include(x => x.chartQuestions)
.ThenInclude(x => x.chartAnswers)
You can search more about how to do navigation properties in EFCore, but ofcourse if the code base is large then this might not be feasible for you.
Try following :
(from chart in db.Chart
join chartQuestion in db.chartQuestion on chart.ChartId equals chartQuestion.ChartId
join chartAnswer in context.ChartAnswers on chartAnswer.ChartAnswerId equals chartQuestion.ChartAnswerId
)

Linq - how to filter dataset based on data in another table?

I'm looking to filter the data in a dataset much like you'd do a where <value> in (select <value> from other_table where year=2016)
So I have a list of the "values":
var BUs = (from b in dc.BusinessUnits
where b.Year == int.Parse(ddlYears.SelectedValue)
orderby b.BuName
select new { b.BUID }).ToList();
So what I need to do is filter this dataset based on the BUID list returned in the BUs var.
IQueryable<Market> markets = (from p in dc.Markets
orderby p.MarketName
select p);
Help? I'm 100% new to linq so I need a concise solution.
Well if you Market entity has a BUID property and this property it's a primitive type (int, string..) or an enum, you can use Contains method:
var BUs = (from b in dc.BusinessUnits
where b.Year == int.Parse(ddlYears.SelectedValue)
orderby b.BuName
select b.BUID );
IQueryable<Market> markets = (from p in dc.Markets
where BUs.Contains(p.BUID)
orderby p.MarketName
select p);
The standard way of filtering by in memory id list is to use Enumerable.Contains method. But you need first to make sure your list contains ids - they way you wrote it it will contain anonymous type with a property called BUID, by changing the first query like this
int year = int.Parse(ddlYears.SelectedValue);
var BUIDs = (from b in dc.BusinessUnits
where b.Year == year
orderby b.BuName
select b.BUID).ToList();
and then use
var markets = (from p in dc.Markets
where BUIDs.Contains(p.BUID)
orderby p.MarketName
select p);
But note that this will be inefficient. Much better option would be to not use the list of BUIDs for filtering, but combining the 2 queries so the whole thing becomes a single query executed in the database, like this
var markets = (from p in dc.Markets
where dc.BusinessUnits.Any(bu => b.Year == year && b.BUID == p.BUID)
orderby p.MarketName
select p);
This is the exact equivalent of, if using your words, much like you'd do a "where in (select from other_table where year=2016)".

How to assign to a List<string> in LinqToEntities C#

Hoping that I'm just missing something obvious but here's my query
var data = (from project in _db.Projects
where project.Id == id
let primaryCategory = (from c in _db.Categories
where c.CategoryID == project.PrimaryCategory
select c.Name)
let categories = (from c in _db.ProjectCategories
join pc in _db.Projects_ProjectCategories on c.ProjectCategoryID equals pc.ProjectCategoryID
where pc.ProjectID == project.ProjectID
select c.Name)
let owner = (from o in _db.Owners
join po in _db.App_Projects_Owners on o.OwnerID equals po.OwnerID
where po.ProjectID == project.ProjectID
select new OwnerModel
{
Owner = o,
Project = project,
PrimaryCategory = primaryCategory.FirstOrDefault(),
Categories = categories.ToList()
})
select new
{
owner,
project
}).FirstOrDefault();
In there OwnerModel.Categories is a List of strings. I can't use ToList() in the query because it gives a materialization error. I've added a custom setter that takes the IQueryable, but that still makes another round trip to the database for every owner that the query returns.
So how are you supposed to assign basic lists in a subquery?
EDIT AND ANSWER (since Robert McKee lead me to the answer in his comment).
The answer is to use the group by clause like so
var data = (from project in _db.Projects
where project.Id == id
let primaryCategory = (from c in _db.Categories
where c.CategoryID == project.PrimaryCategory
select c.Name)
let categories = (from c in _db.ProjectCategories
join pc in _db.Projects_ProjectCategories on c.ProjectCategoryID equals pc.ProjectCategoryID
where pc.ProjectID == project.ProjectID
group c.Name by pc.ProjectCategoryID into x
select x.ToList())
let owner = (from o in _db.Owners
join po in _db.App_Projects_Owners on o.OwnerID equals po.OwnerID
where po.ProjectID == project.ProjectID
select new OwnerModel
{
Owner = o,
Project = project,
PrimaryCategory = primaryCategory.FirstOrDefault(),
Categories = categories
})
select new
{
owner,
project
}).FirstOrDefault();
Specifically note the bits involving
group c.Name by pc.ProjectCategoryID into x
select x.ToList()
It may seem counter-intuitive that it works like this until you dig into exactly what's going on. The method that I was calling above with categories.ToList() was trying to use the System.Collection.Generics ToList function, and that list didn't have any way to convert the expression to sql. However by using the group by clause I was creating a specialized Enumerable IGrouping and calling the ToList function on that. This function is able to be translated into a sql statement and thus doesn't throw the exception.
Learn something new every day.
Set up your navigation properties, and then your query becomes something like:
var data=db.Projects
.Include(p=>p.PrimaryCategory)
.Include(p=>p.Categories)
.Include(p=>p.Owner) // or .Include(p=>p.Owners) if projects can have multiple owners
.First(p=>p.Id == id);
As for child objects, you are looking for the group by clause or .GroupBy method.

What's wrong with such a Linq query

I have two tables:
Team: teamId, teamName
Player: playerId, teamId, playerName
I want to get the teamName through playerName. I wrote two querys, one of them doesn't work.
var query = from t in dc.Teams
where t.teamId == ((from p in dc.Players
where p.playerName == "kobe"
select p.teamId).SingleOrDefault())
select t.teamName; //Doesn't work
var query = from t in dc.Teams
join p in dc.Players
on t.teamId equals p.teamId
where p.playerName == "kobe"
select t.teamName; //Works
Anyone can tell me why the first query couldn't work?
Both queries should yield the same result if there are one or zero players named "kobe". If there are more than one players named "kobe", the first query won't return anything because its subquery uses SingleOrDefault, which returns the default value if the collection doesn't contain exactly one value, while the second query will return the teamName for each player named "kobe".
Risky Martin already mentioned the reasons but in addition to this,
You can use FirstOrDefault instead of SingleOrDefault. By using FirstOrDefault, your query can return any amount of results but you state that you only want the first result.
var query = from t in dc.Teams
where t.teamId == ((from p in dc.Players
where p.playerName == "kobe"
select p.teamId).FirstOrDefault())
select t.teamName; //Now it works

How do I use linq to do a WHERE against a collection object (using Netflix data source)

I am using LinqPad to learn Linq by querying the NetFlix OData source.
(BTW I know their is a similar question already on SO...didn't help me).
Here is the query I got working which is awesome.
from x in Titles
//where x.Rating=="PG"
where x.Instant.Available==true
where x.AverageRating>=4.0
//where x.Rating.StartsWith("TV")
//where x.Genres.First (g => g.Name.Contains("Family") ) //(from y in Genres where y.Name.Contains("Family") select y)
//where x.Genres.First (g => g.Name=="")
//orderby x.Name
orderby x.AverageRating descending
//select x
//)
select new {x.Name, x.Rating, x.AverageRating, x.ShortSynopsis}
(Pardon all the comments...it is a testament to the fact I am experimenting and that I will change the query for various needs).
There are two thing I cannot figure out.
First. Let's say I only want to return the first 10 results.
Second (and most importantly). I want to filter by a partial string of the genre. Each title contains a Genres collection. I want to show only Genres where the Name contains a certain string (like "Family"). Even better filter using Titles where genre.name.contains "firstFilter" AND "secondFilter".
Basically, I want to filter by genre(s) and I cannot figure out how to do it since Title contains its own Genres collection and I cannot figure out how to return only title that are in one or more genres of the collection.
Thanks for your help!
ps...it seems that Netflix OData source does not support Any operator.
Seth
To return the first 10 results, surround your code above with parentheses and put a .Take(10) on the end
var foo = ( from x in Titles... ).Take(10);
There is no way to do take using query syntax in C# currently.
As for the genre filter, as klabranche points out, oData does not support many of the same Linq constructs you can use locally with a regular IEnumerable or IQueryable.
klabranche's solution doesn't support contains. It does however make 2 round trips to the server to get results. (see my comment on his answer as to why this seems necessary)
Here is an alternative which makes one roundtrip to the server to get data, then it processes that data locally. Because some of the query runs locally, you can use string.Contains, "or" clauses, and other goodness.
The downside of this approach is it retrieves more data over the wire than is needed to answer the query. On the other hand, it's easy to understand and it works.
When I combine "Family" and "Children", it returns 21 results.
var oDataQuery = from x in Titles
where x.AverageRating >= 4
&& x.Instant.Available==true
orderby x.AverageRating descending
select new {x.Name, x.Rating, x.AverageRating, x.ShortSynopsis, x.Genres};
var localQuery = from o in oDataQuery.ToList()
where o.Genres.Any(g => g.Name.Contains("Family"))
&& o.Genres.Any(g => g.Name.Contains("Children"))
select new {o.Name, o.Rating, o.AverageRating, o.ShortSynopsis };
localQuery.Dump();
OData and the Netflix API support the Take() and Contains() methods:
from t in Titles
where t.Name.Contains("Matrix")
select t
(from t in Titles
where t.Name.Contains("Matrix")
select t).Take(10)
To get the first 10:
(from x in Titles
where x.Instant.Available==true
where x.AverageRating>=4.0
orderby x.AverageRating descending
select new {x.Name, x.Rating, x.AverageRating, x.ShortSynopsis}
).Take(10)
To filter by a single genre (Not what you want...):
from g in Genres
from t in g.Titles
where g.Name == "Horror"
where t.Instant.Available==true
where t.AverageRating >=4.0
orderby t.AverageRating descending
select new {t.Name, t.Rating, t.AverageRating, t.ShortSynopsis}
However, you wanted to have multiple genres BUT OData doesn't support Select Many queries which is why contains fails or trying to OR the Genre Name.
Below fails because contains returns many...
var q1 = from g in Genres
from t in g.Titles
where g.Name.Contains("Horror")
where t.Instant.Available==true
where t.AverageRating >=4.0
orderby t.AverageRating descending
select new {t.Name, t.Rating, t.AverageRating, t.ShortSynopsis};
To filter by multiple genres I found you can use a Concat or Union query (in LinqPad be sure to change to C# statements not expression):
var q1 = from g in Genres
from t in g.Titles
where g.Name=="Horror"
where t.Instant.Available==true
where t.AverageRating >=4.0
orderby t.AverageRating descending
select new {t.Name, t.Rating, t.AverageRating, t.ShortSynopsis};
var q2 = from g in Genres
from t in g.Titles
where g.Name=="HBO"
where t.Instant.Available==true
where t.AverageRating >=4.0
orderby t.AverageRating descending
select new {t.Name, t.Rating, t.AverageRating, t.ShortSynopsis};
var concat = q1.ToList().Concat(q2);
//var union = q1.Union(q2);
By unioning the two queries it will remove duplicates but these are what you want If I understand you correctly in that you want movies that are only in both genres?
In that case you will want to use Concat which will return all records.
Now you just need to find records that are in the query more than once and you have your results:
var results = from c in concat
group c by c.Name into grp
where grp.Count() > 1
select grp;

Categories