EF Code First: Duplicate foreign keys (one from name convention, one from navigation property)

佐手、 提交于 2020-01-16 05:31:47

问题


I'm trying to create an SQL database by using EF Code First.

Assume I have the following code:

public class Account
{
    public int Id;
    public ICollection<User> Users;
}

public class User
{
    public int Id;
    public int AccountId;
}

public class AccountContext : DbContext
{
    public DbSet<Account> Accounts;
    public DbSet<User> Users;
}

(Note the lack of any Fluent API commands or Data Annotations; I want to do this by convention.)

When the database is created, I get the following fields in the Users table:

Id
AccountId
Account_Id

Why isn't EF picking up on the fact that "AccountId" refers to the "Id" primary key (by convention) of Account? I want to avoid mapping this manually with Fluent API/DA if possible, and I want to avoid having the Account navigation property on User.


回答1:


There are only two ways i know how to perform what you are looking to do, either by Data Annotation (Very quick) or Fluent Mapping. You can't just say public int AccountId; and expect everything to work.

Fluent API Mapping Bi-directional

public class Account
{
   public int Id { get; set; }

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

public class User
{
   public int Id { get; set; }

   public Account Account { get; set; }
}

public class AccountContext : DbContext
{
   public AccountContext()
        : base("DefaultConnection")
    {

    }

   public DbSet<Account> Accounts { get; set; }
   public DbSet<User> Users { get; set; }

   protected override void OnModelCreating(DbModelBuilder modelBuilder)
    {
        modelBuilder.Entity<User>().HasRequired(u => u.Account) 
            .WithMany(a => a.Users) 
            .HasForeignKey(u => u.AccountId);
    }
}

Data Annotation Bi-directional

public class Account
{
   [Key]
   public int Id { get; set; }

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

public class User
{
   [Key]
   public int Id { get; set; }

   [ForeignKey("Account"), DatabaseGenerated(DatabaseGeneratedOption.None)]
   public int AccountId { get; set; }

   public Account Account { get; set; }
}

// and of course you need your context class, but with less code

public class AccountContext : DbContext
{
   public AccountContext()
        : base("DefaultConnection")
    {

    }

   public DbSet<Account> Accounts { get; set; }

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

Without Data Annotation or Fluent API Mapping Bi-directional

public class Account
{
   public int Id { get; set; } //as Id in Accounts Table

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

public class User
{
   public int Id { get; set; } //as Id in Users Table

   public Account Account { get; set; } // as Account_Id in Users Table
}

// and of course you need your context class, but with less code

public class AccountContext : DbContext
{
   public AccountContext()
        : base("DefaultConnection")
    {

    }

   public DbSet<Account> Accounts { get; set; }

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

I hope this helps you or anyone else in doubt

Edit

If you want to avoid Bi-Directional navigation then make changes to Users like this

public class User
{
   public int Id { get; set; }

