Key points
Many rest services contain duplicate patterns;
If you can automatically generate these patterns related to the code can save a lot of time;
Visual Studio T4 and EnvDTE have powerful build code capabilities that do not require additional tool support;
You can also generate calls to WCF and the database using similar techniques.
In Visual Studio, T4 text templates generate text files using a mixture of literal and control logic. Control logic is a block of code written in Visual C # or the Visual Basic language. The new features of C # V6.0 can also be used in T4 template directives in Visual Studio 2015 Update 2 and beyond. The generated file can be any type of text, including Web pages, resource files, or even the source code of any programming language. T4 is widely used in Microsoft's internal business. They use it to generate MVC views, controllers, entityframework contexts, and so on.
For developers who want to generate code from existing patterns or models, or write minimal repetitive code, you can try using T4. We can use T4 to generate code to simply encapsulate calls to business logic or any other service, to increase logging capabilities, implement caching mechanisms, create request/Response classes based on certain models, and even implement business logic ... Wait a minute.
Rest services are often simply encapsulated as business logic, so we can use T4 to automatically generate REST/WCF or any other service for our interfaces and models. This frees developers and gives them more time to concentrate on business logic implemented with C # and SQL.
Case
If we are going to develop a simple service to handle get, batch getting, insert, and update methods. The product entity contains the following properties:
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;}
}
Requirements can be found through the product name, quantity, price range and other aspects to find filtration products. When you insert a product record, we write all the attributes except the ID and number. These are all generated automatically, so we can't change them either. To improve performance, users can also specify whether to add ProductGroup objects. If you do not need a join operation or isolate a productgroup query, you may not add it.
To help you understand, I've drawn a picture here to illustrate the architecture I'm going to use in this article:
Batch Get method
As mentioned above, we need to filter through the product name, quantity or price range, which is often called "filter class" or "Query Object", if it is all done by hand is too boring.
But with the T4, EnvDTE, and model properties, we can also automatically create new filtering classes. For example, we can set the following properties for the model:
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;}
...
}
Use T4 to automatically generate classes that contain these properties:
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 = Valu E }
Public virtual system.nullable<system.decimal> Listpricegte {get; set;}
Public virtual system.nullable<system.decimal> Listpricelte {get; set;}
}
If you use EntityFramework, we can easily generate business processing logic that contains LINQ queries based on this query object and model. To do this, you first define the interfaces and the properties that you want to use. For example,
[Defaultservicebehaviour (Defaultimplementationenum.entityframework, "products")]
Public interface iproductservice:icrudservice<product, Productsearchobject, Productadditionalsearchrequestdata, Productinsertrequest, productupdaterequest>
{
}
After this step, T4 knows what the default implementation should be, and then you can generate retrieval logic based on the query object:
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);
}
}
We can register our default implementation in the IOC framework:
Public partial class Servicesregistration:iservicesregistration
{
& nbsp; public int Priority {get; set;}
Public servicesregistration ()
{
Priority = 0;//this is root, If You are want to override this. ADD new class with higher priority
}
public void Register (UnityContainer container)
{ container. Registertype<iproductservice,productservice> (New Hierarchicallifetimemanager ());
}
}
If built in this way, we can also replace this implementation by overloading this registration process with another, higher-priority class.
When you generate the rest API, T4 also determines which properties are generated for the FETCH function based on the property information in the interface. For example, in the Iproductservice interface we can add functions to the corresponding attributes:
[Defaultmethodbehaviour (Behaviourenum.get)]
Pagedresult<tentity> getpage (Tsearchobject search);
Now that we know what functions are available to get the data, we can generate code for the rest service:
[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);
}
}
As mentioned earlier, we want the client to request ProductGroup This additional information as needed, and to have this functionality, just add [lazyloading] instructions to the ProductGroup attribute.
public partial class Product
{
ommited Code
[Lazyloading]
Public ProductGroup ProductGroup {get; set;}
}
After adding the [lazyloading] directive, T4 adds isproductgrouploadingenabled variables to the newly created class.
public partial class Productadditionalsearchrequestdata:a.core.model.baseadditionalsearchrequestdata
{
Public virtual bool? isproductgrouploadingenabled {get; set;}
}
Using EntityFramework at the bottom generates the following code:
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 method
The list of properties for inserting objects is often different from the complete model. For example, those auto-generated primary keys should not be passed on by the client because they should be ignored. This example is obvious, but some of the fields can be very problematic.
such as the ProductGroup attribute, if we include it in the Insert object, there is a misconception that the client should use this function call to create or update a productgroup. So it's best to provide a clear insert object instead of reusing the complete model.
To avoid creating this code with repetitive manual labor, we can still use instructions to require it to generate the required attributes for us, such as:
[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;}
}
The information above can generate the following code, the Productinsertrequest class:
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;}
}
As before, we have to modify the interface so that T4 knows which functions are responsible for handling the insert request. We can add attributes to the appropriate function, such as:
[Defaultmethodbehaviour (Behaviourenum.insert)]
Tentity Insert (Tinsert request, bool saveChanges = TRUE);
With the information on these models and interfaces, T4 can generate the rest API code we want:
[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 method
The principle is the same as the Insert function. Here we will add the [Requestfield ("Update")] directive to the attributes of the tuple so that the appropriate properties can be generated for the productupdaterequest. Then add instructions to the corresponding interface to let T4 know which function is to process the update.
With these instructions, T4 can generate a function to update the data for the rest service:
[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;
}
Conclusion
As can be seen from the article, we can use T4 to generate code to help us save a lot of time to write repetitive code. The generated code is also good for readability, as it is written by yourself. In the same way, we can also generate code to cache results at the service level and increase logging capabilities.
Another use of this technique is to generate rest and WCF service code at the same time, which is useful when your client supports both browsers and C #.
In my work experience, I used T4 and EnvDTE to generate complete crud Rest Service code for the company's projects, including database calls and unit tests. It took a few minutes, not a few hours.