Entity Framework
Mejores Prácticas para Entity Framework (Simple y Profesional)
Buscar..
Introducción
Este artículo presenta una práctica simple y profesional para utilizar Entity Framework.
Simple: porque solo necesita una clase (con una interfaz)
Profesional: porque aplica los principios de la arquitectura SÓLIDA.
No quiero hablar más ... ¡Vamos a disfrutarlo!
1- Entity Framework @ Data layer (Conceptos básicos)
En este artículo usaremos una base de datos simple llamada "Compañía" con dos tablas:
[dbo]. [Categorías] ([CategoryID], [CategoryName])
[dbo]. [Productos] ([ProductID], [CategoryID], [ProductName])
1-1 Generar código Entity Framework
En esta capa generamos el código de Entity Framework (en la biblioteca del proyecto) (vea este artículo en cómo puede hacerlo) y luego tendrá las siguientes clases
public partial class CompanyContext : DbContext
public partial class Product
public partial class Category
1-2 Crear interfaz básica
Crearemos una interfaz para nuestras funciones básicas.
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 Implementando la interfaz básica
/// <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 @ Business layer
En esta capa escribiremos el negocio de la aplicación.
Se recomienda para cada pantalla de presentación, cree la interfaz de negocios y la clase de implementación que contengan todas las funciones necesarias para la pantalla.
A continuación escribiremos el negocio para pantalla de producto como ejemplo.
/// <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- Usando capa de presentación @ capa de negocios (MVC)
En este ejemplo usaremos la capa de negocios en la capa de presentación. Y usaremos MVC como ejemplo de la capa de presentación (pero puede usar cualquier otra capa de presentación).
Primero necesitamos registrar el IoC (usaremos Unity, pero puede usar cualquier IoC), luego escribir nuestra capa de presentación
3-1 Registrar tipos de Unity dentro de MVC
3-1-1 Agregue el respaldo NuGet de "Unity bootstrapper for ASP.NET MVC"
3-1-2 Agregar UnityWebActivator.Start (); en el archivo Global.asax.cs (función Application_Start ())
3-1-3 Modificar la función UnityConfig.RegisterTypes de la siguiente manera
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 Usando capa de presentación @ Business layer (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 @ Unit Test Layer
En la capa Prueba de unidad usualmente probamos las funcionalidades de la capa de negocios. Y para hacer esto, eliminaremos las dependencias de la capa de datos (Entity Framework).
Y la pregunta ahora es: ¿Cómo puedo eliminar las dependencias de Entity Framework para probar de forma unitaria las funciones de la capa empresarial?
Y la respuesta es simple: haremos una implementación falsa para IDbRepository Interface y luego podremos hacer nuestra prueba de unidad.
4-1 Implementando la interfaz básica (implementación falsa)
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 Ejecuta tu unidad de pruebas
[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());
}
}