chsakell分享了一個前端使用AngularJS,后端使用ASP.NET Web API的項目。
源碼: https://github.com/chsakell/spa-webapi-angularjs
文章:http://chsakell.com/2015/08/23/building-single-page-applications-using-web-api-and-angularjs-free-e-book/
這里記錄下對此項目的理解。分為如下幾篇:
● 對一個前端使用AngularJS后端使用ASP.NET Web API項目的理解(1)--領域、Repository、Service
● 對一個前端使用AngularJS后端使用ASP.NET Web API項目的理解(2)--依賴倒置、Bundling、視圖模型驗證、視圖模型和領域模型映射、自定義handler
● 對一個前端使用AngularJS后端使用ASP.NET Web API項目的理解(3)--主頁面布局
● 對一個前端使用AngularJS后端使用ASP.NET Web API項目的理解(4)--Movie增改查以及上傳圖片
從數據庫開始,項目架構大致為:
→SQL Server Database
→Domain Entities(放在了HomeCinema.Entities類庫中)
→Generic Repositories(放在了HomeCinema.Data類庫中,EF上下文、數據庫遷移等放在了這里)
→Service Layer(與會員驗證有關,放在了HomeCinema.Services類庫中)
→Web API Controllers(放在了HomeCinema.Web的MVC的Web項目中,即ASP.NET Web API寄宿在ASP.NET MVC下)
→前端使用AngularJS
領域
所有的領域都有主鍵,抽象出一個接口,包含一個主鍵屬性。
namespace HomeCinema.Entities { public interface IEntityBase { int ID { get; set; } } }
Genre和Moive,是1對多關系。
namespace HomeCinema.Entities { public class Genre : IEntityBase { public Genre() { Movies = new List<Movie>(); } public int ID { get; set; } public virtual ICollection<Movie> Movies { get; set; } } } namespace HomeCinema.Entities { public class Movie : IEntityBase { public Movie() { Stocks = new List<Stock>(); } public int ID { get; set; } //一個主鍵+一個導航屬性,標配 public int GenreId { get; set; } public virtual Genre Genre { get; set; } public virtual ICollection<Stock> Stocks { get; set; } } } Movie和Stock也是1對多關系。 namespace HomeCinema.Entities { public class Stock : IEntityBase { public Stock() { Rentals = new List<Rental>(); } public int ID { get; set; } public int MovieId { get; set; } public virtual Movie Movie { get; set; } public virtual ICollection<Rental> Rentals { get; set; } } }
Stock和Rental也是1對多關系。
namespace HomeCinema.Entities { public class Rental : IEntityBase { public int ID { get; set; } public int StockId { get; set; } public virtual Stock Stock { get; set; } } }
User和Role是多對多關系,用到了中間表UserRole。
namespace HomeCinema.Entities { public class User : IEntityBase { public User() { UserRoles = new List<UserRole>(); } public int ID { get; set; } public virtual ICollection<UserRole> UserRoles { get; set; } } } namespace HomeCinema.Entities { public class Role : IEntityBase { public int ID { get; set; } public string Name { get; set; } } } namespace HomeCinema.Entities { public class UserRole : IEntityBase { public int ID { get; set; } public int UserId { get; set; } public int RoleId { get; set; } public virtual Role Role { get; set; } } }
HomeCinema.Entities類庫中的Customer和Error類是單獨的,和其它類沒有啥關系。
Repository
首先需要一個上下文類,繼承DbContext,在構造函數中注明連接字符串的名稱,生成數據庫的方式等,提供某個領域的IDbSet<T>以便外界獲取,提供單元提交的方法,以及提供一個方法使有關領域的配置生效。
namespace HomeCinema.Data { public class HomeCinemaContext : DbContext { public HomeCinemaContext() : base("HomeCinema") { Database.SetInitializer<HomeCinemaContext>(null); } #region Entity Sets public IDbSet<User> UserSet { get; set; } ... #endregion public virtual void Commit() { base.SaveChanges(); } protected override void OnModelCreating(DbModelBuilder modelBuilder) { modelBuilder.Conventions.Remove<PluralizingTableNameConvention>(); modelBuilder.Configurations.Add(new UserConfiguration()); ... } } }
以上的UserConfiguration繼承於EntityBaseConfiguration<User>,而Configurations是EntityBaseConfiguration<T>的一個集合。
namespace HomeCinema.Data.Configurations { public class UserConfiguration : EntityBaseConfiguration<User> { public UserConfiguration() { Property(u => u.Username).IsRequired().HasMaxLength(100); Property(u => u.Email).IsRequired().HasMaxLength(200); Property(u => u.HashedPassword).IsRequired().HasMaxLength(200); Property(u => u.Salt).IsRequired().HasMaxLength(200); Property(u => u.IsLocked).IsRequired(); Property(u => u.DateCreated); } } }
接下來,需要一個單元工作類,通過這個類可以獲取到上下文,以及提交所有上下文的變化。
肯定需要上下文,上下文的生產交給工廠,而工廠還能對上下文進行垃圾回收。
垃圾回收就需要實現IDisposable接口。
先來實現IDisposable接口,我們希望實現IDisposable接口的類能騰出一個虛方法來,以便讓工廠可以對上下文進行垃圾回收。
namespace HomeCinema.Data.Infrastructure { public class Disposable : IDisposable { private bool isDisposed; ~Disposable() { Dispose(false); } public void Dispose() { Dispose(true); GC.SuppressFinalize(this); } private void Dispose(bool disposing) { if (!isDisposed && disposing) { DisposeCore(); } isDisposed = true; } // Ovveride this to dispose custom objects protected virtual void DisposeCore() { } } }
以上Disposable類中,只要生產上下文的工廠能繼承它,就可以使用它的虛方法DisposeCore把上下文回收掉。
接下來要創建工廠了,先創建工廠接口。
namespace HomeCinema.Data.Infrastructure { public interface IDbFactory : IDisposable { HomeCinemaContext Init(); } }
好,具體工廠不僅要實現IDbFactory接口,而且還要實現Disposable類,因為還要回收上下文嘛。
namespace HomeCinema.Data.Infrastructure { public class DbFactory : Disposable, IDbFactory { HomeCinemaContext dbContext; public HomeCinemaContext Init() { return dbContext ?? (dbContext = new HomeCinemaContext()); } protected override void DisposeCore() { if (dbContext != null) dbContext.Dispose(); } } }
現在,有了工廠,就可以生產上下文,接下來就到工作單元類了,它要做的工作一個是提供一個提交所有變化的方法,另一個是可以讓外界可以獲取到上下文類。
先來單元工作的接口。
namespace HomeCinema.Data.Infrastructure { public interface IUnitOfWork { void Commit(); } }
最后,具體的單元工作類。
namespace HomeCinema.Data.Infrastructure { public class UnitOfWork : IUnitOfWork { private readonly IDbFactory dbFactory; private HomeCinemaContext dbContext; public UnitOfWork(IDbFactory dbFactory) { this.dbFactory = dbFactory; } public HomeCinemaContext DbContext { get { return dbContext ?? (dbContext = dbFactory.Init()); } } public void Commit() { DbContext.Commit(); } } }
以上,通過構造函數把工廠注入,Commit方法提交所有變化,DbContext類獲取EF到上下文。每當對某個表或某幾個表做了操作,就使用這里的Commit方法一次性提交變化。
針對所有的領域都會有增刪改查等,需要抽象出一個泛型接口。
namespace HomeCinema.Data.Repositories { public interface IEntityBaseRepository<T> where T : class, IEntityBase, new() { IQueryable<T> AllIncluding(params Expression<Func<T, object>>[] includeProperties); IQueryable<T> All { get; } IQueryable<T> GetAll(); T GetSingle(int id); IQueryable<T> FindBy(Expression<Func<T, bool>> predicate); void Add(T entity); void Delete(T entity); void Edit(T entity); } }
如何實現呢?需要上下文,用工廠來創建上下文,通過構造函數把上下文工廠注入進來。
namespace HomeCinema.Data.Repositories { public class EntityBaseRepository<T> : IEntityBaseRepository<T> where T : class, IEntityBase, new() { private HomeCinemaContext dataContext; #region Properties protected IDbFactory DbFactory { get; private set; } protected HomeCinemaContext DbContext { get { return dataContext ?? (dataContext = DbFactory.Init()); } } public EntityBaseRepository(IDbFactory dbFactory) { DbFactory = dbFactory; } #endregion public virtual IQueryable<T> GetAll() { return DbContext.Set<T>(); } public virtual IQueryable<T> All { get { return GetAll(); } } public virtual IQueryable<T> AllIncluding(params Expression<Func<T, object>>[] includeProperties) { IQueryable<T> query = DbContext.Set<T>(); foreach (var includeProperty in includeProperties) { query = query.Include(includeProperty); } return query; } public T GetSingle(int id) { return GetAll().FirstOrDefault(x => x.ID == id); } public virtual IQueryable<T> FindBy(Expression<Func<T, bool>> predicate) { return DbContext.Set<T>().Where(predicate); } public virtual void Add(T entity) { DbEntityEntry dbEntityEntry = DbContext.Entry<T>(entity); DbContext.Set<T>().Add(entity); } public virtual void Edit(T entity) { DbEntityEntry dbEntityEntry = DbContext.Entry<T>(entity); dbEntityEntry.State = EntityState.Modified; } public virtual void Delete(T entity) { DbEntityEntry dbEntityEntry = DbContext.Entry<T>(entity); dbEntityEntry.State = EntityState.Deleted; } } }
Service
這里的Service主要針對於注冊登錄驗證等相關的方面。
當注冊用戶的時候,需要對用戶輸入的密碼加密,先寫一個有關加密的接口:
namespace HomeCinema.Services { public interface IEncryptionService { string CreateSalt(); string EncryptPassword(string password, string salt); } }
如何實現這個接口呢?大致是先把用戶輸入的密碼和某個隨機值拼接在一起,然后轉換成字節數組,計算Hash值,再轉換成字符串。
namespace HomeCinema.Services { public class EncryptionService : IEncryptionService { public string CreateSalt() { var data = new byte[0x10]; using (var cryptoServiceProvider = new RNGCryptoServiceProvider()) { cryptoServiceProvider.GetBytes(data); return Convert.ToBase64String(data); } } public string EncryptPassword(string password, string salt) { using (var sha256 = SHA256.Create()) { var saltedPassword = string.Format("{0}{1}", salt, password); byte[] saltedPasswordAsBytes = Encoding.UTF8.GetBytes(saltedPassword); return Convert.ToBase64String(sha256.ComputeHash(saltedPasswordAsBytes)); } } } }
有關用戶的驗證、創建、獲取、獲取所有角色,先寫一個接口:
namespace HomeCinema.Services { public interface IMembershipService { MembershipContext ValidateUser(string username, string password); User CreateUser(string username, string email, string password, int[] roles); User GetUser(int userId); List<Role> GetUserRoles(string username); } }
驗證用戶ValidateUser方法返回的MembershipContext類型是對Identity中的IPrincipal,User的一個封裝。
namespace HomeCinema.Services.Utilities { public class MembershipContext { public IPrincipal Principal { get; set; } public User User { get; set; } public bool IsValid() { return Principal != null; } } }
具體實現,用到了有關User, Role, UserRole的Repository,用到了有關加密的服務,還用到了工作單元,把所有這些的接口通過構造函數注入進來。
namespace HomeCinema.Services { public class MembershipService : IMembershipService { #region Variables private readonly IEntityBaseRepository<User> _userRepository; private readonly IEntityBaseRepository<Role> _roleRepository; private readonly IEntityBaseRepository<UserRole> _userRoleRepository; private readonly IEncryptionService _encryptionService; private readonly IUnitOfWork _unitOfWork; #endregion public MembershipService(IEntityBaseRepository<User> userRepository, IEntityBaseRepository<Role> roleRepository, IEntityBaseRepository<UserRole> userRoleRepository, IEncryptionService encryptionService, IUnitOfWork unitOfWork) { _userRepository = userRepository; _roleRepository = roleRepository; _userRoleRepository = userRoleRepository; _encryptionService = encryptionService; _unitOfWork = unitOfWork; } #region IMembershipService Implementation ...... } }
接下來實現MembershipContext ValidateUser(string username, string password)這個方法。可以根據username獲取User,再把用戶輸入的password重新加密以便與現有的經過加密的密碼比較,如果User存在,密碼匹配,就來構建MembershipContext的實例。
public MembershipContext ValidateUser(string username, string password) { var membershipCtx = new MembershipContext(); //獲取用戶 var user = _userRepository.GetSingleByUsername(username); //如果用戶存在且密碼匹配 if (user != null && isUserValid(user, password)) { //根據用戶名獲取用戶的角色集合 var userRoles = GetUserRoles(user.Username); //構建MembershipContext membershipCtx.User = user; var identity = new GenericIdentity(user.Username); membershipCtx.Principal = new GenericPrincipal( identity, userRoles.Select(x => x.Name).ToArray()); } return membershipCtx; }
可是,根據用戶名獲取用戶的GetSingleByUsername(username)方法還沒定義呢?而這不在IEntityBaseRepository<User>定義的接口方法之內。現在,就可以針對IEntityBaseRepository<User>寫一個擴展方法。
namespace HomeCinema.Data.Extensions { public static class UserExtensions { public static User GetSingleByUsername(this IEntityBaseRepository<User> userRepository, string username) { return userRepository.GetAll().FirstOrDefault(x => x.Username == username); } } }
在已知用戶存在,判斷用戶密碼是否正確的isUserValid(user, password)也還沒有定義?邏輯必定是重新加密用戶輸入的字符串與用戶現有的加密字符串比較。可是,加密密碼的時候還用到了一個salt值,怎樣保證用的是同一個salt值呢?不用擔心,User類中定義了一個Salt屬性,用來儲存每次加密的salt值。
private bool isPasswordValid(User user, string password) { return string.Equals(_encryptionService.EncryptPassword(password, user.Salt), user.HashedPassword); } private bool isUserValid(User user, string password) { if (isPasswordValid(user, password)) { return !user.IsLocked; } return false; }
另外,根據用戶名獲取用戶角色的方法GetUserRoles(user.Username)也還沒有定義?該方法的邏輯無非是便利當前用戶的導航屬性UserRoles獲取所有的角色。
public List<Role> GetUserRoles(string username) { List<Role> _result = new List<Role>(); var existingUser = _userRepository.GetSingleByUsername(username); if (existingUser != null) { foreach (var userRole in existingUser.UserRoles) { _result.Add(userRole.Role); } } return _result.Distinct().ToList(); }
接下來實現User CreateUser(string username, string email, string password, int[] roles)方法,邏輯是先判斷用戶是否存在,如果不存在,先添加用戶表,再添加用戶角色中間表。
public User CreateUser(string username, string email, string password, int[] roles) { var existingUser = _userRepository.GetSingleByUsername(username); if (existingUser != null) { throw new Exception("Username is already in use"); } var passwordSalt = _encryptionService.CreateSalt(); var user = new User() { Username = username, Salt = passwordSalt, Email = email, IsLocked = false, HashedPassword = _encryptionService.EncryptPassword(password, passwordSalt), DateCreated = DateTime.Now }; _userRepository.Add(user); _unitOfWork.Commit(); if (roles != null || roles.Length > 0) { //遍歷用戶的所有角色 foreach (var role in roles) { //根據用戶和角色添加用戶角色中間表 addUserToRole(user, role); } } _unitOfWork.Commit(); return user; }
添加用戶角色中間表的方法addUserToRole(user, role)還沒定義?其邏輯是根據roleId獲取角色,在創建UserRole這個中間表的實例。
private void addUserToRole(User user, int roleId) { var role = _roleRepository.GetSingle(roleId); if (role == null) throw new ApplicationException("Role doesn't exist."); var userRole = new UserRole() { RoleId = role.ID, UserId = user.ID }; _userRoleRepository.Add(userRole); }
還有一個根據用戶編號獲取用戶的方法。
public User GetUser(int userId) { return _userRepository.GetSingle(userId); }
最后還有一個根據用戶名獲取用戶的方法。
public List<Role> GetUserRoles(string username) { List<Role> _result = new List<Role>(); var existingUser = _userRepository.GetSingleByUsername(username); if (existingUser != null) { foreach (var userRole in existingUser.UserRoles) { _result.Add(userRole.Role); } } return _result.Distinct().ToList(); }
待續~