Restful WebApi项目开发实践
前言
踩过了一段时间的坑,现总结一下,与大家分享,愿与大家一起讨论。
Restful WebApi特点
WebApi相较于Asp.Net MVC/WebForm开发的特点就是前后端完全分离,后端使用WebApi直接针对资源进行暴露,大部分的业务转移到前端进行。前端可以采用Html页面或各平台的原生程序开发,非常灵活。
我们采用的是WebApi+angularjs/WPF的方式开发。
设计思想
目前就算使用Asp.net MVC开发,为了用户体验也需要使用Ajax来异步加载数据,而Html5的单页App也越来越流行,所以干脆让后端只提供数据的存储,Api除极个别情况只针对实体提供实体的增删改查功能,后台尽量摘除业务逻辑,把业务逻辑移到前端实现。使后端专注于数据仓储和数据查询的性能优化,而前端更专注于业务逻辑、UI等方面的优化。
服务端
根据数据模型创建ApiController直接暴露实体,处理增删改查,配合Odata扩展使用非常方便。
这块看上去简单,其实是很重要的一个地方。由于直接对资源/实体进行暴露,通讯采用的又是HTTP协议,前端是无法保证Api访问安全的,而且业务逻辑也移到了前端,所以后端Api的安全性、权限拦截的粒度和灵活性尤为重要。一般进行权限拦截都会针对功能特性进行判断,比如:XX用户能否使用A功能,但是Restful WebApi提供的Api是直接针对资源/实体的,业务逻辑又移到了客户端去实现,后端在业务上功能性的描述弱化了,变成了:能否增/删/改/查A资源,而这种转变就要求权限需要拦截到数据行级别。
服务端插件系统,Host到Asp.net MVC WebApi项目上使用
服务端我是在HappyFramework.OSGi基础上进行的改造:
(注:插件系统没有完整的重构过,所以有部分设计会有些不合理)
- 精简掉主体中不用的的部分,比如ioc、企业库。
- 把主体改造成实现+契约两个类库。
- 添加自写的权限模块、自写服务定位器实现的服务总线、观察者模式的事件总线,全部使用反射进行查找组装。
- 根据服务总线的需要添加预启动插件状态。
- 添加WebApi集成,实现CORS,替换系统WebApi的服务:IAssembliesResolver、IHttpControllerTypeResolver、IHttpControllerSelector实现插件的控制器加载、命名空间隔离。
服务端的主要任务就是开放资源访问和开放一些必须要后端来实现的功能性Api。
模型设计
既然把大部分业务逻辑都移到了前端,那么后端模型设计上就不用设计的太过详细,除了必须的一些字段,比如Id,Time这种会涉及到查询搜索、抢占更新(文章访问量)之类的,我设计了ExtType和ExtData两个String型字段,前端可以自定义数据模型(ExtType),然后把对应模型数据放到ExtData字段中,尽可能提高前端的灵活性和后端数据模型稳定性。
权限设计
权限设计-后端:
先来看一个例子,这个例子对应的Url为:
GET api/XXX/WebApiExt/ACL/Activity/{TenantId}/{AggregationId}/{SiteId}
GET api/XXX/WebApiExt/ACL/Activity/{TenantId}/{AggregationId}/{SiteId}/{id}
POST api/XXX/WebApiExt/ACL/Activity/{TenantId}/{AggregationId}/{SiteId}
PUT api/XXX/WebApiExt/ACL/Activity/{TenantId}/{AggregationId}/{SiteId}/{id}
DELETE api/XXX/WebApiExt/ACL/Activity/{TenantId}/{AggregationId}/{SiteId}/{id}
public class ActivityController : BaseController<Domain.Activity, ActivityModel, Guid>
{
protected override IEnumerable<Domain.Activity> GetAvailableData(Guid TenantId, Guid AggregationId, Guid SiteId)
{
InitVisibleSiteIds(TenantId, AggregationId, SiteId);
return db.AsNoTracking().Where(s => VisibleSiteIds.Contains(s.SiteId));
}
[UppBundleEngine.Web.Permission.WebApi.PermissionAuthorize(
DisplayName = "获取活动",
AuthType = AuthorizeType.ACL | AuthorizeType.PermissionCode,
AuthorizeInfomation = "GetActivities",
Description = ""
)]
[Queryable(AllowedQueryOptions = AllowedQueryOptions.OrderBy | AllowedQueryOptions.Skip | AllowedQueryOptions.Top, MaxTop = 50)]
public override IQueryable GetAll(Guid TenantId, Guid AggregationId, Guid SiteId)
{
var data = GetAvailableData(TenantId, AggregationId, SiteId);
return data.AsEnumerable().Select(model => AutoMapToModel(model, new[]
{
"ExtType",
"ExtData",
})).AsQueryable();
}
[UppBundleEngine.Web.Permission.WebApi.PermissionAuthorize(
DisplayName = "获取活动",
AuthType = AuthorizeType.ACL | AuthorizeType.PermissionCode,
AuthorizeInfomation = "GetActivity"
)]
public override IHttpActionResult GetOne(Guid TenantId, Guid AggregationId, Guid SiteId, Guid id)
{
var model = GetAvailableData(TenantId, AggregationId, SiteId).SingleOrDefault(s => s.Id == id);
if (model == null) return NotFound();
return Ok(AutoMapToModel(model));
}
[UppBundleEngine.Web.Permission.WebApi.PermissionAuthorize(
DisplayName = "添加活动",
AuthType = AuthorizeType.ACL | AuthorizeType.PermissionCode,
AuthorizeInfomation = "PostActivity"
)]
public override IHttpActionResult Post(Guid TenantId, Guid AggregationId, Guid SiteId, ActivityModel model)
{
if (!ModelState.IsValid) return BadRequest(ModelState);
if (model.SiteId != SiteId) return BadRequest();
model.Id = Guid.NewGuid();
db.Add(AutoMapToEntity(model));
dbContext.SaveChanges();
return Ok(model);
}
[UppBundleEngine.Web.Permission.WebApi.PermissionAuthorize(
DisplayName = "修改活动",
AuthType = AuthorizeType.ACL | AuthorizeType.PermissionCode,
AuthorizeInfomation = "PutActivity"
)]
public override IHttpActionResult Put(Guid TenantId, Guid AggregationId, Guid SiteId, Guid id, ActivityModel model)
{
if (!ModelState.IsValid) return BadRequest(ModelState);
if (id != model.Id || SiteId != model.SiteId) return BadRequest();
var oldmodel = GetAvailableData(TenantId, AggregationId, SiteId).SingleOrDefault(s => s.Id == id);
if (oldmodel == null) return NotFound();
dbContext.Entry(AutoMapToEntity(model)).State = EntityState.Modified;
dbContext.SaveChanges();
return StatusCode(System.Net.HttpStatusCode.NoContent);
}
[UppBundleEngine.Web.Permission.WebApi.PermissionAuthorize(
DisplayName = "删除活动",
AuthType = AuthorizeType.ACL | AuthorizeType.PermissionCode,
AuthorizeInfomation = "DeleteActivity"
)]
public override IHttpActionResult Delete(Guid TenantId, Guid AggregationId, Guid SiteId, Guid id)
{
var model = GetAvailableData(TenantId, AggregationId, SiteId).SingleOrDefault(s => s.Id == id);
if (model == null) return NotFound();
dbContext.Entry(model).State = EntityState.Deleted;
dbContext.SaveChanges();
return Ok(AutoMapToModel(model));
}
protected override bool ModelExists(Guid TenantId, Guid AggregationId, Guid SiteId, Guid id)
{
return db.Any(s => s.Id == id);
}
}
其中AuthType可以为:ACL/PermissionCode/NoNeed,需要仅登录可以再加上系统的[Authorize]。可以看到这个Controller里基本都是通用代码,所以实际上可以直接复制粘贴快速的创建资源Api,至于那个自定义的抽象类BaseController实现的功能:
- 从数据上下文中把对应的数据提取出来。
- 使用EmitMapper提供了数据模型和传输模型间的映射。
- InitVisibleSiteIds实现了调用租户模块提供的服务,查找TenantId对应的SiteIds。
- 调用Member模块,读取当前登录用户,用户信息。
- 提供Get、GetOne、Post、Put、Delete模板方法,如果需要可以深度集成Odata for WebApi,就可以使用Patch方法。
权限部分实现了RBAC和ACL两种权限方式,用RBAC来管理“谁能怎么操作哪些资源”这种权限,用ACL来管理“谁能怎么操作哪些数据”这种权限。权限模块可以同时应用于MVC和WebApi。实现的方式是自定义AuthorizeAttribute,来实现拦截,可以很容易拿到RBAC所需要的数据,而ACL就麻烦些了,总不能定死url吧,所以根据Sharepoint的启发设计了这种路由:
api/XXX/WebApiExt/ACL/Activity/{TenantId}/{AggregationId}/{SiteId}/{Id}?xxx
- “XXX/WebApiExt”是插件的命名空间。
- “ACL”代表这些Api需要采用ACL方式进行控制,写什么都可以没有强制定义,一般开放给资源管理者的Api都用ACL(后台),如果不用ACL的,比如开放给资源读取、动词类的Api,我们一般写成“Common”(前台),以示区分。
- 我设计的ACL的判断方式为:比对路由和实际访问路径的差异化部分再加上Http Method作为特征值和数据库存储的用户可访问列表进行比对,支持通配符。所以“{TenantId}/{AggregationId}/{SiteId}”是ACL实现的基础,就是租户模块。
资源A的实体字段里存储SiteId,而租户模块中存储着TenantId和SiteId的对应关系、AggregationId和SiteId的对应关系,AggregationId作为聚合租户内不同子站点的一种方式,甚至可以根据需要聚合不同租户下的数据,为系统提供了足够的灵活性。
权限模块附带的一个功能就是可以在写Api的时候直接把文档写上去,集成后的ASP.NET Web API Help Page页就变成了:
权限设计-前端
后端的权限设计的描述方法是不适合于前端的,所以前端就需要维护相应的对应关系,将前端业务上的的Feature和后端Api的RBAC的权限进行对应,后端的ACL在对用户分组时处理即可。
客户端
客户端主要实现业务逻辑,后端直接暴露资源,所以可以看作是直连数据库操作,并且不用太过考虑安全性问题,数据校验更多的是从交互体验角度去考虑。Web的话我们使用的是AnglarJs做SPA开发,PC应用使用WPF开发。在这种模式的开发下客户端的工作就稍微有些复杂,对于一些模型的ExtType和ExtData都要求有比较好的处理机制,不过因为是客户端所以对处理性能要求就不是很高了。
相关传送门:
Restful WebApi项目开发实践的更多相关文章
- C#项目开发实践前言
曾经没有做过项目开发实现解说,都是在工作过程其中,自动学习,查找资料,由于在曾经的公司就我一人在做c#WinForm开发,所以,有时候在公司培训会上,我也会为新的员工进行过一些简单的项目解说,基于在培 ...
- WebAPI接口开发实践
背景 在团队两年多陆续负责了几个项目的开发上线已经代码的review,特别是对老项目的重构过程中,发现之前的API设计是没有任何规范和约定的,不同的开发同学有不同的习惯,因此需要一套规范去约定,现在分 ...
- Restful WebApi开发实践
随笔分类 - Restful WebApi开发实践 C#对WebApi数据操作 摘要: ## 目标简化并统一程序获取WebApi对应实体数据的过程,方便对实体进行扩充.原理就是数据服务使用反射发现 ...
- 个人项目开发PSP实践-MyWCprj
MyWCprj.exe Github仓库地址 1. What is MyWCprj.exe? wc是linux下一个非常好用的代码统计小工具,可以通过 -c .-w .-l等选项分别进行对指定文件的代 ...
- ASP.NET Core WebAPI 开发-新建WebAPI项目 转
转 http://www.cnblogs.com/linezero/p/5497472.html ASP.NET Core WebAPI 开发-新建WebAPI项目 ASP.NET Core We ...
- MVC5 网站开发实践 1、建立项目
目录 MVC5 网站开发实践 概述 一.建立项目 1.建立团队项目 在办公室和家里使用不同的电脑,为了方便代码的共享将项目建立为团队项目. 如图打开vs2013→新建→团队项目(图1),会自动 ...
- ASP.NET MVC5 网站开发实践(一) - 项目框架
前几天算是开题了,关于怎么做自己想了很多,但毕竟没做过项目既不知道这些想法有无必要,也不知道能不能实现,不过邓爷爷说过"摸着石头过河"吧.这段时间看了一些博主的文章收获很大,特别是 ...
- ASP.NET Core WebAPI 开发-新建WebAPI项目
ASP.NET Core WebAPI 开发-新建WebAPI项目, ASP.NET Core 1.0 RC2 即将发布,我们现在来学习一下 ASP.NET Core WebAPI开发. 网上已经有泄 ...
- ASP.NET MVC5 网站开发实践(一) - 项目框架(转)
前几天算是开题了,关于怎么做自己想了很多,但毕竟没做过项目既不知道这些想法有无必要,也不知道能不能实现,不过邓爷爷说过“摸着石头过河”吧.这段时间看了一些博主的文章收获很大,特别是@kencery,依 ...
随机推荐
- nodejs进阶(6)—连接MySQL数据库
1. 建库连库 连接MySQL数据库需要安装支持 npm install mysql 我们需要提前安装按mysql sever端 建一个数据库mydb1 mysql> CREATE DATABA ...
- ASP.NET Aries 入门开发教程9:业务表单的开发
前言: 经过前面那么多篇的列表的介绍,终于到了大伙期待的表单开发了. 也是本系列的最后一篇文章了! 1:表单页面的权限设置与继承 对于表单页面,权限的设置有两种: 1:你可以选择添加菜单(设置为不显示 ...
- IE6/7下空div占用空间的问题
最近注意力没在前端上面,工作碰到这样一个问题,下意识的写了句 font-size:0;line-height:0;哪知道引发了更大的bug.后来插入数据进去的时候都不显示了..再后来百度一番找到,原来 ...
- JAVA回调机制(CallBack)详解
序言 最近学习java,接触到了回调机制(CallBack).初识时感觉比较混乱,而且在网上搜索到的相关的讲解,要么一言带过,要么说的比较单纯的像是给CallBack做了一个定义.当然了,我在理解了回 ...
- C# 用SoapUI调试WCF服务接口(WCF中包含用户名密码的验证)
问题描述: 一般调试wcf程序可以直接建一个单元测试,直接调接口. 但是,这次,我还要测试在接口内的代码中看接收到的用户名密码是否正确,所以,单一的直接调用接口方法行不通, 然后就想办法通过soapU ...
- 值得注意的ibatis动态sql语法格式
一.Ibatis常用动态sql语法,简单粗暴用一例子 <select id="iBatisSelectList" parameterClass="java.util ...
- JS鼠标事件大全 推荐收藏
一般事件 事件 浏览器支持 描述 onClick HTML: 2 | 3 | 3.2 | 4 Browser: IE3 | N2 | O3 鼠标点击事件,多用在某个对象控制的范围内的鼠标点击 onDb ...
- [转载]敏捷开发之Scrum扫盲篇
现在敏捷开发是越来越火了,人人都在谈敏捷,人人都在学习Scrum和XP... 为了不落后他人,于是我也开始学习Scrum,今天主要是对我最近阅读的相关资料,根据自己的理解,用自己的话来讲述S ...
- 数据库 DML、DDL、DCL区别 .
总体解释: DML(data manipulation language): 它们是SELECT.UPDATE.INSERT.DELETE,就象它的名字一样,这4条命令是用来对数据库里的数据进行操作的 ...
- Hibernatel框架基础使用
Hibernatel框架基础使用 1.简介 1.1.Hibernate框架由来 Struts:基于MVC模式的应用层框架技术 Hibernate:基于持久层的框架(数据访问层使用)! Spring:创 ...