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

关键要点

许多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服务代码,包括数据库调用和单元测试等。几分钟就搞定了,而不是几小时。

时间: 2024-10-11 13:36:01

net中用基于模型和接口的T4来生成RESTful服务的相关文章

基于模型的测试用例设计(1)

介绍 测试设计是测试过程中最重要的部分之一.一个好的测试用例不仅要为被测系统( SUT )提供一些输入,还要验证系统是否如预期进行.也就是说,它有助于确认利益相关者要求得以实现.但测试设计可以做的远不止这些.理想情况下,测试设计有助于沟通两方对这些需求的理解,验证他们能被正确实施,并引发对利益相关者可能增加的更大价值的讨论. 基于模型的测试(MBT)(下文都简称为:基模测试)是一种技术,有时被标榜为"自动化测试设计".虽然一定程度上这并没有错,但它或许会给人以错误的印象.基模测试工具从

基于模型的测试和Spec Explorer简介

要生成高质量的软件,需要在测试阶段进行大量的工作,这可能是软件开发过 程中成本最高.工作量最大的部分. 从最简单的功能黑盒测试到重量级的方法, 包括定理证明程序以及形式化需求说明,有很多方法可以提高测试可靠性和效率 . 但是,测试并不总是能达到必要的细致程度,经常缺乏规范和方法体系. 十多年来,Microsoft 在其内部开发流程中成功应用了基于模型的测试 (MBT) . 事实证明,对于各种内部和外部软件产品而言,MBT 是非常成功的方法. 这 些年来,这种方法采用得越来越多. 相对来说,它在测

使用基于模型的测试工作流进行与安全相关的软件开发

安全相关软件的挑战 嵌入式软件已经逐渐成为当今创新型产品的核心.对于在我们日常生活 中必不可少的产品来说,嵌入式软件是定义其功能,控制其电气和机械系统的重要组件.例如,在飞机 .汽车.火车或医疗设备中,故障可能会导致人身伤亡.此时必须倍加谨慎,也需要付出额外的努力, 确保系统安全运作,保证用户的安全,避免代价高昂的产品召回. 对于极度注重安全的代码, 企业必须遵循严格的开发标准和指导准则,例如针对商业航空电子设备的 DO-178C 和 DO-178B:针对 汽车的 ISO 26262:针对医疗设

中国人工智能学会通讯——基于脑机接口的机械臂遥操作控制

摘要: 脑机接口作为一种新型的人机交互方式,在心理认知.智能控制.康复训练等方面具有很大的应用潜力.基于稳态视觉诱发电位(SSVEP)的脑机接口(BCI)系统具有较高的信息传输速率,而且校正时间较短,一直以来都是脑机接口研究中的热点.本文使用基于多导同步指数(MSI)识别算法进行SSVEP信号的识别分类:结合SSVEP-BCI系统,实现了基于脑机接口的机械臂遥操作系统,为脑机接口的实用化提供了一种可应用范例.脑电识别出来的结果将产生机械臂运动的期望轨迹:在系统的视觉反馈交互设计方面,实现了监控图

Matlab与.NET基于类型安全的接口混合编程入门

原文:[原创]Matlab与.NET基于类型安全的接口混合编程入门 如果这些文章对你有用,有帮助,期待更多开源组件介绍,请不要吝啬手中的鼠标.  [原创分享]Matlab.NET混编调用Figure窗体 http://www.cnblogs.com/asxinyu/archive/2013/04/14/3020813.html   [原创]开源.NET下的XML数据库介绍及入门  http://www.cnblogs.com/asxinyu/archive/2013/03/25/2980086.

【原创】Matlab与.NET基于类型安全的接口混合编程入门

              本博客所有文章分类的总目录:http://www.cnblogs.com/asxinyu/p/4288836.html    Matlab和C#混合编程文章目录 :http://www.cnblogs.com/asxinyu/p/4329753.html 如果这些文章对你有用,有帮助,期待更多开源组件介绍,请不要吝啬手中的鼠标.  [原创分享]Matlab.NET混编调用Figure窗体 http://www.cnblogs.com/asxinyu/archive/2

《基于模型的软件开发》——导读

前言 软件开发是一项极其复杂的智力活动,它是一门朝气蓬勃并且仍在迅速发展的学科.软件开发还不够完善,因此迄今人们仍然在试图找出开发软件的好方法. 尽管如此,多年来软件开发方法仍然获得了大幅提升.许多设计方法学不断发展以促进软件设计的各个方面.其中之一是结构化设计方法,该方法提供了一种非常直观的方式,用以很好地匹配图灵和冯·诺依曼的硬件计算模型. 尽管结构化设计明显优于它之前的特定方法,但它存在着一个致命的弱点:当用户需求随着时间的推移改变时,软件往往很难随之修改,大型的应用尤其如此.与此同时,应

基于模型的测试工具: Spec Explorer

一.什么是基于模型的测试 Wiki的描述如下:基于模型的测试属于软件测试领域的一种测试方法.按照此方法,测试用例可以完全或部分的利用模型自动产生.以上所说的模型通常是指对被测系统(SUT,system under test)某些(通常是功能性的)方面的描述. 模型一般都是对被测系统(SUT,system under test)预期行为动作的抽象描述. 这些测试用例的集合就是我们平时所称的抽象测试套件(abstract test suite). 抽象测试套件不可以直接执行于需测试的系统,因为,他们

《基于模型的软件开发》——第1部分 面向对象开发的根本

第1部分 面向对象开发的根本 基于模型的软件开发方法本质上是一种面向对象的方法.因此,为了充分了解这种方法,有必要大致理解面向对象的开发.由于面向对象的方法不如传统软件开发方法那样直观,因此我们需要理解面向对象方法的工作方式.本书这一部分着眼于面向对象方法诞生的历史背景,使我们能够了解传统方法存在的问题,也即面向对象的方法寻求解决的问题.