OElite.Common.Hosting.AspNetCore 5.0.9-develop.480

OElite.Common.Hosting.AspNetCore

ASP.NET Core web application hosting and lifecycle management for OElite applications.

NuGet Version .NET

Overview

OElite.Common.Hosting.AspNetCore extends the generic OElite.Common.Hosting package with comprehensive ASP.NET Core web application features. It provides standardized patterns for web APIs, authentication, CORS, Swagger documentation, middleware pipeline, and model transformation for HTTP responses.

This package is required for all OElite web applications and provides the recommended OeApp.RunWebAppAsync() and OeApp.RunHybridAppAsync() patterns referenced throughout the OElite coding standards.

Features

🚀 Streamlined Application Lifecycle

  • OeApp.RunWebAppAsync: Complete web application setup with one method call
  • OeApp.RunHybridAppAsync: Console applications with web API endpoints
  • Automatic Configuration: Service registration, middleware pipeline, authentication
  • Graceful Shutdown: Proper resource cleanup and service disposal

🔐 Comprehensive Authentication

  • JWT Bearer Authentication: Automatic JWT validation and claims processing
  • OElite Claim Integration: Built-in support for merchant, user, and client claims
  • Multi-tenant Security: Request context extraction from JWT tokens
  • Flexible Configuration: Enable/disable authentication per application

🌐 Advanced CORS Management

  • Smart CORS Policies: Automatic origin detection and validation
  • Environment-aware: Different policies for development and production
  • Flexible Configuration: Custom CORS options with sensible defaults

📚 Integrated Swagger Documentation

  • Model Transformation Aware: Documents transformed response models
  • JWT Authentication: Built-in Swagger authentication configuration
  • Automatic Generation: API documentation with minimal configuration
  • Custom Attributes: Enhanced documentation with transformation annotations

🔄 HTTP Model Transformation

  • Response Transformation: Automatic model transformation for HTTP responses
  • Context-aware: Different transformations for different request contexts
  • Performance Optimized: Efficient transformation pipeline
  • Swagger Integration: Documented transformed response types

🛡️ Production-ready Middleware

  • Global Exception Handling: Centralized error handling and logging
  • Request Logging: Comprehensive request/response logging with Serilog
  • Rate Limiting: Built-in rate limiting with OElite.Restme.RateLimiting
  • Input Validation: Enhanced API input formatters and validation

Quick Start

1. Installation

dotnet add package OElite.Common.Hosting.AspNetCore

2. Simple Web API Application

using OElite.Common.Hosting.AspNetCore.Extensions;
using OElite.Common.Infrastructure;

public class MyApiConfig : BaseAppConfig
{
    public override string? DbCentreFullClassName =>
        "MyApi.Data.ApiDbCentre, MyApi.Data";

    public MyApiConfig(string jsonConfig, ILogger? logger = null)
        : base(OeAppType.GeneralWebApi, jsonConfig, logger)
    {
    }
}

public class Program
{
    public static async Task Main(string[] args)
    {
        await OeApp.RunWebAppAsync<MyApiConfig>(
            args: args,
            applicationName: "MyApi",
            enableAuthentication: true,
            corsOptions: cors =>
            {
                cors.AllowedOrigins.Add("https://myapp.com");
                cors.AllowCredentials = true;
            });
    }
}

3. Custom Service and App Configuration

public class Program
{
    public static async Task Main(string[] args)
    {
        await OeApp.RunWebAppAsync<MyApiConfig>(
            args: args,
            applicationName: "MyAdvancedApi",
            configureServices: builder =>
            {
                // Add custom services
                builder.Services.AddScoped<ICustomService, CustomService>();

                // Add custom options
                builder.Services.Configure<MyOptions>(
                    builder.Configuration.GetSection("MyOptions"));
            },
            configureApp: async app =>
            {
                // Add custom middleware or endpoints
                app.UseRouting();
                app.MapControllers();

                // Custom health check endpoint
                app.MapGet("/custom-health", () => Results.Ok(new { Status = "Healthy" }));
            },
            dependencyInjectionOptions: options =>
            {
                // Customize service discovery
                options.IncludeAssemblyPrefixes.Add("MyCustom");
                options.ExcludeNamespacePrefixes.Add("MyCustom.Legacy");
            });
    }
}

4. Hybrid Console + API Application

