Add new parameters
This commit is contained in:
@@ -2,6 +2,7 @@
|
||||
using Managing.Application.Abstractions.Services;
|
||||
using Managing.Application.Hubs;
|
||||
using Managing.Domain.Backtests;
|
||||
using Managing.Domain.Bots;
|
||||
using Managing.Domain.MoneyManagements;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
@@ -13,6 +14,7 @@ namespace Managing.Api.Controllers;
|
||||
/// <summary>
|
||||
/// Controller for managing backtest operations.
|
||||
/// Provides endpoints for creating, retrieving, and deleting backtests.
|
||||
/// Returns complete backtest configurations for easy bot deployment.
|
||||
/// Requires authorization for access.
|
||||
/// </summary>
|
||||
[ApiController]
|
||||
@@ -52,8 +54,9 @@ public class BacktestController : BaseController
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves all backtests for the authenticated user.
|
||||
/// Each backtest includes the complete TradingBotConfig for easy bot deployment.
|
||||
/// </summary>
|
||||
/// <returns>A list of backtests.</returns>
|
||||
/// <returns>A list of backtests with complete configurations.</returns>
|
||||
[HttpGet]
|
||||
public async Task<ActionResult<IEnumerable<Backtest>>> Backtests()
|
||||
{
|
||||
@@ -63,10 +66,11 @@ public class BacktestController : BaseController
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves a specific backtest by ID for the authenticated user.
|
||||
/// This endpoint will also populate the candles for visualization.
|
||||
/// This endpoint will also populate the candles for visualization and includes
|
||||
/// the complete TradingBotConfig that can be used to start a new bot.
|
||||
/// </summary>
|
||||
/// <param name="id">The ID of the backtest to retrieve.</param>
|
||||
/// <returns>The requested backtest with populated candle data.</returns>
|
||||
/// <returns>The requested backtest with populated candle data and complete configuration.</returns>
|
||||
[HttpGet("{id}")]
|
||||
public async Task<ActionResult<Backtest>> Backtest(string id)
|
||||
{
|
||||
@@ -94,115 +98,130 @@ public class BacktestController : BaseController
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Runs a backtest with the specified parameters.
|
||||
/// Runs a backtest with the specified configuration.
|
||||
/// The returned backtest includes a complete TradingBotConfig that preserves all
|
||||
/// settings including nullable MaxPositionTimeHours for easy bot deployment.
|
||||
/// </summary>
|
||||
/// <param name="accountName">The name of the account to use for the backtest.</param>
|
||||
/// <param name="botType">The type of bot to use for the backtest.</param>
|
||||
/// <param name="ticker">The ticker symbol to backtest.</param>
|
||||
/// <param name="scenarioName">The name of the scenario to use for the backtest.</param>
|
||||
/// <param name="timeframe">The timeframe for the backtest.</param>
|
||||
/// <param name="watchOnly">Whether to only watch the backtest without executing trades.</param>
|
||||
/// <param name="startDate">The start date for the backtest.</param>
|
||||
/// <param name="endDate">The end date for the backtest.</param>
|
||||
/// <param name="balance">The starting balance for the backtest.</param>
|
||||
/// <param name="moneyManagementName">The name of the money management strategy to use.</param>
|
||||
/// <param name="moneyManagement">The money management strategy details, if not using a named strategy.</param>
|
||||
/// <param name="save">Whether to save the backtest results.</param>
|
||||
/// <param name="cooldownPeriod">The cooldown period for the backtest.</param>
|
||||
/// <param name="maxLossStreak">The maximum loss streak for the backtest.</param>
|
||||
/// <returns>The result of the backtest.</returns>
|
||||
/// <param name="request">The backtest request containing configuration and parameters.</param>
|
||||
/// <returns>The result of the backtest with complete configuration.</returns>
|
||||
[HttpPost]
|
||||
[Route("Run")]
|
||||
public async Task<ActionResult<Backtest>> Run(string accountName,
|
||||
BotType botType,
|
||||
Ticker ticker,
|
||||
string scenarioName,
|
||||
Timeframe timeframe,
|
||||
bool watchOnly,
|
||||
decimal balance,
|
||||
string moneyManagementName,
|
||||
DateTime startDate,
|
||||
DateTime endDate,
|
||||
MoneyManagement? moneyManagement = null,
|
||||
bool save = false,
|
||||
int cooldownPeriod = 1,
|
||||
int maxLossStreak = 0)
|
||||
public async Task<ActionResult<Backtest>> Run([FromBody] RunBacktestRequest request)
|
||||
{
|
||||
if (string.IsNullOrEmpty(accountName))
|
||||
if (request?.Config == null)
|
||||
{
|
||||
throw new ArgumentException($"'{nameof(accountName)}' cannot be null or empty.", nameof(accountName));
|
||||
return BadRequest("Backtest configuration is required");
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(scenarioName))
|
||||
if (string.IsNullOrEmpty(request.Config.AccountName))
|
||||
{
|
||||
throw new ArgumentException($"'{nameof(scenarioName)}' cannot be null or empty.", nameof(scenarioName));
|
||||
return BadRequest("Account name is required");
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(moneyManagementName) && moneyManagement == null)
|
||||
if (string.IsNullOrEmpty(request.Config.ScenarioName))
|
||||
{
|
||||
throw new ArgumentException(
|
||||
$"'{nameof(moneyManagementName)}' and '{nameof(moneyManagement)}' cannot be null or empty.",
|
||||
nameof(moneyManagementName));
|
||||
return BadRequest("Scenario name is required");
|
||||
}
|
||||
|
||||
Backtest backtestResult = null;
|
||||
var scenario = _scenarioService.GetScenario(scenarioName);
|
||||
var account = await _accountService.GetAccount(accountName, true, false);
|
||||
var user = await GetUser();
|
||||
|
||||
if (!string.IsNullOrEmpty(moneyManagementName) && moneyManagement is null)
|
||||
if (string.IsNullOrEmpty(request.MoneyManagementName) && request.MoneyManagement == null)
|
||||
{
|
||||
moneyManagement = await _moneyManagementService.GetMoneyMangement(user, moneyManagementName);
|
||||
}
|
||||
else
|
||||
{
|
||||
moneyManagement.FormatPercentage();
|
||||
return BadRequest("Either money management name or money management object is required");
|
||||
}
|
||||
|
||||
if (scenario == null)
|
||||
return BadRequest("No scenario found");
|
||||
|
||||
switch (botType)
|
||||
try
|
||||
{
|
||||
case BotType.SimpleBot:
|
||||
break;
|
||||
case BotType.ScalpingBot:
|
||||
backtestResult = await _backtester.RunScalpingBotBacktest(
|
||||
account,
|
||||
moneyManagement,
|
||||
ticker,
|
||||
scenario,
|
||||
timeframe,
|
||||
balance,
|
||||
startDate,
|
||||
endDate,
|
||||
user,
|
||||
watchOnly,
|
||||
save,
|
||||
cooldownPeriod: cooldownPeriod,
|
||||
maxLossStreak: maxLossStreak);
|
||||
break;
|
||||
case BotType.FlippingBot:
|
||||
backtestResult = await _backtester.RunFlippingBotBacktest(
|
||||
account,
|
||||
moneyManagement,
|
||||
ticker,
|
||||
scenario,
|
||||
timeframe,
|
||||
balance,
|
||||
startDate,
|
||||
endDate,
|
||||
user,
|
||||
watchOnly,
|
||||
save,
|
||||
cooldownPeriod: cooldownPeriod,
|
||||
maxLossStreak: maxLossStreak);
|
||||
break;
|
||||
Backtest backtestResult = null;
|
||||
var scenario = _scenarioService.GetScenario(request.Config.ScenarioName);
|
||||
var account = await _accountService.GetAccount(request.Config.AccountName, true, false);
|
||||
var user = await GetUser();
|
||||
|
||||
if (scenario == null)
|
||||
return BadRequest("No scenario found");
|
||||
|
||||
// Get money management
|
||||
MoneyManagement moneyManagement;
|
||||
if (!string.IsNullOrEmpty(request.MoneyManagementName))
|
||||
{
|
||||
moneyManagement = await _moneyManagementService.GetMoneyMangement(user, request.MoneyManagementName);
|
||||
if (moneyManagement == null)
|
||||
return BadRequest("Money management not found");
|
||||
}
|
||||
else
|
||||
{
|
||||
moneyManagement = request.MoneyManagement;
|
||||
moneyManagement?.FormatPercentage();
|
||||
}
|
||||
|
||||
// Update config with money management
|
||||
var backtestConfig = new TradingBotConfig
|
||||
{
|
||||
AccountName = request.Config.AccountName,
|
||||
MoneyManagement = moneyManagement,
|
||||
Ticker = request.Config.Ticker,
|
||||
ScenarioName = request.Config.ScenarioName,
|
||||
Timeframe = request.Config.Timeframe,
|
||||
IsForWatchingOnly = request.WatchOnly,
|
||||
BotTradingBalance = request.Balance,
|
||||
BotType = request.Config.BotType,
|
||||
IsForBacktest = true,
|
||||
CooldownPeriod = request.Config.CooldownPeriod,
|
||||
MaxLossStreak = request.Config.MaxLossStreak,
|
||||
MaxPositionTimeHours = request.Config.MaxPositionTimeHours,
|
||||
FlipOnlyWhenInProfit = request.Config.FlipOnlyWhenInProfit,
|
||||
FlipPosition = request.Config.FlipPosition,
|
||||
Name = request.Config.Name ?? $"Backtest-{request.Config.ScenarioName}-{DateTime.UtcNow:yyyyMMdd-HHmmss}"
|
||||
};
|
||||
|
||||
switch (request.Config.BotType)
|
||||
{
|
||||
case BotType.SimpleBot:
|
||||
break;
|
||||
case BotType.ScalpingBot:
|
||||
backtestResult = await _backtester.RunScalpingBotBacktest(
|
||||
account,
|
||||
moneyManagement,
|
||||
request.Config.Ticker,
|
||||
scenario,
|
||||
request.Config.Timeframe,
|
||||
request.Balance,
|
||||
request.StartDate,
|
||||
request.EndDate,
|
||||
user,
|
||||
request.WatchOnly,
|
||||
request.Save,
|
||||
cooldownPeriod: request.Config.CooldownPeriod,
|
||||
maxLossStreak: request.Config.MaxLossStreak,
|
||||
maxPositionTimeHours: request.Config.MaxPositionTimeHours,
|
||||
flipOnlyWhenInProfit: request.Config.FlipOnlyWhenInProfit);
|
||||
break;
|
||||
case BotType.FlippingBot:
|
||||
backtestResult = await _backtester.RunFlippingBotBacktest(
|
||||
account,
|
||||
moneyManagement,
|
||||
request.Config.Ticker,
|
||||
scenario,
|
||||
request.Config.Timeframe,
|
||||
request.Balance,
|
||||
request.StartDate,
|
||||
request.EndDate,
|
||||
user,
|
||||
request.WatchOnly,
|
||||
request.Save,
|
||||
cooldownPeriod: request.Config.CooldownPeriod,
|
||||
maxLossStreak: request.Config.MaxLossStreak,
|
||||
maxPositionTimeHours: request.Config.MaxPositionTimeHours,
|
||||
flipOnlyWhenInProfit: request.Config.FlipOnlyWhenInProfit);
|
||||
break;
|
||||
}
|
||||
|
||||
await NotifyBacktesingSubscriberAsync(backtestResult);
|
||||
|
||||
return Ok(backtestResult);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return StatusCode(500, $"Error running backtest: {ex.Message}");
|
||||
}
|
||||
|
||||
await NotifyBacktesingSubscriberAsync(backtestResult);
|
||||
|
||||
return Ok(backtestResult);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -216,4 +235,50 @@ public class BacktestController : BaseController
|
||||
await _hubContext.Clients.All.SendAsync("BacktestsSubscription", backtesting);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Request model for running a backtest
|
||||
/// </summary>
|
||||
public class RunBacktestRequest
|
||||
{
|
||||
/// <summary>
|
||||
/// The trading bot configuration to use for the backtest
|
||||
/// </summary>
|
||||
public TradingBotConfig Config { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The start date for the backtest
|
||||
/// </summary>
|
||||
public DateTime StartDate { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The end date for the backtest
|
||||
/// </summary>
|
||||
public DateTime EndDate { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The starting balance for the backtest
|
||||
/// </summary>
|
||||
public decimal Balance { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether to only watch the backtest without executing trades
|
||||
/// </summary>
|
||||
public bool WatchOnly { get; set; } = false;
|
||||
|
||||
/// <summary>
|
||||
/// Whether to save the backtest results
|
||||
/// </summary>
|
||||
public bool Save { get; set; } = false;
|
||||
|
||||
/// <summary>
|
||||
/// The name of the money management strategy to use (optional if MoneyManagement is provided)
|
||||
/// </summary>
|
||||
public string? MoneyManagementName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The money management strategy details (optional if MoneyManagementName is provided)
|
||||
/// </summary>
|
||||
public MoneyManagement? MoneyManagement { get; set; }
|
||||
}
|
||||
@@ -4,6 +4,7 @@ using Managing.Application.Hubs;
|
||||
using Managing.Application.ManageBot.Commands;
|
||||
using Managing.Common;
|
||||
using Managing.Domain.Bots;
|
||||
using Managing.Domain.MoneyManagements;
|
||||
using Managing.Domain.Trades;
|
||||
using MediatR;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
@@ -142,6 +143,8 @@ public class BotController : BaseController
|
||||
BotType = request.BotType,
|
||||
CooldownPeriod = request.CooldownPeriod,
|
||||
MaxLossStreak = request.MaxLossStreak,
|
||||
MaxPositionTimeHours = request.MaxPositionTimeHours,
|
||||
FlipOnlyWhenInProfit = request.FlipOnlyWhenInProfit,
|
||||
IsForBacktest = false,
|
||||
FlipPosition = request.BotType == BotType.FlippingBot,
|
||||
Name = request.Name
|
||||
@@ -419,7 +422,9 @@ public class BotController : BaseController
|
||||
AccountName = item.Config.AccountName,
|
||||
MoneyManagement = item.Config.MoneyManagement,
|
||||
Identifier = item.Identifier,
|
||||
AgentName = item.User.AgentName
|
||||
AgentName = item.User.AgentName,
|
||||
MaxPositionTimeHours = item.Config.MaxPositionTimeHours,
|
||||
FlipOnlyWhenInProfit = item.Config.FlipOnlyWhenInProfit
|
||||
});
|
||||
}
|
||||
|
||||
@@ -543,6 +548,102 @@ public class BotController : BaseController
|
||||
return StatusCode(500, $"Error closing position: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates the configuration of a running bot
|
||||
/// </summary>
|
||||
/// <param name="request">The request containing the new bot configuration</param>
|
||||
/// <returns>A response indicating the result of the operation</returns>
|
||||
[HttpPut]
|
||||
[Route("UpdateConfig")]
|
||||
public async Task<ActionResult<string>> UpdateBotConfig([FromBody] UpdateBotConfigRequest request)
|
||||
{
|
||||
try
|
||||
{
|
||||
// Check if user owns the account
|
||||
if (!await UserOwnsBotAccount(request.Identifier))
|
||||
{
|
||||
return Forbid("You don't have permission to update this bot's configuration");
|
||||
}
|
||||
|
||||
var activeBots = _botService.GetActiveBots();
|
||||
var bot = activeBots.FirstOrDefault(b => b.Identifier == request.Identifier);
|
||||
|
||||
if (bot == null)
|
||||
{
|
||||
return NotFound($"Bot with identifier {request.Identifier} not found or is not running");
|
||||
}
|
||||
|
||||
// Get the user for validation
|
||||
var user = await GetUser();
|
||||
|
||||
// Validate money management if provided
|
||||
MoneyManagement moneyManagement = null;
|
||||
if (!string.IsNullOrEmpty(request.MoneyManagementName))
|
||||
{
|
||||
moneyManagement = await _moneyManagementService.GetMoneyMangement(user, request.MoneyManagementName);
|
||||
if (moneyManagement == null)
|
||||
{
|
||||
return BadRequest("Money management not found");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Keep existing money management if not provided
|
||||
moneyManagement = bot.Config.MoneyManagement;
|
||||
}
|
||||
|
||||
// Validate account if provided
|
||||
if (!string.IsNullOrEmpty(request.AccountName))
|
||||
{
|
||||
var account = await _accountService.GetAccount(request.AccountName, true, false);
|
||||
if (account == null || account.User?.Name != user.Name)
|
||||
{
|
||||
return BadRequest("Account not found or you don't have permission to use this account");
|
||||
}
|
||||
}
|
||||
|
||||
// Create updated configuration
|
||||
var updatedConfig = new TradingBotConfig
|
||||
{
|
||||
AccountName = !string.IsNullOrEmpty(request.AccountName) ? request.AccountName : bot.Config.AccountName,
|
||||
MoneyManagement = moneyManagement,
|
||||
Ticker = request.Ticker ?? bot.Config.Ticker,
|
||||
ScenarioName = !string.IsNullOrEmpty(request.ScenarioName) ? request.ScenarioName : bot.Config.ScenarioName,
|
||||
Timeframe = request.Timeframe ?? bot.Config.Timeframe,
|
||||
IsForWatchingOnly = request.IsForWatchingOnly ?? bot.Config.IsForWatchingOnly,
|
||||
BotTradingBalance = request.BotTradingBalance ?? bot.Config.BotTradingBalance,
|
||||
BotType = bot.Config.BotType, // Bot type cannot be changed
|
||||
CooldownPeriod = request.CooldownPeriod ?? bot.Config.CooldownPeriod,
|
||||
MaxLossStreak = request.MaxLossStreak ?? bot.Config.MaxLossStreak,
|
||||
MaxPositionTimeHours = request.MaxPositionTimeHours ?? bot.Config.MaxPositionTimeHours,
|
||||
FlipOnlyWhenInProfit = request.FlipOnlyWhenInProfit ?? bot.Config.FlipOnlyWhenInProfit,
|
||||
IsForBacktest = bot.Config.IsForBacktest, // Cannot be changed for running bots
|
||||
FlipPosition = request.FlipPosition ?? bot.Config.FlipPosition,
|
||||
Name = !string.IsNullOrEmpty(request.Name) ? request.Name : bot.Config.Name
|
||||
};
|
||||
|
||||
// Validate the updated configuration
|
||||
if (updatedConfig.BotTradingBalance <= Constants.GMX.Config.MinimumPositionAmount)
|
||||
{
|
||||
return BadRequest($"Bot trading balance must be greater than {Constants.GMX.Config.MinimumPositionAmount}");
|
||||
}
|
||||
|
||||
// Update the bot's configuration
|
||||
var updateCommand = new UpdateBotConfigCommand(request.Identifier, updatedConfig);
|
||||
var result = await _mediator.Send(updateCommand);
|
||||
|
||||
_logger.LogInformation($"Bot configuration update result for {request.Identifier} by user {user.Name}: {result}");
|
||||
|
||||
await NotifyBotSubscriberAsync();
|
||||
return Ok(result);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error updating bot configuration");
|
||||
return StatusCode(500, $"Error updating bot configuration: {ex.Message}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -627,7 +728,107 @@ public class StartBotRequest
|
||||
/// </summary>
|
||||
public decimal InitialTradingBalance { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The cooldown period in candles between positions
|
||||
/// </summary>
|
||||
public int CooldownPeriod { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The maximum number of consecutive losses before stopping
|
||||
/// </summary>
|
||||
public int MaxLossStreak { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The name of the bot
|
||||
/// </summary>
|
||||
public string Name { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Maximum time in hours that a position can remain open before being automatically closed.
|
||||
/// Supports fractional values (e.g., 2.5 for 2 hours and 30 minutes).
|
||||
/// If null, time-based position closure is disabled.
|
||||
/// </summary>
|
||||
public decimal? MaxPositionTimeHours { get; set; } = null;
|
||||
|
||||
/// <summary>
|
||||
/// If true, positions will only be flipped when the current position is in profit.
|
||||
/// If false, positions will be flipped regardless of profit status.
|
||||
/// </summary>
|
||||
public bool FlipOnlyWhenInProfit { get; set; } = true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Request model for updating bot configuration
|
||||
/// </summary>
|
||||
public class UpdateBotConfigRequest
|
||||
{
|
||||
/// <summary>
|
||||
/// The identifier of the bot to update
|
||||
/// </summary>
|
||||
public string Identifier { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The account name to use (optional - will keep existing if not provided)
|
||||
/// </summary>
|
||||
public string? AccountName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The money management name to use (optional - will keep existing if not provided)
|
||||
/// </summary>
|
||||
public string? MoneyManagementName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The ticker to trade (optional - will keep existing if not provided)
|
||||
/// </summary>
|
||||
public Ticker? Ticker { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The scenario to use (optional - will keep existing if not provided)
|
||||
/// </summary>
|
||||
public string? ScenarioName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The timeframe to use (optional - will keep existing if not provided)
|
||||
/// </summary>
|
||||
public Timeframe? Timeframe { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether the bot is for watching only (optional - will keep existing if not provided)
|
||||
/// </summary>
|
||||
public bool? IsForWatchingOnly { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The bot trading balance (optional - will keep existing if not provided)
|
||||
/// </summary>
|
||||
public decimal? BotTradingBalance { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The cooldown period in candles between positions (optional - will keep existing if not provided)
|
||||
/// </summary>
|
||||
public int? CooldownPeriod { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The maximum number of consecutive losses before stopping (optional - will keep existing if not provided)
|
||||
/// </summary>
|
||||
public int? MaxLossStreak { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Maximum time in hours that a position can remain open before being automatically closed (optional - will keep existing if not provided)
|
||||
/// </summary>
|
||||
public decimal? MaxPositionTimeHours { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// If true, positions will only be flipped when the current position is in profit (optional - will keep existing if not provided)
|
||||
/// </summary>
|
||||
public bool? FlipOnlyWhenInProfit { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether position flipping is enabled (optional - will keep existing if not provided)
|
||||
/// </summary>
|
||||
public bool? FlipPosition { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The name of the bot (optional - will keep existing if not provided)
|
||||
/// </summary>
|
||||
public string? Name { get; set; }
|
||||
}
|
||||
Reference in New Issue
Block a user