disclaimer: the article provides an overview of the specific pattern Repository implementation,considering in detail the methods and features within Incoding Framework. For the best immersion read Repository by Fowler, CQRS vs NLayer
UPD: Article source codes are available on GitHub
What do we get ?
Patterns often become not only a tool to struggle against the complexity of the project, but sometimes at a very “dense” use, are themselves a source of problems. So before applying one or another pattern I have been studying its impact on the architecture of the project and the amount of code to implement it.
- Abstraction over the database
Note: ORM is an abstraction over the database and Repository will be redundant, but eventually I came to the conclusion that it is always better to have a layer between the third-party components, such as ORM, IoC and others.
- Uniform interface for providers (Nhibernate, Entity Framework, etc.)
Note: The first part partly covers the second point, but it is a different problem.
And may it be without it?
DataContext on View (linq to sql used the name DataContext for a class of database access), a kind of “meme” in the development of web sites, which is on par with GOD object and other anti-patterns. In fact, a call does not necessarily have to be on View, because when calling DataContext from Controller or Service, the problem remains. What difficulties the use of Data Context without Repository entails:
- Binding to a specific ORM implementation ( Linq to sql = DataContext, Nhibernate = ISession )
Note: on the one hand ORM replacement in the later stages of the project seems to be a controversial and a difficult step, but personal experience convinced (Linq to Sql replacement for Nhibernate), it is sometimes the only way to solve some problems.
- There are a lot of low-level methods
Note: I hold the opinion that it is much easier to maintain a limited set of methods to work with the database than access to low-level features of a particular ORM operation.
- It is difficult to monitor places where the work with a database is going on (Incoding Framework provides access to the Repository only in Query and Command)
Note: The main difficulty will be to support UnitOfWork to ensure correct operation of transactions
- The difficulties of writing unit tests
Note: due to the fact that the code works with 3rd-party objects, then there is difficulty in creating Mock Up
CRUD
Repository has methods to perform basic tasks related to the creation (create), reading (read), updating (update) and removing (delete):
Create, Update, Delete
- Save – – saves the object to the database
1 |
Repository.Save(new Product { Code = Code, Name = Name, Price = Price, CreateDt = DateTime.Now }); |
- Delete – deletes the object according to the type and Id
1 |
Repository.Delete<Product>(Id); |
- SaveOrUpdate – – saves or updates the object in the database
1 |
Repository.SaveOrUpdate(product); |
Note: SaveOrUpdate method may not be used, because many ORM (Nhibernate, Entity Framework) supports object state tracking (tracking), but if the provider does not have this opportunity, you should always call SaveOrUpdate
1 2 3 |
var product = Repository.GetById<Product>(Id); product.Name = "New Name"; Repository.SaveOrUpdate(product); // if tracking not available |
Read
- GetById -returns an object according to the type and Id
1 |
var product = Repository.GetById<Product>(Id); |
Note: Id parameter is Object, the reason for the absence of a specific type is to maximize flexibility (Id can be string, int, long, guid) of the solution.
Note: If Id null or an object is found, the return will be null
- LoadById – – returns an object according to the type and Id
1 |
var product = Repository.LoadById<Product>(Id); |
Note: LoadById method works just as well as GetById, with the difference that when calling Load will try to find objects in the Cache, and Get always calls the database
1 2 |
var product = Repository.LoadById<Product>(Id); // get from data base product = Repository.LoadById<Product>(Id); // get from cache |
- Query – returns a set of (IQueryable) objects based on the specifications (where, order, fetch, paginated)
1 2 3 |
Repository.Query(whereSpecification: new ProductHavingCodeWhere(Code) .And(new ProductInPriceWhere(From, To)), orderSpecification: new ProductOrder(OrderBy,Desc)) |
Note: The operations manual of the where, order, fetch query specifications are discussed below.
- Paginated – returns paginated result on the basis of specifications (where, order, fetch, paginated)
1 2 3 4 |
Repository.Paginated(paginatedSpecification: new PaginatedSpecification(1, 10), whereSpecification: new ProductHavingCodeWhere(Code) .And(new ProductInPriceWhere(From, To)), orderSpecification: new ProductOrder(ProductOrder.OrderBy, Desc)); |
Note: IncPaginatedResult is an object that was designed to provide a comfortable work with the results that are to be displayed per page. Practice has shown that to construct paged data, you need to know the general (TotalCounts) number of elements, excluding pages and items (Items).
- Total Counts – the total number of items in the database ( include where )
- Items – items that are in the range of the current page
Note: Paginated Specification algorithm will be discussed below
Specification
In C # there is LINQ, which allows to build a query plan, and to implement (broadcast, transmit) them by way of a certain provider. For example, if we chose Nhibernate, as ORM for our application, IQueryable Nhibernate provider can be used in order to transmit conditions (where, order, fetch, paginated) in SQL.
1 |
items.Where(product = > product.Name == "Vlad") |
What is bad about LINQ?
Specifications are cover of LINQ expressions into separate classes, which offer the following benefits:
- Reuse in different Query
Note: a significant advantage, because when writing large projects, it is difficult to maintain “scattered” throughout the code LINQ expressions.
- Substitution for mock-up objects in Query tests
Note: tests of the specifications will be carried out separately
- Encapsulation of additional logic
Sample
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
public override Expression<Func<SearchAdmission, bool>> IsSatisfiedBy() { DateTime? fromDate = null; DateTime? toDate = null; if (_from.HasValue) fromDate = DateTime.Now.AddYears(-_from.Value); if (_to.HasValue) toDate = DateTime.Now.AddYears(-(_to.Value + 1)); if (fromDate.HasValue && toDate.HasValue) return r = > fromDate.Value >= r.Patient.DOB && toDate.Value <= r.Patient.DOB; if (fromDate.HasValue) return r = > fromDate.Value >= r.Patient.DOB; if (toDate.HasValue) return r = > toDate.Value <= r.Patient.DOB; return null; } |
примечание: дополнительная логика, также актуальная и для Fetch и Order спецификаций
А может есть решения ?
Если не использовать specification, то можно применять C# extensions
1 2 3 4 5 6 7 8 9 10 |
public static class ProductExtensions { public static IEnumerable<Product> ProductHavingCode(this IEnumerable<Product> items, string code) { if (string.IsNullOrWhiteSpace(code)) return items; return items.Where(product = > product.Code.Contains(code)); } } |
примечание: данное решение обладает минусом, потому что не получится протестировать Query без выполнения кода Extesnions.
Where
Implementation
All specifications for filtration are inherited from the base class Specification
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
public class ProductHavingCodeWhere : Specification<Product> { readonly string code; public ProductHavingCodeWhere(string code) { this.code = code; } public override Expression<Func<Product, bool>> IsSatisfiedBy() { if (string.IsNullOrWhiteSpace(this.code)) return null; return product = > product.Code.Contains(this.code); } } |
Note: instead of null if code value is empty, expression product => true can be returned, but then in the resulting SQL query conditions will be deprived that would not have logical load.
Use
1 |
Repository.Query(whereSpecification: new ProductHavingCodeWhere(Code)) |
Note: specification can be put together by way of And, Or and Not
1 2 3 |
!new ProductHavingCodeWhere(Code) // not new ProductHavingCodeWhere(Code).And(new ProductInPriceWhere(From, To)) // and new ProductHavingCodeWhere(Code).Or(new ProductInPriceWhere(From, To)) // or |
Linq
1 2 |
if(string.IsNullOrEmpty(code) items = items.Where(product = > product.Code.Contains(this.code)) |
Order
Реализация
Все спецификации для сортировки наследуются от базового класса OrderSpecification<TEntity> ( TEntity – это тип объекта Query ), который имеет абстрактный метод SortedBy.
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 |
public class ProductOrder : OrderSpecification<Product> { readonly OrderBy orderBy; readonly OrderType desc; public ProductOrder(OrderBy orderBy, bool desc) { this.orderBy = orderBy; this.desc = desc ? OrderType.Descending : OrderType.Ascending; } public override Action<AdHocOrderSpecification<Product>> SortedBy() { switch (this.orderBy) { case OrderBy.Article: return specification = > specification.Order(r = > r.Name, this.desc).Order(r = > r.Code, this.desc); case OrderBy.Price: return specification = > specification.Order(r = > r.Price, this.desc); case OrderBy.CreateDt: return specification = > specification.Order(r = > r.CreateDt, this.desc); default: throw new ArgumentOutOfRangeException(); } } } |
- Order(r= >r.Name,OrderType.Ascending) – сортировка Name по возрастанию
примечание: OrderBy(r= >r.Name) сокращенный вариант
- Order(r= >r.Name,OrderType.Descending) – сортировка Name по убыванию
примечание: OrderByDescending(r= >r.Name) сокращенный вариант
Использование
1 |
Repository.Query(orderSpecification: new ProductOrder(OrderBy, Desc)) |
Linq аналог
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 |
if (Desc) { switch (OrderBy) { case ProductOrder.OrderBy.Article: items = items.OrderByDescending(r = > r.Name) .OrderByDescending(r = > r.Code); case ProductOrder.OrderBy.CreateDt: items = items.OrderByDescending(r = > r.CreateDt); case ProductOrder.OrderBy.Price: items = items.OrderByDescending(r = > r.Price); } } else { switch (OrderBy) { case ProductOrder.OrderBy.Article: items = items.OrderBy(r = > r.Name) .OrderBy(r = > r.Code); case ProductOrder.OrderBy.CreateDt: items = items.OrderBy(r = > r.CreateDt); case ProductOrder.OrderBy.Price: items = items.OrderBy(r = > r.Price); } } |
Paginated
Реализация
PaginatedSpecification это уже готовый класс, так что наследников делать не надо, а создаем новый экземпляр через конструктор ( CurrentPage и PageSize )
1 |
new PaginatedSpecification(1,10) |
- Current Page – указывает какую страницу выбирать
- Page Size – указывает сколько элементов на странице
Использование
1 |
Repository.Query(paginatedSpecification: new PaginatedSpecification(2, 10)) |
примечание: если в базе данных содержится 50 записей, то вернутся записи с 10 по 20
Linq аналог
1 |
items.Skip((2 - 1) * 10).Take(10) |
Fetch
Реализация
Все спецификации для выборки наследуются от базового класса FetchSpecification<TEntity> ( TEntity – это тип объекта Query ), который имеет абстрактный метод FetchedBy.
1 2 3 4 5 6 7 8 |
public class AdmissionFetchSpec : FetchSpecification<Admission> { public override Action<AdHocFetchSpecification<Admission>> FetchedBy() { return specification = > specification.Join(r = > r.Protocol) .Join(r = > r.ProfessionalCare); } } |
- Join – подгружает элемент по связи One to Many
примечание: можно получать доступ к полям выбранного элемента
1 |
return specification = > specification.Join(r = > r.Item,s= >s.PropertyFromItem) |
- JoinMany – подгружает элементы по связи Many to One / Many to Many
примечание: можно получить доступ к полям каждого выбранного элемента
1 |
return specification = > specification.JoinMany(r = > r.Items,s= >s.PropertyFromItem) |
Linq аналог
Каждый ORM имеет свой способ выборки элементов
Актуальность
- Fetch нужен когда сценарий Query возвращает объекты базы данных, а не подготовленную “плоскую” модель, но после прихода MVD, данный сценарий практический не востребован.
- Основное применение Fetch это возможность ускорить Query за счет выборки всех данных за один запрос, но как показала практика, лучшим решением будет построение OLAP системы ( пример реализации можно посмотреть в Browsio )
примечание: тема OLAP сложная и хорошо освещена в интернете, но в ближайшие время появится статья с обзором решений в рамках Incoding Framework
Features
To applicate Repository effectively in Incoding Framework, it is necessary to consider the following points:
- Repository works in the context of the Unit of Work and therefore the transaction will not be closed until all the code Command is successfully implemented.
1 2 3 4 5 |
public override void Execute() { Repository.Save(new Product()); throw new Exception(); // rollback changes } |
- Query works with Repository in ReadUncommited mode
Note: it says that saving or changes cannot be made in the database
- Connection to the database is available only in the context of Command and Query
Пример
1 2 3 4 5 6 7 8 9 10 11 12 13 |
public class GetGapsQuery : QueryBase<List<GapVm>> { protected override List<GapVm> ExecuteResult() { return Repository.Query<Gap>() .Select(r = > new GapVm() { Id = r.Id.ToString(), Type = r.Type.Name, }) .ToList(); } } |
Опасность представляет gap.Type.Name, потому что мы обращаемся к дочерней таблице, что заставляет ORM ( если не выключено LazyLoad или добавлен соответствующий для поля Fetch ) делать запрос в базу данных, для того, чтобы подгрузить элемент, поэтому если мы вернем List<Gap> на Controller и там обратимся к поле gap.Type.Name, то получим exception.