Skip to content

Commit

Permalink
Shields style Badge feature (#14)
Browse files Browse the repository at this point in the history
* badge endpoint
* unit test badge
* update package description
  • Loading branch information
salem84 authored Mar 2, 2021
1 parent a0865bd commit c66461b
Show file tree
Hide file tree
Showing 11 changed files with 370 additions and 4 deletions.
52 changes: 51 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:

Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -87,3 +88,52 @@ _AspNetCore.VersionInfo_ package includes following providers:
| AppDomainAssembliesVersionProvider | `<AssemblyName>` | 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)

4 changes: 3 additions & 1 deletion samples/Basic/Pages/Index.cshtml
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,7 @@

<div class="text-center">
<h1 class="display-4">Welcome</h1>
<p>Learn about <a href="https://docs.microsoft.com/aspnet/core">building Web apps with ASP.NET Core</a>.</p>
<p>Application Version Badge </p>

<img src="/version/badge/EntryAssemblyVersion?color=BrightGreen&label=version" />
</div>
3 changes: 2 additions & 1 deletion src/AspNetCore.VersionInfo/AspNetCore.VersionInfo.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
<RepositoryType>git</RepositoryType>
<PublishRepositoryUrl>true</PublishRepositoryUrl>
<Authors>Giorgio Lasala</Authors>
<PackageDescription>A library to expose version information in JSON and HTML format</PackageDescription>
<PackageDescription>A library to expose version information in JSON, HTML or Shields-style badge format</PackageDescription>
<Copyright>Copyright 2021</Copyright>
<PackageRequireLicenseAcceptance>true</PackageRequireLicenseAcceptance>
<PackageLicenseExpression>Apache-2.0</PackageLicenseExpression>
Expand All @@ -28,6 +28,7 @@
<ItemGroup>
<FrameworkReference Include="Microsoft.AspNetCore.App" />
<PackageReference Include="Microsoft.Extensions.Http" Version="5.0.0" />
<PackageReference Include="System.Drawing.Common" Version="5.0.0" />
<PackageReference Include="System.Text.Json" Version="5.0.0" />
</ItemGroup>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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; } = "";
}
}
11 changes: 11 additions & 0 deletions src/AspNetCore.VersionInfo/Constants.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -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";
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,15 @@ public static IEndpointConventionBuilder MapVersionInfo(this IEndpointRouteBuild
var uiEndpoint = builder.Map(options.HtmlPath, uiDelegate)
.WithDisplayName("VersionInfo HTML");

var endpointConventionBuilders = new List<IEndpointConventionBuilder>(new[] { apiEndpoint, uiEndpoint });
var badgeDelegate =
builder.CreateApplicationBuilder()
.UseMiddleware<BadgeEndpoint>()
.Build();

var badgeEndpoint = builder.Map(options.BadgePath, badgeDelegate)
.WithDisplayName("VersionInfo Badge");

var endpointConventionBuilders = new List<IEndpointConventionBuilder>(new[] { apiEndpoint, uiEndpoint, badgeEndpoint });
return new VersionInfoConventionBuilder(endpointConventionBuilders);
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -18,6 +19,10 @@ public static VersionInfoBuilder AddVersionInfo(this IServiceCollection services

services.AddTransient<IInfoCollector, InfoCollector>();

services.AddTransient<IBadgePainter, BadgePainter>();
services.AddTransient<IInfoProvider, ClrVersionProvider>();
services.AddTransient<IInfoProvider, AssemblyVersionProvider>();
services.AddTransient<IInfoProvider, AppDomainAssembliesVersionProvider>();
return builder;
}
}
Expand Down
74 changes: 74 additions & 0 deletions src/AspNetCore.VersionInfo/Middleware/BadgeEndpoint.cs
Original file line number Diff line number Diff line change
@@ -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<BadgeEndpoint> Logger;

