文章目录
概述
NopCommerce源码架构详解-MVC5使用EF6实现Repository模式相关源码分析。
内容
Repository模式是属于领域模型范畴,是架构模式。nop中使用了这个种模板来读取、操作数据。今天我们通过Nop来看看在ASP.NET mvc5使用EF6实现Repository模式的方案。
1、理解Repository模式
首先我们来看看Repository模式相关的概念:
Repository(资源库)
协调领域和数据映射层,利用类似于集合的接口来访问领域对象
定义(来自Martin Fowler的《企业应用架构模式》):
Mediates between the domain and data mapping layers using a collection-like interface for accessing domain objects.
个人理解:Repository是一个独立的层,介于领域层与数据映射层(数据访问层)之间。它的存在让领域层感觉不到数据访问层的存在,它提供一个类似集合的接口提供给领域层进行领域对象的访问。Repository是仓库管理员,领域层需要什么东西只需告诉仓库管理员,由仓库管理员把东西拿给它,并不需要知道东西实际放在哪。
- Repository模式是架构模式,在设计架构时,才有参考价值;
- Repository模式主要是封装数据查询和存储逻辑;
- Repository模式实际用途:更换、升级ORM引擎,不影响业务逻辑;
- Repository模式能提高测试效率,单元测试时,用Mock对象代替实际的数据库存取,可以成倍地提高测试用例运行速度。
评估:应用Repository模式所带来的好处,远高于实现这个模式所增加的代码。只要项目分层,都应当使用这个模式。
关于泛型Repository接口:
仅使用泛型Repository接口并不太合适,因为Repository接口是提供给Domain层的操作契约,不同的entity对于Domain来说可能有不同的操作约束。因此Repository接口还是应该单独针对每个Eneity类来定义。
泛型的Repository类仍然用来减少重复代码,只是不能被UserRepository类直接继承,因为这样Delete方法将侵入User类,所以改为在UserRepository中组合一个Repository,将开放给domain可见且又能使用泛型重用的功能委托给这个Repository
Repository与Dal的区别:
Repository是DDD中的概念,强调Repository是受Domain驱动的,Repository中定义的功能要体现Domain的意图和约束,而Dal更纯粹的就是提供数据访问的功能,并不严格受限于Business层。
使用Repository,隐含着一种意图倾向,就是 Domain需要什么我才提供什么,不该提供的功能就不要提供,一切都是以Domain的需求为核心;而使用Dal,其意图倾向在于我Dal层能使用的数据库访问操作提供给Business层,你Business要用哪个自己选。换一个Business也可以用我这个Dal,一切是以我Dal能提供什么操作为核心。
2、Nop中的Repository模式架构
2.1、主要类及用途
Nop使用的ORM构架是EF,并且采用Repository模式架构。主要用到的类和接口:
1、Nop.Core.Data.IRepository
2、Nop.Data.EfRepository
4、Nop.Data.BaseDataProviderManager,Nop.Data.EfDataProviderManager
5、Nop.Data.IDbContext
6、Nop.Data.NopObjectContext
主要相关类图

