简体   繁体   中英

Different behavior across domain objects for same properties in Entity Framework Core 2

I am new to .NET Core and using EF Core 2

My domain objects are all derived from a base class with some audit fields on it that get set on SaveChanges as needed: (Simplified below)

public abstract class AuditableEntity
{
    public DateTime CreatedOn { get; set; }
    [ForeignKey("CreatedBy")]
    public Guid? CreatedByWebUserId { get; set; }
    public WebUser CreatedBy { get; set; }

    public DateTime? UpdatedOn { get; set; }
    [ForeignKey("UpdatedBy")]
    public Guid? UpdatedByWebUserId { get; set; }
    public WebUser UpdatedBy { get; set; }

    public DateTime? DeletedOn { get; set; }
    [ForeignKey("DeletedBy")]
    public Guid? DeletedByWebUserId { get; set; }
    public WebUser DeletedBy { get; set; }
}

On add-migration, I get the error:

Unable to determine the relationship represented by navigation property 
'Address.CreatedBy' of type 'WebUser'. Either manually configure the 
relationship, or ignore this property using the '[NotMapped]' attribute or by 
using 'EntityTypeBuilder.Ignore' in 'OnModelCreating'.

Address is one of the classes derived from AuditableEntity : (Simplified below)

public class Address : AuditableEntity
{
    public Guid Id { get; set; }
    public string Nickname { get; set; }
    public string Address1 { get; set; }
    public string Address2 { get; set; }
    public string Address3 { get; set; }
    public string City { get; set; }
    public string StateProvince { get; set; }
    public string PostalCode { get; set; }
    public string CountryCode { get; set; }
    public decimal Latitude { get; set; }
    public decimal Longitude { get; set; }
}

However, I have several objects that use the same "agent and timestamp" pair pattern similar to the above that work just fine such as:

    public DateTime? VerifiedOn { get; set; }
    [ForeignKey("VerifiedBy")]
    public Guid? VerifiedByWebUserId { get; set; }
    public WebUser VerifiedBy { get; set; }

The error always comes from Address , and if I remove the base class from Address everything works fine (meaning, these fields get successfully applied to my 15+ other domain objects).

The issue seemingly stems from WebUser having a reference to Address : (Simplified below)

public class WebUser : AuditableEntity
{
    public Guid Id { get; set; }
    public string FirstName { get; set; }
    public string LastName { get; set; }
    public string EmailAddress { get; set; }
    public string Phone1 { get; set; }
    public string Phone1Type { get; set; }
    public string Phone2 { get; set; }
    public string Phone2Type { get; set; }
    [ForeignKey("AddressId")]
    public Address Address { get; set; }
    public Guid? AddressId { get; set; }
}

What is the correct way of creating these references prioritizing keeping the FK constraints (over keeping the ability to navigate)?

The problem is unrelated to the usage of a base class (the same will happen if you remove the base class, but copy its properties to Address class), but the multiple cross references between the two classes.

By convention EF Core tries to automatically "pair" navigation properties of the two entities in order to form a single relationship, which succeeds in most of the cases. However in this case the WebUser has Address type navigation property and Address class has WebUser type navigation property (actually 3).

Since all they have associated FK property via ForeignKey data annotation, EF Core should be able to correctly identify them as different one-to-many relationships, but it doesn't. Not only it fails with the exception in question, but also doesn't create FK relationships for the WebUser .

Everything works correctly if the base class contains only 1 WebUser type of navigation property, so I'm assuming thet unfortunately you are hitting some current EF Core bug.

As a workaround until they fixed it, I would suggest explicitly configuring the problematic relationships using fluent API, by overriding the OnModelCreating and adding the following code:

var auditableEntityTypes = modelBuilder.Model.GetEntityTypes().Where(t => t.ClrType.IsSubclassOf(typeof(AuditableEntity)));
var webUserNavigations = new[] { nameof(AuditableEntity.CreatedBy), nameof(AuditableEntity.DeletedBy), nameof(AuditableEntity.UpdatedBy) };
foreach (var entityType in auditableEntityTypes)
{
    modelBuilder.Entity(entityType.ClrType, builder =>
    {
        foreach (var webUserNavigation in webUserNavigations)
            builder.HasOne(typeof(WebUser), webUserNavigation).WithMany();
    });
}

ie for each entity class that derives from AuditableEntity we explicitly configure the 3 WebUser reference navigation properties to be mapped to 3 separate one-to-many relationships with no inverse collection navigation properties. Once we do that, EF Core has no problem to correctly map the WebUser.Address FK association.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM