Entity Framework
エンティティフレームワークのベストプラクティス(シンプル&プロフェッショナル)
サーチ…
前書き
この記事では、Entity Frameworkを使用する簡単で専門的な方法を紹介します。
シンプル:1つのクラス(1つのインターフェースを持つ)
プロフェッショナル: SOLIDアーキテクチャの原則が適用されるため
私はもっと話したくない...それを楽しもう!
1- Entity Framework @データレイヤー(基本)
この記事では、2つのテーブルを持つ「Company」という単純なデータベースを使用します。
[dbo]。[Categories]([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基本インターフェースの作成
私たちは、基本機能のための1つのインタフェースを作成します
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)の使用
この例では、プレゼンテーションレイヤのビジネスレイヤを使用します。また、プレゼンテーションレイヤの例としてMVCを使用します(他のプレゼンテーションレイヤは使用できます)。
最初にIoCを登録する必要があります(Unityを使用しますが、任意のIoCを使用できます)。その後、プレゼンテーションレイヤー
3-1 MVC内にUnityタイプを登録する
3-1-1「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
}
ユニットテストレイヤー@エンティティフレームワーク
単体テスト層では、通常、ビジネスレイヤ機能をテストします。これを行うために、データレイヤ(Entity Framework)の依存関係を削除します。
ビジネス・レイヤー・ファンクションの単体テストを行うためにEntity Frameworkの依存関係を削除するにはどうしたらよいでしょうか?
答えは簡単です:私たちは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());
}
}