public class Program
{
    public static async Task Main(string[] args)
    {
        await OeApp.RunHybridAppAsync<MyProcessorConfig>(
            args: args,
            applicationName: "DataProcessor",
            backgroundWorkAction: async (serviceProvider, cancellationToken) =>
            {
                // Background processing logic
                var processor = serviceProvider.GetRequiredService<IDataProcessor>();
                while (!cancellationToken.IsCancellationRequested)
                {
                    await processor.ProcessDataAsync();
                    await Task.Delay(TimeSpan.FromMinutes(5), cancellationToken);
                }
            },
            enableWebApi: true, // Exposes management API
            enableAuthentication: false); // Management API without auth
    }
}

Advanced Features

Authentication Configuration

JWT Bearer Setup

// Automatic JWT configuration based on app config
public class MyApiConfig : BaseAppConfig
{
    public MyApiConfig(string jsonConfig) : base(OeAppType.GeneralWebApi, jsonConfig)
    {
        // JWT settings automatically loaded from:
        // "oelite:authentication:jwtSecret"
        // "oelite:authentication:issuer"
        // "oelite:authentication:audience"
    }
}

// JWT configuration in appsettings.json
{
  "oelite": {
    "authentication": {
      "jwtSecret": "your-secret-key-here",
      "issuer": "oelite-platform",
      "audience": "oelite-platform",
      "accessTokenExpirationMinutes": 30
    }
  }
}

Using Authentication in Controllers

[ApiController]
[Route("v{version:apiVersion}/[controller]")]
[Authorize] // Requires JWT authentication
public class ProductsController : ControllerBase
{
    private readonly IProductService _productService;

    public ProductsController(IProductService productService)
    {
        _productService = productService;
    }

    [HttpGet("{id}")]
    public async Task<ActionResult<Product>> GetProduct(string id)
    {
        // RequestContext is automatically populated from JWT claims
        var requestContext = HttpContext.GetOEliteRequestContext();

        var product = await _productService.GetProductAsync(id, requestContext);
        return Ok(product);
    }

    [HttpPost]
    [RequiredRoles("admin", "manager")] // Custom authorization
    public async Task<ActionResult<Product>> CreateProduct([FromBody] CreateProductRequest request)
    {
        var requestContext = HttpContext.GetOEliteRequestContext();
        var product = await _productService.CreateProductAsync(request, requestContext);
        return CreatedAtAction(nameof(GetProduct), new { id = product.Id }, product);
    }
}

Request Context Extraction

public static class HttpContextExtensions
{
    public static RequestContext GetOEliteRequestContext(this HttpContext context)
    {
        var user = context.User;

        return new RequestContext
        {
            MerchantId = user.GetMerchantId(),
            UserId = user.GetUserId(),
            ContactId = user.GetContactId(),
            ClientId = user.GetClientId(),
            ClientIp = context.Connection.RemoteIpAddress,
            UserAgent = context.Request.Headers.UserAgent,
            CorrelationId = context.TraceIdentifier
        };
    }
}

// Extension methods for claims extraction
public static class ClaimsPrincipalExtensions
{
    public static DbObjectId? GetMerchantId(this ClaimsPrincipal user)
    {
        var value = user.FindFirst(OEliteClaimTypes.MerchantId)?.Value;
        return DbObjectId.TryParse(value, out var id) ? id : null;
    }

    public static DbObjectId? GetUserId(this ClaimsPrincipal user)
    {
        var value = user.FindFirst(OEliteClaimTypes.UserId)?.Value;
        return DbObjectId.TryParse(value, out var id) ? id : null;
    }
}

CORS Configuration

Advanced CORS Setup

await OeApp.RunWebAppAsync<MyApiConfig>(
    args: args,
    applicationName: "MyApi",
    corsOptions: cors =>
    {
        // Allow specific origins
        cors.AllowedOrigins.Add("https://app.mycompany.com");
        cors.AllowedOrigins.Add("https://admin.mycompany.com");

        // Development origins
        if (Environment.IsDevelopment())
        {
            cors.AllowedOrigins.Add("http://localhost:3000");
            cors.AllowedOrigins.Add("http://localhost:3001");
        }

        // Credentials and headers
        cors.AllowCredentials = true;
        cors.AllowedHeaders.Add("X-Custom-Header");
        cors.ExposedHeaders.Add("X-Response-Time");

        // Preflight cache
        cors.PreflightMaxAge = TimeSpan.FromHours(1);
    });

Environment-specific CORS

public class ApiCorsOptions : OEliteCorsOptions
{
    public ApiCorsOptions()
    {
        if (Environment.IsDevelopment())
        {
            // Development: Allow localhost origins
            AllowedOrigins.AddRange(new[]
            {
                "http://localhost:3000",
                "http://localhost:3001",
                "https://localhost:3000",
                "https://localhost:3001"
            });
            AllowCredentials = true;
        }
        else if (Environment.IsStaging())
        {
            // Staging: Allow staging domains
            AllowedOrigins.Add("https://staging.myapp.com");
            AllowCredentials = true;
        }
        else
        {
            // Production: Strict CORS policy
            AllowedOrigins.Add("https://myapp.com");
            AllowedOrigins.Add("https://app.myapp.com");
            AllowCredentials = true;
        }

        // Common headers for all environments
        AllowedHeaders.AddRange(new[] { "Authorization", "Content-Type", "X-Requested-With" });
        ExposedHeaders.Add("X-Pagination-Count");
    }
}

Model Transformation for HTTP

Transformation-aware Controllers

[ApiController]
[Route("v{version:apiVersion}/[controller]")]
public class ProductsController : ControllerBase
{
    private readonly IProductService _productService;
    private readonly IModelTransformationService _transformationService;

    public ProductsController(
        IProductService productService,
        IModelTransformationService transformationService)
    {
        _productService = productService;
        _transformationService = transformationService;
    }

    [HttpGet("{id}")]
    [TransformedResponse<ProductApiModel>] // Swagger documentation
    public async Task<ActionResult> GetProduct(string id, [FromQuery] string format = "api")
    {
        var product = await _productService.GetProductAsync(id);

        // Transform based on request format
        var context = new TransformationContext
        {
            TargetFormat = format,
            Properties = new Dictionary<string, object>
            {
                ["IncludeInventory"] = HttpContext.Request.Query.ContainsKey("includeInventory"),
                ["UserId"] = HttpContext.GetOEliteRequestContext().UserId
            }
        };

        var transformed = await _transformationService.TransformAsync(product, context);
        return Ok(transformed);
    }

    [HttpGet]
    [TransformedResponse<List<ProductListModel>>]
    public async Task<ActionResult> GetProducts(
        [FromQuery] int page = 1,
        [FromQuery] int size = 20,
        [FromQuery] string format = "list")
    {
        var products = await _productService.GetProductsAsync(page, size);

        var context = new TransformationContext { TargetFormat = format };
        var transformed = await _transformationService.TransformCollectionAsync(products, context);

        return Ok(new
        {
            Data = transformed,
            Pagination = new { Page = page, Size = size, Total = products.Count }
        });
    }
}

Swagger Integration with Transformations

[HttpGet("{id}")]
[SwaggerOperation(
    Summary = "Get product by ID",
    Description = "Returns a product with optional transformation")]
[SwaggerResponse(200, "Product retrieved successfully", typeof(ProductApiModel))]
[SwaggerResponse(404, "Product not found")]
[TransformedResponse<ProductApiModel>]
public async Task<ActionResult> GetProduct(
    [SwaggerParameter("Product ID", Required = true)] string id,
    [SwaggerParameter("Response format (api, mobile, admin)")] string format = "api")
{
    // Implementation...
}

Custom Middleware Integration

Global Exception Handling

// Custom middleware is automatically registered by OeApp.RunWebAppAsync
public class CustomApiExceptionMiddleware
{
    private readonly RequestDelegate _next;
    private readonly ILogger<CustomApiExceptionMiddleware> _logger;

    public CustomApiExceptionMiddleware(RequestDelegate next, ILogger<CustomApiExceptionMiddleware> logger)
    {
        _next = next;
        _logger = logger;
    }

    public async Task InvokeAsync(HttpContext context)
    {
        try
        {
            await _next(context);
        }
        catch (ValidationException ex)
        {
            await HandleValidationExceptionAsync(context, ex);
        }
        catch (BusinessException ex)
        {
            await HandleBusinessExceptionAsync(context, ex);
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Unhandled exception occurred");
            await HandleGenericExceptionAsync(context, ex);
        }
    }

    private async Task HandleValidationExceptionAsync(HttpContext context, ValidationException ex)
    {
        context.Response.StatusCode = 400;
        var response = new
        {
            Error = "Validation failed",
            Details = ex.Errors.Select(e => new { Field = e.PropertyName, Message = e.ErrorMessage })
        };

        await context.Response.WriteAsync(JsonSerializer.Serialize(response));
    }
}

// Register custom middleware
await OeApp.RunWebAppAsync<MyApiConfig>(
    args: args,
    applicationName: "MyApi",
    configureApp: async app =>
    {
        // Custom middleware is added after OElite middleware
        app.UseMiddleware<CustomApiExceptionMiddleware>();
    });

Request/Response Logging

// Built-in request logging middleware is automatically configured
// Configure additional logging in appsettings.json
{
  "Serilog": {
    "MinimumLevel": {
      "Default": "Information",
      "Override": {
        "Microsoft": "Warning",
        "OElite.Common.Hosting.AspNetCore": "Debug"
      }
    },
    "WriteTo": [
      {
        "Name": "Console",
        "Args": {
          "outputTemplate": "[{Timestamp:HH:mm:ss} {Level:u3}] {SourceContext}: {Message:lj}{NewLine}{Exception}"
        }
      },
      {
        "Name": "File",
        "Args": {
          "path": "logs/api-.txt",
          "rollingInterval": "Day",
          "retainedFileCountLimit": 30,
          "outputTemplate": "[{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz} {Level:u3}] {SourceContext}: {Message:lj}{NewLine}{Exception}"
        }
      }
    ]
  }
}

Swagger Documentation

Automatic Swagger Setup

// Swagger is automatically configured with JWT authentication
// Access at: https://yourapi.com/swagger

// The following features are included automatically:
// - JWT Bearer authentication UI
// - Model transformation documentation
// - API versioning support
// - Response type documentation
// - Custom operation filters

Custom Swagger Configuration

await OeApp.RunWebAppAsync<MyApiConfig>(
    args: args,
    applicationName: "MyApi",
    configureServices: builder =>
    {
        builder.Services.ConfigureSwaggerGen(options =>
        {
            // Custom API information
            options.SwaggerDoc("v1", new OpenApiInfo
            {
                Title = "My Custom API",
                Version = "v1.0",
                Description = "Advanced API with OElite transformation",
                Contact = new OpenApiContact
                {
                    Name = "API Support",
                    Email = "support@mycompany.com"
                }
            });

            // Include XML comments
            var xmlFile = $"{Assembly.GetExecutingAssembly().GetName().Name}.xml";
            var xmlPath = Path.Combine(AppContext.BaseDirectory, xmlFile);
            options.IncludeXmlComments(xmlPath);

            // Custom operation filters
            options.OperationFilter<CustomOperationFilter>();
        });
    });

Performance Considerations

Response Transformation Performance

  • Model transformations are cached per request context
  • Use TransformationContext.Properties for request-specific data
  • Avoid heavy computations in transformation CanHandle methods

Authentication Performance

  • JWT tokens are validated once per request and cached
  • Claims extraction is optimized for OElite claim types
  • Request context creation is lazy and cached

Middleware Pipeline Optimization

  • Critical middleware (authentication, CORS) runs first
  • Exception handling middleware runs early to catch all errors
  • Request logging middleware has minimal performance impact

Memory Management

  • Services are registered as singletons where appropriate
  • HTTP clients are properly managed through IHttpClientFactory
  • Model transformation services use object pooling for performance

Application Patterns

API-only Application

public class Program
{
    public static async Task Main(string[] args)
    {
        await OeApp.RunWebAppAsync<ApiConfig>(
            args: args,
            applicationName: "ProductApi",
            enableAuthentication: true,
            configureApp: async app =>
            {
                // API-only configuration
                app.UseRouting();
                app.MapControllers();

                // Health checks
                app.MapGet("/health", () => Results.Ok(new { Status = "Healthy", Timestamp = DateTime.UtcNow }));
            });
    }
}

Full-stack Web Application

public class Program
{
    public static async Task Main(string[] args)
    {
        await OeApp.RunWebAppAsync<WebAppConfig>(
            args: args,
            applicationName: "ECommerceApp",
            enableAuthentication: true,
            configureServices: builder =>
            {
                // Add MVC with views
                builder.Services.AddControllersWithViews();
                builder.Services.AddRazorPages();
            },
            configureApp: async app =>
            {
                // Static files
                app.UseStaticFiles();

                // Routing
                app.UseRouting();

                // MVC routes
                app.MapControllerRoute(
                    name: "default",
                    pattern: "{controller=Home}/{action=Index}/{id?}");

                // API routes
                app.MapControllers();

                // Razor pages
                app.MapRazorPages();
            });
    }
}

Background Service with Management API

