LinKit.Core
2.0.6
See the version list below for details.
dotnet add package LinKit.Core --version 2.0.6
NuGet\Install-Package LinKit.Core -Version 2.0.6
<PackageReference Include="LinKit.Core" Version="2.0.6" />
<PackageVersion Include="LinKit.Core" Version="2.0.6" />
<PackageReference Include="LinKit.Core" />
paket add LinKit.Core --version 2.0.6
#r "nuget: LinKit.Core, 2.0.6"
#:package LinKit.Core@2.0.6
#addin nuget:?package=LinKit.Core&version=2.0.6
#tool nuget:?package=LinKit.Core&version=2.0.6
LinKit.Core
LinKit.Core is a high-performance, modular toolkit for .NET, providing source-generated helpers for CQRS, Dependency Injection, Minimal API Endpoints, Background Jobs, Mapping, Messaging, and gRPC. LinKit eliminates boilerplate, maximizes runtime performance, and is fully compatible with NativeAOT and trimming.
Why LinKit?
Most .NET libraries rely on runtime reflection, which is slow, memory-intensive, and incompatible with NativeAOT. LinKit uses C# Source Generators to analyze your code and generate optimized, boilerplate-free C# at compile time, linking your application's components together.
Key Benefits:
- 🚀 Zero Reflection: No runtime scanning or reflection.
- ⚡ Fast Startup: No assembly scanning.
- 🗑️ AOT & Trimming Safe: Works with Blazor, MAUI, NativeAOT.
- ✍️ Clean API: Intent-driven, explicit, and easy to use.
- 🤖 Automated Boilerplate: For DI, API endpoints, background jobs, gRPC, messaging, and mapping.
LinKit Ecosystem
| Package | Description | NuGet |
|---|---|---|
LinKit.Core |
Required. Interfaces, attributes, and source generator. | NuGet |
LinKit.Grpc |
gRPC server/client codegen for CQRS requests. | NuGet |
LinKit.Messaging.RabbitMQ |
RabbitMQ implementation for Messaging Kit. | NuGet |
LinKit.Messaging.Kafka |
Kafka implementation for Messaging Kit. | NuGet |
Installation
dotnet add package LinKit.Core
```Add other packages as needed:
```shell
dotnet add package LinKit.Grpc
dotnet add package LinKit.Messaging.RabbitMQ
Kits Overview
1. CQRS Kit
A high-performance, source-generated Mediator for implementing the CQRS (Command Query Responsibility Segregation) pattern with zero reflection.
The LinKit CQRS Kit provides a clean, type-safe API for sending commands and queries. The source generator analyzes your request and handler classes, then creates a highly optimized Mediator implementation that wires everything together at compile time. This approach guarantees the best possible runtime performance and is fully compatible with NativeAOT.
Core Concepts
LinKit defines a clear set of interfaces for your requests:
ICommand: Represents an operation that changes the state of the system but does not return a value (a "fire-and-forget" action).ICommand<TResponse>: Represents an operation that changes state and returns a value (e.g., creating an entity and returning its new ID).IQuery<TResponse>: Represents an operation that retrieves data and does not change the state of the system.
How It Works
- Define Requests: Create classes that implement one of the core request interfaces (
ICommand,ICommand<TResponse>, orIQuery<TResponse>). - Create Handlers: For each request, create a handler class that implements the corresponding handler interface (
ICommandHandlerorIQueryHandler). Mark the handler with the[CqrsHandler]attribute for discovery. - Register Services: In your
Program.cs, call thebuilder.Services.AddLinKitCqrs()extension method. This single call registers the generatedIMediator, all your handlers, and any pipeline behaviors. - Inject and Use: Inject
IMediatorinto your controllers, services, or other handlers and use its simple, intentional API to send requests.
Step-by-Step Usage
Step 1: Define Your Requests
Create classes for each command and query. The interface you implement determines which IMediator method you will use.
Example 1: A Query that returns data
This query will fetch a UserDto object.
// In: Features/Users/GetUserQuery.cs
public class GetUserQuery : IQuery<UserDto>
{
public int UserId { get; set; }
}
Example 2: A Command that does not return a value
This command creates a new user. It implements ICommand, signifying a "void" operation.
// In: Features/Users/CreateUserCommand.cs
public class CreateUserCommand : ICommand
{
public string UserName { get; set; }
public string Email { get; set; }
}
Example 3: A Command that returns a value
This command creates an order and returns the Guid of the newly created entity.
// In: Features/Orders/CreateOrderCommand.cs
public class CreateOrderCommand : ICommand<Guid>
{
public Guid CustomerId { get; set; }
public List<OrderItemDto> Items { get; set; }
}
Step 2: Create Handlers
For each request, create a corresponding handler and mark it with [CqrsHandler].
Handler for the Query:
// In: Features/Users/GetUserHandler.cs
using LinKit.Core.Cqrs;
[CqrsHandler]
public class GetUserHandler : IQueryHandler<GetUserQuery, UserDto>
{
private readonly IUserRepository _userRepository;
public GetUserHandler(IUserRepository userRepository)
{
_userRepository = userRepository;
}
public async Task<UserDto> HandleAsync(GetUserQuery query, CancellationToken cancellationToken)
{
var user = await _userRepository.GetByIdAsync(query.UserId);
// Map user entity to UserDto and return...
return user.ToUserDto();
}
}
Handler for the "void" Command:
For commands that don't return a value (ICommand), the handler must return Task<Unit> and end with return Unit.Value;. The Unit type is a special struct provided by LinKit to represent a void result in a generic context.
// In: Features/Users/CreateUserHandler.cs
using LinKit.Core.Cqrs;
[CqrsHandler]
public class CreateUserHandler : ICommandHandler<CreateUserCommand>
{
private readonly AppDbContext _context;
public CreateUserHandler(AppDbContext context)
{
_context = context;
}
public async Task<Unit> HandleAsync(CreateUserCommand command, CancellationToken cancellationToken)
{
var user = new User { UserName = command.UserName, Email = command.Email };
_context.Users.Add(user);
await _context.SaveChangesAsync(cancellationToken);
// Always return Unit.Value for ICommand handlers
return Unit.Value;
}
}
Step 3: Register the CQRS Kit
In your application's entry point (Program.cs), add the LinKit CQRS services.
var builder = WebApplication.CreateBuilder(args);
// This extension method finds the source generator and registers
// the generated Mediator, all handlers, and all behaviors.
builder.Services.AddLinKitCqrs();
// Add other services like DbContext, repositories, etc.
builder.Services.AddDbContext<AppDbContext>(...);
builder.Services.AddScoped<IUserRepository, UserRepository>();
var app = builder.Build();
Step 4: Use the Mediator
Inject IMediator and call the appropriate method based on your request type. The API is clean, explicit, and type-safe.
using LinKit.Core.Cqrs;
using Microsoft.AspNetCore.Mvc;
[ApiController]
[Route("api/[controller]")]
public class UsersController : ControllerBase
{
private readonly IMediator _mediator;
public UsersController(IMediator mediator)
{
_mediator = mediator;
}
[HttpGet("{id}")]
public async Task<IActionResult> GetUserById(int id)
{
var query = new GetUserQuery { UserId = id };
// Use QueryAsync for IQuery<TResponse>
var userDto = await _mediator.QueryAsync(query);
return Ok(userDto);
}
[HttpPost]
public async Task<IActionResult> CreateUser([FromBody] CreateUserCommand command)
{
// Use SendAsync for ICommand
await _mediator.SendAsync(command);
return Created();
}
[HttpPost("orders")]
public async Task<IActionResult> CreateOrder([FromBody] CreateOrderCommand command)
{
// Use the SendAsync<TResponse> overload for ICommand<TResponse>
Guid newOrderId = await _mediator.SendAsync(command);
return CreatedAtAction(nameof(GetOrderById), new { id = newOrderId }, newOrderId);
}
}
Advanced: Pipeline Behaviors for Cross-Cutting Concerns
The true power of the Mediator pattern comes from its ability to create a pipeline of behaviors to handle cross-cutting concerns like validation, logging, caching, and transactions in a clean and reusable way. The LinKit CQRS Kit has built-in, source-generated support for a flexible and powerful behavior pipeline.
There are two ways to apply behaviors in LinKit:
- Contract Behaviors: Applied automatically to any request that implements a specific "marker" interface. Ideal for broad, rule-based concerns.
- Specific Behaviors: Applied explicitly to a single request class using an attribute. Ideal for unique, ad-hoc logic.
How to Create a Behavior
A behavior is a generic class that implements the IPipelineBehavior<TRequest, TResponse> interface. It receives the current request and a next delegate. Calling await next() passes control to the next behavior in the pipeline, or to the final handler. This allows you to execute code before and after the core business logic runs.
Example: A Simple Logging Behavior
// In: Behaviors/LoggingBehavior.cs
using LinKit.Core.Cqrs;
public class LoggingBehavior<TRequest, TResponse> : IPipelineBehavior<TRequest, TResponse>
where TRequest : IRequest<TResponse>
{
private readonly ILogger<LoggingBehavior<TRequest, TResponse>> _logger;
public LoggingBehavior(ILogger<LoggingBehavior<TRequest, TResponse>> logger)
{
_logger = logger;
}
public async Task<TResponse> HandleAsync(
TRequest request,
RequestHandlerDelegate<TResponse> next,
CancellationToken cancellationToken)
{
var requestName = typeof(TRequest).Name;
_logger.LogInformation("--> Handling request: {RequestName}", requestName);
// Call the next item in the pipeline
var response = await next();
_logger.LogInformation("<-- Finished request: {RequestName}", requestName);
return response;
}
}
1. Contract Behaviors
Use Contract Behaviors to apply logic to entire categories of requests.
Step 1: Create a "Marker" Interface
This is a simple, empty interface used to "tag" your requests.
// In: Contracts/IAuditable.cs
public interface IAuditable { }
// In: Contracts/IValidatable.cs
public interface IValidatable { }
Step 2: Create a Behavior and Register it with [CqrsBehavior]
The [CqrsBehavior] attribute tells the source generator that this class is a behavior and specifies which marker interface (TargetInterface) it should apply to. You can also control the execution order with the Order property (lower numbers run first).
// In: Behaviors/AuditBehavior.cs
using LinKit.Core.Cqrs;
// This behavior will run for any request that implements IAuditable.
// Order = 100 means it runs after behaviors with lower order numbers.
[CqrsBehavior(typeof(IAuditable), Order = 100)]
public class AuditBehavior<TRequest, TResponse> : IPipelineBehavior<TRequest, TResponse>
where TRequest : IRequest<TResponse>, IAuditable
{
// ... your auditing logic ...
public async Task<TResponse> HandleAsync(TRequest request, RequestHandlerDelegate<TResponse> next, CancellationToken ct)
{
// Code before handler
var result = await next();
// Code after handler
return result;
}
}
Step 3: Apply the Marker Interface to Your Requests
Simply implement the marker interface on any command or query you want the behavior to apply to.
public class CreateUserCommand : ICommand, IAuditable, IValidatable
{
public string UserName { get; set; }
public string Email { get; set; }
}
public class UpdateUserCommand : ICommand, IAuditable
{
public int UserId { get; set; }
public string UserName { get; set; }
}
That's it! No further registration is needed. The source generator will automatically detect the [CqrsBehavior] attribute and wire up the pipeline correctly during compilation. When you send a CreateUserCommand, it will pass through both the AuditBehavior and any ValidationBehavior you've defined. When you send UpdateUserCommand, it will only pass through the AuditBehavior.
2. Specific Behaviors
Use Specific Behaviors when you have logic that applies only to one particular request and creating a marker interface would be overkill.
Step 1: Create the Behavior (No Attribute Needed)
Create a standard IPipelineBehavior class. It does not need the [CqrsBehavior] attribute.
// In: Behaviors/IdempotencyCheckBehavior.cs
public class IdempotencyCheckBehavior<TRequest, TResponse> : IPipelineBehavior<TRequest, TResponse>
where TRequest : IRequest<TResponse>
{
// ... logic to check for duplicate requests using a key ...
public async Task<TResponse> HandleAsync(TRequest request, RequestHandlerDelegate<TResponse> next, CancellationToken ct)
{
// Check idempotency key before calling next()
var result = await next();
return result;
}
}
Step 2: Apply the Behavior with [ApplyBehavior]
Decorate the specific command or query class directly with the [ApplyBehavior] attribute, passing the type of the behavior to apply.
using LinKit.Core.Cqrs;
// This specific behavior will ONLY run for the ProcessPaymentCommand.
[ApplyBehavior(typeof(IdempotencyCheckBehavior<,>))]
public class ProcessPaymentCommand : ICommand<PaymentResult>
{
public Guid IdempotencyKey { get; set; }
public decimal Amount { get; set; }
}
Note: When passing the behavior type to the attribute, use the unbound generic form (e.g., typeof(MyBehavior<,>)). The source generator will automatically fill in the correct generic arguments.
The IdempotencyCheckBehavior will now be part of the pipeline for ProcessPaymentCommand but no other request in the system.
2. Dependency Injection Kit
Attribute-based, source-generated DI registration.
- Mark Services:
[RegisterService(Lifetime.Scoped)]on your class. - Register:
builder.Services.AddLinKitDependency();
[RegisterService(Lifetime.Scoped)]
public class MyService : IMyService { ... }
Usage:
builder.Services.AddLinKitDependency();
3. Endpoints Kit (Minimal APIs)
The Endpoints Kit transforms your CQRS requests directly into high-performance Minimal API endpoints using Source Generators. It eliminates the need for Controllers, manual route mapping, and reflection-based dispatching.
- Zero Boilerplate: The request DTO is the API definition.
- Automatic Binding: Infers
[FromBody]for command payloads (POST/PUT/PATCH) and[AsParameters]for queries (GET/DELETE). - Metadata & OpenAPI: Support for Summary, Description, Tags, and Versions for Swagger/OpenAPI generation.
- Security: Built-in support for Policies, Roles, and CORS.
- Exception Handling: Map custom exceptions to HTTP status codes automatically.
Step 1: Define Endpoints
Decorate your ICommand or IQuery classes with the [ApiEndpoint] attribute.
Example 1: A GET Query (Automatic Query String Binding)
using LinKit.Core.Endpoints;
using Microsoft.AspNetCore.Mvc;
// Maps to: GET /users/{Id}
[ApiEndpoint(ApiMethod.Get, "users/{Id}")]
public class GetUserQuery : IQuery<UserDto>
{
// Attributes like [FromRoute] are supported by standard Minimal APIs
[FromRoute] public int Id { get; set; }
}
Example 2: A POST Command (Automatic Body Binding)
using LinKit.Core.Endpoints;
// Maps to: POST /users
[ApiEndpoint(ApiMethod.Post, "users")]
public class CreateUserCommand : ICommand<int>
{
public string UserName { get; set; }
public string Email { get; set; }
}
Example 3: Rich Metadata & Security
You can configure detailed endpoint metadata directly on the request.
[ApiEndpoint(ApiMethod.Put, "users/{Id}/email",
Name = "UpdateUserEmail",
Summary = "Updates a user's email address",
Description = "Requires Admin privileges.",
Tags = new[] { "UserManagement" }, // (Note: Tagging is handled via Feature groupings usually)
Roles = "Admin",
Policies = "CanUpdateUsers",
RateLimitPolicy = "Strict"
)]
public class UpdateEmailCommand : ICommand
{
[FromRoute] public int Id { get; set; }
public string NewEmail { get; set; }
}
Step 2: Route Grouping (Optional)
You can define global prefixes and shared security policies for a group of endpoints using the [ApiRouteGroup] attribute at the Assembly level. This is useful for versioning or feature grouping.
// In Program.cs or AssemblyInfo.cs
[assembly: ApiRouteGroup("/api/v1", Tag = "V1 API", RequireAuthorization = true)]
[assembly: ApiRouteGroup("/api/v2", Tag = "V2 API")]
You can then associate an endpoint with a group using the Group property:
[ApiEndpoint(ApiMethod.Get, "products", Group = "/api/v1")]
public class GetProductsQuery : IQuery<List<ProductDto>> { ... }
Step 3: Global Exception Handling
LinKit can generate a highly optimized exception handler middleware that maps your custom exceptions to specific HTTP status codes and Problem Details.
- Define Mappings: Decorate your custom exception classes.
using LinKit.Core.Endpoints;
[ApiExceptionMapping(StatusCodes.Status404NotFound, Title = "Resource Not Found")]
public class UserNotFoundException : Exception
{
public UserNotFoundException(int id) : base($"User {id} was not found.") { }
}
[ApiExceptionMapping(StatusCodes.Status409Conflict, LogLevel = "Warning")]
public class DuplicateEmailException : Exception { ... }
- Register Middleware: Use the generated middleware in your app pipeline.
var app = builder.Build();
// Replaces the standard app.UseExceptionHandler()
app.UseGeneratedExceptionHandler();
Step 4: Register Endpoints
In your Program.cs, simply call MapGeneratedEndpoints.
var app = builder.Build();
app.UseGeneratedExceptionHandler(); // Optional: If using exception mappings
app.UseAuthentication();
app.UseAuthorization();
// This single line maps all endpoints defined in your assembly
app.MapGeneratedEndpoints();
app.Run();
4. Background Job Kit
The Background Job Kit provides a powerful, configuration-driven system for executing CQRS commands and queries on a schedule. It source-generates the necessary infrastructure to link your job definitions to your CQRS handlers, creating a robust background processing system with zero reflection and full support for hot-reloading configurations.
How It Works
- The
[BackgroundJob]attribute tells the source generator to map a unique, human-readable name to a specific CQRS request type. - At runtime, the registered
BackgroundJobManager(anIHostedService) reads your JSON configuration. - It uses the generated map to find the correct CQRS request for each configured job and uses the Mediator to execute it according to the specified schedule. The entire process is type-safe and performant.
Usage
Step 1: Decorate a CQRS Request
Mark any BackgroundJobCommand that you want to be available as a background job with the [BackgroundJob] attribute, providing a unique name.
[BackgroundJob("ProcessEndOfDayReport")]
public class ProcessEndOfDayReportCommand : BackgroundJobCommand
{
}
[CqrsHandler]
public class ProcessEndOfDayReportHandler : ICommandHandler<ProcessEndOfDayReportCommand>
{
private readonly ILogger<ProcessEndOfDayReportHandler> _logger;
public ProcessEndOfDayReportHandler(ILogger<ProcessEndOfDayReportHandler> logger)
{
_logger = logger;
}
public Task Handle(ProcessEndOfDayReportCommand command, CancellationToken cancellationToken)
{
_logger.LogInformation("Processing end-of-day report of type: {Type}", command.ReportType ?? "Standard");
// ... your business logic here ...
return Task.CompletedTask;
}
}
Step 2: Create Your Configuration
Create a section in your appsettings.json or a separate JSON file to define the schedules for your jobs. The Name property in the JSON must match the name provided in the [BackgroundJob] attribute.
Example appsettings.json:
{
"BackgroundJobConfig": {
"BackgroundJobs": [
{
"Name": "HeartbeatCheck",
"IsActive": true,
"ScheduleType": "Interval",
"TimeIntervalSeconds": 300
},
{
"Name": "SendDailyNewsletter",
"IsActive": true,
"RunOnStart": true,
"ScheduleType": "Daily",
"TimeOfDay": "08:00:00"
},
{
"Name": "WeeklyDatabaseCleanup",
"IsActive": true,
"ScheduleType": "Weekly",
"TimeOfDay": "03:30:00",
"DayOfWeek": "Sunday"
},
{
"Name": "ProcessEndOfDayReport",
"IsActive": false,
"ScheduleType": "Monthly",
"TimeOfDay": "23:59:00",
"DayOfMonth": 99,
"MaxParallel": 2,
"EmbeddedData": "{\"ReportType\": \"Financial\"}"
}
]
}
}
Step 3: Register the Background Job Service
In your Program.cs, call the AddBackgroundJobs extension method.
var builder = WebApplication.CreateBuilder(args);
// Register the Background Job hosted service
builder.AddBackgroundJobs(<path to config file>));
var app = builder.Build();
// ...
This registers the BackgroundJobManager which will automatically start, stop, and reload jobs based on your configuration.
Configuration Details
Below is a detailed description of each property available in the job configuration.
| Property | Type | Description |
|---|---|---|
Name |
string |
Required. The unique identifier for the job. This must exactly match the name provided in the [BackgroundJob("MyUniqueJobName")] attribute. |
IsActive |
bool |
Determines if the job is enabled. If set to false, the job will not run. Changes are detected at runtime. |
RunOnStart |
bool |
If true, the job will execute once immediately upon application start (or when the job is activated via config change), and then follow its regular schedule. Defaults to false. |
ScheduleType |
string |
Required. The scheduling mode. Can be one of four values: Interval, Daily, Weekly, Monthly. |
TimeIntervalSeconds |
int |
Used only when ScheduleType is Interval. Defines the number of seconds to wait between each job execution. |
TimeOfDay |
string |
Used for Daily, Weekly, and Monthly schedules. Defines the time of day (in UTC) to run the job. Format: "HH:mm:ss". Example: "14:30:00" for 2:30 PM UTC. |
DayOfWeek |
string |
Used only when ScheduleType is Weekly. The day of the week to run the job. Examples: "Monday", "Tuesday", etc. |
DayOfMonth |
int |
Used only when ScheduleType is Monthly. The day of the month to run (1-31). Special Value: Use a large number (e.g., 99) to signify the last day of the current month. |
MaxParallel |
int |
The maximum number of instances of this job that can run concurrently. Defaults to 1. Useful for long-running jobs to prevent overlap. |
EmbeddedData |
string (JSON) |
An optional string value that is passed directly to the EmbeddedData property of your command. This can be a simple string, a JSON object, or any other format you wish to parse in your handler. |
5. Mapping Kit
A high-performance, reflection-free, source-generated object mapper. The Mapping Kit provides a fluent and type-safe API to configure mappings, which are then transformed into highly optimized, direct-assignment code at compile time.
- Type-Safe API: Uses lambda expressions (
dest => dest.Property) to eliminate magic strings and catch errors at compile time. - Fluent Configuration: Chain
.ForMember()calls to create readable and maintainable mapping rules. - Convention-Based: Automatically maps properties with matching names or
[JsonPropertyName]/[JsonProperty]attributes. - No DI Required: Generates extension methods (
.ToUserDto()) that can be used anywhere.
Usage
Step 1: Create a Mapper Context
Create a partial class marked with the [MapperContext] attribute. This class will contain your mapping configurations.
using LinKit.Core.Mapping;
using YourApp.Models.Entities;
using YourApp.Models.Dtos;
[MapperContext]
public partial class ApplicationMapperContext : IMappingConfigurator
{
public void Configure(IMapperConfigurationBuilder builder)
{
// Define all your application's mappings here
builder.CreateMap<User, UserDto>()
// Examples of detailed configuration below
;
builder.CreateMap<Order, OrderSummaryDto>();
}
}
Step 2: Configure Mappings with the Fluent API
Use the CreateMap<TSource, TDestination>() method and chain .ForMember() calls to define custom mapping rules.
// Inside the Configure method from Step 1
builder.CreateMap<User, UserDto>()
// 1. Map from a differently named property (type-safe)
.ForMember(dest => dest.FullName, opt => opt.MapFrom(src => src.UserName))
// 2. Ignore a property
.ForMember(dest => dest.PasswordHash, opt => opt.Ignore())
// 3. Perform complex transformations
.ForMember(dest => dest.Initials, opt => opt.MapFrom(src => $"{src.FirstName[0]}{src.LastName[0]}"))
// 4. Use a custom converter method (e.g., from a static helper class)
.ForMember(
dest => dest.AddressString,
opt => opt.ConvertWith(
typeof(AddressFormatter),
nameof(AddressFormatter.Format),
src => src.Address
)
);
Step 3: Use the Generated Extension Methods
The source generator automatically creates .To...() and .To...List() extension methods. Just use them directly on your objects.
// Assuming 'user' is an instance of the User entity
var userDto = user.ToUserDto();
// For collections
// Assuming 'users' is an IEnumerable<User>
var dtoList = users.ToUserDtoList();
Mapping Conventions (Order of Precedence)
The mapper automatically maps properties that are not explicitly configured. It follows these rules in order:
- Explicit Configuration: Rules defined with
.ForMember()are always applied first. [JsonPropertyName]/[JsonProperty]Matching: If a destination property has a[JsonPropertyName]or[JsonProperty]attribute, the mapper looks for a source property with the same attribute and name. This is useful for mapping between C# naming conventions and JSON/API conventions (e.g.,FullNametofull_name).- Name Matching: If no attribute match is found, the mapper maps properties with the same name (case-insensitive). This includes nested objects and collections of the same type.
- Nested Object Mapping: If a property is a complex type (e.g.,
Address), and a map has been defined for it (CreateMap<Address, AddressDto>()), the mapper will automatically generate the call to.ToAddressDto().
6. Messaging Kit
Source-generated publisher/consumer for message brokers (RabbitMQ, Kafka).
- Mark Messages:
[Message]on your event/command. - Write Handlers:
[CqrsHandler]for the message. - Register:
builder.Services.AddLinKitMessaging();and the broker package.
[Message("user-events", RoutingKey = "user.created", QueueName = "email-service-queue")]
public record UserCreatedEvent(int UserId, string Email);
[CqrsHandler]
public class UserCreatedHandler : ICommandHandler<UserCreatedEvent> { ... }
Publisher:
builder.Services.AddLinKitMessaging();
builder.Services.AddLinKitRabbitMQ(configuration);
// await publisher.PublishAsync(new UserCreatedEvent(...));
Consumer:
builder.Services.AddLinKitCqrs();
builder.Services.AddLinKitMessaging();
builder.Services.AddLinKitRabbitMQ(configuration);
7. gRPC Kit (via LinKit.Grpc)
The gRPC Kit transforms your existing CQRS requests into fully functional, high-performance gRPC services with minimal effort. It completely automates the tedious process of writing gRPC service implementations, including request/response mapping, error handling, and CQRS mediator invocation.
- Zero Boilerplate: No need to manually write gRPC service classes. Just decorate your CQRS requests.
- Automatic Mapping: Intelligently maps properties between your Protobuf-generated classes and your C# CQRS request/response DTOs.
- Built-in Error Handling: Automatically translates common exceptions (like
ValidationException) into appropriate gRPC status codes. - Seamless Integration: Works perfectly with the local
IMediatorto execute your existing handlers.
How It Works
The gRPC Kit consists of two parts: a Server Generator and a Client Generator.
Server-Side: You decorate a CQRS request class with the
[GrpcEndpoint]attribute. You specify which gRPC service (from your.protofile) and method this request corresponds to. The source generator then creates a complete gRPC service class implementation (LinKit...Service) that:- Inherits from your Protobuf-generated service base class (e.g.,
UserServiceBase). - Overrides the specified method.
- Receives the gRPC request, maps it to your CQRS request.
- Calls the local
IMediatorto execute the corresponding handler. - Maps the CQRS response back to the gRPC response.
- Handles exceptions gracefully.
- Inherits from your Protobuf-generated service base class (e.g.,
Client-Side: (See next section for details) You decorate the same CQRS request with
[GrpcClient]. This generates anIGrpcMediatorimplementation that allows you to send the CQRS request from a client application, which then makes the gRPC call transparently.
Server-Side Usage
Step 1: Define your .proto file
First, define your gRPC service and messages as you normally would.
Example users.proto:
syntax = "proto3";
option csharp_namespace = "MyCompany.Grpc.Contracts";
package users;
service UserService {
rpc GetUserById (GetUserByIdRequest) returns (GetUserByIdResponse);
rpc CreateUser (CreateUserRequest) returns (CreateUserResponse);
}
message GetUserByIdRequest {
int32 user_id = 1;
}
message UserDtoMessage {
int32 id = 1;
string user_name = 2;
string email = 3;
}
message GetUserByIdResponse {
UserDtoMessage user = 1;
}
message CreateUserRequest {
string user_name = 1;
string email = 2;
}
message CreateUserResponse {
int32 new_user_id = 1;
}
Step 2: Create Corresponding CQRS Requests and Handlers
Create your CQRS queries, commands, and handlers as usual. Ensure the properties match the fields in your .proto messages for automatic mapping.
The Query:
// Features/Users/GetUserQuery.cs
public class GetUserQuery : IQuery<UserDto>
{
// Property "UserId" will be mapped from "user_id" in the proto
public int UserId { get; set; }
}
The Command:
// Features/Users/CreateUserCommand.cs
// This command returns the new user's ID
public class CreateUserCommand : ICommand<int>
{
public string UserName { get; set; }
public string Email { get; set; }
}
The DTO:
// Dtos/UserDto.cs
public class UserDto
{
public int Id { get; set; }
public string UserName { get; set; }
public string Email { get; set; }
}
(You would also have GetUserHandler and CreateUserHandler marked with [CqrsHandler])
Step 3: Decorate CQRS Requests with [GrpcEndpoint]
This is the key step. Add the [GrpcEndpoint] attribute to your CQRS request classes to link them to the gRPC methods defined in your .proto file.
Linking the Query:
using LinKit.Grpc;
using MyCompany.Grpc.Contracts; // Namespace from your .proto file
[GrpcEndpoint(typeof(UserService.UserServiceBase), "GetUserById")]
public class GetUserQuery : IQuery<UserDto>
{
public int UserId { get; set; }
}
typeof(UserService.UserServiceBase): The gRPC service base class generated byprotoc."GetUserById": The exact name of the RPC method in your service.
Linking the Command:
using LinKit.Grpc;
using MyCompany.Grpc.Contracts;
[GrpcEndpoint(typeof(UserService.UserServiceBase), "CreateUser")]
public class CreateUserCommand : ICommand<int>
{
public string UserName { get; set; }
public string Email { get; set; }
}
Step 4: Register and Map the gRPC Service
In your server's Program.cs, register the necessary LinKit services and map the generated gRPC service.
var builder = WebApplication.CreateBuilder(args);
// 1. Register standard LinKit CQRS and your handlers
builder.Services.AddLinKitCqrs();
// 2. Add gRPC core services
builder.Services.AddGrpc();
var app = builder.Build();
// 3. Map the source-generated gRPC service
// The name is always "LinKit" + [YourServiceName]
app.MapGrpcService<LinKitUserService>();
app.Run();
That's it! You now have a fully functional gRPC service. When a client calls the GetUserById method, the generated LinKitUserService will:
- Instantiate a
GetUserQuery. - Map
GetUserByIdRequest.UserIdtoGetUserQuery.UserId. - Call
_mediator.QueryAsync(theQuery). - Receive the
UserDtofrom yourGetUserHandler. - Map the
UserDtoto aGetUserByIdResponse. - Return the response to the client.
Automatic Mapping Conventions
LinKit automatically maps properties between your CQRS objects and Protobuf messages. It follows a simple, case-insensitive name matching rule. For Protobuf's snake_case convention, LinKit will correctly map user_id to a C# property named UserId.
This includes:
- Request Mapping:
GrpcRequest→CqrsRequest - Response Mapping:
CqrsResponse→GrpcResponse - Nested Lists: It can even map collections, like a
List<ProductDto>in C# to arepeated ProductMessagein Protobuf, as long as the item types have matching properties.
Client-Side Usage
(This section can be updated with the new IGrpcMediator logic once you've finalized it)
To call the gRPC service from another .NET application, you use the Client-Side generator.
Step 1: Decorate the SAME CQRS Request with [GrpcClient]
In your client project, reference the shared CQRS request classes and decorate them again, this time with [GrpcClient].
// In the Client Application project
using LinKit.Grpc;
using MyCompany.Grpc.Contracts;
[GrpcClient(typeof(UserService.UserClient), "GetUserByIdAsync")]
public class GetUserQuery : IQuery<UserDto>
{
public int UserId { get; set; }
}
typeof(UserService.UserClient): The gRPC client class generated byprotoc."GetUserByIdAsync": The name of the asynchronous client method.
Step 2: Register the gRPC Client Kit
In your client's Program.cs, configure the IGrpcMediator and provide a channel to the server.
var builder = WebApplication.CreateBuilder(args);
// 1. Register the generated IGrpcMediator
builder.Services.AddLinKitGrpcClient();
// 2. Register a way to get the gRPC channel
// This is a simple example. You can implement IGrpcChannelProvider
// for more complex scenarios (e.g., service discovery).
builder.Services.AddSingleton<IGrpcChannelProvider>(
new DefaultGrpcChannelProvider("https://localhost:7001") // Address of your gRPC server
);
Step 3: Use IGrpcMediator
Inject IGrpcMediator and use it just like the local IMediator. The API is identical.
public class MyFrontendService
{
private readonly IGrpcMediator _grpcMediator;
public MyFrontendService(IGrpcMediator grpcMediator)
{
_grpcMediator = grpcMediator;
}
public async Task<UserDto> GetUserFromRemoteService(int id)
{
var query = new GetUserQuery { UserId = id };
// This looks like a local call, but it's making a gRPC request!
return await _grpcMediator.QueryAsync(query);
}
}
AOT & Trimming
LinKit is fully compatible with NativeAOT and trimming. For best results, use System.Text.Json source generation for DTOs and messages.
Advanced Configuration
- All
AddLinKit...()methods are additive and can be combined. - No manual registration of handlers or mappings is needed.
- For custom mapping, use the Mapping Kit.
- For custom gRPC channel, implement
IGrpcChannelProvider.
Contributing
Contributions, issues, and feature requests are welcome!
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net8.0 is compatible. net8.0-android was computed. net8.0-browser was computed. net8.0-ios was computed. net8.0-maccatalyst was computed. net8.0-macos was computed. net8.0-tvos was computed. net8.0-windows was computed. net9.0 is compatible. net9.0-android was computed. net9.0-browser was computed. net9.0-ios was computed. net9.0-maccatalyst was computed. net9.0-macos was computed. net9.0-tvos was computed. net9.0-windows was computed. net10.0 is compatible. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
-
net10.0
- No dependencies.
-
net8.0
- No dependencies.
-
net9.0
- No dependencies.
NuGet packages (4)
Showing the top 4 NuGet packages that depend on LinKit.Core:
| Package | Downloads |
|---|---|
|
LinKit.Grpc
gRPC server/client code generation for CQRS requests in LinKit. High-performance, source-generated, AOT/Trimming safe. |
|
|
O24OpenAPI.Framework
Package Description |
|
|
LinKit.Messaging.Kafka
Apache Kafka implementation for LinKit.Messaging abstractions. |
|
|
LinKit.Messaging.RabbitMQ
RabbitMQ implementation for LinKit.Messaging abstractions. |
GitHub repositories
This package is not used by any popular GitHub repositories.
| Version | Downloads | Last Updated |
|---|---|---|
| 2.2.3 | 0 | 4/2/2026 |
| 2.2.0 | 36 | 4/1/2026 |
| 2.1.9 | 80 | 3/26/2026 |
| 2.1.8 | 109 | 3/15/2026 |
| 2.1.7 | 95 | 3/14/2026 |
| 2.1.6 | 143 | 2/21/2026 |
| 2.1.5 | 136 | 1/13/2026 |
| 2.1.4 | 116 | 1/12/2026 |
| 2.1.3 | 124 | 1/12/2026 |
| 2.1.2 | 132 | 1/10/2026 |
| 2.1.1 | 222 | 12/31/2025 |
| 2.1.0 | 321 | 12/25/2025 |
| 2.0.9 | 304 | 12/24/2025 |
| 2.0.8 | 309 | 12/23/2025 |
| 2.0.7 | 294 | 12/20/2025 |
| 2.0.6 | 298 | 12/20/2025 |
| 2.0.5 | 369 | 12/15/2025 |
| 2.0.4 | 283 | 12/14/2025 |