写点什么

用基于模型和接口的 T4 来生成 RESTful 服务

2016 年 10 月 24 日

关键要点

  • 许多 REST 服务中都包含重复的模式;
  • 如果能自动生成这些模式相关的代码就可以节省很多时间;
  • Visual Studio 的 T4 和 EnvDTE 具有强大的生成代码功能,不需要更多工具辅助;
  • 也可以用相似技术生成对 WCF 和数据库的调用;

在 Visual Studio 中,T4 文本模板用文字和控制逻辑的混合物来生成文本文件。控制逻辑是用 Visual C#或者 Visual Basic 语言写成的代码块。在 Visual Studio 2015 Update 2 及以后版本中,也可以在 T4 模板指令中使用 C# V6.0 的新功能。生成的文件可以是任意类型的文本,包括网页、资源文件、甚至是任何编程语言的源代码。在微软公司内部 T4 应用得很广泛。他们用它来生成 MVC 视图、控制器、EntityFramework 上下文等等。

对于那些想要根据已有的模式或模型生成代码,或者写最少的重复性代码的开发者来说,都可以尝试使用 T4。我们可以用 T4 生成代码来简单地封装对业务逻辑或者任何其它服务的调用,也可以增加日志功能、实现缓存机制、基于某些模型来创建请求 / 响应类、甚至实现业务逻辑……等等。

REST 服务通常都被简单地作为业务逻辑的封装器,因此我们就可以使用 T4 来自动地为我们的接口和模型生成 REST/WCF 或者任意其它服务。这样就可以把开发者解放出来,让他们有更多的时间去专心处理用 C#和 SQL 实现的业务逻辑。

用例

假如我们准备开发一个简单的服务,来处理 GET、批量 GET、Insert 和 Update 方法。产品实体包含下面这些属性:

复制代码
public partial class Product
{
[Key]
public int Id { get; set; }
public string Name { get; set; }
public string Number { get; set; }
public int ProductGroupId { get; set; }
public decimal? ListPrice { get; set; }
public decimal? Size { get; set; }
public decimal? Weight { get; set; }
public ProductGroup ProductGroup { get; set; }
}

需求要求可以通过产品的名字、数量、价格范围等方面来查找过滤产品。当插入一条产品记录时,我们会把除了 Id 和数量之外的所有属性都写上。这些全是自动生成的,所以我们也不能更改它们。为了提高性能,用户也可以指定是否需要添加 ProductGroup 对象。如果不需要联接操作或者隔离 ProductGroup 的查询,就可以不添加。

为了帮助大家理解,我在这里画了一张图来展示我在这篇文章中要用到的架构:

批量 GET 方法

如上文所述,我们需要通过产品的名字、数量或价格范围等做过滤,这通常叫做“过滤类”或者“查询对象”,如果全用手工实现的话就太枯燥了。

但通过 T4、EnvDTE 和模型的属性,我们也可以自动创建新的过滤类。比如,我们可以设置模型的如下属性:

复制代码
public partial class Product
{
...
[Filter(FilterEnum.GreatherThanOrEqual)]
public string Name { get; set; }
[Filter(FilterEnum.Equal | FilterEnum.List)]
public string Number { get; set; }
[Filter(FilterEnum.GreatherThanOrEqual | FilterEnum.LowerThanOrEqual)]
public decimal? ListPrice { get; set; }
...
}

使用 T4 可以自动生成包含这些属性的类:

复制代码
public partial class ProductSearchObject : BaseSearchObject<ProductAdditionalSearchRequestData>
{
//some code ommited (private members and attributes)
public virtual System.String NameGTE { get; set; }
public virtual System.String Number { get; set; }
public virtual IList<String> NumberList { get {return mNumberList;} set { mNumberList = value; }}
public virtual System.Nullable<System.Decimal> ListPriceGTE { get; set; }
public virtual System.Nullable<System.Decimal> ListPriceLTE { get; set; }
}

