上次搭建好了項目框架,但仍是以爲不太對勁,後來纔想起來沒有對開發目標進行定位,這個小demo雖然不用作需求分析,可是要實現什麼效果還得明確。後來想了一下就作個最簡單的網站,目標定爲小公司進行展現用的網站。功能有顯示用的文章功能(新聞、產品展現、公司介紹等),諮詢留言,評論等,另外還包括用戶管理,網站設置這兩個部分,算是個極簡的網站吧。html
ASP.NET MVC5 網站開發實踐(一) - 項目框架安全
前面項目的層次和調用關係都說明了,關係以下圖服務器
採用三層架構的時候,在羣裏跟 @盛開生命 討論過BLL層的必要性,以爲業務邏輯徹底能夠在controller裏實現,沒有必要單獨作一個項目,另外一個分層多了會影響性能。後來我仍是把業務邏輯獨立出來,緣由以下:架構
對於性能我以爲分層多了確定會有影響,可是不會很大。如今硬件的更新速度遠大於軟件,對業務邏輯處理起來很輕鬆,多實例化幾個類對性能影響不大。通常來講網站運行基本上是一個存數據庫和取數據庫的過程,業務邏輯仍是比較少,只不過如今的網站使用的圖片、動畫更多,效果更加絢麗。我以爲網站的效率瓶頸主要出如今服務器的帶寬、IO性能和存取數據庫上。在代碼方面能作的就是優化數據庫的存取。對了通常項目來講,爲了百分之幾的運行效率遠不如提升開發效率和更加容易的代碼管理重要,能實現需求就好,運行效率是哪是大牛要作的事。框架
對IDAL、DAL、IBLL 、BLL這四個項目:ide
IDAL寫一個Base接口,接口中固定幾個數據庫操做方法,其餘接口都繼承自這個接口;函數
DAL項目作個base類實現這個IDAL的base接口,其餘類都繼承自base類。性能
一樣IBLL中也寫一個Base接口,固定幾個基本的操做方法,一樣其餘接口也繼承自這個base接口優化
IBLL中也寫一個base類來實現IBLL中的base接口,其餘類繼承自這個base類。
這裏以對用戶的操做來構建代碼的基本模式:
這裏寫三個模型類。打開Ninesk.Models分別添加User、UserGroup、UserConfig三個模型類。
用戶模型或者叫帳戶模型,爲何這麼說看下面代碼
using System; using System.ComponentModel.DataAnnotations; namespace Ninesky.Models { /// <summary> /// 用戶模型 /// <remarks> /// 建立:2014.02.02<br /> /// 修改:2014.02.05 /// </remarks> /// </summary> public class User { [Key] public int UserID { get; set; } /// <summary> /// 用戶名 /// </summary> [Required(ErrorMessage="必填")] [StringLength(20,MinimumLength=4,ErrorMessage="{1}到{0}個字符")] [Display(Name="用戶名")] public string UserName { get; set; } /// <summary> /// 用戶組ID /// </summary> [Required(ErrorMessage = "必填")] [Display(Name = "用戶組ID")] public int GroupID { get; set; } /// <summary> /// 顯示名 /// </summary> [Required(ErrorMessage = "必填")] [StringLength(20, MinimumLength = 2, ErrorMessage = "{1}到{0}個字符")] [Display(Name = "顯示名")] public string DisplayName { get; set; } /// <summary> /// 密碼 /// </summary> [Required(ErrorMessage = "必填")] [Display(Name = "密碼")] [DataType(DataType.Password)] public string Password { get; set; } /// <summary> /// 郵箱 /// </summary> [Required(ErrorMessage = "必填")] [Display(Name = "郵箱")] [DataType(DataType.EmailAddress)] public string Email { get; set; } /// <summary> /// 用戶狀態<br /> /// 0正常,1鎖定,2未經過郵件驗證,3未經過管理員 /// </summary> public int Status { get; set; } /// <summary> /// 註冊時間 /// </summary> public DateTime RegistrationTime { get; set; } /// <summary> /// 上次登錄時間 /// </summary> public DateTime LoginTime { get; set; } /// <summary> /// 上次登錄IP /// </summary> public DateTime LoginIP { get; set; } public virtual UserGroup Group { get; set; } } }
這個模型類中只包含用戶名、密碼、用戶組、顯示名、郵箱等屬性,純粹是基本的帳戶信息,目的是讓用戶註冊的時候儘量的少填信息。其餘信息若是須要能夠再寫新類與帳戶進行關聯,用戶須要的時候登陸後再進行補填(如:資本資料、我的信息、聯繫方式等。這裏先不考慮這些)。這裏的顯示名根據須要能夠作暱稱、真實姓名等來使用。
這個類注意下GroupType,這個用來對用戶組進行一下分類的,方便管理,其實沒什麼特別的意義。個人想法是普通類型就放普通的註冊用戶的組,若是大的網站容許用戶升級的話,限定在這個類型的用戶組內。特權組能夠放一些vip之類的用戶組,須要管理員給予,區別普通用戶組,但又沒有管理權。管理類型的用戶組須要後臺管理員給予,能夠對文章、評論、諮詢進行管理。
using System.ComponentModel.DataAnnotations; namespace Ninesky.Models { /// <summary> /// 用戶組 /// <remarks> /// 建立:2014.02.02 /// 修改:2014.02.08 /// </remarks> /// </summary> public class UserGroup { [Key] public int GroupID { get; set; } /// <summary> /// 名稱 /// </summary> [Required(ErrorMessage="必填")] [StringLength(20, MinimumLength = 2, ErrorMessage = "{1}到{0}個字")] [Display(Name="名稱")] public string Name { get; set; } /// <summary> /// 用戶組類型<br /> /// 0普通類型(普通註冊用戶),1特權類型(像VIP之類的類型),3管理類型(管理權限的類型) /// </summary> [Required(ErrorMessage = "必填")] [Display(Name = "用戶組類型")] public int GroupType { get; set; } /// <summary> /// 說明 /// </summary> [Required(ErrorMessage = "必填")] [StringLength(50, ErrorMessage = "少於{0}個字")] [Display(Name = "說明")] public string Description { get; set; } } }
這個類是一些用戶配置信息(暫時只考慮了註冊設置),在後臺管理員處進行設置。
using System.ComponentModel.DataAnnotations; namespace Ninesky.Models { /// <summary> /// 用戶配置 /// <remarks> /// 建立:2014.02.06 /// </remarks> /// </summary> public class UserConfig { [Key] public int ConfigID { get; set; } /// <summary> /// 啓用註冊 /// </summary> [Display(Name = "啓用註冊")] [Required(ErrorMessage="必填")] public bool Enabled { get; set; } /// <summary> /// 禁止使用的用戶名<br /> /// 用戶名之間用「|」隔開 /// </summary> [Display(Name = "禁止使用的用戶名")] public string ProhibitUserName { get; set; } /// <summary> /// 啓用管理員驗證 /// </summary> [Display(Name = "啓用管理員驗證")] [Required(ErrorMessage = "必填")] public bool EnableAdminVerify { get; set; } /// <summary> /// 啓用郵件驗證 /// </summary> [Display(Name = "啓用郵件驗證")] [Required(ErrorMessage = "必填")] public bool EnableEmailVerify { get; set; } /// <summary> /// 默認用戶組Id /// </summary> [Display(Name = "默認用戶組Id")] [Required(ErrorMessage = "必填")] public int DefaultGroupId { get; set; } } }
數據存儲層負責與數據庫打交道,因爲使用了接口產生了兩個項目DAL和IDAL。IDAL是接口項目,DAL是接口的實現項目。
在與數據庫的方便有一些共同的操做,像添加、修改、刪除、查詢等。不想在實際寫代碼的時候在用戶類寫一遍這些東西,用戶組類再寫一遍、之後文章、評論都再重複寫這些代碼。怎麼辦,弄個基類。之後其餘類從基類繼承就把這些公共方法繼承過來了。
首先打開IDAL項目,添加類InterfaceBaseRepository,代碼以下。
using System; using System.Linq; using System.Linq.Expressions; namespace Ninesky.IDAL { /// <summary> /// 接口基類 /// <remarks>建立:2014.02.03 <br /> /// 修改:2014.02.09</remarks> /// </summary> /// <typeparam name="T">類型</typeparam> public interface InterfaceBaseRepository<T> { /// <summary> /// 添加 /// </summary> /// <param name="entity">數據實體</param> /// <returns>添加後的數據實體</returns> T Add(T entity); /// <summary> /// 查詢記錄數 /// </summary> /// <param name="predicate">條件表達式</param> /// <returns>記錄數</returns> int Count(Expression<Func<T, bool>> predicate); /// <summary> /// 更新 /// </summary> /// <param name="entity">數據實體</param> /// <returns>是否成功</returns> bool Update(T entity); /// <summary> /// 刪除 /// </summary> /// <param name="entity">數據實體</param> /// <returns>是否成功</returns> bool Delete(T entity); /// <summary> /// 是否存在 /// </summary> /// <param name="anyLambda">查詢表達式</param> /// <returns>布爾值</returns> bool Exist(Expression<Func<T, bool>> anyLambda); /// <summary> /// 查詢數據 /// </summary> /// <param name="whereLambda">查詢表達式</param> /// <returns>實體</returns> T Find(Expression<Func<T, bool>> whereLambda); /// <summary> /// 查找數據列表 /// </summary> /// <typeparam name="S">排序</typeparam> /// <param name="whereLamdba">查詢表達式</param> /// <param name="isAsc">是否升序</param> /// <param name="orderLamdba">排序表達式</param> /// <returns></returns> IQueryable<T> FindList<S>(Expression<Func<T, bool>> whereLamdba, bool isAsc, Expression<Func<T, S>> orderLamdba); /// <summary> /// 查找分頁數據列表 /// </summary> /// <typeparam name="S">排序</typeparam> /// <param name="pageIndex">當前頁</param> /// <param name="pageSize">每頁記錄數</param> /// <param name="totalRecord">總記錄數</param> /// <param name="whereLamdba">查詢表達式</param> /// <param name="isAsc">是否升序</param> /// <param name="orderLamdba">排序表達式</param> /// <returns></returns> IQueryable<T> FindPageList<S>(int pageIndex, int pageSize, out int totalRecord, Expression<Func<T, bool>> whereLamdba, bool isAsc, Expression<Func<T, S>> orderLamdba); } }
還使用了泛型,在繼承的時候傳入實體類型就能夠直接繼承這些方法了。具體看下InterfaceUserRepository接口就清楚了。
using Ninesky.Models; namespace Ninesky.IDAL { /// <summary> /// 用戶接口 /// <remarks>建立:2014.02.03</remarks> /// </summary> public interface InterfaceUserRepository:InterfaceBaseRepository<User> { } }
簡單吧,繼承自InterfaceBaseRepository接口並傳入實體類User就好了。咱們在類視圖中看下,是否是繼承了基類的接口。
DAL項目是對IDAL項目接口的實現,項目中要建立DbContext類,對於DbContext類不少人討論過它對數據庫存取的效率,MSDN中說其是輕量的, 建立不須要很大開銷,它也不是線程安全的對象,而且具備數據容器的性質(跟蹤),所以不少人認爲不該該將其靜態化、單例化。可是對用戶的單次請求來講實現DbContext惟一是合理的。 先看代碼吧,很是簡單。
using Ninesky.Models; using System.Data.Entity; namespace Ninesky.DAL { /// <summary> /// 數據上下文 /// <remarks>建立:2014.02.03</remarks> /// </summary> public class NineskyDbContext:DbContext { public DbSet<User> Users { get; set; } public DbSet<UserGroup> UserGroups { get; set; } public DbSet<UserConfig> UserConfig { get; set; } public NineskyDbContext() : base("DefaultConnection") { } } }
下面建立一個BaseRepository類,繼承自InterfaceBaseRepository並實現類其接口的方法。
using Ninesky.IDAL; using System; using System.Linq; using System.Linq.Expressions; namespace Ninesky.DAL { /// <summary> /// 倉儲基類 /// <remarks>建立:2014.02.03</remarks> /// </summary> public class BaseRepository<T>: InterfaceBaseRepository<T> where T : class { protected NineskyDbContext nContext = ContextFactory.GetCurrentContext(); public T Add(T entity) { nContext.Entry<T>(entity).State = System.Data.Entity.EntityState.Added; nContext.SaveChanges(); return entity; } public int Count(Expression<Func<T, bool>> predicate) { return nContext.Set<T>().Count(predicate); } public bool Update(T entity) { nContext.Set<T>().Attach(entity); nContext.Entry<T>(entity).State = System.Data.Entity.EntityState.Modified; return nContext.SaveChanges() > 0; } public bool Delete(T entity) { nContext.Set<T>().Attach(entity); nContext.Entry<T>(entity).State = System.Data.Entity.EntityState.Deleted; return nContext.SaveChanges() > 0; } public bool Exist(Expression<Func<T, bool>> anyLambda) { return nContext.Set<T>().Any(anyLambda); } public T Find(Expression<Func<T, bool>> whereLambda) { T _entity = nContext.Set<T>().FirstOrDefault<T>(whereLambda); return _entity; } public IQueryable<T> FindList<S>(Expression<Func<T, bool>> whereLamdba, bool isAsc, Expression<Func<T, S>> orderLamdba) { var _list = nContext.Set<T>().Where<T>(whereLamdba); if (isAsc) _list = _list.OrderBy<T, S>(orderLamdba); else _list = _list.OrderByDescending<T, S>(orderLamdba); return _list; } public IQueryable<T> FindPageList<S>(int pageIndex, int pageSize, out int totalRecord, Expression<Func<T, bool>> whereLamdba, bool isAsc, Expression<Func<T, S>> orderLamdba) { var _list = nContext.Set<T>().Where<T>(whereLamdba); totalRecord = _list.Count(); if (isAsc) _list = _list.OrderBy<T, S>(orderLamdba).Skip<T>((pageIndex - 1) * pageSize).Take<T>(pageSize); else _list = _list.OrderByDescending<T, S>(orderLamdba).Skip<T>((pageIndex - 1) * pageSize).Take<T>(pageSize); return _list; } } }
代碼中都是對數據庫的操做。比較有看頭的是這句protected NineskyDbContext nContext = ContextFactory.GetCurrentContext();
ContextFactory是一個簡單工廠類,GetCurrentContext()是一個靜態函數。利用簡單工廠獲取請求內的當前DbContext,也就是請求內的DbContext單例。先添加一個工廠類ContextFactory
using System.Data.Entity; using System.Runtime.Remoting.Messaging; namespace Ninesky.DAL { /// <summary> /// 上下文簡單工廠 /// <remarks> /// 建立:2014.02.05 /// </remarks> /// </summary> public class ContextFactory { /// <summary> /// 獲取當前數據上下文 /// </summary> /// <returns></returns> public static NineskyDbContext GetCurrentContext() { NineskyDbContext _nContext = CallContext.GetData("NineskyContext") as NineskyDbContext; if (_nContext == null) { _nContext = new NineskyDbContext(); CallContext.SetData("NineskyContext", _nContext); } return _nContext; } } }
這裏是先在CallContext中獲取NineskyContext,若是爲空則初始化一個NineskyContext,若是存在則直接返回。看CallContext,MSDN中講CallContext提供對每一個邏輯執行線程都惟一的數據槽,而在WEB程序裏,每個請求恰巧就是一個邏輯線程因此可使用CallContext來實現單個請求以內的DbContext單例。
下面添加具體的倉儲代碼。
在DAL中再添加一個UserRepository類,繼承自BaseRepository和InterfaceUserRepository。目的是繼承自BaseRepository類,實現InterfaceUserRepositor接口。
using Ninesky.IDAL; using Ninesky.Models; using System.Linq; namespace Ninesky.DAL { /// <summary> /// 用戶倉庫 /// <remarks>建立:2014.02.03</remarks> /// </summary> class UserRepository: BaseRepository<User>, InterfaceUserRepository { } }
UserRepository就直接繼承了基類中的方法,基類中的方法能知足絕大部分須要,UserRepository就不用再增長函數了,其餘Repository類都相似,不在貼代碼了。
這裏咱們在建一個Repository工廠,用來返回項目中的全部Repository類。
using Ninesky.IDAL; namespace Ninesky.DAL { /// <summary> /// 簡單工廠? /// <remarks>建立:2014.02.03</remarks> /// </summary> public static class RepositoryFactory { /// <summary> /// 用戶倉儲 /// </summary> public static InterfaceUserRepository UserRepository { get { return new UserRepository(); } } } }
之後在BLL中調用的時候就不用每次都寫InterfaceUserRepository _iUserRsy = new UserRepository()了,直接寫成InterfaceUserRepository _iUserRsy = RepositoryFactory.UserRepository這個東西的好處就是,之後在DAL項目中實現InterfaceUserRepository接口的類須要修改時咱們能夠直接建立個新類,而後RepositoryFactory類中讓UserRepository屬性返回新類就好了。
IBLL是業務邏輯層的接口,業務邏輯層對數據庫的操做上基本仍是增、刪、改。一樣寫一個基接口把這三個操做寫進去,這裏與IDAL思路相似。
namespace Ninesky.IBLL { /// <summary> /// 接口基類 /// <remarks>建立:2014.02.03</remarks> /// </summary> public interface InterfaceBaseService<T> where T : class { /// <summary> /// 添加 /// </summary> /// <param name="entity">數據實體</param> /// <returns>添加後的數據實體</returns> T Add(T entity); /// <summary> /// 更新 /// </summary> /// <param name="entity">數據實體</param> /// <returns>是否成功</returns> bool Update(T entity); /// <summary> /// 刪除 /// </summary> /// <param name="entity">數據實體</param> /// <returns>是否成功</returns> bool Delete(T entity); } }
在添加一個InterfaceUserService接口,繼承自InterfaceBaseService。根據須要在接口中又添加了幾個方法。在這裏對Find方法的名稱進行統一,凡是返回實體類的名稱爲Find()或FindByXXX(),返回一組數據的方法名稱爲FindList()或FindXXXList,分頁的名稱格式爲FindPageList()或FindxxxPageList()
using Ninesky.Models; using System.Linq; namespace Ninesky.IBLL { /// <summary> /// 用戶相關接口 /// <remarks> /// 建立:2014.02.09 /// </remarks> /// </summary> public interface InterfaceUserService:InterfaceBaseService<User> { /// <summary> /// 用戶是否存在 /// </summary> /// <param name="userName">用戶名</param> /// <returns>布爾值</returns> bool Exist(string userName); /// <summary> /// 查找用戶 /// </summary> /// <param name="userID">用戶ID</param> /// <returns></returns> User Find(int userID); /// <summary> /// 查找用戶 /// </summary> /// <param name="userName">用戶名</param> /// <returns></returns> User Find(string userName); /// <summary> /// 用戶列表 /// </summary> /// <param name="pageIndex">頁碼數</param> /// <param name="pageSize">每頁記錄數</param> /// <param name="totalRecord">總記錄數</param> /// <param name="order">排序:0-ID升序(默認),1ID降序,2註冊時間升序,3註冊時間降序,4登陸時間升序,5登陸時間降序</param> /// <returns></returns> IQueryable<User> FindPageList(int pageIndex, int pageSize, out int totalRecord,int order); } }
BLL項目中要實現InterfaceUserService接口的方法,先添加BaseService的
using Ninesky.IBLL; using Ninesky.IDAL; namespace Ninesky.BLL { /// <summary> /// 服務基類 /// <remarks>建立:2014.02.03</remarks> /// </summary> public abstract class BaseService<T> : InterfaceBaseService<T> where T : class { protected InterfaceBaseRepository<T> CurrentRepository { get; set; } public BaseService(InterfaceBaseRepository<T> currentRepository) { CurrentRepository = currentRepository; } public T Add(T entity) { return CurrentRepository.Add(entity); } public bool Update(T entity) { return CurrentRepository.Update(entity); } public bool Delete(T entity) { return CurrentRepository.Delete(entity); } } }
這個類的構造函數中要傳入一個參數就是currentRepository 這個在繼承的時候進行傳入。這裏仍是看用戶類。
using Ninesky.DAL; using Ninesky.IBLL; using Ninesky.Models; using System.Linq; namespace Ninesky.BLL { /// <summary> /// 用戶服務類 /// <remarks> /// 建立:2014.02.12 /// </remarks> /// </summary> public class UserService:BaseService<User>,InterfaceUserService { public UserService() : base(RepositoryFactory.UserRepository) { } public bool Exist(string userName) { return CurrentRepository.Exist(u => u.UserName == userName);} public User Find(int userID) { return CurrentRepository.Find(u => u.UserID == userID); } public User Find(string userName) { return CurrentRepository.Find(u => u.UserName == userName); } public IQueryable<User> FindPageList(int pageIndex, int pageSize, out int totalRecord, int order) { switch(order) { case 0: return CurrentRepository.FindPageList(pageIndex, pageSize, out totalRecord, u => true, true, u => u.UserID); case 1: return CurrentRepository.FindPageList(pageIndex, pageSize, out totalRecord, u => true, false, u => u.UserID); case 2: return CurrentRepository.FindPageList(pageIndex, pageSize, out totalRecord, u => true, true, u => u.RegistrationTime); case 3: return CurrentRepository.FindPageList(pageIndex, pageSize, out totalRecord, u => true, false, u => u.RegistrationTime); case 4: return CurrentRepository.FindPageList(pageIndex, pageSize, out totalRecord, u => true, true, u => u.LoginTime); case 5: return CurrentRepository.FindPageList(pageIndex, pageSize, out totalRecord, u => true, false, u => u.LoginTime); default: return CurrentRepository.FindPageList(pageIndex, pageSize, out totalRecord, u => true, true, u => u.UserID); } } } }
上面這個FindPageList代碼太累贅了,一時還沒想到好方法。
今天寫到這裏仍是在想項目間的調用實現,寫了兩個base接口、兩個base類,之後其餘的類都從它們繼承,寫法都很相似。下次能夠開始作界面了,在Ninesky.Web項目中基本上是經過IBLL,BLL跟數據進行打交道了。
===================================================
FindPageList() 這個排序的方法確實不太通用,參考@jingming290 提供的代碼修改以下:
一、接口 InterfaceBaseRepository
修改兩個接口方法如圖紅框部分。
二、BaseRepository類
添加OrderBy方法,代碼以下:
/// <summary> /// 排序 /// </summary> /// <typeparam name="T">類型</typeparam> /// <param name="source">原IQueryable</param> /// <param name="propertyName">排序屬性名</param> /// <param name="isAsc">是否正序</param> /// <returns>排序後的IQueryable<T></returns> private IQueryable<T> OrderBy(IQueryable<T> source, string propertyName, bool isAsc) { if (source == null) throw new ArgumentNullException("source", "不能爲空"); if (string.IsNullOrEmpty(propertyName)) return source; var _parameter = Expression.Parameter(source.ElementType); var _property = Expression.Property(_parameter, propertyName); if (_property == null) throw new ArgumentNullException("propertyName", "屬性不存在"); var _lambda = Expression.Lambda(_property, _parameter); var _methodName = isAsc ? "OrderBy" : "OrderByDescending"; var _resultExpression = Expression.Call(typeof(Queryable), _methodName, new Type[] { source.ElementType, _property.Type }, source.Expression, Expression.Quote(_lambda)); return source.Provider.CreateQuery<T>(_resultExpression); }
修改FindList和FindPageList方法,修改下圖
三、修改UserService的FindPageList方法
修改後的代碼以下:
public IQueryable<User> FindPageList(int pageIndex, int pageSize, out int totalRecord, int order) { bool _isAsc = true; string _orderName = string.Empty; switch(order) { case 0: _isAsc = true; _orderName = "UserID"; break; case 1: _isAsc = false; _orderName = "UserID"; break; case 2: _isAsc = true; _orderName = "RegistrationTime"; break; case 3: _isAsc = false; _orderName = "RegistrationTime"; break; case 4: _isAsc = true; _orderName = "LoginTime"; break; case 5: _isAsc = false; _orderName = "LoginTime"; break; default: _isAsc = false; _orderName = "UserID"; break; } return CurrentRepository.FindPageList(pageIndex, pageSize, out totalRecord, u => true, _orderName, _isAsc); }
感謝@jingming290!