Framework Engine
Framework Engine
I built an ASP.NET WebAPI with several enhancements, making the system more flexible:
- Setting from Environment
- Dynamic route prefix
- JWT via Bearer at Swagger UI
- Every config at Global_Data_FRM
Settings loaded from Environment
- Setting values are loaded from Environment {not from a config file}
- when running on Linux, this can be done in the .service file
[Service]
Environment=Swagger_Prefix=docsDEV
Environment=ApiPrefix=apiDEV
Environment=Jwt_Key=abcXYZ
Environment=Jwt_Issuer=xyzABC
- when running on Windows {for example during development}, this can be done in a .bat file
set Swagger_Prefix=docsDEV
set ApiPrefix=apiDEV
set Jwt_Key=abcXYZ
set Jwt_Issuer=xyzABC
Global_Data_FRM
- a
staticclass responsible for reading values from the Environment - serves as the single reference point for all Settings needed by the Framework
- used by
SwaggerExtensions,RoutePrefixExtensions, andAppOtherExtensions
Environment Variable → Global_Data_FRM → Extensions → App
Properties read from the Environment:
| Property | Environment Variable | Description |
|---|---|---|
Swagger_Prefix |
Swagger_Prefix |
URL prefix for Swagger UI |
Api_Prefix |
ApiPrefix |
Route prefix for all Controllers |
Jwt_Key |
Jwt_Key |
Secret key for signing JWT |
Jwt_Issuer |
Jwt_Issuer |
Issuer and Audience for JWT |
note: all Extensions do not read the Environment directly — they always go through
Global_Data_FRM
Route on Controller
- the Route on a Controller is not
hardcoded— its value comes from a Setting- not static, but dynamic
- easy to change at runtime
- supports “aliasing”
- instead of always hardcoding
/api - it can become:
- /api
- /apiDEV
- /apiCompanyX
- /apiCompanyY
- /apiCompanyZ
- this BackEnd Route behaviour has an effect on the FrontEnd
- FrontEnd also has a Setting for a
Url- a .js file for this setting
- fetch that .js file
- store to LocalStorage
- all FrontEnd code, when accessing the BackEnd — must always be aware of that Setting value
- FrontEnd also has a Setting for a
BackEnd - Controller:
namespace OurFramework.Controllers
{
[Route("[controller]")]
[ApiController]
public class XyzController : ControllerBase
note: there will be an Extension that combines
- the prefix taken from the Setting
- the Controller name
Frontend - file app-settings.json:
{
"Endpoint": "http://localhost:9091/apiDEV"
}
Frontend - fetch app-settings.json:
const response = await fetch('app-settings.json');
RoutePrefixConvention
- problem: Controller uses
[Route("[controller]")]— without a prefix - solution: dynamically inject a prefix into all Controllers via
IApplicationModelConvention - result: the prefix from
Global_Data_FRM.Api_Prefixis automatically combined with the Controller name
[Route("[controller]")] + ApiPrefix → /apiDEV/Xyz
How it works:
public class RoutePrefixConvention : IApplicationModelConvention
{
private readonly AttributeRouteModel _routePrefix;
public RoutePrefixConvention(string prefix)
{
_routePrefix = new AttributeRouteModel(new RouteAttribute(prefix));
}
public void Apply(ApplicationModel application)
{
foreach (var controller in application.Controllers)
{
foreach (var selector in controller.Selectors.Where(s => s.AttributeRouteModel != null))
{
selector.AttributeRouteModel = AttributeRouteModel.CombineAttributeRouteModel(_routePrefix, selector.AttributeRouteModel);
}
}
}
}
Registered via RoutePrefixExtensions:
public static class RoutePrefixExtensions
{
public static void UseCustomAddControllers(this IServiceCollection services, IConfiguration config)
{
var routePrefix = Global_Data_FRM.Api_Prefix;
services.AddControllers(options =>
{
options.Conventions.Insert(0, new RoutePrefixConvention(routePrefix));
}).AddJsonOptions(options =>
{
options.JsonSerializerOptions.PropertyNamingPolicy = null;
options.JsonSerializerOptions.PropertyNameCaseInsensitive = true;
});
}
}
note:
PropertyNamingPolicy = nullmeans JSON property names follow the C# property names as-is (not camelCased)
Extensions C#
Several Extensions are created:
- SwaggerExtensions
- RoutePrefixExtensions
- AppOtherExtensions
which are loaded by Program.cs
var builder = WebApplication.CreateBuilder(args);
builder.Services.UseCustomPreparation(builder.Configuration);
builder.Services.UseCustomAddControllers(builder.Configuration);
Authentication — additional Settings for JwtBearer
public static class AppOtherExtensions
{
public static void UseCustomPreparation(this IServiceCollection services, IConfiguration config)
{
services.AddMemoryCache();
services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer(options =>
{
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidateAudience = true,
ValidateLifetime = false, // No need to validate Lifetime here, because Lifetime will be checked in the getCurrentUser() function
ValidateIssuerSigningKey = true,
ValidIssuer = Global_Data_FRM.Jwt_Issuer,
ValidAudience = Global_Data_FRM.Jwt_Issuer,
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(Global_Data_FRM.Jwt_Key))
};
});
}
}
Swagger UI Enhancement
I wanted the Swagger UI to support entering a Token value. I needed the following additional code:
public static class SwaggerExtensions
{
private static string _Name = "Framework Engine";
public static void AddCustomSwagger(this IServiceCollection services)
{
services.AddSwaggerGen(options =>
{
options.SwaggerDoc("v1", new() { Title = _Name + ": " + App_Signature.Signature, Version = "v1" });
// Add JWT Bearer
options.AddSecurityDefinition("Bearer", new Microsoft.OpenApi.OpenApiSecurityScheme
{
Name = "Authorization",
Type = Microsoft.OpenApi.SecuritySchemeType.Http,
Scheme = "bearer",
BearerFormat = "JWT",
In = Microsoft.OpenApi.ParameterLocation.Header,
Description = "Enter 'Bearer' [space] and then your valid token in the text input below.\r\n\r\nExample: \"Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6...\""
});
options.AddSecurityRequirement(document => new OpenApiSecurityRequirement
{
[new OpenApiSecuritySchemeReference("Bearer", document)] = []
});
var xmlFile = $"{Assembly.GetExecutingAssembly().GetName().Name}.xml";
var xmlPath = Path.Combine(AppContext.BaseDirectory, xmlFile);
options.IncludeXmlComments(xmlPath);
});
}
public static void UseCustomSwaggerUI(this IApplicationBuilder x, IConfiguration config)
{
var swaggerPrefix = Global_Data_FRM.Swagger_Prefix + "2";
x.UseSwagger(options =>
{
options.RouteTemplate = $"{swaggerPrefix}//swagger.json";
options.OpenApiVersion = Microsoft.OpenApi.OpenApiSpecVersion.OpenApi3_1;
});
x.UseSwaggerUI(options =>
{
options.RoutePrefix = Global_Data_FRM.Swagger_Prefix;
options.SwaggerEndpoint($"/{swaggerPrefix}/v1/swagger.json", _Name + " v1");
});
}
}