Skip to content

grouping and flatening 1:M:M:1 related data in Entity Framework Core/LINQ #7419

@MontyGvMC

Description

@MontyGvMC

Hello together.
I get exceptions when I try to query 1:M:M:1 related data and especially when I try to flatten and group them.
This happens in a .NET Core web api.

It happens in the following scenario:
I have a database where tasks are assigned to persons. Each task can only be assigned to one person. For each task the person needs access to
zero, one or more rooms.

In the PersonController I try to return a list of rooms to which the person needs access to. The result should be flatened (only the rooms and not the tasks) and grouped (without duplicates).

Unfortunatly in any way I try to do it I ran into unwanted resluts or exceptions (more on the exceptions later).
I also tried to get some help on Stackoverflow: http://stackoverflow.com/questions/41615953/grouping-and-flatening-1mm1-related-data-in-entity-framework-core-linq/

For the scenario there is the following database:

CREATE TABLE [dbo].[Person] (
    [Id]        INT           IDENTITY (1, 1) NOT NULL,
    [Firstname] NVARCHAR (30) NOT NULL,
    [Lastname]  NVARCHAR (30) NOT NULL,
    PRIMARY KEY CLUSTERED ([Id] ASC)
);

CREATE TABLE [dbo].[Task] (
    [Id]       INT           IDENTITY (1, 1) NOT NULL,
    [PersonId] INT           NOT NULL,
    [Name]     NVARCHAR (50) NOT NULL,
    PRIMARY KEY CLUSTERED ([Id] ASC),
    CONSTRAINT [FK_Task_Person] FOREIGN KEY ([PersonId]) REFERENCES [dbo].[Person] ([Id])
);

CREATE TABLE [dbo].[Room] (
    [Id]   INT           IDENTITY (1, 1) NOT NULL,
    [Name] NVARCHAR (50) NOT NULL,
    PRIMARY KEY CLUSTERED ([Id] ASC)
);

CREATE TABLE [dbo].[TaskRoom] (
    [Id]     INT IDENTITY (1, 1) NOT NULL,
    [TaskId] INT NOT NULL,
    [RoomId] INT NOT NULL,
    PRIMARY KEY CLUSTERED ([Id] ASC),
    CONSTRAINT [FK_TaskRoom_Room] FOREIGN KEY ([RoomId]) REFERENCES [dbo].[Room] ([Id]),
    CONSTRAINT [FK_TaskRoom_Task] FOREIGN KEY ([TaskId]) REFERENCES [dbo].[Task] ([Id])
);

INSERT INTO [Person] (Firstname,Lastname) VALUES ('First1', 'Last1');

INSERT INTO [Room] (Name) VALUES ('test room 1');
INSERT INTO [Room] (Name) VALUES ('test room 2');
INSERT INTO [Room] (Name) VALUES ('test room 3');
INSERT INTO [Room] (Name) VALUES ('test room 4');

INSERT INTO [Task] (PersonId, Name) VALUES (1, 'Task 1');
INSERT INTO [Task] (PersonId, Name) VALUES (1, 'Task 2');
INSERT INTO [Task] (PersonId, Name) VALUES (1, 'Task 3');
INSERT INTO [Task] (PersonId, Name) VALUES (1, 'Task 4');

INSERT INTO [TaskRoom] (TaskId, RoomId) VALUES (1, 1);
INSERT INTO [TaskRoom] (TaskId, RoomId) VALUES (2, 2);
INSERT INTO [TaskRoom] (TaskId, RoomId) VALUES (2, 3);
INSERT INTO [TaskRoom] (TaskId, RoomId) VALUES (3, 3);
-- Task 4 exists but has not yet a room assigned

The according controller:

namespace PersonExample.Controllers
{
    [Route("api/[controller]")]
    public class PersonController : Controller
    {

        private readonly PersonDBContext _dbContext;
        private readonly ILogger<PersonRepository> _logger;

        public PersonController(PersonDBContext dbContext, ILogger<PersonRepository> logger)
        {
            _dbContext = dbContext;
            _logger = logger;
        }

        [HttpGet("RoomAccess/{personId:int}")]
        public IActionResult RoomAccess(int personId)
        {
            _logger.LogDebug(string.Format("{0}.RoomAccess(person id: {1})", GetType().Name, personId));

            var result = _dbContext.Person
                .Include(p => p.Task).ThenInclude(t => t.TaskRoom).ThenInclude(tr => tr.Room)
                .Where(p => p.Id == personId)
                .Select(person => new
                    {
                        person.Id,
                        person.Firstname,
                        person.Lastname,

                        rooms = person.Task.SelectMany(ta => ta.TaskRoom.Select(tr => new { id = tr.Room.Id, name = tr.Room.Name })).Distinct()
                    }
                )
                .FirstOrDefault();

            if (result == null) 
            {
                return NotFound(string.Format("person id: {0}", personId));
            }

            return Ok(result);
        }
    
    }
}

Further technical details

EF Core version:
"Microsoft.EntityFrameworkCore.SqlServer": "1.1.0",
"Microsoft.EntityFrameworkCore.SqlServer.Design": "1.1.0",
"Microsoft.EntityFrameworkCore.Tools": "1.1.0-preview4-final"

Database Provider: (e.g. Microsoft.EntityFrameworkCore.SqlServer) (local mssql db)
Operating system: Win7 x64
IDE: Visual Studio 2015

System.ArgumentOutOfRangeException was unhandled by user code
  HResult=-2146233086
  Message=Index was out of range. Must be non-negative and less than the size of the collection.
