Browse Source

JSON and improved Swagger server aspects and new status endpoing

Lukas Angerer 1 năm trước cách đây
mục cha
commit
f4bb0ac0aa

+ 12 - 2
WebTemplate/AppServer.cs

@@ -1,7 +1,10 @@
 using WebTemplate.ServerAspects.Auth;
+using WebTemplate.ServerAspects.Controllers;
 using WebTemplate.ServerAspects.Cors;
+using WebTemplate.ServerAspects.Json;
 using WebTemplate.ServerAspects.Spa;
 using WebTemplate.ServerAspects.Swagger;
+using WebTemplate.Status;
 
 namespace WebTemplate;
 
@@ -9,17 +12,19 @@ public class AppServer
 {
     private readonly IList<IAppConfigurationModule> _modules = new List<IAppConfigurationModule>
     {
+        new JsonModule(),
         new AuthModule(),
         new CorsModule(),
         new SpaRoutingModule(),
         new SwaggerModule(),
-        //new ApiControllersModule(),
+        new StatusEndpointModule(),
+        new ControllersModule(),
     };
 
     public void Start(string[] args)
     {
         var builder = WebApplication.CreateBuilder(args);
-        //builder.Configuration.AddJsonFile()
+        SetupConfiguration(builder.Configuration, builder.Environment);
 
         foreach (var appConfigurationModule in _modules)
         {
@@ -36,4 +41,9 @@ public class AppServer
 
         app.Run();
     }
+
+    private void SetupConfiguration(ConfigurationManager configuration, IWebHostEnvironment env)
+    {
+        configuration.AddJsonFile($"~/{env.ApplicationName}.json", optional: true);
+    }
 }

+ 3 - 4
WebTemplate/Program.cs

@@ -1,6 +1,5 @@
-var builder = WebApplication.CreateBuilder(args);
-var app = builder.Build();
 
-app.MapGet("/", () => "Hello World!");
+using WebTemplate;
 
-app.Run();
+var server = new AppServer();
+server.Start(args);

+ 0 - 13
WebTemplate/ServerAspects/Config/ConfigModule.cs

@@ -1,13 +0,0 @@
-namespace WebTemplate.ServerAspects.Config;
-
-public class ConfigModule : IAppConfigurationModule
-{
-    public void ConfigureServices(IServiceCollection services, IConfigurationRoot config)
-    {
-        //config.
-    }
-
-    public void ConfigureApplication(WebApplication app)
-    {
-    }
-}

+ 8 - 0
WebTemplate/ServerAspects/Controllers/ApiSettings.cs

@@ -0,0 +1,8 @@
+namespace WebTemplate.ServerAspects.Controllers;
+
+public class ApiSettings
+{
+    public const string SectionName = "ApiSettings";
+    
+    // Add your API settings here
+}

+ 15 - 0
WebTemplate/ServerAspects/Controllers/ControllersModule.cs

@@ -0,0 +1,15 @@
+namespace WebTemplate.ServerAspects.Controllers;
+
+public class ControllersModule : IAppConfigurationModule
+{
+    public void ConfigureServices(IServiceCollection services, IConfigurationRoot config)
+    {
+        services.AddControllers();
+        services.Configure<ApiSettings>(config.GetSection(ApiSettings.SectionName));
+    }
+
+    public void ConfigureApplication(WebApplication app)
+    {
+        app.MapControllers();
+    }
+}

+ 20 - 0
WebTemplate/ServerAspects/Json/JsonModule.cs

@@ -0,0 +1,20 @@
+using System.Text.Json.Serialization;
+
+namespace WebTemplate.ServerAspects.Json;
+
+public class JsonModule : IAppConfigurationModule
+{
+    public void ConfigureServices(IServiceCollection services, IConfigurationRoot config)
+    {
+        services.ConfigureHttpJsonOptions(opts =>
+        {
+            // Convert enums as their string values instead of integer values
+            var enumConverter = new JsonStringEnumConverter();
+            opts.SerializerOptions.Converters.Add(enumConverter);
+        });
+    }
+
+    public void ConfigureApplication(WebApplication app)
+    {
+    }
+}

+ 40 - 0
WebTemplate/ServerAspects/Swagger/StringEnumSchemaFilter.cs

@@ -0,0 +1,40 @@
+using System.ComponentModel;
+using System.Reflection;
+using Microsoft.OpenApi.Any;
+using Microsoft.OpenApi.Models;
+using Swashbuckle.AspNetCore.SwaggerGen;
+
+namespace WebTemplate.ServerAspects.Swagger;
+
+/// <summary>
+/// This is a custom schema filter that updates the OpenApi definition of enums to specify their _string_ values
+/// instead of their numeric values and also adds a description from a <see cref="DescriptionAttribute"/> to each
+/// value if that attribute is present on the enum.
+/// </summary>
+// ReSharper disable once ClassNeverInstantiated.Global
+public class StringEnumSchemaFilter : ISchemaFilter
+{
+    public void Apply(OpenApiSchema schema, SchemaFilterContext context)
+    {
+        var type = context.Type;
+        if (type.IsEnum)
+        {
+            schema.Type = "string";
+            schema.Format = null;
+            schema.Enum = Enum.GetNames(type).Select(name => new OpenApiString(name)).Cast<IOpenApiAny>().ToList();
+            var fields = type.GetFields(BindingFlags.Public | BindingFlags.Static);
+
+            var description = new StringWriter();
+            foreach (var field in fields)
+            {
+                var descriptionAttribute = field.GetCustomAttribute<DescriptionAttribute>();
+                if (descriptionAttribute != null)
+                {
+                    description.WriteLine($"* `{field.Name}` - {descriptionAttribute.Description}");
+                }
+            }
+
+            schema.Description = description.ToString();
+        }
+    }
+}

+ 20 - 1
WebTemplate/ServerAspects/Swagger/SwaggerModule.cs

@@ -1,3 +1,5 @@
+using System.Reflection;
+
 namespace WebTemplate.ServerAspects.Swagger;
 
 public class SwaggerModule : IAppConfigurationModule
@@ -6,7 +8,24 @@ public class SwaggerModule : IAppConfigurationModule
 	{
 		// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
 		services.AddEndpointsApiExplorer();
-		services.AddSwaggerGen();
+		services.AddSwaggerGen(swaggerGen =>
+		{
+			swaggerGen.SupportNonNullableReferenceTypes();
+			swaggerGen.EnableAnnotations();
+			// The StringEnumSchemaFilter changes the representation of enums from numbers to strings in
+			// the OpenApi document. This assumes that the JSON serializer has ben configured to use the
+			// JsonStringEnumConverter - the two play hand-in-hand.
+			swaggerGen.SchemaFilter<StringEnumSchemaFilter>();
+			
+			// This requires the <GenerateDocumentationFile> property to be set to true in the
+			// .csrpoj file
+			var xmlFilename = $"{Assembly.GetExecutingAssembly().GetName().Name}.xml";
+			var xmlPath = Path.Combine(AppContext.BaseDirectory, xmlFilename);
+			if (File.Exists(xmlPath))
+			{
+				swaggerGen.IncludeXmlComments(xmlPath);
+			}
+		});
 	}
 
 	public void ConfigureApplication(WebApplication app)

+ 17 - 0
WebTemplate/Status/EnvironmentInfo.cs

@@ -0,0 +1,17 @@
+namespace WebTemplate.Status;
+
+public record EnvironmentInfo
+{
+    public string ApplicationName { get; }
+    public string EnvironmentName { get; }
+    public string ContentRootPath { get; }
+    public string WebRootPath { get; }
+    
+    public EnvironmentInfo(IWebHostEnvironment env)
+    {
+        ApplicationName = env.ApplicationName;
+        EnvironmentName = env.EnvironmentName;
+        ContentRootPath = env.ContentRootPath;
+        WebRootPath = env.WebRootPath;
+    }
+}

+ 10 - 0
WebTemplate/Status/ServiceStatus.cs

@@ -0,0 +1,10 @@
+namespace WebTemplate.Status;
+
+/// <summary>
+/// Overall service status result.
+/// </summary>
+/// <param name="Status">The value "OK" if everything is OK. Otherwise this service will not actually return a 200
+/// HTTP result</param>
+/// <param name="Version">Detailed version information</param>
+/// <param name="Environment">Collection of environment variables. Only enabled in _Development_ mode</param>
+public record ServiceStatus(string Status, VersionInfo Version, EnvironmentInfo Environment);

+ 24 - 0
WebTemplate/Status/StatusEndpointModule.cs

@@ -0,0 +1,24 @@
+namespace WebTemplate.Status;
+
+public class StatusEndpointModule : IAppConfigurationModule
+{
+    public void ConfigureServices(IServiceCollection services, IConfigurationRoot config)
+    {
+    }
+
+    public void ConfigureApplication(WebApplication app)
+    {
+        app.MapGet("/v1/status", () => new ServiceStatus("OK", new VersionInfo(), new EnvironmentInfo(app.Environment)))
+            .WithName("StatusService")
+            .WithOpenApi(operation =>
+            {
+                operation.Description =
+                    """
+                    Returns the overall service status, including version and environment information. This can be used
+                    as a health check endpoint.
+                    """;
+
+                return operation;
+            });
+    }
+}

+ 19 - 0
WebTemplate/Status/VersionInfo.cs

@@ -0,0 +1,19 @@
+namespace WebTemplate.Status;
+
+public record VersionInfo
+{
+    public string AssemblyVersion { get; }
+    public string Version { get; }
+    public string Commit { get; }
+    public string Branch { get; }
+    public bool IsDirty { get; }
+    
+    public VersionInfo()
+    {
+        AssemblyVersion = typeof(VersionInfo).Assembly.GetName().Version?.ToString() ?? "<unknown>";
+        Version = ThisAssembly.Git.Tag;
+        Commit = ThisAssembly.Git.Sha;
+        Branch = ThisAssembly.Git.Branch;
+        IsDirty = ThisAssembly.Git.IsDirty;
+    }
+}

+ 7 - 0
WebTemplate/WebTemplate.csproj

@@ -4,12 +4,19 @@
     <TargetFramework>net8.0</TargetFramework>
     <Nullable>enable</Nullable>
     <ImplicitUsings>enable</ImplicitUsings>
+    <GenerateDocumentationFile>true</GenerateDocumentationFile>
+    <NoWarn>$(NoWarn);1591</NoWarn>
   </PropertyGroup>
 
   <ItemGroup>
+    <PackageReference Include="GitInfo" Version="3.3.4">
+      <PrivateAssets>all</PrivateAssets>
+      <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
+    </PackageReference>
     <PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="8.0.1" />
     <PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="8.0.1" />
     <PackageReference Include="Swashbuckle.AspNetCore" Version="6.5.0" />
+    <PackageReference Include="Swashbuckle.AspNetCore.Annotations" Version="6.5.0" />
   </ItemGroup>
 
 </Project>