Implementing Source Generator For Handler Discovery

by Viktoria Ivanova 52 views

Hey guys! Let's dive into implementing a Source Generator for handler discovery. This is super useful for library developers who want to boost initialization speed and ensure compatibility with Ahead-Of-Time (AOT) compilation. We're going to explore the context, requirements, technical specs, and acceptance criteria. Buckle up, it's gonna be a fun ride!

1. CONTEXT & OBJECTIVE (The Why)

As library developers, we always strive for performance and efficiency. One common challenge is the overhead associated with reflection, especially during application startup. Reflection can be slow, and it's not always compatible with AOT compilation. This is where Source Generators come to the rescue! Source Generators allow us to generate code at compile-time, which means no more runtime reflection overhead. This is a huge win for performance and AOT compatibility.

The Core Idea

The main idea is to create a Source Generator that automatically discovers all implementations of ICommandHandler and IEventHandler in a project. This discovery happens during compilation, and the Source Generator emits code that registers these handlers in the Dependency Injection (DI) container. Imagine the possibilities! No more manual registration of handlers, no more reflection overhead, and a blazing-fast application startup. The goal here is to make our libraries ultra-fast and AOT-friendly.

Why Source Generators?

Source Generators are a game-changer for several reasons:

  1. Performance: They eliminate runtime reflection, which is a significant performance bottleneck.
  2. AOT Compatibility: AOT compilation requires that all code be known at compile time. Source Generators fit perfectly into this model.
  3. Developer Experience: They reduce boilerplate code and make it easier to manage handlers in a project.
  4. Maintainability: Auto-generated code is less prone to human error and easier to update.

So, the objective here is clear: use a Source Generator to discover and register handlers, making our libraries faster, more efficient, and AOT-compatible. Let's get into the nitty-gritty details!

2. FUNCTIONAL REQUIREMENTS (RFs) (The What)

Functional Requirements, or RFs, define what the system should do. In our case, we have one primary functional requirement (RF-01) that encapsulates the core behavior of our Source Generator. Let's break it down.

RF-01: Auto-Registering Handlers

This requirement focuses on the automatic registration of handlers in the DI container. It describes a scenario where the library consumer's project includes classes that implement ICommandHandler and IEventHandler. When the project is compiled, the Source Generator springs into action and generates a source code file. This generated file contains an extension method that registers each discovered handler in the DI container.

Given: A project that consumes the library and has classes implementing ICommandHandler and IEventHandler.

When: The project is compiled.

Then: A source code file is automatically generated, containing an extension method that registers each handler found in the DI container.

The Details

Let's dive a bit deeper into what this means. Imagine a scenario where a developer has created several command handlers and event handlers in their project. Manually registering each of these handlers in the DI container can be tedious and error-prone. The Source Generator automates this process, making the developer's life much easier. It does this by:

  1. Discovering Handlers: Identifying classes that implement ICommandHandler and IEventHandler.
  2. Generating Code: Creating an extension method (e.g., AddGeneratedHandlers()) that registers these handlers.
  3. DI Registration: Using calls like services.AddScoped<MyCommandHandler, MyCommandHandler>(); to register the handlers in the DI container with the appropriate lifetime (in this case, Scoped).

By automating this process, we ensure that handlers are correctly registered, reducing the risk of errors and saving developers a significant amount of time. This is a game-changer for developer productivity.

3. TECHNICAL SPECIFICATIONS & RESTRICTIONS (The How)

Alright, let's get into the how! This section outlines the technical specifications and restrictions for our Source Generator. We'll cover the files to be created, the language and frameworks to use, the dependencies, specific logic, and any restrictions we need to keep in mind.

Project Structure and Files

We'll start by creating a new project specifically for the Source Generator. A suggested file name for the main generator class is RiseOn.ChannelBus.SourceGenerator/Generator.cs. This file will contain the core logic for the Source Generator.

Language and Framework

We'll be using C# as the primary language for our Source Generator. C# provides excellent support for Source Generators and the Roslyn compiler APIs. We'll also be leveraging the .NET framework, specifically the Roslyn libraries for code analysis and generation.

Dependencies and Libraries

To build our Source Generator, we'll rely on the following key dependencies:

  • Microsoft.CodeAnalysis.CSharp: This library provides the C# syntax and semantic analysis APIs.
  • Microsoft.CodeAnalysis.Analyzers: This library includes analyzers that help us write robust and efficient Source Generators.

These libraries are essential for parsing C# code, understanding its structure, and generating new code. They're the powerhouse behind our Source Generator.

Specific Logic

Here's where the magic happens! The logic of our Source Generator involves several key steps:

  1. Using IIncrementalGenerator for Performance: We'll use the IIncrementalGenerator interface, which is designed for high performance. Incremental generators optimize the generation process by only re-running when necessary, making them incredibly efficient.
  2. Using a SyntaxProvider: A SyntaxProvider will help us efficiently find all class declarations that implement the handler interfaces. This allows us to quickly identify potential handlers without parsing the entire codebase.
  3. Leveraging the SemanticModel: The SemanticModel provides detailed information about the code, such as types, symbols, and relationships. We'll use it to confirm that the discovered classes correctly implement the ICommandHandler and IEventHandler interfaces.
  4. Generating the Extension Method: Finally, we'll generate an extension method for IServiceCollection (e.g., AddGeneratedHandlers()). This method will contain calls to services.AddScoped<MyCommandHandler, MyCommandHandler>(); for each discovered handler, registering them in the DI container.

This logic ensures that we accurately discover handlers and register them in a way that's both efficient and maintainable. It's like having a smart assistant that handles all the tedious registration work for us!

Restrictions

We need to be mindful of a crucial restriction: our Source Generator should not introduce any runtime dependencies to the consumer's project. This means we can't add any additional NuGet packages that the consumer would need to install. Our goal is to generate code that seamlessly integrates into the consumer's project without adding any extra baggage. This keeps the library lightweight and easy to use.

4. ACCEPTANCE CRITERIA (The Definition of