interconnect (Micro-)services in .NET with ease
# for shared libraries
dotnet add package Core.ServiceMesh.Abstractions
dotnet add package Core.ServiceMesh.SourceGen
# for containers
dotnet add package Core.ServiceMesh
dotnet add package Core.ServiceMesh.SourceGen
- based on https://nats.io
- uses source generators for remote and telemetry proxies
- service request reponse pattern (sync)
- strongly typed clients out of the box
- response streaming with IAsyncEnumerable
- event streaming via NATS JetStream (async)
- durable and transient consumers
- open telemetry support
- supports local service traces in "AutoTrace" mode
builder.AddServiceMesh(options =>
{
options.ConfigureNats = opts => opts with
{
Url = "nats://localhost:4222"
};
options.ConfigureStream = (name, config) =>
{
config.MaxAge = TimeSpan.FromDays(1);
};
options.InterfaceMode = ServiceInterfaceMode.Auto;
options.Assemblies = [typeof(ISomeService).Assembly, typeof(SomeService).Assembly];
});
- service interfaces/contracts should be placed in abstraction libraries to be shared among your microservices
- only ValueTask and ValueTask<T> and IAsyncEnumerable supported
- open generics with optional constraints are supported
[ServiceMesh("someservice")]
public interface ISomeService
{
ValueTask<string> GetSomeString(int a, string b);
ValueTask CreateSomeObject();
ValueTask<T> GenericAdd<T>(T a, T b) where T : INumber<T>;
IAsyncEnumerable<string> StreamResponse();
};
[ServiceMesh]
public class SomeService(ILogger<SomeService> logger) : ISomeService
{
public async ValueTask<string> GetSomeString(int a, string b)
{
await Task.Delay(100);
return b + " " + a;
}
public async ValueTask CreateSomeObject()
{
await Task.Delay(100);
logger.LogInformation(nameof(CreateSomeObject));
}
public async ValueTask<T> GenericAdd<T>(T a, T b) where T : INumber<T>
{
await Task.Delay(100);
return a + b;
}
public async IAsyncEnumerable<string> StreamResponse()
{
await Task.Delay(100);
yield return "a";
await Task.Delay(100);
yield return "b";
await Task.Delay(100);
yield return "c";
}
}
- inject service interface into your controllers/services
- they are automatically proxied over nats when not available in the same container
public class DevController(ISomeService someService) : ControllerBase
{
[HttpPost("add-ints")]
public async Task<ActionResult<int>> CreateIntObject([FromQuery] int a = 3, [FromQuery] int b = 5)
{
return await someService.GenericAdd(a, b);
}
[HttpPost("add-doubles")]
public async Task<ActionResult<double>> CreateDoubleObject([FromQuery] double a = 3.1, [FromQuery] double b = 5.1)
{
return await someService.GenericAdd(a, b);
}
}
- durable consumers need to have a unique name (so you can rename your class later on)
- PublishAsync will await confirmation by nats broker
- SendAsync means Fire and Forget
public record SomeCommand(string Name);
[DurableConsumer("SomeCommandHandler", "default")]
public class SomeCommandHandler(ILogger<SomeCommandHandler> logger) : IConsumer<SomeCommand>
{
public ValueTask ConsumeAsync(SomeCommand message, CancellationToken token)
{
// do stuff
return ValueTask.CompletedTask;
}
}
public class DevController(IServiceMesh mesh) : ControllerBase
{
[HttpPost("publish")]
public async Task<IActionResult> Publish([FromQuery] string message)
{
await mesh.PublishAsync(new SomeCommand(message));
await mesh.SendAsync(new SomeCommand(message));
return Ok();
}
}
- to lazy to write controllers?
- services and consumer messages may be exposed directly via http endpoints
- for services only methods with a single complex parameter are supported
- no generics
- no simple types
- for this example types ending with ..Command or ..Message will be exposed as endpoints
app.MapServiceMesh(["Command", "Message"]);
builder.AddServiceMesh(options =>
{
options.MapHttpPublishRoute =
(app, type, handler) =>
{
app.MapPost("/api/publish/" + type.Name, handler)
.WithTags("publish");
};
options.MapHttpSendRoute =
(app, type, handler) =>
{
// no handlers without nats ack
//app.MapPost("/api/send/" + type.Name, handler).WithTags("send");
};
options.MapHttpRequestRoute = { get; set; } =
(app, requestType, responseType, service, method, handler) =>
{
app.MapPost("/api/" + service + "/" + method.Name, handler)
.Produces(200, responseType)
.WithTags(service);
};
});