CRUD Operations Using the Generic Repository Pattern and Unit of Work in MVC

Original author: Sandeep Singh Shekhawat
  • Translation
  • Tutorial

От переводчика


После публикации статьи пошло активное обсуждение. Советую почитать комментарии, так как статья не совсем правильна с технической точки зрения. Я рекомендую ее как материал для рассуждений, а не пособие к действию.

Вступление


Сегодня познакомимся с паттерном «Общий репозиторий» (Generic Repository) и подходом Unit of Work в приложениях ASP.NET MVC. Для этого создадим приложение «Книги», в котором будут реализованы операции создания, получения, обновления и удаления данных. Для упрощения понимания материала пример будет построен на использовании только одной сущности книги.

В предыдущей статье «CRUD Operations Using the Repository Pattern in MVC» мы уже создавали приложение с использованием репозитория для сущности книги, реализующее CRUD операции. Но если мы захотим создать подобное приложение, но уже корпоративного уровня, то нам придется создавать по репозиторию на каждую новую сущность. Проще говоря, мы будем дублировать код, что идет в разрез с подходом DRY — поэтому необходимо воспользоваться паттерном «общий репозиторий».



Теперь посмотрим на картинку. Двапрограммиста, которые мучаются вопросом: создать дублирующий код или же использовать еще раз уже существующий код? Как реализовать первый подход мы уже знаем. Поэтому попробуем разобраться во втором. Поехали.

Паттерн Репозиторий


Этот паттерн предназначен для создания абстракции между слоем доступа к данным и бизнес логикой приложения. Это паттерн доступа к данным, который помогает организовать более слабосвязанный подход. Мы создаем логику доступа к данным в одном классе или их наборе, называемом репозиторий.
В этот раз мы создадим общий репозиторий и класс Unit of work, который будет создавать экземпляр репозитория для каждой сущности. Мы же создадим экземпляр класса UnitOfWork в контроллере, потом создадим экземпляр репозитория, в зависимости от необходимой сущности, ну и в итоги воспользуемся необходимыми методами репозитория.



На следующем изображении показана взаимосвязь между репозиторием и контекстом данных Entity Framework, в котором контроллеры взаимодействуют с репозиторием через Unit of Work, а не непосредственно через Entity Framework.
Теперь давайте разберемся, за что именно отвечает Unit of Work? Он ответственен за создание экземпляра нашего DbContext, в следствие чего, все все репозитории будут использовать один и тот же DbContext для работы с БД. То есть паттерн Unit of Work гарантирует, что все репозитории работают с одним контекстом данных.

Реализация общего репозитория и класса Unit of Work


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

Создаем два проекта: EF.Core и EF.Data. Будем использовать подход «сначала код». EF.Core будет содержать сущности, отображение которых должны быть в БД. В проекте EF.Core создаем две сущности: класс BaseEntity, в котором описываем общие свойства для наследования каждой сущностью и класс Book. Рассмотрим более подробно каждый класс.
Код для BaseEntity выглядит следующим образом

using System;
namespace EF.Core
{
   public abstract class BaseEntity
    {
        public Int64 ID { get; set; }
        public DateTime AddedDate { get; set; }
        public DateTime ModifiedDate { get; set; }
        public string IP { get; set; }
    }
}

Далее создадим класс Book в папке Data проекта EF.Core, который в свою очередь наследует BaseEntity

using System;

namespace EF.Core.Data
{
   public class Book : BaseEntity
    {
       public string Title { get; set; }
       public string Author { get; set; }
       public string ISBN { get; set; }
       public DateTime Published { get; set; }
    }
}

В проекте EF.Data содержатся отображение сущности Book, репозиторий и Unit of Work классы. Подход «сначала код» подразумевает создание класса доступа к данным, который наследует от DbContext, поэтому создаем класс EFDbContext, в котором перепишем метод OnModelCreating(). Этот метод вызывается при инициализации модели контекста и позволяет провести дополнительную ее настройку, но перед ее блокировкой. Пример кода этого класса:

using System;
using System.Data.Entity;
using System.Data.Entity.ModelConfiguration;
using System.Linq;
using System.Reflection;
using EF.Core;

namespace EF.Data
{
   public class EFDbContext : DbContext
    {
       public EFDbContext()
           : base("name=DbConnectionString")
       {
       }

       public new IDbSet<TEntity> Set<TEntity>() where TEntity : BaseEntity
       {
           return base.Set<TEntity>();
       }

