I am curios if I can make this easier/better.
The user is able to create new tickets and with each ticket can be several files associated, one file can be related to multiple tickets.
Now, when the user creates a ticket, he can already add files. Meaning I have no Id for the ticket and thus no way to build a relation. How should I solve this?
public class FilesPerTicket
{
public int TickedId;
public Ticket Ticket;
public int FileId;
public File File;
}
public class File
{
public ICollection<FilesPerTicket> FilesperTicket;
}
public class Ticket
{
public ICollection<FilesPerTicket> FilesperTicket;
}
Now the user creates the ticket and adds several files to it.
public IActionResult Create(MyModel model)
{
// .....
var filesPerTicket = model.Files.Select(x => new FilesPerTicket() { FileId = x.Value }).ToList();
var newTicket = new Ticket() { //...... };
newTicket.FilesPerTicket = filesPerTicket;
// ....
context.Add(newTicket);
context.SaveChanges();
}
This doesn't work because we haven't provided a Ticketid and therefore every TicketId in FilesPerTicket is 0.
I know that I just can save the ticket and afterwards it will have the primary key in it. Which I then can use to fill the FilesPerTicket and save that one.
But then I would have those in separate transactions (because SaveChanges was called) and use another try/catch.
Is there an way to tell EF Core to fill this TicketId automatically when this entity gets saved at the same time as a ticket? Or some other way to save that entity?
Using ASP.NET Core 2.2 with EF Core, I have followed various guides in trying to implement the automatic creation of date/time values when creating either a new record or editing/updating an existing one.
The current result is when i initially create a new record, the CreatedDate & UpdatedDate column will be populated with the current date/time.
However first time I edit this same record, the UpdatedDate column is then given a new date/time value (as expected) BUT for some reason, EF Core is wiping out the value of the original CreatedDate which results in SQL assigning a default value.
Required result I need as follows:
Step 1: New row created, both CreatedDate & UpdatedDate column is given a date/time value (this already works)
Step 2: When editing and saving an existing row, I want EF Core to update the UpdatedDate column with the updated date/time only, BUT leave the other CreatedDate column unmodified with the original creation date.
I'm using EF Core code first, and do no want to go down the fluent API route.
One of the guides i was partially following is https://www.entityframeworktutorial.net/faq/set-created-and-modified-date-in-efcore.aspx but neither this or other solutions I've tried is giving the result I am after.
Baseclass:
public class BaseEntity
{
public DateTime? CreatedDate { get; set; }
public DateTime? UpdatedDate { get; set; }
}
DbContext Class:
public override Task<int> SaveChangesAsync(bool acceptAllChangesOnSuccess, CancellationToken cancellationToken = default(CancellationToken))
{
var entries = ChangeTracker.Entries().Where(E => E.State == EntityState.Added || E.State == EntityState.Modified).ToList();
foreach (var entityEntry in entries)
{
if (entityEntry.State == EntityState.Modified)
{
entityEntry.Property("UpdatedDate").CurrentValue = DateTime.Now;
}
else if (entityEntry.State == EntityState.Added)
{
entityEntry.Property("CreatedDate").CurrentValue = DateTime.Now;
entityEntry.Property("UpdatedDate").CurrentValue = DateTime.Now;
}
}
return base.SaveChangesAsync(acceptAllChangesOnSuccess, cancellationToken);
}
UPDATE FOLLOWING ADVICE FROM STEVE IN COMMENTS BELOW
I spent a bit more time debugging today, turns out the methods I posted above are appear to be functioning as expected i.e. when editing an existing row and saving it, only the entityEntry.State == EntityState.Modified IF statement is being called. So what I'm finding is that after saving the entity, the CreatedDate column is being overwitten with a Null value, I can see this by watching the SQL explorer after a refresh. I believe the issue is along the lines of what Steve mentions below "If it is #null then this might also explain the behavior in that it is not being loaded with the entity for whatever reason."
But i'm a little lost in tracing where this CreatedDate value is being dropped somewhere through edit/save process.
Image below shows the result at the point just before the entity is saved following an update. In the debugger I'm not quite sure where to find the entry of the CreatedDate to see what value is held at this step, but it appears to be missing from the debugger list so wandering whether somehow it doesn't know about the existence of this field when saving.
Below is the method I have in my form 'Edit' Razor page model class:
public class EditModel : PageModel
{
private readonly MyProject.Data.ApplicationDbContext _context;
public EditModel(MyProject.Data.ApplicationDbContext context)
{
_context = context;
}
[BindProperty]
public RuleParameters RuleParameters { get; set; }
public async Task<IActionResult> OnGetAsync(int? id)
{
if (id == null)
{
return NotFound();
}
RuleParameters = await _context.RuleParameters
.Include(r => r.SystemMapping).FirstOrDefaultAsync(m => m.ID == id);
if (RuleParameters == null)
{
return NotFound();
}
ViewData["SystemMappingID"] = new SelectList(_context.SystemMapping, "ID", "MappingName");
return Page();
}
public async Task<IActionResult> OnPostAsync()
{
if (!ModelState.IsValid)
{
return Page();
}
_context.Attach(RuleParameters).State = EntityState.Modified;
try
{
await _context.SaveChangesAsync();
}
catch (DbUpdateConcurrencyException)
{
if (!RuleParametersExists(RuleParameters.ID))
{
return NotFound();
}
else
{
throw;
}
}
return RedirectToPage("./Index");
}
private bool RuleParametersExists(int id)
{
return _context.RuleParameters.Any(e => e.ID == id);
}
}
Possibly one of the reasons for this issue is the fact that I have not included the CreatedDate field in my Edit Razor Page form, so when I update the entity which in turn will run the PostAsync method server side, there is no value stored for the CreatedDate field and therefore nothing in the bag by the tine the savechangesasync method is called in my DbContext Class. But I also didn't think this was necessary? otherwise I'd struggle to see what value there is in the this process of using an inherited BaseEntity class i.e. not having to manually add the CreatedDate & UpdatedDate attribute to every model class where I want to use it...
It may be easier to just give your BaseEntity a constructor:
public BaseEntity()
{
UpdatedDate = DateTime.Now;
CreatedDate = CreatedDate ?? UpdatedDate;
}
Then you can have your DbContext override SaveChangesAsync like:
public override Task<int> SaveChangesAsync(
bool acceptAllChangesOnSuccess,
CancellationToken token = default)
{
foreach (var entity in ChangeTracker
.Entries()
.Where(x => x.Entity is BaseEntity && x.State == EntityState.Modified)
.Select(x => x.Entity)
.Cast<BaseEntity>())
{
entity.UpdatedDate = DateTime.Now;
}
return base.SaveChangesAsync(acceptAllChangesOnSuccess, token);
}
Possibly one of the reasons for this issue is the fact that I have not included the CreatedDate field in my Edit Razor Page form, so when I update the entity which in turn will run the PostAsync method server side, there is no value stored for the CreatedDate field and therefore nothing in the bag by the tine the savechangesasync method is called in my DbContext Class.
That's true.Your post data does not contains the original CreatedDate,so when save to database, it is null and could not know what the exact value unless you assign it before saving.It is necessary.
You could just add below code in your razor form.
<input type="hidden" asp-for="CreatedDate" />
Update:
To overcome it in server-side,you could assign data manually:
public async Task<IActionResult> OnPostAsync()
{
RuleParameters originalData = await _context.RuleParameters.FirstOrDefaultAsync(m => m.ID == RuleParameters.ID);
RuleParameters.CreatedDate = originalData.CreatedDate;
_context.Attach(RuleParameters).State = EntityState.Modified;
await _context.SaveChangesAsync();
}
I don't suspect EF is doing this, but rather your database, or you're inadvertently inserting records instead of updating them.
A simple test: Put break-points in your SaveChangesAsnc method within both the Modified and Added handlers and then run a unit test that loads an entity, edits it, and saves. Which breakpoint is hit? If the behavior seems to be normal with a simple unit test, repeat again with your code.
If the Modified breakpoint is hit, and only the Modified handler is hit then check the state of the CreatedDate value in the entity modified. Does it still reflect the original CreatedDate? If yes, then it would appear that something in your schema will be overwriting it on save. If no then you have a bug in your code that has caused it to update. If it is #null then this might also explain the behaviour in that it is not being loaded with the entity for whatever reason. Check that the property has not been configured as something like a Computed property.
If the Added breakpoint is hit at all, then this would point at a scenario where you're dealing with a detached entity, such as an entity that was read from a different DB Context and being associated to another entity in the current DB Context and saved as a byproduct. When a DbContext encounters an entity that was loaded and disassociated with a different DbContext, it will treat that entity as a completely new entity and insert a new record. The biggest single culprit for this is invariably MVC code where people pass entities to/from views. Entity references are loaded in one request, serialized to the view, and then passed back on another request. Devs assume they are receiving an entity that they can just associate to a new entity and save, but the Context of this request doesn't know about that entity, and that "entity" isn't actually an entity, it is now a POCO shell of data that the serializer created. It's no different to you newing up a new class and populating fields. EF won't know the difference. The result of this is you will trip the Added condition for your entity, and after completion you will have a duplicate record. (with different PK if EF is configured to treat PKs as Identity)
So an example is an Order screen: When presenting a screen to create a new order I may have loaded the Customer and passed that to the view to display customer information and will want to associate to the new order:
var customer = context.Customers.Single(x => x.CustomerId == 15);
var newOrder = new Order { Customer = customer };
return View(newOrder);
This looks innocent enough. When we go to save the new order after setting their details:
public ActionResult Save(Order newOrder)
{
context.Orders.Add(newOrder);
newOrder.Customer.Orders.Add(newOrder);
context.SaveChanges();
// ...
}
newOrder had a reference to Customer #14, so all looks good. We're even associating the new order to the customer's order collection. We might even want to have updated fields on the customer record to reflect a change to the Modified date. However, newOrder in this case, and all associated data including .Customer are plain 'ol C# objects at this point. We've added the new order to the Context, but as far as the context is concerned, the Customer referenced is also a new record. It will ignore the Customer ID if that is set as an Identity column and it will save a brand new Customer record (ID #15 for example) with all of the same details as Customer ID 14 and associate that to the new order. It can be subtle and easy to miss until you start querying Customers and spotting duplicate looking rows.
If you are passing entities to/from views, I'd be very wary of this gotcha. Attaching and setting modified state is one option, but that involves trusting that the data has not been tampered with. As a general rule, calls to update entities should never pass entities & attach them, but rather re-load those entities, validate row version, validate the data coming in, and only copy across fields you expect should ever be modified before saving the entity associated to the DbContext.
Hopefully that gives you a few ideas on things to check to get to the bottom of the issue.
I am having a problem with an Entity Framework 6.1.3 CodeFirst Many-to-Many relationship. My model is essentially like so:
class Schedule
{
int Id { get; set; }
}
class Contest
{
int Id { get; set; }
ICollection<Schedule> GameSchedules { get; set; }
}
My context for reference:
class MyContext
{
MyContext() : base("name=DefaultConnection")
{
// no lazy loading for us
this.Configuration.LazyLoadingEnabled = false;
// do not auto detect changes for me
this.Configuration.AutoDetectChangesEnabled = false;
// we don't want our stuff to be wrapped in proxies
this.Configuration.ProxyCreationEnabled = false;
}
}
Entity Farmework CodeFirst configuraiton:
class ContestConfiguration : EntityTypeConfiguration<Contest>
{
ContestConfiguration()
{
// setup many-to-many table between game schedules and contests
this.HasMany(contest => contest.GameSchedules)
.WithMany(schedule => schedule.Contests)
.Map(
contestSchedule =>
{
contestSchedule.MapLeftKey("ContestId");
contestSchedule.MapRightKey("ScheduleId");
contestSchedule.ToTable("ContestSchedule", "something");
});
}
}
Upon creating a contest with an existing schedule I do the following and I see taht two sql statements went in, one that creates the contest and one that creates a record for the MTM table.
Contest Add(Contest entity)
{
// setup schedules
entity.GameSchedules.ToList().ForEach(schedule => this.Context.Entry(schedule).State = EntityState.Unchanged);
// call base add method
return base.Add(entity);
}
However, when I try to Update, its a very different story. I have tried numerous ways, and cannot get CodeFirst to update the relationshiop in the MTM table. It either tries to delete a schedule along with the MTM record or it does nothing. Any ideas on how to accomplish this mind blowing feat?
Ok, figured it out. First and foremost, remove the ManyToManyCascadeDeleteConvention or else on each DELETE from the MTM table, it will try to remove the schedule or contest as well. This is because by default EntityFramework is setting the cascade delete to true.
// remove many-to-many cascade auto delete
modelBuilder.Conventions.Remove<ManyToManyCascadeDeleteConvention>();
now here is the code that updates
1. Ensure that you have copied over the new schedules to a new collection
2. Modify the existing entity with new data (this can be done either before or after the MTM table is updated)
3. To make it simple, I just removed all of the current rows from the MTM table that are linked to this and added the new ones for the sake of brevity.
4. Save your changes to make sure the schedules are deleted
5. Now add the new ones
6. Save your changes again to make sure the new schedules are added
public void Update(Contest entity)
{
// (1) generate new collection so that the reference doesn't get overwritten
var schedulesToAdd = entity.GameSchedules.ToList();
// (2) get entry
var entry = this.context.Entry(entity);
// set entry to modified
entry.State = EntityState.Modified;
// save the current state
this.Context.SaveChanges();
// get the existing context from the db
var existingContest = this.Set.Where(contest => contest.Id == entity.Id).Include(contest => contest.GameSchedules).SingleOrDefault();
// get the object context
var context = ((IObjectContextAdapter)this.Context).ObjectContext;
// iterate over existing relationships and eliminate
foreach (var existingSchedule in existingContest.GameSchedules.ToList())
{
var schedule = this.Context.Schedules.Find(existingSchedule.Id);
// (3) mark relationship as deleted
context.ObjectStateManager.ChangeRelationshipState(existingContest, schedule, contest => contest.GameSchedules, EntityState.Deleted);
}
// (4) save the current state
result = this.SaveChanges();
// iterate over new entity relationships and add them
foreach (var newSchedule in schedulesToAdd)
{
// find the schedule
var schedule = this.Context.Schedules.Find(newSchedule.Id);
// (5) mark relationship as added
context.ObjectStateManager.ChangeRelationshipState(existingContest, schedule, contest => contest.GameSchedules, EntityState.Added);
}
// (6) save the current state
this.SaveChanges();
}
keep in mind, you will want to come up with some more elaborate behavior for removing eixsting relationships and adding new ones. This was just an exmaple.
I have the following tables/views
Users (View)
-> UserId
-> Name
Roles (Table)
-> RoleId
-> Name
UserRoles (Table)
-> UserId
-> RoleId
and the classes
public class Role{
public int RoleId{get;set}
public string Name{get;set}
}
public class User{
public int UserId{get;set}
public string Name{get;set}
public ICollection<Role> Roles{get;set}
}
and the save method
using (var helper = new DbContext())
{
helper.Users.Attach(user);
helper.SaveChanges();
}
As you can see above, the Users is a view and UserRoles is a mapping table. I am able to retrieve User entities along with the mapped Roles. But while saving it is not throwing any exceptions nor is it saving. I tried checking the db using profiler and it is not even hitting the db.
Since Users is a view I don't want to save the User entity but only the changes made in the Roles collection.
This cannot save anything. Attach puts the whole object graph into the context, but in state Unchanged. If all objects in the context are unchanged SaveChanges won't issue any command to the DB (because for EF nothing has changed).
If you want to make changes which EF recognizes as such you must first attach the object which represents the state in the Db and then make your changes, something like:
using (var helper = new DbContext())
{
helper.Users.Attach(user);
helper.Roles.Attach(myNewRole);
user.Name = myNewName;
user.Roles.Add(myNewRole);
// etc.
helper.SaveChanges();
}
Alternatively you can mark the user as modified:
helper.Entry(user).State = EntityState.Modified;
But I believe this only affects scalar properties of the entity and it doesn't solve the problem to add a new role to the user.
I have created a model POCO class called Recipe; a corresponding RecipeRepository persists these objects. I am using Code First on top of an existing database.
Every Recipe contains an ICollection<RecipeCategory> of categories that link the Recipes and the Categories table in a many-to-many relationship. RecipeCategory contains the corresponding two foreign keys.
A simplified version of my controller and repository logic looks like this (I have commented out all checks for authorization, null objects etc. for simplicity):
public ActionResult Delete(int id)
{
_recipeRepository.Remove(id);
return View("Deleted");
}
The repository's Remove method does nothing but the following:
public void Remove(int id)
{
Recipe recipe = _context.Recipes.Find(id);
_context.Recipes.Remove(recipe);
_context.SaveChanges();
}
Howevery, the code above does not work since I receive a System.InvalidOperationException every time I run it: Adding a relationship with an entity which is in the Deleted state is not allowed.
What does the error message stand for and how can I solve the problem? The only thing I try to achieve is deleting an entity.
#Ladislav: I have replaced ICollection<RecipeCategory> by ICollection<Category>. Accidentially, ReSharper refactored away the virtual keyword.
However, the problem remains — I cannot delete a Category from a Recipe entity. The following code does not persist the deletion of the categories to the database:
private void RemoveAllCategoriesAssignedToRecipe()
{
foreach (Category category in _recipe.Categories.ToArray())
{
_recipe.Categories.Remove(category);
category.Recipes.Remove(_recipe);
}
_context.SaveChanges();
}
I have debugged the code and can confirm that the collections are modified correctly — that is, they contain no elements after the loop (I have also used the Clear() method). After calling SaveChanges(), they are populated again.
What am I doing wrong?
(Maybe it is important: I am using the Singleton pattern to only have one instance of the context.)
I was able to solve the problem the following way:
private void RemoveAllCategoriesAssignedToRecipe()
{
foreach (Category category in _recipe.Categories.ToArray())
{
Category categoryEntity = _categoryRepository.Retrieve(category.CategoryID);
var recipesAssignedToCategory = categoryEntity.Recipes.ToArray();
categoryEntity.Recipes.Clear();
foreach (Recipe assignedRecipe in recipesAssignedToCategory)
{
if (assignedRecipe.RecipeID == _recipe.RecipeID)
{
continue;
}
categoryEntity.Recipes.Add(assignedRecipe);
}
_context.Entry(categoryEntity).State = EntityState.Modified;
}
_recipe.Categories.Clear();
_context.SaveChanges();
}