Automating the Repetitive with Source Generators

Dule Pop-AndovDule Pop-Andov
5 min read

Overview

We often write repetitive code when starting a new Web API project, whether using the modern, minimal API style or using classical .NET Controllers.

The most common cases are:

  • Rewriting pagination and sorting filters across projects;

  • Handling navigation properties and managing joins manually;

  • Creating controllers and database queries from scratch.

What I will go through within this article won’t be a replacement, but rather a package to quickly build and test an API, whether in minutes or hours… it depends on many things. All you have to do is think about the models and their relations beforehand. The rest can be built by using proper annotations.

Disclaimer: This is not a full implementation, or maybe it could be in the near future. For now, its peak potential will likely serve as a prototype for testing your ideas or perhaps for a personal project that doesn’t require heavy “configuration.” I mean, it is possible to do that, but it’s just not the intent— at least for now, though maybe in the future.

Meet dappi

The name stands for dotnet application pre-programming interface; it’s all lowercase because it’s small, if that makes sense at all (the truth is, dapi was taken, and I had to settle for dappi, which I’m equally happy with).

Now, I will introduce you to the annotation [CCController]. While the name may not seem directly related to dappi, I chose it because of my strong affection for the company where I work (CodeChem), hence the initials as a prefix. As for the name Controller, well, it speaks for itself.

 [CCController]

This can be used on top of any class representing an entity in your db, something we want to expose to the world in a REST API manner.

Let’s imagine we’re building a simple API to list our books and their author. We probably, almost always, would start by creating the class, right?
Yes, so let’s start with a book class.

[CCController]
public class Book
{
    [Key]
    [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
    public Guid Id { get; set; }
    public string Title { get; set; }
    public int YearPublished { get; set; }

    // Foreign key to reference the Author
    [Required]
    [ForeignKey("Author")]
    public Guid AuthorId { get; set; }

    // Navigation property: Each book has one author
    public Author? Author { get; set; }
}

Yep, it looks like every other class represents a table in the database. The attributes mentioned here are part of Entity Framework or .NET's System.ComponentModel.DataAnnotations namespace. These attributes help Entity Framework understand how to map our class properties to database schema columns and create relationships. While we’re not discussing Fluent API or EF in detail right now, we are simply referencing the tools we use.

Since we have finished the Book class and mentioned the Author there, let’s also create the Author class.

[CCController]
public class Author
{
    [Key]
    [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
    public Guid Id { get; set; }

    [Required]
    [MaxLength(100)]
    public string Name { get; set; }

    [Required]
    [Range(0, 120)]
    public int Age { get; set; }

    // Navigation property: One author can have many books
    public ICollection<Book>? Books { get; set; }
}

Yes, that's it, folks! Believe it or not, we now have e a "fully” functional API ready to serve our needs. It includes all the CRUD operations that we would typically write manually and it is ready for testing. Simply just run dotnet run <your api-project> and open swagger in the browser… if you have it configured, of course. :)

Something like this should pop up:

Filtering, Sorting, and Pagination

Now only that, but our read operation come along with features for filtering, sorting, pagination, and database manipulation. And yes, they all work!
Here’s an example of what a GET request for books looks like:

The result:

In the query, we have a limit of 3, sorted by YearPublished in ascending order.

Here is an example for filtering, can be done for any property:
We are asking for books that are published in 2019.

The result

We have retrieved the books that have been published 2019, this query can also be combined with sorting, we can limit our picks and we can add pagination.

What is this magic? Where can I find the controllers’ code?

This will definitely make it onto the FAQ list, so here it is: It’s located in the /obj folder generated at compile time, specifically during the C# build process. As for any other IDEs, it’s typically found under Dependencies/Source Generators/.

For VS Code, you can load it from the/Generated folder under /obj, but previously, you had to modify your .csproj like this:

<PropertyGroup>
    <!-- This for setting the path to find your source generated files -->
    <EmitCompilerGeneratedFiles>true</EmitCompilerGeneratedFiles>
    <CompilerGeneratedFilesOutputPath>$(BaseIntermediateOutputPath)\Generated</CompilerGeneratedFilesOutputPath>
</PropertyGroup>

Can I debug the code?

Yes, fully; just locate the files from the section above and place breakpoints.

Can I change the implementation?

No, unfortunately, you cannot change the implementation, but you can definitely extend your controller by adding code to your implementation. For instance, if we had a system to rent books, we would want to incorporate that functionality. Just ensure you declare it as a partial class, something like this:

namespace CCApi.WebApiExample.Controllers;

public partial class BookController
{
    [HttpGet("rent/{id}")]
    public async Task<IActionResult> RentBook(Guid id)
    {
        var book = await dbContext.Books.FirstOrDefaultAsync(book => book.Id == id);
        // rent the book.
        return Ok(book);
    }
}

This will register it under the same BookController, and swagger will recognize it and display it in the same section.

What happened? How did this code end up here?

Simply put, this package utilizes the C# Source Generators, which is how it’s building our API in the background.

How to Install

It’s fairly easy! Just add the NuGet package by running the command below, and happy coding! No real dependencies, just a dotnet-api project with <TargetFramework>8.0</TargetFramework>.

dotnet add package Codechem.Dappi.Generator --version 1.0.0

Note: The package is not yet deployed, will be within the next couple of days

Final Thoughts

Yes, I plan to develop this further and make it production-friendly. I hope to help many developers and save their time, potentially even creating a GUI. It’s not a replacement for custom solutions but a big leap forward in saving time and reducing boilerplate.

Until then, I hope some of you will try this package and build something good with it. I am open to feedback, suggestions, and contributions, so please reach out to me at dule@codechem.com.

Until next time, stay curious and keep exploring!

Dule Pop-Andov

25
Subscribe to my newsletter

Read articles from Dule Pop-Andov directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Dule Pop-Andov
Dule Pop-Andov