📜 ⬆️ ⬇️

Implementing the data access layer on the Entity Framework Code First



Greetings

In this topic, I want to talk about the data access layer (Data Access Level) in relation to the Entity Framework, then EF, about what tasks were and how I solved them. All submitted code from the post, as well as the attached demo project is published under the liberal MIT license, that is, you can use the code as you wish.
At once I want to emphasize that all the presented code is a complete solution and has been used for more than 2 years in a project for a rather large Russian company, but nevertheless it is not suitable for highly loaded systems.
')
Details under the cut.

Tasks

When writing the application, I had several tasks in relation to the data access layer:
1. All changes to the data should be logged, including information about which user actually did it.
2. Using the “Repository” pattern
3. Control over the change of objects, that is, if we want to update only one object in the database, then exactly one object must.
I will explain:
By default, EF tracks changes to all objects within a specific context, while the ability to save one object is absent, unlike NHibernate. This situation is fraught with various unpleasant errors. For example, a user edits two objects at the same time, but wants to save only one. In case these two objects are connected to the same database context, the EF will save the changes to both objects.

Decision

There is quite a lot of code, so I add comments to the most interesting points.
Perhaps I'll start with the most important object - the context of the database.
In standard and simplified form, it is a list of database objects:
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()); } } } 


Extend it using the following interface:
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; } 


The resulting modified 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(); } } } 


Interaction with database objects occurs through repositories specific to each object. All repositories inherit the base class, which provides the basic CRUD functionality.
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); } 


Implementation of 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 database object:
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; } } } 


The repository for the User object, with a number of additional methods extending the standard CRUD functionality of the base class:
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(); } } } } 


In my case, all repositories are initialized once and added to the simplest self-written service locator RepositoryContainer. This made for the possibility of writing tests.
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)); } } } 


To all repositories, during initialization, the IContextManager object is transmitted, this is done to enable working with several contexts and their centralized creation:
IContextManager
 namespace DataAccess.Interfaces { public interface IContextManager { DemoAppDbContext CreateDatabaseContext(); } } 


And its implementation of 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); } } } 


Logging occurs in the object implementing the ILogger interface:
Ilogger
 namespace DataAccess.Interfaces { internal interface ILogger { void Run(); } } 


ILogger interface implementation
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), }); } } } 


Using

In order to start working with the database, the application needs to initialize the repository factory:
 RepositoryContainerFactory.RegisterAllRepositories(new ContextManager(Settings.Default.DBConnectionString)); 

After, you need to pass authorization and specify the current user. This is necessary in order to save in the history information about the user who made this or that change. In the demo project this item is omitted.
InitDefaultUser
 private void InitDefaultUser() { User defaultUser = RepositoryContainer.Instance.Resolve<UsersRepository>().GetFirst(); DemoAppDbContext.CurrentUser = defaultUser; } 


Calling repository methods is done by getting an instance from the service locator. In the example below, the call goes to the GetFirst () method of the repository of the UsersRepository type:
 User defaultUser = RepositoryContainer.Instance.Resolve<UsersRepository>().GetFirst(); 

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

Validation before saving objects

Validation and getting a list of errors:
 var newUser = new User { Email = "UserEmail@email.ru", IsBlocked = false, }; DbEntityValidationResult dbEntityValidationResult = RepositoryContainer.Instance.Resolve<UsersRepository>().Validate(newUser); 

Getting a string with errors:
 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); } 

Demo project

Fully working draft you can pick up on the Yandex disk http://yadi.sk/d/P9XDDznpMj6p8 .
Please note that an installed MSSQL is required for the operation.
In case of using MSSQL Express, you need to fix the connection string with
  <value>Data Source=.\; Initial Catalog=EFDemoApp; Integrated Security=True; Connection Timeout=5</value> 

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

Afterword

All the above code is my solution to the tasks. It may not be right, not optimal, but nevertheless it has been successfully working on one of the projects for several years.
At one time, I spent quite a lot of time and effort to make this system and I hope that my results will be useful to someone.

Thanks to all!

Source: https://habr.com/ru/post/219947/


All Articles