0

Small EF question. I have a many to many relationship mapped in EF. X..Y So when I have one X there is a property X.Ys.

Now what I want to do is use a Linq Query to get several X's but I don't want to have all Y's inside the selected X's. I want the Y's filtered on Y.RegistrationDate > Date.Today.

So when I have one X and itterate through .Y's I will only get future Y's.

UPDATE This works, resulting in S having distinct ug's with it's relationship only containing upcoming events. But don't tell me this cant be simplified??!!

        var t = (from ug in uof.Ugs.All().ToList()
                 from upcomingEvent in ug.Events
                 where upcomingEvent.Date >= DateTime.Today
                 select new
                 {
                     ug,
                     upcomingEvent
                 }).ToList();

        var s = (from ug in t.Select(x => x.ug).Distinct()
                 select new UG
                 {
                     Id = ug.Id,
                     Name = ug.Name,
                     Description = ug.Description,
                     WebSite = ug.WebSite,
                     Events = ug.Events.Where(x => x.Date >= DateTime.Today).ToList()
                 }).ToList();

UPDATE2

Added image to show that even with basic context manipulation I'm still getting 2 events, event when I take 1!

exampledebugimage

Depechie
  • 6,102
  • 24
  • 46

2 Answers2

3

EF does not support this scenario as you want it, what you can do however is this:

var date = DateTime.Date;
var query = from x in Xs                
            select new 
            {
                X = x
                Ys = x.Ys.Where(i = > i.RegistrationDate > date)
            }

Which will give you a collection of X's with their corresponding Y's that match your criteria.

ntziolis
  • 10,091
  • 1
  • 34
  • 50
  • Almost correct... but now I'm getting several X's double! And adding a distinct after the query doesn't help... This because of the many2many, so the select new should only happen if the 'found' X is not yet in the result list. Any thoughts? – Depechie Mar 18 '12 at 19:40
  • @Depechie and ntziolis: I think you must remove the `from y in x.Ys` line and replace `i` by `y`. – Slauma Mar 18 '12 at 21:56
2

Have you tried?:

var query = Xs
.Select(x => new { x, yCol = x.YCol.Where(y => y.Date >= DateTime.Today) })
.AsEnumerable()
.Select(x => x.x)
.ToList();

See: http://blogs.msdn.com/b/alexj/archive/2009/10/13/tip-37-how-to-do-a-conditional-include.aspx

All those .ToList you use will mean you load the whole table from the db before filtering. So watch out for that.

UPDATE: As fixup doesn't work with Many-To-Many

As Slauma mentioned in the comments make sure you don't use this technique if you are going to submit the changes as the changetracking will think you altered the collection. Or even better make sure you use .AsNoTracking() which will improve performance anyway.

We can use the same solution as above but slightly different for many-to-many. See this example:

[TestClass]
public class ContextTest
{
    [TestMethod]
    public void FixupTest()
    {
        Database.SetInitializer(new DropCreateDatabaseAlways<Context>());

        using (var db = new Context())
        {
            db.Groups.Add(new Group
            {
                Name = "G1",
                Users = new List<User>{
                    new User{ Name = "M"},
                    new User{Name = "S"}
                }
            });

            db.SaveChanges();
        }

        using (var db = new Context())
        {
            var group = db.Groups
                .Select(g => new { g, Users = g.Users.Where(u => u.Name == "M") })
                .AsEnumerable()
                .Select(g => {
                    g.g.Users = g.Users.ToList();
                    return g.g;
                })
                .First();

            Assert.AreEqual(1, group.Users.Count);
        }
    }


}



public class User
{
    public int ID { get; set; }
    public string Name { get; set; }

    public ICollection<Group> Groups { get; set; }
}



public class Group
{
    public int ID { get; set; }
    public string Name { get; set; }

    public ICollection<User> Users { get; set; }
}

The test pass and the generated sql is:

