Auditing data with Envers

A common business requirement in application development is auditing changes to stored entities. Which entities were added, modified, or deleted? Who made the change? Which properties were modified?

In this recipe, we'll use NHibernate Envers, which provides exactly that kind of auditing functionality.

Getting ready

Complete the Eg.Core model from Chapter 2, Models and Mappings.

How to do it…

  1. Create a new project named Eg.Envers.
  2. Copy the Eg.Core model and mappings from Chapter 2, Models and Mappings to this new project.
  3. Change the namespace and assembly references in the mappings to Eg.Envers.
  4. Install NHibernate Envers using NuGet Package Manager Console, by executing the following command:
    Install-Package NHibernate.Envers
    
  5. On the Product class, add the following attributes:
    public class Product : Entity
    {
      public virtual string Name { get; set; }
      public virtual string Description { get; set; }
    
      [Audited]
      public virtual Decimal UnitPrice { get; set; }
    }
  6. Create a new console application named Eg.Envers.Runner.
  7. Install the NHibernate, NHibernate Envers, and log4net packages to the Eg.Envers.Runner project using NuGet Package Manager Console, by running the following commands:
    Install-Package NHibernate
    Install-Package NHibernate.Envers
    Install-Package log4net
    
  8. Set up an App.config with the standard log4net and hibernate-configuration sections, just as we did in the Configuring NHibernate with App.config and Configuring NHibernate logging recipes in Chapter 1, The Configuration and Schema.
  9. In Program.cs, use the following code:
    class Program
    {
        static void Main()
        {
            XmlConfigurator.Configure();
    
            var cfg = new Configuration().Configure();
    
            cfg.IntegrateWithEnvers(
            new AttributeConfiguration());
    
            var sessionFactory = cfg.BuildSessionFactory();
    
            var schemaExport = new SchemaExport(cfg);
            schemaExport.Execute(true, true, false);
    
            using (var session = sessionFactory.OpenSession())
            using (var tx = session.BeginTransaction())
            {
                session.Delete("from Product");
                tx.Commit();
            }
    
            Guid productId;
    
            using (var session = sessionFactory.OpenSession())
            using (var tx = session.BeginTransaction())
            {
                productId = (Guid) session.Save(new Product
                {
                    Name = "A product",
                    Description = "Some product we sell",
                    UnitPrice = 143.73M
                });
                tx.Commit();
            }
    
            using (var session = sessionFactory.OpenSession())
            using (var tx = session.BeginTransaction())
            {
                var product = session.Get<Product>(productId);
                product.UnitPrice = 900M;
                tx.Commit();
            }
    
            using (var session = sessionFactory.OpenSession())
            using (session.BeginTransaction())
            {
                var reader = AuditReaderFactory.Get(session);
                var oldProduct = 
                reader.Find<Product>(productId, 1);
                Console.WriteLine(
                "Product price at revision 1 is " +
                   oldProduct.UnitPrice);
            }
        }
    }
  10. Build and run your program.
  11. You should see Product price at revision 1 is 143,73000.

How it works…

NHibernate Envers implements event listeners to intercept changes to entities. The changes are recorded in tables that Envers creates for each audited entity. These tables can be used to retrieve and query historical data without much effort.

Similar to source control systems, Envers has the concept of revision. One transaction is one revision (unless the transaction did not modify any audited entities). As revisions are global and have a revision number, you can query for various entities of that revision, retrieving a (partial) view of the database at that revision. You can find a revision number having a date, and the other way round, you can get the date at which a revision was committed.

In this recipe, we configured Envers using the attribute configuration. First, we mark the properties that we want to audit with the [Audit] attribute. Then we tell NHibernate that we want to integrate with Envers. If we want to audit all properties in an entity, you can mark the whole class with the [Audit] attribute, and if you need to exclude some properties from being audited you can exclude them by marking them with the [Exclude] attribute.

At the cfg.IntegrateWithEnvers(new AttributeConfiguration());line, Envers analyzes all mapped entities and creates additional metadata for auditing. It also registers the necessary event listeners.

We continue with saving an entity to the database. Later, we get this entity from a database and change the audited property UnitPrice.

At the end, we create an instance of IAuditReader. The IAuditReader interface provides an interface to query the audit data stored in a database. This interface is very rich, and you can perform complex queries in addition to just retrieving old entity states.

There's more…

NHibernate Envers also offers fluent configuration by code. To configure by code, just use the following code:

var enversConf = new FluentConfiguration();
enversConf.Audit<Product>()
  .Exclude(x => x.Name)
  .Exclude(x => x.Description);
cfg.IntegrateWithEnvers(enversConf);

As you can see, when using FluentConfiguration there is no ability to specify that we want to audit only one property in an Entity, so we need to exclude properties we do not want to audit.

More information about NHibernate Envers is available at https://envers.bitbucket.io.

See also

  • Creating an audit-event listener
..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.133.158.36