I'm currently building a simple solution that will have a few simple web APIs built using ASP.Net Core. The APIs are not going to be fully fledged REST APIs according to Richardson's maturity model. So there will not be any HATEOAS with resource linking etc. Each API will most likely have one or two endpoints. I want the root endpoint to provide some simple discovery info and not extensive Swagger enabled API docs but info like: deployed version, current time and available routes. This endpoint is used for simple discovery purposes and checking if a service is alive with e.g. simple smoke tests after deploy etc.
Please remember. This is very early sample code and just something for you to get inspired by.
Sample result
{
"version": "1.0.0.0",
"time": "2017-10-29T09:27:50.5469861Z",
"routes": [
{
"name": "UploadFile",
"template": "Uploads/{year:range(2000, 2100)}/{month:range(1, 12)}/{context:minlength(2)}",
"httpMethods": [
"POST"
]
}
]
}
Middle-ware
Even though this creates coupling between my services, in the form of shared infrastructure code, I've decided to expose this as a middle-ware hook:
app.MapWhen(ctx => ctx.Request.Path == "/", c => c.UseApiInfo());
The middle-ware is very simple. It will only support JSON as output. I could have used ObjectResult and used content-negotiation, but my design decision for these internal services has been to just use JSON for it.
public static class ApiInfoMiddlewareExtensions
{
public static IApplicationBuilder UseApiInfo(this IApplicationBuilder builder)
=> builder.UseMiddleware<ApiInfoMiddleware>();
}
public class ApiInfoMiddleware
{
private readonly IApiInfoProvider _provider;
private readonly JsonSerializerSettings _jsonSerializerSettings;
public ApiInfoMiddleware(
RequestDelegate _,
IApiInfoProvider provider,
JsonSerializerSettings jsonSerializerSettings)
{
_provider = provider;
_jsonSerializerSettings = jsonSerializerSettings;
}
public async Task Invoke(HttpContext httpContext)
{
var response = _provider.Get();
httpContext.Response.ContentType = "application/json";
await httpContext.Response.WriteAsync(
JsonConvert.SerializeObject(response, _jsonSerializerSettings));
}
}
By specifying the dependencies in the constructor I get support for the wired up dependency injection. This will be a terminating middle-ware, hence why next
is not being used.
ApiInfoProvider
There's one simple implementation of an IApiInfoProvider
, that produces a simple response that the middle-ware just serializes to JSON.
public class ApiInfoProvider : IApiInfoProvider
{
private readonly ApiVersion _version;
private readonly Func<DateTime> _timeFn;
private readonly Lazy<IRouteInfo[]> _routeInfoFn;
public ApiInfoProvider(ApiVersion version, IClock clock, IRouteInfoProvider routeInfoProvider)
{
_version = version;
_timeFn = () => clock.Now;
_routeInfoFn = new Lazy<IRouteInfo[]>(routeInfoProvider.Get);
}
public IApiInfo Get() => new ApiInfo(
_version,
_timeFn(),
_routeInfoFn.Value);
private class ApiInfo : IApiInfo
{
public string Version { get; }
public DateTime Time { get; }
public IRouteInfo[] Routes { get; }
public ApiInfo(string version, DateTime time, IRouteInfo[] routes)
{
Version = version;
Time = time;
Routes = routes;
}
}
}
public class ApiVersion
{
private readonly string _version;
public ApiVersion(string version)
{
_version = version;
}
public static implicit operator string(ApiVersion v) => v.ToString();
public override string ToString() => _version;
}
The dependencies are registered in the IServiceCollection
:
services.AddSingleton(AppEnvironment.Clock);
services.AddSingleton(DefaultJsonSettings.Create());
services.AddSingleton<IRouteInfoProvider, RouteInfoProvider>();
services.AddSingleton(new ApiVersion(
apiAssembly.GetCustomAttribute<AssemblyFileVersionAttribute>().Version));
services.AddSingleton<IApiInfoProvider, ApiInfoProvider>();
RouteInfoProvider
The final piece worth looking at is the provider that extracts the routes.
public class RouteInfoProvider : IRouteInfoProvider
{
private readonly IActionDescriptorCollectionProvider _actionDescriptorCollectionProvider;
public RouteInfoProvider(IActionDescriptorCollectionProvider actionDescriptorCollectionProvider)
{
_actionDescriptorCollectionProvider = actionDescriptorCollectionProvider;
}
public IRouteInfo[] Get() => _actionDescriptorCollectionProvider
.ActionDescriptors
.Items
.Where(ad => ad.AttributeRouteInfo != null)
.Select(RouteInfo.Create).ToArray();
private class RouteInfo : IRouteInfo
{
public string Name { get; private set; }
public string Template { get; private set; }
public string[] HttpMethods { get; private set; }
internal static IRouteInfo Create(ActionDescriptor ad) => new RouteInfo
{
Name = ad.AttributeRouteInfo.Name,
Template = ad.AttributeRouteInfo.Template,
HttpMethods = ad
.ActionConstraints
.OfType<HttpMethodActionConstraint>()
.SelectMany(c => c.HttpMethods)
.ToArray()
};
}
}
That's it. All wrapped up in some simple extension methods used by my APIs in their respective Startup
classes.
public class Startup
{
public void ConfigureServices(IServiceCollection services) => services
.AddMvcApi(GetType().Assembly); //Extension in custom NuGet
public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory) => app
.ConfigureMvcApi(env, loggerFactory); /Extension in custom NuGet
}
That's it. Hope you can use it to something.
//Daniel