I have the following class that I would like to use as property of a Login object. I'd prefer that this class not have any navigation properties (as it will just be used for quick checks) if that is at all possible, but I'm willing to allow the one shown below.
public class LoginFeature
{
[Key, Column(Order = 0)]
public int RoleId { get; set; } //Role is another table in the db, but not looking for a nav. property or constraint here.
[Key, Column(Order = 1)]
public virtual Login Login { get; set; }
public bool Deny { get; set; }
}
The class containing a collection of these is (stripped down for space)
public class Login
{
[Key, DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public int Id { get; set; } //Database ID
public virtual List<LoginFeature> LoginFeatures { get; set; }
}
My DbContext is defined as
public class MyContext : DbContext
{
public DbSet<Login> Logins { get; set; }
public DbSet<LoginFeature> LoginFeatures { get; set; }
}
But the following test generates an error saying that
System.Data.Entity.Infrastructure.DbUpdateException : An error occurred while saving entities that do not expose foreign key properties for their relationships. The EntityEntries property will return null because a single entity cannot be identified as the source of the exception. Handling of exceptions while saving can be made easier by exposing foreign key properties in your entity types. See the InnerException for details.
----> System.Data.UpdateException : An error occurred while updating the entries. See the inner exception for details.
----> System.Data.SqlClient.SqlException : Cannot insert the value NULL into column 'RoleId', table 'TEST.dbo.LoginFeatures'; column does not allow nulls. INSERT fails.
The statement has been terminated.
[Test]
public void LoginCanHaveFeatures()
{
using (var ctx = new MyContext())
{
var login = ctx.Logins.FirstOrDefault(x => x.Id == 30);
Assert.IsNotNull(login);
for (int i = 10; i < 15; i++)
{
var feature = new LoginFeature();
feature.Login = login;
feature.RoleId = i;
feature.Deny = true;
login.LoginFeatures.Add(feature);
}
ctx.SaveChanges();
}
}
The SQL being generated as show in EFProf is
insert [dbo].[LoginFeatures]
([Deny],
[Login_Id])
values (1 /* #0 */,
30 /* #1 */)
Which seems to imply that the data annotation attributes on LoginFeature are incorrect. Is what I am trying to do possible?
Thanks,
Joe
You cannot define a navigation property as a key. Only primitive properties are supported as keys. So, you should define the class like so:
public class LoginFeature
{
[Key, Column(Order = 0)]
public int RoleId { get; set; }
[Key, Column(Order = 1)]
public int LoginId { get; set; }
public virtual Login Login { get; set; }
public bool Deny { get; set; }
}
Mapping conventions will detect LoginId as foreign key for Login. The error probably occurs because EF did ignore your second key attribute (as it was on a navigation property), uses only the first key RoleId and by default (for single keys) assumes that the key is autogenerated in the database (which it apparently isn't) and doesn't send its value to the database.
Related
I have a set of Entities:
public class Board : EntityBase
{
public ICollection<Slot> Slots { get; set; }
}
public class Slot : EntityBase
{
public ICollection<Card> Cards { get; set; }
public string Header { get; set; }
public int BoardId { get; set; }
}
public class Card : EntityBase
{
public string Description { get; set; }
public string Title { get; set; }
public int SlotId { get; set; }
}
And corresponding database tables:
CREATE TABLE Boards
(
Id INT PRIMARY KEY,
UserId INT NOT NULL,
CONSTRAINT FK_Users_UserId FOREIGN KEY (UserId)
REFERENCES Users(Id)
)
CREATE TABLE Slots
(
Id INT PRIMARY KEY,
Header NVARCHAR(MAX),
BoardId INT NOT NULL,
CONSTRAINT FK_Slots_BoardId FOREIGN KEY (BoardId)
REFERENCES Boards(Id)
)
CREATE TABLE Cards
(
Id INT PRIMARY KEY,
Title NVARCHAR(MAX),
Description NVARCHAR(MAX),
SlotId INT NOT NULL,
CONSTRAINT FK_Cards_SlotId FOREIGN KEY (SlotId)
REFERENCES Slots(Id)
)
When attempting retrieving and instantiate a 'Board' from the database it's not populating the 'Slots' property. It seems that Entity framework is unable to recognise that there's a foreign key constraint. My understanding is that if the properties are not virtual they will be eager loaded, please correct me if i'm wrong.
Is there something that I'm missing / need to setup to make navigation properties work?
The calling code:
Context.Boards.Find(id);
My DbContext:
public class SampleContext : DbContext, IUnitOfWork
{
public SampleContext() : base("name=SampleApplication") { }
public void Save()
{
SaveChanges();
}
public DbSet<Board> Boards { get; set; }
public DbSet<Card> Cards { get; set; }
public DbSet<Slot> Slots { get; set; }
}
I have made the navigation properties virtual and loaded as follows, this is now working:
public Board GetBoard(int id)
{
var board = Context.Boards.Find(id);
Context.Entry(board)
.Collection(b => b.Slots)
.Load();
return board;
}
You must make navigation properties virtual for EF proxy to be able to override it.
And you're wrong about non-virtual properties to be eager loaded. They do not. You must load them explicitly with Include methods. Read here about it: https://msdn.microsoft.com/en-us/data/jj574232.aspx
Eager loading does not happen automatically like lazy loading does when you include the virtual keyword. You will need to use the Include() method
so something like
var graph = context.Boards.Include("Slots");
foreach(var board in graph)
{
Console.Writeline("Slot value {0}",board.Slots);
}
I'm using CodeFirst at a new company and at my first Code Review I've been told that they don't use IDENTITY in their database tables. :( It makes me sad too, but I don't have any choice.
I'm allowed to use a simple function to generate my ID (which I then add to the DefaultValue on the Key field) - this works in SQL - but CodeFirst is just thinking I've got an ID of 0 each time.
I've tried all of the following ... can anyone assist please?
public class Test
{
[Key]
public int ID { get; set; }
}
public class Test
{
[Key, DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public int ID { get; set; }
}
public class Test
{
[Key, DatabaseGenerated(DatabaseGeneratedOption.None)]
public int ID { get; set; }
}
public class Test
{
[Key, DatabaseGenerated(DatabaseGeneratedOption.Computed)]
public int ID { get; set; }
}
In the end I went with ...
[Key, DatabaseGenerated(DatabaseGeneratedOption.None)]
public int ID { get; set; }
... and then overrode the SaveChanges method of the database context to catch the 0 ids ...
public override int SaveChanges()
{
var pendingChanges = ChangeTracker.Entries().Where(e => e.State == EntityState.Added && e.Entity is IEntity);
foreach (var entry in pendingChanges)
{
var entity = ((IEntity)entry.Entity);
if (IsIDGenerated(entity)) continue;
if (entity.ID == 0) entity.ID = GetNextID(entry.Entity.GetType());
}
return base.SaveChanges();
}
Why don't you simply use a Guid then? The only reason I can think of for not using incrementing ids is when you need to migrate databases from time to time, and Guids are excellent for this.
Try this:
public class Foo {
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public Guid Id {get;set;}
public string Name {get;set;}
}
Or in your Context class:
modelBuilder.Entity<Foo>().Property(f => f.Id).HasDatabaseGeneratedOption(DatabaseGeneratedOption.Identity);
Although the link tables which facilitate a many-to-many relationship are usually hidden by EF, I have an instance where I think I need to create (and manage) one myself:
I have the following entities:
public class TemplateField
{
public int Id
{
get;
set;
}
[Required]
public string Name
{
get;
set;
}
}
public class TemplateFieldInstance
{
public int Id
{
get;
set;
}
public bool IsRequired
{
get;
set;
}
[Required]
public virtual TemplateField Field
{
get;
set;
}
[Required]
public virtual Template Template
{
get;
set;
}
}
public class Template
{
public int Id
{
get;
set;
}
public string Name
{
get;
set;
}
public virtual ICollection<TemplateFieldInstance> Instances
{
get;
set;
}
}
So essentially; a Template can have many TemplateField and a TemplateField can have many Template.
I believe I could just add a navigation property in the form of a collection of Template items on the TemplateField entity and have EF manage the link entity, but I need to store some additional information around the relationship, hence the IsRequired property on TemplateFieldInstance.
The actual issue I'm having is when updating a Template. I'm using code similar to the following:
var template = ... // The updated template.
using (var context = new ExampleContext())
{
// LoadedTemplates is just Templates with an Include for the child Instances.
var currentTemplate = context.LoadedTemplates.Single(t => t.Id == template.Id);
currentTemplate.Instances = template.Instances;
context.Entry(currentTemplate).CurrentValues.SetValues(template);
context.SaveChanges();
}
However; if I try and update a Template to - for example - remove one of the TemplateFieldInstance entities, it this throws an exception (with an inner exception) which states:
A relationship from the 'TemplateFieldInstance_Template'
AssociationSet is in the 'Deleted' state. Given multiplicity
constraints, a corresponding 'TemplateFieldInstance_Template_Source'
must also in the 'Deleted' state.
After doing some research, it sounds like this is because EF has essentially marked the TemplateFieldInstance foreign key to the Template as being null and then tried to save it, which would violate the Required constraint.
I'm very new to Entity Framework, so this is all a bit of a journey of discovery for me, so I'm fully anticipating there being errors in my approach or how I'm doing the update!
Thanks in advance.
You must map the relationships in your model as two one-to-many relationships. The additional field in the link table makes it impossible to create a many-to-many relationship. I would also recommend to use a composite key in your "link entity" TemplateFieldInstance where both components are foreign keys to the other entities. This ensures in the database that you can only have one row for a unique combination of a template field and a template and comes closest to the idea of a "many-to-many link table with additional data":
public class TemplateField
{
public int Id { get; set; }
[Required]
public string Name { get; set; }
public virtual ICollection<TemplateFieldInstance> Instances { get; set; }
}
public class TemplateFieldInstance
{
[Key, Column(Order = 0)]
public int FieldId { get; set; }
[Key, Column(Order = 1)]
public int TemplateId { get; set; }
public bool IsRequired { get; set; }
public virtual TemplateField Field { get; set; }
public virtual Template Template { get; set; }
}
public class Template
{
public int Id { get; set; }
public string Name { get; set; }
public virtual ICollection<TemplateFieldInstance> Instances { get; set; }
}
EF naming conventions will detect the FK relations in this model if you use the property names above.
More details about such a model type are here: https://stackoverflow.com/a/7053393/270591
Your approach to update the template is not correct: context.Entry(currentTemplate).CurrentValues.SetValues(template); will only update the scalar fields of the template, not the navigation properties nor will it add or remove any new or deleted child entities of the parent entity. Unfortunately updating detached object graphs doesn't work that easy and you have to write a lot more code, something like this:
var template = ... // The updated template.
using (var context = new ExampleContext())
{
// LoadedTemplates is just Templates with an Include for the child Instances.
var currentTemplate = context.LoadedTemplates
.Single(t => t.Id == template.Id);
context.Entry(currentTemplate).CurrentValues.SetValues(template);
foreach (var currentInstance in currentTemplate.Instances.ToList())
if (!template.Instances.Any(i => i.Id == currentInstance.Id))
context.TemplateFieldInstances.Remove(currentInstance); // DELETE
foreach (var instance in template.Instances)
{
var currentInstance = currentTemplate.Instances
.SingleOrDefault(i => i.Id == instance.Id);
if (currentInstance != null)
context.Entry(currentInstance).CurrentValues.SetValues(instance);
// UPDATE
else
currentTemplate.Instances.Add(instance); // INSERT
}
context.SaveChanges();
}
A similar example with more comments what is happening is here: https://stackoverflow.com/a/5540956/270591
I am using EF 4.1 code first and I am struggling with the association entity and getting the value that was set in the association table. I tried to follow the post on: Create code first, many to many, with additional fields in association table.
My tables are as follows (all are in plural form):
Table: Products
Id int
Name varchar(50)
Table: Specifications
Id int
Name varchar(50)
Table: ProductSpecifications
ProductId int
SpecificationId int
SpecificationValue varchar(50)
My related classes:
public class Product : IEntity
{
public int Id { get; set; }
public string Name { get; set; }
public virtual ICollection<ProductSpecification> ProductSpecifications { get; set; }
}
public class Specification : IEntity
{
public int Id { get; set; }
public string Name { get; set; }
public virtual ICollection<ProductSpecification> ProductSpecifications { get; set; }
}
public class ProductSpecification
{
public int ProductId { get; set; }
public virtual Product Product { get; set; }
public int SpecificationId { get; set; }
public virtual Specification Specification { get; set; }
public string SpecificationValue { get; set; }
}
My context class:
public class MyContext : DbContext
{
public DbSet<Product> Products { get; set; }
public DbSet<Specification> Specifications { get; set; }
public DbSet<ProductSpecification> ProductSpecifications { get; set; }
protected override void OnModelCreating(DbModelBuilder dbModelBuilder)
{
}
}
My repository method where I do my call (not sure if it is correct):
public class ProductRepository : IProductRepository
{
MyContext db = new MyContext();
public Product GetById(int id)
{
var product = db.Products
.Where(x => x.Id == id)
.Select(p => new
{
Product = p,
Specifications = p.ProductSpecifications.Select(s => s.Specification)
})
.SingleOrDefault();
return null; // It returns null because I don't know how to return a Product object?
}
}
Here is the error that I am getting back:
One or more validation errors were detected during model generation:
System.Data.Edm.EdmEntityType: : EntityType 'ProductSpecification' has no key defined. Define the key for this EntityType.
System.Data.Edm.EdmEntitySet: EntityType: EntitySet �ProductSpecifications� is based on type �ProductSpecification� that has no keys defined.
What does it mean that no keys are defined? Won't the ProductId and SpecificationId map to Id of Product and Id of Specification respectively?
How would I return a single product with the all the specifications for it?
Entity Framework will recognize that ProductId is a foreign key property for the Product navigation property and SpecificationId is a foreign key property for the Specification navigation property. But the exception complains about a missing primary key ("Key" = "Primary Key") on your ProductSpecification entity. Every entity needs a key property defined. This can happen either by conventions - by a specific naming of the key property - or explicity with data annotations or Fluent API.
Your ProductSpecification class doesn't have a property which EF would recognize as a key by convention: No Id property and no ProductSpecificationId (class name + "Id").
So you must define it explicitely. Defining it with data annotations is shown in the post you linked:
public class ProductSpecification
{
[Key, Column(Order = 0)]
public int ProductId { get; set; }
public virtual Product Product { get; set; }
[Key, Column(Order = 1)]
public int SpecificationId { get; set; }
public virtual Specification Specification { get; set; }
public string SpecificationValue { get; set; }
}
And in Fluent API it would be:
modelBuilder.Entity<ProductSpecification>()
.HasKey(ps => new { ps.ProductId, ps.SpecificationId });
Both ways define a composite key and each of the parts is a foreign key to the Product or Specification table at the same time. (You don't need to define the FK properties explicitely because EF recognizes it due to their convention-friendly names.)
You can return a product including all specifications with eager loading for example:
var product = db.Products
.Include(p => p.ProductSpecifications.Select(ps => ps.Specification))
.SingleOrDefault(x => x.Id == id);
I am having trouble getting referential integrity dialled down enough to allow my delete trigger to fire.
I have a dependent entity with three FKs. I want it to be deleted when any of the principal entities is deleted.
For principal entities Role and OrgUnit (see below) I can rely on conventions to create the required one-many relationship and cascade delete does what I want, ie: Association is removed when either principal is deleted.
For Member, however, I have multiple cascade delete paths (not shown here) which SQL Server doesn't like, so I need to use fluent API to disable cascade deletes.
Here is my (simplified) model:
public class Association
{
public int id { get; set; }
public int roleid { get; set; }
public virtual Role role { get; set; }
public int? memberid { get; set; }
public virtual Member member { get; set; }
public int orgunitid { get; set; }
public virtual OrgUnit orgunit { get; set; }
}
public class Role
{
public int id { get; set; }
public virtual ICollection<Association> associations { get; set; }
}
public class Member
{
public int id { get; set; }
public virtual ICollection<Association> associations { get; set; }
}
public class Organization
{
public int id { get; set; }
public virtual ICollection<Association> associations { get; set; }
}
My first run at fluent API code looks like this:
protected override void OnModelCreating(System.Data.Entity.ModelConfiguration.ModelBuilder modelBuilder)
{
DbDatabase.SetInitializer<ConfDB_Model>(new ConfDBInitializer());
modelBuilder.Entity<Member>()
.HasMany(m=>m.assocations)
.WithOptional(a=>a.member)
.HasForeignKey(a=>a.memberId)
.WillCascadeOnDelete(false);
}
My seed function creates the delete trigger:
protected override void Seed(ConfDB_Model context)
{
context.Database.SqlCommand("CREATE TRIGGER MemberAssocTrigger ON dbo.Members FOR DELETE AS DELETE Assocations FROM Associations, deleted WHERE Associations.memberId = deleted.id");
}
PROBLEM --
When I run this, create a Role, a Member, an OrgUnit, and an Association tying the three together all is fine. When I delete the Role, the Association gets cascade deleted as I expect, same with OrgUnit. -- HOWEVER -- when I delete the Member I get an exception with a referential integrity error. I have tried setting ON CASCADE SET NULL because my memberid FK is nullable but SQL complains again about multiple cascade paths, so apparently I can cascade nothing in the Member-Association relationship.
To get this to work I must add the following code to Seed():
context.Database.SqlCommand("ALTER TABLE dbo.ACLEntries DROP CONSTRAINT member_associations");
As you can see, this drops the constraint created by the model builder.
QUESTION: this feels like a complete hack. Is there a way using fluent API for me to say that referential integrity should NOT be checked, or otherwise to get it to relax enough for the Member delete to work and allow the trigger to be fired?
Thanks in advance for any help you can offer. Although fluent APIs may be "fluent" I find them far from intuitive.
var listTriggers = new List<string>();
var listStoreProcedures = new List<string>();
using (var command = _context.Database.GetDbConnection().CreateCommand())
{
command.CommandText = "select group_concat(TRIGGER_NAME) from information_schema.TRIGGERS where TRIGGER_SCHEMA = 'yourschema'";
command.CommandType = CommandType.Text;
_context.Database.OpenConnection();
using (var reader = command.ExecuteReader())
{
while (reader.Read())
{
var value = reader[0].ToString();
if (!string.IsNullOrEmpty(value))
listTriggers.AddRange(value.Split(","));
}
}
command.CommandText = "select group_concat(ROUTINE_NAME) from information_schema.ROUTINES where ROUTINE_TYPE = 'PROCEDURE' and ROUTINE_SCHEMA = 'yourschema'";
using (var reader = command.ExecuteReader())
{
while (reader.Read())
{
var value = reader[0].ToString();
if (!string.IsNullOrEmpty(value))
listStoreProcedures.AddRange(value.Split(","));
}
}
}
foreach (var item in listTriggers)
_context.Database.ExecuteSqlRaw($"drop trigger if exists {item}");
foreach (var item in listStoreProcedures)
_context.Database.ExecuteSqlRaw($"drop procedure if exists {item}");