entity framework does not map more than one list in model C#

1.2k views Asked by At

I have a class "Project" that hat two lists one for "Employee" and one for "tool", when i build my project with only one of the lists the database updates with corrrect relation, if add the second list to the class entity framework can not figureout how to create the relation.

 public class Project
{        
    public Guid Id { get; set; }
    public List<Employee> RequiredEmployees {get;set;}
    public List<Tools> RequiredTools {get;set;}
}

Public class Tool
{
    public Guid Id { get; set; }
    public string Name { get; set; }
    public Project Project { get; set; }
    public Guid ProjectId { get; set; }
}

I get the error "There are no primary or candidate keys in the referenced table" i hve tryed to map in ModelBuilder without success

 modelBuilder.Entity<Employee>()
    .HasRequired<Project>(s => s.Project)
    .WithMany(g => g.Employee)
    .HasForeignKey<Guid>(s => s.ProjectId);

any ideas ?

2

There are 2 answers

2
Harald Coppoolse On BEST ANSWER

You have maneuvered yourself in several problems. The reported error is about the primary key. Furthermore you have problems specifying the one-to-many relations.

One-To-Many relations

Entity framework is easy if you follow the code first conventions For every deviation you need to tell entity framework about your deviations.

You planned to design a one-to-many between a Project and Tool: every Project has zero or more Tools, every Tool belongs to exactly one Project

If you follow the convention for one-to-many, you don't have to inform entity framework about this relation; entity framework will detect the relation by convention:

  • Use an ICollection instead of a List, after all, what would RequiredTools[4] mean?
  • Make the ICollection virtual. As long as your Project is a query, it is not a real ICollection, it only contains a class that can produce an ICollection.

Consider using the proper naming conventions.:

class Project
{        
    public Guid Id { get; set; }
    // a project has zero or more Employees:
    public virtual ICollection<Employee> Employees {get;set;}
    // a project has zero or more Tools
    public virtual ICollectioin<Tool> Tools {get;set;}
}

Every Tool belongs to exactly one Project, using foreign key:

class Tool
{
    public Guid Id { get; set; }
    public string Name { get; set; }
    public Guid ProjectId {get; set;}
    public virtual Project Project { get; set; }
}

Similarly: Every Employee belongs to one Project

class Employee
{
    public Guid Id { get; set; }
    public string Name { get; set; }
    public Guid ProjectId {get; set;}
    public virtual Project Project { get; set; }
}

Or use many-to-many if an Employee can participate in several Projects:

class Employee
{
    public Guid Id { get; set; }
    public string Name { get; set; }
    // an Employee works in zero or more Projects
    public virtual ICollection<Project> Projects { get; set; }
}

And your Dbcontext:

class MyDbContext : DbContext
{
    public DbSet<Employee> Employees {get; set;}
    public DbSet<Project> Projects {get; set;}
    public DbSet<Tool> Tools {get; set;}
}

This would be enough for entity framework to detect your relations and table names. I'd advice you to reconsider your decision to deviate from the code-first conventions.

However, if you really need to deviate your names you'll have to tell entity framework about your relations and table names:

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

    // a Project has zero or more Tools via property RequiredTools and table TableName:
    var entityProject = modelBuilder.Entity<Project>();
    entityProject.ToTable(... /* table name */);
    // a project has zero or more tools via property RequiredTools
    // every tool belongs to one required Project 
    // using foreign key ProjectId
    entityproject.HasMany(project => project.RequiredTools)
        .WithRequired(tool => tool.Project)
        .HasFreignKey(tool => tool.ProjectId);
}

Primary Keys

You chose not to use the conventional type for your primary Keys. The problem is that entity framework does not know how to fill your Id. You'll have to tell entity framework that you'll fill the values for the primary keys.

This is done by overriding DbContext.SaveChanges

There are also two SaveChangesAsync; we will override them to. Both procedures will call the same function that will generate your Id:

public override int SaveChanges()
{
    GenerateIds();
    return base.SaveChanges();
}
public override Task<int> SaveChangesAsync()
{
    GenerateIds();
    return base.SaveChangesAsync();
}
public override Task<int> SaveChangesAsync(CancellationToken cancellationToken)
    {
        this.GenerateIds();
        return base.SaveChangesAsync(cancellationToken);
    }

Function GenerateIds will get all items that need an Id and give it an Id. The objects that need an Id are all objects with state Added.

However, the object in the DbContext that remembered all changes that need to be saved, only knows that these objects are DbEntityEntries. It does not know which property in your DbEntityEntry represents the primary key.

You could solve this using KeyAttribute. Get the added entry, ask for its type, ask the type for the property that has the KeyAttribute and set the value of this property.

This method has several disadvantages. Reflection is a fairly slow process, it depends on you giving all classes that need a GUID to add the KeyAttribute, it is not type safe, and you will only detect that your made mistakes at run-time.

It is much better to define that all your entity classes should implement an interface that defines the primary key:

public interface IPrimaryKey
{
     GUID Id {get; set;}
}

class Tool : IPrimaryKey {...}
class Project : IPrimaryKey {...}

If you forgot to implement your Id, your compiler will warn you. GenerateIds is now fairly simple:

private void GenerateIds()
{
    foreach (DbEntityEntry addedEntry in this.ChangeTracker.Entries()
       .Where(entry => entry.State == EntityState.Added))
    {
        // every entry implements IPrimaryKey. Fill the Id           
        (IPrimaryKey)entry.Id = this.CreateId
    }
}

private GUID CreateId()
{
    return Guid.NewGuid(); // or use your own Guid creator
}
1
Tao Gómez Gil On

To be sure we would need to see your Tools class, but I think that the problem is that Entity Framework can't find a property to use as Primary Key in it.

The Entity Framework convention establishes that the Primary Key of a class is named Id or <ClassName>Id (in this case ToolsId). If you want a property with a name different from this ones to be the PK, you have to specify it explicitly, with the [Key] attribute or with the Fluent API.