LINQ - Searching one to many for exact match of array - c#

Let me preface by saying that this is self study and I am trying to self learn LINQ and Entity Framework. I've spent a few days attempting to turn the SQL statement at the bottom of this question into LINQ with terrible results. I've also included the SQL diagram at the bottom.
My goal is to select all stories that have the same characters as the passed in string array. I'm not wanting stories returned that have additional characters or missing characters. This is what my feeble LINQ skills have come up with so far:
var characters = new string[] { "Harry", "Tom" };
var cq = _context.TblCharacter.AsNoTracking().Where(c => characters.Contains(c.NameVc));
var q = from c in cq
join sc in _context.TblStoryCharacter.AsNoTracking()
on c.IdI equals sc.CharacterIdI
join s in _context.TblStory.AsNoTracking().Include(s => s.TblStoryCharacter).ThenInclude(sc => sc.CharacterIdINavigation)
on sc.StoryIdI equals s.IdI
where s.TblStoryCharacter.Count() == characters.Length
where s.TblStoryCharacter.Where(sc => characters.Contains(sc.CharacterIdINavigation.NameVc)).Count() == characters.Length
select s;
The above code is spawning a bunch of queries (SQL profiler image below) and is loading a bunch of objects into memory. Is there any LINQ magic for this scenario?
LINQ spawned queries:
SELECT [t0].[StoryId_i]
FROM [tbl_story_character] AS [t0]
SELECT [sc1].[StoryId_i]
FROM [tbl_story_character] AS [sc1]
INNER JOIN [tbl_character] AS [sc.CharacterIdINavigation0] ON [sc1].[CharacterId_i] = [sc.CharacterIdINavigation0].[Id_i]
WHERE [sc.CharacterIdINavigation0].[Name_vc] IN ('Harry', 'Tom')
And this is the SQL I started out trying to convert to LINQ:
select *
from tbl_story
where Id_i in (
select sc.StoryId_i
from tbl_story_character sc
inner join tbl_character c
on c.Id_i = sc.CharacterId_i
where c.Name_vc in ('Harry', 'Tom')
and not exists (
select *
from tbl_story_character subsc
inner join tbl_character subc
on subc.Id_i = subsc.CharacterId_i
where subc.Name_vc not in ('Harry', 'Tom')
and subsc.StoryId_i = sc.StoryId_i
)
group by sc.StoryId_i
having count(*) = 2
)
Database diagram:
EDIT:
The models were generated by EFCore based on an existing database, each model contains navigation properties based on the foreign keys in the diagram.
New LINQ after taking advice of Jon Skeet and Munzer.
from s in _context.TblStory.AsNoTracking()
.Include(s => s.AuthorIdINavigation)
.Include(s => s.TblStoryCharacter)
.ThenInclude(sc => sc.CharacterIdINavigation)
where s.TblStoryCharacter.All(sc => characters.Contains(sc.CharacterIdINavigation.NameVc))
where s.TblStoryCharacter.Count == 2
select s;
This results in the following SQL which seems correct.
SELECT [s].[Id_i], [s].[AuthorId_i], [s].[Published_dt]
FROM [tbl_story] AS [s]
INNER JOIN [tbl_author] AS [t2] ON [s].[AuthorId_i] = [t2].[Id_i]
WHERE NOT EXISTS (
SELECT 1
FROM [tbl_story_character] AS [sc]
INNER JOIN [tbl_character] AS [sc.CharacterIdINavigation] ON [sc].[CharacterId_i] = [sc.CharacterIdINavigation].[Id_i]
WHERE ([s].[Id_i] = [sc].[StoryId_i]) AND [sc.CharacterIdINavigation].[Name_vc] NOT IN ('Harry', 'Tom')) AND ((
SELECT COUNT(*)
FROM [tbl_story_character] AS [t]
WHERE [s].[Id_i] = [t].[StoryId_i]
) = 2)
ORDER BY [s].[Id_i]

