- PVSM.RU - https://www.pvsm.ru -
Создание систем с низкой связанностью (Low Coupling) между модулями обеспечивает множество преимуществ при разработке ПО. В приложениях, написанных с использованием каркаса CSLA .NET [1], применение стандартных шаблонов для разрыва зависимостей не всегда может быть очевидно.
В данной статье будет рассмотрен вариант отделения слоя доступа к данным (Data Access Layer, DAL) от слоя бизнес логики (Business Layer) при помощи шаблона Repository [2] и описан наиболее распространенный способ внедрения зависимостей (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.
Сперва обратимся к коду, относящемуся к созданию объекта 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 )
};
}
}
Пока что мы видели стандартную реализацию бизнес-объектов CSLA, которую часто можно увидеть в коде многих легаси программ, разработанных при помощи CSLA .NET. Как видно из кода, слой доступа к данным и слой бизнес логики тесно связаны друг с другом. Если мы абстрагируемся от слоя доступа к данным, то избавимся от низкоуровневых деталей в бизнес логике и специфических зависимостей, которые они имеют. Это упростит юнит-тестирование наших бизнес-объектов и позволит менять логику доступа к данным независимо от логики бизнес уровня. Поэтому возникает задача по созданию дополнительного уровня косвенности между бизнес логикой и слоя доступа к данным. К примеру, можно воспользоваться известным шаблоном Repository [2]. Создадим интерфейс с необходимыми методами:
//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 (использование фабричных методов), поэтому рассмотрим другие возможные способы решить эту задачу.
Добавим в класс 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 [3]):
//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 [4]. Им создана библиотека CSLAContrib [5], которая содержит множество полезных дополнений к CSLA. В частности, в ней есть набор стереотипов CSLA, поддерживающих Property Setter Injection. Код этих стереотипов подобен коду, приведенному выше. В качестве DI-контейнера используется MEF [6] (Managed Extensibility Framework), появившийся в .NET Framework 4.0.
Отметим, что всех сложностей можно избежать, просто использовав Container напрямую:
private IPersonRepository GetPersonRepository {
return Core.Container.Kernel.Get<IPersonRepository>();
}
В данном случае класс Container используется как Service Locator, который часто определяют как антипаттерн [7]. Однако данный способ тоже может быть полезен, особенно при сопровождении больших легаси проектов на CSLA.
Рассмотрим подробнее реализацию шаблона 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 [8], который позволяет использовать одно и то же соединение и ассоциированную с ним 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 вполне работоспособен. Однако есть и недостатки:
На этом, пожалуй, всё. Спасибо за внимание, и да пребудет с вами Сила.
Автор: Sasha_Markov
Источник [9]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/net/204897
Ссылки в тексте:
[1] CSLA .NET: http://cslanet.com/
[2] шаблона Repository: https://msdn.microsoft.com/en-us/library/ff649690.aspx
[3] Ninject: http://www.ninject.org/
[4] блога Johny Bekkum: https://jonnybekkum.wordpress.com/2010/12/30/cslacontrib-mef-and-repository-pattern-with-csla4/
[5] CSLAContrib: https://github.com/marimerllc/cslacontrib
[6] MEF: https://msdn.microsoft.com/ru-ru/library/dd460648(v=vs.110).aspx
[7] определяют как антипаттерн: http://blog.ploeh.dk/2010/02/03/ServiceLocatorisanAnti-Pattern/
[8] TransactionManager: https://github.com/nschonni/csla-svn/blob/master/Source/Csla/Data/TransactionManager.cs
[9] Источник: https://habrahabr.ru/post/314110/?utm_source=habrahabr&utm_medium=rss&utm_campaign=sandbox
Нажмите здесь для печати.