zzzprojects/EntityFramework.DynamicFilters

InvalidOperationException: variable 'x' of type 'X' referenced from scope '', but it is not defined

icnocop opened this issue · 4 comments

Hi.

Thank you for DynamicFilters.

DynamicFilters works without issues for one of my simple cases, but it throws an exception when trying to make it work for a little more complex case, so I'm not sure if DynamicFilters supports what I'm trying to do.

Basically, I'm trying to filter Groups from a User based on the User's permissions.

A User can be authorized for multiple Groups.
A Group can be authorized to multiple Users.
This is a basic many-to-many relationship.

The complexity comes when I introduce permission inheritance.
In this case a Group can have a parent Group.
If a user is authorized for a Group, it is also authorized for its children.

.NET Framework 4.5.1
EntityFramework 6.1.3
EntityFramework.DynamicFilters 2.11.0-beta2

MyDatabaseContext.cs

using EntityFramework.DynamicFilters;
using System.Data.Entity;
using System.Linq;
using System.Threading;

namespace DynamicFiltersTest
{
    public class MyDatabaseContext : DbContext
    {
        public DbSet<User> Users { get; set; }

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

        protected override void OnModelCreating(DbModelBuilder modelBuilder)
        {
            modelBuilder.Entity<Group>()
                .HasMany(e => e.Users)
                .WithMany(e => e.Groups)
                .Map(m => m.ToTable("UserGroups").MapLeftKey("GroupId").MapRightKey("UserId"));

            // the following filter works without issues, but doesn't check permission inheritance
            //modelBuilder.Filter(
            //    "UserGroupsFilter",
            //    (Group g, long? userId) => g.Users.Any(x => x.UserId == userId),
            //    (MyDatabaseContext context) => GetUserId(context));

            // the following filter causes the following exception:
            // System.InvalidOperationException: variable 'g' of type 'DynamicFiltersTest.Group' referenced from scope '', but it is not defined
            modelBuilder.Filter(
                "UserGroupsFilter",
                (Group g, long? userId) => IsUserIdAuthorized(g, userId),
                (MyDatabaseContext context) => GetUserId(context));
        }

        private bool IsUserIdAuthorized(Group group, long? userId)
        {
            if ((group == null) || (userId == null))
            {
                return false;
            }

            if (group.Users.Any(x => x.UserId == userId))
            {
                return true;
            }

            // check parent
            return IsUserIdAuthorized(group.Parent, userId);
        }

        private long? GetUserId(MyDatabaseContext context)
        {
            return context.Users.SingleOrDefault(x => x.Name == Thread.CurrentPrincipal.Identity.Name)?.UserId;
        }
    }
}

User.cs

using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;

namespace DynamicFiltersTest
{
    public class User
    {
        public User()
        {
            Groups = new HashSet<Group>();
        }

        [Key]
        public long UserId { get; set; }

        public string Name { get; set; }

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

Group.cs

using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

namespace DynamicFiltersTest
{
    public class Group
    {
        public Group()
        {
            Users = new HashSet<User>();
        }

        [Key]
        public long GroupId { get; set; }

        public string Name { get; set; }

        public long? ParentGroupId { get; set; }

