Backend services customization¶
HttpApiHost¶
- Enable api versioning, abp framework integrates the ASPNET-API-Versioning feature and adapts to C# and JavaScript Static Client Proxies
install Asp.Versioning.Mvc.ApiExplorer from nuget package resource
// https://github.com/dotnet/aspnet-api-versioning/issues/1029
context.Services.AddTransient<IApiControllerFilter, NoControllerFilter>();
context.Services.AddApiVersioning(options =>
{
options.AssumeDefaultVersionWhenUnspecified = true;
options.ReportApiVersions = true;
})
.AddMvc()
.AddApiExplorer(options =>
{
// The specified format code will format the version as "'v'major[.minor][-status]
options.GroupNameFormat = "'v'VVV";
// Note : this option is only necessary when versioning by url segment. the SubstitutionFormat
// Can also be used to control the format of the API version in route templates
options.SubstituteApiVersionInUrl = true;
});
Configure<AbpAspNetCoreMvcOptions>(options =>
{
options.ChangeControllerModelApiExplorerGroupName = false;
});
configure swagger by using IConfigureOptions<SwaggerGenOptions> interface, in order to do that create new folder named Swagger and then add new class ConfigureSwaggerOptions.cs at GridLab.GMSS.Odms.HttpApi.Host project.
using Asp.Versioning.ApiExplorer;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
using Microsoft.OpenApi.Models;
using Swashbuckle.AspNetCore.SwaggerGen;
using System;
namespace GridLab.GMSS.Swagger
{
public class ConfigureSwaggerOptions : IConfigureOptions<SwaggerGenOptions>
{
readonly IApiVersionDescriptionProvider provider;
public ConfigureSwaggerOptions(IApiVersionDescriptionProvider provider) => this.provider = provider;
public void Configure(SwaggerGenOptions options)
{
foreach (var description in provider.ApiVersionDescriptions)
{
options.SwaggerDoc(description.GroupName, CreateInfoForApiVersion(description));
}
}
static OpenApiInfo CreateInfoForApiVersion(ApiVersionDescription description)
{
var info = new OpenApiInfo()
{
Title = "GMSS API",
Version = description.ApiVersion.ToString(),
Contact = new OpenApiContact()
{
Name = "GMS²",
Email = "noreply@gridlab.io",
Url = new Uri("https://gridlab.io"),
},
License = new OpenApiLicense()
{
Name = "GPL",
Url = new Uri(uriString: "http://www.gnu.org/licenses/gpl-3.0.html")
},
TermsOfService = new Uri(uriString: "https://gridlab.io/general/terms-of-use.html")
};
if (description.IsDeprecated)
{
info.Description += " This API version has been deprecated.";
}
return info;
}
}
}

- Replace existing
context.Services.AddAbpSwaggerGenwith following in order to configure swagger ui and open-api-spec generation
context.Services.AddAbpSwaggerGen(options =>
{
// Add a custom operation filter which sets default values
options.OperationFilter<AddDefaultValuesOperationFilter>();
options.CustomSchemaIds(type => type.FullName);
options.AddSecurityDefinition("oauth2", new OpenApiSecurityScheme
{
Type = SecuritySchemeType.OAuth2,
Flows = new OpenApiOAuthFlows
{
AuthorizationCode = new OpenApiOAuthFlow
{
AuthorizationUrl = new Uri($"{configuration["Swagger:Authority"].TrimEnd('/')}{"/connect/authorize".EnsureStartsWith('/')}"),
Scopes = new Dictionary<string, string>
{
{"GMSS", "GMSS API"}
},
TokenUrl = new Uri($"{configuration["Swagger:Authority"].EnsureEndsWith('/')}connect/token")
}
}
});
options.AddSecurityRequirement(new OpenApiSecurityRequirement
{
{
new OpenApiSecurityScheme
{
Reference = new OpenApiReference
{
Type = ReferenceType.SecurityScheme,
Id = "oauth2"
}
},
Array.Empty<string>()
}
});
options.HideAbpEndpoints(); // Hides ABP related endpoints at Swagger UI
});
update oauth client configuration as well as versioning stragety at swagger ui middleware
public override void OnApplicationInitialization(ApplicationInitializationContext context)
{
// ...
app.UseAbpSwaggerUI(options =>
{
var provider = app.ApplicationServices.GetRequiredService<IApiVersionDescriptionProvider>();
// Build a swagger endpoint for each discovered API version
foreach (var description in provider.ApiVersionDescriptions)
{
options.SwaggerEndpoint($"/swagger/{description.GroupName}/swagger.json", description.GroupName.ToUpperInvariant());
}
options.OAuthClientId(context.GetConfiguration()["Swagger:ClientId"]);
// options.OAuthClientSecret(context.GetConfiguration()["Swagger:ClientSecret"]);
});
// ...
}
update appsettings.json accordingly
"Swagger": {
"Authority": "https://localhost:44324", // Note port number needs to adjusted according to project requirement
"ClientId": "GMSS_Swagger",
"ClientSecret": "1q2w3e*" // Swagger is non-public client therefore no need to specify secret, however it can be adjusted according to project needs
}
- Add
DefaultValuesOperationfilter for handling "default values" at swagger ui, in order to achieve this create new folder namedFiltersunderSwaggerfolder and then addAddDefaultValuesOperationFilter.cs.
using Asp.Versioning.ApiExplorer;
using Microsoft.OpenApi.Models;
using Swashbuckle.AspNetCore.SwaggerGen;
using System.Linq;
using System.Text.Json;
namespace GridLab.GMSS.Swagger.Filters
{
public class AddDefaultValuesOperationFilter : IOperationFilter
{
/// <summary>
/// Applies the filter to the specified operation using the given context.
/// </summary>
/// <param name="operation">The operation to apply the filter to.</param>
/// <param name="context">The current operation filter context.</param>
public void Apply(OpenApiOperation operation, OperationFilterContext context)
{
var apiDescription = context.ApiDescription;
operation.Deprecated |= apiDescription.IsDeprecated();
// REF: https://github.com/domaindrivendev/Swashbuckle.AspNetCore/issues/1752#issue-663991077
foreach (var responseType in context.ApiDescription.SupportedResponseTypes)
{
// REF: https://github.com/domaindrivendev/Swashbuckle.AspNetCore/blob/b7cf75e7905050305b115dd96640ddd6e74c7ac9/src/Swashbuckle.AspNetCore.SwaggerGen/SwaggerGenerator/SwaggerGenerator.cs#L383-L387
var responseKey = responseType.IsDefaultResponse ? "default" : responseType.StatusCode.ToString();
var response = operation.Responses[responseKey];
foreach (var contentType in response.Content.Keys)
{
if (!responseType.ApiResponseFormats.Any(x => x.MediaType == contentType))
{
response.Content.Remove(contentType);
}
}
}
if (operation.Parameters == null)
{
return;
}
// REF: https://github.com/domaindrivendev/Swashbuckle.AspNetCore/issues/412
// REF: https://github.com/domaindrivendev/Swashbuckle.AspNetCore/pull/413
foreach (var parameter in operation.Parameters)
{
var description = apiDescription.ParameterDescriptions.First(p => p.Name == parameter.Name);
if (parameter.Description == null)
{
parameter.Description = description.ModelMetadata?.Description;
}
if (parameter.Schema.Default == null && description.DefaultValue != null)
{
// REF: https://github.com/Microsoft/aspnet-api-versioning/issues/429#issuecomment-605402330
var json = JsonSerializer.Serialize(description.DefaultValue, description.ModelMetadata.ModelType);
parameter.Schema.Default = OpenApiAnyFactory.CreateFromJson(json);
}
parameter.Required |= description.IsRequired;
}
}
}
}
- Add Hangfire support.
Install GridLab.Abp.Hangfire module.
<ItemGroup>
<PackageReference Include="GridLab.Abp.Hangfire" Version="X.Y.Z" />
</ItemGroup>
Replace version number with latest available.
Add the AbpHangfireModule to the dependency list of your module:
[DependsOn(
//...other dependencies
typeof(AbpGridLabHangfireModule)
)]
public class GMSSHttpApiHostModule : AbpModule
{
}
Configure Hangfire module;
private void ConfigureHangfire(ServiceConfigurationContext context, IConfiguration configuration)
{
string serverName = configuration["Hangfire:ServerName"] ?? "gmss-auth-server";
string schemaName = configuration["Hangfire:SchemaName"] ?? string.Empty;
string? connectionString = configuration.GetConnectionString(GMSSDbProperties.ConnectionStringHangfireName);
if (connectionString.IsNullOrEmpty())
throw new ArgumentNullException(paramName: GMSSDbProperties.ConnectionStringHangfireName);
var serverStorageOptions = new EFCoreStorageOptions
{
CountersAggregationInterval = new TimeSpan(0, 5, 0),
DistributedLockTimeout = new TimeSpan(0, 10, 0),
JobExpirationCheckInterval = new TimeSpan(0, 30, 0),
QueuePollInterval = new TimeSpan(0, 0, 15),
Schema = schemaName,
SlidingInvisibilityTimeout = new TimeSpan(0, 5, 0),
};
var options = new DbContextOptionsBuilder<GMSSHangfireDbContext>()
.UseSqlServer(connectionString)
.Options;
GlobalConfiguration.Configuration.UseEFCoreStorage(
() => new GMSSHangfireDbContext(options, schemaName),
serverStorageOptions
);
Configure<AbpHangfireOptions>(options =>
{
options.Storage = EFCoreStorage.Current;
options.ServerOptions = new BackgroundJobServerOptions() { ServerName = serverName };
});
}
Add a new section for hangfire to application configuration settings.
"Hangfire": {
"ServerName": "gmss-auth-server"
},
Make sure you have updated your connection strings defined under ConnectionString at appsettings.json file.
"ConnectionStrings": {
...
"Hangfire": "Server=(localdb)\\MSSQLLocalDB;Database=gmss-hangfire;Trusted_Connection=True;TrustServerCertificate=true"
}
- Configure caching.
Replace Volo.Abp.Caching.StackExchangeRedis with GridLab.Abp.Caching.StackExchangeValkey module.
Remove:
<ItemGroup>
<PackageReference Include="Volo.Abp.Caching.StackExchangeRedis" Version="X.Y.Z" />
</ItemGroup>
Add:
<ItemGroup>
<PackageReference Include="GridLab.Abp.Caching.StackExchangeValkey" Version="X.Y.Z" />
</ItemGroup>
Replace package at GMSSHttpApiHostModule.cs
[DependsOn(
typeof(GMSSEntityFrameworkCoreModule)
//...other dependencies
~~typeof(AbpStackExchangeRedisModule),~~
typeof(AbpGridLabStackExchangeValkeyModule),
//...other dependencies
)]
public class GMSSHttpApiHostModule : AbpModule
{
}
Add ConfigureCache, ConfigureDataProtection and ConfigureDistributedLocking methods for handling cache connections.
private void ConfigureCache(IConfiguration configuration)
{
Configure<AbpDistributedCacheOptions>(options =>
{
options.KeyPrefix = "GMSS:";
});
}
Use AbpValkeyConfigurationHelper.CreateConfigurationOptions helper method to create connection.
private void ConfigureDataProtection(
ServiceConfigurationContext context,
IConfiguration configuration,
IWebHostEnvironment hostingEnvironment)
{
if (AbpStudioAnalyzeHelper.IsInAnalyzeMode)
{
return;
}
var dataProtectionBuilder = context.Services.AddDataProtection().SetApplicationName("GMSS");
if (!hostingEnvironment.IsDevelopment())
{
var certificateProvider = context.Services.GetCertificateProvider();
var certificateValidator = context.Services.GetCertificateValidator();
var options = configuration
.GetSection(AbpGridLabValkeyOptions.ServiceName)
.Get<AbpGridLabValkeyOptions>() ?? new AbpGridLabValkeyOptions();
var configurationOptions = AbpValkeyConfigurationHelper.CreateConfigurationOptions(
serviceOptions: options,
certificateProvider: certificateProvider,
certificateValidator: certificateValidator
);
var redis = ConnectionMultiplexer.Connect(configurationOptions);
dataProtectionBuilder.PersistKeysToStackExchangeRedis(redis, "GMSS-Protection-Keys");
}
}
private void ConfigureDistributedLocking(ServiceConfigurationContext context, IConfiguration configuration)
{
if (AbpStudioAnalyzeHelper.IsInAnalyzeMode)
{
return;
}
context.Services.AddSingleton<IDistributedLockProvider>(sp =>
{
var certificateProvider = context.Services.GetCertificateProvider();
var certificateValidator = context.Services.GetCertificateValidator();
var options = configuration
.GetSection(AbpGridLabValkeyOptions.ServiceName)
.Get<AbpGridLabValkeyOptions>() ?? new AbpGridLabValkeyOptions();
var configurationOptions = AbpValkeyConfigurationHelper.CreateConfigurationOptions(
serviceOptions: options,
certificateProvider: certificateProvider,
certificateValidator: certificateValidator
);
var connection = ConnectionMultiplexer.Connect(configurationOptions);
return new RedisDistributedSynchronizationProvider(connection.GetDatabase());
});
}
Add a new section for caching to application configuration settings.
"Valkey": {
"Enabled": true,
"Configuration": {
"Configuration": "127.0.0.1"
},
"Ssl": {
"Enabled": false
},
"Connection": {
"ConnectTimeout": 30,
"ConnectRetry": 3
}
},
- Add RabbitMQ support.
Install GridLab.Abp.RabbitMQ and Volo.Abp.EventBus.RabbitMQ module.
<ItemGroup>
<PackageReference Include="GridLab.Abp.RabbitMQ" Version="X.Y.Z" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Volo.Abp.EventBus.RabbitMQ" Version="X.Y.Z" />
</ItemGroup>
Replace version number with latest available.
Add the AbpEventBusRabbitMqModule and AbpGridLabRabbitMqModule with given order to the dependency list of your module:
[DependsOn(
//...other dependencies
typeof(AbpEventBusRabbitMqModule),
typeof(AbpGridLabRabbitMqModule),
//...other dependencies
)]
public class GMSSHttpApiHostModule : AbpModule
{
}
Configure RabbitMQ module;
private void ConfigureRabbitMq(ServiceConfigurationContext context, IConfiguration configuration)
{
Configure<AbpRabbitMqEventBusOptions>(options =>
{
options.ClientName = configuration["RabbitMQ:EventBus:ClientName"]!;
options.ExchangeType = configuration["RabbitMQ:EventBus:ExchangeType"]!;
options.ExchangeName = configuration["RabbitMQ:EventBus:ExchangeName"]!;
});
}
Add a new section for RabbitMQ to application configuration settings.
"RabbitMQ": {
"Enabled": true,
"Authentication": {
"Username": "user",
"Password": "bitnami"
},
"Connection": {
"HostName": "localhost",
"Port": 5672,
"VirtualHost": "/"
},
"Ssl": {
"Enabled": false
},
"EventBus": {
"ClientName": "Common", // Client Name is the name of this application, which is used as the queue name on the RabbitMQ
"ExchangeType": "topic", // Options: direct, topic, fanout, headers
"ExchangeName": "GMSS"
}
}
- Activate clock at
GMSSHttpApiHostModule.cs, defaultKindisUnspecifiedthat actually make the Clock as it doesn't exists at all.
private void ConfigureClock()
{
Configure<AbpClockOptions>(options =>
{
options.Kind = DateTimeKind.Utc;
});
}
- Install
GridLab.Abp.Security.Providers.DockerSecretsmodule.
<ItemGroup>
<PackageReference Include="GridLab.Abp.Security.Providers.DockerSecrets" Version="X.Y.Z" />
</ItemGroup>
Replace version number with latest available.
Extend dependency list at GMSSHttpApiHostModule.cs
[DependsOn(
typeof(GMSSEntityFrameworkCoreModule)
//
typeof(AbpGridLabSecurityProvidersDockerSecretsModule),
//...other dependencies
)]
public class GMSSHttpApiHostModule : AbpModule
{
}
.NET Core configuration provider extensions allows reading docker secrets files and pull them into the .net core configuration.
builder.Host
.UseAutofac()
....
.UseSerilog((context, services, loggerConfiguration) =>
{
loggerConfiguration
#if DEBUG
.MinimumLevel.Debug()
.MinimumLevel.Override("DockerSecrets", LogEventLevel.Debug)
#else
.MinimumLevel.Information()
.MinimumLevel.Override("DockerSecrets", LogEventLevel.Information)
#endif
.MinimumLevel.Override("Microsoft", LogEventLevel.Information)
.MinimumLevel.Override("Microsoft.EntityFrameworkCore", LogEventLevel.Warning)
.WriteTo.Async(c => c.Console())
})
....
.ConfigureAppConfiguration((context, config) => {
if (!context.HostingEnvironment.IsDevelopment())
{
config.AddDockerSecrets(
options: () => new DockerSecretsConfigurationOptions
{
ColonPlaceholder = "__"
},
secretsPath: "/run/secrets",
logger: new SerilogLoggerProvider(Log.Logger).CreateLogger("DockerSecrets")
);
}
})
....