|
|
В данной статье описывается создание инфраструктуры, позволяющей использовать технологию Linq to SQL и методику DDD. Сразу же даю ссылку на исходники, для тех кому надо по быстрому получить рабочее решение. Замечательная книга Джимми Нильсена Applying Domain-Driven Design and Patterns: With Examples in C# and.NET хороший материал о разработке комплексных проектов через тестирование. Книга была написана еще до выхода технологии LINQ. Майкрософт сделал многое для того, чтоб ORM стал простым в.NET разработке. Некоторые принципы DDD: Обьекты — POCO — они не знают про ORM, являясь наследниками Object. Репозитории — классы отвечающие за извлечение и сохранение обьектов. Архитектура, базируется на модели. В блоге Скота Гу, много материала по LINQ, но там ни слова про DDD. Класс DataContext, идущий рядом с паттерном UnitOfWork, имеет серьезный недостаток — не реализует интерфейса. Что сильно затрудняет юнит тестинг (нельзя подделать реализацию с помощью Mock objects) Здорово, что проблема уже решена: Using Mock Objects When Testing LINQ Code — вполне рабочее решение, но мне ближе другая реализация. Trying Out Persistence Ignorance with LINQ — эту реализацию я и взял за основу решения, использованного в моем проекте. Способ состоит в том, чтоб сделать интерфейс UnitOfWork, который инкапсулирует в себе все взаимодействие с хранилищем. Так что, будет очень легко подставить в классы для тестирования поодельную реализацию. Сама реализация кратко ниже: 1
2
3
4
5
6
7
8
9
10
11
12 | public interface IUnitOfWork : IDisposable
{
IDataSource<T> GetDataSource<T>() where T : class;
void SubmitChanges();
DataLoadOptions LoadOptions { get; set; }
IEnumerable<TResult> ExecuteQuery<TResult>(string query, params object[] parameters);
}
public interface IDataSource<T> : IQueryable<T>, IEnumerable<T>,
ITable, IQueryProvider
{
}
|
Настоящий источник данных делегирует все родному классу DataContext.
| public class PersistentDataSource<T> : IDataSource<T> where T : class
{
Table<T> _table = null;
DataContext _context = null;
public PersistentDataSource(DataContext context)
{
_context = context;
_table = context.GetTable<T>();
}
|
Настоящий UnitOfWork создает PersistentDataSource и считывает маппинг классов из хмл файла, дизайнер классов я не использую и вам не советую.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19 | public class UnitOfWork : IUnitOfWork
{
DataContext _context = null;
public UnitOfWork()
{
_context = GetContext();
}
private DataContext GetContext()
{
try
{
return new DataContext(GetConnectionString(), GetMapping());
}
catch (InvalidOperationException ex)
{
throw new UnitOfWorkException("File datamap.xml is not valid", ex);
}
}
|
Поддельная реализация IDataSource весьма проста — она все делегирует классу List
| public class InMemoryDataStore<T> : IDataSource<T>
{
List<T> _list = new List<T>();
#region IEnumerable<T> Members
public IEnumerator<T> GetEnumerator()
{
return _list.GetEnumerator();
}
|
С помощью этого всего кода была создана инфраструктура для комфортной разработки через тестирование.
Пример
Есть таблица со странами.
| <Table Name="dbo.Countries">
<Type Name="NS.ChanceDiary.Data.Country">
<Column Name="CountryId" Member="CountryId" DbType="UniqueIdentifier NOT NULL" IsPrimaryKey="true" />
<Column Name="Country" Member="CountryName" DbType="NVarChar(70) NOT NULL" CanBeNull="false" />
<Column Name="CountryCode" Member="CountryCode" DbType="NVarChar(5) NOT NULL" CanBeNull="false" />
<Association Member="Users" ThisKey="CountryId" OtherKey="CountryId" />
</Type>
</Table>
|
Для демонстрации связи с другой таблицей я указал
| <Association Member="Users" ThisKey="CountryId" OtherKey="CountryId" />
|
Это означает, что в стране могут быть привязанны пользователи.
Класс страны
| public class Country
{
public System.Guid CountryId { get; set; }
public string CountryName { get; set; }
public string CountryCode { get; set; }
private EntitySet<User> _Users = new EntitySet<User>();
public EntitySet<User> Users {
get { return this._Users;}
set { this._Users.Assign(value);}
}
}
|
Простой класс, практически POCO. Практически, потому что связь описывается при помощи коллекции EntitySet. Линк использует этот класс для автоматической загрузки графа обьектов.
Так выглядит ссылка в классе User на класс страны.
| protected Guid CountryId { get; set; }
private EntityRef<Country> _Country = default(EntityRef<Country>);
public Country Country {
get{ return _Country.Entity;}
set
{
_Country.Entity = value;
if (_Country.Entity != null) CountryId = value.CountryId;
}
}
|
Так в хмл
| <Association Member="Country" OtherKey="CountryId" ThisKey="CountryId"/>
|
Сохраняемые в БД классы я храню в одном проекте с ифраструктурой сохраняемости. Иначе у меня не получалось корректно инициализировать UnitOfWork.
Далее все как по книге Нильсена.
Репозитории
Я создал базовый код для репозитория, чтоб уменьшить дублирование
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53 | public interface IRepository<T>
{
IDataSource<T> DataSource { get; }
IUnitOfWork Context { get; }
void SubmitChanges();
}
public class Repository<T>: IRepository<T> where T: class
{
//депенденси инжекшн (внедрять фреймворки по-типу Spring.NET смысла не увидел) довольно прост,
//один конструктор с реальным юнит ов ворк
public Repository():this(new UnitOfWork()) {
}
//тут же можно настраивать
public Repository(IUnitOfWork context) {
_context = context;
_dataSource = _context.GetDataSource<T>();
}
#region IRepository<T> Members
IDataSource<T> _dataSource;
public IDataSource<T> DataSource
{
get { return _dataSource; }
}
IUnitOfWork _context;
public IUnitOfWork Context
{
get { return _context; }
}
public void SubmitChanges()
{
_context.SubmitChanges();
}
#endregion
}
public class CountryRepository : Repository<Country>, ICountryRepository
{
public CountryRepository():base() { }
public CountryRepository(IUnitOfWork context) : base(context) { }
public Country GetCountry(string countryCode, string countryName)
{
return (from u in DataSource where u.CountryName == countryName || u.CountryCode == countryCode
select u).SingleOrDefault();
}
public void AddCountry(Country country)
{
DataSource.InsertOnSubmit(country);
Context.SubmitChanges();
}
}
|
Так выглядит NUnit тест
1
2
3
4
5
6
7
8
9
10
11
12
13 | [TestFixture]
public class CountryRepositoryTest
{
[Test]
public void GetCountryByCodeOrName()
{
CountryRepository Repository = new CountryRepository(TestHelper.GetUnitOfWork());
Repository.AddCountry(new Country {CountryCode="UA",CountryName ="Ukraine"});
Country ua = Repository.GetCountry("UA", "");
Country Ukraine = Repository.GetCountry("", "Ukraine");
Assert.AreEqual(ua.CountryName, Ukraine.CountryName,"Should be found the same country");
}
}
|
|
|