       protected override void OnModelCreating(DbModelBuilder modelBuilder)
       {
           var typesToRegister = Assembly.GetExecutingAssembly().GetTypes()
          .Where(type => !String.IsNullOrEmpty(type.Namespace))
          .Where(type => type.BaseType != null && type.BaseType.IsGenericType
               && type.BaseType.GetGenericTypeDefinition() == typeof(EntityTypeConfiguration<>));
           foreach (var type in typesToRegister)
           {
               dynamic configurationInstance = Activator.CreateInstance(type);
               modelBuilder.Configurations.Add(configurationInstance);
           }
           base.OnModelCreating(modelBuilder);
       }
    }
}

Так же следует напомнить, что концепция «сначала код» следует конвенции по конфигурации, поэтому необходимо передать в конструктор строку с названием соединения и которая в точности совпадает с таковой в настройка приложения в файле App.Config. После этих действий возможно автоматическое подключение к серверу БД.
В методе OnModelCreating() используем отражение на карту сущности к классу ее настройки в данном конкретном проекте.


А теперь определим настройки для сущности книги, которые будут использоваться при создании таблицы в БД. Настройки будут находиться в проекте EF.Data, в папке Mapping.

Класс BookMap выглядит так:

using System.ComponentModel.DataAnnotations.Schema;
using System.Data.Entity.ModelConfiguration;
using EF.Core.Data;

namespace EF.Data.Mapping
{
   public class  BookMap : EntityTypeConfiguration<Book>
    {
       public BookMap()
       {
           HasKey(t => t.ID);
           Property(t => t.ID).HasDatabaseGeneratedOption(DatabaseGeneratedOption.Identity);
           Property(t => t.Title).IsRequired();
           Property(t => t.Author).IsRequired();
           Property(t => t.ISBN).IsRequired();
           Property(t => t.Published).IsRequired();
           ToTable("Books");
       }
    }
}

Наступил момент создания общего репозитория. Для упрощения примера не будем создавать интерфейс. В репозитории будут реализованы все операции CRUD. Так же в репозитории будет присутствовать параметризированный конструктор, который будет принимать
Context, поэтому при создании экземпляра объекта репозитория все репозитории будут иметь один и тот же контекст. Мы будем использовать метод saveChanges() контекста, однако можно использовать и метод save() класса Unit of Work, так как у них обоих будет один и тот же контекст данных.

Класс общего репозитория выглядит приблизительно так:

using System;
using System.Data.Entity;
using System.Data.Entity.Validation;
using System.Linq;
using EF.Core;

namespace EF.Data
{
   public class Repository<T> where T : BaseEntity
    {
        private readonly EFDbContext context;
        private IDbSet<T> entities;
        string errorMessage = string.Empty;

        public Repository(EFDbContext context)
        {
            this.context = context;
        }

        public T GetById(object id)
        {
            return this.Entities.Find(id);
        }

        public void Insert(T entity)
        {
            try
            {
                if (entity == null)
                {
                    throw new ArgumentNullException("entity");
                }
                this.Entities.Add(entity);
                this.context.SaveChanges();
            }
            catch (DbEntityValidationException dbEx)
            {

                foreach (var validationErrors in dbEx.EntityValidationErrors)
                {
                    foreach (var validationError in validationErrors.ValidationErrors)
                    {
                        errorMessage += string.Format("Property: {0} Error: {1}",
                        validationError.PropertyName, validationError.ErrorMessage) + Environment.NewLine;
                    }
                }
                throw new Exception(errorMessage, dbEx);
            }
        }

        public void Update(T entity)
        {
            try
            {
                if (entity == null)
                {
                    throw new ArgumentNullException("entity");
                }
                this.context.SaveChanges();
            }
            catch (DbEntityValidationException dbEx)
            {
                foreach (var validationErrors in dbEx.EntityValidationErrors)
                {
                    foreach (var validationError in validationErrors.ValidationErrors)
                    {
                        errorMessage += Environment.NewLine + string.Format("Property: {0} Error: {1}",
                        validationError.PropertyName, validationError.ErrorMessage);
                    }
                }

                throw new Exception(errorMessage, dbEx);
            }
        }

        public void Delete(T entity)
        {
            try
            {
                if (entity == null)
                {
                    throw new ArgumentNullException("entity");
                }

                this.Entities.Remove(entity);
                this.context.SaveChanges();
            }
            catch (DbEntityValidationException dbEx)
            {

                foreach (var validationErrors in dbEx.EntityValidationErrors)
                {
                    foreach (var validationError in validationErrors.ValidationErrors)
                    {
                        errorMessage += Environment.NewLine + string.Format("Property: {0} Error: {1}",
                        validationError.PropertyName, validationError.ErrorMessage);
                    }
                }
                throw new Exception(errorMessage, dbEx);
            }
        }

        public virtual IQueryable<T> Table
        {
            get
            {
                return this.Entities;
            }
        }

        private IDbSet<t> Entities
        {
            get
            {
                if (entities == null)
                {
                    entities = context.Set<t>();
                }
                return entities;
            }
        }
    }
}  

Теперь нужно создать класс Unit of Work. Он наследует от интерфейса IDisposable и будет удаляться в каждом контроллере. Так же этот класс будет инициализировать DataContext приложения. А сердцем этого класса будет метод Repository(), который и будет возвращать репозиторий для нужной сущности, которая в свою очередь наследует от BaseEntity. Смотрим:

using System;
using System.Collections.Generic;
using EF.Core;

namespace EF.Data
{
   public class UnitOfWork : IDisposable
    {
       private readonly EFDbContext context;
       private bool disposed;
       private Dictionary<string,object> repositories;

       public UnitOfWork(EFDbContext context)
       {
           this.context = context;
       }

       public UnitOfWork()
       {
           context = new EFDbContext();
       }

       public void Dispose()
       {
           Dispose(true);
           GC.SuppressFinalize(this);
       }

       public void Save()
       {
           context.SaveChanges();
       }

       public virtual void Dispose(bool disposing)
       {
           if (!disposed)
           {
               if (disposing)
               {
                   context.Dispose();
               }
           }
           disposed = true;
       }

       public Repository<T> Repository<T>() where T : BaseEntity
       {
           if (repositories == null)
           {
               repositories = new Dictionary<string,object>();
           }

           var type = typeof(T).Name;

           if (!repositories.ContainsKey(type))
           {
               var repositoryType = typeof(Repository<>);
               var repositoryInstance = Activator.CreateInstance(repositoryType.MakeGenericType(typeof(T)), context);
               repositories.Add(type, repositoryInstance);
           }
           return (Repository<t>)repositories[type];
       }
    }
}


Приложение MVC использующее паттерн общий репозиторий


Создаем MVC приложение (EF.Web). Это уже будет третий проект в решении, в котором будет содержаться непосредственно пользовательский интерфейс для сущности Book и контроллер для CRUD операций над ней. Создадим контроллер BookController. За каждую CRUD операцию будет отвечать свой ActionResult метод.
В первую очередь создаем экземпляр класса Unit of Work для инициализации конструктором контроллера нужного репозитория для соответствующей сущности.

using System.Collections.Generic;
using System.Linq;
using System.Web.Mvc;
using EF.Core.Data;
using EF.Data;

namespace EF.Web.Controllers
{
    public class BookController : Controller
    {
        private UnitOfWork unitOfWork = new UnitOfWork();
        private Repository<book> bookRepository;

        public BookController()
        {
            bookRepository = unitOfWork.Repository<book>();
        }

        public ActionResult Index()
        {
            IEnumerable<book> books = bookRepository.Table.ToList();
            return View(books);
        }

        public ActionResult CreateEditBook(int? id)
        {
            Book model = new Book();
            if (id.HasValue)
            {
                model = bookRepository.GetById(id.Value);
            }
            return View(model);
        }

        [HttpPost]
        public ActionResult CreateEditBook(Book model)
        {
            if (model.ID == 0)
            {
                model.ModifiedDate = System.DateTime.Now;
                model.AddedDate = System.DateTime.Now;
                model.IP = Request.UserHostAddress;
                bookRepository.Insert(model);
            }
            else
            {
                var editModel = bookRepository.GetById(model.ID);
                editModel.Title = model.Title;
                editModel.Author = model.Author;
                editModel.ISBN = model.ISBN;
                editModel.Published = model.Published;
                editModel.ModifiedDate = System.DateTime.Now;
                editModel.IP = Request.UserHostAddress;
                bookRepository.Update(editModel);
            }

            if (model.ID > 0)
            {
                return RedirectToAction("Index");
            }
            return View(model);
        }

        public ActionResult DeleteBook(int id)
        {
            Book model = bookRepository.GetById(id);
            return View(model);
        }

        [HttpPost,ActionName("DeleteBook")]
        public ActionResult ConfirmDeleteBook(int id)
        {
            Book model = bookRepository.GetById(id);
            bookRepository.Delete(model);
            return RedirectToAction("Index");
        }

        public ActionResult DetailBook(int id)
        {
            Book model = bookRepository.GetById(id);
            return View(model);
        }

        protected override void Dispose(bool disposing)
        {
            unitOfWork.Dispose();
            base.Dispose(disposing);
        }
    }
}

Теперь нужно создать пользовательский интерфейс.

Create / Edit Book View


Создадим бщий вид для создания и редактирования книги CreateEditBook.cshtml. Для выбора даты публикации книги используем date picker. Код для него на JavaScript:

(function ($) {
    function Book() {
        var $thisthis = this;
        function initializeAddEditBook() {
            $('.datepicker').datepicker({
                "setDate": new Date(),
                "autoclose": true
            });
        }
        $this.init = function () {
            initializeAddEditBook();
        }
    }
    $(function () {
        var self = new Book();
        self.init();
    });
}(jQuery))

И непосредственно сам CreateEditBook.cshtml.:

@model EF.Core.Data.Book

@{
    ViewBag.Title = "Create Edit Book";
}
<div class="book-example panel panel-primary">
    <div class="panel-heading panel-head">Add / Edit Book</div>
    <div class="panel-body">
        @using (Html.BeginForm())
        {
            <div class="form-horizontal">
                <div class="form-group">
                    @Html.LabelFor(model => model.Title,
                    new { @class = "col-lg-1 control-label" })
                    <div class="col-lg-9">
                        @Html.TextBoxFor(model => model.Title,
                        new { @class = "form-control" })
                    </div>
                </div>
                <div class="form-group">
                    @Html.LabelFor(model => model.ISBN,
                    new { @class = "col-lg-1 control-label" })
                    <div class="col-lg-9">
                        @Html.TextBoxFor(model => model.ISBN,
                        new { @class = "form-control" })
                    </div>
                </div>
                <div class="form-group">
                    @Html.LabelFor(model => model.Author,
                    new { @class = "col-lg-1 control-label" })
                    <div class="col-lg-9">
                        @Html.TextBoxFor(model => model.Author,
                        new { @class = "form-control" })
                    </div>
                </div>
                <div class="form-group">
                    @Html.LabelFor(model => model.Published,
                    new { @class = "col-lg-1 control-label" })
                    <div class="col-lg-9">
                        @Html.TextBoxFor(model => model.Published,
                        new { @class = "form-control datepicker" })
                    </div>
                </div>
                <div class="form-group">
                    <div class="col-lg-8"></div>
                    <div class="col-lg-3">
                        @Html.ActionLink("Back to List", "Index",
                        null, new { @class = "btn btn-default" })
                        <button class="btn btn-success"
                        id="btnSubmit" type="submit">
                            Submit
                        </button>
                    </div>
                </div>
            </div>
        }
    </div>
</div>
@section scripts
{
    <script src="~/Scripts/bootstrap-datepicker.js" type="text/javascript"></script>
    <script src="~/Scripts/book-create-edit.js" type="text/javascript"></script>
}


После запуска должны увидеть нечто, похожее на картинку.


Book List View


Эта страница – индекс. Точка входа. Выводим список всех книг.

@model IEnumerable<EF.Core.Data.Book>
@using EF.Web.Models

<div class="book-example panel panel-primary">
    <div class="panel-heading panel-head">Books Listing</div>
    <div class="panel-body">
        <a id="createEditBookModal"
        href="@Url.Action("CreateEditBook")" class="btn btn-success">
            <span class="glyphicon glyphicon-plus"></span>Book
        </a>

        <table class="table" style="margin: 4px">
            <tr>
                <th>
                    @Html.DisplayNameFor(model => model.Title)
                </th>
                <th>
                    @Html.DisplayNameFor(model => model.Author)
                </th>
                <th>
                    @Html.DisplayNameFor(model => model.ISBN)
                </th>
                <th>Action
                </th>

                <th></th>
            </tr>
            @foreach (var item in Model)
            {
                <tr>
                    <td>
                        @Html.DisplayFor(modelItem => item.Title)
                    </td>
                    <td>
                        @Html.DisplayFor(modelItem => item.Author)
                    </td>
                    <td>
                        @Html.DisplayFor(modelItem => item.ISBN)
                    </td>
                    <td>
                        @Html.ActionLink("Edit", "CreateEditBook",
                        new { id = item.ID }, new { @class = "btn btn-success" }) |
                        @Html.ActionLink("Details", "DetailBook",
                        new { id = item.ID }, new { @class = "btn btn-primary" }) |
                        @Html.ActionLink("Delete", "DeleteBook",
                        new { id = item.ID }, new { @class = "btn btn-danger" })
                    </td>
                </tr>
            }

        </table>
    </div>
</div>



Ели же нажать на кнопку редактировать то увидим:


Book Detail View


Подробности по выбранной книге.

@model EF.Core.Data.Book
@{
    ViewBag.Title = "Detail Book";
}
<div class="book-example panel panel-primary">
    <div class="panel-heading panel-head">Book Detail</div>
    <div class="panel-body">
        <div class="form-horizontal">
            <div class="form-group">
                @Html.LabelFor(model => model.Title, new { @class = "col-lg-1 control-label" })
                <div class="col-lg-9">
                    @Html.DisplayFor(model => model.Title, new { @class = "form-control" })
                </div>
            </div>

            <div class="form-group">
                @Html.LabelFor(model => model.Author, new { @class = "col-lg-1 control-label" })
                <div class="col-lg-9">
                    @Html.DisplayFor(model => model.Author, new { @class = "form-control" })
                </div>
            </div>

            <div class="form-group">
                @Html.LabelFor(model => model.ISBN, new { @class = "col-lg-1 control-label" })
                <div class="col-lg-9">
                    @Html.DisplayFor(model => model.ISBN, new { @class = "form-control" })
                </div>
            </div>

            <div class="form-group">
                @Html.LabelFor(model => model.Published, new { @class = "col-lg-1 control-label" })
                <div class="col-lg-9">
                    @Html.DisplayFor(model => model.Published, new { @class = "form-control" })
                </div>
            </div>

            <div class="form-group">
                @Html.LabelFor(model => model.AddedDate, new { @class = "col-lg-1 control-label" })
                <div class="col-lg-9">
                    @Html.DisplayFor(model => model.AddedDate, new { @class = "form-control" })
                </div>
            </div>

            <div class="form-group">
                @Html.LabelFor(model => model.ModifiedDate, new { @class = "col-lg-1 control-label" })
                <div class="col-lg-9">
                    @Html.DisplayFor(model => model.ModifiedDate, new { @class = "form-control" })
                </div>
            </div>

            <div class="form-group">
                @Html.LabelFor(model => model.IP, new { @class = "col-lg-1 control-label" })
                <div class="col-lg-9">
                    @Html.DisplayFor(model => model.IP, new { @class = "form-control" })
                </div>
            </div>

            @using (Html.BeginForm())
            {
                <div class="form-group">
                    <div class="col-lg-1"></div>
                    <div class="col-lg-9">
                        @Html.ActionLink("Edit", "CreateEditBook",
                        new { id = Model.ID }, new { @class = "btn btn-primary" })
                        @Html.ActionLink("Back to List", "Index",
                        null, new { @class = "btn btn-success" })
                    </div>
                </div>
            }
        </div>
    </div>
</div>   




Delete Book


При нажатии на кнопку удалить попадаем на страничку с общей информацией по книге и кнопкой подтверждения удаления. Используем HttpGet. При нажатии на кнопку подтверждения используем HttpPost.

@model EF.Core.Data.Book

@{
    ViewBag.Title = "Delete Book";
}

<div class="book-example panel panel-primary">
    <div class="panel-heading panel-head">Delete Book</div>
    <div class="panel-body">
        <h3>Are you sure you want to delete this?</h3>
        <h1>@ViewBag.ErrorMessage</h1>
        <div class="form-horizontal">
            <div class="form-group">
                @Html.LabelFor(model => model.Title,
                new { @class = "col-lg-1 control-label" })
                <div class="col-lg-9">
                    @Html.DisplayFor(model => model.Title,
                    new { @class = "form-control" })
                </div>
            </div>

            <div class="form-group">
                @Html.LabelFor(model => model.Author,
                new { @class = "col-lg-1 control-label" })
                <div class="col-lg-9">
                    @Html.DisplayFor(model => model.Author,
                    new { @class = "form-control" })
                </div>
            </div>

            <div class="form-group">
                @Html.LabelFor(model => model.ISBN,
                new { @class = "col-lg-1 control-label" })
                <div class="col-lg-9">
                    @Html.DisplayFor(model => model.ISBN,
                    new { @class = "form-control" })
                </div>
            </div>

            <div class="form-group">
                @Html.LabelFor(model => model.Published,
                new { @class = "col-lg-1 control-label" })
                <div class="col-lg-9">
                    @Html.DisplayFor(model => model.Published,
                    new { @class = "form-control" })
                </div>
            </div>

            <div class="form-group">
                @Html.LabelFor(model => model.AddedDate,
                new { @class = "col-lg-1 control-label" })
                <div class="col-lg-9">
                    @Html.DisplayFor(model => model.AddedDate,
                    new { @class = "form-control" })
                </div>
            </div>

            <div class="form-group">
                @Html.LabelFor(model => model.ModifiedDate,
                new { @class = "col-lg-1 control-label" })
                <div class="col-lg-9">
                    @Html.DisplayFor(model => model.ModifiedDate,
                    new { @class = "form-control" })
                </div>
            </div>

            <div class="form-group">
                @Html.LabelFor(model => model.IP,
                new { @class = "col-lg-1 control-label" })
                <div class="col-lg-9">
                    @Html.DisplayFor(model => model.IP,
                    new { @class = "form-control" })
                </div>
            </div>

            @using (Html.BeginForm())
            {
                <div class="form-group">
                    <div class="col-lg-1"></div>
                    <div class="col-lg-9">
                        <input type="submit" value="Delete"
                        class="btn btn-danger" />
                        @Html.ActionLink("Back to List", "Index",
                        null, new { @class = "btn btn-success" })
                    </div>
                </div>
            }
        </div>
    </div>
</div>

Share post
AdBlock has stolen the banner, but banners are not teeth — they will be back

More
Ads

Comments 18

    +4
    О! Как я такое люблю)

    В синтетических тестах всё ок, а когда пройдёт год разработки и система усложнится будут кровавые слёзы.
      0
      Да там и тестов-то нет никаких.
        –2
        Ваше заявление безосновательно, кроме использования непонятных абстракций. Я вам скажу так:
        1. Я не имею отношения к автору статьи или переводчику;
        2. Наша система I-LDS, можете её найти не просторе интернета, содержит больше 350 проектов в основном solution и развивается больше 8 лет коллективом из 10-15 человек, данная система содержит и подобные вещи, по работе с данными. Правда BLToolkit, да и без UOfW, который тут тоже диспосится, но Save не вызывает нигде, насколько я вижу:)
        Где мы не правы?:)
          0
          данная система содержит и подобные вещи, по работе с данными. Правда BLToolkit, да и без UOfW

          Какие «подобные», учитывая, что UoW у вас, по вашим же словам, нет, а BLToolkit с EF ничего общего не имеет (в частности, ни object map, ни change tracking в нем, когда я последний раз смотрел, не было)?
            0
            я о общем подходе, а не о частностях реализации. Вы видимо давно смотрели BLTOolkitr, ибо EditableObject там прилично давно, хотя я и стараюсь их не использовать, а реализую свою абстракцию
              +1
              Тут вся статья — это частности реализации, а общий подход-то и не озвучен. Вы о каком «общем подходе» говорите?
                0
                Я говорю о использовании репозитариев, но расширенных, с различными функциями, минуя UoW, но иногда используя EditableObject
                Мне показалось, что статья именно об этом
                  +2
                  Вам показалось. В этой статье тривиальный обобщенный (то есть заведомо без специфичных расширений) репозиторий, который не имеет ровным счетом никакого added value. При этом в статье как раз есть (избыточный) UoW и никак не упоминается change tracking (хотя он и есть, благодаря чему и возникают описанные мной побочные эффекты). Так что вы описываете подход противоположный тому, что в статье; общего у них только слово «репозиторий» (да и то...).
        +5
        Вся эта статья вызывает один большой вопрос: «зачем?»

        Окей, если вы не верите, то вот много маленьких вопросов «зачем»:
        Он ответственен за создание экземпляра нашего DbContext, в следствие чего, все все репозитории будут использовать один и тот же DbContext для работы с БД. То есть паттерн Unit of Work гарантирует, что все репозитории работают с одним контекстом данных.

        А зачем нужно, чтобы все репозитории работали с одним и тем же контекстом данных? И почему это нельзя сделать без UoW?

        класс BaseEntity, в котором описываем общие свойства для наследования каждой сущностью

        А зачем нужен этот класс? Особенно учитывая, что:

        public abstract class BaseEntity
            {
                public Int64 ID { get; set; }
                public DateTime AddedDate { get; set; }
                public DateTime ModifiedDate { get; set; }
                public string IP { get; set; }
            }
        

        но
        public class Repository<T> where T : BaseEntity
            {
                public T GetById(object id)
                {
                    return this.Entities.Find(id);
                }
            }
        

        То есть даже навязанные свойства не используются.

        public class EFDbContext : DbContext
            {
               protected override void OnModelCreating(DbModelBuilder modelBuilder)
               {
                   var typesToRegister = Assembly.GetExecutingAssembly().GetTypes()
               }
            }
        

        Во-первых, снова не объяснили, зачем. А во-вторых — для какого EF это написано? В шестом есть прекрасный метод AddFromAssembly.

        В репозитории будут реализованы все операции CRUD.

        А вот теперь — самое важное «зачем». Скажите, пожалуйста, зачем нужен репозиторий, который не делает ничего, чего не делал бы DbContext? От текущей абстракции IQueryable вы не избавляетесь — она у вас явно выставлена наружу. Никакой полезной работы (удаления по id или обновления отсоединенной сущности) у вас нет. В чем смысл? На пустом месте сделано два слоя (repository и unit of work), хотя они прекрасно делаются четырьмя generic-методами в контексте.

        Ну и еще не «зачем», а просто так:
        На следующем изображении показана взаимосвязь между репозиторием и контекстом данных Entity Framework, в котором контроллеры взаимодействуют с репозиторием через Unit of Work, а не непосредственно через Entity Framework.

        Вот только на картинке это не нарисовано. Нельзя взаимодействовать с репозиторием через EF, потому что репозиторий — это абстракция над EF, а не наоборот.

        Так же следует напомнить, что концепция «сначала код» следует конвенции по конфигурации, поэтому необходимо передать в конструктор строку с названием соединения и которая в точности совпадает с таковой в настройка приложения в файле App.Config.

        Вообще-то можно просто передать строку подключения.

        public class  BookMap : EntityTypeConfiguration<Book>
            {
               public BookMap()
               {
                   HasKey(t => t.ID);
                   Property(t => t.ID).HasDatabaseGeneratedOption(DatabaseGeneratedOption.Identity);
                   Property(t => t.Title).IsRequired();
                   Property(t => t.Author).IsRequired();
                   Property(t => t.ISBN).IsRequired();
                   Property(t => t.Published).IsRequired();
                   ToTable("Books");
               }
            }
        

        Так делать тоже не надо. Это приводит к тому, что на уровне БД EF считает, что колонки обязательные, а на уровне модели — нет. Намного правильнее разметить свойства модели атрибутом Required, после чего не только EF автоматически подтянет валидацию (и свойства БД), но и все остальные слои приложения — тоже.

        Мы будем использовать метод saveChanges() контекста, однако можно использовать и метод save() класса Unit of Work, так как у них обоих будет один и тот же контекст данных.

        Нет, нельзя. Изнутри репозитория нельзя использовать метод Save от UoW, потому что репозиторий ничего не знает о UoW. А для любого потребителя метод Save от UoW не имеет смысл, потому что все изменения всегда сохраняются внутри репозитория.

        public class UnitOfWork : IDisposable
            {
               public Repository<T> Repository<T>() where T : BaseEntity
               {
                   if (repositories == null)
                   {
                       repositories = new Dictionary<string,object>();
                   }
        
                   var type = typeof(T).Name;
        
                   if (!repositories.ContainsKey(type))
                   {
                       var repositoryType = typeof(Repository<>);
                       var repositoryInstance = Activator.CreateInstance(repositoryType.MakeGenericType(typeof(T)), context);
                       repositories.Add(type, repositoryInstance);
                   }
                   return (Repository<t>)repositories[type];
               }
            }

        Какая религия запрещает сделать new Repository<T>(context)? Зачем нужен словарь репозиториев (да еще и по именам без пространств имен, так что коллизии прямо за углом)?

        И там же:
               public void Save()
               {
                   context.SaveChanges();
               }
        
               public void Dispose()
               {
                   Dispose(true);
                   GC.SuppressFinalize(this);
               }
        
               public virtual void Dispose(bool disposing)
               {
                   if (!disposed)
                   {
                       if (disposing)
                       {
                           context.Dispose();
                       }
                   }
                   disposed = true;
               }
        

        Метод Save, как уже говорилось, не нужен. А всю реализацию Dispose можно заменить одной строчкой: if (context != null) context.Dispose() (да, с обязательной проверкой).
          0
          Это перевод одной статьи из цикла. Там есть развитие этого подхода дальше. Мне было интересно с этим ознакомиться, и может кому — то еще пригодится, потому и перевел… Ваши замечания интересны. Благодарю. Будет время переведу и остальные.
            +4
            Эта статья бессмысленна — именно тем, что в ней не объясняется, зачем что-то делать. Более того, она еще и опасна — потому что в ней есть архитектурные ошибки.

            А где там, простите, развитие подхода, учитывая, что этой статье меньше двух недель, и следующей за ней нет?
              0
              Думаю выйдет.
              А коменты уже поучительней статьи.
                0
                «Думаю, выйдет» — это совсем не то же самое, что «есть развитие подхода дальше».

                А «комменты уже поучительнее статьи» — это хороший повод добавить к статье сверху жирное: «в комментах рассказывают, почему так, как в статье, делать не надо». Потому что иначе никто до них не дочитает.
          +2
          Кстати, поучительный пример того, чего можно достичь таким «прекрасным» подходом.

          var productRepository = uow.Repository<Product>();
          var product = productRepository.GetById(15);
          product.Name = "Good";
          
          var sellerRepository = uow.Repository<Seller>();
          var seller = sellerRepository.GetById(1);
          seller.Name = "Ouch!";
          
          productRepository.Update(product);
          


          Внимание, вопрос: как сейчас в БД назвается продавец с идентификатором «1»?
            +2
            Добавлю критики.

            1. У репозитория нет интерфейса, и ни слова про тесты. А ведь именно модульные тесты, которые необходимо абстрагировать от контекста БД, и являются единственным оправданием существованию репозиториев как отдельной сущности. Во всех остальных случаях очень хорошей реализацией репозитория является IDbSet<>, а удовлетворительной реализацией UnitOfWork — сам контекст.

            2. Ни слова про транзакции — между тем, многого без транзакций не сделать. Использование TransactionScope при работе с всего одной базой — не лучший вариант, поскольку при каждом чихе транзакция пытается стать распределенной. Следовало бы либо добавить транзакции в общий UoW — либо сделать отдельный UoW с транзакциями.

            3. Что вообще в UoW делает фабрика репозиториев? UoW не должен решать никаких задач, кроме управления записью в БД.

            4. UoW не должен вызывать Dispose у контекста! Общее правило: не ты порождал — не тебе и убивать. Исключение — явная спецификация владения, такая как Owned<> в Autofac.

            4+. Что будет, если к одному и тому же контексту будут применены два UoW — но у первого не будет вызван метод сохранения? Ответ: сохранение произойдет во втором. А это — совсем не то, что ожидалось. Поэтому UoW должен либо производить очистку контекста (а это нетривиальная задача, хотя и не сильно сложная), либо просто не создаваться на «грязном» контексте (защитное программирование).

            Также нельзя забывать и про защиту контекста от изменений за пределами UoW или после сохранения. Разумеется, все это актуально только лишь если контекст не разрушается при выходе из UoW.

            5. Если у репозитория отобрать функцию сохранения (потому что это вообще-то задача UoW) — то метод Update окажется пустым. Поскольку EF делает проверку всех сущностей на изменения, он не нужен.

            6. Репозиторию не хватает кучи действительно полезных операций. К примеру, для многоуровневой архитектуры очень пригодилась бы операция Attach.

            А еще мне нравится операция, которую я называю Fetch. Она работает так же, как и GetById — но возвращает не объект, а IQueryable<>, содержащий этот единственный объект. Смысл в том, что вокруг такой операции можно нарастить сложный запрос, который в противном случае привел бы к множественным срабатываниям ленивой загрузки вложенных записей.

            Еще вариант этой операции принимает не id сущности, а саму сущность. Смысл в том, что для сущности, загруженной из БД, делается запрос — а для сущности, которая была добавлена в репозиторий, но еще не сохранена, возвращается массив. Это упрощает многие операции

            7. Класс BookMap не имеет особого смысла, занимая место в проекте. Такие классы хорошо смотрятся, когда сущности были сгенерированы по готовой БД — но не в Code First. Когда сущностей становится 50, лишние файлы начинают сильно мешаться. Как уже было написано, обязательность поля желательно указывать атрибутом, про поле Id можно написать конвенцию или так же использовать атрибут, ну а имя таблицы указывать вообще необязательно.
              0
              Одному мне кажется, что в примере можно полностью выпилить UOW и репозиторий и заменить на прямое обращение к контексту и DbSet, и функционал не изменится?
                0
                Нет. Выше уже написано.
                +1
                Просто статья, перевод гайда индуса www.codeproject.com/Articles/814768/CRUD-Operations-Using-the-Generic-Repository-Patte
                Этим я думаю все сказано ;)

                Only users with full accounts can post comments. Log in, please.