2.2、核心代码
1、泛型的接口IRepository:
Nop抽象出一个实体常用的一些操作,据此定义一个泛型的接口IRepository,T限制为是必须继承于基类实体BaseEntity。这样接口就可能适用于所有的实体,接口中定义了实体的一些常用的公用方法,比如:增、删、查、改。
using System.linq; namespace Nop.Core.Data { /// <summary> /// Repository /// </summary> public partial interface IRepository<T> where T : BaseEntity { /// <summary> /// 通过ID获取实体 /// </summary> /// <param name="id">Identifier</param> /// <returns>Entity</returns> T GetById(object id); /// <summary> /// Insert entity /// </summary> /// <param name="entity">Entity</param> void Insert(T entity); /// <summary> /// Update entity /// </summary> /// <param name="entity">Entity</param> void Update(T entity); /// <summary> /// Delete entity /// </summary> /// <param name="entity">Entity</param> void Delete(T entity); /// <summary> /// Gets a table /// </summary> IQueryable<T> Table { get; } /// <summary> /// Gets a table with "no tracking" enabled (EF feature) Use it only when you load record(s) only for read-only operations /// </summary> IQueryable<T> TableNoTracking { get; } } }
我们可以看到Nop的Service中都引用这个接口,如:BlogService的构造函数就用IRepository blogPostRepository。
2、IDbContext
Nop定义了下数据库上下文接口,定义了针对数据库最基本的一些操作,比如设置实体,保存实体,执行sql或存储过程等等。
using System.Collections.Generic; using System.Data.Entity; using Nop.Core; namespace Nop.Data { public interface IDbContext { /// <summary> /// Get DbSet /// </summary> /// <typeparam name="TEntity">Entity type</typeparam> /// <returns>DbSet</returns> IDbSet<TEntity> Set<TEntity>() where TEntity : BaseEntity; /// <summary> /// Save changes /// </summary> /// <returns></returns> int SaveChanges(); /// <summary> /// 执行存储过程并返回实体的List /// </summary> /// <typeparam name="TEntity">Entity type</typeparam> /// <param name="commandText">Command text</param> /// <param name="parameters">Parameters</param> /// <returns>Entities</returns> IList<TEntity> ExecuteStoredProcedureList<TEntity>(string commandText, params object[] parameters) where TEntity : BaseEntity, new(); /// <summary> /// 执行sql语句并返回一个指定类型实体的集合 /// </summary> /// <typeparam name="TElement">The type of object returned by the query.</typeparam> /// <param name="sql">The SQL query string.</param> /// <param name="parameters">The parameters to apply to the SQL query string.</param> /// <returns>Result</returns> IEnumerable<TElement> SqlQuery<TElement>(string sql, params object[] parameters); /// <summary> /// 执行一个指定 DDL/DML 命令 /// </summary> /// <param name="sql">The command string</param> /// <param name="doNotEnsureTransaction">false - the transaction creation is not ensured; true - the transaction creation is ensured.</param> /// <param name="timeout">Timeout value, in seconds. A null value indicates that the default value of the underlying provider will be used</param> /// <param name="parameters">The parameters to apply to the command string.</param> /// <returns>The result returned by the database after executing the command.</returns> int ExecuteSqlCommand(string sql, bool doNotEnsureTransaction = false, int? timeout = null, params object[] parameters); } }
3、EfRepository
EfRepository类就是Nop实现RePository最核心的部分了,它真正的实现了接口IRepositoryj里面定义的所有方法,并且里面引用了接口IDbContext,而不是IDbContext的实现,这样面向接口编程大大的降低了系统模块的耦合性,其实细心的同学可以随处看到Nop中这种面向接口编程的思路。
下面我们来看看EfRepository类的实现:
using System; using System.Data.Entity; using System.Data.Entity.Validation; using System.Linq; using Nop.Core; using Nop.Core.Data; namespace Nop.Data { /// <summary> /// Entity Framework repository /// </summary> public partial class EfRepository<T> : IRepository<T> where T : BaseEntity { private readonly IDbContext _context; private IDbSet<T> _entities; /// <summary> /// Ctor /// </summary> /// <param name="context">Object context</param> public EfRepository(IDbContext context) { this._context = context; } /// <summary> /// Get entity by identifier /// </summary> /// <param name="id">Identifier</param> /// <returns>Entity</returns> public virtual T GetById(object id) { //see some suggested performance optimization (not tested) //http://stackoverflow.com/questions/11686225/dbset-find-method-ridiculously-slow-compared-to-singleordefault-on-id/11688189#comment34876113_11688189 return this.Entities.Find(id); } /// <summary> /// Insert entity /// </summary> /// <param name="entity">Entity</param> public virtual void Insert(T entity) { try { if (entity == null) throw new ArgumentNullException("entity"); this.Entities.Add(entity); this._context.SaveChanges(); } catch (DbEntityValidationException dbEx) { var msg = string.Empty; foreach (var validationErrors in dbEx.EntityValidationErrors) foreach (var validationError in validationErrors.ValidationErrors) msg += string.Format("Property: {0} Error: {1}", validationError.PropertyName, validationError.ErrorMessage) + Environment.NewLine; var fail = new Exception(msg, dbEx); //Debug.WriteLine(fail.Message, fail); throw fail; } } /// <summary> /// Update entity /// </summary> /// <param name="entity">Entity</param> public virtual void Update(T entity) { try { if (entity == null) throw new ArgumentNullException("entity"); this._context.SaveChanges(); } catch (DbEntityValidationException dbEx) { var msg = string.Empty; foreach (var validationErrors in dbEx.EntityValidationErrors) foreach (var validationError in validationErrors.ValidationErrors) msg += Environment.NewLine + string.Format("Property: {0} Error: {1}", validationError.PropertyName, validationError.ErrorMessage); var fail = new Exception(msg, dbEx); //Debug.WriteLine(fail.Message, fail); throw fail; } } /// <summary> /// Delete entity /// </summary> /// <param name="entity">Entity</param> public virtual void Delete(T entity) { try { if (entity == null) throw new ArgumentNullException("entity"); this.Entities.Remove(entity); this._context.SaveChanges(); } catch (DbEntityValidationException dbEx) { var msg = string.Empty; foreach (var validationErrors in dbEx.EntityValidationErrors) foreach (var validationError in validationErrors.ValidationErrors) msg += Environment.NewLine + string.Format("Property: {0} Error: {1}", validationError.PropertyName, validationError.ErrorMessage); var fail = new Exception(msg, dbEx); //Debug.WriteLine(fail.Message, fail); throw fail; } } /// <summary> /// Gets a table /// </summary> public virtual IQueryable<T> Table { get { return this.Entities; } } /// <summary> /// Gets a table with "no tracking" enabled (EF feature) Use it only when you load record(s) only for read-only operations /// </summary> public virtual IQueryable<T> TableNoTracking { get { return this.Entities.AsNoTracking(); } } /// <summary> /// Entities /// </summary> protected virtual IDbSet<T> Entities { get { if (_entities == null) _entities = _context.Set<T>(); return _entities; } } } }
4、NopObjectContext NopObjectContext是EfRepository类中引用的接口IDbContext真正实现。
using System; using System.Collections.Generic; using System.Data; using System.Data.Common; using System.Data.Entity; using System.Data.Entity.Infrastructure; using System.Data.Entity.ModelConfiguration; using System.Linq; using System.Reflection; using Nop.Core; namespace Nop.Data { /// <summary> /// Object context /// </summary> public class NopObjectContext : DbContext, IDbContext { #region Ctor public NopObjectContext(string nameOrConnectionString) : base(nameOrConnectionString) { //((IObjectContextAdapter) this).ObjectContext.ContextOptions.LazyLoadingEnabled = true; } #endregion #region Utilities protected override void OnModelCreating(DbModelBuilder modelBuilder) { //dynamically load all configuration //System.Type configType = typeof(LanguageMap); //any of your configuration classes here //var typesToRegister = Assembly.GetAssembly(configType).GetTypes() var typesToRegister = Assembly.GetExecutingAssembly().GetTypes() .Where(type => !String.IsNullOrEmpty(type.Namespace)) .Where(type => type.BaseType != null && type.BaseType.IsGenericType && type.BaseType.GetGenericTypeDefinition() == typeof(EntityTypeConfiguration<>)); foreach (var type in typesToRegister) { dynamic configurationInstance = Activator.CreateInstance(type); modelBuilder.Configurations.Add(configurationInstance); } //...or do it manually below. For example, //modelBuilder.Configurations.Add(new LanguageMap()); base.OnModelCreating(modelBuilder); } /// <summary> /// 将一个实体Attach到一个数据库上下文,如果已经存在就直接返回这个实体 /// </summary> /// <typeparam name="TEntity">TEntity</typeparam> /// <param name="entity">Entity</param> /// <returns>Attached entity</returns> protected virtual TEntity AttachEntityToContext<TEntity>(TEntity entity) where TEntity : BaseEntity, new() { //little hack here until Entity Framework really supports stored procedures //otherwise, navigation properties of loaded entities are not loaded until an entity is attached to the context var alreadyAttached = Set<TEntity>().Local.FirstOrDefault(x => x.Id == entity.Id); if (alreadyAttached == null) { //attach new entity Set<TEntity>().Attach(entity); return entity; } else { //entity is already loaded. return alreadyAttached; } } #endregion #region Methods /// <summary> /// 创建一个数据库sql脚本 /// </summary> /// <returns>SQL to generate database</returns> public string CreateDatabaseScript() { return ((IObjectContextAdapter)this).ObjectContext.CreateDatabaseScript(); } /// <summary> /// Get DbSet /// </summary> /// <typeparam name="TEntity">Entity type</typeparam> /// <returns>DbSet</returns> public new IDbSet<TEntity> Set<TEntity>() where TEntity : BaseEntity { return base.Set<TEntity>(); } /// <summary> /// 执行存储过程并返回实体的List /// </summary> /// <typeparam name="TEntity">Entity type</typeparam> /// <param name="commandText">Command text</param> /// <param name="parameters">Parameters</param> /// <returns>Entities</returns> public IList<TEntity> ExecuteStoredProcedureList<TEntity>(string commandText, params object[] parameters) where TEntity : BaseEntity, new() { //add parameters to command if (parameters != null && parameters.Length > 0) { for (int i = 0; i <= parameters.Length - 1; i++) { var p = parameters[i] as DbParameter; if (p == null) throw new Exception("Not support parameter type"); commandText += i == 0 ? " " : ", "; commandText += "@" + p.ParameterName; if (p.Direction == ParameterDirection.InputOutput || p.Direction == ParameterDirection.Output) { //output parameter commandText += " output"; } } } var result = this.Database.SqlQuery<TEntity>(commandText, parameters).ToList(); //performance hack applied as described here - http://www.NopCommerce.com/boards/t/25483/fix-very-important-speed-improvement.aspx bool acd = this.Configuration.AutoDetectChangesEnabled; try { this.Configuration.AutoDetectChangesEnabled = false; for (int i = 0; i < result.Count; i++) result[i] = AttachEntityToContext(result[i]); } finally { this.Configuration.AutoDetectChangesEnabled = acd; } return result; } /// <summary> /// 执行sql语句并返回一个指定类型实体的集合 /// <typeparam name="TElement">The type of object returned by the query.</typeparam> /// <param name="sql">The SQL query string.</param> /// <param name="parameters">The parameters to apply to the SQL query string.</param> /// <returns>Result</returns> public IEnumerable<TElement> SqlQuery<TElement>(string sql, params object[] parameters) { return this.Database.SqlQuery<TElement>(sql, parameters); } /// <summary> /// 执行一个指定 DDL/DML 命令 /// </summary> /// <param name="sql">The command string</param> /// <param name="doNotEnsureTransaction">false - the transaction creation is not ensured; true - the transaction creation is ensured.</param> /// <param name="timeout">Timeout value, in seconds. A null value indicates that the default value of the underlying provider will be used</param> /// <param name="parameters">The parameters to apply to the command string.</param> /// <returns>The result returned by the database after executing the command.</returns> public int ExecuteSqlCommand(string sql, bool doNotEnsureTransaction = false, int? timeout = null, params object[] parameters) { int? previousTimeout = null; if (timeout.HasValue) { //store previous timeout previousTimeout = ((IObjectContextAdapter) this).ObjectContext.CommandTimeout; ((IObjectContextAdapter) this).ObjectContext.CommandTimeout = timeout; } var transactionalBehavior = doNotEnsureTransaction ? TransactionalBehavior.DoNotEnsureTransaction : TransactionalBehavior.EnsureTransaction; var result = this.Database.ExecuteSqlCommand(transactionalBehavior, sql, parameters); if (timeout.HasValue) { //Set previous timeout back ((IObjectContextAdapter) this).ObjectContext.CommandTimeout = previousTimeout; } //return result return result; } #endregion } }
可以看到这个类NopObjectContext里面使用了EF的一些API,对实体进行操作。比如:
base.OnModelCreating(modelBuilder); var result = this.Database.SqlQuery<TEntity>(commandText, parameters).ToList(); return this.Database.SqlQuery<TElement>(sql, parameters); var result = this.Database.ExecuteSqlCommand(transactionalBehavior, sql, parameters);
5、Nop对**Repository**调用
Nop中对业务都操作都是抽象为某个服务(XXService),也就是对Repository架构用Service对其进行了进一步的隔离。在Nop的Controller中引用都是引用的Service的接口。如后台项目Nop.Admin中的BlogController:
public partial class BlogController : BaseAdminController { #region Fields private readonly IBlogService _blogService; private readonly ILanguageService _languageService; private readonly IDateTimeHelper _dateTimeHelper; private readonly ILocalizationService _localizationService; private readonly IPermissionService _permissionService; private readonly IUrlRecordService _urlRecordService; private readonly IStoreService _storeService; private readonly IStoreMappingService _storeMappingService; #endregion public BlogController(IBlogService blogService, ILanguageService languageService, IDateTimeHelper dateTimeHelper, ILocalizationService localizationService, IPermissionService permissionService, IUrlRecordService urlRecordService, IStoreService storeService, IStoreMappingService storeMappingService) { this._blogService = blogService; this._languageService = languageService; this._dateTimeHelper = dateTimeHelper; this._localizationService = localizationService; this._permissionService = permissionService; this._urlRecordService = urlRecordService; this._storeService = storeService; this._storeMappingService = storeMappingService; } //省略其它代码... }
我们来看其中一个Service的实现,Nop.Services.Blogs.BlogService.cs:
namespace Nop.Services.Blogs { /// <summary> /// Blog service /// </summary> public partial class BlogService : IBlogService { #region Fields private readonly IRepository<BlogPost> _blogPostRepository; private readonly IRepository<BlogComment> _blogCommentRepository; private readonly IRepository<StoreMapping> _storeMappingRepository; private readonly CatalogSettings _catalogSettings; private readonly IEventPublisher _eventPublisher; #endregion #region Ctor public BlogService(IRepository<BlogPost> blogPostRepository, IRepository<BlogComment> blogCommentRepository, IRepository<StoreMapping> storeMappingRepository, CatalogSettings catalogSettings, IEventPublisher eventPublisher) { this._blogPostRepository = blogPostRepository; this._blogCommentRepository = blogCommentRepository; this._storeMappingRepository = storeMappingRepository; this._catalogSettings = catalogSettings; this._eventPublisher = eventPublisher; } #endregion //省略其它代码... } }
我可以看到在Service里面我们指定了多个IRepository具体的泛型类型(IRepository,IRepository,IRepository),而IRepository是通过依赖注入,研究里面代码我们可以知道是EfRepository,也就是IRepository这些真正的对应是(EfRepository,EfRepository,EfRepository),,这样在Service里面就可以通过这些IRepository相应的变量进行操作数据了。