diff --git a/README.md b/README.md index eb9e640..0665a0f 100644 --- a/README.md +++ b/README.md @@ -8,6 +8,7 @@ AspNetCore.VersionInfo is a library to expose information about assembly version In particular there are two endpoints, which returns: * a JSON-formatted data (_/version/json_) * an HTML user-friendly page (_/version/html_) +* a nice badge image (_/version/badge_) Library offers some in-bundle providers to capture versions information, such as the version of entry assembly or the version of the common language runtime. A typical JSON output is: @@ -39,7 +40,7 @@ Release packages are on [Nuget](http://www.nuget.org/packages/AspNetCore.Version | - | - | | *HTML* | [/version/html](https://aspnetcoreversioninfo-demo.azurewebsites.net/version/html) | | *JSON* | [/version/json](https://aspnetcoreversioninfo-demo.azurewebsites.net/version/json) | - +| *Badge* | [![/version/badge](https://aspnetcoreversioninfo-demo.azurewebsites.net/version/badge/EntryAssemblyVersion?color=Blue&label=version)](https://aspnetcoreversioninfo-demo.azurewebsites.net/version/badge/EntryAssemblyVersion?color=Blue&label=version) | ## Getting Started @@ -87,3 +88,52 @@ _AspNetCore.VersionInfo_ package includes following providers: | AppDomainAssembliesVersionProvider | `` | version of assemblies loaded in App Domain | +### Options + +`MapVersionInfo` extension method accepts an optional `VersionInfoOptions` argument to change default URLs: + +```csharp + +public void Configure(IApplicationBuilder app) +{ + app.UseRouting(); + + app.UseEndpoints(endpoints => + { + endpoints.MapVersionInfo(o => + { + o.HtmlPath = CUSTOM_HTML_URL; + o.ApiPath = CUSTOM_JSON_URL; + }); + }); +} + +``` + + +### Badge + +Badge image can be obtained with url + +`/version/badge/{versionInfoId}` + +where `{versionInfoId}` is a key returned by providers. + +Moreover endpoint accepts following parameters in querystring: +* `label`: it's the name to show in the image +* `color`: a string as defined in the colors table, custom colors are not (yet) supported + +| Color | String | +| - | - | +| ![#4c1](https://via.placeholder.com/15/4c1/000000?text=+)| BrightGreen | +| ![#97CA00](https://via.placeholder.com/15/97CA00/000000?text=+) | Green | +| ![#dfb317](https://via.placeholder.com/15/dfb317/000000?text=+) | Yellow | +| ![#a4a61d](https://via.placeholder.com/15/a4a61d/000000?text=+) | YellowGreen | +| ![#fe7d37](https://via.placeholder.com/15/fe7d37/000000?text=+) | Orange | +| ![#e05d44](https://via.placeholder.com/15/e05d44/000000?text=+) | Red | +| ![#007ec6](https://via.placeholder.com/15/007ec6/000000?text=+) | Blue | +| ![#555](https://via.placeholder.com/15/555/000000?text=+) | Gray | +| ![#9f9f9f](https://via.placeholder.com/15/9f9f9f/000000?text=+) | LightGray | + +Thanks to [Rebornix](https://github.com/rebornix) and [DotBadge library](https://github.com/rebornix/DotBadge) + diff --git a/samples/Basic/Pages/Index.cshtml b/samples/Basic/Pages/Index.cshtml index b5f0c15..1f6ae5a 100644 --- a/samples/Basic/Pages/Index.cshtml +++ b/samples/Basic/Pages/Index.cshtml @@ -6,5 +6,7 @@

Welcome

-

Learn about building Web apps with ASP.NET Core.

+

Application Version Badge

+ +
diff --git a/src/AspNetCore.VersionInfo/AspNetCore.VersionInfo.csproj b/src/AspNetCore.VersionInfo/AspNetCore.VersionInfo.csproj index c62484c..7dcad7a 100644 --- a/src/AspNetCore.VersionInfo/AspNetCore.VersionInfo.csproj +++ b/src/AspNetCore.VersionInfo/AspNetCore.VersionInfo.csproj @@ -8,7 +8,7 @@ git true Giorgio Lasala - A library to expose version information in JSON and HTML format + A library to expose version information in JSON, HTML or Shields-style badge format Copyright 2021 true Apache-2.0 @@ -28,6 +28,7 @@ + diff --git a/src/AspNetCore.VersionInfo/Configuration/VersionInfoOptions.cs b/src/AspNetCore.VersionInfo/Configuration/VersionInfoOptions.cs index 2e39fee..efdc1a2 100644 --- a/src/AspNetCore.VersionInfo/Configuration/VersionInfoOptions.cs +++ b/src/AspNetCore.VersionInfo/Configuration/VersionInfoOptions.cs @@ -10,6 +10,7 @@ public class VersionInfoOptions { public string HtmlPath { get; set; } = Constants.DEFAULT_HTML_ENDPOINT_URL; public string ApiPath { get; set; } = Constants.DEFAULT_API_ENDPOINT_URL; + public string BadgePath { get; set; } = Constants.DEFAULT_BADGE_ENDPOINT_URL; internal string RoutePrefix { get; set; } = ""; } } diff --git a/src/AspNetCore.VersionInfo/Constants.cs b/src/AspNetCore.VersionInfo/Constants.cs index 034fa14..2145ed6 100644 --- a/src/AspNetCore.VersionInfo/Constants.cs +++ b/src/AspNetCore.VersionInfo/Constants.cs @@ -9,10 +9,12 @@ namespace AspNetCore.VersionInfo public static class Constants { internal const string DEFAULT_API_RESPONSE_CONTENT_TYPE = "application/json"; + internal const string DEFAULT_BADGE_RESPONSE_CONTENT_TYPE = "image/svg+xml"; //public const string DEFAULT_HTML_ENDPOINT_URL = "/version/html/{id?}"; public const string DEFAULT_HTML_ENDPOINT_URL = "/version/html"; public const string DEFAULT_API_ENDPOINT_URL = "/version/json"; + public const string DEFAULT_BADGE_ENDPOINT_URL = "/version/badge/{versionInfoId}"; public const string KEY_ENTRY_ASSEMBLY_VERSION = "EntryAssemblyVersion"; public const string KEY_RUNTIME_VERSION = "RuntimeVersion"; @@ -23,10 +25,19 @@ public static class Constants public const string KEY_RUNTIMEINFORMATION_OSARCHITECTURE = "RuntimeInformation.OsArchitecture"; public const string KEY_RUNTIMEINFORMATION_PROCESSARCHITECTURE = "RuntimeInformation.ProcessArchitecture"; public const string KEY_RUNTIMEINFORMATION_RUNTIMEIDENTIFIER = "RuntimeInformation.RuntimeIdentifier"; + + public const string BADGE_PARAM_VERSIONINFOID = "versionInfoId"; + public const string BADGE_PARAM_LABEL = "label"; + public const string BADGE_PARAM_COLOR = "color"; + + public const string BADGE_DEFAULT_COLOR = "Green"; + } public static class Messages { public const string DUPLICATED_KEY = "Duplicated key: {0}"; + public const string BADGE_KEY_NOT_FOUND = "Key not found"; + public const string BADGE_VERSIONINFOID_EMPTY = "VersionInfoId not valid in url"; } } diff --git a/src/AspNetCore.VersionInfo/Extensions/EndpointRouteBuilderExtensions.cs b/src/AspNetCore.VersionInfo/Extensions/EndpointRouteBuilderExtensions.cs index 67da201..b38a3ae 100644 --- a/src/AspNetCore.VersionInfo/Extensions/EndpointRouteBuilderExtensions.cs +++ b/src/AspNetCore.VersionInfo/Extensions/EndpointRouteBuilderExtensions.cs @@ -33,7 +33,15 @@ public static IEndpointConventionBuilder MapVersionInfo(this IEndpointRouteBuild var uiEndpoint = builder.Map(options.HtmlPath, uiDelegate) .WithDisplayName("VersionInfo HTML"); - var endpointConventionBuilders = new List(new[] { apiEndpoint, uiEndpoint }); + var badgeDelegate = + builder.CreateApplicationBuilder() + .UseMiddleware() + .Build(); + + var badgeEndpoint = builder.Map(options.BadgePath, badgeDelegate) + .WithDisplayName("VersionInfo Badge"); + + var endpointConventionBuilders = new List(new[] { apiEndpoint, uiEndpoint, badgeEndpoint }); return new VersionInfoConventionBuilder(endpointConventionBuilders); } } diff --git a/src/AspNetCore.VersionInfo/Extensions/ServiceCollectionExtensions.cs b/src/AspNetCore.VersionInfo/Extensions/ServiceCollectionExtensions.cs index d2ea56d..fb14648 100644 --- a/src/AspNetCore.VersionInfo/Extensions/ServiceCollectionExtensions.cs +++ b/src/AspNetCore.VersionInfo/Extensions/ServiceCollectionExtensions.cs @@ -1,5 +1,6 @@ using AspNetCore.VersionInfo; using AspNetCore.VersionInfo.Configuration; +using AspNetCore.VersionInfo.Providers; using AspNetCore.VersionInfo.Services; using System; using System.Collections.Generic; @@ -18,6 +19,10 @@ public static VersionInfoBuilder AddVersionInfo(this IServiceCollection services services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); return builder; } } diff --git a/src/AspNetCore.VersionInfo/Middleware/BadgeEndpoint.cs b/src/AspNetCore.VersionInfo/Middleware/BadgeEndpoint.cs new file mode 100644 index 0000000..96df986 --- /dev/null +++ b/src/AspNetCore.VersionInfo/Middleware/BadgeEndpoint.cs @@ -0,0 +1,74 @@ +using AspNetCore.VersionInfo.Services; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace AspNetCore.VersionInfo.Middleware +{ + class BadgeEndpoint + { + private readonly IServiceScopeFactory _serviceScopeFactory; + private readonly ILogger Logger; + + public BadgeEndpoint(RequestDelegate next, IServiceScopeFactory serviceScopeFactory, ILogger logger) + { + this._serviceScopeFactory = serviceScopeFactory; + this.Logger = logger; + } + + public async Task InvokeAsync(HttpContext context) + { + Dictionary versionInfo; + string responseContent; + + // Read VersionInfoId to use as key in providers dictionary + // (it's never empty because of route configuration) + var id = context.Request.RouteValues[Constants.BADGE_PARAM_VERSIONINFOID] as string; + + using (var scope = _serviceScopeFactory.CreateScope()) + { + var infoHandler = scope.ServiceProvider.GetService(); + var badgePainter = scope.ServiceProvider.GetService(); + + // Collect all data + versionInfo = infoHandler.AggregateData(); + + // Retrieve versionInfo data by QueryString key + var found = versionInfo.TryGetValue(id, out string versionInfoValue); + if (!found) + { + Logger.LogWarning($"Badge Endpoint Error: {Messages.BADGE_KEY_NOT_FOUND} - {id}"); + context.Response.StatusCode = StatusCodes.Status404NotFound; + return; + } + + // Set color found in QueryString, otherwise set BADGE_DEFAULT_COLOR + var color = context.Request.Query[Constants.BADGE_PARAM_COLOR]; + if (string.IsNullOrEmpty(color)) + { + color = Constants.BADGE_DEFAULT_COLOR; + } + + // Set label found in QueryString, otherwise set as Key + var label = context.Request.Query[Constants.BADGE_PARAM_LABEL]; + if(string.IsNullOrEmpty(label)) + { + label = id; + } + + // Draw badge + responseContent = badgePainter.Draw(label, versionInfoValue, color, Style.Flat); + } + + // Set ContentType as image/svg+xml + context.Response.ContentType = Constants.DEFAULT_BADGE_RESPONSE_CONTENT_TYPE; + + await context.Response.WriteAsync(responseContent); + } + } +} diff --git a/src/AspNetCore.VersionInfo/Services/BadgePainter.cs b/src/AspNetCore.VersionInfo/Services/BadgePainter.cs new file mode 100644 index 0000000..e614168 --- /dev/null +++ b/src/AspNetCore.VersionInfo/Services/BadgePainter.cs @@ -0,0 +1,105 @@ +using System; +using System.Collections.Generic; +using System.Drawing; +using System.Globalization; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + + +// Based on https://github.com/rebornix/DotBadge + +namespace AspNetCore.VersionInfo.Services +{ + public interface IBadgePainter + { + string Draw(string subject, string status, string statusColor, Style style); + } + + + public enum Style + { + Flat, + FlatSquare, + Plastic + } + + public static class ColorScheme + { + public const string BrightGreen = "#4c1"; + public const string Green = "#97CA00"; + public const string Yellow = "#dfb317"; + public const string YellowGreen = "#a4a61d"; + public const string Orange = "#fe7d37"; + public const string Red = "#e05d44"; + public const string Blue = "#007ec6"; + public const string Gray = "#555"; + public const string LightGray = "#9f9f9f"; + } + + public static class Resources + { + /// + /// The flat 2. + /// + public const string Flat = @"{5}{5}{6}{6}"; + + public const string FlatSquare = @"{5}{6}"; + + public const string Plastic = @"{5}{5}{6}{6}"; + } + + public class BadgePainter : IBadgePainter + { + public string Draw(string subject, string status, string statusColor, Style style) + { + string template; + string color; + switch (style) + { + case Style.Flat: + template = Resources.Flat; + break; + case Style.FlatSquare: + template = Resources.FlatSquare; + break; + case Style.Plastic: + template = Resources.Plastic; + break; + default: + throw new ArgumentException("Style not supported", nameof(style)); + } + + Font font = new Font("DejaVu Sans,Verdana,Geneva,sans-serif", 11, FontStyle.Regular); + Graphics g = Graphics.FromImage(new Bitmap(1, 1)); + var subjectWidth = g.MeasureString(subject, font).Width; + var statusWidth = g.MeasureString(status, font).Width; + + color = ParseColor(statusColor); + + var result = string.Format( + CultureInfo.InvariantCulture, + template, + subjectWidth + statusWidth, + subjectWidth, + statusWidth, + subjectWidth / 2 + 1, + subjectWidth + statusWidth / 2 - 1, + subject, + status, + color); + return result; + } + + private static string ParseColor(string input) + { + var type = typeof(ColorScheme); + var fieldInfo = type.GetField(input); + if (fieldInfo == null) + { + return String.Empty; + } + return (string)fieldInfo.GetValue(type); + } + } +} diff --git a/tests/AspNetCore.VersionInfo.Tests/BadgeTest.cs b/tests/AspNetCore.VersionInfo.Tests/BadgeTest.cs new file mode 100644 index 0000000..051619d --- /dev/null +++ b/tests/AspNetCore.VersionInfo.Tests/BadgeTest.cs @@ -0,0 +1,63 @@ +using AspNetCore.VersionInfo.Middleware; +using AspNetCore.VersionInfo.Services; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Logging; +using Moq; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Xunit; + +namespace AspNetCore.VersionInfo.Tests +{ + public class BadgeTest : BaseIocTest + { + [Fact] + public async Task GetStandardBadge() + { + const string expectedOutput = "Request handled by next request delegate"; + var requestDelegate = new RequestDelegate((innerHttpContext) => + { + innerHttpContext.Response.WriteAsync(expectedOutput); + return Task.CompletedTask; + }); + + // Arrange + DefaultHttpContext defaultContext = new DefaultHttpContext(); + defaultContext.Response.Body = new MemoryStream(); + defaultContext.Request.Path = "/"; + defaultContext.Request.RouteValues.Add("versionInfoId", "Key1"); + + var infoHandler = new Mock(); + var simpleData = new Dictionary() + { + { "Key1", "Value1" } + }; + infoHandler.Setup(x => x.AggregateData()).Returns(simpleData); + RegisterServiceWithInstance(infoHandler.Object); + + var mockLogger = new Mock>(); + var mockBadgePainter = new Mock(); + var svgReturnForKey1 = "Value1"; + mockBadgePainter.Setup(x => x.Draw("Key1", It.IsAny(), It.IsAny(), It.IsAny