Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Entity Framework Code First Base Class Not Mapped To DB

I am using the Entity Framework Code First in C# and I have many entities that have the same columns used for tracking. The columns I am using are Active, IsDeleted, CreatedBy, ModifiedBy, DateCreated, and DateUpdated. It seems tedious to have to add these properties to every entity that I want to track. I would like to have a base class that my entities can inherit from such as the one below.

public abstract class TrackableEntity
{
    public bool Active { get; set; }
    public bool IsDeleted { get; set; }
    public virtual User CreatedBy { get; set; }
    public virtual User ModifiedBy { get; set; }
    public DateTime DateCreated { get; set; }
    public DateTime DateModified { get; set; }
}

Then I could inherit from this class in my entities to have these properties and when the database gets generated it would have these columns for each entity.

public class UserProfile : TrackableEntity, IValidatableObject
{
    public int Id { get; set; }
    public string FirstName { get; set; }
    public string LastName { get; set; }
    public string Email { get; set; }
    public string Phone { get; set; }

    public bool IsValid { get { return this.Validate(null).Count() == 0; } }

    public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
    {
        if (String.IsNullOrEmpty(FirstName))
            yield return new ValidationResult("First name cannot be blank", new[] { "Username" });

        if (String.IsNullOrEmpty(LastName))
            yield return new ValidationResult("Last cannot be blank", new[] { "Password" });

        //Finish Validation Rules
    }
}

I really want to cut back on code duplication and use some type of method like this but I can't get this to work. I keep receiving the error below:

Unable to determine the principal end of an association between the types 'Namespace.Models.User' and 'Namespace.Models.User'. The principal end of this association must be explicitly configured using either the relationship fluent API or data annotations.

I have been searching around for a few days now and can't find an answer for what I am wanting to do. I really don't want to create a separate table that everything links to. I have read about TPT, TPH, and TPC. TPC seemed pretty close to what I want but sharing PKs across pretty much all my tables is some I definitely don't want to do.

Here is an example of what I would like my tables to look like. These tables would be created by entities that inherit from the TrackableEntity.

[UserProfile]
    [Id] [int] IDENTITY(1,1) NOT NULL
    [FirstName] [varchar](50) NOT NULL
    [LastName] [varchar](50) NOT NULL
    [Email] [varchar](255) NOT NULL
    [Phone] [varchar](20) NOT NULL
    [CreatedBy] [int] NOT NULL
    [ModifiedBy] [int] NOT NULL
    [DateCreated] [datetime] NOT NULL
    [DateModified] [datetime] NOT NULL
    [Active] [bit] NOT NULL
    [IsDeleted] [bit] NOT NULL

[CaseType]
    [Id] [int] IDENTITY(1,1) NOT NULL
    [Name] [varchar](50) NOT NULL
    [CreatedBy] [int] NOT NULL
    [ModifiedBy] [int] NOT NULL
    [DateCreated] [datetime] NOT NULL
    [DateModified] [datetime] NOT NULL
    [Active] [bit] NOT NULL
    [IsDeleted] [bit] NOT NULL
like image 531
DSlagle Avatar asked Oct 31 '12 03:10

DSlagle


1 Answers

Most likely you are having this exception because you have your User class derived from TrackableEntity as well:

public class User : TrackableEntity

The consequence is that the User entity now contains the two inherited properties

public virtual User CreatedBy { get; set; }
public virtual User ModifiedBy { get; set; }

and Entity Framework by convention will assume a relationship between the two properties, i.e. one relationship that has those two navigation properties as its two ends. Because the navigation properties are references and not collections EF infers a one-to-one relationship. Because in the model isn't specified which of the two navigation properties has the related foreign key for the relationship EF cannot determine what's the principal and what's the dependent of the relationship - which causes the exception.

Now, this problem could be solved - as the exception tells - by defining principal and dependent explicitly. But in your model the convention - namely to assume a one-to-one relationship - is incorrect. You actually need two relationships, one from CreatedBy (with its own foreign key) and one from ModifiedBy (with another foreign key). Both relationships are one-to-many (because a User can be the creator or modifier of many other users) and don't have a navigation collection at the other end of the relationship. You must supply a mapping with Fluent API to override the convention and define those two one-to-many relationships:

public class UnicornsContext : DbContext
{
    public DbSet<User> Users { get; set; }
    public DbSet<UserProfile> UserProfiles { get; set; }
    public DbSet<CaseType> CaseTypes { get; set; }
    // ... etc.

    protected override void OnModelCreating(DbModelBuilder modelBuilder)
    {
        modelBuilder.Entity<User>()
            .HasOptional(u => u.CreatedBy)
            .WithMany()
            .Map(m => m.MapKey("CreatedBy")); // FK column name in DB table

        modelBuilder.Entity<User>()
            .HasOptional(u => u.ModifiedBy)
            .WithMany()
            .Map(m => m.MapKey("ModifiedBy")); // FK column name in DB table
    }
}

Note, that I have used HasOptional instead of HasRequired (which means that the FK in the database will be nullable), because - at least with autoincremented Ids - you can't create the very first user with a CreatedBy or ModifiedBy being set - it could only refer to itself but there isn't any valid FK value to be used because the very first PK hasn't been created yet.

(You could also consider to avoid the relationship and referential constraint of CreatedBy or ModifiedBy altogether and store only the user's name as creator and modifier instead, since for auditing and tracking purposes it might be enough to save a unique person's identity. Do you really need to navigate from every entity to its creator and modifier? Even if you need it in exceptional cases, you could still manually join to the Users table. And would it be a problem if the user gets deleted from the Users table as long as the name is still stored in CreatedBy or ModifiedBy?)

You don't have to deal with any inheritance mapping as long as you don't introduce a DbSet for the abstract base class...

public DbSet<TrackableEntity> TrackableEntities { get; set; } // NO!

...or a mapping in Fluent API for that class...

modelBuilder.Entity<TrackableEntity>()... // NO!

...or use the class as a navigation property in any other class:

public class SomeEntity
{
    //...
    public virtual TrackableEntity Something { get; set; } // NO!
    public virtual ICollection<TrackableEntity> Somethings { get; set; } // NO!
}

With any of these EF will infer TrackableEntity as an entity and introduce an inheritance mapping between model and database tables (TPH by default). Otherwise TrackableEntity is just a base class and every property within the base class will be considered as if it were a property in the derived entity and mapped as such to a database table.

like image 79
Slauma Avatar answered Oct 27 '22 20:10

Slauma