Skip to content

Commit

Permalink
Implemented a base type routing convention and updated examples to de…
Browse files Browse the repository at this point in the history
…monstrate this as well as added swashbuckle.
  • Loading branch information
Jonathan Swieboda committed Mar 21, 2020
1 parent 7cb3682 commit e4854a5
Show file tree
Hide file tree
Showing 14 changed files with 274 additions and 121 deletions.
75 changes: 65 additions & 10 deletions src/SimpleEndPoints/Conventions/EndpointRoutingConvention.cs
Original file line number Diff line number Diff line change
@@ -1,22 +1,77 @@
using Microsoft.AspNetCore.Mvc.ApplicationModels;
using System;
using System.Linq;
using System.Text;
using Microsoft.AspNetCore.Mvc.ApplicationModels;
using Microsoft.AspNetCore.Routing;
using SimpleEndpoints.Extensions;
using SimpleEndpoints.VerbScoped;

namespace SimpleEndpoints.Conventions
{
public class EndpointRoutingConvention: IApplicationModelConvention
public class EndpointRoutingConvention : IApplicationModelConvention
{
private const string EndpointString = "endpoint";
private const string EndpointPlaceholder = "[endpoint]";

private readonly SimpleEndpointsConfiguration _configuration;

public EndpointRoutingConvention(SimpleEndpointsConfiguration configuration)
{
_configuration = configuration;
}

public void Apply(ApplicationModel application)
{
foreach (var controller in application.Controllers)
{
controller.Selectors[0].AttributeRouteModel.Template = controller
.Selectors[0]
.AttributeRouteModel
.Template
.Replace($"[{EndpointString}]",
controller.ControllerName.Substring(0,
controller.ControllerName.Length - EndpointString.Length));
MapRouteConvention(controller);
}
}

private void MapRouteConvention(ControllerModel controller)
{
var routeTemplate = controller.Selectors[0].AttributeRouteModel.Template;

//If we haven't set a route with [Route("")] on our endpoint
if (routeTemplate.Equals(EndpointPlaceholder, StringComparison.OrdinalIgnoreCase))
{
var routeBuilder = new StringBuilder();

//Do we want to set a global route prefix e.g. api/*?
if (!string.IsNullOrWhiteSpace(_configuration.RoutePrefix))
{
routeBuilder.Append($"{_configuration.RoutePrefix}/");
}

//Do a replacement on the Endpoint name e.g. MyGreatEndpoint to just MyGreat
routeBuilder.Append(routeTemplate.Replace($"{EndpointPlaceholder}",
controller.ControllerName.Replace(_configuration.EndpointReplacementToken, string.Empty)));

controller.Selectors[0].AttributeRouteModel.Template = routeBuilder.ToString();
}

AddHttpMethodMetadata(controller);
}

//This allows us to get away from having to add Http verb attributes to our action methods, it can be driven by base class
private static void AddHttpMethodMetadata(ControllerModel controller)
{
var controllerInterfaces = controller.ControllerType.ImplementedInterfaces.ToArray();

if (controllerInterfaces.Contains(typeof(IDeleteEndpoint)))
{
controller.Selectors[0].EndpointMetadata.Add(new HttpMethodMetadata(new[] {"DELETE"}));
}
else if (controllerInterfaces.Contains(typeof(IGetEndpoint)))
{
controller.Selectors[0].EndpointMetadata.Add(new HttpMethodMetadata(new[] {"GET"}));
}
else if (controllerInterfaces.Contains(typeof(IPostEndpoint)))
{
controller.Selectors[0].EndpointMetadata.Add(new HttpMethodMetadata(new[] {"POST"}));
}
else if (controllerInterfaces.Contains(typeof(IPutEndpoint)))
{
controller.Selectors[0].EndpointMetadata.Add(new HttpMethodMetadata(new[] {"PUT"}));
}
}
}
Expand Down
89 changes: 86 additions & 3 deletions src/SimpleEndPoints/Extensions/MvcOptionsExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,98 @@
using System.Collections.Generic;
using System.Text;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.ApiExplorer;
using Microsoft.AspNetCore.Mvc.Controllers;
using Microsoft.Extensions.DependencyInjection;
using SimpleEndpoints.Conventions;
using SimpleEndpoints.VerbScoped;

namespace SimpleEndpoints.Extensions
{
public static class MvcOptionsExtensions
{
public static void AddEndpointRoutingConvention(this MvcOptions options)
public static void WithSimpleEndpoints(this MvcOptions options, Action<SimpleEndpointsConfiguration> configure = null)
{
options.Conventions.Add(new EndpointRoutingConvention());
var configuration = new SimpleEndpointsConfiguration();
configure?.Invoke(configuration);

options.Conventions.Add(new EndpointRoutingConvention(configuration));
}
}

public class SimpleEndpointsConfiguration
{
public string RoutePrefix { get; private set; }
public string EndpointReplacementToken { get; private set; }

public SimpleEndpointsConfiguration WithRoutePrefix(string prefix)
{
RoutePrefix = prefix;
return this;
}

public SimpleEndpointsConfiguration WithEndpointNamingConvention(string endpointReplacementToken = "Endpoint")
{
EndpointReplacementToken = endpointReplacementToken;
return this;
}
}

public static class ServiceCollectionExtensions
{
//We need to decorate the api description for swagger
public static IServiceCollection AddSimpleEndpointRouting(this IServiceCollection services)
{
services.AddSingleton<ApiDescriptionGroupCollectionProvider>();
services.AddSingleton<IApiDescriptionGroupCollectionProvider>(x =>
new ApiDescriptionGroupCollectionProviderDecorator(x.GetService<ApiDescriptionGroupCollectionProvider>()));

return services;
}
}

public class ApiDescriptionGroupCollectionProviderDecorator : IApiDescriptionGroupCollectionProvider
{
private readonly IApiDescriptionGroupCollectionProvider _inner;

public ApiDescriptionGroupCollectionProviderDecorator(IApiDescriptionGroupCollectionProvider inner)
{
_inner = inner;
}

public ApiDescriptionGroupCollection ApiDescriptionGroups
{
get
{
//As we are not using HttpVerb attributes for out actions so swagger doesn't know how to render it
foreach (var apiDescriptionGroup in _inner.ApiDescriptionGroups.Items)
{
foreach (var apiDescription in apiDescriptionGroup.Items)
{
if (apiDescription.ActionDescriptor is ControllerActionDescriptor controller)
{
if (typeof(IDeleteEndpoint).IsAssignableFrom(controller.ControllerTypeInfo))
{
apiDescription.HttpMethod = "DELETE";
}
if (typeof(IGetEndpoint).IsAssignableFrom(controller.ControllerTypeInfo))
{
apiDescription.HttpMethod = "GET";
}
if (typeof(IPostEndpoint).IsAssignableFrom(controller.ControllerTypeInfo))
{
apiDescription.HttpMethod = "POST";
}
if (typeof(IPutEndpoint).IsAssignableFrom(controller.ControllerTypeInfo))
{
apiDescription.HttpMethod = "PUT";
}
}
}
}

return _inner.ApiDescriptionGroups;
}
}
}
}
}
1 change: 1 addition & 0 deletions src/SimpleEndPoints/SimpleEndpoints.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Mvc.ApiExplorer" Version="2.2.0" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.Core" Version="2.2.5" />
</ItemGroup>

