danielwertheim

danielwertheim


notes from a passionate developer

Developer that lives by the mantra "code is meant to be shared".

Share


Tags


Disclaimer

This is a personal blog. The opinions expressed here represent my own and not those of my employer, nor current or previous. All content is published "as is", without warranty of any kind and I don't take any responsibility and can't be liable for any claims, damages or other liabilities that might be caused by the content.

ASP.Net Core and a simple discovery endpoint

Daniel WertheimDaniel Wertheim

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

Developer that lives by the mantra "code is meant to be shared".

Comments