Skip to content

Commit

Permalink
Added the initial version of the endpoints API. (#2)
Browse files Browse the repository at this point in the history
* Added endpoint auto mapping.

* Implemented grouping and naming by convention.

* Added options to be able to configure the endpoints global prefix.

* Added unit tests.

* Removed dependency on OpenAPI package. This can now be configured from the client.

* Added docs.
  • Loading branch information
mgernand authored Nov 17, 2023
1 parent 648fd0e commit 2b542de
Show file tree
Hide file tree
Showing 34 changed files with 1,195 additions and 77 deletions.
86 changes: 86 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,89 @@
# AspNetCore.Endpoints

A library that helps in building and configuring object-oriented minimal API endpoints.

Mapping every single minimal API endpoint in the ```Program.cs``` file can become
confusing very fast. For applications hosting a larger amount of endpoints this
library allows to implement an endpoint in a structured way. This endpoints will
then be automatically mapped, removing clutter from the ```Program.cs``` file.

Everything related to and endpoint like the mapping and the implementation itself
is encapsulated in a single class. The library offers a default way of naming groups
and endpoints. The default convention for the group name an endpoint belongs to is
the last part of the namespace the endpoint belongs to. The default convention for
the endppoint name id the class name of the endpoint.

This default conventions can be overridden by using attributes athe endpoints class level.

- ```[EndpointGroupName("GroupName")]```

The name of the group this endpoint belongs to.

- ```[EndpointName("SomeOtherName")]```

The name of the endpoint.

## Endpoints Usage

Every endpoint is implemented in it's own class, deriving from ```EndpointBase```.
Endpoints are discovered from the available types using this base class.

```C#

public sealed class Get : EndpointBase
{
/// <inheritdoc />
public override void Map(IEndpointRouteBuilder endpoints)
{
endpoints
.MapGet(this.Execute, "{id}")
.AllowAnonymous()
.Produces<Customer>(200, "application/json");
}

public async Task<IResult> Execute(HttpContext httpContext, string id)
{
return Results.Ok(new Customer
{
Name = "John Connor"
});
}
}

```

The mapping and configuration of additional meta data configuration of the endpoint is done
in the ```Map``` method. The actual endpoint implementation is done in the ```Execute``` method.
The name of the method free to choose, it doesn't affect the mapping of the endpoint in any way.

To allow the endpoint beeing automatically mapped one has to add this to the ```Program.cs```:

```C#

app.MapEndpoints();

```

This it!

## Additional Configuration

The endpoints are by default mapped under a global route prefix ```api```. To change this
default value, one can configure the ```EndpointsOptions``` when configuring the application.

```C#

builder.Services.Configure<EndpointsOptions>(options =>
{
options.EndpointsRoutePrefix = "endpoints";
options.MapGroup = groupBuilder =>
{
groupBuilder.WithOpenApi();
};
});

```

In this exampple the global route prefix for all enpoints is changed to ```endpoints``` and
an additional endpoint group configuration is added using the ```MapGroup``` callback,
which is called for every endpoint group.
13 changes: 13 additions & 0 deletions samples/SampleApplication/Endpoints/Customers/Customer.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
namespace SampleApplication.Endpoints.Customers
{
using JetBrains.Annotations;

[PublicAPI]
public class Customer
{
/// <summary>
/// Gets or sets the name.
/// </summary>
public string Name { get; set; }
}
}
25 changes: 25 additions & 0 deletions samples/SampleApplication/Endpoints/Customers/GetCustomer.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
namespace SampleApplication.Endpoints.Customers
{
using JetBrains.Annotations;
using MadEyeMatt.AspNetCore.Endpoints;

[PublicAPI]
public sealed class GetCustomer : EndpointBase
{
/// <inheritdoc />
public override void Map(IEndpointRouteBuilder endpoints)
{
endpoints
.MapGet(this.Execute, "{id}")
.Produces<Customer>(200, "application/json");
}

public async Task<IResult> Execute(HttpContext httpContext, string id)
{
return Results.Ok(new Customer
{
Name = "John Connor"
});
}
}
}
32 changes: 32 additions & 0 deletions samples/SampleApplication/Endpoints/Customers/GetCustomers.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
namespace SampleApplication.Endpoints.Customers
{
using JetBrains.Annotations;
using MadEyeMatt.AspNetCore.Endpoints;

[PublicAPI]
public sealed class GetCustomers : EndpointBase
{
/// <inheritdoc />
public override void Map(IEndpointRouteBuilder endpoints)
{
endpoints
.MapGet(this.Execute)
.Produces<Customer>(200, "application/json");
}

public async Task<IResult> Execute(HttpContext httpContext)
{
return Results.Ok(new Customer[]
{
new Customer
{
Name = "John Connor"
},
new Customer
{
Name = "Sarah Connor"
}
});
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
namespace SampleApplication.Endpoints.WeatherForecast
{
using JetBrains.Annotations;
using MadEyeMatt.AspNetCore.Endpoints;

[PublicAPI]
[EndpointName("SomeOtherName")]
[EndpointGroupName("weather_forecast")]
public sealed class GetWeatherForecasts : EndpointBase
{
private string[] summaries = new string[]
{
"Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching"
};

/// <inheritdoc />
public override void Map(IEndpointRouteBuilder endpoints)
{
endpoints.MapGet(this.Execute);
}

public async Task<IEnumerable<WeatherForecast>> Execute(HttpContext httpContext)
{
WeatherForecast[] forecast = Enumerable.Range(1, 5).Select(index =>
new WeatherForecast
{
Date = DateOnly.FromDateTime(DateTime.Now.AddDays(index)),
TemperatureC = Random.Shared.Next(-20, 55),
Summary = this.summaries[Random.Shared.Next(this.summaries.Length)]
})
.ToArray();

return forecast;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
namespace SampleApplication.Endpoints.WeatherForecast
{
using JetBrains.Annotations;

[PublicAPI]
public class WeatherForecast
{
public DateOnly Date { get; set; }

public int TemperatureC { get; set; }

public int TemperatureF => 32 + (int)(this.TemperatureC / 0.5556);

public string? Summary { get; set; }
}
}
39 changes: 16 additions & 23 deletions samples/SampleApplication/Program.cs
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@

namespace SampleApplication
{
public class Program
using MadEyeMatt.AspNetCore.Endpoints;
using Microsoft.Extensions.DependencyInjection;

public static class Program
{
public static void Main(string[] args)
{
var builder = WebApplication.CreateBuilder(args);
WebApplicationBuilder builder = WebApplication.CreateBuilder(args);

// Add services to the container.
builder.Services.AddAuthorization();
Expand All @@ -14,7 +16,16 @@ public static void Main(string[] args)
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();

var app = builder.Build();
builder.Services.Configure<EndpointsOptions>(options =>
{
options.EndpointsRoutePrefix = "endpoints";
options.MapGroup = groupBuilder =>
{
groupBuilder.WithOpenApi();
};
});

WebApplication app = builder.Build();

// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
Expand All @@ -27,25 +38,7 @@ public static void Main(string[] args)

app.UseAuthorization();

var summaries = new[]
{
"Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching"
};

app.MapGet("/weatherforecast", (HttpContext httpContext) =>
{
var forecast = Enumerable.Range(1, 5).Select(index =>
new WeatherForecast
{
Date = DateOnly.FromDateTime(DateTime.Now.AddDays(index)),
TemperatureC = Random.Shared.Next(-20, 55),
Summary = summaries[Random.Shared.Next(summaries.Length)]
})
.ToArray();
return forecast;
})
.WithName("GetWeatherForecast")
.WithOpenApi();
app.MapEndpoints();

app.Run();
}
Expand Down
30 changes: 2 additions & 28 deletions samples/SampleApplication/Properties/launchSettings.json
Original file line number Diff line number Diff line change
@@ -1,38 +1,12 @@
{
"$schema": "http://json.schemastore.org/launchsettings.json",
"iisSettings": {
"windowsAuthentication": false,
"anonymousAuthentication": true,
"iisExpress": {
"applicationUrl": "http://localhost:44986",
"sslPort": 44356
}
},
"profiles": {
"http": {
"SampleApplication": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": true,
"launchUrl": "swagger",
"applicationUrl": "http://localhost:5151",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"https": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": true,
"launchUrl": "swagger",
"applicationUrl": "https://localhost:7089;http://localhost:5151",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"IIS Express": {
"commandName": "IISExpress",
"launchBrowser": true,
"launchUrl": "swagger",
"applicationUrl": "https://localhost:5001;http://localhost:5000",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
Expand Down
26 changes: 15 additions & 11 deletions samples/SampleApplication/SampleApplication.csproj
Original file line number Diff line number Diff line change
@@ -1,15 +1,19 @@
<Project Sdk="Microsoft.NET.Sdk.Web">

<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<InvariantGlobalization>true</InvariantGlobalization>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="8.0.0" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.4.0" />
</ItemGroup>
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<InvariantGlobalization>true</InvariantGlobalization>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="8.0.0" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.4.0" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\..\src\AspNetCore.Endpoints\AspNetCore.Endpoints.csproj" />
</ItemGroup>

</Project>
9 changes: 7 additions & 2 deletions samples/SampleApplication/SampleApplication.http
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
@SampleApplication_HostAddress = http://localhost:5151
@SampleApplication_HostAddress = https://localhost:5001/api

GET {{SampleApplication_HostAddress}}/weatherforecast/
GET {{SampleApplication_HostAddress}}/weather_forecast
Accept: application/json

###

GET {{SampleApplication_HostAddress}}/customers
Accept: application/json

###
13 changes: 0 additions & 13 deletions samples/SampleApplication/WeatherForecast.cs

This file was deleted.

4 changes: 4 additions & 0 deletions src/AspNetCore.Endpoints/AspNetCore.Endpoints.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,10 @@
<PackageIcon>icon.png</PackageIcon>
</PropertyGroup>

<ItemGroup>
<FrameworkReference Include="Microsoft.AspNetCore.App" />
</ItemGroup>

<ItemGroup>
<None Include="..\..\icon.png" Link="Properties\icon.png">
<Pack>True</Pack>
Expand Down
Loading

0 comments on commit 2b542de

Please sign in to comment.