5

I have three tables Exam, Test and UserTest.

CREATE TABLE [dbo].[Exam] (
    [ExamId]                      INT            IDENTITY (1, 1) NOT NULL,
    [SubjectId]                   INT            NOT NULL,
    [Name]                        NVARCHAR (50)  NOT NULL,
    [Description]                 NVARCHAR (MAX) NOT NULL,
    CONSTRAINT [PK_Exam] PRIMARY KEY CLUSTERED ([ExamId] ASC),
    CONSTRAINT [FK_ExamSubject] FOREIGN KEY ([SubjectId]) REFERENCES [dbo].[Subject] ([SubjectId]),
    CONSTRAINT [FK_Exam_ExamType] FOREIGN KEY ([ExamTypeId]) REFERENCES [dbo].[ExamType] ([ExamTypeId])
);

CREATE TABLE [dbo].[Test] (
    [TestId]      INT            IDENTITY (1, 1) NOT NULL,
    [ExamId]      INT            NOT NULL,
    [Title]       NVARCHAR (100) NULL,
    [Status]      INT            NOT NULL,
    [CreatedDate] DATETIME       NOT NULL,
    CONSTRAINT [PK_Test] PRIMARY KEY CLUSTERED ([TestId] ASC),
    CONSTRAINT [FK_TestExam] FOREIGN KEY ([ExamId]) REFERENCES [dbo].[Exam] ([ExamId])
);

CREATE TABLE [dbo].[UserTest] (
    [UserTestId]              INT            IDENTITY (1, 1) NOT NULL,
    [UserId]                  NVARCHAR (128) NOT NULL,
    [TestId]                  INT            NOT NULL,
    [Result]                  INT            NULL
    CONSTRAINT [PK_UserTest] PRIMARY KEY CLUSTERED ([UserTestId] ASC),
    CONSTRAINT [FK_UserTestTest] FOREIGN KEY ([TestId]) REFERENCES [dbo].[Test] ([TestId])
);

An exam can have many tests and a user can try any test a number of times.

How can I code a LINQ statement using the extension method syntax that allows me to see the following for UserId == 1 (I assume UserId == 1 in a Where clause) :

Exam       Test      Title           UserTestID  UserId     Result
1          1         1a               1           1          20 
1          1         1a               2           1          30
1          1         1a               3           1          40         
1          2         1b               4           1          98 
1          3         1c               5           1          44
2          4         2a
2          5         2b               6           1          12

Or if UserId == 2:

Exam       Test      Title           UserTestID  UserId     Result
1          1         1a               7           2          27  
1          2         1b        
1          3         1c               8           2          45
2          4         2a
2          5         2b        

Or if UserId is null

Exam       Test      Title           UserTestID  UserId     Result
1          1         1a        
1          2         1b
1          3         1c  
2          4         2a
2          5         2b   

Note this question has undergone a few changes thanks to suggestions I received. Now there is a bounty I hope for a quick answer that I can accept.

  • Why is calling it from the dbcontext important? – BenCr Mar 25 '14 at 10:54
  • I'm using entity framework code first in my application and I have the dbcontext available. I thought this would be the best way to go. –  Mar 25 '14 at 10:59
  • It's convenient for you but I'm not sure your users would care. Also in regard to the provided answer, why do you see it as important to create a sproc/view rather than just use the entity framework/linq methods? There must be thousands of sites explaining how to call sprocs/views from EF by now. – BenCr Mar 25 '14 at 11:19
  • @bENcr - I've not been able to find out how to do this with LINQ. I actually just opened another question about that: http://stackoverflow.com/questions/22633565/can-i-have-a-where-clause-that-works-on-a-grandchild-in-a-linq-query I've had a hard time finding out much about calling stored procs from EF. –  Mar 25 '14 at 11:49
  • @Malkit's answer shows how to do a join in LINQ. I'd just create a view and then add the where clause at run time using LINQ. The same database queries will be executed as far as I can tell. – BenCr Mar 25 '14 at 12:29
  • 1
    How is TestId 33 related to Result 2? Shouldn't be 75? – thepirat000 Mar 29 '14 at 03:52
  • thepirat000 - Thanks for the correction. I upvoted your comment. I edit the question to match your correction. –  Mar 29 '14 at 11:54

5 Answers5

5

If your Test entity has a UserTests collection you can use this query:

