NopCommerce源码架构详解-MVC5使用EF6实现Repository模式相关源码分析

NopCommerce源码架构详解-MVC5使用EF6实现Repository模式相关源码分析

4555发表于2015-10-19

Repository模式是属于领域模型范畴,是架构模式。nop中使用了这个种模板来读取、操作数据。今天我们通过Nop来看看在ASP.NET mvc5使用EF6实现Repository模式的方案。

一、理解Repository模式

首先我们来看看Repository模式相关的概念:

Repository(资源库)

               协调领域和数据映射层,利用类似于集合的接口来访问领域对象 

定义(来自Martin Fowler的《企业应用架构模式》):

               Mediates between the domain and data mapping layers using a collection-like interface for accessing domain objects.

              个人理解:Repository是一个独立的层,介于领域层与数据映射层(数据访问层)之间。它的存在让领域层感觉不到数据访问层的存在,它提供一个类似集合的接口提供给领域层进行领域对象的访问。Repository是仓库管理员,领域层需要什么东西只需告诉仓库管理员,由仓库管理员把东西拿给它,并不需要知道东西实际放在哪。

1. Repository模式是架构模式,在设计架构时,才有参考价值;

2. Repository模式主要是封装数据查询和存储逻辑;

3. Repository模式实际用途:更换、升级ORM引擎,不影响业务逻辑;

4. Repository模式能提高测试效率,单元测试时,用Mock对象代替实际的数据库存取,可以成倍地提高测试用例运行速度。

评估:应用Repository模式所带来的好处,远高于实现这个模式所增加的代码。只要项目分层,都应当使用这个模式。

关于泛型Repository接口:

仅使用泛型Repository接口并不太合适,因为Repository接口是提供给Domain层的操作契约,不同的entity对于Domain来说可能有不同的操作约束。因此Repository接口还是应该单独针对每个Eneity类来定义。

泛型的Repository<T>类仍然用来减少重复代码,只是不能被UserRepository类直接继承,因为这样Delete方法将侵入User类,所以改为在UserRepository中组合一个Repository<T>,将开放给domain可见且又能使用泛型重用的功能委托给这个Repository<T>

Repository与Dal的区别:

Repository是DDD中的概念,强调Repository是受Domain驱动的,Repository中定义的功能要体现Domain的意图和约束,而Dal更纯粹的就是提供数据访问的功能,并不严格受限于Business层。

使用Repository,隐含着一种意图倾向,就是 Domain需要什么我才提供什么,不该提供的功能就不要提供,一切都是以Domain的需求为核心;而使用Dal,其意图倾向在于我Dal层能使用的数据库访问操作提供给Business层,你Business要用哪个自己选。换一个Business也可以用我这个Dal,一切是以我Dal能提供什么操作为核心。

二、Nop中的Repository模式架构

2.1、主要类及用途

Nop使用的ORM构架是EF,并且采用Repository模式架构。主要用到的类和接口:

1、Nop.Core.Data.IRepository<T>

2、Nop.Data.EfRepository<T>

4、Nop.Data.BaseDataProviderManager,Nop.Data.EfDataProviderManager

5、Nop.Data.IDbContext

6、Nop.Data.NopObjectContext

主要相关类图


2.2、核心代码

1、泛型的接口IRepository<T>:

Nop抽象出一个实体常用的一些操作,据此定义一个泛型的接口IRepository<T>,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<BlogPost> 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<BlogPost>,IRepository<BlogComment>,IRepository<StoreMapping>),而IRepository<T>是通过依赖注入,研究里面代码我们可以知道是EfRepository<T>,也就是IRepository<BlogPost>这些真正的对应是(EfRepository<BlogPost>,EfRepository<BlogComment>,EfRepository<StoreMapping>),,这样在Service里面就可以通过这些IRepository相应的变量进行操作数据了。


小编蓝狐