要是使用 EntityFramework 的话,我们就可以轻松生成业务处理逻辑,就是包含基于这个查询对象和模型的 LINQ 查询。要这样做,首先要定义接口和准备使用的属性。比如:

复制代码
[DefaultServiceBehaviour(DefaultImplementationEnum.EntityFramework, "products")]
public interface IProductService : ICRUDService<Product, ProductSearchObject, ProductAdditionalSearchRequestData, ProductInsertRequest, ProductUpdateRequest>
{
}

做完这一步,T4 就知道了默认的实现应该是怎样,然后就可以生成基于查询对象的检索逻辑了:

复制代码
protected override void AddFilterFromGeneratedCode(ProductSearchObject search, ref System.Linq.IQueryable<Product> query)
{
//call to partial method
base.AddFilterFromGeneratedCode(search, ref query);
if(!string.IsNullOrWhiteSpace(search.NameGTE))
{
query = query.Where(x => x.Name.StartsWith(search.NameGTE));
}
if(!string.IsNullOrWhiteSpace(search.Number))
{
query = query.Where(x => x.Number == search.Number);
}
if(search.NumberList != null && search.NumberList.Count > 0)
{
query = query.Where(x => search.NumberList.Contains(x.Number));
}
if(search.ListPriceGTE.HasValue)
{
query = query.Where(x => x.ListPrice >= search.ListPriceGTE);
}
if(search.ListPriceLTE.HasValue)
{
query = query.Where(x => x.ListPrice <= search.ListPriceLTE);
}
}

可以把我们的默认实现注册到 IoC 框架中:

复制代码
public partial class ServicesRegistration : IServicesRegistration
{
public int Priority {get; set; }
public ServicesRegistration()
{
Priority = 0; //This is root, If you want to override this. Add new class with higher priority
}
public void Register(UnityContainer container)
{ container.RegisterType<IProductService,ProductService>(new HierarchicalLifetimeManager());
}
}

如果用这种方法构建,我们还可以非常容易地用另一个更高优先级的类来重载这种注册过程,以此来替换这种实现。

在生成 REST API 时,T4 还会根据接口中的属性信息来决定要为哪些属性生成获取函数。比如,在 IProductService 接口中我们可以为相应属性这样添加函数:

复制代码
[DefaultMethodBehaviour(BehaviourEnum.Get)]
PagedResult<TEntity> GetPage(TSearchObject search);

既然我们知道了有哪些函数可以用于获取数据,我们就可以为 REST 服务生成代码了:

复制代码
[RoutePrefix("products")]
public partial class productsController : System.Web.Http.ApiController
{
[Dependency]
public IProductService Service { get; set; }
[Route("")]
[ResponseType(typeof(PagedResult<Product>))]
[HttpGet]
public System.Web.Http.IHttpActionResult GetPage ([FromUri] ProductSearchObject search)
{
//call to partial method
var result = Service.GetPage(search);
return Ok(result);
}
}

如前文所述,我们希望客户端可以按需要请求 ProductGroup 这种附加信息,要具备这个功能,只要给 ProductGroup 属性加上 [LazyLoading] 指令就可以了。

复制代码
public partial class Product
{
//ommited code
[LazyLoading]
public ProductGroup ProductGroup { get; set; }
}

加上 [LazyLoading] 指令之后,T4 就会给新创建的类中加上 IsProductGroupLoadingEnabled 变量。

复制代码
public partial class ProductAdditionalSearchRequestData : A.Core.Model.BaseAdditionalSearchRequestData
{
public virtual bool? IsProductGroupLoadingEnabled { get; set; }
}

在底层使用 EntityFramework 会生成如下代码:

复制代码
protected override void AddInclude(ProductSearchObject search, ref System.Linq.IQueryable<Product> query)
{
if(search.AdditionalData.IsProductGroupLoadingEnabled.HasValue && search.AdditionalData.IsProductGroupLoadingEnabled == true)
{ search.AdditionalData.IncludeList.Add("ProductGroup");
}
base.AddInclude(search, ref query); //calls EF .Include method
}