I believe All is what you are looking for here
it should be something like this
var q = from c in cq
join sc in _context.TblStoryCharacter.AsNoTracking()
on c.IdI equals sc.CharacterIdI
join s in _context.TblStory.AsNoTracking().Include(s => s.TblStoryCharacter).ThenInclude(sc => sc.CharacterIdINavigation)
on sc.StoryIdI equals s.IdI
where s.TblStoryCharacter.All(sc => characters.Contains(sc.CharacterIdINavigation.NameVc))
select s;

Try something like this :
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace ConsoleApplication1
{
class Program
{
static void Main(string[] args)
{
string[] names = {"Harry", "Tom"};
var results = (from sc in Story_Character.story_character
join chr in Character.character on sc.Id_i equals chr.Id_i
join st in Story.story on sc.Id_i equals st.Id_i
join auth in Author.author on sc.Id_i equals auth.Id_i
where names.Contains(chr.Name_vc)
select new { sc = sc, chr = chr, st = st, auth = auth })
.GroupBy(x => x.sc.StoryId_i).Where(x => x.Count() >= 2).ToList();
}
}
public class Story_Character
{
public static List<Story_Character> story_character = new List<Story_Character>();
public int Id_i { get; set; }
public int StoryId_i { get; set; }
public int CharacterId_i { get; set; }
}
public class Character
{
public static List<Character> character = new List<Character>();
public int Id_i { get; set; }
public string Name_vc { get; set; }
}
public class Story
{
public static List<Story> story = new List<Story>();
public int Id_i { get; set; }
public DateTime Published_dt { get; set; }
public string Title_vc { get; set; }
public string AuthorId_i { get; set; }
}
public class Author
{
public static List<Author> author = new List<Author>();
public int Id_i { get; set; }
public string Name_vc { get; set; }
public string Url_vc { get; set; }
}
}

Related

How to query multiple tables using LINQ syntax in .NET core 3

I need to query two database tables for a search term and return the results. The following was working in EF Core 2:
var SearchTerm = "hello";
IQueryable<TableA> q;
q = (from a in context.TableA
join b in context.TableB on a equals b.A into leftjoin
from c in leftjoin.DefaultIfEmpty()
where c.Column1.Contains(SearchTerm)
|| a.Column1.Contains(SearchTerm)
|| a.Column2.Contains(SearchTerm)
select a);
return q.Include(a => a.TableD)
.GroupBy(a => a.Id)
.Select(group => group.First())
.ToList();
The idea of the above is to take a SearchTerm and query two columns from TableA, join to TableB and also query a column in this one then select distinct values from TableA.
In .NET 3 the above throws an error saying it can't be translated to SQL. I tried to rewrite this, the best I can do is the below:
var SearchTerm = "hello";
var q = (from a in context.TableA
join b in context.TableB on a equals b.A into leftjoin
from c in leftjoin.DefaultIfEmpty()
where c.Column1.Contains(SearchTerm)
|| a.Column1.Contains(SearchTerm)
|| a.Column2.Contains(SearchTerm)
select a.Id).Distinct().ToList();
return context.TableA
.Where(a => q.Contains(a.Id))
.Include(c => c.TableD)
.ToList();
Which works ok but involves two database queries, since I already have the list of TableA from the first query it would be great to be able to just use this without having to extract the Ids and performing the second query. Also making sure the database continues to handle the distinct part rather than C# would be preferable too.
The definitions of A and B are:
public class TableA
{
public int Id { get; set; }
public string Column1 { get; set; }
public string Column2 { get; set; }
public int TableDId { get; set; }
public TableD TableD { get; set; }
}
public class TableB
{
public int Id { get; set; }
public string Column1 { get; set; }
public int TableAId { get; set; }
public TableA TableA { get; set; }
}
If I understood you correctly you have one-to-many relation between TableA and TableB, so it should be possible to add collection navigation property to TableA like in this tutorial for example:
public class TableA
{
public int Id { get; set; }
...
public ICollection<TableB> TableBs { get; set; }
}
So you can try to do something like this:
context.TableA
.Where(ta => ta.TableBs.Any(tb => tb.Column1.Contains(SearchTerm))
|| ta.Column1.Contains(SearchTerm)
|| ta.Column2.Contains(SearchTerm))
.Include(c => c.TableD)
.ToList();
Another option is to try subquery:
var q = (from a in context.TableA
join b in context.TableB on a.Id equals b.TableAId into leftjoin
from c in leftjoin.DefaultIfEmpty()
where c.Column1.Contains(SearchTerm)
|| a.Column1.Contains(SearchTerm)
|| a.Column2.Contains(SearchTerm)
select a.Id); // remove Distinct and ToList
return context.TableA
.Where(a => q.Contains(a.Id))
.Include(c => c.TableD)
.ToList();

