Реализация уровня доступа к данным на 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
protected override void OnModelCreating (DbModelBuilder modelBuilder)
{
modelBuilder.Configurations.Add (new UserMap ());
}
}
}
Расширим его с помощью следующего интерфейса: IDbContext
public interface IDbContext
{
IQueryable
void MarkAsAdded
void MarkAsDeleted
void MarkAsModified
void Commit (bool withLogging);
//откатывает изменения во всех модифицированных объектах void Rollback ();
// включает или отключает отслеживание изменений объектов void EnableTracking (bool isEnable);
EntityState GetEntityState
void SetEntityState
// возвращает объект содержащий список объектов с их состоянием DbChangeTracker GetChangeTracker ();
DbEntityEntry GetDbEntry
private readonly ILogger _logger;
#region Context Entities
public DbSet
public DbSet
#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
public void MarkAsDeleted
public void MarkAsModified
public void Attach
public void Commit (bool withLogging) { BeforeCommit (); if (withLogging) { _logger.Run (); } SaveChanges (); }
private void BeforeCommit () { UndoExistAddedEntitys (); }
//исправление ситуации, когда у есть объекты помеченные как новые, но при этом существующие в базе данных
private void UndoExistAddedEntitys ()
{
IEnumerable
// откат всех изменений в объектах public void Rollback () { ChangeTracker.Entries ().ToList ().ForEach (x => x.Reload ()); }
public void EnableTracking (bool isEnable) { Configuration.AutoDetectChangesEnabled = isEnable; }
public void SetEntityState
public DbChangeTracker GetChangeTracker () { return ChangeTracker; }
public EntityState GetEntityState
public IQueryable
public DbEntityEntry GetDbEntry
public static int GetKeyValue
return dbEntity.GetPrimaryKey ();
}
}
}
Взаимодействие с объектами базы данных происходит через репозитории специфичные для каждого объекта. Все репозитории наследуют базовый класс, который предоставляет базовый CRUD функционалIRepository
interface IRepository
List
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
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
public T Find (int entityId)
{
using (var context = CreateDatabaseContext ())
{
return context.Set
// виртуальный метод. вызывает перед сохранением объектов, может быть определен в дочерних классах 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
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
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
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
}
public User FindByLogin (string login)
{
using (var db = CreateDatabaseContext ())
{
return db.Set
public bool ExistUser (string login)
{
using (var db = CreateDatabaseContext ())
{
return db.Set
public User GetByUserId (int userId)
{
using (var db = CreateDatabaseContext ())
{
return db.Set
}
public User GetFirst ()
{
using (var db = CreateDatabaseContext ())
{
return db.Set
public static readonly RepositoryContainer Instance = new RepositoryContainer ();
private RepositoryContainer () { }
public T Resolve
public void Register
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 ();
}
}
Реализация интерфейса ILoggerLogger
public class Logger: ILogger
{
Dictionary
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
private string GetOperationName (EntityState entityState) { return _operationTypes[entityState]; }
private void LogChangedEntities (EntityState entityState)
{
IEnumerable
private void LogChangedEntitie (DbEntityEntry dbEntityEntry, EntityState entityState) { string operationHash = HashGenerator.GenerateHash (10); int enitityId = DemoAppDbContext.GetKeyValue (dbEntityEntry.Entity);
Type type = dbEntityEntry.Entity.GetType ();
IEnumerable
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
bool isValid=true;
string errors = RepositoryContainer.Instance.Resolve