Pull to refresh

Repository Pattern via CSLA .NET

Reading time 24 min
Views 6.5K
Создание систем с низкой связанностью (Low Coupling) между модулями обеспечивает множество преимуществ при разработке ПО. В приложениях, написанных с использованием каркаса CSLA .NET, применение стандартных шаблонов для разрыва зависимостей не всегда может быть очевидно.

В данной статье будет рассмотрен вариант отделения слоя доступа к данным (Data Access Layer, DAL) от слоя бизнес логики (Business Layer) при помощи шаблона Repository и описан наиболее распространенный способ внедрения зависимостей (Depency Injection) в бизнес-объекты CSLA .NET. Используется CSLA версии 4.1.

Пример


Итак, пусть у нас есть корневой бизнес-объект Person. У данного бизнес-объекта есть несколько простых свойств из предметной области, дочерняя коллекция объектов Orders и дочерний объект Address:

//Person.cs
[Serializable]
public sealed partial class Person : BusinessBase<Person> {
   private Person( ) { }

   public static Person NewPerson( ) {
      return DataPortal.Create<Person>( );
   }

   public static Person GetPerson( int personId ) {
      return DataPortal.Fetch<Person>( new SingleCriteria<Person, int>( personId ) );
   }

   public static void RemovePerson( int personId ) {
      DataPortal.Delete<Person>( new SingleCriteria<Person, int>( personId ) );
   }

   private static readonly PropertyInfo<int> IdProperty = RegisterProperty<int>( c => c.Id );

   public int Id {
      get { return GetProperty( IdProperty ); }
      private set { LoadProperty( IdProperty, value ); }
   }

   public static readonly PropertyInfo<string> FirstNameProperty = 
           RegisterProperty<string>( c => c.FirstName );

   public string FirstName {
      get { return GetProperty( FirstNameProperty ); }
      set { SetProperty( FirstNameProperty, value ); }
   }

   public static readonly PropertyInfo<string> SecondNameProperty = 
           RegisterProperty<string>( c => c.SecondName );

   public string SecondName {
      get { return GetProperty( SecondNameProperty ); }
      set { SetProperty( SecondNameProperty, value ); }
   }

   public static readonly PropertyInfo<int> AgeProperty = RegisterProperty<int>( c => c.Age );

   public int Age {
      get { return GetProperty( AgeProperty ); }
      set { SetProperty( AgeProperty, value ); }
   }

   public static readonly PropertyInfo<string> CommentProperty = 
           RegisterProperty<string>( c => c.Comment );

   public string Comment {
      get { return GetProperty( CommentProperty ); }
      set { SetProperty( CommentProperty, value ); }
   }

   public static readonly PropertyInfo<Orders> OrdersProperty = 
           RegisterProperty<Orders>( c => c.Orders,
                       RelationshipTypes.Child | RelationshipTypes.LazyLoad );

   public Orders Orders {
      get {
         if ( !FieldManager.FieldExists( OrdersProperty ) ) {
            Orders = Orders.NewOrders( );
         }
         return GetProperty( OrdersProperty );
      }
      private set {
         LoadProperty( OrdersProperty, value );
         OnPropertyChanged( OrdersProperty );
      }
   }

   public static readonly PropertyInfo<Address> AddressProperty = 
           RegisterProperty<Address>( c => c.Address,
                       RelationshipTypes.Child | RelationshipTypes.LazyLoad);

   public Address Address {
      get {
         if ( !FieldManager.FieldExists( AddressProperty ) ) {
            Address = Address.NewAddress( );
         }
         return GetProperty( AddressProperty );
      }
      private set {
         LoadProperty( AddressProperty, value );
         OnPropertyChanged( AddressProperty );
      }
   }
}

Код дочерних классов
//Orders.cs    
[Serializable]
public sealed partial class Orders : BusinessListBase<Orders, Order> {
   private Orders( ) { }

   public static Orders NewOrders( ) {
      return DataPortal.CreateChild<Orders>( );
   }
}

//Order.cs
[Serializable]
public sealed partial class Order : BusinessBase<Order> {
   private Order( ) { }

   public static Order NewOrder( ) {
      return DataPortal.CreateChild<Order>( );
   }

   public static readonly PropertyInfo<int> IdProperty = RegisterProperty<int>( c => c.Id );

   public int Id {
      get { return GetProperty( IdProperty ); }
      set { LoadProperty( IdProperty, value ); }
   }

   public static readonly PropertyInfo<string> DescriptionProperty = 
           RegisterProperty<string>( c => c.Description );

   public string Description {
      get { return GetProperty( DescriptionProperty ); }
      set { SetProperty( DescriptionProperty, value ); }
   }
}

//Address.cs
[Serializable]
public partial class Address : BusinessBase<Address> {
   private Address( ) { }

   public static Address NewAddress( ) {
      return DataPortal.CreateChild<Address>( );
   }

   private static readonly PropertyInfo<int> IdProperty = RegisterProperty<int>( c => c.Id );

   private int Id {
      get { return GetProperty( IdProperty ); }
      set { LoadProperty( IdProperty, value ); }
   }

   public static readonly PropertyInfo<string> FirstAddressProperty = 
           RegisterProperty<string>( c => c.FirstAddress );

   public string FirstAddress {
      get { return GetProperty( FirstAddressProperty ); }
      set { SetProperty( FirstAddressProperty, value ); }
   }

   public static readonly PropertyInfo<string> SecondAddressProperty = 
           RegisterProperty<string>( c => c.SecondAddress );

   public string SecondAddress {
      get { return GetProperty( SecondAddressProperty ); }
      set { SetProperty( SecondAddressProperty, value ); }
   }
}


Получившаяся диаграмма классов представлена на рисунке:



Обратите внимание, что бизнес-классы помечены модификатором partial, и весь код доступа к данным перенесен в другие части классов для улучшения читаемости. Давайте теперь рассмотрим код доступа к DAL.

Стандартное взаимодействие с DAL


Сперва обратимся к коду, относящемуся к созданию объекта Person. Как видим, у класса определен единственный конструктор без параметров. За создание и извлечение объекта отвечают фабричные методы, которые обращаются к клиентскому порталу данных CSLA. Клиентский портал данных, в свою очередь, обращается к серверному порталу данных. Последний обращается через рефлексию к закрытым методам DataPortal_Create или DataPortal_Fetch класса Person – для создания или извлечения объекта соответственно:




Для вставки, обновления и удаления объекта Person действует та же схема, но используются методы DataPortal_Insert, DataPortal_Update и DataPortal_DeleteSelf соответственно:



Напомню, что при этом метод Save (инфраструктурный метод CSLA) возвращает фактически новый бизнес-объект, созданный на серверном портале данных, именно поэтому следует обновлять все ссылки на сохраненный объект в клиентском коде (концепция т.н. мобильных бизнес-объектов, Mobile Object Pattern):



Наконец, для форсированного удаления используется метод DataPortal_Delete:



Таким образом, в методах DataPortal_XYZ находится вся логика доступа к данным – не только для создания и извлечения данных, но и для обновления и удаления объекта Person в DAL. В этих методах мы видим специфичную логику, которая обычно используется для доступа к данным: код SQL-запросов, соединение с базой данных, создание транзакций для обновления дочерних объектов и т.д.

//Person.Server.cs
public partial class Person {
   public static readonly PropertyInfo<object> LastChangedProperty = 
           RegisterProperty<object>( c => c.LastChanged );

   public object LastChanged {
      get { return ReadProperty( LastChangedProperty ); }
      private set { LoadProperty( LastChangedProperty, value ); }
   }

   protected override void DataPortal_Create( ) {
      BusinessRules.CheckRules( );
   }

   private void DataPortal_Fetch( SingleCriteria<Person, int> idCriteria ) {
      const string query = @"SELECT id 
                                   ,first_name
                                   ,second_name
                                   ,age
                                   ,comment
                                   ,last_changed
                                   ,address_id
                                   ,first_address
                                   ,second_address
                               FROM All_persons
                              WHERE id = :p_id";
       using ( var manager = ConnectionManager<OracleConnection>.GetManager( "CSLAPROJECT" ) ) {
          using ( var command = manager.Connection.CreateCommand( ) ) {
             command.CommandType = CommandType.Text;
             command.CommandText = query;
             command.Parameters.AddWithValue( "p_id", idCriteria.Value );
             using ( var reader = 
                          new SafeDataReader( command.ExecuteReader( CommandBehavior.SingleRow ) ) ) {
                if ( reader.Read( ) ) {
                   FetchFromReader( reader );
                }
             }
          }
          LoadProperty( OrdersProperty, Orders.GetOrders( this ) ); 
      }
   }

   private void FetchFromReader( SafeDataReader reader ) {
      LoadProperty( FirstNameProperty, reader.GetString( "first_name" ) );
      LoadProperty( SecondNameProperty, reader.GetString( "second_name" ) );
      LoadProperty( CommentProperty, reader.GetString( "comment" ) );
      LoadProperty( AddressProperty, Address.GetAddress( reader ) );
      Id = reader.GetInt32( "Id" );
      LastChanged = reader[ "last_changed" ];
   }

   protected override void DataPortal_Insert( ) {
      using ( var manager = ConnectionManager<OracleConnection>.GetManager( "CSLAPROJECT" ) ) {
         using ( var transaction = manager.Connection.BeginTransaction( ) ) {
            try {
               using ( var command = manager.Connection.CreateCommand( ) ) {
                  command.CommandType = CommandType.StoredProcedure;
                  command.CommandText = "csla_project.add_person";
                  var personParameters = GetPersonParameters( );
                  command.Parameters.AddRange( personParameters );
                  command.Transaction = transaction;
                  command.ExecuteNonQuery( );
                  Id = ( int )command.Parameters[ "p_id" ].Value;
                  LastChanged = command.Parameters[ "p_last_changed" ];
               }
               FieldManager.UpdateChildren( this, transaction );
               transaction.Commit( );
            } 
            catch {
               transaction.Rollback( );
               throw;
            }
         }
      }
   }

   protected override void DataPortal_Update( ) {
      using ( var manager = ConnectionManager<OracleConnection>.GetManager( "CSLAPROJECT" ) ) {
         using ( var transaction = manager.Connection.BeginTransaction( ) ) {
            try {
               using ( var command = manager.Connection.CreateCommand( ) ) {
                  command.CommandType = CommandType.StoredProcedure;
                  command.CommandText = "csla_project.add_person";
                  var personParameters = GetPersonParameters( );
                  command.Parameters.AddRange( personParameters );
                  command.Transaction = transaction;
                  command.ExecuteNonQuery( );
                  LastChanged = command.Parameters[ "p_last_changed" ];
               }
               FieldManager.UpdateChildren( this, transaction );
               transaction.Commit( );
            } 
            catch {
               transaction.Rollback( );
               throw;
            }
         }
      }
   }

   private void DataPortal_Delete( SingleCriteria<Person, int> idCriteria  ) {
      DoDelete( idCriteria.Value );
   }

   protected override void DataPortal_DeleteSelf( ) {
      DoDelete( Id );
   }

   private void DoDelete( int personId ) {
      using ( var manager = ConnectionManager<OracleConnection>.GetManager( "CSLAPROJECT" ) ) {
         using ( var transaction = manager.Connection.BeginTransaction( ) ) {
            try {
               using ( var command = manager.Connection.CreateCommand( ) ) {
                  command.CommandType = CommandType.StoredProcedure;
                  command.CommandText = "csla_project.delete_person";
                  command.Parameters.AddWithValue( "p_id", personId );
                  command.Transaction = transaction;
                  command.ExecuteNonQuery( );
               }
               transaction.Commit( );
            } 
            catch {
               transaction.Rollback( );
               throw;
            }
         }
      }
   }

   private OracleParameter[] GetPersonParameters( ) {
      return new[] {
         new OracleParameter( "p_id", Id ) {Direction = ParameterDirection.InputOutput},
         new OracleParameter( "p_first_name", FirstName ),
         new OracleParameter( "p_second_name", SecondName ),
         new OracleParameter( "p_age", Age ),
         new OracleParameter( "p_comment", Comment ),
         new OracleParameter( "p_last_changed", OracleType.Int32 ) {Direction = ParameterDirection.Output}
      };
   }
}

Код доступа к DAL дочерних объектов Person аналогичен, за исключением того, что при обновлении дочерних объектов Person передает порталу данных ссылку на самого себя и экземпляр транзакции ADO .NET, поэтому соответствующие методы DataPortal_XYZ дочерних объектов содержат дополнительные аргументы.

Код дочерних классов:
//Orders.Server.cs
public partial class Orders {
   internal static Orders GetOrders( Person person ) {
      return DataPortal.FetchChild<Orders>( person );
   }

   private void Child_Fetch( Person person ) {
      const string query = @"SELECT id 
                                   ,description
                               FROM All_orders
                              WHERE person_id = :p_person_id";
      using ( var manager = ConnectionManager<OracleConnection>.GetManager( "CSLAPROJECT" ) ) {
         using ( var command = manager.Connection.CreateCommand( ) ) {
            command.CommandType = CommandType.Text;
            command.CommandText = query;
            command.Parameters.AddWithValue( "p_person_id", person.Id );
            using ( var reader = 
                         new SafeDataReader( command.ExecuteReader( CommandBehavior.SingleRow ) ) ) {
               RaiseListChangedEvents = false;
               while ( reader.Read( ) ) {
                  Add( Order.GetOrder( reader ) );
               }
               RaiseListChangedEvents = true;
            }
         }
      }
   }

   private void Child_Update( Person person, OracleTransaction transaction ) {
      base.Child_Update( person, transaction );
   }
}


//Order.Server.cs
public partial class Order {

   internal static Order GetOrder( SafeDataReader reader ) {
      return DataPortal.FetchChild<Order>( reader );
   }

   private void Child_Fetch( SafeDataReader reader ) {
      LoadProperty( IdProperty, reader.GetInt32( "id" ) );
      LoadProperty( DescriptionProperty, reader.GetString( "description" ) );
   }

   private void Child_Insert( Person person, OracleTransaction transaction ) {
      using ( var manager = ConnectionManager<OracleConnection>.GetManager( "CSLAPROJECT" ) ) {
         using ( var command = manager.Connection.CreateCommand( ) ) {
            command.CommandType = CommandType.StoredProcedure;
            command.CommandText = "csla_project.add_order";
            command.Transaction = transaction;
            command.Parameters.AddRange( GetParameters( ) );
            command.Parameters.AddWithValue( "person_id", person.Id );
            command.ExecuteNonQuery( );
            Id = ( int )command.Parameters[ "p_id" ].Value;
         }
      }
   }

   private void Child_Update( Person person, OracleTransaction transaction ) {
      using ( var manager = ConnectionManager<OracleConnection>.GetManager( "CSLAPROJECT" ) ) {
         using ( var command = manager.Connection.CreateCommand( ) ) {
            command.CommandType = CommandType.StoredProcedure;
            command.CommandText = "csla_project.edit_order";
            command.Transaction = transaction;
            command.Parameters.AddRange( GetParameters( ) );
            command.Parameters.AddWithValue( "person_id", person.Id );
            command.ExecuteNonQuery( );
         }
      }
   }

   private void Child_DeleteSelf( Person person, OracleTransaction transaction ) {
      using ( var manager = ConnectionManager<OracleConnection>.GetManager( "CSLAPROJECT" ) ) {
         using ( var command = manager.Connection.CreateCommand( ) ) {
            command.CommandType = CommandType.StoredProcedure;
            command.CommandText = "csla_project.remove_order";
            command.Transaction = transaction;
            command.Parameters.AddWithValue( "p_person_id", person.Id );
            command.Parameters.AddWithValue( "p_order_id", Id );
            command.ExecuteNonQuery( );
         }
      }
   }

   private OracleParameter[] GetParameters( ) {
      return new[] {
         new OracleParameter( "p_id", Id ) {Direction = ParameterDirection.InputOutput},
         new OracleParameter( "p_description", Description )
      };
   }
}


//Address.Server.cs
public sealed partial class Address {
   internal static Address GetAddress( SafeDataReader reader ) {
      return DataPortal.Fetch<Address>( reader );
   }

   private void Child_Fetch( SafeDataReader reader ) {
      using ( BypassPropertyChecks ) {
         LoadProperty( IdProperty, reader.GetInt32( "address_id" ) );
         LoadProperty( FirstAddressProperty, reader.GetString( "first_address" ) );
         LoadProperty( SecondAddressProperty, reader.GetString( "second_address" ) );
      }
   }

   private void Child_Insert( Person person, OracleTransaction transaction ) {
      using ( var manager = ConnectionManager<OracleConnection>.GetManager( "CSLAPROJECT" ) ) {
         using ( var command = manager.Connection.CreateCommand( ) ) {
            command.CommandType = CommandType.StoredProcedure;
            command.CommandText = "csla_project.add_address";
            command.Parameters.AddRange( GetParameters( ) );
            command.Parameters.AddWithValue( "p_person_id", person.Id );
            command.Transaction = transaction;
            command.ExecuteNonQuery( );
            Id = ( int )command.Parameters[ "p_id" ].Value;
         }
      }
   }

   private void Child_Update( Person person, OracleTransaction transaction ) {
      using ( var manager = ConnectionManager<OracleConnection>.GetManager( "CSLAPROJECT" ) ) {
         using ( var command = manager.Connection.CreateCommand( ) ) {
            command.CommandType = CommandType.StoredProcedure;
            command.CommandText = "csla_project.edit_address";
            command.Parameters.AddRange( GetParameters( ) );
            command.Parameters.AddWithValue( "p_person_id", person.Id );
            command.Transaction = transaction;
            command.ExecuteNonQuery( );
         }
      }
   }

   private void Child_DeleteSelf( Person person, OracleTransaction transaction ) {
      using ( var manager = ConnectionManager<OracleConnection>.GetManager( "CSLAPROJECT" ) ) {
         using ( var command = manager.Connection.CreateCommand( ) ) {
            command.CommandType = CommandType.StoredProcedure;
            command.CommandText = "csla_project.remove_address";
            command.Parameters.AddWithValue( "p_address_id", Id );
            command.Parameters.AddWithValue( "p_person_id", person.Id );
            command.Transaction = transaction;
            command.ExecuteNonQuery( );
         }
      }
   }

   private OracleParameter[] GetParameters( ) {
      return new[] {
         new OracleParameter( "p_id", Id ) {Direction = ParameterDirection.InputOutput},
         new OracleParameter( "p_first_address", FirstAddress ),
         new OracleParameter( "p_second_address", SecondAddress )
      };
   }
}


Repository pattern


Пока что мы видели стандартную реализацию бизнес-объектов CSLA, которую часто можно увидеть в коде многих легаси программ, разработанных при помощи CSLA .NET. Как видно из кода, слой доступа к данным и слой бизнес логики тесно связаны друг с другом. Если мы абстрагируемся от слоя доступа к данным, то избавимся от низкоуровневых деталей в бизнес логике и специфических зависимостей, которые они имеют. Это упростит юнит-тестирование наших бизнес-объектов и позволит менять логику доступа к данным независимо от логики бизнес уровня. Поэтому возникает задача по созданию дополнительного уровня косвенности между бизнес логикой и слоя доступа к данным. К примеру, можно воспользоваться известным шаблоном Repository. Создадим интерфейс с необходимыми методами:

//IPersonRepository.cs
public interface IPersonRepository {
   PersonData FindPerson( int id );

   void AddPerson( PersonData newPerson, out int newId, out object lastChanged );

   void EditPerson( PersonData existingPerson, out object lastChanged );

   void RemovePerson( int personId );
}

Класс PersonData – это простой объект передачи данных ( DTO, Data Transfer Object ):

//PersonData.cs
public sealed class PersonData {
   public int Id { get; set; }

   public string FirstName { get; set; }

   public string SecondName { get; set; }

   public int Age { get; set; }

   public string Comment { get; set; }

   public object LastChanged { get; set; }        
}

Для абстрагирования от кода ADO .NET поддержки транзакций создадим простые интерфейсы IContext и ITransaction:

//IContext.cs
public interface IContext {
   ITransaction BeginTransaction( );
}

//ITransaction.cs
public interface ITransaction : IDisposable {
   void Commit( );

   void Rollback( );
}

Теперь хотелось бы, чтобы в методах DataPortal_XYZ класса Person вместо специфического DAL кода использовались методы определенных выше интерфейсов. Возникает задача внедрения зависимостей в бизнес-объект Person. Классический способ – протащить зависимости через конструктор Person нельзя из-за ограничений CSLA (использование фабричных методов), поэтому рассмотрим другие возможные способы решить эту задачу.

Setter Method injection


Добавим в класс Person закрытое поле типа IPersonRepository:

[NonSerialized] [NotUndoable] 
private IPersonRepository _personRepository;