        [ForeignKey(nameof(ParentGroupId))]
        public virtual Group Parent { get; set; }

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

UnitTest1.cs

using Microsoft.VisualStudio.TestTools.UnitTesting;
using System.Linq;
using System.Collections.Generic;

namespace DynamicFiltersTest
{
    [TestClass]
    public class UnitTest1
    {
        [TestMethod]
        public void TestMethod1()
        {
            using (MyDatabaseContext context = new MyDatabaseContext())
            {
                List<Group> groupsList1 = context.Groups.ToList(); // the exception is thrown here
            }
        }
    }
}

Any advice?

Thank you!

Hello @icnocop ,

One problem I see is you try to filter with code in your application.

The filter normally append some filtering SQL to the statement that will be created.

In your case, unless I'm wrong, the logic used to filter is currently a recursive function. There is no way to convert it into a SQL to allow some filtering.

What I tried is something similar to this

modelBuilder.Filter("UserGroupsFilter", (Group g, long? userId) =>
    userId.HasValue && g.Users.Any(x => x.UserId == userId) ||
    g.Parent.Users.Any(y => y.UserId == userId), (int?)null);

However, that currently throw a Stack Overflow error when adding the Users.Any part.

I believe it may be possible to fix the Stack Overflow, by example, by commenting a line (that will have some side effect but at least doesn't throw a Stack Overflow error,

The following code return the group 1, 2, 3 as expected.

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using System.Data;
using System.Data.Entity;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;
using EntityFramework.DynamicFilters;

namespace Z.Lab
{
    public partial class Form_Request_ManyToMany : Form
    {
        public Form_Request_ManyToMany()
        {
            InitializeComponent();
            
            // CLEAR
            using (var ctx = new EntityContext())
            {
                ctx.Groups.RemoveRange(ctx.Groups);
                ctx.Users.RemoveRange(ctx.Users);
                ctx.SaveChanges();
            }

            // SEED
            using (var ctx = new EntityContext())
            {
                var group1 = ctx.Groups.Add(new Group() { Name = "Group_1" });
                var group2 = ctx.Groups.Add(new Group() { Name = "Group_2" });
                var group3 = ctx.Groups.Add(new Group() { Name = "Group_3" });
                var group4 = ctx.Groups.Add(new Group() { Name = "Group_4" });

                var user1 = ctx.Users.Add(new User() {Name = "User_1"});

                group1.Users = new List<User>() {user1};
                group2.Parent = group3;
                group3.Users = new List<User>() { user1 };

                ctx.SaveChanges();
            }

            // TEST
            using (var ctx = new EntityContext())
            {
                var list = ctx.Groups.ToList();
            }
        }

        public class EntityContext : DbContext
        {
            public EntityContext() : base("CodeFirstEntities")
            {
            }

            public DbSet<Group> Groups { get; set; }
            public DbSet<User> Users { get; set; }

            protected override void OnModelCreating(DbModelBuilder modelBuilder)
            {
                //modelBuilder.Filter("UserGroupsFilter", (Group g, long? userId) =>
                //    userId.HasValue && g.Users.Union(g.Parent.Users).Any(x => x.UserId == userId), (int?)null);

                modelBuilder.Filter("UserGroupsFilter", (Group g, long? userId) =>
                    userId.HasValue && g.Users.Any(x => x.UserId == userId) ||
                    g.Parent.Users.Any(y => y.UserId == userId), 1);
                //modelBuilder.Filter("UserGroupsFilter", (Group g, long? userId) =>
                //    userId.HasValue && g.Users.Any(x => x.UserId == userId), (int?)null);

                modelBuilder.Types().Configure(x => x.ToTable(GetType().DeclaringType != null ? GetType().DeclaringType.FullName.Replace(".", "_") + "_" + x.ClrType.Name : ""));

                base.OnModelCreating(modelBuilder);
            }
        }

        public class Group
        {
            public Group()
            {
                Users = new HashSet<User>();
            }

            [Key]
            public long GroupId { get; set; }

            public string Name { get; set; }

            public long? ParentGroupId { get; set; }

            [ForeignKey(nameof(ParentGroupId))]
            public virtual Group Parent { get; set; }

            public virtual ICollection<User> Users { get; set; }
        }
        public class User
        {
            public User()
            {
                Groups = new HashSet<Group>();
            }

            [Key]
            public long UserId { get; set; }

            public string Name { get; set; }

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

Let me know if the filter provided could work in your scenario.

If you confirm me, I will try to work on the Stack Overflow issue during the weekend.

Best Regards,

Jonathan

Hi @JonathanMagnan.

Thank you for the follow up.

Unfortunately, in my case, the inheritance tree of Groups and their Parents may consist of more than one hierarchical level, so just checking the Parent in your query wouldn't work for my scenario.

It now seems like I have to perform the filter on objects rather than on the Entity Framework layer, after the SQL has been executed.

Thank you!

Yup, this is what I thought :(

Can we close this request?

Best Regards,

Jonathan