How to track which entities are updated in Entity Framework - c#

I am working on an eCommerce based web application with MVC and Entity Framework.
I just want to track/know which entities are updated. For example, when the user changes the stock and price for any product and clicks the Save button, I want to track that these two entities are updated with their ProductId.
Is there any way to achieve this?
Thanks.

Disclaimer: I'm the owner of the project Entity Framework Plus
This project allows to audit / track everything what's is modified. There is a lot of options available if you only want to access modified entities.
// using Z.EntityFramework.Plus; // Don't forget to include this.
var ctx = new EntityContext();
// ... ctx changes ...
var audit = new Audit();
audit.CreatedBy = "ZZZ Projects"; // Optional
ctx.SaveChanges(audit);
// Access to all auditing information
var entries = audit.Entries;
foreach(var entry in entries)
{
foreach(var property in entry.Properties)
{
}
}
Documentation: EF+ Audit
NuGet: https://www.nuget.org/packages/Z.EntityFramework.Plus.EF6/

You can use rowversion if it is not in the same http request.
http://www.asp.net/mvc/overview/older-versions/getting-started-with-ef-5-using-mvc-4/handling-concurrency-with-the-entity-framework-in-an-asp-net-mvc-application

Yes you can with this
//let 'myObj' is entity instance
//let db is your context instance
if ( db.Entry(myObj).State == System.Data.EntityState.Modified )
{
//Try your code here
}

Related

Dynamics plugin update record after create

Hi I'm new to Dynamics and plugins in dynamics. I have created a simple Entity called Library that holds books.
After a new book is created I want the price of the book to increment by a GST of 10% on the server side via a plugin.
I know this would normally occur on the page before saving by I'm trying to work out how server side logic works.
I have created a postOperation (synchronous) step for the "Create" message to call the Plugin Execute() method. From my reading this should occur AFTER the record is saved in the database.
I also have a post image entity that I access.
In the Execute method I try to access the saved record via the PostMessageEntity to update the price, but I get an exception saying the record does not exist based on the record identifier that i have obtained. I can confirm the record was never created in the system, yet the postOperation has been called.
How do I access the just saved record in the plugin so that I can update the Price?
My code:
public void Execute(IServiceProvider serviceProvider)
{
// Obtain the execution context from the service provider.
Microsoft.Xrm.Sdk.IPluginExecutionContext context = (Microsoft.Xrm.Sdk.IPluginExecutionContext)
serviceProvider.GetService(typeof(Microsoft.Xrm.Sdk.IPluginExecutionContext));
// create a trace log so you can see where in the code it breaks
ITracingService tracingService = (ITracingService)serviceProvider.GetService(typeof(ITracingService));
// create access to service
IOrganizationServiceFactory serviceFactory = (IOrganizationServiceFactory)serviceProvider.GetService(typeof(IOrganizationServiceFactory));
IOrganizationService service = serviceFactory.CreateOrganizationService(context.UserId);
tracingService.Trace("have reached execute event in plugin.");
// The InputParameters collection contains all the data passed in the message request.
if (context.InputParameters.Contains("Target") &&
context.InputParameters["Target"] is Entity)
{
tracingService.Trace("We have a target and it is an entity.");
// Obtain the target entity from the input parameters.
Entity entity = (Entity)context.InputParameters["Target"];
if (entity.LogicalName == "new_books")
{
tracingService.Trace("the entity id of the record that was created is .." + entity.Attributes["new_booksid"].ToString());
// do we have a post update image of the new_books entity
if (context.PostEntityImages.Contains("newbookpostImage") && context.PostEntityImages["newbookpostImage"] is Entity)
{
tracingService.Trace("we have a postEntityImage.");
// // yep lets grab it.
Entity postMessageEntity = (Entity)context.PostEntityImages["newbookpostImage"];
// get book price as just saved to db
decimal bookPrice = ((Money)postMessageEntity.Attributes["new_price"]).Value;
// get id of the the record we have
Guid RecordID = ((Guid)postMessageEntity.Attributes["new_booksid"]);
tracingService.Trace("we have a post update bookprice.");
tracingService.Trace("the entity id of the post image entity is ..." + postMessageEntity.Attributes["new_booksid"].ToString());
Entity created_book = new Entity("new_books");
// use service to access a field of the current record as it is in the database and column we want to update.
created_book = service.Retrieve(created_book.LogicalName, RecordID, new ColumnSet(true));
//And the last line is where it dies and tells me new_books with id with d7bfc9e2 - 2257 - ec11 - 8f8f - 00224814e6e0 does not exist.
}
}
}
}
Entity postMessageEntity = (Entity)context.PostEntityImages["newbookpostImage"];
Is your PostEntityImage new_books entity?
Also if you have an entity postMessageEntity you can directly get Entity Record ID by
postMessageEntity.ID
rather than Guid RecordID = ((Guid)postMessageEntity.Attributes["new_booksid"]);
Here your code does nothing more than create empty object of type Entity new_books.
You have not set Priamry name field of entiy or any other. Also you have not created a record, you should use
Entity created_book = new Entity("new_books")
service.Create(created_book);
Below you are trying to fecth Record from Entity new_books based on postMessageEntity.Id
You should check postMessageEntity logical name is same as created_book.LogicalName and then use postMessageEntity.ID rather than RecordID
created_book = service.Retrieve(created_book.LogicalName, RecordID, new ColumnSet(true));
In the plugin pipeline you can actually add, modify and even remove attributes in the entity object on the fly. This must be done before the main operations take place: in the prevalidation or in the preoperation stage.
So, your code can be simplified like this:
public void Execute(IServiceProvider serviceProvider)
{
var context = (IPluginExecutionContext)serviceProvider.GetService(typeof(IPluginExecutionContext));
Debug.Assert(context.Stage <= 20); // This only works in prevalidation and preoperation stages.
var book = (Entity)context.InputParameters["Target"]; // For message Create a Target parameter is always available.
// Using GetAttributeValue is more safe, because when price is not set, the attribute will not be available in the collection.
decimal? price = book.GetAttributeValue<Money>("new_price")?.Value;
if (price.HasValue)
book["new_price"] = new Money(price.Value * 1.1M);
}
In the synchronous post create stage you are still in a database transaction. At that point the record is created, but not yet committed.
The ID of the record created can be found in the OutputParameters collection. You can pick it up like this:
var recordId = (Guid)context.OutputParameters["id"];
There is no need to do checks on the context object. When your plugin is registered properly, all items you would expect to be available will be there. If not, a proper exception log will be your best friend. Just add a generic exception handler responsible for writing the error context to the plugin trace log.

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

Using Entity Framework to insert multiple child objects while updating the parent

I'm trying to keep track of changes to my Asset's properties via the setters in the Asset, which is working.
I then pass this Asset along with its changes:
(Business) Asset
- public string Name...
- public DateTime ChangedDate...
-List<ChangeLogs>
to a method and I'm using AutoMapper to convert it along with its children to an (EntityFramework) Asset; this is working as well.
In my method I
using(var dbContext = new DBContext())
{
// Map our (Business)Asset to our (Entity Framework)Asset
var dataAsset = Mapper.Map<Business.Asset, Data.Asset>(asset);
dbContext.Assets.Attach(asset);
dbCore.SaveChanges();
}
I'm getting this error on the Attach line:
An object with the same key already exists in the ObjectStateManager.
The ObjectStateManager cannot track multiple objects with the same
key.
The believe the key its referring to is my ChangeLog Id (PK).
Is there a way to add multiple ChangeLogs to my Asset and then save it?
When you have a detached entity and try to attach it back that already exists in the context, you will get this exception.
To resolve this, try to load the old entity and save the changes from the new entity.
using(var dbContext = new DBContext())
{
// Map our (Business)Asset to our (Entity Framework)Asset
var dataAsset = Mapper.Map<Business.Asset, Data.Asset>(asset);
var oldEntity = dbContext.Assets.FirstOrDefault(m => m.Id == dataAsset.Id);
dbContext.Entry(oldEntity).CurrentValues.SetValues(dataAsset);
dbContext.Entry(oldEntity.ChangeLogs).CurrentValues.SetValues(dataAsset.ChangeLogs);
dbContext.SaveChanges();
}

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.

