DEV Community

Cover image for Source generators and a boilerplate code
Aleksander Parchomenko
Aleksander Parchomenko

Posted on

Source generators and a boilerplate code

Introduction

Source generators first were introduced in C# 9.0 in Spring 2020 as a new compiler feature that lets developers to generate new C# source code that can be added to a compilation. Using it you can inspect code with all of the rich metadata that the compiler builds up during compilation, then emit C# code back into the same compilation that is based on the data you’ve analyzed. If you’re familiar with Roslyn Analyzers, you can think of Source Generators as analyzers that can emit C# source code. It is a powerfull developers tool, that can augment you code starting from generation custom serializations and ending with generated fast dependency injection containers.

Code refactoring is one of the processes when developers maintain applications to minimize technical debth, refresh libraries used or maintain code readability. The main drawback of the boilerplate code is blackout the business logic: instead of analysis of functionality programmers need also to find it among overall code.
Here I’d like to introduce 3 libraries written by myself that are based on Source Generators features: SourceMapper, SourceConfig and SourceApi and are aimed to decrease boilerplate code in solution. An idea of each of the packages is to autogenerate code with some functionalities that can be used in code.

SourceMapper

During my work on different cloud-native applications with a various tech stacks I’ve paid attention to Java’s widely used mapping library MapStruct, where developers define mappings using Java annotations (in C# attributes). The SourceMapper package uses Source Generators and generates objects mappings based on C# attributes actually during coding. Of course, there is widely used .NET Mapper library AutoMapper, but the main difference between tham, that developer can see (and control) mappings in generated code.
The package can be installed using Nuget Package Manager:

Install-Package Compentio.SourceMapper
Enter fullscreen mode Exit fullscreen mode

For example, definition of UserDao mapping to UserInfo object can be defined in interface, ClassName — defines the target mapper class name that is generated:

[Mapper(ClassName = "UserMapper")]
public interface IUserMapper
{
    [Mapping(Source = nameof(UserDao.FirstName), Target = nameof(UserInfo.Name))]
    UserInfo MapToDomainModel(UserDao userDao);
}

Enter fullscreen mode Exit fullscreen mode

The Package than generates mapping code for you, that can be used in solution:

// <mapper-source-generated />
// <generated-at '01.10.2021 08:35:50' />
using System;

namespace Compentio.SourceMapper.Tests.Mappings
{
  public class UserMapper : IUserMapper
  {
      public static UserMapper Create() => new();
      public virtual Compentio.SourceMapper.Tests.Entities.UserInfo MapToDomainModel(Compentio.SourceMapper.Tests.Entities.UserDao userDao)
      {
          var target = new Compentio.SourceMapper.Tests.Entities.UserInfo();
          target.Name = userDao.FirstName;
          target.BirthDate = userDao.BirthDate;
          return target;
      }
   }
 }
Enter fullscreen mode Exit fullscreen mode

Dependency injection extension code for various .NET containers also generated based on the Dependency Injection container you’ve been used in project. In this way you can inject mappers in your services, controllers or repositories and stay clean with Domain, DTO and DAO transformations in solution.

More about code, examples and contributing you can find on SourceMapper GitHub.

SourceConfig

SourceConfig package uses Source Generators Additional File Transaformation feature to generate additional C# code. The idea of this package is simple: instead of creating POCO classes for configuration in you project, the package generates these objects intead of you. First lets intall it:

Install-Package Compentio.SourceConfig
Enter fullscreen mode Exit fullscreen mode

After that, even if you have few configs for different enviromnets, thay are merged in one class. Lets assume, that you have 2 configuration files for development and production like appsettings.json:

{
  "$NoteEmailAddresses": [
    "admin@test.com",
    "technical.admin@test.com",
    "business.admin@test.com"
  ],
  "ConnectionTimeout": "30",
  "ConnectionHost": "https://test.com",
  "DefaultNote": {
    "Title": "DefaultTitle",
    "Description":  "DefaultDescription"
  }
}
Enter fullscreen mode Exit fullscreen mode

and for development environment appsettings.development.json:

{
  "ConnectionTimeout": "300",
  "DatabaseSize": "200"
}
Enter fullscreen mode Exit fullscreen mode

All you need, is toset their properties as C# analyzer additional file in Visual Studio:

Image description
or in your *.cproj file:

<ItemGroup>
    <AdditionalFiles Include="Appsettings.Development.json">
      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
    </AdditionalFiles>
    <AdditionalFiles Include="Appsettings.json">
      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
    </AdditionalFiles>
</ItemGroup>
Enter fullscreen mode Exit fullscreen mode

This will generate object for you configuration and you do not need to stay in sync when new configuration properties will be added to teh files: thay will apppear in you class automatically!:

// <mapper-source-generated />
// <generated-at '01.12.2021 15:34:34' />
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;

namespace Compentio.SourceConfig.App
{
    [ExcludeFromCodeCoverage]
    public class AppSettings
    {
        public string ConnectionTimeout { get; set; }

        public string DatabaseSize { get; set; }

        public IEnumerable<string> _NoteEmailAddresses { get; set; }

        public string ConnectionHost { get; set; }

        public DefaultNote DefaultNote { get; set; }
    }

    [ExcludeFromCodeCoverage]
    public class DefaultNote
    {
        public string Title { get; set; }

        public string Description { get; set; }
    }
}
Enter fullscreen mode Exit fullscreen mode

Source code and more, of course, on SourceConfig GitHub.

SourceApi

There are two approaches when implemening Web API: code first, that most of developers prefer: to create Web API controllers, DTO’s, to add Swagger UI and that’s all!; and API first, when API needs to be designed or discussed and only after that we start to implement it. In distributed systems with a various technologies and consumers of our API it is good to have language agnostic tools to agree and share API between the consumers. Open API Specification has been created for that:

The OpenAPI Specification (OAS) defines a standard, language-agnostic interface to RESTful APIs which allows both humans and computers to discover and understand the capabilities of the service without access to source code, documentation, or through network traffic inspection.

The third library, that I called SourceApi created for API first approach: you (or your team) define API in yaml or json format, add it to Web API project and the package generates Controller base classes with DTO’s and documentations for your API. All you need, is to implement logic in controllers. You even do not need to create DTO’s, since it already generated.
For example, for standard Open API example, defined like

openapi: 3.0.1
info:
  title: Swagger Petstore
  description: 'This is a sample server Petstore server.  You can find out more about     Swagger
    at [http://swagger.io](http://swagger.io) or on [irc.freenode.net, #swagger](http://swagger.io/irc/).      For
    this sample, you can use the api key `special-key` to test the authorization     filters.'
  termsOfService: http://swagger.io/terms/
  contact:
    email: apiteam@swagger.io
  license:
    name: Apache 2.0
    url: http://www.apache.org/licenses/LICENSE-2.0.html
  version: 1.0.0
externalDocs:
  description: Find out more about Swagger
  url: http://swagger.io
servers:
- url: https://petstore.swagger.io/api/v1
- url: http://petstore.swagger.io/api/v1
paths:
  /store/inventory:
    get:
      summary: Returns pet inventories by status
      description: Returns a map of status codes to quantities
      operationId: getInventory
      responses:
        200:
          description: successful operation
          content:
            application/json:
              schema:
                type: object
                additionalProperties:
                  type: integer
                  format: int32
      security:
      - api_key: []
  /store/order:
    post:
      summary: Place an order for a pet
      operationId: placeOrder
      requestBody:
        description: order placed for purchasing the pet
        content:
          '*/*':
            schema:
              $ref: '#/components/schemas/Order'
        required: true
      responses:
        200:
          description: successful operation
          content:
            application/xml:
              schema:
                $ref: '#/components/schemas/Order'
            application/json:
              schema:
                $ref: '#/components/schemas/Order'
        400:
          description: Invalid Order
          content: {}
      x-codegen-request-body-name: body
  /store/order/{orderId}:
    get:
      summary: Find purchase order by ID
      description: For valid response try integer IDs with value >= 1 and <= 10.  Other
        values will generated exceptions
      operationId: getOrderById
      parameters:
      - name: orderId
        in: path
        description: ID of pet that needs to be fetched
        required: true
        schema:
          maximum: 10.0
          minimum: 1.0
          type: integer
          format: int64
      responses:
        200:
          description: successful operation
          content:
            application/xml:
              schema:
                $ref: '#/components/schemas/Order'
            application/json:
              schema:
                $ref: '#/components/schemas/Order'
        400:
          description: Invalid ID supplied
          content: {}
        404:
          description: Order not found
          content: {}
    delete:
      summary: Delete purchase order by ID
      description: For valid response try integer IDs with positive integer value.         Negative
        or non-integer values will generate API errors
      operationId: deleteOrder
      parameters:
      - name: orderId
        in: path
        description: ID of the order that needs to be deleted
        required: true
        schema:
          minimum: 1.0
          type: integer
          format: int64
      responses:
        400:
          description: Invalid ID supplied
          content: {}
        404:
          description: Order not found
          content: {}
components:
  schemas:
    Order:
      type: object
      additionalProperties: false
      properties:
        id:
          type: integer
          format: int64
        petId:
          type: integer
          format: int64
        quantity:
          type: integer
          format: int32
        shipDate:
          type: string
          format: date-time
        status:
          type: string
          description: Order Status
          enum:
          - placed
          - approved
          - delivered
        complete:
          type: boolean
          default: false
      xml:
        name: Order

Enter fullscreen mode Exit fullscreen mode

StoreControllerBase class is generated as (underhood it uses NSwag CSharpControllerGenerator to generate abstract controllers):

namespace Compentio.SourceApi.WebExample.Controllers
{
    using System = global::System;

    [System.CodeDom.Compiler.GeneratedCode("NSwag", "13.13.2.0 (NJsonSchema v10.5.2.0 (Newtonsoft.Json v12.0.0.2))")]
    [Microsoft.AspNetCore.Mvc.Route("api/v1")]
    public abstract class StoreControllerBase : Microsoft.AspNetCore.Mvc.ControllerBase
    {
        /// <summary>Returns pet inventories by status</summary>
        /// <returns>successful operation</returns>
        [Microsoft.AspNetCore.Mvc.HttpGet, Microsoft.AspNetCore.Mvc.Route("store/inventory")]
        public abstract System.Threading.Tasks.Task<Microsoft.AspNetCore.Mvc.ActionResult<System.Collections.Generic.IDictionary<string, int>>> GetInventory();
        /// <summary>Place an order for a pet</summary>
        /// <param name = "body">order placed for purchasing the pet</param>
        /// <returns>successful operation</returns>
        [Microsoft.AspNetCore.Mvc.HttpPost, Microsoft.AspNetCore.Mvc.Route("store/order")]
        public abstract System.Threading.Tasks.Task<Microsoft.AspNetCore.Mvc.ActionResult<Order>> PlaceOrder([Microsoft.AspNetCore.Mvc.FromBody][Microsoft.AspNetCore.Mvc.ModelBinding.BindRequired] Order body);
        /// <summary>Find purchase order by ID</summary>
        /// <param name = "orderId">ID of pet that needs to be fetched</param>
        /// <returns>successful operation</returns>
        [Microsoft.AspNetCore.Mvc.HttpGet, Microsoft.AspNetCore.Mvc.Route("store/order/{orderId}")]
        public abstract System.Threading.Tasks.Task<Microsoft.AspNetCore.Mvc.ActionResult<Order>> GetOrderById([Microsoft.AspNetCore.Mvc.ModelBinding.BindRequired] long orderId);
        /// <summary>Delete purchase order by ID</summary>
        /// <param name = "orderId">ID of the order that needs to be deleted</param>
        [Microsoft.AspNetCore.Mvc.HttpDelete, Microsoft.AspNetCore.Mvc.Route("store/order/{orderId}")]
        public abstract System.Threading.Tasks.Task<Microsoft.AspNetCore.Mvc.IActionResult> DeleteOrder([Microsoft.AspNetCore.Mvc.ModelBinding.BindRequired] long orderId);
    }

    [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "10.5.2.0 (Newtonsoft.Json v12.0.0.2)")]
    public partial class Order
    {
        [Newtonsoft.Json.JsonProperty("id", Required = Newtonsoft.Json.Required.DisallowNull, NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore)]
        public long Id { get; set; }

        [Newtonsoft.Json.JsonProperty("petId", Required = Newtonsoft.Json.Required.DisallowNull, NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore)]
        public long PetId { get; set; }

        [Newtonsoft.Json.JsonProperty("quantity", Required = Newtonsoft.Json.Required.DisallowNull, NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore)]
        public int Quantity { get; set; }

        [Newtonsoft.Json.JsonProperty("shipDate", Required = Newtonsoft.Json.Required.DisallowNull, NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore)]
        public System.DateTimeOffset ShipDate { get; set; }

        /// <summary>Order Status</summary>
        [Newtonsoft.Json.JsonProperty("status", Required = Newtonsoft.Json.Required.DisallowNull, NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore)]
        [Newtonsoft.Json.JsonConverter(typeof(Newtonsoft.Json.Converters.StringEnumConverter))]
        public OrderStatus Status { get; set; }

        [Newtonsoft.Json.JsonProperty("complete", Required = Newtonsoft.Json.Required.DisallowNull, NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore)]
        public bool Complete { get; set; } = false;
    }

    [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "10.5.2.0 (Newtonsoft.Json v12.0.0.2)")]
    public enum OrderStatus
    {
        [System.Runtime.Serialization.EnumMember(Value = @"placed")]
        Placed = 0,
        [System.Runtime.Serialization.EnumMember(Value = @"approved")]
        Approved = 1,
        [System.Runtime.Serialization.EnumMember(Value = @"delivered")]
        Delivered = 2,
    }
}
Enter fullscreen mode Exit fullscreen mode

