left-icon

Entity Framework Code First Succinctly®
by Ricardo Peres

Previous
Chapter

of
A
A
A

CHAPTER 7

Handling Events

Handling Events


Saving and Loading Events

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

ObjectMaterialized

Raised when an entity is materialized, as the result of the execution of a query.

SavingChanges

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:

  • Inheriting classes can override the OnObjectMaterialized and OnSavingChanges methods.
  • Interested parties can subscribe to the SavingChanges and ObjectMaterialized 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:

  • All IImmutable entities loaded from queries (ObjectMaterialized) are immediately detached from the context.
  • Dirty IImmutable entities about to be saved (SavingChanges) are set to detached, so they aren’t saved.

In another case, there is auditing changes made to an entity. For that we want to record:

  • The user who first created the entity.
  • When it was created.
  • The user who last modified the entity.
  • When it was last updated.

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:

  • List all IAuditable entities that are new and set their CreatedAt and CreatedBy properties.
  • At the same time, all modified entities have their UpdatedAt and UpdatedBy properties set.

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

}

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.

Scroll To Top
Disclaimer
DISCLAIMER: Web reader is currently in beta. Please report any issues through our support system. PDF and Kindle format files are also available for download.

Previous

Next



You are one step away from downloading ebooks from the Succinctly® series premier collection!
A confirmation has been sent to your email address. Please check and confirm your email subscription to complete the download.