Framework Engine

Saya membuat ASP.NET WebAPI dengan beberapa enhancement, sehingga system yang saya buat menjadi lebih fleksibel:

  • Setting dari Environment
  • Dynamic route prefix
  • JWT via Bearer di Swagger UI
  • Semua config terpusat di Global_Data_FRM

Setting di load dari Environment

  • nilai Setting di load dari Environment {bukan dari file config}
  • saat running di Linux, bisa dilakukan pada file .service
[Service]
Environment=Swagger_Prefix=docsDEV
Environment=ApiPrefix=apiDEV
Environment=Jwt_Key=abcXYZ
Environment=Jwt_Issuer=xyzABC
  • saat running di Windows {misalnya saat proses development}, bisa dilakukan pada file .bat
set Swagger_Prefix=docsDEV
set ApiPrefix=apiDEV
set Jwt_Key=abcXYZ
set Jwt_Issuer=xyzABC

Global_Data_FRM

  • class static yang bertugas membaca nilai dari Environment
  • menjadi satu titik referensi untuk semua Setting yang dibutuhkan oleh Framework
  • digunakan oleh SwaggerExtensions, RoutePrefixExtensions, dan AppOtherExtensions
Environment Variable  →  Global_Data_FRM  →  Extensions  →  App

Properti yang dibaca dari Environment:

Properti Environment Variable Keterangan
Swagger_Prefix Swagger_Prefix Prefix URL untuk Swagger UI
Api_Prefix ApiPrefix Prefix route untuk semua Controller
Jwt_Key Jwt_Key Secret key untuk signing JWT
Jwt_Issuer Jwt_Issuer Issuer dan Audience untuk JWT

catatan: semua Extensions tidak membaca Environment secara langsung — selalu melalui Global_Data_FRM

Route pada Controller

  • Route pada Controller, tidak hardcoded, tetapi nilainya suatu Setting
    • tidak static, tetapi dynamic
    • mudah diubah saat runtime
    • bisa “aliasing”
    • daripada selalu hardcoded /api
    • bisa menjadi:
      • /api
      • /apiDEV
      • /apiCompanyX
      • /apiCompanyY
      • /apiCompanyZ
  • behaviour Route pada BackEnd di atas, akan efek ke FrontEnd
    • FrontEnd juga mempunyai Setting terhadap suatu Url
      • file .js untuk setting ini
      • fetch ke file .js tersebut
      • simpan ke LocalStorage
    • semua coding pada FrontEnd, saat akses ke BackEnd – harus selalu aware terhadap nilai Setting tersebut

BackEnd - Controller:

namespace OurFramework.Controllers
{
    [Route("[controller]")]
    [ApiController]
    public class XyzController : ControllerBase

catatan: akan ada Extensions yang akan melakukan kombinasi

  • prefix yang diambil dari Setting
  • nama Controller

Frontend - file app-settings.json:

{
    "Endpoint": "http://localhost:9091/apiDEV"
}

Frontend - fetch app-settings.json:

const response = await fetch('app-settings.json');

RoutePrefixConvention

  • masalah: Controller menggunakan [Route("[controller]")] — tanpa prefix
  • solusi: inject prefix secara dinamis ke semua Controller via IApplicationModelConvention
  • hasilnya: prefix dari Global_Data_FRM.Api_Prefix otomatis digabung dengan nama Controller
[Route("[controller]")]  +  ApiPrefix  →  /apiDEV/Xyz

Cara kerjanya:

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

Didaftarkan melalui 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;
        });
    }
}

catatan: PropertyNamingPolicy = null berarti nama JSON property mengikuti nama C# property apa adanya (tidak di-camelCase)

Extensions C#

dibuat beberapa Extensions:

  • SwaggerExtensions
  • RoutePrefixExtensions
  • AppOtherExtensions

yang akan diload oleh Program.cs

var builder = WebApplication.CreateBuilder(args);

builder.Services.UseCustomPreparation(builder.Configuration);
builder.Services.UseCustomAddControllers(builder.Configuration);

Authentication ditambahkan beberapa nilai Setting untuk 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,  // Tidak usah dilakukan Validasi Lifetime di sini, karena Lifetime akan dicheck pada function getCurrentUser()
                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))
            };
        });
    }
}

Enhancement pada UI Swagger

Saya ingin pada UI Swagger, kita bisa input nilai Token. Saya memerlukan tambahan coding berikut:

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