LINQ to SQL and LINQ to Entity Join with static AND condition

I'm trying to convert the following Join statement into LINQ TO SQL or LINQ to Entity. I know how to join tables in either implementation; but, i'm struggling with the AND clause in the Join statement.
SELECT DISTINCT
p.LastName,
p.FirstName
FROM
dbo.Patient p INNER JOIN dbo.FormPat fp ON p.PatientID = fp.PatientID
INNER JOIN dbo.TxCyclePhase tcp ON fp.TxCyclePhase = tcp.TxCyclePhaseID AND tcp.Type = 2
As far as LINQ to SQL is concerned, I have the followings:
var query = (from p in Context.Set<Patient>().AsNoTracking()
join fp in Context.Set<PatientForm>().AsNoTracking() on p.Id equals fp.PatientId
join tcp in Context.Set<TxCyclePhase>().AsNoTracking() on new { fp.TxCyclePhaseId, seconProperty = true } equals new { tcp.Id, seconProperty = tcp.Type == 2 }
select new
{
p.FirstName,
p.LastName,
}).Distinct();
However, I'm getting an ArgumentNullException on the second join statement.
For the LINQ to Entity, I have the followings, however, this is giving me a distinct IQueryable of FormPat, instead of Patient.
var patients = Context.Set<Patient>().AsNoTracking()
.SelectMany(p => p.Forms)
.Where(fp => fp.Phase.Type == 2)
.Distinct();
As far as the LINQ to Entity is concerned, I was able to figure it out. I'd still like to know how to do it in LINQ to SQL tho.
I'm using the EF fluent API. My Patient object looks like:
public Patient()
{
Programs = new HashSet<Program>();
}
public virtual ICollection<PatientForm> Forms { get; set; }
My PatientForm object looks like:
public class PatientForm
{
public int FormId { get; set; }
public Patient CurrentPatient { get; set; }
public TxCyclePhase Phase { get; set; }
}
And the CyclePhase object looks like:
public TxCyclePhase()
{
this.FormPats = new HashSet<PatientForm>();
}
public int Id { get; set; }
public virtual ICollection<PatientForm> FormPats { get; set; }
In the entity configurations, I have the relationships set. So, in the repository, all I have to do is to use the Any() function when selecting the Patient forms.
var patients = Context.Set<Patient>().AsNoTracking()
.Where(p => p.Forms.Any(f => f.Phase.Type == 2))
.Distinct();

How to use LINQ on a join table?

