I am using Entity Framework (with a code-first approach) and the database is created successfully with the expected foreign keys and unique key constraints.
I have those two model classes:
public class Foo
{
[Key, DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public Guid Id { get; set; }
public Bar Bar { get; set; }
}
public class Bar
{
[Key, DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public Guid Id { get; set; }
[Index(IsUnique = true), StringLength(512)]
public string Url { get; set; }
}
And this application code:
var foo = GetData();
using (DatabaseContext db = new DatabaseContext())
{
db.Entry(foo).State = EntityState.Added;
if (foo.Bar != null)
{
var bar = await db.Bar.FirstOrDefaultAsync(x => x.Url == foo.Bar.Url);
if (bar != null)
{
// When I assign an existing entity ...
foo.Bar = bar;
}
}
// ... following exception will be thrown.
await db.SaveChangesAsync();
}
SqlException: Cannot insert duplicate key row in object 'dbo.Bar' with unique index 'IX_Url'. The duplicate key value is (https://api.example.com/rest/v1.0/bar/FD6FAB72-DCE2-47DB-885A-424F3D4A70B6).
The statement has been terminated.
I don't understand why Entity Framework is trying to add the navigation property Bar, even after obtaining and assigning it from the same DbContext. Similar StackOverflow questions haven't provided any working solution yet.
Please tell me if I need to provide additional information.
Did I forget to set any EF related configuration or something like that? Thank you in advance!
It's because
db.Entry(foo).State = EntityState.Added;
marks the foo.Bar (and any referenced entity not tracked by the context) as Added as well.
You should resolve the referenced entities before adding the Foo entity:
var foo = GetData();
using (DatabaseContext db = new DatabaseContext())
{
// Resolve references
if (foo.Bar != null)
{
var bar = await db.Bar.FirstOrDefaultAsync(x => x.Url == foo.Bar.Url);
if (bar != null)
foo.Bar = bar;
}
// Then add the entity
db.Entry(foo).State = EntityState.Added;
await db.SaveChangesAsync();
}
Related
The minimal project sources to reproduce the issue is here :
https://wetransfer.com/downloads/8d9325ce7117bb362bf0d61fc7c8571a20220708100401/326add
===================
This error is a classic; In layman's terms it is usually caused by a "bad" insertion when a navigation is not properly taken in account, causing a faulty Ef state somewhere.
Many solutions have been posted along the years but I fail to see how my specific scenario could cause the issue!
My schema is a many-to-many between Groups and Users. The middle entity is named GroupUser.
There's a twist : Each GroupUser has an owned entity containing extra data, DataPayload. This choice was made for versatility -- we wanted that payload to be stored in its own table.
Schema:
public class User {
public Guid Id { get; private set; }
public IList<GroupUser> GroupUsers { get; private set; } = new List<GroupUser>();
public User(Guid id) { Id = id; }
}
public class Group {
public Guid Id { get; private set; }
public Group(Guid id) { Id = id; }
}
public class GroupUser {
public Guid Id { get; private set; }
public Guid GroupId { get; private set; }
public Guid UserId { get; private set; }
// Navigation link
public Group? Group { get; private set; }
public DataPayload? Data { get; private set; }
public GroupUser(Guid groupId, Guid userId, DataPayload data) {
Id = Guid.NewGuid(); //Auto generated
UserId = userId;
GroupId = groupId;
Data = data;
}
// This extra constructor is only there to make EF happy! We do not use it.
public GroupUser(Guid id, Guid groupId, Guid userId) {
Id = id;
UserId = userId;
GroupId = groupId;
}
}
public class DataPayload {
//Note how we did not defined an explicit Id; we let EF do it as part of the "Owned entity" mechanism.
///// <summary>foreign Key to the Owner</summary>
public Guid GroupUserId { get; private set; }
public int DataValue { get; private set; }
public DataPayload(int dataValue) {
DataValue = dataValue;
}
public void SetDataValue(int dataValue) {
DataValue = dataValue;
}
}
To make it all work, we configure the navigations like this :
// --------- Users ---------
builder
.ToTable("Users")
.HasKey(u => u.Id);
// --------- Groups ---------
builder
.ToTable("Groups")
.HasKey(g => g.Id);
// --------- GroupUsers ---------
builder
.ToTable("GroupUsers")
.HasKey(gu => gu.Id);
builder
.HasOne<User>() //No navigation needed
.WithMany(u => u.GroupUsers)
.HasForeignKey(gu => gu.UserId);
builder
.HasOne<Group>(gu => gu.Group) //Here, we did define a navigation
.WithMany()
.HasForeignKey(gu => gu.GroupId);
builder
.OwnsOne(gu => gu.Data,
navBuilder => {
navBuilder.ToTable("PayloadDatas");
navBuilder.Property<Guid>("Id"); //Note: Without this EF would try to use 'int'
navBuilder.HasKey("Id");
//Configure an explicit foreign key to the owner. It will make our life easier in our Unit Tests
navBuilder.WithOwner().HasForeignKey(d => d.GroupUserId);
}
);
//.OnDelete(DeleteBehavior.Cascade) // Not needed (default behaviour for an owned entity)
Now, you know how everything is defined.
Basic setup : works!
var group = new Group(groupId);
await dbContext.Groups.AddAsync(group);
await dbContext.SaveChangesAsync();
var user = new User(userId);
await dbContext.Users.AddAsync(user);
await dbContext.SaveChangesAsync();
Follow-up scenario : fails!
var groupUser = new GroupUser(groupId, userId, new DataPayload(dataValue: 777777));
user.GroupUsers.Add(groupUser);
await dbContext.SaveChangesAsync(); // Crash happens here!!!
Error:
Microsoft.EntityFrameworkCore.DbUpdateConcurrencyException : The database operation was expected to affect 1 row(s), but actually affected 0 row(s); data may have been modified or deleted since entities were loaded.
I suspect that EF gets confused by the addition of two entities at once, where it has to compute some Ids itself : the new GroupUser and the DataPayload it contains. I'm not sure how it's supposed to know that it needs to give an Id to the GroupUser first and then use that Id as the foreign key in PayloadData. But that's just me; it might or might not be related to the issue.
But what do I need to change?
The mistake was in GroupUser's id-less constructor:
Id = Guid.NewGuid();
The code needs to let EF manage the keys when it comes to owned entities such as DataPayload which rely on a foreign key (GroupUserId) that's still in the making at the time of saving.
If you set a key value (Guid.NewGuid()) yourself, then EF gets confused between:
linking the new DataPayload entity to the GroupUser entity where you've shoehorned an Id value,
OR
just expecting an empty value (foreign key) and setting all the keys (both the GroupUser's Id and DataPayload's GroupUserId) itself.
All in all, EF feels like you announced that you were about to let it create 1 entity, but you've pulled the rug under its feet and done it yourself, so it ends up with 0 entity to work with. Hence the error message.
It should have been :
Id = Guid.Empty;
With Guid.Empty, EF clearly identifies that this entity is new and that has to be the same one as the one you told it to create and link to the new PayloadData -- that is, the instance that you've set in GroupUser.Data.
I am trying to link Shipments to Road using a clean database, with fresh data unlinked, first time trying to relate these 2 entities.
public class Road
{
[Key]
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public int Id { get; set; }
public ShipmentMode ShipmentMode { get; set; }
public string RoadName { get; set; }
public virtual List<Shipment> Shipments { get; set; }
}
public void SaveToDatabase()
{
using (var db = new DbContext())
{
foreach (var road in this.Roads)
{
road.Shipments.ForEach(shipment => shipment = db.Shipments.FirstOrDefault(s => s.Id == shipment.Id));
var input = db.Roads.Add(road);
}
db.SaveChanges();
}
}
At the line var input = db.Roads.Add(road); it will throw the error Message "The instance of entity type 'Shipment' cannot be tracked because another instance with the key value '{Id: 46}' is already being tracked. When attaching existing entities, ensure that only one entity instance with a given key value is attached.'.
I had this error before, fixed it but I rearranged the code and now it's back to throwing this error. I am just trying to get Shipments to link to Road.
This code below has worked for me. If anyone has a better solution, please feel free to post.
public void SaveToDatabase()
{
using (var db = new DbContext())
{
foreach (var road in this.Roads)
{
var shipments = road.Shipments;
road.Shipments = null;
var input = db.Roads.Add(road);
db.SaveChanges();
var getRoad = db.Lanes.Include(p => p.Shipments).FirstOrDefault(t => t.Id == input.Entity.Id);
getRoad.Shipments.AddRange(shipments);
db.SaveChanges();
}
}
}
Alternative solution
public void SaveToDatabase()
{
using (var db = new DbContext())
{
foreach (var road in this.Roads)
{
db.UpdateRange(road.Shipments);
var input = db.Roads.Add(road);
db.SaveChanges();
}
}
}
I did not specify the DbSet in my applicationdbcontext.
However, I am able to create order payments using the following method:
public List<OrderPaymentDto> Create(CreateOrderPaymentDto createInput)
{
if (createInput == null) return null;
var orderTotalPrice = this.orderRepository.GetSingleAsync(o => o.Id == createInput.OrderId).Await()?.Price;
if (orderTotalPrice == null)
{
throw new NotFoundException($"An order with an id {createInput.OrderId} has not been found! ");
}
var list = new List<OrderPaymentDto>();
if (createInput.OrderPaymentsTemplateGroupId != null && createInput.OrderPaymentsTemplateGroupId != 0)
{
var orderTemplates = this.orderPaymentsTemplateManager.GetAll(op => op.OrderPaymentsTemplateGroupId == createInput.OrderPaymentsTemplateGroupId);
if (orderTemplates == null)
{
throw new NotFoundException("No order templates were found!");
}
//take the order repository total price
foreach (var orderTemplate in orderTemplates)
{
OrderPayment orderPaymentToBeCreated = new OrderPayment
{
Amount = ((orderTotalPrice.Value * orderTemplate.Amount) / 100),
OrderId = createInput.OrderId,
DueDate = DateTime.Now.AddDays(orderTemplate.PaymentPeriod),
PaymentType = orderTemplate.PaymentType,
Name = orderTemplate.Name
};
var addedOrderPayment = this.repository.AddAsync(orderPaymentToBeCreated).Await();
list.Add(mapper.Map<OrderPaymentDto>(addedOrderPayment));
}
}
else
{
OrderPayment orderPaymentToBeCreated = new OrderPayment
{
Amount = createInput.Amount,
OrderId = createInput.OrderId,
DueDate = DateTime.Now.AddDays(createInput.PaymentPeriod),
PaymentType = createInput.PaymentType,
Name = createInput.Name
};
var addedOrderPayment = this.repository.AddAsync(orderPaymentToBeCreated).Await();
list.Add(mapper.Map<OrderPaymentDto>(addedOrderPayment));
}
this.notificationService.OnCreateEntity("OrderPayment", list);
return list;
}
the repository addasync method is this:
public async Task<TEntity> AddAsync(TEntity entity)
{
ObjectCheck.EntityCheck(entity);
await dbContext.Set<TEntity>().AddAsync(entity);
await dbContext.SaveChangesAsync();
return entity;
}
The table itself is created in PostGre, I am able to create entities.
What is the point of including them in the ApplicationDbContext?
The model itself has a reference to Order which has a dbset in the ApplicationDbContext. If entities are related can I just include one db set and not the rest?
My previous understanding of a DBSet is that it is used to have crud operations on the database. Now my understanding is challenged.
Can someone please clarify?
My colleague helped me find an answer.
https://learn.microsoft.com/en-us/aspnet/core/data/ef-mvc/intro?view=aspnetcore-3.1
In this documentation in the section of Creating the DbContext example :
using ContosoUniversity.Models;
using Microsoft.EntityFrameworkCore;
namespace ContosoUniversity.Data
{
public class SchoolContext : DbContext
{
public SchoolContext(DbContextOptions<SchoolContext> options) : base(options)
{
}
public DbSet<Course> Courses { get; set; }
public DbSet<Enrollment> Enrollments { get; set; }
public DbSet<Student> Students { get; set; }
}
}
This code creates a DbSet property for each entity set. In Entity Framework terminology, an entity set typically corresponds to a database table, and an entity corresponds to a row in the table.
You could've omitted the DbSet (Enrollment) and DbSet(Course) statements and it would work the same. The Entity Framework would include them implicitly because the Student entity references the Enrollment entity and the Enrollment entity references the Course entity.
I have Payments and Reviews.
public class Payment {
[Key]
[DatabaseGeneratedAttribute(DatabaseGeneratedOption.Identity)]
public int PaymentId { get; set; }
[ForeignKey("Review")]
public int? ReviewId { get; set; }
public virtual Review Review { get; set; }
// other properties
}
public class Review {
[Key]
public int ReviewId { get; set; }
[ForeignKey("PeerPayment")]
public int? PeerPaymentId { get; set; }
public virtual PeerPayment PeerPayment { get; set; }
}
I've mapped them as follows:
public ReviewConfiguration() {
// One-to-One optional:optional
HasOptional(s => s.Payment).WithMany().HasForeignKey(x => x.PaymentId);
}
public PaymentConfiguration() {
// One-to-One optional:optional
HasOptional(s => s.Review).WithMany().HasForeignKey(s => s.ReviewId);
}
My database FKs are created as nullable, which is great. However the following test fails:
public async Task ReviewPeerPaymentWorks() {
using (var db = new AppContext()) {
var user1 = db.FindUser("M1");
var user2 = db.FindUser("M2");
var review = new Review() {
FromUserId = user1.UserId,
ToUserId = user2.UserId
};
db.Reviews.Add(review);
var payment = new Payment() {
FromUserId = user1.UserId,
ToUserId = user2.UserId,
ReviewId = review.ReviewId
};
db.Payments.Add(payment);
db.SaveChanges();
review = db.Reviews.First(s => s.ReviewId == review.ReviewId);
payment = db.Payments.First(s => s.PaymentId == payment.PaymentId);
Assert.AreEqual(review.PaymentId, payment.PaymentId); // fails - review.PaymentId is always null
Assert.AreEqual(review.ReviewId, payment.ReviewId); // passes
Assert.AreEqual(review.Payment.PaymentId, payment.Review.PaymentId); // fails - review.Payment is null, payment.Review.PaymentId is null
}
}
What am I doing wrong?
To be clear - I don't want to modify my test, unless someone thinks it's an invalid way to test the relationship for some reason.
One-to-One relation is a bit tricky in the EF. Especially in Code-First.
In the DB-First I would make the UNIQUE constraint on the both FK properties to make sure that this relation appear only once per table (but please make sure that it's not One-to-One-or-Zero because UNIQUE constraint will allow you to have only one NULL FK value). In case of the NULLable FK it can be done via the 3rd relation table with the UNIQUE constraint over all columns in relation table.
Here is the URL which explains how EF maps the One-to-One relationship: http://www.entityframeworktutorial.net/entity-relationships.aspx
Here is the URL which explains how to configure One-to-One relationship: http://www.entityframeworktutorial.net/code-first/configure-one-to-one-relationship-in-code-first.aspx
Generally they suggest to use the same PK values for both tables:
"So, we need to configure above entities in such a way that EF creates
Students and StudentAddresses table in the DB where it will make
StudentId in Student table as PK and StudentAddressId column in
StudentAddress table as PK and FK both."
I have tried your example and you did two independent One-to-Many relations between Review and Payment. But without the Many navigation property.
Look WithMany():
.HasOptional(s => s.Review).WithMany().HasForeignKey(s => s.ReviewId);
So it's real One-To-Many relation with no MANY (List) navigation properties. Look at the MSSQL diagram:
And the reason why did you get the NULL values - because you have assigned Payment.ReviewId relation and didn't assign the Review.PaymentId relation. But you expected EF did it automatically. Since they are separate relations - EF left one of the as NULL because you didn't assign it
public async Task ReviewPeerPaymentWorks() {
using (var db = new AppContext()) {
var user1 = db.FindUser("M1");
var user2 = db.FindUser("M2");
var review = new Review() {
FromUserId = user1.UserId,
ToUserId = user2.UserId
};
db.Reviews.Add(review);
var payment = new Payment() {
FromUserId = user1.UserId,
ToUserId = user2.UserId,
ReviewId = review.ReviewId
};
db.Payments.Add(payment);
db.SaveChanges();
review.Payment = payment;
db.SaveChanges();
review = db.Reviews.First(s => s.ReviewId == review.ReviewId);
payment = db.Payments.First(s => s.PaymentId == payment.PaymentId);
Assert.AreEqual(review.PaymentId, payment.PaymentId); // fails - review.PaymentId is always null
Assert.AreEqual(review.ReviewId, payment.ReviewId); // passes
Assert.AreEqual(review.Payment.PaymentId, payment.Review.PaymentId); // fails - review.Payment is null, payment.Review.PaymentId is null
}
}
I have faced this issue two times. Each time I have removed those two entities from Edmx and re-added. It solves my issue.
Hopefully it will work with you
I am using the Code First approach with my project.
Here is my object:
public class Account
{
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public Guid ID { get; set; }
public string name { get;set; }
}
If I create a new record it is OK:
using(var context = new context())
{
context.Accounts.add(account);
context.savechanges(); //This saves fine
}
But when I change a property it saves another record in the database:
using (var context = new context())
{
var account = context.Account.where(x => x.ID == GUID).FirstOrDefault();
if (account != null)
{
account.name = "UpdatedName";
context.savechanges(); // This creates a new record!!
}
}
I am fairly new to Entity framework; why is it creating new records each time? Is it the attribute on the ID? If it is, then how can I use GUIDS as IDs instead of integers?
The attribute in your Account option should work fine to set up the ID column as the primary key for your objects.
If you are getting a new entry added to the database when you save changes, it is almost certainly the result of you having changed the primary key (ID property) of the object after having received it from the DB. Maybe you are trying to set the GUID property yourself in some piece of code that you haven't included? (You should be letting the DB assign it).
In any case, this simple console app uses your setup and works as expected. If you don't see an obvious place in your code where you are changing the GUID, maybe you can post your actual code? (I notice a couple of typos in what is pasted in, so it doesn't appear to be what you are actually using)
public class Account
{
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public Guid ID { get; set; }
public string name { get; set; }
}
public class MyContext : DbContext
{
public DbSet<Account> Accounts { get; set; }
}
class Program
{
static Guid MyGuid = Guid.Empty;
static void Main(string[] args)
{
using (var context = new MyContext())
{
Account account = new Account { name = "OldName" };
context.Accounts.Add( account );
context.SaveChanges();
MyGuid = account.ID;
}
using (var context = new MyContext())
{
var account = context.Accounts.Where(x => x.ID == MyGuid).FirstOrDefault();
if (account != null)
{
account.name = "UpdatedName";
context.SaveChanges();
}
}
}
}