Как стать автором
Обновить

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

Время на прочтение 14 мин
Количество просмотров 57K
Автор оригинала: Sandeep Singh Shekhawat

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


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

Вступление


Сегодня познакомимся с паттерном «Общий репозиторий» (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>

Теги:
Хабы:
+3
Комментарии 20
Комментарии Комментарии 20

Публикации

Истории

Работа

Ближайшие события

Московский туристический хакатон
Дата 23 марта – 7 апреля
Место
Москва Онлайн
Геймтон «DatsEdenSpace» от DatsTeam
Дата 5 – 6 апреля
Время 17:00 – 20:00
Место
Онлайн