Обратите внимание на использование атрибутов [NonSerialized] [NotUndoable] – зависимость IPersonRepository не должна сериализоваться при перемещении объекта Person от одного физического узла к другому (Mobile Object Pattern) и участвовать в многоуровневой отмене CSLA (N-Level Undo).

Далее добавим метод конфигурации репозитория:

[Inject] // Используется атрибут DI-контейнера Ninject
private void Configure( IPersonRepository personRepository ) {
   _personRepository = personRepository;
}

Вместо метода конфигурации можно использовать свойство PersonRepository (т.н. Property Setter Injection):

[Inject]
[EditorBrowsable( EditorBrowsableState.Never )]
private IPersonRepository PersonRepository {
   get { return _personRepository; }
   set { _personRepository = value; }
}

Обратите внимание, что в нашем случае свойство PersonRepository будет использоваться только на серверной стороне портала данных, поэтому его можно скрыть от Intellisence с помощью атрибута EditorBrowsable.

В общем случае внедрить зависимости в уже созданный инфраструктурой CSLA бизнес-объект можно, воспользовавшись следующими якорями абстрактного класса BusinessBase:

DataPortal_OnDataPortalInvoke, Child_OnDataPortalInvoke и OnDeserialized. Первые два метода вызываются серверной частью портала данных до вызова методов DataPortal_XXX бизнес-объекта. Первый метод – в случае если бизнес-объект является корневым, второй – если дочерним. Третий метод вызывается после десериализации мобильного объекта на сервере или клиенте. В рамках шаблона Repository его переопределять необязательно (зависимости используются только на сервере), но необходимо для общего случая внедрения зависимостей в бизнес-объекты CSLA.

Таким образом, мы можем написать метод Inject, который будет вызываться во всех трех методах и внедрять зависимости в созданный CSLA бизнес-объект. Исходя из всего вышесказанного напишем новый стереотип InjectableBusinessBase, который послужит заменой для стандартного стереотипа BusinessBase:

//InjectableBusinessBase.cs
[Serializable]
public abstract class InjectableBusinessBase<T> : BusinessBase<T> where T : BusinessBase<T> {
   protected override void DataPortal_OnDataPortalInvoke( DataPortalEventArgs e ) {
      Inject( );
      base.DataPortal_OnDataPortalInvoke( e );
   }

   protected override void Child_OnDataPortalInvoke( DataPortalEventArgs e ) {
      Inject( );
      base.Child_OnDataPortalInvoke( e );
   }

   protected override void OnDeserialized( System.Runtime.Serialization.StreamingContext context ) {
      Inject( );
      base.OnDeserialized( context );
   }

   private void Inject( ) {
      // Здесь должен быть код для разрешения зависимостей данного экземпляра.
   }
}

Заметим, что для каждого стереотипа бизнес-объектов CSLA должен быть создан новый стереотип с переопределенными якорями. Код новых стереотипов будет аналогичен коду выше. Теперь классы бизнес-уровня с зависимостями должны наследовать новым стереотипам:

[Serializable]
public partial class Person : InjectableBusinessBase<Person> {
   //…
}
[Serializable]
public partial class Orders : InjectableBusinessListBase<Orders, Order> {
   //…
}
[Serializable]
public partial class Address : InjectableBusinessBase<Address> {
   //…
}

Для реализации метода Inject добавим следующий фасадный класс DI-контейнера (используется Ninject):

//Container.cs
public static class Container {
   private static readonly object SyncRoot = new object( );

   private static volatile IKernel _kernel; // в качестве DI-контейнера используется Ninject

   public static IKernel Kernel {
      get {
         if ( _kernel == null ) {
            lock ( SyncRoot ) {
               if ( _kernel == null ) {
                  ConfigureKernel( );
               }
            }
         }
         return _kernel;
      }
   }

   //Метод внедрения зависимостей в уже созданный объект.
   public static void InjectInto( object target ) {
      Kernel.Inject( target );
   }

   private static void ConfigureKernel( ) {
      // конфигурация контейнера
   }

   public static void InjectKernel( IKernel kernel ) {
      lock ( SyncRoot ) {
         _kernel = kernel;
      }
   }
}

Как сконфигурировать непосредственно сам DI-контейнер нам пока неважно, а важен лишь метод InjectInto. Теперь можно вернуться к методу Inject( ) стереотипа InjectableBusinessBase:

private void Inject( ) {
   Container.InjectInto( this ); 
}

Наконец, добавим свойства IPersonRepository и IContext в серверную часть класса Person и перепишем методы DataPortal_XYZ следующим образом:

//Person.Server.cs
public partial class Person {
   public static readonly PropertyInfo<object> LastChangedProperty = 
           RegisterProperty<object>( c => c.LastChanged );

   public object LastChanged {
      get { return ReadProperty( LastChangedProperty ); }
      private set { LoadProperty( LastChangedProperty, value ); }
   }

   [NonSerialized] [NotUndoable] 
   private IPersonRepository _personRepository;

   [Inject]
   [EditorBrowsable( EditorBrowsableState.Never )]
   private IPersonRepository PersonRepository {
      get { return _personRepository; }
      set { _personRepository = value; }
   }
        
   [NonSerialized][NotUndoable]
   private IContext _context;

   [Inject]
   [EditorBrowsable(EditorBrowsableState.Never)]
   private IContext Context {
      get { return _context; }
      set { _context = value; }
   }

