对一个前端使用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();
Property(u => u.Email).IsRequired().HasMaxLength();
Property(u => u.HashedPassword).IsRequired().HasMaxLength();
Property(u => u.Salt).IsRequired().HasMaxLength();
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 > )
{
//遍历用户的所有角色
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();
}
待续~
对一个前端使用AngularJS后端使用ASP.NET Web API项目的理解(1)的更多相关文章
- 对一个前端使用AngularJS后端使用ASP.NET Web API项目的理解(4)
chsakell分享了一个前端使用AngularJS,后端使用ASP.NET Web API的项目. 源码: https://github.com/chsakell/spa-webapi-angula ...
- 对一个前端使用AngularJS后端使用ASP.NET Web API项目的理解(3)
chsakell分享了一个前端使用AngularJS,后端使用ASP.NET Web API的项目. 源码: https://github.com/chsakell/spa-webapi-angula ...
- 对一个前端使用AngularJS后端使用ASP.NET Web API项目的理解(2)
chsakell分享了一个前端使用AngularJS,后端使用ASP.NET Web API的项目. 源码: https://github.com/chsakell/spa-webapi-angula ...
- 购物车Demo,前端使用AngularJS,后端使用ASP.NET Web API(3)--Idetity,OWIN前后端验证
原文:购物车Demo,前端使用AngularJS,后端使用ASP.NET Web API(3)--Idetity,OWIN前后端验证 chsakell分享了前端使用AngularJS,后端使用ASP. ...
- 购物车Demo,前端使用AngularJS,后端使用ASP.NET Web API(2)--前端,以及前后端Session
原文:购物车Demo,前端使用AngularJS,后端使用ASP.NET Web API(2)--前端,以及前后端Session chsakell分享了前端使用AngularJS,后端使用ASP.NE ...
- 购物车Demo,前端使用AngularJS,后端使用ASP.NET Web API(1)--后端
原文:购物车Demo,前端使用AngularJS,后端使用ASP.NET Web API(1)--后端 chsakell分享了前端使用AngularJS,后端使用ASP.NET Web API的购物车 ...
- 对一个前端AngularJS,后端OData,ASP.NET Web API案例的理解
依然chsakell,他写了一篇前端AngularJS,后端OData,ASP.NET Web API的Demo,关于OData在ASP.NET Web API中的正删改查没有什么特别之处,但在前端调 ...
- 在ASP.NET Web API项目中使用Hangfire实现后台任务处理
当前项目中有这样一个需求:由前端用户的一个操作,需要触发到不同设备的消息推送.由于推送这个具体功能,我们采用了第三方的服务.而这个服务调用有时候可能会有延时,为此,我们希望将消息推送与用户前端操作实现 ...
- 如何创建一个Asp .Net Web Api项目
1.点击文件=>新建=>项目 2.创建一个Asp .NET Web项目 3.选择Empty,然后选中下面的MVC和Web Api,也可以直接选择Web Api选项,注意将身份验证设置为无身 ...
随机推荐
- 【Python】Flask系列-数据库笔记
MySQL-python中间件的介绍与安装: 1.如果是在类unix系统上,直接进入虚拟环境,输入sudo pip install mysql-python. 2.如果是在windows系统上,那么在 ...
- elasticsearch分别在windows和linux系统安装
WINDOWS系统安装1.安装JDKElastic Search要求使用较高版本JDK,本文使用D:\DevTools\jdk1.8.0_131,并配置环境变量 2.安装Elastic Search官 ...
- 004_加速国内docker源下载速度
docker下载慢的不行.国内加速器地址 http://355dbe53.m.daocloud.iohttps://docker.mirrors.ustc.edu.cn https://hub-mir ...
- Ibatis.Net 数据库操作学习(四)
一.查询select 还记得第一篇示例中是如何读出数据库里3条数据的吗?就是调用了一个QueryForList方法,从方法名就知道,查询返回列表. 1.QueryForList 返回List< ...
- LeetCode(10):正则表达式匹配
Hard! 题目描述: 给定一个字符串 (s) 和一个字符模式 (p).实现支持 '.' 和 '*' 的正则表达式匹配. '.' 匹配任意单个字符. '*' 匹配零个或多个前面的元素. 匹配应该覆盖整 ...
- CF1063A 【Oh Those Palindromes】
考虑在一个部分串中加入字符使得最终构造的串回文子串最多的方案 考虑简单情况,对于只含一种元素的串,我们要插入其他元素 记原有元素为$a$,新加元素为$b$ 考虑$b$的最优插入位置 原串$aaaa.. ...
- .NetCore关于Cap(RabbitMQ)结合MySql使用出现MySql相关类冲突问题解决办法
问题还原 引用了 DotNetCore.CAP.MySql MySql.Data.EntityFrameworkCore 在使用MySql相关对象的时候会出现如下冲突,在命名空间加入伪空间名称是不能解 ...
- ReentrantLock和AQS
AQS(AbstractQueuedSynchronizer)是JDK1.5提供的一个用来构建锁和同步工具的框架,子类包括常用的ReentrantLock.CountDownLatch.Semapho ...
- 针对LDAP安装web接口,进行管理
1. 通过SSH连接LDAP服务器 2. 安装phpLDAPadmin运行以下命令. $ sudo apt-get install phpldapadmin 3. 配置phpLDAPadmin. $ ...
- 简单实现一个EventEmiter
在前端开发中,“发布-订阅”也是“观察者模式”是一种常用的设计模式:之前对设计模式没有过深的认识,直到前段时间在封装一个运用AngularJS封装table组件时,遇到一个难题,那就是AngularJ ...