public class Program
{
    public static async Task Main(string[] args)
    {
        await OeApp.RunHybridAppAsync<ProcessorConfig>(
            args: args,
            applicationName: "DataProcessor",
            backgroundWorkAction: async (serviceProvider, cancellationToken) =>
            {
                var processor = serviceProvider.GetRequiredService<IDataProcessor>();
                var logger = serviceProvider.GetRequiredService<ILogger<Program>>();

                logger.LogInformation("Starting background data processing...");

                while (!cancellationToken.IsCancellationRequested)
                {
                    try
                    {
                        await processor.ProcessBatchAsync();
                        await Task.Delay(TimeSpan.FromSeconds(30), cancellationToken);
                    }
                    catch (Exception ex)
                    {
                        logger.LogError(ex, "Error in background processing");
                        await Task.Delay(TimeSpan.FromMinutes(1), cancellationToken);
                    }
                }
            },
            enableWebApi: true,
            enableAuthentication: false, // Management API without auth
            configureApp: async app =>
            {
                // Management endpoints
                app.MapGet("/status", (IDataProcessor processor) =>
                    Results.Ok(processor.GetStatus()));

                app.MapPost("/process-now", async (IDataProcessor processor) =>
                {
                    await processor.ProcessBatchAsync();
                    return Results.Ok(new { Message = "Processing triggered" });
                });
            });
    }
}

Testing Integration

Unit Testing with OElite.Common.Hosting.AspNetCore

public class ApiControllerTests
{
    [Test]
    public async Task TestProductEndpoint()
    {
        var services = new ServiceCollection();
        var configuration = new ConfigurationBuilder()
            .AddInMemoryCollection(new Dictionary<string, string>
            {
                ["oelite:data:mongodb:app"] = "mongodb://localhost:27017/test",
                ["oelite:authentication:jwtSecret"] = "test-secret-key"
            })
            .Build();

        var appConfig = new TestApiConfig(configuration);

        // Configure services like OeApp does
        services.AddOeApp(configuration, appConfig);
        services.AddOEliteApi();

        var provider = services.BuildServiceProvider();

        var controller = new ProductsController(
            provider.GetRequiredService<IProductService>(),
            provider.GetRequiredService<IModelTransformationService>());

        // Test controller methods
        var result = await controller.GetProduct("test-id");
        Assert.IsNotNull(result);
    }
}

Integration Testing

public class ApiIntegrationTests : IClassFixture<WebApplicationFactory<Program>>
{
    private readonly WebApplicationFactory<Program> _factory;

    public ApiIntegrationTests(WebApplicationFactory<Program> factory)
    {
        _factory = factory;
    }

    [Test]
    public async Task Get_Products_Returns_Success()
    {
        var client = _factory.CreateClient();

        // Add JWT token for authentication
        client.DefaultRequestHeaders.Authorization =
            new AuthenticationHeaderValue("Bearer", GetTestJwtToken());

        var response = await client.GetAsync("/v1.0/products");

        response.EnsureSuccessStatusCode();
        var content = await response.Content.ReadAsStringAsync();
        var products = JsonSerializer.Deserialize<List<Product>>(content);

        Assert.NotNull(products);
    }

    private string GetTestJwtToken()
    {
        // Generate test JWT token
        var key = Encoding.ASCII.GetBytes("test-secret-key");
        var tokenHandler = new JwtSecurityTokenHandler();
        var tokenDescriptor = new SecurityTokenDescriptor
        {
            Subject = new ClaimsIdentity(new[]
            {
                new Claim(OEliteClaimTypes.UserId, "test-user-id"),
                new Claim(OEliteClaimTypes.MerchantId, "test-merchant-id")
            }),
            Expires = DateTime.UtcNow.AddHours(1),
            SigningCredentials = new SigningCredentials(new SymmetricSecurityKey(key), SecurityAlgorithms.HmacSha256Signature)
        };

        var token = tokenHandler.CreateToken(tokenDescriptor);
        return tokenHandler.WriteToken(token);
    }
}

Troubleshooting

Common Issues

Authentication Not Working

// Check JWT configuration in appsettings.json
{
  "oelite": {
    "authentication": {
      "jwtSecret": "your-secret-key-here",  // Must match token issuer
      "issuer": "oelite-platform",          // Must match token issuer
      "audience": "oelite-platform"         // Must match token audience
    }
  }
}

// Verify JWT token claims
public static void ValidateJwtToken(string token)
{
    var handler = new JwtSecurityTokenHandler();
    var jsonToken = handler.ReadJwtToken(token);

    Console.WriteLine($"Issuer: {jsonToken.Issuer}");
    Console.WriteLine($"Audience: {string.Join(", ", jsonToken.Audiences)}");
    Console.WriteLine($"Claims: {string.Join(", ", jsonToken.Claims.Select(c => $"{c.Type}={c.Value}"))}");
}