   protected override void DataPortal_Create( ) {
      BusinessRules.CheckRules( );
   }

   private void DataPortal_Fetch( SingleCriteria<Person, int> idCriteria ) {
      var personData = PersonRepository.FindPerson( idCriteria.Value );
      if ( personData != null ) {
         CopyValuesFrom( personData );
         LoadProperty( OrdersProperty, Orders.GetOrders( personData ) );
         LoadProperty( AddressProperty, Address.GetAddress( personData ) );
      }
   }

   private void CopyValuesFrom( PersonData personData ) {
      using ( BypassPropertyChecks ) {
         DataMapper.Map( personData, this ); 
      }
   }

   protected override void DataPortal_Insert( ) {
      using ( var transaction = Context.BeginTransaction( ) ) {
         try {
            var personData = GetPersonData( );
            int newId;
            object lastChanged;
            PersonRepository.AddPerson( personData, out newId, out lastChanged );
            Id = newId;
            LastChanged = lastChanged;
            FieldManager.UpdateChildren( personData);
            transaction.Commit( );
         } 
         catch {
            transaction.Rollback( );
            throw;
         }
      }
   }

   protected override void DataPortal_Update( ) {
      using ( var transaction = Context.BeginTransaction( ) ) {
         try {
            var personData = GetPersonData( );
            object lastChanged;
            PersonRepository.EditPerson( personData, out lastChanged );
            LastChanged = lastChanged;
            FieldManager.UpdateChildren( personData );
            transaction.Commit( );
         } 
         catch {
            transaction.Rollback( );
            throw;
         }
      }
   }

   private PersonData GetPersonData( ) {
      // Обратите внимание, что поскольку в нашем случае имена свойств 
      //бизнес-класса Person и DTO PersonData совпадают, то можно воспользоваться 
      //классом CSLA DataMapper при работе с DTO, что делает код еще компактнее.
      var personData = new PersonData( );
      DataMapper.Map( this, personData, OrdersProperty.Name, AddressProperty.Name );
      return personData;
   }

   private void DataPortal_Delete( SingleCriteria<Person, int> idCriteria ) {
      PersonRepository.RemovePerson( idCriteria.Value );
   }

   protected override void DataPortal_DeleteSelf( ) {
      PersonRepository.RemovePerson( Id );
   }
}

Код дочерних классов:
//Orders.Server.cs
public partial class Orders {
   [NonSerialized]
   [NotUndoable]
   private IOrderRepository _orderRepository;

   [Inject]
   [EditorBrowsable( EditorBrowsableState.Never )]
   protected IOrderRepository OrderRepository {
      get { return _orderRepository; }
      set { _orderRepository = value; }
   }

   internal static Address GetAddress( Person person ) {
      return DataPortal.FetchChild<Address>( person );
   }

   protected void Child_Fetch( PersonData person) {
      var data = OrderRepository.FindOrders( person.Id );
      RaiseListChangedEvents = false;
      AddRange( data.Select( Order.GetOrder ) );
      RaiseListChangedEvents = true;
   }

   protected void Child_Update( PersonData person ) {
      base.Child_Update( person, OrderRepository );
   }
}

//Order.Server.cs
public partial class Order {
   internal static Order GetOrder( OrderData orderData ) {
      return DataPortal.FetchChild<Order>( orderData );
   }

   protected void Child_Fetch( OrderData orderData ) {
      using ( BypassPropertyChecks ) {
         DataMapper.Map( orderData, this );
      }
   }

   protected void Child_Insert( PersonData person, IOrderRepository orderRepository ) {
      var data = GetOrderData( );
      Id = orderRepository.AddOrder( person.Id, data );
   }

   protected void Child_Update( PersonData person, IOrderRepository orderRepository ) {
      var data = GetOrderData( );
      orderRepository.EditOrder( person.Id, data );
   }

   protected void Child_DeleteSelf( PersonData person, IOrderRepository orderRepository ) {
      orderRepository.RemoveOrder( person.Id, Id );
   }

   private OrderData GetOrderData( ) {
      var orderData = new OrderData( );
      DataMapper.Map( this, orderData );
      return orderData;
   }
}


//Address.Server.cs
public partial class Address {
   [NonSerialized, NotUndoable]
   private IAddressRepository _addressRepository;

   [Inject]
   [EditorBrowsable( EditorBrowsableState.Never )]
   protected IAddressRepository AddressRepository {
      get { return _addressRepository; }
      set { _addressRepository = value; }
   }

   internal static Address GetAddress( PersonData person ) {
      return DataPortal.FetchChild<Address>( person );
   }

   protected void Child_Fetch( PersonData personData ) {
      using ( BypassPropertyChecks ) {
         var addressData = AddressRepository.FindAddress( personData.Id );
         DataMapper.Map( addressData, this );
      }
   }

   protected void Child_Insert( PersonData person ) {
      var data = GetAddressData( );
      Id = AddressRepository.AddAddress( person.Id, data );
   }

   protected void Child_Update( PersonData person ) {
      var data = GetAddressData( );
      AddressRepository.EditAddress( person.Id, data );
   }

   protected void Child_DeleteSelf( PersonData person ) {
      AddressRepository.RemoveAddress( person.Id, Id );
   }

   private AddressData GetAddressData( ) {
      var addressData = new AddressData( );
      DataMapper.Map( this, addressData );
      return addressData;
   }
}


Специфичный код обращения к DAL исчез. Теперь перед обращением к методам DataPortal_XYZ серверный портал данных при помощи переопределенных якорей разрешит все зависимости класса Person, что позволит инкапсулировать весь код доступа к DAL в реализации репозиториев.

Идея этого способа взята из блога Johny Bekkum. Им создана библиотека CSLAContrib, которая содержит множество полезных дополнений к CSLA. В частности, в ней есть набор стереотипов CSLA, поддерживающих Property Setter Injection. Код этих стереотипов подобен коду, приведенному выше. В качестве DI-контейнера используется MEF (Managed Extensibility Framework), появившийся в .NET Framework 4.0.

Отметим, что всех сложностей можно избежать, просто использовав Container напрямую:

private IPersonRepository GetPersonRepository {
   return Core.Container.Kernel.Get<IPersonRepository>();
}

В данном случае класс Container используется как Service Locator, который часто определяют как антипаттерн. Однако данный способ тоже может быть полезен, особенно при сопровождении больших легаси проектов на CSLA.

CSLA via Repository Pattern


Рассмотрим подробнее реализацию шаблона Repository. Вынесем общий для всех будущих репозиториев код для работы с БД Oracle в базовый класс RepositoryBase:

//RepositoryBase.cs
internal class RepositoryBase {
   private readonly string _databaseName;

   protected RepositoryBase( string databaseName ) {
      _databaseName = databaseName;
   }

   protected virtual void ExecuteProcedure( string procName, params OracleParameter[] parameters ) {
      using ( var manager = 
                 TransactionManager<OracleConnection, OracleTransaction>.GetManager( _databaseName ) ) {
         using ( var command = manager.Connection.CreateCommand( ) ) {
            command.CommandType = CommandType.StoredProcedure;
            command.CommandText = procName;
            command.Transaction = manager.Transaction;
            if ( parameters != null ) {
               command.Parameters.AddRange( parameters );
            }
            command.ExecuteNonQuery( );
            if ( manager.RefCount == 1 ) {
               manager.Commit( );
            }
         }
      }
   }

   protected virtual IEnumerable<T> GetRows<T>( string query, 
              Func<SafeDataReader, T> fetchFromReader, params OracleParameter[] parameters ) {
      using ( var manager = ConnectionManager<OracleConnection>.GetManager( _databaseName ) ) {
         using ( var command = manager.Connection.CreateCommand( ) ) {
            command.CommandType = CommandType.Text;
            command.CommandText = query;
            if ( parameters != null ) {
               command.Parameters.AddRange( parameters );
            }                    
            using ( var reader = new SafeDataReader( command.ExecuteReader( ) ) ) {
               while ( reader.Read( ) ) {
                  yield return fetchFromReader( reader );
               }
            }
         }
      }
   }
}

Теперь реализуем контракт IPersonRepository, используя RepositoryBase:

//PersonRepository.cs
internal sealed class PersonRepository : RepositoryBase, IPersonRepository {
   public PersonRepository( ) : base( "CSLAPROJECT" ) { }

   public PersonData FindPerson( int id ) {
      const string query = @"SELECT id 
                                   ,first_name
                                   ,second_name
                                   ,age
                                   ,comment
                                   ,last_changed
                               FROM All_persons
                              WHERE id = :p_id";
      return GetRows( query, FetchFromReader, new OracleParameter( "p_id", id ) ).First( );
   }

   public void AddPerson( PersonData newPerson, out int newId, out object lastChanged ) {
      var parameters = GetPersonParameters( newPerson );
      ExecuteProcedure( "csla_project.add_person", parameters );
      newId = ( int )parameters.First( ).Value;
      lastChanged = parameters.Last( ).Value;
   }

   public void EditPerson( PersonData existingPerson, out object lastChanged ) {
      var parameters = GetPersonParameters( existingPerson );
      ExecuteProcedure( "csla_project.update_person", parameters );
      lastChanged = parameters.Last( ).Value;
   }

   public void RemovePerson( int personId ) {
      ExecuteProcedure( "csla_project.delete_person", new OracleParameter("p_id", personId) );
   }

   private PersonData FetchFromReader( SafeDataReader reader ) {
      return new PersonData {
         Id = reader.GetInt32( "id" ),
         FirstName = reader.GetString( "first_name" ),
         SecondName = reader.GetString( "second_name" ),
         Age = reader.GetInt32( "age" ),
         Comment = reader.GetString( "comment" ),
         LastChanged = reader["last_changed"] 
      };
   }

   private OracleParameter[] GetPersonParameters( PersonData personData ) {
      return new[] {
         new OracleParameter( "p_id", personData.Id ){Direction = ParameterDirection.InputOutput},
         new OracleParameter( "p_first_name", personData.FirstName ),
         new OracleParameter( "p_second_name", personData.SecondName ),
         new OracleParameter( "p_age", personData.Age ),
         new OracleParameter( "p_comment", personData.Comment ),
         new OracleParameter( "p_last_changed", OracleType.Int32 ) {
            Value = personData.LastChanged, 
            Direction = ParameterDirection.Output
         }
      };
   }
}

Классы поддержки транзакций выглядит следующим образом:

//Context.cs
internal sealed class Context : IContext {
   ITransaction IContext.BeginTransaction( ) {
      return new Transaction( );
   }
}

