Entity Framework
Лучшие практики для платформы Entity (простой и профессиональный)
Поиск…
Вступление
В этой статье мы рассмотрим простую и профессиональную практику использования Entity Framework.
Простой: потому что ему нужен только один класс (с одним интерфейсом)
Профессионал: поскольку он применяет принципы архитектуры SOLID
Я не хочу больше говорить ... давайте наслаждаться этим!
1- Entity Framework @ Уровень данных (основы)
В этой статье мы будем использовать простую базу данных под названием «Компания» с двумя таблицами:
[dbo]. [Категории] ([CategoryID], [CategoryName])
[dbo]. [Продукты] ([ProductID], [CategoryID], [ProductName])
1-1 Создать код платформы Entity Framework
В этом слое мы генерируем код Entity Framework (в библиотеке проектов) (см. Эту статью, как вы это можете сделать), тогда у вас будут следующие классы
public partial class CompanyContext : DbContext
public partial class Product
public partial class Category
1-2 Создать базовый интерфейс
Мы создадим один интерфейс для наших основных функций
public interface IDbRepository : IDisposable
{
#region Tables and Views functions
IQueryable<TResult> GetAll<TResult>(bool noTracking = true) where TResult : class;
TEntity Add<TEntity>(TEntity entity) where TEntity : class;
TEntity Delete<TEntity>(TEntity entity) where TEntity : class;
TEntity Attach<TEntity>(TEntity entity) where TEntity : class;
TEntity AttachIfNot<TEntity>(TEntity entity) where TEntity : class;
#endregion Tables and Views functions
#region Transactions Functions
int Commit();
Task<int> CommitAsync(CancellationToken cancellationToken = default(CancellationToken));
#endregion Transactions Functions
#region Database Procedures and Functions
TResult Execute<TResult>(string functionName, params object[] parameters);
#endregion Database Procedures and Functions
}
1-3 Внедрение базового интерфейса
/// <summary>
/// Implementing basic tables, views, procedures, functions, and transaction functions
/// Select (GetAll), Insert (Add), Delete, and Attach
/// No Edit (Modify) function (can modify attached entity without function call)
/// Executes database procedures or functions (Execute)
/// Transaction functions (Commit)
/// More functions can be added if needed
/// </summary>
/// <typeparam name="TEntity">Entity Framework table or view</typeparam>
public class DbRepository : IDbRepository
{
#region Protected Members
protected DbContext _dbContext;
#endregion Protected Members
#region Constractors
/// <summary>
/// Repository constructor
/// </summary>
/// <param name="dbContext">Entity framework databse context</param>
public DbRepository(DbContext dbContext)
{
_dbContext = dbContext;
ConfigureContext();
}
#endregion Constractors
#region IRepository Implementation
#region Tables and Views functions
/// <summary>
/// Query all
/// Set noTracking to true for selecting only (read-only queries)
/// Set noTracking to false for insert, update, or delete after select
/// </summary>
public virtual IQueryable<TResult> GetAll<TResult>(bool noTracking = true) where TResult : class
{
var entityDbSet = GetDbSet<TResult>();
if (noTracking)
return entityDbSet.AsNoTracking();
return entityDbSet;
}
public virtual TEntity Add<TEntity>(TEntity entity) where TEntity : class
{
return GetDbSet<TEntity>().Add(entity);
}
/// <summary>
/// Delete loaded (attached) or unloaded (Detached) entitiy
/// No need to load object to delete it
/// Create new object of TEntity and set the id then call Delete function
/// </summary>
/// <param name="entity">TEntity</param>
/// <returns></returns>
public virtual TEntity Delete<TEntity>(TEntity entity) where TEntity : class
{
if (_dbContext.Entry(entity).State == EntityState.Detached)
{
_dbContext.Entry(entity).State = EntityState.Deleted;
return entity;
}
else
return GetDbSet<TEntity>().Remove(entity);
}
public virtual TEntity Attach<TEntity>(TEntity entity) where TEntity : class
{
return GetDbSet<TEntity>().Attach(entity);
}
public virtual TEntity AttachIfNot<TEntity>(TEntity entity) where TEntity : class
{
if (_dbContext.Entry(entity).State == EntityState.Detached)
return Attach(entity);
return entity;
}
#endregion Tables and Views functions
#region Transactions Functions
/// <summary>
/// Saves all changes made in this context to the underlying database.
/// </summary>
/// <returns>The number of objects written to the underlying database.</returns>
public virtual int Commit()
{
return _dbContext.SaveChanges();
}
/// <summary>
/// Asynchronously saves all changes made in this context to the underlying database.
/// </summary>
/// <param name="cancellationToken">A System.Threading.CancellationToken to observe while waiting for the task to complete.</param>
/// <returns>A task that represents the asynchronous save operation. The task result contains the number of objects written to the underlying database.</returns>
public virtual Task<int> CommitAsync(CancellationToken cancellationToken = default(CancellationToken))
{
return _dbContext.SaveChangesAsync(cancellationToken);
}
#endregion Transactions Functions
#region Database Procedures and Functions
/// <summary>
/// Executes any function in the context
/// use to call database procesdures and functions
/// </summary>>
/// <typeparam name="TResult">return function type</typeparam>
/// <param name="functionName">context function name</param>
/// <param name="parameters">context function parameters in same order</param>
public virtual TResult Execute<TResult>(string functionName, params object[] parameters)
{
MethodInfo method = _dbContext.GetType().GetMethod(functionName);
return (TResult)method.Invoke(_dbContext, parameters);
}
#endregion Database Procedures and Functions
#endregion IRepository Implementation
#region IDisposable Implementation
public void Dispose()
{
_dbContext.Dispose();
}
#endregion IDisposable Implementation
#region Protected Functions
/// <summary>
/// Set Context Configuration
/// </summary>
protected virtual void ConfigureContext()
{
// set your recommended Context Configuration
_dbContext.Configuration.LazyLoadingEnabled = false;
}
#endregion Protected Functions
#region Private Functions
private DbSet<TEntity> GetDbSet<TEntity>() where TEntity : class
{
return _dbContext.Set<TEntity>();
}
#endregion Private Functions
}
2- Entity Framework @ Бизнес-уровень
В этом слое мы напишем бизнес приложения.
Для каждого экрана презентации рекомендуется создать бизнес-интерфейс и класс реализации, которые содержат все необходимые функции для экрана.
Ниже мы будем писать бизнес для экрана продукта в качестве примера
/// <summary>
/// Contains Product Business functions
/// </summary>
public interface IProductBusiness
{
Product SelectById(int productId, bool noTracking = true);
Task<IEnumerable<dynamic>> SelectByCategoryAsync(int CategoryId);
Task<Product> InsertAsync(string productName, int categoryId);
Product InsertForNewCategory(string productName, string categoryName);
Product Update(int productId, string productName, int categoryId);
Product Update2(int productId, string productName, int categoryId);
int DeleteWithoutLoad(int productId);
int DeleteLoadedProduct(Product product);
IEnumerable<GetProductsCategory_Result> GetProductsCategory(int categoryId);
}
/// <summary>
/// Implementing Product Business functions
/// </summary>
public class ProductBusiness : IProductBusiness
{
#region Private Members
private IDbRepository _dbRepository;
#endregion Private Members
#region Constructors
/// <summary>
/// Product Business Constructor
/// </summary>
/// <param name="dbRepository"></param>
public ProductBusiness(IDbRepository dbRepository)
{
_dbRepository = dbRepository;
}
#endregion Constructors
#region IProductBusiness Function
/// <summary>
/// Selects Product By Id
/// </summary>
public Product SelectById(int productId, bool noTracking = true)
{
var products = _dbRepository.GetAll<Product>(noTracking);
return products.FirstOrDefault(pro => pro.ProductID == productId);
}
/// <summary>
/// Selects Products By Category Id Async
/// To have async method, add reference to EntityFramework 6 dll or higher
/// also you need to have the namespace "System.Data.Entity"
/// </summary>
/// <param name="CategoryId">CategoryId</param>
/// <returns>Return what ever the object that you want to return</returns>
public async Task<IEnumerable<dynamic>> SelectByCategoryAsync(int CategoryId)
{
var products = _dbRepository.GetAll<Product>();
var categories = _dbRepository.GetAll<Category>();
var result = (from pro in products
join cat in categories
on pro.CategoryID equals cat.CategoryID
where pro.CategoryID == CategoryId
select new
{
ProductId = pro.ProductID,
ProductName = pro.ProductName,
CategoryName = cat.CategoryName
}
);
return await result.ToListAsync();
}
/// <summary>
/// Insert Async new product for given category
/// </summary>
public async Task<Product> InsertAsync(string productName, int categoryId)
{
var newProduct = _dbRepository.Add(new Product() { ProductName = productName, CategoryID = categoryId });
await _dbRepository.CommitAsync();
return newProduct;
}
/// <summary>
/// Insert new product and new category
/// Do many database actions in one transaction
/// each _dbRepository.Commit(); will commit one transaction
/// </summary>
public Product InsertForNewCategory(string productName, string categoryName)
{
var newCategory = _dbRepository.Add(new Category() { CategoryName = categoryName });
var newProduct = _dbRepository.Add(new Product() { ProductName = productName, Category = newCategory });
_dbRepository.Commit();
return newProduct;
}
/// <summary>
/// Update given product with tracking
/// </summary>
public Product Update(int productId, string productName, int categoryId)
{
var product = SelectById(productId,false);
product.CategoryID = categoryId;
product.ProductName = productName;
_dbRepository.Commit();
return product;
}
/// <summary>
/// Update given product with no tracking and attach function
/// </summary>
public Product Update2(int productId, string productName, int categoryId)
{
var product = SelectById(productId);
_dbRepository.Attach(product);
product.CategoryID = categoryId;
product.ProductName = productName;
_dbRepository.Commit();
return product;
}
/// <summary>
/// Deletes product without loading it
/// </summary>
public int DeleteWithoutLoad(int productId)
{
_dbRepository.Delete(new Product() { ProductID = productId });
return _dbRepository.Commit();
}
/// <summary>
/// Deletes product after loading it
/// </summary>
public int DeleteLoadedProduct(Product product)
{
_dbRepository.Delete(product);
return _dbRepository.Commit();
}
/// <summary>
/// Assuming we have the following procedure in database
/// PROCEDURE [dbo].[GetProductsCategory] @CategoryID INT, @OrderBy VARCHAR(50)
/// </summary>
public IEnumerable<GetProductsCategory_Result> GetProductsCategory(int categoryId)
{
return _dbRepository.Execute<IEnumerable<GetProductsCategory_Result>>("GetProductsCategory", categoryId, "ProductName DESC");
}
#endregion IProductBusiness Function
}
3- Использование уровня бизнес-уровня @ Уровень представления (MVC)
В этом примере мы будем использовать уровень Business на уровне Presentation. И мы будем использовать MVC в качестве примера слоя Presentation (но вы можете использовать любой другой слой Presentation).
Нам нужно сначала зарегистрировать IoC (мы будем использовать Unity, но вы можете использовать любой IoC), а затем напишите наш слой Presentation
3-1 Регистрация типов Unity в MVC
3-1-1 Добавьте «Unity bootstrapper для ASP.NET MVC» NuGet backage
3-1-2 Добавьте UnityWebActivator.Start (); в файле Global.asax.cs (функция Application_Start ())
3-1-3. Изменить функцию UnityConfig.RegisterTypes следующим образом.
public static void RegisterTypes(IUnityContainer container)
{
// Data Access Layer
container.RegisterType<DbContext, CompanyContext>(new PerThreadLifetimeManager());
container.RegisterType(typeof(IDbRepository), typeof(DbRepository), new PerThreadLifetimeManager());
// Business Layer
container.RegisterType<IProductBusiness, ProductBusiness>(new PerThreadLifetimeManager());
}
3-2 Использование уровня бизнес-уровня @ Уровень представления (MVC)
public class ProductController : Controller
{
#region Private Members
IProductBusiness _productBusiness;
#endregion Private Members
#region Constractors
public ProductController(IProductBusiness productBusiness)
{
_productBusiness = productBusiness;
}
#endregion Constractors
#region Action Functions
[HttpPost]
public ActionResult InsertForNewCategory(string productName, string categoryName)
{
try
{
// you can use any of IProductBusiness functions
var newProduct = _productBusiness.InsertForNewCategory(productName, categoryName);
return Json(new { success = true, data = newProduct });
}
catch (Exception ex)
{ /* log ex*/
return Json(new { success = false, errorMessage = ex.Message});
}
}
[HttpDelete]
public ActionResult SmartDeleteWithoutLoad(int productId)
{
try
{
// deletes product without load
var deletedProduct = _productBusiness.DeleteWithoutLoad(productId);
return Json(new { success = true, data = deletedProduct });
}
catch (Exception ex)
{ /* log ex*/
return Json(new { success = false, errorMessage = ex.Message });
}
}
public async Task<ActionResult> SelectByCategoryAsync(int CategoryId)
{
try
{
var results = await _productBusiness.SelectByCategoryAsync(CategoryId);
return Json(new { success = true, data = results },JsonRequestBehavior.AllowGet);
}
catch (Exception ex)
{ /* log ex*/
return Json(new { success = false, errorMessage = ex.Message },JsonRequestBehavior.AllowGet);
}
}
#endregion Action Functions
}
4- Entity Framework @ Единичный тестовый уровень
В слое Test Test мы обычно тестируем функциональные возможности Business Layer. И для этого мы удалим зависимости Data Layer (Entity Framework).
И теперь возникает вопрос: как я могу удалить зависимости Entity Framework для модульного тестирования функций Business Layer?
И ответ прост: мы будем поддельной реализацией интерфейса IDbRepository, тогда мы сможем выполнить наш модульный тест
4-1 Реализация базового интерфейса (поддельная реализация)
class FakeDbRepository : IDbRepository
{
#region Protected Members
protected Hashtable _dbContext;
protected int _numberOfRowsAffected;
protected Hashtable _contextFunctionsResults;
#endregion Protected Members
#region Constractors
public FakeDbRepository(Hashtable contextFunctionsResults = null)
{
_dbContext = new Hashtable();
_numberOfRowsAffected = 0;
_contextFunctionsResults = contextFunctionsResults;
}
#endregion Constractors
#region IRepository Implementation
#region Tables and Views functions
public IQueryable<TResult> GetAll<TResult>(bool noTracking = true) where TResult : class
{
return GetDbSet<TResult>().AsQueryable();
}
public TEntity Add<TEntity>(TEntity entity) where TEntity : class
{
GetDbSet<TEntity>().Add(entity);
++_numberOfRowsAffected;
return entity;
}
public TEntity Delete<TEntity>(TEntity entity) where TEntity : class
{
GetDbSet<TEntity>().Remove(entity);
++_numberOfRowsAffected;
return entity;
}
public TEntity Attach<TEntity>(TEntity entity) where TEntity : class
{
return Add(entity);
}
public TEntity AttachIfNot<TEntity>(TEntity entity) where TEntity : class
{
if (!GetDbSet<TEntity>().Contains(entity))
return Attach(entity);
return entity;
}
#endregion Tables and Views functions
#region Transactions Functions
public virtual int Commit()
{
var numberOfRowsAffected = _numberOfRowsAffected;
_numberOfRowsAffected = 0;
return numberOfRowsAffected;
}
public virtual Task<int> CommitAsync(CancellationToken cancellationToken = default(CancellationToken))
{
var numberOfRowsAffected = _numberOfRowsAffected;
_numberOfRowsAffected = 0;
return new Task<int>(() => numberOfRowsAffected);
}
#endregion Transactions Functions
#region Database Procedures and Functions
public virtual TResult Execute<TResult>(string functionName, params object[] parameters)
{
if (_contextFunctionsResults != null && _contextFunctionsResults.Contains(functionName))
return (TResult)_contextFunctionsResults[functionName];
throw new NotImplementedException();
}
#endregion Database Procedures and Functions
#endregion IRepository Implementation
#region IDisposable Implementation
public void Dispose()
{
}
#endregion IDisposable Implementation
#region Private Functions
private List<TEntity> GetDbSet<TEntity>() where TEntity : class
{
if (!_dbContext.Contains(typeof(TEntity)))
_dbContext.Add(typeof(TEntity), new List<TEntity>());
return (List<TEntity>)_dbContext[typeof(TEntity)];
}
#endregion Private Functions
}
4-2 Запустите тестирование устройства
[TestClass]
public class ProductUnitTest
{
[TestMethod]
public void TestInsertForNewCategory()
{
// Initialize repositories
FakeDbRepository _dbRepository = new FakeDbRepository();
// Initialize Business object
IProductBusiness productBusiness = new ProductBusiness(_dbRepository);
// Process test method
productBusiness.InsertForNewCategory("Test Product", "Test Category");
int _productCount = _dbRepository.GetAll<Product>().Count();
int _categoryCount = _dbRepository.GetAll<Category>().Count();
Assert.AreEqual<int>(1, _productCount);
Assert.AreEqual<int>(1, _categoryCount);
}
[TestMethod]
public void TestProceduresFunctionsCall()
{
// Initialize Procedures / Functions result
Hashtable _contextFunctionsResults = new Hashtable();
_contextFunctionsResults.Add("GetProductsCategory", new List<GetProductsCategory_Result> {
new GetProductsCategory_Result() { ProductName = "Product 1", ProductID = 1, CategoryName = "Category 1" },
new GetProductsCategory_Result() { ProductName = "Product 2", ProductID = 2, CategoryName = "Category 1" },
new GetProductsCategory_Result() { ProductName = "Product 3", ProductID = 3, CategoryName = "Category 1" }});
// Initialize repositories
FakeDbRepository _dbRepository = new FakeDbRepository(_contextFunctionsResults);
// Initialize Business object
IProductBusiness productBusiness = new ProductBusiness(_dbRepository);
// Process test method
var results = productBusiness.GetProductsCategory(1);
Assert.AreEqual<int>(3, results.Count());
}
}