Skip to content

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.AddAbpSwaggerGen with 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 DefaultValuesOperation filter for handling "default values" at swagger ui, in order to achieve this create new folder named Filters under Swagger folder and then add AddDefaultValuesOperationFilter.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, default Kind is Unspecified that 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.DockerSecrets module.
<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")
          );
        }
    })
    ....