ZedEndpoints 1.1.2
dotnet add package ZedEndpoints --version 1.1.2
NuGet\Install-Package ZedEndpoints -Version 1.1.2
<PackageReference Include="ZedEndpoints" Version="1.1.2" />
<PackageVersion Include="ZedEndpoints" Version="1.1.2" />
<PackageReference Include="ZedEndpoints" />
paket add ZedEndpoints --version 1.1.2
#r "nuget: ZedEndpoints, 1.1.2"
#:package ZedEndpoints@1.1.2
#addin nuget:?package=ZedEndpoints&version=1.1.2
#tool nuget:?package=ZedEndpoints&version=1.1.2
ZedEndpoints
A minimal library for automatic endpoint discovery and organization in ASP.NET Core Minimal APIs.
Why ZedEndpoints
Carter and similar libraries rely on reflection-based discovery that requires
public classes. This prevents using C#'s file and internal modifiers,
which are essential for proper encapsulation in feature-based architectures.
ZedEndpoints solves this by combining explicit registration within groups and automatic group discovery — giving you encapsulation without sacrificing convenience.
// Carter — requires public
public sealed class CreateUserEndpoint : ICarterModule { ... }
// ZedEndpoints — file modifier works
file sealed class CreateUserEndpoint : IEndpoint { ... }
Features
- Automatic endpoint discovery via reflection
- Group-based endpoint organization with shared configuration
- Optional global route prefix for all endpoint groups
- Per-group prefix override via
[NoGlobalPrefix]attribute - Type-safe interfaces and generics
- Fluent API with method chaining support
- Idempotent operations (safe to call multiple times)
- Thread-safe implementation
Installation
dotnet add package ZedEndpoints
Quick Start
Define an Endpoint
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Routing;
using ZedEndpoints.Abstractions;
public class GetProductEndpoint : IEndpoint
{
public static void Map(IEndpointRouteBuilder app)
{
app.MapGet("/products/{id}", (int id) =>
{
return Results.Ok(new { Id = id, Name = "Product" });
})
.WithName("GetProduct")
.WithTags("Products");
}
}
Create an Endpoint Group
using Microsoft.AspNetCore.Routing;
using ZedEndpoints.Abstractions;
using ZedEndpoints.Extensions;
public class ProductEndpoints : IEndpointGroup
{
public void MapGroup(IEndpointRouteBuilder app)
{
var group = app.MapGroup("/products")
.WithTags("Products")
.RequireAuthorization();
group.MapEndpoint<GetProductEndpoint>()
.MapEndpoint<CreateProductEndpoint>()
.MapEndpoint<UpdateProductEndpoint>()
.MapEndpoint<DeleteProductEndpoint>();
}
}
Register in Program.cs
using ZedEndpoints.Extensions;
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
// Automatic discovery and registration
app.MapEndpointGroups();
app.Run();
Usage Patterns
Manual Endpoint Registration
app.MapEndpoint<GetProductEndpoint>();
Manual Group Registration
var productGroup = new ProductEndpoints();
productGroup.MapGroup(app);
Working with Route Groups
var apiV1 = app.MapGroup("/api/v1");
apiV1.MapEndpoint<GetProductEndpoint>();
Scanning Specific Assembly
app.MapEndpointGroups(typeof(ProductEndpoints).Assembly);
Global Route Prefix
Apply a common prefix to all discovered endpoint groups at once:
// All routes will be prefixed with /api/v1
app.MapEndpointGroups(globalPrefix: "api/v1");
// Combined with a specific assembly
app.MapEndpointGroups(typeof(ProductEndpoints).Assembly, globalPrefix: "api/v1");
This is useful for applying a consistent API versioning prefix without modifying each group individually. For example, a group that maps /products will automatically become /api/v1/products.
Opting Out of the Global Prefix
Use the [NoGlobalPrefix] attribute on a group to bypass the global prefix:
using ZedEndpoints.Attributes;
[NoGlobalPrefix]
public class HealthEndpoints : IEndpointGroup
{
public void MapGroup(IEndpointRouteBuilder app)
{
app.MapGet("/health", () => Results.Ok())
.WithName("HealthCheck");
}
}
With a global prefix of api/v1, all other groups will be prefixed normally while HealthEndpoints will remain at /health.
Architecture
IEndpoint Interface
Defines a single endpoint with a static mapping method.
public interface IEndpoint
{
static abstract void Map(IEndpointRouteBuilder app);
}
IEndpointGroup Interface
Defines a group of related endpoints with shared configuration.
public interface IEndpointGroup
{
void MapGroup(IEndpointRouteBuilder app);
}
NoGlobalPrefix Attribute
Marks an endpoint group to be excluded from the global route prefix.
[AttributeUsage(AttributeTargets.Class, Inherited = false)]
public sealed class NoGlobalPrefixAttribute : Attribute;
Best Practices
Endpoint Organization
Organize endpoints by feature or domain area:
Features/
├── Products/
│ ├── GetProduct.cs
│ ├── CreateProduct.cs
│ └── ProductEndpoints.cs
├── Orders/
│ ├── GetOrder.cs
│ ├── CreateOrder.cs
│ └── OrderEndpoints.cs
Naming Conventions
- Endpoints:
{Verb}{Resource}Endpoint(e.g.,GetProductEndpoint) - Groups:
{Resource}Endpoints(e.g.,ProductEndpoints)
Shared Configuration
Apply common settings at the group level:
public class ProductEndpoints : IEndpointGroup
{
public void MapGroup(IEndpointRouteBuilder app)
{
var group = app.MapGroup("/products")
.WithTags("Products")
.RequireAuthorization()
.RequireRateLimiting("fixed")
.WithOpenApi();
group.MapEndpoint<GetProductEndpoint>()
.MapEndpoint<CreateProductEndpoint>();
}
}
API Versioning with Global Prefix
Use the global prefix for versioning instead of repeating it in every group:
// Instead of hardcoding /api/v1 in every IEndpointGroup
app.MapEndpointGroups(globalPrefix: "api/v1");
For groups that should not follow the versioning scheme, such as health checks or webhooks, use [NoGlobalPrefix]:
[NoGlobalPrefix]
public class HealthEndpoints : IEndpointGroup { ... }
[NoGlobalPrefix]
public class WebhookEndpoints : IEndpointGroup { ... }
How It Works
Automatic Discovery
The MapEndpointGroups() extension method:
- Scans the specified assembly (or entry assembly by default)
- Finds all concrete classes implementing
IEndpointGroup - Creates instances using parameterless constructors
- Checks for the
[NoGlobalPrefix]attribute on each group - Invokes their
MapGroup()methods with the appropriate route builder
Global Prefix
When a globalPrefix is provided, a root RouteGroupBuilder is created and passed to all endpoint groups instead of the WebApplication directly. Both implement IEndpointRouteBuilder, so existing group implementations require no changes. Groups decorated with [NoGlobalPrefix] always receive the WebApplication directly, bypassing the prefix entirely.
Idempotency
The library prevents duplicate registrations:
app.MapEndpointGroups(); // Registers all endpoint groups
app.MapEndpointGroups(); // Safe: does nothing (already registered)
This is achieved through assembly tracking:
- Each assembly is processed only once
- Thread-safe using lock mechanism
- Prevents accidental duplicate endpoint registration
Integration Testing with WebApplicationFactory
When using WebApplicationFactory<T> for integration or acceptance tests, the default assembly resolution via Assembly.GetEntryAssembly() may return the test runner assembly instead of your application assembly. This means no endpoints will be discovered.
This is handled automatically since v1.1.1 — the library now uses app.Environment.ApplicationName to resolve the correct assembly, which works seamlessly with WebApplicationFactory.
If you're on an older version, pass the assembly explicitly:
app.MapEndpointGroups(typeof(Program).Assembly, globalPrefix: "api/v1");
Requirements
- .NET 8.0 or higher
- ASP.NET Core
License
MIT
Contributing
Contributions are welcome. Please open an issue or submit a pull request.
Support
For issues and questions, please use the GitHub issue tracker.
| 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
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.