//Transaction.cs
internal sealed class Transaction : ITransaction {
   private readonly TransactionManager<OracleConnection, OracleTransaction> _manager =
          TransactionManager<OracleConnection, OracleTransaction>.GetManager( "CSLAPROJECT" );

   void ITransaction.Commit( ) {
      _manager.Commit( );
      Dispose( );
   }

   void ITransaction.Rollback( ) {
      Dispose( );
   }

   public void Dispose( ) {
      _manager.Dispose( );
   }
}

В данном случае для обеспечения транзакций был использован класс CSLA TransactionManager, который позволяет использовать одно и то же соединение и ассоциированную с ним ADO .NET транзакцию во всем графе бизнес объекта при выполнении одной операции портала данных. Классы AddressRepository и OrderRepository аналогичны приведенному выше PersonRepository.

Диаграмма классов после всех изменений выглядит следующим образом:



Распределим бизнес-объекты, репозитории и контракты по разным проектам. Итоговая диаграмма пакетов выглядит так:



Наконец, добавим в CslaProject.DataAccess.OracleDb модуль Ninject для конфигурации DAL:

//Module.cs
public class Module : NinjectModule {
   public override void Load( ) {
      Bind<IPersonRepository>( ).To<PersonRepository>( ).InSingletonScope( );
      Bind<IOrderRepository>( ).To<OrderRepository>( ).InSingletonScope( );
      Bind<IAddressRepository>( ).To<AddressRepository>( ).InSingletonScope( );
      Bind<IGroupRepository>( ).To<GroupRepository>( ).InSingletonScope( );
      Bind<IContext>( ).To<Context>( ).InSingletonScope( );
   }
}

Конфигурация контейнера


Вернемся к фасадному классу Container и для полноты примера приведем код конфигурации DAL в корневом проекте.

private static void ConfigureKernel( ) {
   // InjectNonPublic = true, т.к. свойства закрыты
   var kernel = new StandardKernel( new NinjectSettings {InjectNonPublic = true} );
   // получение каталогов зависимостей.
   var dependencyCatalogs = GetDependencyCatalogs( );
   if ( dependencyCatalogs.Any( ) ) {
      foreach ( var values in 
                   dependencyCatalogs.Select( d => ConfigurationManager.AppSettings[ d ].Split( ';' ) ) ) {
         var catalogPath = Path.Combine( AppDomain.CurrentDomain.BaseDirectory, values[ 0 ] );
         IEnumerable<string> depedencyLibNames;
         if ( values.Count( ) > 1 ) {
            var searchPattern = values[ 1 ];
            depedencyLibNames = Directory.GetFiles( catalogPath, searchPattern );
         } 
         else {
            depedencyLibNames = Directory.GetFiles( catalogPath );
         }
         foreach ( var file in depedencyLibNames ) {
            var dependency = Assembly.LoadFile( Path.Combine( catalogPath, file ) );
            kernel.Load( dependency );
         }
      }
   } 
   else {
      //Если нет сконфигурированных зависимостей, загружаем все 
      //находящиеся в корневом каталоге сборки, название которых начинается с CslaProject
      kernel.Load("CslaProject*.dll");
   }
   _kernel = kernel;
}

private static string[] GetDependencyCatalogs( ) {
   return ConfigurationManager.AppSettings.AllKeys.Where( 
               p => p.StartsWith( "CslaProject.Depencies", true, CultureInfo.InvariantCulture ) ).ToArray( );
}

Читается App.Config примерно следующего вида:

<?xml version="1.0"?>
<configuration>
    <appSettings>
    <add key="CslaProject.Dependencies" value="Dependencies;CslaProject*.dll"/>
  </appSettings>
  <connectionStrings>
    <!-- 
    <add name="CSLAPROJECT" providerName="System.Data.OracleClient" connectionString="user id=test_user;password=12345;data source=testdb;" />
    -->
  </connectionStrings>
<startup>
  <supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.0"/>
</startup>
</configuration> 

Конфигурация зависимостей проекта задается в секции appSettings в опциях с ключом CslaProject.Dependencies. Значением опции является название каталога в корневом каталоге приложения, в котором расположены библиотеки с реализациями зависимостей.

Если у запускаемого проекта отсутствует App.Config с ключами CslaProject.Dependencies (например, если код бизнес-логики вызывается из проекта с юнит-тестами CslaProject.UnitTests), то подгружаются все сборки, название которых начинается с CslaProject. Так, если в проекте с юнит-тестами добавить модуль Ninject, который будет конфигурировать DAL тестовыми репозиториями, то в методах DataPortal_XYZ будут использоваться именно они.

Заключение


Рассмотренный способ отделения слоя доступа к данным от бизнес-классов CSLA вполне работоспособен. Однако есть и недостатки:

  • Все бизнес-классы должны наследовать новым стереотипам Injectables, что не всегда возможно.
  • Дополнительные накладные расходы. Метод внедрения зависимостей будет выполняться перед каждым вызовом методов DataPortal_XYZ и после десериализации на сервере или клиенте, что для бизнес-объекта с большим числом дочерних объектов может стать проблемой.
  • Увеличение сложности проекта.
  • Привязка к конкретному DI-контейнеру.

На этом, пожалуй, всё. Спасибо за внимание, и да пребудет с вами Сила.
Tags:
Hubs:
+6
Comments 4
Comments Comments 4

Articles