EntityFramework 4.3.1 Code First Navigation Proper

2019-08-27 15:59发布

问题:

This is a long introduction for a short question, sorry!!

I'm working with EF 4.3.1 Code First and I've got the following model

public class Action
{
    protected Action()
    { }

    public virtual int ActionID { get; protected set; }

    [Required]
    [StringLength(DataValidationConstants.NameLength)]
    public virtual string Name {get; set;}

    [StringLength(DataValidationConstants.DescriptionLength)]
    public virtual string Description { get; set; }

    public virtual ICollection<Role> Roles { get; set; }

    public virtual void AuthorizeRole(Role role)
    {
        if (IsRoleAuthorized(role))
            throw new ArgumentException("This role is already authorized", "role");

        Roles.Add(role);
    }
}

public class Role
{
    protected Role()
    { }        

    public virtual int RoleID { get; protected set; }

    [Required]
    [StringLength(DataValidationConstants.NameLength)]
    public virtual string Name { get; set; }

    [StringLength(DataValidationConstants.DescriptionLength)]
    public virtual string Description { get; set; }
}

And my DBContext class, defined in some other class library, with many to many mapping:

public class myDB : DbContext
{
    public DbSet<Domain.Action> Actions { get; set; }
    public DbSet<Role> Roles { get; set; }

    protected override void OnModelCreating(DbModelBuilder modelBuilder)
    {
        base.OnModelCreating(modelBuilder);

        modelBuilder.Entity<Domain.Action>()
            .HasMany(action => action.Roles)
            .WithMany()
            .Map(map => map.ToTable("AuthorizedRoles"));
    }
}

So, this works alright. But if noticing the method Action.AuthorizeRole(Role role) is easy to asume that the Role Authorization logic might be complex (some already-authorized-validation now, but could be any validation, right?), which is a completely valid thing to have on a good old fashioned domain model. But... the Roles collection public virtual ICollection<Role> Roles {get; set;} needs to be public, the getter at least, according to Requirements for Creating POCO Proxies. That means that any client of the Action class could add or remove roles, bypassing any validation logic. And yes, I want lazy loading, change tracking, the works, so I do need proxies to be created.

Regardlles, I set out to test some ways in which I'd be able to make this property public virtual ICollection<Role> Roles {get; set;} a non public property, to later test for proxy creation. As the proxies generated subclass my own class, and as I trust my inheritors but not my clients, I decide to make the property protected like so protected virtual ICollection<Role> Roles {get; set;}. But then, of course, I got a compilation error on the line

modelBuilder.Entity<Domain.Action>()
            .HasMany(action => action.Roles)
            .WithMany()
            .Map(map => map.ToTable("AuthorizedRoles"));

because now the property is protected and can't be accessed outside Action class or its inheritors, and certainly myDB context class is not one of those.

So I needed to try and access the property from myDB class without it (the property) being made public. And I though of reflection. My context class looked like this then:

public class myDB : DbContext
{
    public DbSet<Domain.Action> Actions { get; set; }
    public DbSet<Role> Roles { get; set; }

    protected override void OnModelCreating(DbModelBuilder modelBuilder)
    {
        base.OnModelCreating(modelBuilder);

        modelBuilder.Entity<Domain.Action>()
            .HasMany(action => ExtractPropertyValue<ICollection<Role>>(action, "Roles"))
            .WithMany()
            .Map(map => map.ToTable("AuthorizedRoles"));
    }

    protected virtual TProperty ExtractPropertyValue<TProperty>(object instance, string propertyName)
    {
        if(instance == null)
            throw new ArgumentNullException("instance", "Can't be null");
        if (string.IsNullOrWhiteSpace(propertyName))
            throw new ArgumentException("Can't be null or white spaced", "propertyName");

        Type instanceType = instance.GetType();
        PropertyInfo propertyInfo = instanceType.GetProperty(propertyName, BindingFlags.NonPublic);

        return (TProperty)propertyInfo.GetValue(instance, null);
    }
}

Notice the new method ExtractPropertyValue, ant the call to it on the many to many mapping instruction. This sould work right? The HasMany method is expecting a function that receives an Action and returns an ICollection of something (Role in this case) and that's what is getting. But no, it does not work, it compiles of courrse, but on runtime, I got and exception that was something like "This expresion must be of a valid property like obj => obj.MyProperty".

So ok, it needs to be a "direct" property and it needs to be accesible to de DBContext class. I decided to set my property to protected internal and to move my DBContext class to my Domain class library (where all the entities are defined), which I really dont like that much, but which I liked better thant having my property being accesible by everyone. My property looked like this:

protected internal virtual ICollection<Role> Roles { get; set; }

And my DBContext class like this, exactly as I first had it, only that it is now defined in the same class library as all the entities are:

public class mrMantisDB : DbContext
{
    public DbSet<Domain.Action> Actions { get; set; }
    public DbSet<Role> Roles { get; set; }

    protected override void OnModelCreating(DbModelBuilder modelBuilder)
    {
        base.OnModelCreating(modelBuilder);

        modelBuilder.Entity<Domain.Action>()
            .HasMany(action => action.Roles)
            .WithMany()
            .Map(map => map.ToTable("AuthorizedRoles"));
     }
}

And this works all right. So, now the only thing left to check was the creation of proxies, that is, having lazy loading and change tracking. Being the property protected internal instead of public I was afraid it might not work, but yes it did, all of it, and smoothly, really.

Now, here comes my question/request. If a navigation property does not really needs to be public for proxies to be created, protected is enough (I'm leaving internal out because I assume that only influences the ability to use that property for the relation mapping), why on earth the restriction on the expression to extract the property for the HasMany method, or better yet, since I understand the property must be a property of the type being mapped and not some random collection, why is not there an overload for HasMany which takes a string propertyName and searches the property for itself, even if it is not a public property. This would allow to have non public navigation properties, which in my point of view, go all the way to allow a neatly design object domian.

Maybe I'm missing something here.

Thanx a lot.

回答1:

Your question asked why the restriction on the protected property for the modelbuilder,and I'm not sure why that's the case. However, I've had success implementing a solution from this blog if you're wanting a workaround.

You would update your entity with an expression so the modelbuilder can find it:

  protected virtual ICollection<Role> Roles { get; set; }

        public class PropertyAccessExpressions
        {
            public static readonly Expression<Func<User, ICollection<Role>>> ID = x => x.Roles;
        }

Then your builder should be able to find this:

  modelBuilder.Entity<Domain.Action>()
            .HasMany(action => action.Roles)