Creating RESTful Services with T4 Based on Model and Interfaces

Datetime:2016-08-22 23:01:06          Topic: .Net  REST           Share

Key takeaways

  • Most REST services contain repeatable patterns.
  • A lot of time can be saved by using code generation to these patterns.
  • Visual Studio’s T4 and EnvDTE can provide robust code generation without additional tooling.
  • WCF and database calls can be generated using these same techniques.

In Visual Studio, a T4 text template is a mixture of text blocks and control logic that can generate a text file. The control logic is written as fragments of program code in Visual C# or Visual Basic. In Visual Studio 2015 Update 2 and later, you can use C# version 6.0 features in T4 templates directives. The generated file can be text of any kind, such as a Web page, or a resource file, or program source code in any language. T4 is widely used by Microsoft. They use it to generate MVC views, controllers, Entity Framework contexts etc.

T4 is aimed at developers who would like to either create modules based on some patterns and model or to minimize boilerplate code. With T4 we can generate code which simply wraps calls to business logic or any other service. We can also add logging, implement caching mechanisms, create Request/Response classes based on some model or even implement business logic etc.

Since REST services should simply be a wrapper around our business logic, T4 we can automate creation of REST/WCF or any other service technology based on our interface and model. This leaves the developer more time to focus on the business logic written in C# and SQL.

User story

Let’s say we need to develop simple service for handling products with GET, GET many, Insert, and Update methods. The product entity has 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; }
    }

Products can be filtered by their Name, Number, and range of prices. When inserting a product, we specify every value except for Id and Number. Those are automatically generated and therefore we can’t update them either. In order to improve performance, the client can indicate whether or not it needs the product group object. Omitting this will eliminate the need for a join or separate product group lookup.

For ease of understanding, here is a diagram representing the architecture I will be using for this article:

Get many methods

As mentioned, we need to filter products by their Name, Number, or price range. This is known as a “filter class” or “search object” and can be quite tedious to code by hand.

By using T4, EnvDTE, and attributes on the model, we can automatically create a new filter class. For example, we will put these attributes on our 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; }
        ...
    }

With T4 we can automatically generate a class with the properties based on those attributes:

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; }
}

If we are using EntityFramework, we can easily generate business logic that will populate a LINQ query based on this search object and model. In order to do that we would first need to define an interface and with the attributes what we want to use. For example:

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

    }

Once this is done, T4 can determine what the default implementation should be and generate logic for populating query based on search 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 then register our default implementation in our IoC framework:

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());
		}
	}

When we set it up this way, we can easily replace this implementation with different one by overriding this registration in another class with a higher priority.

When generating the REST API, T4 will look for attributes in the interface to determine which method is used for retrieving information. For example, in our IProductService interface we can add method with the corresponding attribute:

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

Now that we know which method is for retrieving records, 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 we mentioned, we would like to give the client the ability to optionally request extra data such as the ProductGroup details. We can do that by adding [ LazyLoading ] attribute on ProductGroup property and generate code that will handle this scenario.

public partial class Product
    {
        //ommited code

        [LazyLoading]
        public ProductGroup ProductGroup { get; set; }
    }

Once we add [ LazyLoading ] attribute, T4 will create new class that holds IsProductGroupLoadingEnabled property.

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

On BL side if we are using Entity Framework, generated code looks like this:

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 methods

The list of properties on an insert object are often different than the full model. For example, auto-generated keys shouldn’t be passed in by the client, as they would be ignored. While that is pretty obvious, some of the other fields can be quite problematic.

Consider the ProductGroup property. If we included that in the insert object, then it would give the false impression that the client could use this call to create or update a ProductGroup. So it’s best to provide an explicit insert object rather than reusing the full model.

To avoid the tediousness of creating this by hand, we will again use attributes to indicate which properties are needed. For example:

[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; }
    }

Based on this information, T4 can generate 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; }
}

As before, we need to modify our interface so that T4 will know which method is responsible for handling the insert request. We can do it by adding attribute to suitable method, for example:

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

Using information from the model and interface, T4 will generate REST API code such as:

[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 methods

The principle is same as with the insert method. Here we add [RequestField("Update")] attribute on entity’s properties and that will generate ProductUpdateRequest with suitable properties. After that we would add attribute on interface to notify T4 which method is responsible for handling update.

When we add those attributes, T4 will generate method for updating data on REST service following any practice that we prefer:

[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 we have seen, with T4 we can save a lot of time by generating code that follows repeating patterns. The code we generate can be as easily readable as hand-written code. Using this same technique, we can generate code that will cache results or add logging at the service level.

Another use for this technique is generating both REST and WCF services, which is helpful when you have a mix of browser and C# clients.

During my career, I’ve also used T4 and EnvDTE generate complete CRUD REST services for enterprise solutions, including the database calls and unit tests, in minutes instead of hours .

If you would like to know more, proof of concept can be found at GitHub page or contact me via LinkedIn .

About author

Amel Musić started his career developing solutions for broker companies. Working mostly for banks and government institutions he specialized in design and optimization of solutions based on MS SQL Server and .NET platform. After spending few years in designing those systems, he started creating T4 scripts together with aspect oriented programming that helps him to remove boilerplate code and focus more on client needs.





About List