CHAPTER 7
Events are .NET’s implementation of the Observer design pattern, used to decouple a class from other classes that are interested in changes that may occur in it. While Entity Framework Code First does not directly expose any events, because it sits on top of the “classic” Entity Framework, it is very easy to make use of the events it exposes.
The Entity Framework’s ObjectContext class exposes two events.
Table 6: ObjectContext events
Event | Purpose |
Raised when an entity is materialized, as the result of the execution of a query. | |
Raised when the context is about to save its attached entities. |
Why do we need this? Well, we may want to perform some additional tasks just after an entity is loaded from the database or just before it is about to be saved or deleted.
One way to bring these events to Code First land is as follows.
public class ProjectsContext : DbContext { public ProjectsContext() { this.AddEventHandlers(); }
//raised when the context is about to save dirty entities public event EventHandler<EventArgs> SavingChanges; //raised when the context instantiates an entity as the result of a query public event EventHandler<ObjectMaterializedEventArgs> ObjectMaterialized;
public void AddEventHandlers() { //access the underlying ObjectContext var octx = (this as IObjectContextAdapter).ObjectContext; //add local event handlers octx.SavingChanges += (s, e) => this.OnSavingChanges(e); octx.ObjectMaterialized += (s, e) => this.OnObjectMaterialized(e); } protected virtual void OnObjectMaterialized(ObjectMaterializedEventArgs e) { var handler = this.ObjectMaterialized;
if (handler != null) { //raise the ObjectMaterialized event handler(this, e); } }
protected virtual void OnSavingChanges(EventArgs e) { var handler = this.SavingChanges;
if (handler != null) { //raise the SavingChanges event handler(this, e); } } } |
So, we have two options for handling the ObjectMaterialized and the SavingChanges events:
Now imagine this: you want to define a marked interface such as IImmutable that, when implemented by an entity, will prevent it from ever being tracked by Entity Framework. Here’s a possible solution for this scenario.
//a simple marker interface public interface IImmutable { } public class Project : IImmutable { /* … */ } public class ProjectsContext : DbContext { protected virtual void OnObjectMaterialized(ObjectMaterializedEventArgs e) { var handler = this.ObjectMaterialized;
if (handler != null) { handler(this, e); } //check if the entity is meant to be immutable if (e.Entity is IImmutable) { //if so, detach it from the context this.Entry(e.Entity).State = EntityState.Detached; } } protected virtual void OnSavingChanges(EventArgs e) { var handler = this.SavingChanges;
if (handler != null) { handler(this, e); } //get all entities that are not unchanged (added, deleted or modified) foreach (var immutable in this.ChangeTracker.Entries() .Where(x => x.State != EntityState.Unchanged && x.Entity is IImmutable).Select(x => x.Entity).ToList()) { //set the entity as detached this.Entry(e.Entity).State = EntityState.Detached; } } } |
Very quickly, what it does is:
In another case, there is auditing changes made to an entity. For that we want to record:
We will start by defining a common auditing interface, IAuditable, where these auditing properties are defined, and then we will provide an appropriate implementation of OnSavingChanges.
//an interface for the auditing properties public interface IAuditable { String CreatedBy { get; set; }
DateTime CreatedAt { get; set; }
String UpdatedBy { get; set; }
DateTime UpdatedAt { get; set; } } public class Project : IAuditable { /* … */ } public class ProjectsContext : DbContext { protected virtual void OnSavingChanges(EventArgs e) { var handler = this.SavingChanges;
if (handler != null) { handler(this, e); } foreach (var auditable in this.ChangeTracker.Entries() .Where(x => x.State == EntityState.Added).Select(x => x.Entity).OfType<IAuditable>()) { auditable.CreatedAt = DateTime.Now; auditable.CreatedBy = Thread.CurrentPrincipal.Identity.Name; }
foreach (var auditable in this.ChangeTracker.Entries() .Where(x => x.State == EntityState.Modified).Select(x => x.Entity) .OfType<IAuditable>()) { auditable.UpdatedAt = DateTime.Now; auditable.UpdatedBy = Thread.CurrentPrincipal.Identity.Name; } } } |
On the OnSavingChanges method we:
Another typical use for the SavingChanges event is to generate a value for the primary key when IDENTITY cannot be used. In this case, we need to fetch this next value from somewhere, such as a database sequence, function, or table, and assign it to the identifier property.
//an interface for accessing the identifier property of entities that require explicit identifier assignments public interface IHasGeneratedIdentifier { Int32 Identifier { get; set; } } public class ProjectsContext : DbContext { protected virtual void OnSavingChanges(EventArgs e) { var handler = this.SavingChanges;
if (handler != null) { handler(this, e); } foreach (var entity in this.ChangeTracker.Entries() .Where(x => x.State == EntityState.Added).Select(x => x.Entity) .OfType<IHasGeneratedIdentifier>()) { //call some function that returns a valid identifier entity.Identifier = this.Database.SqlQuery<Int32>("EXEC GetNextId()"); } } } |
Tip: How you implement the GetNextId function is up to you.