CORS Issues

// Enable CORS debugging
await OeApp.RunWebAppAsync<MyApiConfig>(
    args: args,
    applicationName: "MyApi",
    corsOptions: cors =>
    {
        cors.AllowedOrigins.Add("http://localhost:3000");
        cors.AllowCredentials = true;

        // Debug CORS in development
        if (Environment.IsDevelopment())
        {
            cors.AllowAnyOrigin = true; // Only for development!
            cors.AllowAnyHeader = true;
            cors.AllowAnyMethod = true;
        }
    });

Swagger Not Accessible

// Ensure Swagger is enabled in production if needed
if (app.Environment.IsDevelopment() || app.Environment.IsStaging())
{
    app.UseSwagger();
    app.UseSwaggerUI(c =>
    {
        c.SwaggerEndpoint("/swagger/v1/swagger.json", "My API V1");
        c.RoutePrefix = "swagger"; // Access at /swagger
    });
}

Model Transformation Not Working

// Check transformer registration
var transformers = serviceProvider.GetServices<IModelTransformer>();
Console.WriteLine($"Registered transformers: {string.Join(", ", transformers.Select(t => t.GetType().Name))}");

// Debug transformation context
var context = new TransformationContext
{
    TargetFormat = "api",
    Properties = new Dictionary<string, object>()
};

var canHandle = transformer.CanHandle(context);
Console.WriteLine($"Transformer can handle context: {canHandle}");

Version History

  • 5.0.9: Current version with .NET 10.0 support
  • Enhanced JWT authentication with OElite claims
  • Improved CORS handling with environment awareness
  • Advanced Swagger integration with transformation documentation
  • Optimized middleware pipeline for performance
  • Added hybrid console+API application pattern
  • OElite.Common: Core shared components and infrastructure
  • OElite.Common.Hosting: Generic hosting foundation (required dependency)
  • OElite.Services: Business logic services that integrate with authentication
  • OElite.Restme.RateLimiting: Rate limiting integration for APIs
  • Swashbuckle.AspNetCore: Swagger/OpenAPI documentation

License

Copyright © OElite Limited. All rights reserved.

Support

For support and documentation, visit https://www.oelite.com


Integration with OElite Coding Standards

This package implements the patterns described in:

Always use OeApp.RunWebAppAsync() for web applications as specified in the coding standards.

No packages depend on OElite.Common.Hosting.AspNetCore.

Version Downloads Last updated
5.0.9-develop.511 4 11/26/2025
5.0.9-develop.510 4 11/26/2025
5.0.9-develop.509 5 11/23/2025
5.0.9-develop.480 4 11/17/2025
5.0.9-develop.478 3 11/17/2025
5.0.9-develop.477 4 11/17/2025
5.0.9-develop.476 5 11/17/2025
5.0.9-develop.472 4 11/15/2025
5.0.9-develop.471 4 11/15/2025
5.0.9-develop.470 4 11/15/2025
5.0.9-develop.462 4 11/14/2025
5.0.9-develop.448 5 11/11/2025
5.0.9-develop.446 4 11/11/2025
5.0.9-develop.443 5 11/11/2025
5.0.9-develop.441 4 11/09/2025
5.0.9-develop.415 8 10/28/2025
5.0.9-develop.412 3 10/27/2025
5.0.9-develop.411 3 10/27/2025
5.0.9-develop.410 4 10/27/2025
5.0.9-develop.409 4 10/27/2025
5.0.9-develop.402 16 10/26/2025
5.0.9-develop.399 13 10/26/2025
5.0.9-develop.394 14 10/25/2025
5.0.9-develop.391 11 10/25/2025
5.0.9-develop.389 7 10/25/2025
5.0.9-develop.376 32 10/25/2025
5.0.9-develop.374 11 10/24/2025
5.0.9-develop.373 6 10/24/2025
5.0.9-develop.372 7 10/24/2025
5.0.9-develop.259 96 10/20/2025
5.0.9-develop.258 7 10/20/2025
5.0.9-develop.244 5 10/19/2025
5.0.9-develop.240 4 10/18/2025
5.0.9-develop.239 5 10/18/2025
5.0.9-develop.238 4 10/18/2025
5.0.9-develop.236 5 10/18/2025
5.0.9-develop.235 5 10/18/2025
5.0.9-develop.234 5 10/17/2025