I have a problem I am using lazy loading and virtual, but when using this it generates the following error
Violation of PRIMARY KEY constraint Cannot insert duplicate key in object
How can I solve it?
These are my classes:
public class Producto
{
[Key]
public Guid ProductoId { get; set; }
public Guid InquilinoId { get; set; }
public string Nombre { get; set; }
public decimal Precio_Publico { get; set; }
public string Detalle_producto { get; set; }
public DateTime? Fecha_publicacion { get; set; }
public bool Activo { get; set; }
public string Img_Producto { get; set; }
public string CodigoBarras { get; set; }
public virtual Concepto VigenciaPrecio { get; set; }
public virtual ICollection<Precio> Precios { get; set; }
public bool Es_Almacenable { get; set; }
public int Dias_de_Garantia { get; set; }
public bool Es_Importado { get; set; }
public virtual List<Categoria> Categoria { get; set; } = new List<Categoria>();
public virtual Impuesto Impuesto { get; set; }
public virtual Precio Precio { get; set; }
}
public class Categoria
{
public Guid CategoriaId { get; set; }
public string Nombre { get; set; }
public virtual Producto Producto { get; set; }
}
[HttpPost]
public async Task<ActionResult<Guid>> Post(Producto producto)
{
var user = await userManager.GetUserAsync(HttpContext.User);
var usercontodo = context.Users.Where(x => x.Id == user.Id).Include(x => x.InquilinoActual).FirstOrDefault();
if (!string.IsNullOrWhiteSpace(producto.Img_Producto))
{
var imgProducto = Convert.FromBase64String(producto.Img_Producto);
producto.Img_Producto = await almacenadorDeArchivos.GuardarArchivo(imgProducto, "jpg", "productos");
}
producto.InquilinoId = usercontodo.InquilinoActual.ClienteId;
context.Add(producto);
await context.SaveChangesAsync();
return producto.ProductoId;
}
This is the table categorias:
This is the table productos:
This is the error:
Error:
Microsoft.EntityFrameworkCore.DbUpdateException: An error occurred while updating the entries. See the inner exception for details.
Microsoft.Data.SqlClient.SqlException (0x80131904): Violation of
PRIMARY KEY constraint 'PK_Categorias'. Cannot insert duplicate key in
object 'dbo.Categorias'. The duplicate key value is
(1737b24b-93a4-4ad9-4d8b-08d85a75ca8e)
This error is usually the result of passing detached entity graphs, or entity graphs that are constructed in a client to a server to be added/updated.
Producto has a set of categories. If EF is trying persist a category, this means that the Producto you sent it and added to the context had at least one Category in it.
Lets say for example in my client code (JavaScript or some other server code calling the API) I create a new Producto:
// C# Pseudo
var producto = new Producto
{
// set up product fields...
// I have a list of categories pre-loaded, so I want to assign one of them...
Categorias = (new [] { new Categoria { CategoriaId = 3, Nombre = "Three" }}).ToList()
}
Now I pass that Producto to my service. That request has its own scoped DbContext that I set up some info on the Producto and Add it to the context's Producto DbSet. We can assume that the database already has a Category with an ID of 3, but the context isn't aware of it because Producto.Categorias has a reference to a new entity that just happens to have the ID of 3. EF will treat that Category as a new entity and try to insert it along-side the Product. Hence, FK violation as EF tries to insert another Category ID=3.
The complex solution is to attach entities to the current DbContext. As a simple example, with the above Producto coming in:
foreach(var categoria in producto.Categorias)
context.Attach(categoria); // which will treat the category as unmodified, but expect it to be an existing record.
context.Producto.Add(producto);
context.SaveChanges();
This would have to be done for every existing entity associated with and referenced by the Product. This gets complicated because it assumes that each associated entity is unknown by the DbContext. If you have a scenario where you are dealing with multiple Producto objects, or the Category could be referenced by the Producto and another new row under the Producto, or it's possible that the categories could have been read by the DbContext prior to saving the Producto, attempting to Attach the category could fail if EF is already tracking one with the same ID. This can lead to intermittent errors.
To be safer, before attaching an entity, you should test that the Context isn't already tracking it. If it is already tracking a reference, then you need to replace the reference in Producto:
foreach(var categoria in producto.Categorias)
{
var existingCategoria = context.Categorias.Local.SingleOrDefault(x => x.CategoriaId == categoria.CategoriaId);
if (existingCategoria != null)
{ // Context is tracking one already, so replace the reference in Producto
product.Categorias.Remove(categoria);
product.Categorias.Add(existingCategoria);
}
else
context.Attach(categoria); // context isn't tracking it yet.
context.Producto.Add(producto);
context.SaveChanges();
Again, that needs to be done for EVERY reference to safely save a detached entity.
The better solution is to avoid passing entity structures between client and server, and instead pass View Models or DTOs which are POCO (Plain Old C# Objects) containing just the details needed to build an entity.
Given a Producto ViewModel like this:
[Serializable]
public class NewProductoViewModel
{
public string Nombre { get; set; }
public ICollection<Guid> CategoriaIds { get; set; } = new List<Guid>();
// ... Other details needed to create a new Producto
}
When we go to add this new Producto:
[HttpPost]
public async Task<ActionResult<Guid>> Post(NewProductoViewModel viewModel)
{
var user = await userManager.GetUserAsync(HttpContext.User);
var usercontodo = context.Users.Where(x => x.Id == user.Id).Include(x => x.InquilinoActual).FirstOrDefault();
var producto = new Producto(); // Here is our new, fresh entity..
// TODO: Here we would copy across all non-reference data, strings, values, etc. from the ViewModel to the Entity...
if (!string.IsNullOrWhiteSpace(viewModle.Img_Producto))
{
var imgProducto = Convert.FromBase64String(viewModel.Img_Producto);
producto.Img_Producto = await almacenadorDeArchivos.GuardarArchivo(imgProducto, "jpg", "productos"); // This is fine, we get our object from the DbContext.
}
producto.InquilinoId = usercontodo.InquilinoActual.ClienteId;
// Now handle the Categorias....
foreach(var categoriaId in viewModel.CategoriaIds)
{
var categoria = context.Categorias.Single(categoriaId);
producto.Categorias.Add(categoria)
}
context.Add(producto);
await context.SaveChangesAsync();
return producto.ProductoId;
}
Much of your code is pretty much left as-is, but what it accepts is a deserialized view model, not to be confused with a deserialized block of data that can be confused with an entity. The method constructs a new Entity, then would copy across any details from the view model, before performing a lookup against the Context for any references to associate to the new entity. In the above example I use .Single() which would throw an exception if we passed a CategoriaId that didn't exist. Alternatively you could use .SingleOrDefault() and ignore CategoriaIds that don't exist.
The added benefit of using ViewModels is that you can minimize the amount of data being sent to just the fields needed. We don't send entire Categoria classes to the server, just the IDs that were associated. Most cases where I've seen people passing entities around, the reason is to avoid re-loading the entities more than once. (once when the categorias were read to send to the client, and again when the Producto is saved) This rationale is flawed because what gets sent back to the server may "look" like the entity that the server would have sent the client, but it is not an entity. It is a deserialized block of JSON with the same signature of an entity. It is also "stale" in the sense that any data sent to the server may be many minutes old. When updating entities, the first thing you should check is whether the row version of the data coming from the client matches what is in the server. (Has the server data been updated since?) This means touching the database anyways. We also shouldn't trust anything coming from the server. It is tempting to Attach entities, set a Modified State and call SaveChanges but this will overwrite every field on that entity. Clever people can intercept requests from their browsers and modify the data that is serialized into the request which means that data you don't intend to allow to be updated can be replaced if you merely attach that entity.
Your classes mean that, using the standard conventions, a Category can only belong to 1 Product, and a Product has many Categories.
You didn't include the code that builds up a Product (before it is Posted) but the error means that you try to add an existing Category to a new Product.
I think you want to use Categoria as a link table, this should more or less work:
public class Categoria
{
[Key]
public Guid CategoriaId { get; set; }
[Key]
public Guid ProductoId { get; set; }
public string Nombre { get; set; }
public virtual Producto Producto { get; set; }
}
but you get more control by mapping it in OnModelCreating of the DbContext class.
Related
I am using Entity Framework 6 Code First. I have two Entities:
public class User
{
[Key]
public Guid Id { get; set; }
public string Name { get; set; }
public string Mail { get; set; }
public DateTime PwLastSet { get; set; }
public virtual ICollection<Group> Groups { get; set; }
}
and
public class Group
{
[Key]
public Guid Id { get; set; }
public string Name { get; set; }
public string Description{ get; set; }
public int GroupType { get; set; }
public virtual ICollection<User> Members { get; set; }
}
in a many-to-many relationship and a joining entity GroupUsers which is generated automatically by EFCore and only includes the two Key properties from the original entities which make the primary key as well as foreign keys from the two base Entities. All this is fine and the database objects including the joining table have been created in the migration with no issues.
The problem starts when trying to Insert data into either of the two tables using EFCore.
private async Task SynchronizeUsersAsync()
{
var localUsers = await _userRepo.ListAllAsync();
List<User> remoteUsers = _usersHelper.GetUsers();
var toAdd = new List<User>();
foreach (var remoteUser in remoteUsers)
{
var localUser = localUsers.FirstOrDefault(x => x.Id.Equals(remoteUser.Id));
if (localUser == null)
{
toAdd.Add(remoteUser);
}
else if (!localUser.Equals(remoteUser))
{
_mapper.Map(remoteUser, localUser);
}
}
await DbContext.Set<User>().AddRangeAsync(toAdd);
await DbContext.SaveChangesAsync();
//delete is irrelevant at the moment
}
I get an exception:
System.InvalidOperationException: The instance of entity type 'Group' cannot be tracked because another instance with the same key value for {'Id'} is already being tracked. When attaching existing entities, ensure that only one entity instance with a given key value is attached.
As every User potentially belongs to multiple Groups this is also supposed to insert a new row into the joining table GroupUsers for every object in ICollection<Group> Groups of a User.
I have also tried replacing .AddRangeAsync() with .AddAsync() to Insert on every new User which ended up with the same exception.
The problem occurs because of _mapper.Map. When it copies the Groups collection, it also copies every Group inside of it instead of using the same reference. This way you add different group objects to the DbContext, but they have the same Id.
The solution would be to create a mapper configuration for Groups field to make a shallow copy instead of a deep copy. In other words copy the reference to the collection (or create a new collection, but copy the references of the objects without copying them). You should probably do the same for Users field in the Group class.
Fairly new to EF.Core and I'm having some issues as my tables start getting more complex. Here's an example of what I have defined for my classes. Note ... there are many more columns and tables than what I have defined below. I've paired them down for brevity.
public class User
{
public int Id { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
public bool Active { get; set; }
}
Followed by
public class JournalEntry
{
public int Id { get; set; }
public int UserId { get; set; }
public string Details { get; set; }
public DateTime DateEntered { get; set; }
public virtual User User { get; set; }
}
I want to be able to issue the following query and INCLUDE the User Table so that I can then populate a ViewModel with columns from the User Table without having to do another lookup and also to sort the data while retrieving it:
public IQueryable<JournalEntry> GetByUser(int userId)
{
return _DbContext.JournalEntries.Where(j => j.UserId == userId)
.Include(u => u.User)
.OrderBy(u=> u.User.FirstName)
.ThenBy(j => j.DateEntered);
}
My controller would then have something similar to the following:
public IActionResult List(int userId)
{
var journalEntries = new _dbRepository.GetByUser(userId);
var myViewModel = new MyViewModel();
myViewModel.UserName = ($"{journalEntries.User.FirstName} {journalEntries.User.LastName}");
myViewModel.Entries = journalEntries;
etc ....
return View(myViewModel);
}
I'm loading the user's first and last name in the View Model and whatever other attributes from the various tables that are referenced. The problem that I'm having is that I'm getting errors on the Migration creation "Foreign key constraint may cause cycle or multiple cascade paths." And of course, if I remove the line reading public virtual User User { get; set; } from the JournalEntry class then the problem goes away (as one would expect).
I believe that the way I'm doing the models is incorrect. What would be the recommended way that I should code these models? I've heard of "lazy loading". Is that what I should be moving towards?
Thanks a bunch.
--- Val
Your query returns an IQueryable<JournalEntry> not a JournalEntry.
Change the code to get the user details from the first object:
var myViewModel.UserName = ($"{journalEntries.First().User.FirstName} {journalEntries.First().User.LastName}");
In the line above I'm calling First() on your journal entries collection and that would have a User. Then I can access FirstName and LastName.
Also, don't bother with LazyLoading since you are learning. It could cause select n+1 issues if used incorrectly
In Entity Framework, I have a class Order:
public class Order
{
public Order()
{
this.Info = new HashSet<OrderInfo>();
}
[Required]
public int Id { get; set; }
[NotNull, MapTo("RecordId")]
public virtual ICollection<OrderInfo> Info { get; set; }
}
with a one-to-many relationship to the class OrderInfo:
public class OrderInfo
{
public OrderInfo() { }
public int RecordId { get; set; }
}
In my OrderInfo table, RecordId is the foreign key that corresponds to the Order's Id column (Order's primary key).
I am creating an Order object, then creating an OrderInfo that will map to the order. Like so:
using (var context = new MyDbContextClass())
{
// method that creates an Order, adds it to the context's change tracker
// and returns the order's Id
var orderId = await CreateOrder();
// method that creates an OrderInfo with orderId as its RecordId
// and adds it to the context's change tracker
await CreateOrderInfo(orderId, "Order info contents");
// calls DbContext.SaveChanges()
await context.Commit();
var order = context.Order.Find(orderId);
var associatedOrderInfo = order.Info;
var queriedOrderInfo = context.OrderInfo.Where(info => info.RecordId == orderId);
}
associatedOrderInfo is always empty, and queriedOrderInfo always has the expected values. Also, if I then open a new context and retrieve the order by Id again, its Info contains the expected values.
I have confirmed the following:
context.Configuration.ProxyCreationEnabled is true (verified with debugger)
context.Configuration.LazyLoadingEnabled is true (verified with debugger)
The navigation property is declared public virtual
The type referenced in the navigation property has a public, parameterless constructor.
From my understanding of lazy loading, it queries the database for OrderInfo objects who have the order's Id as their RecordId. At this point, the data has been committed and my isolation level is read-committed. Therefore, the order should give me related OrderInfo objects the first time I ask, and not make me open a new context to find them. Why does it come back empty?
Edit:
I have also attempted to force lazy initialization of the navigation properties as in this question. Here is my order class:
public class Order
{
public Order()
{
}
[Required]
public int Id { get; set; }
private ICollection<OrderInfo> _Info;
[NotNull, MapTo("RecordId")]
public virtual ICollection<OrderInfo> Info
{
get
{
return this._Info ?? (this._Info= new HashSet<OrderInfo>());
}
set
{
this._Info = value;
}
}
}
Even with this implementation of order, it comes back empty.
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.
When using Entity Framework 6, how is the most efficient way to create an object or objects with additional data from other DbSet entities, when I have a DbContext or IQueryable<T>?
Here is some code:
If I have an Data class as follows:
public class Data
{
[Key]
public int id { get; set; }
public string name { get; set; }
public string data { get; set; }
public int parentId { get; set; }
public int otherDataId { get; set; }
}
And an OtherData class as follows:
public class OtherData
{
[Key]
public int id { get; set; }
public string name { get; set; }
public string data { get; set; }
}
In the Data class, the parentId is a foreign key reference to another Data object in the same DbSet, and the otherDataId is a foreign key reference to an OtherData object in a DbSet<OtherData>.
I would like to get all Data objects in the DbSet<Data>, with the additional DbSet data of the parent Data objects id and name and the OtherData object's id and name. I need this to be in one object to be sent from a webservice GET.
I am not sure on how to do this.
Do I need some code along the lines of:
var result = DbContext.Data.Select(x=> x...).Join(y=> y...) .. new { id = x.id... y.name.. }
Can I please have some help with this code?
You can use a join and project the result. In the below snippet CombinedData is a another class with 2 string fields Name and OtherName. You can also use a view but I think the Join is less work.
IQueryable<CombinedData> result = DbContext.Data.Join(
DbContext.Data.DbContext.OtherData,
outer => outer.OtherDataId,
inner => inner.Id),
(outer, inner) => new { Name = outer.Name, OtherName = inner.Name}
);
Depending on your overall architecture, this may be a good or a bad answer but often when faced with this in the past our teams would create a view in the database to combine the fields. Write your optimized query in the view and then treat it like any other table in your data layer.
You could accomplish the same end result using includes or joins or even writing out the expression in a cross-table query but in my opinion the view is the cleanest and most efficient method.