在线时间:8:00-16:00
迪恩网络APP
随时随地掌握行业动态
扫描二维码
关注迪恩网络微信公众号
Web application data access layers have evolved over time to provide increasing flexibility and maintainability in software architecture. Often, many web applications begin by manually maintaining database connections and SQL query strings. However, as architecture designs grow, more web applications are using ORM models to help increase code reuse and maintainability of ever-growing C# ASP .NET web applications. The latest version of the Entity Framework EF4 provides several new features, which support implementing a loosely-coupled repository pattern design. Using the Repository Pattern, along with a UnitOfWork pattern, we can greatly enhance the maintainability of our web applications with a solid architecture, greater code reuse, and unit test capability.
In this article, we'll step through 3 methods of accessing the Entity Framework data access layer in C# ASP .NET. We'll then detail an implementation of the Repository and UnitOfWork Pattern to get the most out of the design.
Method 1: The Beginner's Out-of-the-Box Entity Framework Design The Entity Framework is quite powerful. With a few clicks in Visual Studio, we can instantly create an ORM relational model of our database, ready for querying. One of the most simplest implementation of accessing data with the Entity Framework in a C# ASP .NET web application would be the following: void Main() { // Instantiate the Entity Framework data context in a using statement. using (MyEntities context = new MyEntities()) { // Create a dragon. Dragon dragon = new Dragon(); dragon.Name = "Fang"; dragon.Age = 210; // Create a weapon. Weapon weapon = new Weapon(); weapon.Name = "Fire"; weapon.Type = "Breath"; weapon.Damage = 100; // Set the dragon's weapon. dragon.Weapon = weapon; context.AddToDragons(dragon); // Save the dragon and weapon to the database. context.SaveChanges(); } } In the above code, we've implemented the basic Entity Framework data context within a using block. The using block will automatically dispose of the context object upon completion. Note, the database connection itself is opened and closed automatically upon calling SaveChanges(). The basic C# ASP .NET Entity Framework sample demonstrates using the ORM objects and persisting to the database. However, a more complicated web application, containing large numbers of business rules for saving and relating entities, could get quite complex using the above design. For example, what if the Dragon object could only accept weapons of type "Breath"? A new block of code would need to be inserted to perform the check. Similarly, a new block of code may need to be added to the Weapon as well. To abstract the more complicated business logic that may need to be added, we can utilize the Repository Pattern. The Repository Pattern is a Repository .. is a Repository The C# ASP .NET Repository Pattern is a layer created to manage the persistence of data to and from the Entity Framework ORM. While the repository pattern is often extended to include business logic (unrelated to persistence), it is often a cleaner approach to keep the repository pattern strictly for data persistence. A separate business manager class or repository wrapper can be created to manage business logic. In either case, as your repository business logic spans across the various domain objects, you'll find yourself requiring the Entity Framework data context in each method, in order to save data. Multiple Entity Framework Context Blocks One way of resolving the issue of using multiple repositories for a single work transaction may be to open and close individual Entity Framework context objects, within using blocks, throughout the code. However, for more data expensive C# ASP .NET web applications, executing queries over many database connections would be less than optimal. void Page_Load() { // Create a weapon. A database connection will be opened and closed. Weapon weapon = WeaponRepositoryManager.CreateWeapon(); // Create a dragon with the weapon. A database connection will be opened and closed. DragonRepositoryManager.CreateDragon(weapon); } public static class DragonRepositoryManager { public static void CreateDragon() { using (MyEntities context = new MyEntities()) { // Do some work with the dragon. // Check business rules. } } } public static class WeaponRepositoryManager() { public static Weapon CreateWeapon() { using (MyEntities context = new MyEntities()) { // Do some work with the weapon. // Check business rules. } } } In the above code, you can see how two separate queries will be executed against the database, each one requiring a database connection. For better performance, the queries could be combined into a single database query and transaction. However, to continue using the repository pattern and it's separation of code concerns, we'll need a way of sharing the context between them. This is where the UnitOfWork pattern comes in. Method 2: Sharing the Context with the UnitOfWork Pattern The UnitOfWork pattern is a design for grouping a set of tasks into a single group of transactional work. The UnitOfWork pattern is the solution to sharing the Entity Framework data context across multiple managers and repositories. The UnitOfWork pattern allows us to execute a single database transaction (implicitly as part of the Entity Framework), which spans across multiple blocks of code, methods, classes, and repositories. There are two primary ways to implement the Repository and UnitOfWork patterns. One method allows individual "using" blocks, similar to the above code, but sharing the context between them. For example: public static class DragonRepositoryManager { public static void CreateDragon() { Dragon dragon = new Dragon(); using (MyEntities context = new MyEntities()) { UnitOfWork uow = new UnitOfWork(context); // ... Weapon weapon = uow.Weapons.GetById(12); dragon.Weapon = weapon; uow.Dragons.Add(dragon); uow.Commit(); } return dragon; } } Notice in the above code, we've shared the Entity Framework's data context across the two repositories. Both, the Dragon and Weapon repositories are using the same data context to persist and query in the database. The UnitOfWork pattern, in this case, holds a copy of the data context and passes it to the repositories for usage. The actual database SQL execution is delayed until calling uow.Commit(). We can implement this design using the code shown below. A Basic Repository and UnitOfWork Implementation The design for the basic Repository and UnitOfWork pattern can be implemented with the following code, with original design from Revisiting the Repository and Unit of Work Patterns with Entity Framework EF4. public interface IUnitOfWork { IRepository<Dragon> Dragons { get; } IRepository<Weapon> Weapons { get; } void Commit(); } public interface IRepository<T> where T : class { T GetById(object id); IEnumerable<T> GetAll(); IEnumerable<T> Query(Expression<Func<T, bool>> filter); void Add(T entity); void Remove(T entity); } public class UnitOfWork : IUnitOfWork { private readonly ObjectContext _context; private DragonRepository _dragons; private WeaponRepository _weapons; public UnitOfWork(ObjectContext context) { if (context == null) { throw new ArgumentNullException("Context was not supplied"); } _context = context; } #region IUnitOfWork Members public IRepository<Dragon> Dragons { get { if (_dragons == null) { _dragons = new DragonRepository(_context); } return _dragons; } } public IRepository<Weapon> Weapons { get { if (_weapons == null) { _weapons = new WeaponRepository(_context); } return _weapons; } } public void Commit() { _context.SaveChanges(); } #endregion } public abstract class Repository<T> : IRepository<T> where T : class { protected IObjectSet<T> _objectSet; public Repository(ObjectContext context) { _objectSet = context.CreateObjectSet<T>(); } #region IRepository<T> Members public abstract T GetById(object id); public IEnumerable<T> GetAll() { return _objectSet; } public IEnumerable<T> Query(Expression<Func<T, bool>> filter) { return _objectSet.Where(filter); } public void Add(T entity) { _objectSet.AddObject(entity); } public void Remove(T entity) { _objectSet.DeleteObject(entity); } #endregion }With the framework setup, we can define the concrete repository implementations as follows: public class DragonRepository : Repository<Dragon> { public DragonRepository(ObjectContext context) : base(context) { } public override Dragon GetById(object id) { return _objectSet.SingleOrDefault(s => s.DragonId == (int)id); } } public class WeaponRepository : Repository<Weapon> { public WeaponRepository(ObjectContext context) : base(context) { } public override Weapon GetById(object id) { return _objectSet.SingleOrDefault(s => s.WeaponId == (int)id); } } While the above design allows us to share the Entity Framework context across repositories and query within individual using blocks, it still doesn't allow us to share a context across multiple classes or repository managers. For this, we would need to pass the UnitOfWork around between the classes. Rather than passing the UnitOfWork object, we can instead create a static global implementation, per http web request, which may better suit an enterprise architecture. Method 3: A Global Context with the UnitOfWork Pattern Another method for managing the Entity Framework Repository and UnitOfWork pattern is by using a global UnitOfWork object. The UnitOfWork object will have a lifetime of a single HTTP web request. Once the web request completes, the Entity Framework context is disposed. This can be considered as having one big using block around the web application code-base. In this design, each repository can query against the Entity Framework ORM, without requiring individual using blocks. Once all necessary business logic has been performed, a final UnitOfWork.Commit() can be called to execute the database queries and persist data. For example, the global UnitOfWork pattern allows us to utilize code as follows: void Page_Load() { // Create a weapon. No SQL is executed yet. Weapon weapon = WeaponManager.CreateWeapon(); // Create a dragon with the weapon. No SQL is executed yet. DragonManager.CreateDragon(weapon); // The complete unit of work is executed and changes committed to the database. UnitOfWork.Commit(); } public static class DragonManager { public static void CreateDragon(Weapon weapon) { Dragon dragon = new Dragon(); // Do some work with the dragon. // Check business rules. dragon.Weapon = weapon; _dragonRepository.Add(dragon); } } public static class WeaponManager() { public static Weapon CreateWeapon() { Weapon weapon = new Weapon(); // Do some work with the weapon. // Check business rules. _weaponRepository.Add(weapon); } } Notice in the above code, the main method calls two separate repository business managers, which query two individual repositories. The actual database execution is delayed until the final UnitOfWork.Commit() call is executed. This allows the complete unit of work (creation of a Weapon and a Dragon entity) to occur within the same transactional context. We can easily call multiple repositories across methods and classes, and still utilize the same UnitOfWork database context. The actual context is persisted per HTTP web request, allowing a thread-safe solution. The global Repository and UnitOfWork pattern can be implemented as discussed below, with original design from Entity Framework 4.0 POCO ObjectSet, Repository and UnitOfWork. The Generic IRepository Interface We'll define three generic interfaces to support the repository pattern, unitofwork pattern, and a factory for producing UnitOfWork objects. public interface IRepository<T> where T : class { IQueryable<T> GetQuery(); IEnumerable<T> GetAll(); IEnumerable<T> Find(Func<T, bool> where); T Single(Func<T, bool> where); T First(Func<T, bool> where); void Delete(T entity); void Add(T entity); void Attach(T entity); void SaveChanges(); } public interface IUnitOfWork : IDisposable { void Commit(); } public interface IUnitOfWorkFactory { IUnitOfWork Create(); } By using a generic template interface, we can bind repositories to individual Entity Framework entities and reuse the same interface for all required repositories. In effect, each entity in the database will have its own repository. UnitOfWork Implementation The core to the global Repository and UnitOfWork design is the base UnitOfWork class itself. This class will take care of providing a thread-safe unit of work pattern, committing data, and persisting itself for the lifetime of the context. For web applications, the lifetime will be per HTTP request. For desktop applications, the lifetime will be per thread. public static class UnitOfWork { private const string HTTPCONTEXTKEY =Notice the above code is using the HttpConext.Current.Items[] array to store the actual UnitOfWork context. For web applications, a new context will be created for each web request. We can create a concrete implementation of the UnitOfWork interface, specifically for the Entity Framework, as follows: public class EFUnitOfWork : IUnitOfWork, IDisposable { public ObjectContext Context { get; private set; } public EFUnitOfWork(ObjectContext context) { Context = context; context.ContextOptions.LazyLoadingEnabled = true; } public void Commit() { Context.SaveChanges(); } public void Dispose() { if (Context != null) { Context.Dispose(); Context = null; } GC.SuppressFinalize(this); } } The above implementation of IUnitOfWork simply wraps the C# ASP .NET Entity Framework. The constructor takes an ObjectContext and implements committing the changes and disposing of the context. The Dispose method will be called automatically by garbage collection. However, to tie up loose ends, this method can be called from the Global.asax event Application_EndRequest(). To help decouple the UnitOfWork implementation details from the layers of the design, we'll include a factory pattern, as follows: public class EFUnitOfWorkFactory : IUnitOfWorkFactory { private static Func<ObjectContext> _objectContextDelegate; private static readonly Object _lockObject = new object(); public static void SetObjectContext(Func<ObjectContext> objectContextDelegate) { _objectContextDelegate = objectContextDelegate; } public IUnitOfWork Create() { ObjectContext context; lock (_lockObject) { context = _objectContextDelegate(); } return new EFUnitOfWork(context); } }The factory pattern simply calls the delegate method for instantiating a new UnitOfWork context. We'll provide the setup for this delegate method in the initialization code for the C# ASP .NET web application, similar to the following code: // Select an Entity Framework model to use with the factory. EFUnitOfWorkFactory.SetObjectContext(() => new MyEntities()); The Entity Framework Repository Pattern With the UnitOfWork implementation complete, we can define a concrete base class for the generic repository pattern. This implementation will be designed for the Entity Framework. public class EFRepository<T> : IRepository<T> where T : class { private ObjectContext _context; private IObjectSet<T> _objectSet; protected ObjectContext Context { get { if (_context == null) { _context = GetCurrentUnitOfWork<EFUnitOfWork>().Context; } return _context; } } protected IObjectSet<T> ObjectSet { get { if (_objectSet == null) { _objectSet = this.Context.CreateObjectSet<T>(); } return _objectSet; } } public TUnitOfWork GetCurrentUnitOfWork<TUnitOfWork>() The above repository implementation obtains an Entity Framework context from the UnitOfWork class. It also instantiates an ObjectSet collection for the specific entity this repository will handle. The repository class also provides a set of common query methods for accessing the entity data, with support for LINQ queries. Initializing the repository pattern can be performed within the Global.asax event Application_Start() as follows: // Setup StructureMap to determine the concrete repository pattern to use. ObjectFactory.Initialize( x => { x.ForRequestedType<IUnitOfWorkFactory>(). Repository Business Logic Managers The Repository and UnitOfWork pattern are now complete for the Entity Framework. While the repositories provide the necessary methods for querying the Entity Framework objects, most C# ASP .NET web applications will require additional business logic for manipulating entities and persisting data. This type of logic can be added within manager classes, which wrap the repositories, and utilize/share the global UnitOfWork object. Example Using the Repository and UnitOfWork Pattern protected void Page_Load() { if (!IsPostBack) { BindGrid(); } } private void BindGrid() { lstDragons.Items.Clear(); // Call the repository manager to query all Dragon entities. foreach (Dragon dragon in DragonManager.GetAll()) { lstDragons.Items.Add(dragon.Name); } } protected void btnAdd_Click() { // Create a weapon. No SQL is executed yet. Weapon weapon = WeaponManager.CreateWeapon(); // Create a dragon with the weapon. No SQL is executed yet. DragonManager.CreateDragon(weapon); // The complete unit of work is executed and changes committed to the database. UnitOfWork.Commit(); BindGrid(); } public static class DragonManager { private static IRepository<Dragon> _dragonRepository { get { return ObjectFactory.GetInstance<IRepository<Dragon>>(); } } public static List<Dragon> GetAll() { List<Dragon> dragonList = new List<Dragon>(); // Fetch all Dragon entities from the repository. dragonList = _dragonRepository.GetAll().ToList(); return dragonList; } public static void CreateDragon(Weapon weapon) { Dragon dragon = new Dragon(); // Do some work with the dragon. // Check business rules. dragon.Weapon = weapon; // Add the new Dragon to the repository. _dragonRepository.Add(dragon); } public static void Delete(Dragon dragon) { // A custom business rule - the Immortal dragon can't be deleted. if (dragon.Name == "Immortal") { throw new Exception("You can't delete the Immortal dragon!"); } _dragonRepository.Delete(dragon); } } public static class WeaponManager() { private static IRepository<Weapon> _weaponRepository { get { return ObjectFactory.GetInstance<IRepository<Weapon>>(); } } public static Weapon CreateWeapon() { Weapon weapon = new Weapon(); // Do some work with the weapon. // Check business rules. _weaponRepository.Add(weapon); } } // Global.asax.cs void Application_Start(object sender, EventArgs e) { // Setup StructureMap to determine the concrete repository pattern to use. ObjectFactory.Initialize( x => { x.ForRequestedType<IUnitOfWorkFactory>(). Notice in the above code, we've created individual business managers, which wrap the repositories. This allows us to perform specific business functionality along with the processing of Entity Framework repository entities. The managers themselves do not commit to the database, although they certainly could if required. Instead, upon an HTTP request, any business logic executed will queue up SQL calls to finally be executed at the end of the web page event, providing a single transactional event per web request. We take advantage of the Global.asax EndRequest event to dispose of the UnitOfWork and Entity Framework context. While disposing would occur automatically via garbage collection, and database connections are automatically managed by the Entity Framework context (opening and closing the database connection only upon calling SaveChanges), it's still good practice to be aware of the physical creation and disposing of the context. An added benefit to the design of the business managers is that they provide an easy access point for unit testing and TDD test driven development. You can download the example source code for this project here.
Conclusion The C# ASP .NET Entity Framework is a powerful ORM for managing database entities in an object oriented design. By enhancing .NET web application design and providing a Repository and UnitOfWork architecture, benefits can be achieved in code reuse, maintainability, and support for unit testing. Two primary implementations of the Entity Framework Repository and UnitOfWork pattern provide a design for persisting entities within individual blocks of context or via a global context per web request. The Repository and UnitOfWork pattern provide a solid architecture to help take C# ASP .NET web applications into the future. About the Author
|
2023-10-27
2022-08-15
2022-08-17
2022-09-23
2022-08-13
请发表评论