Implementing Audit Log / Change History with MVC & Entity Framework

I am building in a Change History / Audit Log to my MVC app which is using the Entity Framework.
So specifically in the edit method public ActionResult Edit(ViewModel vm), we find the object we are trying to update, and then use TryUpdateModel(object) to transpose the values from the form on to the object that we are trying to update.
I want to log a change when any field of that object changes. So basically what I need is a copy of the object before it is edited and then compare it after the TryUpdateModel(object) has done its work. i.e.
[HttpPost]
public ActionResult Edit(ViewModel vm)
{
//Need to take the copy here
var object = EntityFramework.Object.Single(x=>x.ID = vm.ID);
if (ModelState.IsValid)
{
//Form the un edited view model
var uneditedVM = BuildViewModel(vm.ID); //this line seems to confuse the EntityFramework (BuildViewModel() is used to build the model when originally displaying the form)
//Compare with old view model
WriteChanges(uneditedVM, vm);
...
TryUpdateModel(object);
}
...
}
But the problem is when the code retrieves the "unedited vm", this is causing some unexpected changes in the EntityFramework - so that TryUpdateModel(object); throws an UpdateException.
So the question is - in this situation - how do I create a copy of the object outside of EntityFramework to compare for change/audit history, so that it does not affect or change the
EntityFramework at all
edit: Do not want to use triggers. Need to log the username who did it.
edit1: Using EFv4, not too sure how to go about overriding SaveChanges() but it may be an option
This route seems to be going nowhere, for such a simple requirement! I finally got it to override properly, but now I get an exception with that code:
public partial class Entities
{
public override int SaveChanges(SaveOptions options)
{
DetectChanges();
var modifiedEntities = ObjectStateManager.GetObjectStateEntries(EntityState.Modified);
foreach (var entry in modifiedEntities)
{
var modifiedProps = ObjectStateManager.GetObjectStateEntry(entry).GetModifiedProperties(); //This line throws exception The ObjectStateManager does not contain an ObjectStateEntry with a reference to an object of type 'System.Data.Objects.EntityEntry'.
var currentValues = ObjectStateManager.GetObjectStateEntry(entry).CurrentValues;
foreach (var propName in modifiedProps)
{
var newValue = currentValues[propName];
//log changes
}
}
//return base.SaveChanges();
return base.SaveChanges(options);
}
}
IF you are using EF 4 you can subscribe to the SavingChanges event.
Since Entities is a partial class you can add additional functionality in a separate file. So create a new file named Entities and there implement the partial method OnContextCreated to hook up the event
public partial class Entities
{
partial void OnContextCreated()
{
SavingChanges += OnSavingChanges;
}
void OnSavingChanges(object sender, EventArgs e)
{
var modifiedEntities = ObjectStateManager.GetObjectStateEntries(EntityState.Modified);
foreach (var entry in modifiedEntities)
{
var modifiedProps = ObjectStateManager.GetObjectStateEntry(entry.EntityKey).GetModifiedProperties();
var currentValues = ObjectStateManager.GetObjectStateEntry(entry.EntityKey).CurrentValues;
foreach (var propName in modifiedProps)
{
var newValue = currentValues[propName];
//log changes
}
}
}
}
If you are using EF 4.1 you can go through this article to extract changes
See FrameLog, an Entity Framework logging library that I wrote for this purpose. It is open-source, including for commercial use.
I know that you would rather just see a code snippet showing how to do this, but to properly handle all the cases for logging, including relationship changes and many-to-many changes, the code gets quite large. Hopefully the library will be a good fit for your needs, but if not you can freely adapt the code.
FrameLog can log changes to all scalar and navigation properties, and also allows you to specify a subset that you are interested in logging.
There is an article with high rating here at the codeproject: Implementing Audit Trail using Entity Framework . It seems to do what you want. I have started to use this solution in a project. I first wrote triggers in T-SQL in the database but it was too hard to maintain them with changes in the object model happening all the time.

Categories

Resources