public BadgeEndpoint(RequestDelegate next, IServiceScopeFactory serviceScopeFactory, ILogger<BadgeEndpoint> logger)
{
this._serviceScopeFactory = serviceScopeFactory;
this.Logger = logger;
}

public async Task InvokeAsync(HttpContext context)
{
Dictionary<string, string> 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<IInfoCollector>();
var badgePainter = scope.ServiceProvider.GetService<IBadgePainter>();

// 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);
}
}
}
105 changes: 105 additions & 0 deletions src/AspNetCore.VersionInfo/Services/BadgePainter.cs
Original file line number Diff line number Diff line change
@@ -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
{
/// <summary>
/// The flat 2.
/// </summary>
public const string Flat = @"<svg xmlns=""http://www.w3.org/2000/svg"" width=""{0}"" height=""20""><linearGradient id=""b"" x2=""0"" y2=""100%""><stop offset=""0"" stop-color=""#bbb"" stop-opacity="".1""/><stop offset=""1"" stop-opacity="".1""/></linearGradient><mask id=""a""><rect width=""{0}"" height=""20"" rx=""3"" fill=""#fff""/></mask><g mask=""url(#a)""><path fill=""#555"" d=""M0 0h{1}v20H0z""/><path fill=""{7}"" d=""M{1} 0h{2}v20H{1}z""/><path fill=""url(#b)"" d=""M0 0h{0}v20H0z""/></g><g fill=""#fff"" text-anchor=""middle"" font-family=""DejaVu Sans,Verdana,Geneva,sans-serif"" font-size=""11""><text x=""{3}"" y=""15"" fill=""#010101"" fill-opacity="".3"">{5}</text><text x=""{3}"" y=""14"">{5}</text><text x=""{4}"" y=""15"" fill=""#010101"" fill-opacity="".3"">{6}</text><text x=""{4}"" y=""14"">{6}</text></g></svg>";

public const string FlatSquare = @"<svg xmlns=""http://www.w3.org/2000/svg"" width=""{0}"" height=""20""><g shape-rendering=""crispEdges""><path fill=""#555"" d=""M0 0h{1}v20H0z""/><path fill=""{7}"" d=""M{1} 0h{2}v20H{1}z""/></g><g fill=""#fff"" text-anchor=""middle"" font-family=""DejaVu Sans,Verdana,Geneva,sans-serif"" font-size=""11""><text x=""{3}"" y=""14"">{5}</text><text x=""{4}"" y=""14"">{6}</text></g></svg>";

public const string Plastic = @"<svg xmlns=""http://www.w3.org/2000/svg"" width=""{0}"" height=""18""><linearGradient id=""b"" x2=""0"" y2=""100%""><stop offset=""0"" stop-color=""#fff"" stop-opacity="".7""/><stop offset="".1"" stop-color=""#aaa"" stop-opacity="".1""/><stop offset="".9"" stop-opacity="".3""/><stop offset=""1"" stop-opacity="".5""/></linearGradient><mask id=""a""><rect width=""{0}"" height=""18"" rx=""4"" fill=""#fff""/></mask><g mask=""url(#a)""><path fill=""#555"" d=""M0 0h{1}v18H0z""/><path fill=""{7}"" d=""M{1} 0h{2}v18H{1}z""/><path fill=""url(#b)"" d=""M0 0h{0}v18H0z""/></g><g fill=""#fff"" text-anchor=""middle"" font-family=""DejaVu Sans,Verdana,Geneva,sans-serif"" font-size=""11""><text x=""{3}"" y=""14"" fill=""#010101"" fill-opacity="".3"">{5}</text><text x=""{3}"" y=""13"">{5}</text><text x=""{4}"" y=""14"" fill=""#010101"" fill-opacity="".3"">{6}</text><text x=""{4}"" y=""13"">{6}</text></g></svg>";
}

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);
}
}
}
Loading

0 comments on commit c66461b

Please sign in to comment.