and can be used as base abstract class for your Web API controller. You only need to concentrate on implementation logic instead of definitions of response codes, stay in sync with DTO’s, etc. When you change Open API definition file, the base abstract class and DTO’s are refreshed:

namespace Compentio.SourceApi.WebExample.Controllers
{
    [ApiController]
    [ApiConventionType(typeof(DefaultApiConventions))]
    public class StoreController : StoreControllerBase
    {
        /// <inheritdoc />
        public async override Task<IActionResult> DeleteOrder([BindRequired] long orderId)
        {
            // Implement your async code here
            return Accepted();
        }

        /// <inheritdoc />
        public async override Task<ActionResult<IDictionary<string, int>>> GetInventory()
        {
            throw new NotImplementedException();
        }

        /// <inheritdoc />
        public async override Task<ActionResult<Order>> GetOrderById([BindRequired] long orderId)
        {
            throw new NotImplementedException();
        }

        /// <inheritdoc />
        public async override Task<ActionResult<Order>> PlaceOrder([BindRequired, FromBody] Order body)
        {
            throw new NotImplementedException();
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Some configuration properties also added to the package: you can define the namespace of your base controllers, or you can generate only DTO’s in a case when you are the consumer of some REST API.
Source code and documentation can be found on SourceApi GitHub.

Latest comments (0)