问题
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 User
s? 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