Сайт Игоря Кононученко   Статьи

Linq to SQL и DDD

4 июня 2008

В данной статье описывается создание инфраструктуры, позволяющей использовать технологию 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.

1
2
3
4
5
6
7
8
9
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

1
2
3
4
5
6
7
8
9
public class InMemoryDataStore<T> : IDataSource<T>
    {
        List<T> _list = new List<T>();
        #region IEnumerable<T> Members

        public IEnumerator<T> GetEnumerator()
        {
            return _list.GetEnumerator();
        }

С помощью этого всего кода была создана инфраструктура для комфортной разработки через тестирование.

Пример

Есть таблица со странами.

1
2
3
4
5
6
7
8
<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>
Для демонстрации связи с другой таблицей я указал
1
<Association Member="Users" ThisKey="CountryId" OtherKey="CountryId" />
Это означает, что в стране могут быть привязанны пользователи.

Класс страны

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
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 на класс страны.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
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;
            }
        }
Так в хмл
1
<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");
    }     
}

Оптимизация JavaScript — делаем билд процесс
Прагматичное юнит-тестирование
Ctrl
Continuous Integration в django-проекте
Template из Prototype.js на C# 3.0