Read-only field after INSERT with EF - c#

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;
}

Related

Previously no-tracking entities get tracked upon calling Savechages(). Is this normal?

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};

Updating EF model in new DbContext instance

What is the best way to update an entity framework model in the database with two instances of a DbContext. For example i have a simple User model which is loaded in one DbContext but saved in another:
User model = null;
using(SampleDbContext dbContext = new SampleDbContext())
{
model = dbContext.User.Find(0);
}
model.UserName = "Test-User";
using(SampleDbContext dbContext = new SampleDbContext())
{
// Here is the place i want to save the changes of the model
// without loading it again from the database and set each property
// ...
dbCotnext.SaveChanges();
}
In my case i want to write some UserManager, which has Create, Update and Delete methods. I think creating one DbContext instance for the howle manager
is no solution, because i only want to save the changes of a specific model.
I also don't want to load the models for updating again from the database and settings each value from the source instance, like:
// Update user
using(SampleDbContext dbContext = new SampleDbContext())
{
// I don't want this:
var model = dbContect.User.Find(0);
model.UserName = sourceModel.UserName;
// ...
dbCotnext.SaveChanges();
}
Maybe my problem with manager classes is very simple, but i could not find any good solution.
P.S.: My manager classes are often singleton classes.
Thank you.
You could in your second DbContext do something like this:
using (SampleDbContext dbContext = new SampleDbContext())
{
dbContext.Entry(model).State = EntityState.Modified;
dbContext.SaveChanges();
}
This would save the changes you have made to the entity to the database. However I can't remember if dbContext.Entry queries the DB for the entity.

Entity framework edit primary key and add row

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.

Not able to retrieve OriginalValues in Entity Framework 5

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.

Capturing old values for model in MVC3?

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);
}

Categories

Resources