Insert 方法

插入对象的属性列表常常与完整的模型不同。比如,那些自动生成的主键就不应该由客户端传入,因为它们应该被忽略掉。这个例子当然很明显,但还是有些字段会非常容易出问题。

比如 ProductGroup 属性,如果我们把它也包含到插入对象之中,那大家就会有误解,以为客户端应该用这个函数调用去创建或者更新一个 ProductGroup。所以最好是提供一个明确的插入对象,而不要重用完整模型。

为了避免用重复性地手工劳动来创建这些代码,我们仍然可以用指令来要求它为我们生成需要的属性,比如:

复制代码
[Entity]
public partial class Product
{
[Key]
public int Id { get; set; }
[Filter(FilterEnum.GreatherThanOrEqual)]
[RequestField("Insert")]
public string Name { get; set; }
[Filter(FilterEnum.Equal | FilterEnum.List)]
public string Number { get; set; }
[RequestField("Insert")]
public int ProductGroupId { get; set; }
[RequestField("Insert")]
[Filter(FilterEnum.GreatherThanOrEqual | FilterEnum.LowerThanOrEqual)]
public decimal? ListPrice { get; set; }
[RequestField("Insert")]
public decimal? Size { get; set; }
[RequestField("Insert")]
public decimal? Weight { get; set; }
[LazyLoading]
public ProductGroup ProductGroup { get; set; }
}

上面的信息可以生成下面的代码,即 ProductInsertRequest 类:

复制代码
public partial class ProductInsertRequest
{
public System.String Name { get; set; }
public System.Int32 ProductGroupId { get; set; }
public System.Nullable<System.Decimal> ListPrice { get; set; }
public System.Nullable<System.Decimal> Size { get; set; }
public System.Nullable<System.Decimal> Weight { get; set; }
}

和以前一样,我们要修改一下接口,这样 T4 就知道哪些函数是负责处理插入请求的。我们可以为合适的函数加上属性,比如:

复制代码
[DefaultMethodBehaviour(BehaviourEnum.Insert)]
TEntity Insert(TInsert request, bool saveChanges = true);

有了这些模型和接口的信息,T4 就可以生成我们想要的 REST API 代码了:

复制代码
[Route("")]
[ResponseType(typeof(Product))]
[HttpPost]
public HttpResponseMessage Insert([FromBody] ProductInsertRequest request)
{
var result = Service.Insert(request);
var response = Request.CreateResponse<Product>(HttpStatusCode.Created, result);
return response;
}

Update 方法

原理也和插入函数一样。在这里我们要为元组的属性加上 [RequestField(“Update”)] 指令,这样就可以为 ProductUpdateRequest 生成合适的属性。然后再为相应的接口加上指令来让 T4 知道哪个函数是要处理 Update 的。

加上这些指令后,T4 就可以为 REST 服务生成更新数据的函数了:

复制代码
[Route("{id}")]
[ResponseType(typeof(A.Core.Model.Product))]
[HttpPut]
public HttpResponseMessage Update([FromUri] Int32 id, [FromBody]ProductUpdateRequest request)
{
//can return "Not Found" if Update throws NotFoundException
var result = Service.Update(id,request);
var response = Request.CreateResponse<Product>(HttpStatusCode.OK, result);
return response;
}

结论

从文中可以看出,我们可以用 T4 来生成代码,帮助我们节省很多写重复性代码的时间。生成的代码易读性也很好,和自己写的一样。用相同的办法,我们也可以生成代码来在服务级别缓存结果和增加日志功能。

这种技术的另一种用途是同时生成 REST 和 WCF 服务代码,当你的客户端既要支持浏览器也要支持 C#时这就很有用。

在我的工作经历中,我曾经用 T4 和 EnvDTE 来为公司的项目生成完整的 CRUD REST 服务代码,包括数据库调用和单元测试等。几分钟就搞定了,而不是几小时。