Expand Down
15 changes: 7 additions & 8 deletions src/SimpleEndPoints/VerbScoped/DeleteEndpoint.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,30 +5,29 @@

namespace SimpleEndpoints.VerbScoped
{
public interface IDeleteEndpoint
{
}

public abstract class AsyncDeleteEndpoint<TRequest> : AsyncEndpointWithRequest<TRequest>
public abstract class AsyncDeleteEndpoint<TRequest> : AsyncEndpointWithRequest<TRequest>, IDeleteEndpoint
{
[HttpDelete]
public abstract override Task<IActionResult> HandleAsync(TRequest model,
CancellationToken cancellationToken = default);
}

public abstract class AsyncDeleteEndpoint<TRequest, TResponse> : AsyncEndpoint<TRequest, TResponse>
public abstract class AsyncDeleteEndpoint<TRequest, TResponse> : AsyncEndpoint<TRequest, TResponse>, IDeleteEndpoint
{
[HttpDelete]
public abstract override Task<ActionResult<TResponse>> HandleAsync(TRequest model,
CancellationToken cancellationToken = default);
}

public abstract class DeleteEndpoint<TRequest> : EndpointWithRequest<TRequest>
public abstract class DeleteEndpoint<TRequest> : EndpointWithRequest<TRequest>, IDeleteEndpoint
{
[HttpDelete]
public abstract override IActionResult Handle(TRequest model);
}

public abstract class DeleteEndpoint<TRequest, TResponse> : Endpoint<TRequest, TResponse>
public abstract class DeleteEndpoint<TRequest, TResponse> : Endpoint<TRequest, TResponse>, IDeleteEndpoint
{
[HttpDelete]
public abstract override ActionResult<TResponse> Handle(TRequest model);
}
}
27 changes: 11 additions & 16 deletions src/SimpleEndPoints/VerbScoped/GetEndpoint.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,55 +5,50 @@

namespace SimpleEndpoints.VerbScoped
{
public interface IGetEndpoint
{
}

public abstract class AsyncGetEndpoint : AsyncEndpoint
public abstract class AsyncGetEndpoint : AsyncEndpoint, IGetEndpoint
{
[HttpGet]
public abstract override Task<IActionResult> HandleAsync(CancellationToken cancellationToken = default);
}

public abstract class AsyncGetEndpointWithRequest<TRequest> : AsyncEndpointWithRequest<TRequest>
public abstract class AsyncGetEndpointWithRequest<TRequest> : AsyncEndpointWithRequest<TRequest>, IGetEndpoint
{
[HttpGet]
public abstract override Task<IActionResult> HandleAsync([FromQuery] TRequest model,
CancellationToken cancellationToken = default);
}

public abstract class AsyncGetEndpoint<TResponse> : AsyncEndpoint<TResponse>
public abstract class AsyncGetEndpoint<TResponse> : AsyncEndpoint<TResponse>, IGetEndpoint
{
[HttpGet]
public abstract override Task<ActionResult<TResponse>> HandleAsync(
CancellationToken cancellationToken = default);
}

public abstract class AsyncGetEndpoint<TRequest, TResponse> : AsyncEndpoint<TRequest, TResponse>
public abstract class AsyncGetEndpoint<TRequest, TResponse> : AsyncEndpoint<TRequest, TResponse>, IGetEndpoint
{
[HttpGet]
public abstract override Task<ActionResult<TResponse>> HandleAsync([FromQuery] TRequest model,
CancellationToken cancellationToken = default);
}

public abstract class GetEndpoint : Endpoint
public abstract class GetEndpoint : Endpoint, IGetEndpoint
{
[HttpGet]
public abstract override IActionResult Handle();
}

public abstract class GetEndpoint<TResponse> : Endpoint<TResponse>
public abstract class GetEndpoint<TResponse> : Endpoint<TResponse>, IGetEndpoint
{
[HttpGet]
public abstract override ActionResult<TResponse> Handle();
}

public abstract class GetEndpointWithRequest<TRequest> : EndpointWithRequest<TRequest>
public abstract class GetEndpointWithRequest<TRequest> : EndpointWithRequest<TRequest>, IGetEndpoint
{
[HttpGet]
public abstract override IActionResult Handle([FromQuery] TRequest model);
}

public abstract class GetEndpoint<TRequest, TResponse> : Endpoint<TRequest, TResponse>
public abstract class GetEndpoint<TRequest, TResponse> : Endpoint<TRequest, TResponse>, IGetEndpoint
{
[HttpGet]
public abstract override ActionResult<TResponse> Handle([FromQuery] TRequest model);
}
}
Expand Down
17 changes: 8 additions & 9 deletions src/SimpleEndPoints/VerbScoped/PostEndpoint.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,29 +5,28 @@

namespace SimpleEndpoints.VerbScoped
{

public abstract class AsyncPostEndpoint<TRequest> : AsyncEndpointWithRequest<TRequest>
public interface IPostEndpoint
{
}

public abstract class AsyncPostEndpoint<TRequest> : AsyncEndpointWithRequest<TRequest>, IPostEndpoint
{
[HttpPost]
public abstract override Task<IActionResult> HandleAsync(TRequest model, CancellationToken cancellationToken = default);
}

public abstract class AsyncPostEndpoint<TRequest, TResponse> : AsyncEndpoint<TRequest, TResponse>
public abstract class AsyncPostEndpoint<TRequest, TResponse> : AsyncEndpoint<TRequest, TResponse>, IPostEndpoint
{
[HttpPost]
public abstract override Task<ActionResult<TResponse>> HandleAsync(TRequest model,
CancellationToken cancellationToken = default);
}

public abstract class PostEndpoint<TRequest> : EndpointWithRequest<TRequest>
public abstract class PostEndpoint<TRequest> : EndpointWithRequest<TRequest>, IPostEndpoint
{
[HttpPost]
public abstract override IActionResult Handle(TRequest model);
}

public abstract class PostEndpoint<TRequest, TResponse> : Endpoint<TRequest, TResponse>
public abstract class PostEndpoint<TRequest, TResponse> : Endpoint<TRequest, TResponse>, IPostEndpoint
{
[HttpPost]
public abstract override ActionResult<TResponse> Handle(TRequest model);
}
}
25 changes: 12 additions & 13 deletions src/SimpleEndPoints/VerbScoped/PutEndpoint.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,30 +5,29 @@

namespace SimpleEndpoints.VerbScoped
{

public abstract class AsyncPutEndpoint<TRequest> : AsyncEndpointWithRequest<TRequest>
public interface IPutEndpoint
{
}

public abstract class AsyncPutEndpoint<TRequest> : AsyncEndpointWithRequest<TRequest>, IPutEndpoint
{
[HttpPut]
public abstract override Task<IActionResult> HandleAsync(TRequest model,
CancellationToken cancellationToken = default);
}
public abstract class AsyncPutEndpoint<TRequest, TResponse> : AsyncEndpoint<TRequest, TResponse>

public abstract class AsyncPutEndpoint<TRequest, TResponse> : AsyncEndpoint<TRequest, TResponse>, IPutEndpoint
{
[HttpPut]
public abstract override Task<ActionResult<TResponse>> HandleAsync(TRequest model,
CancellationToken cancellationToken = default);
}
public abstract class PutEndpoint<TRequest> : EndpointWithRequest<TRequest>

public abstract class PutEndpoint<TRequest> : EndpointWithRequest<TRequest>, IPutEndpoint
{
[HttpPut]
public abstract override IActionResult Handle(TRequest model);
}
public abstract class PutEndpoint<TRequest, TResponse> : Endpoint<TRequest, TResponse>

public abstract class PutEndpoint<TRequest, TResponse> : Endpoint<TRequest, TResponse>, IPutEndpoint
{
[HttpPut]
public abstract override ActionResult<TResponse> Handle(TRequest model);
}
}
}
Loading

0 comments on commit e4854a5

Please sign in to comment.