   //delete below from User class to avoid Bi-directional navigation 
   //public Account Account { get; set; }
}

Note: Not tested but the logic is sound




回答2:


You can write your own custom convention, but it would really get complex (e.g. what if Account has two collection nav properties of Users? How would EF know which collection nav property is referenced by the single User.AccountId property? There are numerous possible caveats/gotchas and it might not be possible to account for them all.

My below example will work in the scenario you describe, but may start to break down if your models get more complex. You should filter the possible entity types to those you expect to be in your model (the below example checks all types in the app domain). I strongly recommend you simply use the fluent api or data annotations over the below, but it does work for your stated needs and is an interesting example of a custom convention.

// I recommend filtering this
var possibleEntityTypes = AppDomain.CurrentDomain.GetAssemblies()
    .SelectMany( ass => ass.GetTypes() );

modelBuilder.Properties()
    .Where( cp => 
        IsValidForeignKeyType( cp.PropertyType ) &&
        cp.Name.Length > 2 && 
        ( cp.Name.EndsWith( "ID" ) || cp.Name.EndsWith( "Id" ) ) &&
        !cp.Name.Substring( 0, cp.Name.Length - 2 ).Equals( cp.ReflectedType.Name, StringComparison.OrdinalIgnoreCase ) )
    .Configure( cppc => 
    {
        var sourcePropertyType = cppc.ClrPropertyInfo.PropertyType;
        var sourceEntityType = cppc.ClrPropertyInfo.ReflectedType;
        var targetEntityName = cppc.ClrPropertyInfo.Name.Substring( 0, cppc.ClrPropertyInfo.Name.Length - 2 );
        var icollectionType = typeof( ICollection<> ).MakeGenericType( sourceEntityType );

        // possible problem of multiple classes with same name but different namespaces
        // for this example I simply select the first but this should be more robust
        // e.g. check for ID/ClassNameID property in the class or require same 
        // namespace as the property's class
        var targetEntityType = possibleEntityTypes.FirstOrDefault( t =>
            t.Name == targetEntityName &&
            // check if the type has a nav collection property of the source type
            t.GetProperties().Any( pi =>
                pi.PropertyType.IsGenericType &&
                icollectionType.IsAssignableFrom( pi.PropertyType ) ) );

        if( null != targetEntityType )
        {
            // find the nav property
            var navPropInfos = targetEntityType.GetProperties()
                .Where( pi =>
                    pi.PropertyType.IsGenericType && 
                    icollectionType.IsAssignableFrom( pi.PropertyType ) && 
                    pi.PropertyType.GetGenericArguments().First() == sourceEntityType );

            if( 1 != navPropInfos.Count() )
            {
                // more than one possible nav property, no way to tell which to use; abort
                return;
            }

            var navPropInfo = navPropInfos.First();

            // get EntityTypeConfiguration for target entity
            var etc = modelBuilder.GetType().GetMethod( "Entity" )
                .MakeGenericMethod( targetEntityType )
                .Invoke( modelBuilder, new object[] { } );

            var etcType = etc.GetType();

            var tetArg = Expression.Parameter( targetEntityType, "tet" );

            // Invoke EntityTypeConfiguration<T>.HasMany( x => x.Col )
            // returns ManyNavigationPropertyConfiguration object
            var mnpc = etcType.GetMethod( "HasMany" ).MakeGenericMethod( sourceEntityType )
                .Invoke( etc, new[] { 
                    Expression.Lambda(
                        Expression.Convert( 
                            Expression.Property( 
                                tetArg, 
                                navPropInfo ),
                            icollectionType ),
                        tetArg ) } );

            string withMethodName = ( sourcePropertyType.IsPrimitive || sourcePropertyType == typeof( Guid ) )
                ? "WithRequired"
                : "WithOptional";

            // Invoke WithRequired/WithOptional method
            // returns DependentNavigationPropertyConfiguration object
            var dnpc = mnpc.GetType().GetMethods().Single( mi =>
                mi.Name == withMethodName && !mi.GetParameters().Any() )
                .Invoke( mnpc, new object[] { } );

            var setArg = Expression.Parameter( sourceEntityType, "set" );

            // Invoke HasForiegnKey method
            var x = dnpc.GetType().GetMethod( "HasForeignKey" ).MakeGenericMethod( sourcePropertyType )
                .Invoke( dnpc, new[]{
                    Expression.Lambda(
                        Expression.Property(
                            setArg,
                            cppc.ClrPropertyInfo ),
                        setArg ) } );
        }
    });

Helper method:

public static bool IsValidForeignKeyType( Type type )
{
    var retVal = type.IsPrimitive ||
        type == typeof( string ) ||
        type == typeof( Guid );

    if( !retVal )
    {
        if( type.IsGenericType && type.GetGenericTypeDefinition() == typeof( Nullable<> ) )
        {
            var genArgType = type.GetGenericArguments().Single();

            retVal = genArgType.IsPrimitive || genArgType == typeof( Guid );
        }
    }

    return retVal;
}


来源:https://stackoverflow.com/questions/21766591/ef-code-first-duplicate-foreign-keys-one-from-name-convention-one-from-naviga

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!