Parameter name: index
  ParamName=index
  Source=System.Private.CoreLib
  StackTrace:
       at System.ThrowHelper.ThrowArgumentOutOfRangeException(ExceptionArgument argument, ExceptionResource resource)
       at System.Collections.ObjectModel.Collection`1.RemoveAt(Int32 index)
       at Microsoft.EntityFrameworkCore.Query.ExpressionVisitors.Internal.NavigationRewritingExpressionVisitor.RewriteSelectManyNavigationsIntoJoins(QuerySourceReferenceExpression outerQuerySourceReferenceExpression, IEnumerable`1 navigations, AdditionalFromClause additionalFromClauseBeingProcessed)
       at Microsoft.EntityFrameworkCore.Query.ExpressionVisitors.Internal.NavigationRewritingExpressionVisitor.<>c__DisplayClass20_0.<VisitMember>b__0(IEnumerable`1 ps, IQuerySource qs)
       at Microsoft.EntityFrameworkCore.Query.EntityQueryModelVisitor.BindPropertyExpressionCore[TResult](Expression propertyExpression, IQuerySource querySource, Func`3 propertyBinder)
       at Microsoft.EntityFrameworkCore.Query.EntityQueryModelVisitor.BindNavigationPathPropertyExpression[TResult](Expression propertyExpression, Func`3 propertyBinder)
       at Microsoft.EntityFrameworkCore.Query.ExpressionVisitors.Internal.NavigationRewritingExpressionVisitor.VisitMember(MemberExpression node)
       at Remotion.Linq.Clauses.FromClauseBase.TransformExpressions(Func`2 transformation)
       at Remotion.Linq.QueryModelVisitorBase.VisitQueryModel(QueryModel queryModel)
       at Microsoft.EntityFrameworkCore.Query.ExpressionVisitors.Internal.NavigationRewritingExpressionVisitor.Rewrite(QueryModel queryModel, QueryModel parentQueryModel)
       at Microsoft.EntityFrameworkCore.Query.ExpressionVisitors.Internal.NavigationRewritingExpressionVisitor.VisitSubQuery(SubQueryExpression expression)
       at Remotion.Linq.Clauses.FromClauseBase.TransformExpressions(Func`2 transformation)
       at Microsoft.EntityFrameworkCore.Query.ExpressionVisitors.Internal.NavigationRewritingExpressionVisitor.NavigationRewritingQueryModelVisitor.VisitAdditionalFromClause(AdditionalFromClause fromClause, QueryModel queryModel, Int32 index)
       at Remotion.Linq.QueryModelVisitorBase.VisitBodyClauses(ObservableCollection`1 bodyClauses, QueryModel queryModel)
       at Remotion.Linq.QueryModelVisitorBase.VisitQueryModel(QueryModel queryModel)
       at Microsoft.EntityFrameworkCore.Query.ExpressionVisitors.Internal.NavigationRewritingExpressionVisitor.Rewrite(QueryModel queryModel, QueryModel parentQueryModel)
       at Microsoft.EntityFrameworkCore.Query.ExpressionVisitors.Internal.NavigationRewritingExpressionVisitor.VisitSubQuery(SubQueryExpression expression)
       at System.Linq.Expressions.ExpressionVisitor.VisitAndConvert[T](ReadOnlyCollection`1 nodes, String callerName)
       at Remotion.Linq.Parsing.RelinqExpressionVisitor.VisitNew(NewExpression expression)
       at Remotion.Linq.Clauses.SelectClause.TransformExpressions(Func`2 transformation)
       at Microsoft.EntityFrameworkCore.Query.ExpressionVisitors.Internal.NavigationRewritingExpressionVisitor.NavigationRewritingQueryModelVisitor.VisitSelectClause(SelectClause selectClause, QueryModel queryModel)
       at Remotion.Linq.QueryModelVisitorBase.VisitQueryModel(QueryModel queryModel)
       at Microsoft.EntityFrameworkCore.Query.ExpressionVisitors.Internal.NavigationRewritingExpressionVisitor.Rewrite(QueryModel queryModel, QueryModel parentQueryModel)
       at Microsoft.EntityFrameworkCore.Query.EntityQueryModelVisitor.OptimizeQueryModel(QueryModel queryModel)
       at Microsoft.EntityFrameworkCore.Query.EntityQueryModelVisitor.CreateQueryExecutor[TResult](QueryModel queryModel)
       at Microsoft.EntityFrameworkCore.Query.Internal.QueryCompiler.CompileQueryCore[TResult](Expression query, INodeTypeProvider nodeTypeProvider, IDatabase database, ILogger logger, Type contextType)
       at Microsoft.EntityFrameworkCore.Query.Internal.QueryCompiler.<>c__DisplayClass19_0`1.<CompileQuery>b__0()
       at Microsoft.EntityFrameworkCore.Query.Internal.CompiledQueryCache.GetOrAddQueryCore[TFunc](Object cacheKey, Func`1 compiler)
       at Microsoft.EntityFrameworkCore.Query.Internal.QueryCompiler.Execute[TResult](Expression query)
       at PersonExample.Controllers.PersonController.RoomAccess(Int32 personId)
       at lambda_method(Closure , Object , Object[] )
       at Microsoft.AspNetCore.Mvc.Internal.ControllerActionInvoker.<InvokeActionMethodAsync>d__27.MoveNext()
  InnerException: 

Any way to get this working?
Also with those nuget versions:
"Microsoft.EntityFrameworkCore.Tools": "1.1.0-preview4-final",
"Microsoft.EntityFrameworkCore.SqlServer": "1.0.1",
"Microsoft.EntityFrameworkCore.SqlServer.Design": "1.0.1",
"Microsoft.EntityFrameworkCore": "1.1.0"

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions