Реализация уровня доступа к данным на Entity Framework Code First

от автора

Приветствую!

В данном топике я хочу поговорить о слое доступа к данным (Data Access Level) по отношению к Entity Framework-у, далее EF, о том какие задачи стояли и как я их решил. Весь представленных код из поста, а также прикрепленный демо проект публикуется под либеральной лицензией MIT, то есть вы можете использовать код как вам угодно.
Сразу хочу подчеркнуть, что весь представленный код представляет собой законченное решение и используется более 2-х лет в проекте для достаточно крупной российский компании, но тем не менее не подходит для высоконагруженных систем.

Подробности под катом.

Задачи

При написании приложения, передо мной стояло несколько задач по отношению к слою доступа к данным:
1. Все изменения данных должны логироваться, включая информацию о том какой именно пользователь это сделал
2. Использование паттерна «Репозиторий»
3. Контроль над изменением объектов, то есть если мы хотим обновить в базе данных только один объект, то должен именно один объект.
Поясню:
По умолчанию, EF отслеживает изменения всех объектов в рамках конкретного контекста, при этом возможность сохранить один объект отсутствует, в отличии от NHibernate. Такая ситуация чревата различного рода неприятными ошибками. Например, пользователь редактирует одновременно два объекта, но хочет сохранить только один. В случае, если эти два объекта связанны с один контекстом базы данных, EF сохранит изменения обоих объектов.

Решение

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

UsersContext

namespace TestApp.Models {     public partial class UsersContext : DbContext     {          public UsersContext()             : base("Name=UsersContext")         {         }          public DbSet<User> Users { get; set; }          protected override void OnModelCreating(DbModelBuilder modelBuilder)         {             modelBuilder.Configurations.Add(new UserMap());         }     } } 

Расширим его с помощью следующего интерфейса:

IDbContext

    public interface IDbContext     {         IQueryable<T> Find<T>() where T : class;          void MarkAsAdded<T>(T entity) where T : class;          void MarkAsDeleted<T>(T entity) where T : class;          void MarkAsModified<T>(T entity) where T : class;          void Commit(bool withLogging);         //откатывает изменения во всех модифицированных объектах         void Rollback();          // включает или отключает отслеживание изменений объектов         void EnableTracking(bool isEnable);          EntityState GetEntityState<T>(T entity) where T : class;          void SetEntityState<T>(T entity, EntityState state) where T : class;          // возвращает объект содержащий список объектов с их состоянием         DbChangeTracker GetChangeTracker();          DbEntityEntry GetDbEntry<T>(T entity) where T : class;     } 

Получившийся модифицированный DbContext:

DemoAppDbContext

namespace DataAccess.DbContexts {     public class DemoAppDbContext : DbContext, IDbContext     {         public static User CurrentUser { get; set; }          private readonly ILogger _logger;          #region Context Entities          public DbSet<EntityChange> EntityChanges { get; set; }          public DbSet<User> Users { get; set; }          #endregion          static DemoAppDbContext()         {             //устанавливаем инициализатор             Database.SetInitializer(new CreateDBContextInitializer());         }          // метод вызывается при создании базы данных         public static void Seed(DemoAppDbContext context)         {             // добавляем пользователя по умолчанию             var defaultUser = new User { Email = "UserEmail@email.ru", Login = "login", IsBlocked = false, Name = "Vasy Pupkin" };             context.Users.Add(defaultUser);             context.SaveChanges();         }          public DemoAppDbContext(string nameOrConnectionString)             : base(nameOrConnectionString)         {            // инициализация логгера             _logger = new Logger(this);         }          protected override void OnModelCreating(DbModelBuilder modelBuilder)         {              modelBuilder.Configurations.Add(new EntityChangeMap());             modelBuilder.Configurations.Add(new UserMap());         }          public void MarkAsAdded<T>(T entity) where T : class         {             Entry(entity).State = EntityState.Added;             Set<T>().Add(entity);         }          public void MarkAsDeleted<T>(T entity) where T : class         {             Attach(entity);             Entry(entity).State = EntityState.Deleted;             Set<T>().Remove(entity);         }          public void MarkAsModified<T>(T entity) where T : class         {             Attach(entity);             Entry(entity).State = EntityState.Modified;         }          public void Attach<T>(T entity) where T : class         {             if (Entry(entity).State == EntityState.Detached)             {                 Set<T>().Attach(entity);             }         }          public void Commit(bool withLogging)         {             BeforeCommit();             if (withLogging)             {                 _logger.Run();             }             SaveChanges();         }          private void BeforeCommit()         {             UndoExistAddedEntitys();         }          //исправление ситуации, когда у есть объекты помеченные как  новые, но при этом существующие в базе данных         private void UndoExistAddedEntitys()         {             IEnumerable<DbEntityEntry> dbEntityEntries = GetChangeTracker().Entries().Where(x => x.State == EntityState.Added);             foreach (var dbEntityEntry in dbEntityEntries)             {                 if (GetKeyValue(dbEntityEntry.Entity) > 0)                 {                     SetEntityState(dbEntityEntry.Entity, EntityState.Unchanged);                 }             }         }          // откат всех изменений в объектах         public void Rollback()         {             ChangeTracker.Entries().ToList().ForEach(x => x.Reload());         }          public void EnableTracking(bool isEnable)         {             Configuration.AutoDetectChangesEnabled = isEnable;         }          public void SetEntityState<T>(T entity, EntityState state) where T : class         {             Entry(entity).State = state;         }          public DbChangeTracker GetChangeTracker()         {             return ChangeTracker;         }          public EntityState GetEntityState<T>(T entity) where T : class         {             return Entry(entity).State;         }          public IQueryable<T> Find<T>() where T : class         {             return Set<T>();         }          public DbEntityEntry GetDbEntry<T>(T entity) where T : class         {             return Entry(entity);         }          public static int GetKeyValue<T>(T entity) where T : class         {             var dbEntity = entity as IDbEntity;             if (dbEntity == null)                 throw new ArgumentException("Entity should be IDbEntity type - " + entity.GetType().Name);              return dbEntity.GetPrimaryKey();         }     } } 

Взаимодействие с объектами базы данных происходит через репозитории специфичные для каждого объекта. Все репозитории наследуют базовый класс, который предоставляет базовый CRUD функционал

IRepository

    interface IRepository<T> where T : class     {         DemoAppDbContext CreateDatabaseContext();          List<T> GetAll();          T Find(int entityId);          T SaveOrUpdate(T entity);          T Add(T entity);          T Update(T entity);          void Delete(T entity);          // возвращает список ошибок         DbEntityValidationResult Validate(T entity);         // возвращает строку с ошибками        string ValidateAndReturnErrorString(T entity, out bool isValid);     } 

Реализация IRepository:

BaseRepository

namespace DataAccess.Repositories {     public abstract class BaseRepository<T> : IRepository<T> where T : class     {         private readonly IContextManager _contextManager;          protected BaseRepository(IContextManager contextManager)         {             _contextManager = contextManager;         }          public DbEntityValidationResult Validate(T entity)         {             using (var context = CreateDatabaseContext())             {                 return context.Entry(entity).GetValidationResult();             }         }          public string ValidateAndReturnErrorString(T entity, out bool isValid)         {             using (var context = CreateDatabaseContext())             {                 DbEntityValidationResult dbEntityValidationResult = context.Entry(entity).GetValidationResult();                 isValid = dbEntityValidationResult.IsValid;                 if (!dbEntityValidationResult.IsValid)                 {                     return DbValidationMessageParser.GetErrorMessage(dbEntityValidationResult);                 }                 return string.Empty;             }         }          // создание контекста базы данных. необходимо использовать using         public DemoAppDbContext CreateDatabaseContext()         {             return _contextManager.CreateDatabaseContext();         }                public List<T> GetAll()         {             using (var context = CreateDatabaseContext())             {                 return context.Set<T>().ToList();             }         }          public T Find(int entityId)         {             using (var context = CreateDatabaseContext())             {                 return context.Set<T>().Find(entityId);             }         }          // виртуальный метод. вызывает перед сохранением объектов, может быть определен в дочерних классах         protected virtual void BeforeSave(T entity, DemoAppDbContext db)         {                      }          public T SaveOrUpdate(T entity)         {             var iDbEntity = entity as IDbEntity;              if (iDbEntity == null)                 throw new ArgumentException("entity should be IDbEntity type", "entity");              return iDbEntity.GetPrimaryKey() == 0 ? Add(entity) : Update(entity);         }                public T Add(T entity)         {             using (var context = CreateDatabaseContext())             {                 BeforeSave(entity, context);                 context.MarkAsAdded(entity);                 context.Commit(true);             }             return entity;         }          public T Update(T entity)         {             using (var context = CreateDatabaseContext())             {                 var iDbEntity = entity as IDbEntity;                 if (iDbEntity == null)                     throw new ArgumentException("entity should be IDbEntity type", "entity");                  var attachedEntity = context.Set<T>().Find(iDbEntity.GetPrimaryKey());                 context.Entry(attachedEntity).CurrentValues.SetValues(entity);                                BeforeSave(attachedEntity, context);                 context.Commit(true);             }             return entity;         }          public void Delete(T entity)         {             using (var context = CreateDatabaseContext())             {                 context.MarkAsDeleted(entity);                 context.Commit(true);             }         }     } } 

Объект базы данных User:

User

namespace DataAccess.Models {     public class User : IDbEntity     {         public User()         {             this.EntityChanges = new List<EntityChange>();         }          public int UserId { get; set; }          [Required(AllowEmptyStrings = false, ErrorMessage = @"Please input Login")]         [StringLength(50, ErrorMessage = @"Login должен быть меньше 50 символов")]         public string Login { get; set; }          [Required(AllowEmptyStrings = false, ErrorMessage = @"Please input Email")]         [StringLength(50, ErrorMessage = @"Email должен быть меньше 50 символов")]         public string Email { get; set; }          [Required(AllowEmptyStrings = false, ErrorMessage = @"Please input Name")]         [StringLength(50, ErrorMessage = @"Имя должно быть меньше 50 символов")]         public string Name { get; set; }          public bool IsBlocked { get; set; }          public virtual ICollection<EntityChange> EntityChanges { get; set; }          public override string ToString()         {             return string.Format("Тип: User; Название:{0}, UserId:{1} ", Name, UserId);         }          public int GetPrimaryKey()         {             return UserId;         }     } } 

Репозиторий для объекта «User», c рядом дополнительных методов расширяющий стандартный CRUD функционал:

UsersRepository

namespace DataAccess.Repositories {     public class UsersRepository : BaseRepository<User>     {         public UsersRepository(IContextManager contextManager)             : base(contextManager)         {          }          public User FindByLogin(string login)         {             using (var db = CreateDatabaseContext())             {                 return db.Set<User>().FirstOrDefault(u => u.Login == login);             }         }          public bool ExistUser(string login)         {             using (var db = CreateDatabaseContext())             {                 return db.Set<User>().Count(u => u.Login == login) > 0;             }         }          public User GetByUserId(int userId)         {             using (var db = CreateDatabaseContext())             {                 return db.Set<User>().SingleOrDefault(c => c.UserId == userId);             }          }          public User GetFirst()         {             using (var db = CreateDatabaseContext())             {                 return db.Set<User>().First();             }         }     } } 

В моем случае, все репозитории инициализируются один раз и добавляются в простейший самописный service locator RepositoryContainer. Это сделало для возможности написания тестов.

RepositoryContainer

namespace DataAccess.Container {     public class RepositoryContainer     {         private readonly IContainer _repositoryContainer = new Container();          public static readonly RepositoryContainer Instance = new RepositoryContainer();          private RepositoryContainer()         {                      }          public T Resolve<T>() where T : class         {             return _repositoryContainer.Resolve<T>();         }          public void Register<T>(T entity) where T : class         {             _repositoryContainer.Register(entity);         }     } }  namespace DataAccess.Container {     public static class RepositoryContainerFactory     {         public static void RegisterAllRepositories(IContextManager dbContext)         {             RepositoryContainer.Instance.Register(dbContext);             RepositoryContainer.Instance.Register(new EntityChangesRepository(dbContext));             RepositoryContainer.Instance.Register(new UsersRepository(dbContext));         }     } } 

Всем репозиториям, при инициализации передается объект IContextManager, это сделано для возможности работы с несколькими контекстами и их централизованным созданием:

IContextManager

namespace DataAccess.Interfaces {     public interface IContextManager     {         DemoAppDbContext CreateDatabaseContext();     } } 

И его реализация ContextManager:

ContextManager

using DataAccess.Interfaces;  namespace DataAccess.DbContexts {     public class ContextManager : IContextManager     {         private readonly string _connectionString;          public ContextManager(string connectionString)         {             _connectionString = connectionString;         }          public DemoAppDbContext CreateDatabaseContext()         {             return new DemoAppDbContext(_connectionString);         }     } } 

Логирование происходит в объекте реализующем интерфейс ILogger:

ILogger

namespace DataAccess.Interfaces {     internal interface ILogger     {         void Run();     } } 

Реализация интерфейса ILogger

Logger

 public class Logger : ILogger     {         Dictionary<EntityState, string> _operationTypes;          private readonly IDbContext _dbContext;          public Logger(IDbContext dbContext)         {             _dbContext = dbContext;             InitOperationTypes();         }          public void Run()         {             LogChangedEntities(EntityState.Added);             LogChangedEntities(EntityState.Modified);             LogChangedEntities(EntityState.Deleted);         }          private void InitOperationTypes()         {             _operationTypes = new Dictionary<EntityState, string>                 {                     {EntityState.Added, "Добавление"},                     {EntityState.Deleted, "Удаление"},                     {EntityState.Modified, "Изменение"}                 };         }          private string GetOperationName(EntityState entityState)         {             return _operationTypes[entityState];         }          private void LogChangedEntities(EntityState entityState)         {             IEnumerable<DbEntityEntry> dbEntityEntries = _dbContext.GetChangeTracker().Entries().Where(x => x.State == entityState);             foreach (var dbEntityEntry in dbEntityEntries)             {                 LogChangedEntitie(dbEntityEntry, entityState);             }         }          private void LogChangedEntitie(DbEntityEntry dbEntityEntry, EntityState entityState)         {             string operationHash = HashGenerator.GenerateHash(10);             int enitityId = DemoAppDbContext.GetKeyValue(dbEntityEntry.Entity);              Type type = dbEntityEntry.Entity.GetType();              IEnumerable<string> propertyNames = entityState == EntityState.Deleted                                                     ? dbEntityEntry.OriginalValues.PropertyNames                                                     : dbEntityEntry.CurrentValues.PropertyNames;              foreach (var propertyName in propertyNames)             {                 DbPropertyEntry property = dbEntityEntry.Property(propertyName);                  if (entityState == EntityState.Modified && !property.IsModified)                     continue;                  _dbContext.MarkAsAdded(new EntityChange                 {                     UserId = DemoAppDbContext.CurrentUser.UserId,                     Created = DateTime.Now,                     OperationHash = operationHash,                     EntityName = string.Empty,                     EntityType = type.ToString(),                     EntityId = enitityId.ToString(),                     PropertyName = propertyName,                     OriginalValue =                         entityState != EntityState.Added && property.OriginalValue != null                             ? property.OriginalValue.ToString()                             : string.Empty,                     ModifyValue =                         entityState != EntityState.Deleted && property.CurrentValue != null                         ? property.CurrentValue.ToString()                         : string.Empty,                     OperationType = GetOperationName(entityState),                 });             }         }     } 

Использование

Для того чтобы начать работать с базой данных, в приложении необходимо инициализовать фабрику репозиториев:

RepositoryContainerFactory.RegisterAllRepositories(new ContextManager(Settings.Default.DBConnectionString)); 

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

InitDefaultUser

private void InitDefaultUser() { 	User defaultUser = RepositoryContainer.Instance.Resolve<UsersRepository>().GetFirst(); 	DemoAppDbContext.CurrentUser = defaultUser; } 

Вызов к методов репозитория происходит через получение экземпляра у service locator-a. В приведенном ниже примере, обращение идет к методу GetFirst() репозитория типа UsersRepository:

User defaultUser = RepositoryContainer.Instance.Resolve<UsersRepository>().GetFirst(); 

Добавление нового пользователя:

var newUser = new User { Email = "UserEmail@email.ru", Login = "login", IsBlocked = false, Name = "Vasy Pupkin"}; RepositoryContainer.Instance.Resolve<UsersRepository>().SaveOrUpdate(newUser); 

Валидация перед сохранением объектов

Валидация и получение списка ошибок:

var newUser = new User { Email = "UserEmail@email.ru",  IsBlocked = false,  }; DbEntityValidationResult dbEntityValidationResult = RepositoryContainer.Instance.Resolve<UsersRepository>().Validate(newUser); 

Получение строки с ошибками:

var newUser = new User { Email = "UserEmail@email.ru", IsBlocked = false, };  bool isValid=true; string errors = RepositoryContainer.Instance.Resolve<UsersRepository>().ValidateAndReturnErrorString(newUser, out isValid); if (!isValid) { 	MessageBox.Show(errors, "Error..", MessageBoxButtons.OK, MessageBoxIcon.Error); } 

Демо проект

Полностью рабочий проект вы можете забрать на яндекс диске http://yadi.sk/d/P9XDDznpMj6p8.
Пожалуйста, обратите внимания, что для работы требуется установленная СУБД MSSQL.
В случае использования MSSQL Express, необходимо исправить строку подключение с

 <value>Data Source=.\; Initial Catalog=EFDemoApp; Integrated Security=True; Connection Timeout=5</value> 

на

 <value>Data Source=.\SQLEXPRESS; Initial Catalog=EFDemoApp; Integrated Security=True; Connection Timeout=5</value> 

Послесловие

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

Всем спасибо!

ссылка на оригинал статьи http://habrahabr.ru/post/219947/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *