I'm using Entity Framework Code-First to rebuild an application that used to run from an Access database. One of the requirements is that the new data schema should be auditable, that is it should show who created a record and who updated it and when etc.
I've created a base Entity class as follows:
public class Entity
{
public int Id { get; set; }
public int CreatedByUserId { get; set; }
public int? UpdatedByUserId { get; set; }
public virtual User CreatedBy { get; set; }
public virtual User UpdatedBy { get; set; }
}
Then I created a class that inherits from EntityTypeConfiguration as follows
public class BaseEntityTypeConfiguration<T> : EntityTypeConfiguration<T> where T : Entity
{
Property(e => e.Id).HasColumnName(typeof(T).Name + "Id");
HasRequired(e => e.CreatedBy).WithMany().HasForeignKey(e => e.CreatedById);
HasOptional(e => e.UpdatedBy).WithMany().HasForeignKey(e => e.UpdatedById);
}
Now I create configurations that inherit from BaseEntityTypeConfiguration for the rest of my business classes that inherit from my Entity class.
The problem comes when I try to make my User class inherit from entity as follows:
public class User : Entity
{
public string Username { get; set; }
// etc
}
I'll be adding a "ghost" user for records where the evidence isn't there to determine who created the record, but this ghost user will essentially be created by itself.
I'm getting the following error from Entity Framework when I try to add this ghost user:
Unable to determine a valid ordering for dependent operations. Dependencies may exist due to foreign key constraints, model requirements or store-generated values.
There may be problems in my domain model that could be causing this error, but my theory is that it's down to this user that's trying to create itself in this instance.
Is having a self-referencing foreign key constraint problematic?
Your PK is an identity column and you're setting the ghost user's CreatedByUser property with itself. This causes a chicken/egg scenario - you need the User.Id value as the User.CreatedById value to insert the record into the DB table, but you don't know what User.Id is until after the record is inserted.
If you can be sure of the identity's seed value (EF seems to default to 1), you can set the CreatedByUserId property to that value instead of CreatedByUser.
Otherwise, create your ghost user by executing a SQL statement allowing you to manually set the Id and CreatedByUserId fields to the same value then reseed the identity to Id + 1.
Example of the former:
public class UserWithCreatedBy
{
[Key]
[DatabaseGenerated( DatabaseGeneratedOption.Identity )]
public int Id { get; set; }
public int CreatedById { get; set; }
[ForeignKey( "CreatedById" )]
public UserWithCreatedBy CreatedBy { get; set; }
}
static void Main( string[] args )
{
using( var db = new TestContext() )
{
var u = new UserWithCreatedBy();
// doesn't work with identity
//u.CreatedBy = u;
// this will work as long as you know what the identity seed is
// (whatever the next identity value will be)
u.CreatedById = 1;
db.UsersWithCreatedBy.Add( u );
db.SaveChanges();
}
}
Related
I'm new to entity framework and even if i know how to do it in Merise, i can't do it using code first.
In an entity User, i should have a foreign Key 'Promotion_Id'
In an entity Promotion, i should have a foreign key 'Pilote_Id' that points out to the User entity.
Here is the thing : i also have a List in Promotion which is a list of all users in a promotion. Pilote_Id is the Id of the pilote of that formation, who's, of course, a user.
I tried the following :
public class User : EntityWithId
{
public string FirstName { get; set; }
public string LastName { get; set; }
public string Email { get; set; }
public string Password { get; set; }
public string Phone { get; set; }
public virtual Promotion Promotion { get; set; }
}
public class Promotion : EntityWithNameAndId
{
//Site is another entity, the place where the promotion is
public virtual Site Site { get; set; }
public List<User> Users { get; set; }
public virtual User Pilote { get; set; }
}
(Note : EntityWithId only contains an Id and EntityWithNameAndId inherits from EntityWithId and only contains a name)
But it only results in having 2 foreign keys in User named Promotion_Id and Promotion_Id1.
I already maked the whole thing work by changing
public virtual User Pilote { get; set; }
with
public virtual Guid PiloteId { get; set; }
But i want some consistency in my entities so.. Is there a correct way to achieve this ?
You will probably need to use explicit mapping to achieve this:
In the OnModelCreating() for your context:
modelBuilder.Entity<User>()
.HasOptional(u => u.Promotion)
.WithRequired(p => p.Pilote)
.Map(u => u.MapKey("PiloteId"); // EF6
// .HasForeignKey("PilotId") // EF Core
This assumes that a user may, or may not have a Promotion, but all promotions have a Pilot.
The Promotion.Users will probably map ok by convention using a UserId on the promotion table, but if there is any issue there:
However, there is a big caveat with this approach which relates to the schema, not EF. There is no restriction/guard that will ensure that the Pilot is one of the Users associated with the promotion. A PiloteId could point to any user, and that user's promotionId may be different.
In any case, the logic around managing who is the pilot will need to be done by code, but this means either checking IDs for valid combinations, or something like:
If a User can only be associated to 1 Promotion, and one user on that promotion can be the Pilot, then you could consider adding a flag to User called "IsPilot".
Then in Promotion:
public virtual ICollection<User> Users { get; set; } = new List<User>();
[NotMapped]
public User Pilote
{
get { return Users.SingleOrDefault(u => u.IsPilote); }
set
{
var newPilote = Users.Single(u => u.UserId == value.UserId); // Ensure the user nominated for Pilote is associated with this Promotion.
var existingPilote = Pilote;
if (existingPilote != null)
existingPilote.IsPilote = false;
newPilote.IsPilote = true;
}
}
If users can be assigned to multiple promotions then you will want to update the schema and mappings to support a many-to-many relationship between user and promotions, such as a UserPromotions table containing UserId and PromotionId. In this case I would consider assigning the IsPilote in this table / linking entity, but again this would need logic to ensure that rules around 1 pilot per promotion, and whether a user can be pilot for more than one promotion.
How would you delete a relationship assuming you had the 2 entities, but did not have the 'relationship' entity?
Assuming the following entities...
Model classes:
public class DisplayGroup
{
[Key]
public int GroupId { get; set; }
public string Description { get; set; }
public string Name { get; set; }
public ICollection<LookUpGroupItem> LookUpGroupItems { get; set; }
}
public class DisplayItem
{
[Key]
public int ItemId { get; set; }
public string Description { get; set; }
public string FileType { get; set; }
public string FileName { get; set; }
public ICollection<LookUpGroupItem> LookUpGroupItems { get; set; }
}
public class LookUpGroupItem
{
public int ItemId { get; set; }
public DisplayItem DisplayItem { get; set; }
public int GroupId { get; set; }
public DisplayGroup DisplayGroup { get; set; }
}
Here is the code for deleting a relationship. Note: I do not want to delete the entities, they just no longer share a relationship.
public void RemoveLink(DisplayGroup g, DisplayItem d)
{
_dataContext.Remove(g.LookUpGroupItems.Single(x => x.ItemId == d.ItemId));
}
The method above causes an error:
System.ArgumentNullException occurred
Message=Value cannot be null.
It looks like this is the case because LookUpGroupItems is null, but these were called from the database. I would agree that I do not want to load all entity relationship objects whenever I do a Get from the database, but then, what is the most efficient way to do this?
Additional NOTE: this question is not about an argument null exception. It explicitly states how to delete a relationship in Entity Framework Core.
The following is not the most efficient, but is the most reliable way:
public void RemoveLink(DisplayGroup g, DisplayItem d)
{
var link = _dataContext.Find<LookUpGroupItem>(g.GroupId, d.ItemId); // or (d.ItemId, g.GroupId) depending of how the composite PK is defined
if (link != null)
_dataContext.Remove(link);
}
It's simple and straightforward. Find method is used to locate the entity in the local cache or load it the from the database. If found, the Remove method is used to mark it for deletion (which will be applied when you call SaveChanges).
It's not the most efficient because of the database roundtrip when the entity is not contained in the local cache.
The most efficient is to use "stub" entity (with only FK properties populated):
var link = new LookUpGroupItem { GroupId = g.GroupId, ItemId = d.ItemId };
_dataContext.Remove(link);
This will only issue DELETE SQL command when ApplyChanges is called. However it has the following drawbacks:
(1) If _dataContext already contains (is tracking) a LookUpGroupItem entity with the same PK, the Remove call will throw InvalidOperationException saying something like "The instance of entity type 'LookUpGroupItem' cannot be tracked because another instance with the key value 'GroupId:1, ItemId:1' is already being tracked. When attaching existing entities, ensure that only one entity instance with a given key value is attached."
(2) If database table does not contain a record with the specified composite PK, the SaveChanges will throw DbUpdateConcurrencyException saying "Database operation expected to affect 1 row(s) but actually affected 0 row(s). Data may have been modified or deleted since entities were loaded. See http://go.microsoft.com/fwlink/?LinkId=527962 for information on understanding and handling optimistic concurrency exceptions." (this behavior is actually considered a bug by many people including me, but this is how it is).
Shorty, you can use the optimized method only if you use short lived newly create DbContext just for that operation and you are absolutely sure the record with such PK exists in the database. In all other cases (and in general) you should use the first method.
I have a class Ricevuta which holds a collection of VoceRicevuta:
public partial class Ricevuta : GestPreBaseBusinessObject
{
[Key, DatabaseGenerated(DatabaseGeneratedOption.None)]
public long Id { get; set; }
...
[InverseProperty("Ricevuta")]
public virtual ObservableListSource<VoceRicevuta> Voci { get; set; }
}
A VoceRicevuta holds optional references to different classes, let's consider only the one to Prestazione class:
public partial class VoceRicevuta
{
[Key, DatabaseGenerated(DatabaseGeneratedOption.None)]
public long Id { get; set; }
public long IdRicevuta { get; set; }
....
public virtual Prestazione Prestazione { get; set; }
[ForeignKey("IdRicevuta")]
public virtual Ricevuta Ricevuta { get; set; }
}
Ok, the problem now is the following: I create an Instance of Prestazione (let's call it PreInst) and save it to the database. I detach PreInst from that DbContext instance.
Then, I pass PreInst to another form (another DbContext - I must do that), create a Ricevuta, a VoceRicevuta (added to the just created Ricevuta) and assign PreInst to VoceRicevuta Prestazione property. I also made some changes to PreInst.
Now I want to save to database the new Ricevuta, the new VoceRicevuta, its relation to PreInst and the changes to PreInst.
I run the following:
db.UpdateGraph(Ricevuta, map =>
map.OwnedCollection(ric => ric.Voci,
with => with
.OwnedEntity(voce => voce.Prestazione)
));
But i get the error:
"Violation of PRIMARY KEY constraint 'PK_dbo.Prestazioni'. Cannot insert duplicate key in object 'dbo.Prestazioni'. Vlue of duplicate key: (12115)"
I can't understand why! Isn't Graph diff purpose cheching if any part of the graph is already present in Db and behave consquently?
I tried to attach PreInst to DbContext before running the code above and saving. Now the error thrown is:
Multiplicity constraint violated. The role 'Prestazione_VoceRicevuta_Source' of the relationship 'Gestione_Prestazioni.Prestazione_VoceRicevuta' has multiplicity 1 or 0..1.
Any hint?
I just wan't to know if it's possible to assign a customized primary key only when the ObjectContext.SaveChanges() is called?
I'm trying to search for any solutions but it seems like I'am the only one thinking about this.
It is the same as what is happening when you have an identity column which the EF will automatically assign a primary key once the ObjectContext.SaveChanges() is called but instead of the primary key being an auto identity column, I want to customize my own primary key.
Thanks in advance guys.
Edit: Additional Details
example I have this class:
public class Transaction()
{
public string ID { get; set; }
public DateTime TransactionDate { get; set; }
}
I wan't to add the class in the objectset like this:
Transaction trans = new Transaction()
{
ID = null,
TransactionDate = DateTime.Now
};
ObjectSet.AddObject(trans);
Notice that I haven't assign the ID yet, I want it to be assigned only when the user click save which will call the
ObjectContext.SaveChanges();
One the user call this, I will get a new primary key and assign it to the trans instance.
Not ideal but consider changing your model to this. Your POCO now contains some logic which is not good but it allows you to customize the save to create your id from the third party class library. The string ID column will resolve to a primary key of type nvarchar(128) by default. Your ID will be null prior to the save.
public abstract class Entity
{
public virtual void OnSave();
}
public class Transaction : Entity
{
public string ID { get; set; }
public DateTime TransactionDate { get; set; }
public override void OnSave()
{
ID = Guid.NewGuid().ToString(); //call to class library
}
}
You can hook into the SaveChanges method by overriding it in your DbContext
public class MyContext : DbContext
{
public DbSet<Transaction> Trans { get; set; }
public MyContext () : base()
{
}
public override int SaveChanges()
{
var changedEntities = ChangeTracker.Entries();
foreach (var changedEntity in changedEntities)
{
var entity = (Entity)changedEntity.Entity;
entity.OnSave();
}
return base.SaveChanges();
}
}
I need to insert an entity (Picture) that holds a related entity (Ad) based on TPH architecture:
Picture model:
public class Picture
{
// Primary properties
public int Id { get; set; }
public string Name { get; set; }
}
public class AdPicture : Picture
{
public Ad Ad { get; set; }
}
Ad model:
public class Ad
{
// Primary properties
public int Id { get; set; }
public string Title { get; set; }
}
public class AdCar : Ad
{
public int? CubicCapacity { get; set; }
public int? Power { get; set; }
}
I want to insert a new Picture in the AdCar, and I tried:
AdPicture picture = new AdPicture()
{
Ad = _adRepository.GetById(adId),
Filename = newFileName
};
_pictureService.CreateAdPicture(picture);
CreateAdPicture:
public void CreateAdPicture(AdPicture adPicture)
{
_adPictureRepository.Add(adPicture);
_adPictureRepository.Save();
}
But Entity Framework says
*Invalid column name 'AdCar_Id'.*
When I check the SQL command text, I can see
insert [dbo].[Pictures]([Name], [Filename], [URL], [Rank], [Discriminator], [PictureType_Id], [AdCar_Id], [Ad_Id])
values (null, #0, null, #1, #2, #3, #4, null)
It's putting AdCar_Id and Ad_Id, why? How can I insert the Picture related with the AdCar?
First up: You may need to define the key for the subclass explicitly to be Ad_Id as it is assuming the AdCar_Id (or vice-versa).
Something like this in your DbContext:
modelBuilder.Entity<AdPicture>()
.HasRequired(o => o.Ad)
.WithMany().Map(f => f.MapKey("Ad_Id"));
Second: Also, just to check - all of these classes have completely separate tables (TPH) and not tables with just the additional properties specified (TPH) I.e. an AdPicture does not have data in both a Picture and AdPicture table?
Having not done TPH (only TPT) I am unsure of if Entity Framework handles the subclass-ing differently, but I suspect it must. Someone else can hopefully answer that better if you are missing something there.
I found the problem:
I was defining the Navigation property "public IList Pictures { get; set; }" in the Ad Model that was creating an aditional field in the SQL Command "Ad_Id".
As soon as I removed it, the problem was solved and all the theory I had about TPH came togheter again :)
Thanks for your feedback.