I have the following classes (I am only showing the properties that matter):
public class TypeLocation
{
[Key]
public int Id {get; set;}
public string Country {get; set;}
public string State {get; set;}
public string County {get; set;}
public string City {get; set;}
public List<Offer> Offers {get; set; }
public TypeLocation()
{
this.Offers = new List<Offer>();
}
}
public class Offer
{
public int Id { get; set; }
public ICollection<TypeLocation> LocationsToPublish { get; set; }
}
This creates the following in the database:
My Question/Problem
The TypeLocations table is prepopulated with a static list of country/state/county/city records, one or many of them can be associated with the Offer in LocationsToPublish property. When I try to add a location, using the following code, Entity Framework adds a NEW record in the TypeLocation table and then makes the association by adding a record in the OfferTypeLocations table.
public static bool AddPublishLocation(int id, List<TypeLocation> locations)
{
try
{
using (AppDbContext db = new AppDbContext())
{
Offer Offer = db.Offers
.Include("LocationsToPublish")
.Where(u => u.Id == id)
.FirstOrDefault<Offer>();
//Add new locations
foreach (TypeLocation loc in locations)
{
Offer.LocationsToPublish.Add(loc);
}
db.SaveChanges();
}
return true;
}
catch
{
return false;
}
}
I don't want a new record added to the TypeLocations table, just a relational record creating an association in the OfferTypeLocations table. Any thoughts on what I am doing wrong?
Solution
Thanks to #Mick who answered below, I have found the solution.
public static bool AddPublishLocation(int id, List<TypeLocation> locations)
{
try
{
using (AppDbContext db = new AppDbContext())
{
Offer Offer = db.Offers
.Include("LocationsToPublish")
.Where(u => u.Id == id)
.FirstOrDefault<Offer>();
//Add new locations
foreach (TypeLocation loc in locations)
{
//SOLUTION
TypeLocation ExistingLoc = db.AppLocations.Where(l => l.Id == loc.Id).FirstOrDefault<TypeLocation>();
Offer.LocationsToPublish.Add(loc);
}
db.SaveChanges();
}
return true;
}
catch
{
return false;
}
}
What happens is, using the existing AppDbContext, I retrieve an existing record from the TypeLocations table (identified here as AppLocations) and then Add it to the LocationsToPublish entity.
The key is that I was to use the current AppDbContext (wrapped with the Using() statement) for all the work. Any data outside of this context is purely informational and is used to assist in the record lookups or creation that happen within the AppDbContext context. I hope that makes sense.
The TypeLocations are being loaded from a different AppDbContext, they are deemed new entities in the AppDbContext you're constructing within your method. To fix either:-
Assuming the locations were detatched them from the instance of the AppDbContext outside of your method, you can attach these entities to the new context.
OR
Pass in the AppDbContext used to load the locations into your AddPublishLocation method.
I'd choose 2:-
public static bool AddPublishLocation(AppDbContext db, int id, List<TypeLocation> locations)
{
try
{
Offer Offer = db.Offers
.Include("LocationsToPublish")
.Where(u => u.Id == id)
.FirstOrDefault<Offer>();
//Add new locations
foreach (TypeLocation loc in locations)
{
Offer.LocationsToPublish.Add(loc);
}
db.SaveChanges();
return true;
}
catch
{
return false;
}
}
Related
I was doing a audit log setup on dbcontext.cs and my next step is to call the primary key values of join table in order log the parent ID in the audit log table.
And now I am working on the calling property of a table called "TAXONOMY" which is a join table of "CONSERVATIONSTATUS" with many to many relationship called "CONSERVATIONSTATUSLINK".
Here is the structure of table generated in edmx:
public partial class CONSERVATIONSTATU
{
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Usage", "CA2214:DoNotCallOverridableMethodsInConstructors")]
public CONSERVATIONSTATU()
{
this.TAXONOMies = new HashSet<TAXONOMY>();
}
public int SPECIESCONSERVATIONID { get; set; }
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Usage", "CA2227:CollectionPropertiesShouldBeReadOnly")]
public virtual ICollection<TAXONOMY> TAXONOMies { get; set; }
}
public partial class TAXONOMY
{
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Usage", "CA2214:DoNotCallOverridableMethodsInConstructors")]
public TAXONOMY()
{
this.CONSERVATIONSTATUS = new HashSet<CONSERVATIONSTATU>();
}
public int TAXONID { get; set; }
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Usage", "CA2227:CollectionPropertiesShouldBeReadOnly")]
public virtual ICollection<CONSERVATIONSTATU> CONSERVATIONSTATUS { get; set; }
}
}
When it post and go through save changes through dbcontext.cs
// This is overridden to prevent someone from calling SaveChanges without specifying the user making the change
public override int SaveChanges()
{
// Get all Added/Deleted/Modified entities (not Unmodified or Detached)
foreach (var ent in this.ChangeTracker.Entries().Where(p => p.State == System.Data.Entity.EntityState.Added || p.State == System.Data.Entity.EntityState.Deleted || p.State == System.Data.Entity.EntityState.Modified))
{
// For each changed record, get the audit record entries and add them
foreach (var x in GetAuditRecordsForChange(ent, userId))
{
this.AUDITLOGs.Add(x);
}
}
// Call the original SaveChanges(), which will save both the changes made and the audit records
return base.SaveChanges();
}
private List<AUDITLOG> GetAuditRecordsForChange(DbEntityEntry dbEntry, string userId)
{
var testingvalue = dbEntry.Member("TAXONOMies").CurrentValue; //Count = 1
var testingvalue1 = dbEntry.Member("TAXONOMies").CurrentValue.ToString(); //"System.Collections.Generic.HashSet`1[oraFresh.TAXONOMY]"
var testingvalue2 = dbEntry.Member("TAXONOMies").CurrentValue.GetType(); //{Name = "HashSet`1" FullName = "System.Collections.Generic.HashSet`1[[oraFresh.TAXONOMY, oraFresh, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null]]"}
}
In the variable testingvalue defined in GetAuditRecordsForChange, when I add watch to it but I can't access the "TAXONID":
Name Value Type
testingvalue Count = 1 object{System.Collections.Generic.HashSet<db.TAXONOMY>}
[0] {db.TAXONOMY} db.TAXONOMY
TAXONID 300000 int
Raw View
Comparer {System.Collections.Generic.ObjectEqualityComparer<db.TAXONOMY>} System.Collections.Generic.IEqualityComparer<db.TAXONOMY>{System.Collections.Generic.ObjectEqualityComparer<db.TAXONOMY>}
Count 1 int
Could someone provide suggestion do I approach wrong path to call property values or it is possible to access it please? Thanks.
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.
What I want to do: When a user selects a product, populate a data grid with every Product. If that Product / Event combination have an associated EventProduct, fill in other pieces of the data grid with that data. If not, create a new EventProduct and default all properties to 0. On saving the event, if the EventProduct properties have changed or been populated, save that EventProduct to the DB as a new EventProduct.
My current approach:
I have three classes: Event, Product, and EventProduct as defined here (truncated).
public partial class Event
{
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Usage", "CA2214:DoNotCallOverridableMethodsInConstructors")]
public Event()
{
EventProducts = new HashSet<EventProduct>();
}
[Key]
public int index { get; set; }
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Usage", "CA2227:CollectionPropertiesShouldBeReadOnly")]
public virtual ICollection<EventProduct> EventProducts { get; set; }
}
public partial class Product
{
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Usage", "CA2214:DoNotCallOverridableMethodsInConstructors")]
public Product()
{
EventProducts = new HashSet<EventProduct>();
}
[Key]
public int index { get; set; }
[StringLength(200)]
public string name { get; set; }
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Usage", "CA2227:CollectionPropertiesShouldBeReadOnly")]
public virtual ICollection<EventProduct> EventProducts { get; set; }
}
public partial class EventProduct
{
public EventProduct()
{
Event = new Event();
Product = new Product();
quantity_allocated = 0;
quantity_sold = 0;
quantity_sampled = 0;
}
public int index { get; set; }
public int EventID { get; set; }
public int ProductID { get; set; }
public int? quantity_allocated { get; set; }
public int? quantity_sold { get; set; }
public decimal? quantity_sampled { get; set; }
public virtual Event Event { get; set; }
public virtual Product Product { get; set; }
}
I'm populating the table by querying and joining my Products to my EventProducts, and creating a new Associative Object which has a Product and an EventProduct in a one-to-one relationship. I'm setting my itemsource equal to the following:
public static List<ProductEventProduct> GetProductEventProduct(Event e, DatabaseModel dbContext)
{
var query = from product in dbContext.Products
join eventProduct in dbContext.EventProducts
on new { pIndex = product.index, eIndex = e.index }
equals new { pIndex = eventProduct.Product.index, eIndex = eventProduct.Event.index } into temp
from eventProduct in temp.DefaultIfEmpty()
select new ProductEventProduct
{
Product = product,
EventProduct = eventProduct
};
var dataSource = query.ToList();
foreach (ProductEventProduct entry in dataSource)
{
if (entry.EventProduct == null)
{
entry.EventProduct = new EventProduct()
{
EventID = e.index,
ProductID = entry.Product.index,
Product = entry.Product,
Event = e
};
}
}
return dataSource;
}
And when I have a single, manually input (direct into my data source) EventProduct it works as intended, and users can edit the Allocated amount (sold and sampled are locked in this view):
My problem is with saving. Right now I'm iterating through each row of the data grid, and if it's been changed or if the value is not null, create an EventProduct from that and add that EventProduct to my Database Context:
List<Associations.ProductEventProduct> entries = (List<Associations.ProductEventProduct>)EventProductDataGrid.ItemsSource;
IEnumerable<Associations.ProductEventProduct> changedEntries = entries.Where(association =>
association.EventProduct.quantity_allocated != 0 ||
association.EventProduct.quantity_sampled != 0 ||
association.EventProduct.quantity_sold != 0);
foreach (Associations.ProductEventProduct entry in changedEntries)
{
// if there are no event products in the database that have the same product and event, it's new so save it to DB
if (!(dbContext.EventProducts.Any(ep =>
ep.EventID == entry.EventProduct.EventID && ep.ProductID == entry.Product.index)))
{
dbContext.EventProducts.Add(entry.EventProduct); // line where I get the error described below
dbContext.SaveChanges();
}
else // if it is an EventProduct which exists in the database already
{
EventProduct modifyEvent = dbContext.EventProducts.Single(ep => ep.Event.index == entry.EventProduct.Event.index && ep.Product.index == entry.Product.index);
modifyEvent.quantity_allocated = entry.EventProduct.quantity_allocated;
modifyEvent.quantity_sampled = entry.EventProduct.quantity_sampled;
modifyEvent.quantity_sold = entry.EventProduct.quantity_sold;
}
}
dbcontext.SaveChanges();
But when adding my EventProduct to my DBContext, I get the error, "'A referential integrity constraint violation occurred: A primary key property that is a part of referential integrity constraint cannot be changed when the dependent object is Unchanged unless it is being set to the association's principal object. The principal object must be tracked and not marked for deletion.'". Which doesn't make sense to me, since both its references to Product and Event are populated, valid, and correct in my debugger.
I've been stuck on various pieces of this issue for days now and I know that my approach is wrong, any advice would be enormously appreciated.
I imagine your problem is that the EventProduct you are adding to your DbContext refers to an Event or Product (or both) that already exist in the database but are not currently being tracked by the DbContext. When calling dbContext.EventProducts.Add(entry.EventProduct); it has the effect that it's trying to add the entry.EventProduct.Event and entry.EventProduct.Product in the DbContext as if they are new entities.
If you know that entry.EventProduct.Event and entry.EventProduct.Product already exists in the database, then you can add them to the change tracker letting EF know that they already exist and haven't changed:
// Let EF know the entities already exist
dbContext.Set<Event>().Attach(entry.EventProduct.Event);
dbContext.Set<Product>().Attach(entry.EventProduct.Product);
// Now add the EventProduct letting it refer to the existing Event and Product
dbContext.EventProducts.Add(entry.EventProduct);
dbContext.SaveChanges();
Note: as per the documentation the entities you attach will be given the state Unchanged which means if you do have changes to the Event or Product that you want to update in the database you should instead use DbContext.Entry() and set the returned Entry.State to Modified.
I have the following two classes:
[Table("Products")]
public class Product
{
public int Id { get; set; }
public string Code { get; set; }
public string Name { get; set; }
public virtual ICollection<Process> Processes { get; set; }
public Product()
{
this.Processes = new HashSet<Process>();
}
}
and
[Table("Processes")]
public class Process
{
public int Id { get; set; }
public string Name { get; set; }
public string MachineName { get; set; }
//list of all products
public virtual ICollection<Product> Products { get; set; }
public Process()
{
this.Products = new HashSet<Product>();
}
}
As you can see, One product can have multiple processes and one process can be bound to multiple products. (e.g. product Chair consists of the following processes: Cutting, Drilling, Screwing, etc...
Under my OnModelCreating method in my DataContext I have specified Many to Many relationships as follows:
modelBuilder.Entity<Process>()
.HasMany<Product>(s => s.Products)
.WithMany(c => c.Processes)
.Map(cs =>
{
cs.MapLeftKey("Process_ProcessId");
cs.MapRightKey("Product_ProductId");
cs.ToTable("ProcessProducts");
});
Which creates a new table named ProcessProducts where many to many relationships are being stored.
My Problem now is, that when I remove e.g. Product from my database, I would like to automatically remove all rows from ProcessProducts table, where the particular ProductId has been used. I wanted to configure CascadeDelete under my modelBuilder, but it does not allow me to do so.
Maybe it's the way, how I am removing the item which is wrong?
public void Delete(TEntity entity)
{
if (entity == null) throw new ArgumentNullException("entity");
_context.Set<TEntity>().Attach(entity);
_context.Set<TEntity>().Remove(entity);
_context.SaveChanges();
}
Any help in regards to this matter would be highly appreciated.
Do delete in cascade mode you can use SQL or EF.
Each case need to be configured how deep the cascade will go. So the question is: How deep you want to go:
If you delete a Product should delete de Process(es) connected with it and if this Process(es) has other Product(s) should cascade keep this chain of deletion?
Perhaps by cascade delete, you only meant delete the connection (many-to-many relationship) between Product and Process. If this is the case you only need to do:
public void DeleteProduct(Product entity)
{
if (entity == null) throw new ArgumentNullException("entity");
entity = _context.Set<Product>().First(f => f.Id == entity.Id);
_context.Set<Product>().Remove(entity);
_context.SaveChanges();
}
This should delete your Product and all ProcessProducts registries connected with your Product.
I have 3 Entities (MasterDoc, Folder, User) that i link together in another Entity (Folder_User).
public class Folder_User
{
public int Id { get; set; }
public MasterDoc MasterDoc { get; set; }
public User User { get; set; }
public Folder Folder { get; set; }
}
After ive inserted an object of type Folder_User into the database, i can (on the same DbContext) query it and retreive the child objects.
public static List<Folder_User> GetAllFolder_USer(User user, DataModel dbContext)
{
List<Folder_User> list = null;
var query = from f in dbContext.Folder_User
where f.User.Id == user.Id
select f;
list = new List<Folder_User>(query);
return list;
}
But after a page-refresh (new dbcontext) when i run the same query the objects reference to MasterDoc is null.
*I have tried turn of lazy loading but nothing seems to fix it.
*Have also checked the database and the table is correctly containing a row with a MasterDoc Id.
You need to Include the MasterDoc in the query:
public static List<Folder_User> GetAllFolder_USer(User user, DataModel dbContext)
{
var query = dbContext.Folder_User.
Include(f => f.MasterDoc).
Where(f => f.User.Id == user.Id);
return query.ToList();
}