对一个前端使用AngularJS后端使用ASP.NET Web API项目的理解(1)
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(); }