My database has a 'LastModifiedUser' column on every table in which I intend to collect the logged in user from an application who makes a change. I am not talking about the database user so essentially this is just a string on each entity. I would like to find a way to default this for each entity so that other developers don't have to remember to assign it any time they instantiate the entity.
So something like this would occur:
using (EntityContext ctx = new EntityContext())
{
MyEntity foo = new MyEntity();
// Trying to avoid having the following line every time
// a new entity is created/added.
foo.LastModifiedUser = Lookupuser();
ctx.Foos.Addobject(foo);
ctx.SaveChanges();
}
There is a perfect way to accomplish this in EF 4.0 by leveraging ObjectStateManager
First, you need to create a partial class for your ObjectContext and subscribe to
ObjectContext.SavingChanges Event. The best place to subscribe to this event is inside the OnContextCreated Method. This method is called by the context object’s constructor and the constructor overloads which is a partial method with no implementation:
partial void OnContextCreated() {
this.SavingChanges += Context_SavingChanges;
}
Now the actual code that will do the job:
void Context_SavingChanges(object sender, EventArgs e) {
IEnumerable<ObjectStateEntry> objectStateEntries =
from ose
in this.ObjectStateManager.GetObjectStateEntries(EntityState.Added
| EntityState.Modified)
where ose.Entity != null
select ose;
foreach (ObjectStateEntry entry in objectStateEntries) {
ReadOnlyCollection<FieldMetadata> fieldsMetaData = entry.CurrentValues
.DataRecordInfo.FieldMetadata;
FieldMetadata modifiedField = fieldsMetaData
.Where(f => f.FieldType.Name == "LastModifiedUser").FirstOrDefault();
if (modifiedField.FieldType != null) {
string fieldTypeName = modifiedField.FieldType.TypeUsage.EdmType.Name;
if (fieldTypeName == PrimitiveTypeKind.String.ToString()) {
entry.CurrentValues.SetString(modifiedField.Ordinal, Lookupuser());
}
}
}
}
Code Explanation:
This code locates any Added or Modified entries that have a LastModifiedUser property and then updates that property with the value coming from your custom Lookupuser() method.
In the foreach block, the query basically drills into the CurrentValues of each entry. Then, using the Where method, it looks at the names of each FieldMetaData item for that entry, picking up only those whose Name is LastModifiedUser. Next, the if statement verifies that the LastModifiedUser property is a String field; then it updates the field's value.
Another way to hook up this method (instead of subscribing to SavingChanges event) is by overriding the ObjectContext.SaveChanges Method.
By the way, the above code belongs to Julie Lerman from her Programming Entity Framework book.
EDIT for Self Tracking POCO Implementation:
If you have self tracking POCOs then what I would do is that I first change the T4 template to call the OnContextCreated() method. If you look at your ObjectContext.tt file, there is an Initialize() method that is called by all constructors, therefore a good candidate to call our OnContextCreated() method, so all we need to do is to change ObjectContext.tt file like this:
private void Initialize()
{
// Creating proxies requires the use of the ProxyDataContractResolver and
// may allow lazy loading which can expand the loaded graph during serialization.
ContextOptions.ProxyCreationEnabled = false;
ObjectMaterialized += new ObjectMaterializedEventHandler(HandleObjectMaterialized);
// We call our custom method here:
OnContextCreated();
}
And this will cause our OnContextCreated() to be called upon creation of the Context.
Now if you put your POCOs behind the service boundary, then it means that the ModifiedUserName must come with the rest of data from your WCF service consumer. You can either expose this
LastModifiedUser property to them to update or if it stores in another property and you wish to update LastModifiedUser from that property, then you can modify the 2nd code as follows:
foreach (ObjectStateEntry entry in objectStateEntries) {
ReadOnlyCollection fieldsMetaData = entry.CurrentValues
.DataRecordInfo.FieldMetadata;
FieldMetadata sourceField = fieldsMetaData
.Where(f => f.FieldType.Name == "YourPropertyName").FirstOrDefault();
FieldMetadata modifiedField = fieldsMetaData
.Where(f => f.FieldType.Name == "LastModifiedUser").FirstOrDefault();
if (modifiedField.FieldType != null) {
string fieldTypeName = modifiedField.FieldType.TypeUsage.EdmType.Name;
if (fieldTypeName == PrimitiveTypeKind.String.ToString()) {
entry.CurrentValues.SetString(modifiedField.Ordinal,
entry.CurrentValues[sourceField.Ordinal].ToString());
}
}
}
Hope this helps.
There is a nuget package for this now : https://www.nuget.org/packages/TrackerEnabledDbContext
Github: https://github.com/bilal-fazlani/tracker-enabled-dbcontext
Related
I have a problem with EF Core 5 that is really getting me down.
FYI, LazyLoadingProxies are used (something else that just gives me a headache, but well, different topic).
Information for the code below:
Service: A service per entity, contains all CRUD operations into the database and other methods if needed.
Workflow: Uses multiple services at once to perform certain operations (e.g. create product -> create product folder -> save product).
Problem:
I have an entity "Product" which contains the following update method which is used to update the properties of the entity with those of another object:
public override void Update(Product source)
{
// Properties
AnnualPrice = source.AnnualPrice;
...
// Relations
var sourceRelatedProductIds = source.RelatedWithProductIds.Where(x => x != Id);
if (sourceRelatedProductIds.Count() != 0)
{
RelatedWithProducts.Clear();
foreach (var relatedWithProduct in ctx.Set<Product>().Where(x => source.RelatedWithProductIds.Contains(x.Id)).AsNoTracking())
{
RelatedWithProducts.Add(relatedWithProduct);
}
}
var oldShortDescriptions = ShortDescriptions.ToList(); <--- EXCEPTION
ShortDescriptions.Clear();
foreach (var shortDescription in source.ShortDescriptions)
{
shortDescription.Id = oldShortDescriptions.FirstOrDefault(x => x.Culture == shortDescription.Culture)?.Id ?? 0;
ShortDescriptions.Add(shortDescription);
}
...
}
In the line with the arrow and "Exception", I get the following exception:
System.InvalidOperationException: 'The instance of entity type 'Product' cannot be tracked because another instance with the key value '{Id: 1}' is already being tracked. When attaching existing entities, ensure that only one entity instance with a given key value is attached.'
In itself, I understand what the exception is trying to tell me. My problem is that I can't find the reason for it anywhere. Because as far as I can tell, the product with ID 1 can't be tracked yet.
Of course, the problem is not in the update method, but before it, so here is the rest of the code.
ProductController.Update:
[HttpPut("update")]
public IActionResult Update(C.Product[] products)
{
if (!ModelState.IsValid)
{
return UnprocessableEntity(ModelState.Values.SelectMany(x => x.Errors));
}
var dbProducts = products.Select(ToDatabase).ToArray(); <--- Just converts the given client model into a Database model
var result = productWorkflow.Update(dbProducts); <--- Calls a workflow class, NOT the update method of the entity
return CoreToActionResultConverter.ToActionResult<Db.Product>(result);
}
ProductWorkflow.Update:
public ResultBase Update(params Product[] products)
{
var result = productService.AddOrUpdate(products); <--- This calls the Service CRUD AddOrUpdate method
if (result is not ServiceResult<Product>)
{
return result;
}
return new ServiceResult<Product>(ResultType.AddedOrUpdated);
}
ProductService.AddOrUpdate:
public virtual ResultBase AddOrUpdate(IEnumerable<TEntity> entities)
{
var currentEntities = new List<TEntity>();
foreach (var entity in entities)
{
var currentEntity = Get(entity.Id); <--- This line is the only one where I could imagine that it is already tracked here. The problem is only that it does not work ONLY with the workflow. If I call my AddOrUpdate method from the controller, which directly calls THIS method, it works (although this line is just executed the same way).
if (currentEntity == null)
{
currentEntity = Ctx.CreateProxy<TEntity>();
Ctx.Attach(currentEntity);
}
if (currentEntity != entity)
{
currentEntity.Update(entity);
}
currentEntities.Add(currentEntity);
}
Ctx.AddRange(currentEntities.Where(x => x.Id == 0));
Ctx.UpdateRange(currentEntities.Where(x => x.Id != 0));
try
{
Ctx.SaveChanges();
}
catch (DbUpdateException ex)
{
// Commented out the error handling to remove unnecessary things for the post
}
return new ServiceResult<TEntity>(ServiceResult.ResultType.AddedOrUpdated, currentEntities);
}
I found the problem and it was not on the line where the exception was thrown, but before.
In my Product.Update() method (the first code snippet), I get the Related Products by ID and add them to the list (Simply a Many to Many relationship, from Product <--> Product). When calling Update, I specified ID 1 in the RelatedProductIds, however the entity itself also has ID 1, so it references itself. I have now just fixed that by omitting the ID, if the same as the object itself.
This still doesn't explain why it works with a breakpoint, because it's still tracked in that case (or not tracked, since I'm using AsNoTracking(), but good).
I'm trying to apply the unit of work pattern as described in this blog, but have bumped into the following problem: If I inject the associated DbSet into the repo only, e.g.
public ArticleRepository(DbSet<Article> articles)
{
this.articles = articles;
}
then how do I update records or set their status to modified?
Before I used
public void Update(Article article)
{
this.context.Entry(article).State = EntityState.Modified;
}
but with the new approach I don't have access to DbContext anymore. Neither DbSet.Add nor DbSet.Attach will work here, so how can I update the object in the context?
System.Data.Entity.Migrations.IDbSetExtensions contains the IDbSet extension AddOrUpdate<TEntity>. This will update the entity.
Some people like the advantage of not knowing whether they are adding a new entity or changing an existing one.
However, if you really want an error if you are updating an item that is not added yet, take a look at the Source Code of IDbSetExtensions.AddOrUpdate
Here you can see that the function first checks if the item exists and depending on the result adds or updates it as follows:
var existing = set.SingleOrDefault
(Expression.Lambda<Func <TEntity, bool>> (matchExpression, new[]
{parameter}));
if (existing != null)
{ // entity exists: update it
foreach (var keyProperty in keyProperties)
{
keyProperty.Single().SetValue
(entity, keyProperty.Single().GetValue (existing, null), null);
}
internalSet.InternalContext.Owner.Entry(existing)
.CurrentValues.SetValues (entity);
}
else
{ // not existing entity: Add it
internalSet.Add(entity);
}
If you don't want the AddOrUpdate, but really only an update, consider Creating your own Extension method for IDbSet. See Extension Methods (C# Programming Guide)
I have a problem with refreshing the data from the database. See the code below. What I try to accomplish here is this: I create a ProductProtocol (p) and add one ProductProtocolContent to the list of ProductProtocolContents. Then I save the whole lot to the database. After that I simulate a change I make (the addition of another ProductProtocolContent ) that I want to undo and I want to test that after refreshing the object using my repository the
var p = new ProductProtocol { Name = "test1" };
p.ProtocolContents.Add(new ProductProtocolContent { ValidFrom = DateTime.Now, Value = "9_qwe2341" });
var r = RepositoryFactory.Create<ProductProtocol>(unitOfWork);
r.Add(p);
unitOfWork.Commit();
// add another ProductProtocolContent, p.ProductProtocolContents.Count == 2
p.ProtocolContents.Add(new ProductProtocolContent { ValidFrom = DateTime.Now, Value = "9_truf7461" });
// undo the changes to the object
r.Refresh(p);
// reload the ProductProtocolContents from the database
r.LoadPropertySet(p, pc => pc.ProtocolContents);
// p.ProductProtocolContents.Count should be 1
Assert.IsTrue(p.ProtocolContents.Count == 1);
My problem is that p.ProtocolContents.Count == 0 at the time Assert.IsTrue is called.
The implementation of r.LoadPropertySet() is as follows:
public void LoadPropertySet<Y>(T target, Expression<Func<T, ICollection<Y>>> spec) where Y : BaseBusinessObject
{
context.Entry(target).Collection(spec).CurrentValue.Clear();
context.Entry(target).Collection(spec).Load();
}
using Entity Framework 4 (using the ObjectContext instead of DbContext)I simply did this and that worked:
context.LoadProperty(target, spec, MergeOption.OverwriteChanges);
How would I do this using the DbSet, Must I convert back to ObjectContext or is there a equivalent for DbSet?
If you can't do it on DBContext, you can always cast to ObjectContext and use its functionality.
MSDN: DbContext wraps ObjectContext and exposes the most commonly used features of ObjectContext by using simplified and more intuitive APIs. You can access the underlying ObjectContext whenever you need to use features that are not supported by DbContext.
If I run the following code it throws the following error:
An entity object cannot be referenced by multiple instances of IEntityChangeTracker
public void Save(Category category)
{
using(var db = new NorthwindContext())
{
if(category.CategoryID == 0)
{
db.AddToCategorySet(category);
}
else
{
//category.RemoveTracker();
db.Attach(category);
}
db.SaveChanges();
}
}
The reason is of course that the category is sent from interface which we got from GetById method which already attached the EntityChangeTracker to the category object. I also tried to set the entity tracker to null but it did not update the category object.
protected void Btn_Update_Category_Click(object sender, EventArgs e)
{
_categoryRepository = new CategoryRepository();
int categoryId = Int32.Parse(txtCategoryId.Text);
var category = _categoryRepository.GetById(categoryId);
category.CategoryName = txtUpdateCategoryName.Text;
_categoryRepository.Save(category);
}
I'm still learning Entity Framework myself, but maybe I can help a little. When working with the Entity Framework, you need to be aware of how you're handling different contexts. It looks like you're trying to localize your context as much as possible by saying:
public void Save(Category category)
{
using (var db = new NorthwindContext())
{
...
}
}
... within your data access method. Did you do the same thing in your GetById method? If so, did you remember to detach the object you got back so that it could be attached later in a different context?
public Category GetById(int categoryId)
{
using (var db = new NorthwindContext())
{
Category category = (from c in db.Category where Category.ID == categoryId select c).First();
db.Detach(category);
}
}
That way when you call Attach it isn't trying to step on an already-attached context. Does that help?
As you pointed out in your comment, this poses a problem when you're trying to modify an item and then tell your database layer to save it, because once an item is detached from its context, it no longer keeps track of the changes that were made to it. There are a few ways I can think of to get around this problem, none of them perfect.
If your architecture supports it, you could expand the scope of your context enough that your Save method could use the same context that your GetById method uses. This helps to avoid the whole attach/detach problem entirely, but it might push your data layer a little closer to your business logic than you would like.
You can load a new instance of the item out of the new context based on its ID, set all of its properties based on the category that is passed in, and then save it. This costs two database round-trips for what should really only need one, and it isn't very maintainable.
You can dig into the context itself to mark the Category's properties as changed.
For example:
public void Save(Category category)
{
using (var db = new NorthwindContext())
{
db.Attach(category);
var stateEntry = db.ObjectStateManager.GetObjectStateEntry(category);
foreach (var propertyName in stateEntry.CurrentValues.DataRecordInfo.FieldMetadata.Select(fm => fm.FieldType.Name)) {
stateEntry.SetModifiedProperty(propertyName);
}
db.SaveChanges();
}
}
This looks a little uglier, but should be more performant and maintainable overall. Plus, if you want, you could make it generic enough to throw into an extension method somewhere so you don't have to see or repeat the ugly code, but you still get the functionality out of it.
I want to invoke a validation function inside the entities objects right before they are stored with ObjectContext#SaveChanges(). Now, I can keep track of all changed objects myself and then loop through all of them and invoke their validation methods, but I suppose an easier approach would be implement some callback that ObjectContext will invoke before saving each entity. Can the latter be done at all? Is there any alternative?
I've figured out how. Basically, we can intercept SavingChanges event of ObjectContext and loop through the newly added/modified entities to invoke their validation function. Here's the code I used.
partial void OnContextCreated()
{
SavingChanges += PerformValidation;
}
void PerformValidation(object sender, System.EventArgs e)
{
var objStateEntries = ObjectStateManager.GetObjectStateEntries(
EntityState.Added | EntityState.Modified);
var violatedRules = new List<RuleViolation>();
foreach (ObjectStateEntry entry in objStateEntries)
{
var entity = entry.Entity as IRuleValidator;
if (entity != null)
violatedRules.AddRange(entity.Validate());
}
if (violatedRules.Count > 0)
throw new ValidationException(violatedRules);
}
Well, you could do it that way, but it means that you're allowing your clients to directly access the ObjectContext, and, personally, I like to abstract that away, in order to make the clients more testable.
What I do is use the repository pattern, and do the validation when save is called on a repository.