SELECT 
[Project1].[ID] AS [ID], 
[Project1].[Name] AS [Name], 
[Project1].[C1] AS [C1], 
[Project1].[ID1] AS [ID1], 
[Project1].[Name1] AS [Name1]
FROM ( SELECT 
    [Extent1].[ID] AS [ID], 
    [Extent1].[Name] AS [Name], 
    [Join1].[ID] AS [ID1], 
    [Join1].[Name] AS [Name1], 
    CASE WHEN ([Join1].[Group_ID] IS NULL) THEN CAST(NULL AS int) ELSE 1 END AS [C1]
    FROM  [dbo].[Groups] AS [Extent1]
    LEFT OUTER JOIN  (SELECT [Extent2].[Group_ID] AS [Group_ID], [Extent3].[ID] AS [ID], [Extent3].[Name] AS [Name]
        FROM  [dbo].[GroupUsers] AS [Extent2]
        INNER JOIN [dbo].[Users] AS [Extent3] ON [Extent3].[ID] = [Extent2].[User_ID] ) AS [Join1] ON ([Extent1].[ID] = [Join1].[Group_ID]) AND (N'Mikael' = [Join1].[Name])
)  AS [Project1]
ORDER BY [Project1].[ID] ASC, [Project1].[C1] ASC
Mikael Eliasson
  • 5,157
  • 23
  • 27
  • Hey Mike, because it's a many to many, not doing the ToList() and waiting till end of query, will result in an error. This because of the circular reference of the many-to-many. But I'll try out your option to see if this is still in effect. – Depechie Mar 18 '12 at 20:29
  • Like I thought, the error: "There is already an open DataReader associated with this Command which must be closed first" still there! This is on the y => y.Date... part, because Y also has a Y.Xs collection! – Depechie Mar 18 '12 at 20:32
  • I think that is a connection string problem: See here http://social.msdn.microsoft.com/Forums/en-MY/adonetefx/thread/8cbe2049-53c9-4c02-b040-18ee30fd7800 – Mikael Eliasson Mar 18 '12 at 20:40
  • Mike that is GENIUS! Wasn't aware of that addition in querystring! So now I don't get the error, but with your linq example I'm still having older Y's inside my X's... – Depechie Mar 18 '12 at 20:48
  • That's strange. Do you have any .Includes specified? – Mikael Eliasson Mar 18 '12 at 21:07
  • 1
    @Depeche and Mikael: This relationship fixup trick unfortunately doesn't work for many-to-many relationships, only for one-to-many. Hence your x.Ys should be actually empty. If it contains *all* unfiltered Ys this can only be because of lazy loading. – Slauma Mar 18 '12 at 21:50
  • @MikaelEliasson a bit more detail I'm also using the repo pattern ( a bit like: http://stackoverflow.com/questions/5332061/how-to-eager-load-child-entities-using-repository-pattern but not with "the include" ) – Depechie Mar 19 '12 at 07:59
  • @Slauma ok, after much work I would think the same, any suggestions than on how to fix this? Or is this not possible at all? I also found this but still need to go through it: http://stackoverflow.com/questions/6141988/ef-4-1-loading-filtered-child-collections-not-working-for-many-to-many – Depechie Mar 19 '12 at 08:00
  • @Depechie: ntziolis answer shows the only way to do the filtering in a single roundtrip. If you want to have the result in the Ys collection of X you must copy the result in the anonymous object to the Ys collection in memory after the query has been executed. The only alternative is explicit loading (= multiple queries to the DB) as shown at the bottom of the answer in the linked question. – Slauma Mar 19 '12 at 16:00
  • Slauma, nice catch with the many-to-many and fixup not working. Created a slightly updated version as almost the same code works anyway. – Mikael Eliasson Mar 19 '12 at 17:03
  • 1
    Yes, that's the "*...copy the result...*" I meant. One has to keep in mind though that the code is dangerous if you want to make changes and save them because EF's change tracking considers everything after `AsEnumerable()` as a change on the attached entities. When you call `SaveChanges` directly after the `First()` EF will try to write INSERTs to the join table because it "thinks" you have added *new* relationships between group and user (should result in primary key constraint violation in the join table). For read-only the code should work fine (+1). – Slauma Mar 19 '12 at 17:24
  • Awesome catch. Will add a warning. – Mikael Eliasson Mar 19 '12 at 17:51
  • @Slauma and Mikael thanks for all the details, need to go through this! And do some testing. But to give detail on the 'update' tracking I'm giving the data as a REST service for HTTP GET, currently the POST implementation is not yet there. And when it comes it will be stateless because of the REST, without change tracking I mean. – Depechie Mar 19 '12 at 20:25
  • @MikaelEliasson now I'm getting this exception: http://stackoverflow.com/questions/4069563/why-is-my-entity-framework-code-first-proxy-collection-null-and-why-cant-i-set with my code when I try to use: g.g.Users = g.Users.ToList() I'll try to disable all the virtuals as suggested in the other stacko related link – Depechie Mar 19 '12 at 20:41
  • Ok so deleting all the vritual indications on all but the relationships and with the code given by @MikaelEliasson this works! Man what a loop hole kind of thing to get this done :) – Depechie Mar 19 '12 at 20:44