Friday, March 31, 2017

CompileTimeAOP.TransactionAwareness

Transaction Awareness Framework

nuget package: https://www.nuget.org/packages/CompileTimeAOP.TransactionAwareness/

Transaction Awareness Framework is implemented by CompileTimeWeaver.Fody to gain the best performance. It removes the burden of NHibernate developers of managing sessions and transactions, and domain model validations.

1. Transaction-aware Object
A Transaction-aware Object is a .net object that automatically execute within the scope of a NHibernate session, it participates in an existing or ongoing session, or be the root of a new session. Similar to enterprise component in .net or session bean in EJB, a Transaction-aware Object is a business logic object accessing domain objects (or entity objects). The declarative transaction aware attribute specifies how an object participates in a session, and is configured programmatically. The following example shows a Transaction-aware c# class:

    [TransactionAware]
    public class MyClass: IDisposable
    {
        public void TravelAllPersons()
        {
            var session = TransactionContext.CurrentSession;

            var persons = session.Query<Person>();

            foreach (var person in persons)
            {
                Trace.WriteLine(person.Id + " - " + person.FirstName);
            }
        }

        public void Dispose()

        {
            //do nothing
        }
    }

You see there is nothing special except the C# class is decorated by TransactionAwareAttribute.

2. Context Session

    A transaction-aware object is always in a context session, no matter it is newly created or is propagated from caller.
    §  Owner of context session
         The transaction-aware object that creates new context session is the owner of the context session.
    §  Transaction-aware methods
         Transaction-aware methods are non-static methods decorated by TransactionAwareAttribute, or the non-static methods of a class decorated by TransactionAwareAttribute. Transaction-aware methods can enter session context and access context session.
    §  Life-cycle of context session
          A context session is created for the owner when the owner is created or when first time a transaction-aware method of its owner is called, depending on TransactionAwarePoint, and it doesn't disappear until its owner is disposed. (see TransactionAwarePoint below)
    §  Attaching moment
         If TransactionAwarePoint is OnCall, a transaction-aware object is attached to a context session when first time any one of its transaction-aware methods is called from any thread. If TransactionAwarePoint is OnConstructor, the object is attached to a context session when it is created. The attaching relation doesn't break or change once established.

    A transaction-aware object can access context session with static property TransactionContext.CurrentSession or static method TransactionContext.GetCurrentSession(string dbname), and then take full advantage of NHibernate powers to access database, but a transaction-aware object is not allowed to close context session explicitly. Below is correct code snippet to access database through context session:
        ISession session = TransactionContext.CurrentSession;
        var user1 = session.Get<User>(1);
        user1.Name = "Simon Lu";
        session.Update(users);

    3. Requirements of a Transaction-aware Object

      A transaction-aware object is an instance of an ordinary .net class meeting of few simple requirements:
      §  Decorated with TransactionAwareAttribute.
      §  Implement IDisposable interface, this method must exist event though it is empty.
      §  Call TransactionContext methods/properties in transaction-aware methods only.

      When applying the transaction attribute, you can set TransactionOption property and TransactionAwarePoint property. The following table lists and describes each constructor variation.
      TransactionOption
      Description
      Required (default)
      Indicates that the object requires a session. It runs in the scope of an existing session, if one exists. If no session exists, the object starts one.
      RequiresNew
      Indicates that the object requires a session and a new session is started for each request.
      TransactionAwarePoint
      Description
      OnCall(default)
      Indicates that the object attaches to a session when any of its virtual method is first called.
      OnConstructor
      Indicates that the object attaches to a session right after it is created.

      Example:
        [TransactionAware(TransactionAwareOption = TransactionAwareOption.Required,
                          TransactionAwarePoint = TransactionAwarePoint.OnConstructor)]
        public class MyClass: IDisposable
          {
              //...
          }

      4. Object Injection

        A Transaction-aware object can inject objects as its properties or fields right after it is created by declare the properties/fields with InjectedOnCreatedAttribute, and transparently dispose them when it is disposed. This open the door for developers to inject Transaction-aware objects into the other Transaction-aware object so that they can share the same database session.  Below is an example class, and instances of EntityRepository is created and injected as private field _entityRepository, and it is disposed automatically even though the Dispose method body is empty.
            [TransactionAware]
            public class TransactionAwareUnitOfWork : IDisposable
            {
                [InjectedOnCreated]
                private EntityRepository _entityRepository;
                ...
                public void Dispose()
                {
                    //Transaction Awareness framework will dispose all injected objects
                }
            }

        5. Voting in an Automatic Transaction

          Transaction-aware methods of transaction-aware objects can vote to commit or abort their current transaction. The absence of an explicit vote in a method means the database update in the method accumulate the context transaction without commit or rollback. If there is not explicit vote during the life-cycle of the owner of a context session, the context transaction is rollback when the owner is disposed, other than that, a context transaction is neither committed nor aborted until a transaction-aware method explicitly vote.
          Explicit voting also allows your class to abort a transaction if it encounters a significant error. Again, you can improve your application's performance by catching a fatal error early in the transaction, ending the transaction, and releasing resources. There are two options to issue explicit voting: SetComplete/SetAbort or AutoCompleteAttribute.

          Using SetAbort and SetComplete

          You can use TransactionContext class, which exposes the SetComplete or SetAbort methods, to explicitly commit or abort a transaction. SetComplete indicates that your object votes to commit its work; SetAbort indicates that your object encountered a problem and votes to abort the ongoing transaction, a single abort vote from any object participating in the transaction causes the entire transaction to fail.
          The following code fragment shows the SetAbort and SetComplete methods in use.
          //Try to do something crucial to the transaction in progress.
          if( !DoSomeWork() )
          {
            //Something goes wrong.
            TransactionContext.SetAbort();
          }
          else
          {
            //All goes well.
            TransactionContext.SetComplete();
          }

          Using AutoComplete

          The AutoCompleteAttribute causes a transaction-aware method participating in a transaction to vote in favor of completing the transaction if the method returns normally. If the method call throws an exception, the transaction is aborted. To use this feature, insert the attribute before the class method. If you add the attribute to an interface method, the common language run-time ignores it. The following code fragment shows the placement of the attribute on a transaction-aware class.
          [TransactionAware]
          public class Account
          {
              [AutoComplete]
              public virtual void Debit(int amount)
              {
                  // Do some database work. Any exception thrown here aborts the transaction;
                  // otherwise, transaction commits.
              }
          }

          6. Practice

            Prerequisite: domain classes and mapping classes are ready.

            Step 1) Download and add NuGet package CompileTimeAOP.TransactionAwareness to your c# project.

            Step 2) Add a Transaction-aware class to your project

                using CompileTimeAOP.Transaction;

                [TransactionAware]

                public class MyClass: IDisposable
                {
                    public void TravelAllPersons()
                    {
                        var session = TransactionContext.CurrentSession;

                        var persons = session.Query<Person>();
                        foreach (var person in persons)
                        {
                            Trace.WriteLine(person.Id + " - " + person.FirstName);
                        }
                    }

                    public void Dispose()
                    {
                        //do nothing
                    }


                }

            Step 3) Add this code snippet into application startup code to setup database connection
                using CompileTimeAOP.Transaction;

                DbSessionFactory.Create("your_database_name",
                                                "hibernate.yourdb.config",
                                                fluentCfg =>
                                                {
                                                    fluentCfg.Mappings(m =>
                                                                       {
                                                                           m.FluentMappings.AddFromAssemblyOf<YourFluentNHibernateMapClass>();
                                                                       });
                                                    fluentCfg.ExposeConfiguration(cfg => new NHibernate.Tool.hbm2ddl.SchemaUpdate(cfg).Execute(false, true));
                                                });
            Step 4) Use Transaction-aware class
                        using (var testObj = new MyClass())
                        {
                            testObj.TravelAllPersons();

                        }

            7. Multiple Database

              Sometimes it is necessary for a Transaction-aware object to access two databases in one method, for example, an online shopping cart object updates transaction database and access customer service database in its PlaceOrder () method. Transaction-awareness framework let it do it easily by decorating the PlaceOrder method with two TransactionAwareAttribute with different database names as constructor parameter. See it below:
                  [TransactionAware("TransactionDb"]
                  public class OnlineShoppingStore: IDisposable
                  {
                      [TransactionAware("CustomerDb"]
                      [AutoComplete]
                      public void PlaceOrder(OrderDto order)
                      {
                          var customerDbSession = TransactionContext.GetCurrentSession("CustomerDb");
                          var customer = customerDbSession.Query<CustomerEntity>(order.CustomerId);
                          ...
                          var transDbSession = TransactionContext.GetCurrentSession("TransactionDb");
                          transDbSession.Save(new OrderEntity(){...});
                      }
                      ...
                  }

              Notes:
              • use TransactionContext.GetCurrentSession(string dbName) to get database sessions from different database where multiple databases exist.
              • Call DbSessionFactory.Create() for each database with different database name to setup connection.

              8. Fluent Validation

                Transaction-aware framework uses FluentValidation to keep the integrity of data model whenever there is database update.

                Usage:
                • Create a C# project to contain all the FluentValidation validators.
                • Add all validators to validate entity classes, such as the PersonValidator class below to validate Person entity class:
                    using FluentValidation;

                    public class PersonValidator : AbstractValidator<Person>
                    {
                        public PersonValidator()
                        {
                            RuleFor(x => x.Id).NotEmpty();
                            RuleFor(x => x.FirstName).NotNull().Length(1, 10);
                        }
                    }
                • Add validator assembly to the call to DbSessionFactory.Create() as the last parameter, the framework validates entity objects once there is update on them, and throws FluentValidation.ValidationException exception if there is any break to validation rules.
                    DbSessionFactory.Create("your_database_name",
                                                    "hibernate.yourdb.config",
                                                    fluentCfg =>
                                                    {
                                                        fluentCfg.Mappings(m =>
                                                                           {
                                                                               m.FluentMappings.AddFromAssemblyOf<YourFluentNHibernateMapClass>();
                                                                           });
                                                        fluentCfg.ExposeConfiguration(cfg => new NHibernate.Tool.hbm2ddl.SchemaUpdate(cfg).Execute(false, true));
                                                    },
                                                    typeof (PersonValidator).Assembly);


                No comments:

                Post a Comment