I have an edit/create page for a model. When the user submits the form, it'll be added or updated to the database.
After that happens, I want to redirect them back to the same form while keeping the same data, with a cleared out id as well as some other values.
public ActionResult AddProduct(MyModel myModel)
{
// Save
if (myModel.AuditID != 0) {
Update(myModel);
} else {
Add(myModel);
}
// Set some values so it's seen as new, as well as some
// other values that need to be cleared
myModel.ID = 0;
myModel.Product = "";
// Edit/Create page
ActionResult ret = EditCreateKnownRow(myModel);
return ret;
}
I want it to be treated as a completely new entity, but I get an InvalidOperationException with these details:
The property 'ID' is part of the object's key information and cannot be modified.
I get that entity framework doesn't want to deal with the foreign key constraints that may exist, but that has nothing to do with what I'm looking for. Is there a way to treat it as a new entity without having to create a copy constructor?
Thanks.
Would doing something to the ModelState help? I've tried ModelState.Remove(myModel.ID.ToString()); and ModelState.Clear(); before modifying the key, but it didn't work.
Related
I have a simple controller in which I create and add a user and a profile to the database
public async ActionResult AddUserAndProfile(string id)
{
_context.Users.Add(new User { Id = id });
_context.SaveChanges(); // If this line is removed, error doesn't occur.
var profile = new Profile
{
Id = "id",
User = _context.Users.AsNoTracking().FirstOrDefault(u => u.Id.Equals(id))
}; // Exception given on this line.
_context.Profiles.Add(profile);
_context.SaveChanges();
return Ok();
}
When I call this controller with id = "0" I get the following exception:
The instance of entity type 'User' cannot be tracked because another instance with the key value '{Id: 0}' is already being tracked. When attaching existing entities, ensure that only one entity instance with a given key value is attached.
However, if I remove the first SaveChanges() call, this error is not shown.
1st question: Is it intended that entities get tracked after saving changes, wouldn't it make sense that they get tracked beforehand only? Also afaik, add actions don't mark entities as tracked.
2nd question: When is the best time to call SaveChages() in this situation? (It is important to note that add user and add profile actions are in different repo methods in the real project. I simplified the code here.)
3rd question: What is the best way to add foreign keys in situations like this?
You should be able to save whole hierarchy within one call to SaveChanges:
var profile = new Profile
{
Id = "id",
User = new User { Id = id }
};
_context.Profiles.Add(profile);
_context.SaveChanges();
It should answer 2nd and 3rd questions I think. As for the first one - there is a github issue with request to "Do not track after SaveChanges()".
Since you have different methods you can just set the UserId property on Profile (if you have explicitly added it to Profile entity) directly.
Save the user you create, and then specify that user for the profile.
var user = new User { Id = id };
_context.Users.Add(user);
_context.SaveChanges();
var profile = new Profile {Id = "id", User = user};
Entity Framework, Code First
I have a model with a lot of fields. One field ("Name") I do not want to be editable but read-only after insert. But I still want to show this field as part of my "Edit" form (and viewModel for edit). I'd like to show it as readonly textbox (like #Html.TextBoxFor(c => c.Name, new {#readonly = "readonly"})) but if someone change it's value with browser dev.tools it should not be saved to DB.
Is there some beautiful way to do it?
Now I'm getting an instance from DB on and set "Name" field of form data back to value from DB.
When processing the ViewModel which is sent from the frontend, ignore the "read-only" fields before saving them in the database. AFAIK EF does not support ignoring of properties.
One way to approach this using Entity Framework is to create a database model and a view model. Make sure you have the CRUD translation in the back end when saving.
public async Task<YourModelContext> SaveModel(YourModelContext context)
{
var existing = await YourTable.FirstOrDefaultAsync(f => f.Id == context.Id);
if (existing == null)
{
existing = new YourDatabaseModel
{
Created = DateTime.UtcNow
}
YourTable.Add(existing);
}
// Name will be saved and changed by the user
// existing.Name = context.Name;
// existing.Description = context.Description;
// existing.SomeOtherField = context.SomeOtherField;
// Not specifying name will not update the name.
// You'll have to set the readonly on textbox in the web page
existing.Description = context.Description;
existing.SomeOtherField = context.SomeOtherField;
// Save the changes to the underlying database
await SaveChangesAsync();
// Assign the inserted database id to the view model id
context.Id = existing.Id;
// Return the view model context to the html page
return context;
}
I am writing a asp.net mvc4 app and I am using entity framework 5. Each of my entities have fields like EnteredBy, EnteredOn, LastModifiedBy and LastModifiedOn.
I am trying to auto-save them by using the SavingChanges event. The code below has been put together from numerous blogs, SO answeres etc.
public partial class myEntities : DbContext
{
public myEntities()
{
var ctx = ((IObjectContextAdapter)this).ObjectContext;
ctx.SavingChanges += new EventHandler(context_SavingChanges);
}
private void context_SavingChanges(object sender, EventArgs e)
{
ChangeTracker.DetectChanges();
foreach (ObjectStateEntry entry in
((ObjectContext)sender).ObjectStateManager
.GetObjectStateEntries
(EntityState.Added | EntityState.Modified))
{
if (!entry.IsRelationship)
{
CurrentValueRecord entryValues = entry.CurrentValues;
if (entryValues.GetOrdinal("LastModifiedBy") > 0)
{
HttpContext currContext = HttpContext.Current;
string userName = "";
DateTime now = DateTime.Now;
if (currContext.User.Identity.IsAuthenticated)
{
if (currContext.Session["userId"] != null)
{
userName = (string)currContext.Session["userName"];
}
else
{
userName = currContext.User.Identity.Name;
}
}
entryValues.SetString(
entryValues.GetOrdinal("LastModifiedBy"), userName);
entryValues.SetDateTime(
entryValues.GetOrdinal("LastModifiedOn"), now);
if (entry.State == EntityState.Added)
{
entryValues.SetString(
entryValues.GetOrdinal("EnteredBy"), userName);
entryValues.SetDateTime(
entryValues.GetOrdinal("EnteredOn"), now);
}
else
{
string enteredBy =
entry.OriginalValues.GetString(entryValues.GetOrdinal("EnteredBy"));
DateTime enteredOn =
entry.OriginalValues.GetDateTime(entryValues.GetOrdinal("EnteredOn"));
entryValues.SetString(
entryValues.GetOrdinal("EnteredBy"),enteredBy);
entryValues.SetDateTime(
entryValues.GetOrdinal("EnteredOn"), enteredOn);
}
}
}
}
}
}
My problem is that entry.OriginalValues.GetString(entryValues.GetOrdinal("EnteredBy")) and entry.OriginalValues.GetDateTime(entryValues.GetOrdinal("EnteredOn")) are not returning the original values but rather the current values which is null. I tested with other fields in the entity and they are returning the current value which were entered in the html form.
How do I get the original value here?
I think the problem may be that you are using the instance provided by the model binder as the input to your controller method, so EF does not know anything about that entity and its original state. Your code may look like this:
public Review Update(Review review)
{
_db.Entry(review).State = EntityState.Modified;
_db.SaveChanges();
return review;
}
In that case, EF knows nothing about the Review instance that is being saved. It is trusting you and setting it as modified, so it will save all of its properties to the database, but it does not know the original state\values of that entity.
Check the section named Entity States and the Attach and SaveChanges Methods of this tutorial. You can also check the first part of this article, that shows how EF does not know about the original values and will update all properties in the database.
As EF will need to know about the original properties, you may first load your entity from the database and then update its properties with the values received in the controller. Something like this:
public Review Update(Review review)
{
var reviewToSave = _db.Reviews.SingleOrDefault(r => r.Id == review.Id);
//Copy properties from entity received in controller to entity retrieved from the database
reviewToSave.Property1 = review.Property1;
reviewToSave.Property2 = review.Property2;
...
_db.SaveChanges();
return review;
}
This has the advantage that only modified properties will be send and updated in the database and that your views and view models don't need to expose every field in your business objects, only those that can be updated by the users. (Opening the door for having different classes for viewModels and models\business objects). The obvious disadvantage is that you will incur an additional hit to the database.
Another option mentioned in the tutorial I referenced above is for you to save the original values somehow (hidden fields, session, etc) and on save use the original values to attach the entity to the database context as unmodified. Then update that entity with the edited fields. However I would not recommend this approach unless you really need to avoid that additional database hit.
Hope that helps!
I was running into a similar problem when trying to audit log the Modified values of an Entity.
It turns out during the post back the ModelBinder doesn't have access to the original values so the Model received is lacking the correct information. I fixed my problem by using this function which clones the current values, relods the object, and then reset the current values.
void SetCorrectOriginalValues(DbEntityEntry Modified)
{
var values = Modified.CurrentValues.Clone();
Modified.Reload();
Modified.CurrentValues.SetValues(values);
Modified.State = EntityState.Modified;
}
You can gain access to the DbEntityEntry though the change tracker, or the entry function from your context.
I'm wanting to capture the old values within a model so I can compare with the new values after submission, and create audit logs of changes a user makes.
My guess is doing it with hidden input boxes with duplicated old value properties would be one way. But wondering if there are any other good alternatives?
Thanks
In the save method, just go and get the original object from the database before saving the changes, then you have your old and new values to compare against? :)
This sounds like standard auditing. You should not worry about what has changed just capture EVERYTHING and who made the change. Unless there is some sort of real time reporting that needs to be done.
Possible auditing implementations:
CQRS, in a nutshell it tracks every change to a given object. The downside is it's an architecture that is more involved to implement.
The Rolling ledger. Each insert is a new row in the database. The most current row is used for display purposes, but with each update, a new row is inserted into the database.
Yet another approach is to save it off into an audit table.
All get the job done.
You could also store the original model in the view bag and do something like this...
// In the controller
public ActionResult DoStuff()
{
// get your model
ViewBag.OriginalModel = YourModel;
return View(YourModel);
}
// In the View
<input type="hidden" name="originalModel" value="#Html.Raw(Json.Encode(ViewBag.OriginalModel));" />
// In the controller's post...
[HttpPost]
public ActionResult DoStuff(YourModel yourModel, string originalModel)
{
// yourModel will be the posted data.
JavaScriptSerializer JSS = new JavaScriptSerializer();
YourModel origModel = JSS.Deserialize<YourModel>(originalModel);
}
I didn't get a chance to test this, just a theory :)
Exactly what mattytommo says is the preferred method all around
Instantiate new view model for creating a new entity
public ActionResult Edit(int id) {
var entity = new Entity(id); // have a constructor in your entity that will populate itself and return the instance of what is in the db
// map entity to ViewModel using whatever means you use
var model = new YourViewModel();
return View(model);
}
Post changes back
[HttpPost]
public ActionResult Edit(YourViewModel model) {
if (ModelState.IsValid) {
var entity = new YourEntity(model.ID); // re-get from db
// make your comparison here
if(model.LastUserID != entity.LastUserID // do whatever
... etc...
}
return View(model);
}
I am working through sample MVC Nerdinner tutorial and using it on the AdventureWorks database. I have created an Edit action in the CategoryController to edit Product Category in AdventureWorks. The only updateable field in this table is the Name (the other fields - ID, RowGUID and UpdateDate are autogenerated). So my edit form View has only 1 field for the Name (of Product Category). My "Save" action for the edit is below: -
[AcceptVerbs(HttpVerbs.Post)]
public ActionResult Edit(int id, FormCollection collection){
ProductCategory p = awRepository.GetProductCategory(id);
try
{
//UpdateModel(p);
p.Name = Request.Form["Name"];
awRepository.Save();
return RedirectToAction("Details", new { id = p.ProductCategoryID });
}
catch
{
foreach (var err in p.GetRuleViolations())
{
ModelState.AddModelError(err.PropertyName, err.ErrorMessage);
}
return View(p);
}
}
If I use the code as above, everything works as long as the Name I enter is valid (thus there is no exception). If I introduce an error (which is raised by GetRuleViolations if the Name is blank or for testing purposes is a particular "Test" string) I get a NullReferenceException (Object reference not set to an instance of an object) on this line in the View (Category/Edit.aspx) when the Edit View is redrawn (to show the user the error and allow him to correct)
<%= Html.TextBox("Name") %>
If I update my ProductCategory using UpdateModel(p) instead of using the Request.Form variable, everything works fine; Valid data is saved and invalid data redraws the view showing the error message.
My question is: what is the difference between UpdateModel and manual updating my variable by reading the values from Request.Form collection? The Nerdinner tutorial seems to suggest that both are equivalent. So I am surprised that one works smoothly and the other raises an exception.
Sounds like this:
http://forums.asp.net/p/1396019/3006051.aspx
So, for every error you add with
ModelState.AddModelError() and call
the View again, MVC Framework will try
to find an AttemptedValue for every
error it finds. Because you didn't add
them, MVC will throw an exception.
Normally you don't need to add these
values: AttemptedValues are
automaticaly populated when you use
DefaultBinding (by calling
UpdateModel() or by passing the object
to bind as an Action Method paramter:
public ActionResult
Create(FormCollection Form,
YourObjectType yourObject).
Looks like the following is done automatically by UpdateModel, but not done manually by yourself?
if (Form["Name"].Trim().Length == 0)
{
ModelState.AddModelError("Name", "Name is required");
//You missed off SetModelValue?
ModelState.SetModelValue("Name", Form.ToValueProvider()["Name"]);
}