如果你想了解更多内容,可以通过 GitHub ,或者直接通过 LinkedIn 联系我。

关于作者

Amel Musić的职业生涯早年间是为证券公司开发解决方案,主要是银行和政府部门,他的专长是基于 MS SQL Server 和.NET 平台来开发和优化解决方案。在这些系统上做了几年设计之后,他开始学习使用 T4 和面向切面编程,这让他得以减少重复性工作,专心处理客户的业务需求。

阅读英文原文 Creating RESTful Services with T4 Based on Model and Interfaces

2016 年 10 月 24 日 18:092138
用户头像

发布了 152 篇内容, 共 59.5 次阅读, 收获喜欢 53 次。

关注

评论

发布
暂无评论
发现更多内容

Kafka系列第7篇:你必须要知道集群内部工作原理的一些事!

z小赵

大数据 kafka 实时计算

给苹果提醒APP配个助手

BabyKing

提醒助手 TODO 奇妙清单 Reminders Helper

识别代码中的坏味道(三)

Page

敏捷开发 面向对象 重构 代码质量 代码坏味道

重新强调完成的定义

Bob Jiang

Scrum 完成的定义 DoD definition of done

MySQL事务解析

一个有志气的DB

MySQL 事务隔离级别 mysql事务

从零开始制作一台计算机-概述

小兵

计算机基础

DDD 实践手册(番外篇: 事件风暴-概念)

Joshua

领域驱动设计 DDD 事件风暴 事件驱动 Event Storming

东哥和刘亦菲的故事

张利东

R

ZooKeeper,到底如何选主?

奈学教育

程序员的晚餐 | 5 月 18 日 瓠子,年少时的味道

清远

美食

Web3极客日报 #128

谢锐 | Frozen

区块链 开源 技术社区 Rebase Web3 Daily

游戏夜读 | Two Sum问题的八个解

game1night

npm下载electron缓慢的问题

玏佾

npm Electron

NIO看破也说破(四)—— Java的NIO

小眼睛聊技术

Java 学习 开源 架构 后端

产品周刊 | 第 15 期(20200517)

Herbert

产品 设计 产品经理 产品设计

Live2D for Unity入门篇 4.x

波波

编程 游戏开发 Live2D Unity

半小时手工解决的活,让我意外学会了 python 的 pdfkit 库

小匚

Python python教程

Web3极客日报#127

谢锐 | Frozen

区块链 开源 技术社区 Rebase Web3 Daily

Deno 入门手册:附大量 TypeScript 代码实例

寇云

node.js typescript

谈谈控制感(7):底线思维与控制感

史方远

职场 心理 成长

Redis缓存三大问题

Bruce Duan

redis 缓存穿透 缓存击穿 缓存雪崩

JAVA主流锁

颇风

Java 多线程

换脸新潮流:BIGO风靡全球的人脸风格迁移技术

DT极客

回“疫”录(20):世界从来不会欺负听话的人

小天同学

疫情 回忆录 现实纪录 纪实

Kotlin 协程实践(2)之 异步和Callback地狱

陈吉米

Java kotlin 协程

Vue+SpringBoot+SpreadJS 实现的在线文档

Geek_Willie

Spring Boot Vue SpreadJS

Spring Security 中的授权操作原来这么简单

江南一点雨

Java spring Spring Boot spring security

单核小鸡上的Minikube实践(一)

摩登土狗

Docker Linux DevOps k8s minikube

中小企业如何做运维自动化?

Spug运维

运维 spug 运维自动化 jenkins ansible

如何更自信的写作

七镜花园-董一凡

写作

设计模式前传——为什么要学设计模式

海星

Java 面试 设计模式

「中国技术开放日·长沙站」现场直播

「中国技术开放日·长沙站」现场直播

用基于模型和接口的T4来生成RESTful服务-InfoQ