string userId = "1";
var result = context.Tests
    .SelectMany(t => t.UserTests
        .Where(ut => ut.UserId == userId)
        .DefaultIfEmpty()
        .Select(ut => new
        {
            ExamId = t.ExamId,
            TestId = t.TestId,
            Title = t.Title,
            UserTestId = (int?)ut.UserTestId,
            UserId = ut.UserId,
            Result = ut.Result
        }))
    .OrderBy(x => x.ExamId)
    .ThenBy(x => x.TestId)
    .ThenBy(x => x.UserTestId)
    .ToList();

Using DefaultIfEmpty() here ensures a LEFT OUTER JOIN so that you always have at least one UserTest entity (which is possibly null) for a given Test. Casting the non-nullable properties of the UserTest - like UserTestId - to a nullable type (int? for example) is important here, otherwise you can get an exception that a NULL value returned from the database can't be stored in a non-nullable .NET type.

If you don't have and don't want a UserTests collection in you Test entity you can use a GroupJoin as alternative which would basically left outer join the two tables by the TestId:

string userId = "1";
var result = context.Tests
    .GroupJoin(context.UserTests.Where(ut => ut.UserId == userId),
        t => t.TestId,
        ut => ut.TestId,
        (t, utCollection) => new
        {
            Test = t,
            UserTests = utCollection
        })
    .SelectMany(x => x.UserTests
        .DefaultIfEmpty()
        .Select(ut => new
        {
            ExamId = x.Test.ExamId,
            TestId = x.Test.TestId,
            Title = x.Test.Title,
            UserTestId = (int?)ut.UserTestId,
            UserId = ut.UserId,
            Result = ut.Result
        }))
    .OrderBy(x => x.ExamId)
    .ThenBy(x => x.TestId)
    .ThenBy(x => x.UserTestId)
    .ToList();
Slauma
  • 175,098
  • 59
  • 401
  • 420
1
 var tests = (from t in context.Tests
       // where !t.UsertTests.Any() //if no user took the test
         //    || t.UserTests.Any(ut=>ut.Student.StudentId == stId)
        select new {Test = t, Exam = t.Exam, 
                 UserTests = t.UserTests.Where(ut=>ut.Student.StudentId == stId))
       .ToList();

On 2nd thought, may be this will be better. This will give you exam, test and usertests if there is any matching ones or null usertests

AD.Net
  • 13,352
  • 2
  • 28
  • 47
  • Thank very much. I will test this out tomorrow (almost midnight here) and add an update to the comment are. Also can you show me what would this look if it was coded using the LINQ extension method syntax? –  Mar 29 '14 at 14:48
  • Melina - check out Resharper - it can convert linq statements to and from the extension method syntax. – PeteGO Apr 04 '14 at 18:40
0

Check this line here

http://social.msdn.microsoft.com/Forums/en-US/aba0fb67-d290-475c-a639-075424096b29/linq-joining-3-tables?forum=linqtosql

you might get some idea for how to do this.

Malkit Singh
  • 349
  • 1
  • 8
  • I'd like to get a Server solution to this. It's a one off query so I think it would be better to have a view or sproc. I have just no idea how to do this. –  Mar 25 '14 at 10:55
  • you can create a table in db, and through sproc you can delete any prior data of this table and insert new data from sproc itself, and finally you can select all the records from this table using db context. – Malkit Singh Mar 25 '14 at 17:27
  • This syntax is results in an inner join. – tsacodes Nov 25 '20 at 15:04
0

Here is a link to a discussion that shows how to call stored procedures with a parameter: How to use DbContext.Database.SqlQuery<TElement>(sql, params) with stored procedure? EF Code First CTP5

Here is one way to code the stored procedure:

CREATE PROCEDURE dbo.sample1 (
@oneId NVARCHAR(128) = N'xx') AS
BEGIN
SET NOCOUNT ON;

SELECT @oneId AS userId,
    r.TestId, 
    r.Result
FROM (
    SELECT t.UserId, e.testId, t.Result
    FROM dbo.UserTest AS e
    LEFT OUTER JOIN dbo.UserTest AS t ON e.TestId = t.TestId AND t.UserId = @oneId
    WHERE  e.UserId = 0) AS r 
ORDER BY r.TestId 

END 
go
Community
  • 1
  • 1
Ramoth
  • 239
  • 1
  • 7
0

Try this:

var tests = context.Tests.Include( "Exam" )
    .Select( t => new
    {
        Test = t,
        UserTests = t.UserTests.Where( ut => ut.UserId == studentId )
    } )
    .ToList();
Craig W.
  • 17,838
  • 6
  • 49
  • 82