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
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);
}
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;
}
}
}
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
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"
}
}
and for development environment appsettings.development.json:
{
"ConnectionTimeout": "300",
"DatabaseSize": "200"
}
All you need, is toset their properties as C# analyzer additional file in Visual Studio:
<ItemGroup>
<AdditionalFiles Include="Appsettings.Development.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</AdditionalFiles>
<AdditionalFiles Include="Appsettings.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</AdditionalFiles>
</ItemGroup>
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; }
}
}
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
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,
}
}
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();
}
}
}
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.
Top comments (0)