The query I am trying to replicate in LINQ is:
SELECT count(*) FROM joinTable WHERE object1ID = input_parameter1_from_code
AND object2ID = input_parameter2_from_code;
I have access to a IdentityDbContext, but it only contains references to the constituent objects' tables, not for the join table itself, so I don't know what to look for to try to get the result.
Alternatively, if I can just use this raw query, I would like to know how to do that as well.
Thank you.
I assume you have in mind many-to-many relationship with implicit "link" ("join", "junction") table. Something like this (most likely you are speaking for User and Role, but that's not essential):
public class One
{
public int Id { get; set; }
public string Name { get; set; }
public ICollection<Two> Twos { get; set; }
}
public class Two
{
public int Id { get; set; }
public string Name { get; set; }
public ICollection<One> Ones { get; set; }
}
public class MyDbContext : DbContext
{
public DbSet<One> Ones { get; set; }
public DbSet<Two> Twos { get; set; }
}
Although you have no direct access to the link table, you can use either of the two "primary" tables combined with the navigation property of the other.
So, given
var db = new MyDbContext();
both
int count =
(from one in db.Ones
from two in one.Twos
where one.Id == input_parameter1_from_code && two.Id == input_parameter2_from_code
select new { one, two })
.Count();
and
int count =
(from two in db.Twos
from one in two.Ones
where one.Id == input_parameter1_from_code && two.Id == input_parameter2_from_code
select new { one, two })
.Count();
will produce identical SQL query similar to this:
SELECT
[GroupBy1].[A1] AS [C1]
FROM ( SELECT
COUNT(1) AS [A1]
FROM [dbo].[TwoOne] AS [Extent1]
WHERE (1 = [Extent1].[One_Id]) AND (2 = [Extent1].[Two_Id])
) AS [GroupBy1]
which as you can see is against the link table.
In query syntax:
var amount = (from record in DBcontext.joinTable
where record.object1ID = input_parameter1_from_code &&
record.object2ID = input_parameter2_from_code
select record).Count();
In Method syntax:
var amount = DBcontext.joinTable
.Where(record => record.object1ID = input_parameter1_from_code &&
record.object2ID = input_parameter2_from_code)
.Count();
You can use Database.SqlQuery method which accepts raw sql query along with the parameters that you need to use with your query and advantage of using sql parameter is to avoid sql injection.
Try like this:
var data = yourContext.Database.SqlQuery<int>(
"SELECT count(*) FROM joinTable WHERE object1ID = #code1 AND object2ID = #code2",
new SqlParameter("#code1", input_parameter1_from_code),
new SqlParameter("#code2", input_parameter2_from_code)
);
Let me know if this didnt work for you :)
You can definitely use that query with a DbContext. Take a look at the MSDN documentation over here:
https://msdn.microsoft.com/en-us/library/system.data.linq.datacontext.executequery(v=vs.110).aspx
It will be something like:
var Count = DbContext.ExecuteQuery("SELECT count(*) FROM joinTable where object1ID = input_parameter1_from_code
AND object2ID = input_parameter2_from_code;");
This should work, even in case of link table
dbContext.CollectionOne.where(x => x.Id == 1).SelectMany(x => x.Collection2).where(y => y.Id == 2).Count()

Linq query with first or default join

I have the following data model:
public class Course
{
public int CourseId { get; set; }
public int StateId { get; set; }
}
public class CompletedCourse
{
public int CompletedCourseId { get; set; }
public int UserId { get; set; }
public Course Course { get; set; }
public string LicenseNumber { get; set; }
}
public class License
{
public int LicenseId { get; set; }
public int UserId { get; set; }
public int StateId { get; set; }
public string LicenseNumber { get; set; }
}
I'm trying to come up with an IQueryable for CompletedCourses and I would like to populate CompletedCourse.LicenseNumber with the LicenseNumber property of the FirstOrDefault() selection from my Licenses table where UserId and StateId match the completed course records.
Here is my query, but I don't think this will handle duplicate licenses correctly:
var entries =
(from course in context.CompletedCourses
join license in context.Licenses on course.UserId equals license.UserId
where license.StateId == course.Course.StateId
select course)
.Include(x => x.Agent)
.Include(x => x.Course.State);
Is this something that can be done in a single query? Thanks in advance.
Here is how you can do that:
var entries =
(from course in context.CompletedCourses
join license in context.Licenses
on new { course.UserId, course.Course.StateId }
equals new { license.UserId, license.StateId }
into licenses
let licenseNumber = licenses.Select(license => license.LicenseNumber).FirstOrDefault()
select new { course, licenseNumber });
But please note that with this type of projection you cannot have Includes in your query (you can, but they will not be in effect).
The EF generated query I'm getting from the above is:
SELECT
[Extent1].[CompletedCourseId] AS [CompletedCourseId],
[Extent1].[UserId] AS [UserId],
[Extent1].[LicenseNumber] AS [LicenseNumber],
[Extent1].[Course_CourseId] AS [Course_CourseId],
(SELECT TOP (1)
[Extent2].[LicenseNumber] AS [LicenseNumber]
FROM [dbo].[Licenses] AS [Extent2]
INNER JOIN [dbo].[Courses] AS [Extent3] ON [Extent3].[StateId] = [Extent2].[StateId]
WHERE ([Extent1].[Course_CourseId] = [Extent3].[CourseId]) AND ([Extent1].[UserId] = [Extent2].[UserId])) AS [C1]
FROM [dbo].[CompletedCourses] AS [Extent1]
It can be noticed that EF effectively ignores the join, so the same result can be obtained by simple natural query:
var entries =
(from course in db.CompletedCourses
let licenseNumber =
(from license in db.Licenses
where license.UserId == course.UserId && license.StateId == course.Course.StateId
select license.LicenseNumber).FirstOrDefault()
select new { course, licenseNumber });
#IvanStoev's answer was very helpful in joining on anonymous types, but ultimately I couldn't use it because I needed Includes. Here is the solution I went with that results in two DB queries instead of one which is fine for my situation.
var entries = context.CompletedCourses
.Include(x => x.Agent)
.Include(x => x.Course);
var courses = entries.ToList();
var courseIds = entries.Select(x => x.CompletedCourseId);
var licenses =
(from course in entries
join license in context.Licenses
on new { course.AgentId, course.Course.StateId }
equals new { AgentId = license.UserId, license.StateId }
where courseIds.Contains(course.CompletedCourseId)
select license);
foreach (var course in courses)
{
var license = agentLicenses.FirstOrDefault(x => x.UserId == course.AgentId &&
x.StateId == course.Course.StateId);
if (license != null)
{
course.LicenseNumber = license.LicenseNumber;
}
}
return courses;

Get Distinct List using LINQ

I want to translate this query in LINQ format:
select m.MenuName,m.ParentID from Menu m where Id in(
select distinct m.ParentID from Menu m inner join MenuRole mr on mr.MenuID=m.Id)
This is what I have tried
var _employee = _db.Employees.AsEnumerable().Where(e => e.Id == Int32.Parse(Session["LoggedUserId"].ToString()))
.FirstOrDefault();
var _dashboardVM = new DashboardVM
{
MenuParentList = _employee.Designation.Role.MenuRoles
.Select(x => new SMS.Models.ViewModel.DashboardVM.MenuParent
{
MenuParentID=x.Menu.ParentID ,
MenuParentName=x.Menu.MenuName
})
.Distinct().ToList()
};
I am getting all list instead of distinct List
Dashboard VM
public class DashboardVM
{
public class MenuParent
{
public int? MenuParentID { get; set; }
public string MenuParentName { get; set; }
}
public List<MenuParent> MenuParentList { get; set; }
public List<Menu> MenuList { get; set; }
public User User { get; set; }
}
The Distinct() method checks reference equality for reference types. This means it is looking for literally the same object duplicated, not different objects which contain the same values.
Can you try the following? You may need to tweek as I have no testing environment:
MenuParentList = _employee.Designation.Role.MenuRoles.GroupBy ( r => r.Menu.ParentID + r.Menu.MenuName ).
.Select (y => y.First ())
.Select(x => new SMS.Models.ViewModel.DashboardVM.MenuParent
{
MenuParentID=x.Menu.ParentID ,
MenuParentName=x.Menu.MenuName
}).ToList();

Categories

Resources