Initial commit
This commit is contained in:
@@ -0,0 +1,447 @@
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using RobotNet10.CustomConfiguration.DTOs;
|
||||
using RobotNet10.CustomConfiguration.DTOs.Requests;
|
||||
using RobotNet10.CustomConfiguration.Helpers;
|
||||
using RobotNet10.CustomConfiguration.Models;
|
||||
using RobotNet10.CustomConfiguration.Services;
|
||||
|
||||
namespace RobotNet10.CustomConfiguration.Controllers;
|
||||
|
||||
/// <summary>
|
||||
/// API controller cho quản lý configuration files
|
||||
/// </summary>
|
||||
[ApiController]
|
||||
[Route("api/configs")]
|
||||
[Authorize]
|
||||
public class ConfigController(IConfigService configService) : ControllerBase
|
||||
{
|
||||
private readonly IConfigService _configService = configService;
|
||||
|
||||
// ==========================================
|
||||
// CONFIG FILE MANAGEMENT
|
||||
// ==========================================
|
||||
|
||||
/// <summary>
|
||||
/// Tạo config mới
|
||||
/// </summary>
|
||||
[HttpPost]
|
||||
[ProducesResponseType(typeof(ConfigFileDto), StatusCodes.Status201Created)]
|
||||
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
||||
[ProducesResponseType(StatusCodes.Status409Conflict)]
|
||||
public async Task<ActionResult<ConfigFileDto>> CreateConfig([FromBody] CreateConfigRequest request)
|
||||
{
|
||||
try
|
||||
{
|
||||
// Validate request
|
||||
if (string.IsNullOrWhiteSpace(request.ConfigType))
|
||||
{
|
||||
return BadRequest(new { error = "ConfigType cannot be empty" });
|
||||
}
|
||||
|
||||
// Check if ConfigType already exists
|
||||
if (await _configService.ConfigTypeExistsAsync(request.ConfigType))
|
||||
{
|
||||
return Conflict(new { error = $"Config with type '{request.ConfigType}' already exists" });
|
||||
}
|
||||
|
||||
// Convert DTOs to Models
|
||||
var variables = request.Variables.Select(MapDtoToVariable).ToList();
|
||||
|
||||
// Create config
|
||||
var config = await _configService.CreateConfigAsync(
|
||||
request.ConfigType,
|
||||
variables,
|
||||
request.Description
|
||||
);
|
||||
|
||||
var dto = MapConfigToDto(config);
|
||||
return CreatedAtAction(
|
||||
nameof(GetConfigById),
|
||||
new { id = config.Id },
|
||||
dto);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return BadRequest(new { error = ex.Message });
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Lấy tất cả configs (metadata only)
|
||||
/// </summary>
|
||||
[HttpGet]
|
||||
[ProducesResponseType(typeof(List<ConfigFileMetadataDto>), StatusCodes.Status200OK)]
|
||||
public async Task<ActionResult<List<ConfigFileMetadataDto>>> GetAllConfigs([FromQuery] string? search)
|
||||
{
|
||||
var configs = await _configService.SearchConfigsAsync(search);
|
||||
var dtos = configs.Select(MapMetadataToDto).ToList();
|
||||
return Ok(dtos);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Lấy config theo ID
|
||||
/// </summary>
|
||||
[HttpGet("{id:guid}")]
|
||||
[ProducesResponseType(typeof(ConfigFileDto), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
public async Task<ActionResult<ConfigFileDto>> GetConfigById(Guid id)
|
||||
{
|
||||
var config = await _configService.GetConfigByIdAsync(id);
|
||||
if (config == null)
|
||||
{
|
||||
return NotFound(new { error = $"Config with ID '{id}' not found" });
|
||||
}
|
||||
|
||||
return Ok(MapConfigToDto(config));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Lấy config theo ConfigType
|
||||
/// </summary>
|
||||
[HttpGet("by-type/{configType}")]
|
||||
[ProducesResponseType(typeof(ConfigFileDto), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
public async Task<ActionResult<ConfigFileDto>> GetConfigByType(string configType)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(configType))
|
||||
{
|
||||
return BadRequest(new { error = "ConfigType cannot be empty" });
|
||||
}
|
||||
|
||||
var config = await _configService.GetConfigByTypeAsync(configType);
|
||||
if (config == null)
|
||||
{
|
||||
return NotFound(new { error = $"Config with type '{configType}' not found" });
|
||||
}
|
||||
|
||||
return Ok(MapConfigToDto(config));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Kiểm tra ConfigType có tồn tại không
|
||||
/// </summary>
|
||||
[HttpGet("exists/{configType}")]
|
||||
[ProducesResponseType(typeof(bool), StatusCodes.Status200OK)]
|
||||
public async Task<ActionResult<bool>> ConfigTypeExists(string configType)
|
||||
{
|
||||
var exists = await _configService.ConfigTypeExistsAsync(configType);
|
||||
return Ok(exists);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Cập nhật config
|
||||
/// </summary>
|
||||
[HttpPut("{id:guid}")]
|
||||
[ProducesResponseType(typeof(ConfigFileDto), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
||||
public async Task<ActionResult<ConfigFileDto>> UpdateConfig(Guid id, [FromBody] UpdateConfigRequest request)
|
||||
{
|
||||
try
|
||||
{
|
||||
// Convert DTOs to Models if provided
|
||||
List<ConfigVariable>? variables = null;
|
||||
if (request.Variables != null)
|
||||
{
|
||||
variables = [.. request.Variables.Select(MapDtoToVariable)];
|
||||
}
|
||||
|
||||
var config = await _configService.UpdateConfigAsync(id, variables, request.Description);
|
||||
return Ok(MapConfigToDto(config));
|
||||
}
|
||||
catch (KeyNotFoundException)
|
||||
{
|
||||
return NotFound(new { error = $"Config with ID '{id}' not found" });
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return BadRequest(new { error = ex.Message });
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Xóa config
|
||||
/// </summary>
|
||||
[HttpDelete("{id:guid}")]
|
||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
public async Task<ActionResult> DeleteConfig(Guid id)
|
||||
{
|
||||
var deleted = await _configService.DeleteConfigAsync(id);
|
||||
if (!deleted)
|
||||
{
|
||||
return NotFound(new { error = $"Config with ID '{id}' not found" });
|
||||
}
|
||||
|
||||
return NoContent();
|
||||
}
|
||||
|
||||
// ==========================================
|
||||
// IMPORT/EXPORT
|
||||
// ==========================================
|
||||
|
||||
/// <summary>
|
||||
/// Import config từ JSON file
|
||||
/// </summary>
|
||||
[HttpPost("import")]
|
||||
[ProducesResponseType(typeof(ConfigFileDto), StatusCodes.Status201Created)]
|
||||
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
||||
[ProducesResponseType(StatusCodes.Status409Conflict)]
|
||||
public async Task<ActionResult<ConfigFileDto>> ImportConfig(
|
||||
IFormFile file,
|
||||
[FromForm] string configType,
|
||||
[FromForm] string? description = null)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (file == null || file.Length == 0)
|
||||
{
|
||||
return BadRequest(new { error = "File is required" });
|
||||
}
|
||||
|
||||
if (!file.FileName.EndsWith(".json", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return BadRequest(new { error = "Only JSON files are supported" });
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(configType))
|
||||
{
|
||||
return BadRequest(new { error = "ConfigType is required" });
|
||||
}
|
||||
|
||||
// Validate ConfigType doesn't exist
|
||||
var exists = await _configService.ConfigTypeExistsAsync(configType);
|
||||
if (exists)
|
||||
{
|
||||
return Conflict(new { error = $"Config with type '{configType}' already exists" });
|
||||
}
|
||||
|
||||
using var stream = file.OpenReadStream();
|
||||
// Import config with description (if provided, it overrides description from file)
|
||||
// Note: CreateConfigAsync inside ImportConfigFromJsonAsync already validates before saving
|
||||
var config = await _configService.ImportConfigFromJsonAsync(stream, configType, description);
|
||||
|
||||
var dto = MapConfigToDto(config);
|
||||
return CreatedAtAction(
|
||||
nameof(GetConfigById),
|
||||
new { id = config.Id },
|
||||
dto);
|
||||
}
|
||||
catch (InvalidOperationException ex)
|
||||
{
|
||||
return Conflict(new { error = ex.Message });
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return BadRequest(new { error = ex.Message });
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Export config ra JSON file
|
||||
/// </summary>
|
||||
[HttpGet("{id:guid}/export")]
|
||||
[ProducesResponseType(typeof(FileResult), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
|
||||
public async Task<ActionResult> ExportConfig(Guid id)
|
||||
{
|
||||
try
|
||||
{
|
||||
var config = await _configService.GetConfigByIdAsync(id);
|
||||
|
||||
if (config == null)
|
||||
{
|
||||
return NotFound(new { error = $"Config with ID '{id}' not found" });
|
||||
}
|
||||
|
||||
var stream = await _configService.ExportConfigToJsonAsync(config);
|
||||
|
||||
// Ensure stream is at the beginning
|
||||
if (stream.CanSeek && stream.Position != 0)
|
||||
{
|
||||
stream.Position = 0;
|
||||
}
|
||||
|
||||
// Ensure stream has data
|
||||
if (stream.Length == 0)
|
||||
{
|
||||
return StatusCode(500, new { error = "Export stream is empty" });
|
||||
}
|
||||
|
||||
return File(stream, "application/json", $"{config.ConfigType}.config.json");
|
||||
}
|
||||
catch (KeyNotFoundException)
|
||||
{
|
||||
return NotFound(new { error = $"Config with ID '{id}' not found" });
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return StatusCode(500, new { error = $"Error exporting config: {ex.Message}" });
|
||||
}
|
||||
}
|
||||
|
||||
// ==========================================
|
||||
// VARIABLE MANAGEMENT
|
||||
// ==========================================
|
||||
|
||||
/// <summary>
|
||||
/// Cập nhật giá trị của một variable
|
||||
/// </summary>
|
||||
[HttpPut("{id:guid}/variables/{variableName}")]
|
||||
[ProducesResponseType(typeof(ConfigFileDto), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
||||
public async Task<ActionResult<ConfigFileDto>> UpdateVariable(
|
||||
Guid id,
|
||||
string variableName,
|
||||
[FromBody] UpdateVariableRequest request)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(variableName))
|
||||
{
|
||||
return BadRequest(new { error = "Variable name cannot be empty" });
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var config = await _configService.UpdateVariableAsync(id, variableName, request.Value);
|
||||
return Ok(MapConfigToDto(config));
|
||||
}
|
||||
catch (KeyNotFoundException ex)
|
||||
{
|
||||
return NotFound(new { error = ex.Message });
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return BadRequest(new { error = ex.Message });
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Thêm variable mới vào config
|
||||
/// </summary>
|
||||
[HttpPost("{id:guid}/variables")]
|
||||
[ProducesResponseType(typeof(ConfigFileDto), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
||||
public async Task<ActionResult<ConfigFileDto>> AddVariable(
|
||||
Guid id,
|
||||
[FromBody] ConfigVariableDto variableDto)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(variableDto.Name))
|
||||
{
|
||||
return BadRequest(new { error = "Variable name cannot be empty" });
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(variableDto.Type))
|
||||
{
|
||||
return BadRequest(new { error = "Variable type cannot be empty" });
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var variable = MapDtoToVariable(variableDto);
|
||||
var config = await _configService.AddVariableAsync(id, variable);
|
||||
return Ok(MapConfigToDto(config));
|
||||
}
|
||||
catch (KeyNotFoundException ex)
|
||||
{
|
||||
return NotFound(new { error = ex.Message });
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return BadRequest(new { error = ex.Message });
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Xóa variable khỏi config
|
||||
/// </summary>
|
||||
[HttpDelete("{id:guid}/variables/{variableName}")]
|
||||
[ProducesResponseType(typeof(ConfigFileDto), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
public async Task<ActionResult<ConfigFileDto>> RemoveVariable(Guid id, string variableName)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(variableName))
|
||||
{
|
||||
return BadRequest(new { error = "Variable name cannot be empty" });
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var config = await _configService.RemoveVariableAsync(id, variableName);
|
||||
return Ok(MapConfigToDto(config));
|
||||
}
|
||||
catch (KeyNotFoundException ex)
|
||||
{
|
||||
return NotFound(new { error = ex.Message });
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return BadRequest(new { error = ex.Message });
|
||||
}
|
||||
}
|
||||
|
||||
// ==========================================
|
||||
// MAPPING HELPERS
|
||||
// ==========================================
|
||||
|
||||
private static ConfigFileDto MapConfigToDto(ConfigFile config)
|
||||
{
|
||||
return new ConfigFileDto
|
||||
{
|
||||
Id = config.Id,
|
||||
ConfigType = config.ConfigType,
|
||||
Variables = [.. config.Variables.Select(MapVariableToDto)],
|
||||
CreatedAt = config.CreatedAt,
|
||||
UpdatedAt = config.UpdatedAt,
|
||||
Description = config.Description
|
||||
};
|
||||
}
|
||||
|
||||
private static ConfigFileMetadataDto MapMetadataToDto(ConfigFileMetadata metadata)
|
||||
{
|
||||
return new ConfigFileMetadataDto
|
||||
{
|
||||
Id = metadata.Id,
|
||||
ConfigType = metadata.ConfigType,
|
||||
CreatedAt = metadata.CreatedAt,
|
||||
UpdatedAt = metadata.UpdatedAt,
|
||||
Description = metadata.Description
|
||||
};
|
||||
}
|
||||
|
||||
private static ConfigVariableDto MapVariableToDto(ConfigVariable variable)
|
||||
{
|
||||
return new ConfigVariableDto
|
||||
{
|
||||
Name = variable.Name,
|
||||
Type = variable.Type.ToString().ToLower(),
|
||||
Value = variable.Value,
|
||||
Min = variable.Min,
|
||||
Max = variable.Max,
|
||||
Roles = variable.Roles,
|
||||
EnumValues = variable.EnumValues
|
||||
};
|
||||
}
|
||||
|
||||
private static ConfigVariable MapDtoToVariable(ConfigVariableDto dto)
|
||||
{
|
||||
// Parse type string to enum
|
||||
var type = VariableTypeConverter.ParseType(dto.Type);
|
||||
|
||||
return new ConfigVariable
|
||||
{
|
||||
Name = dto.Name,
|
||||
Type = type,
|
||||
Value = dto.Value,
|
||||
Min = dto.Min,
|
||||
Max = dto.Max,
|
||||
Roles = dto.Roles ?? string.Empty,
|
||||
EnumValues = dto.EnumValues
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
namespace RobotNet10.CustomConfiguration.DTOs;
|
||||
|
||||
/// <summary>
|
||||
/// DTO cho ConfigFile (response)
|
||||
/// </summary>
|
||||
public class ConfigFileDto
|
||||
{
|
||||
public Guid Id { get; set; }
|
||||
public string ConfigType { get; set; } = string.Empty; // Tên định danh duy nhất
|
||||
public List<ConfigVariableDto> Variables { get; set; } = [];
|
||||
public DateTime CreatedAt { get; set; }
|
||||
public DateTime UpdatedAt { get; set; }
|
||||
public string? Description { get; set; }
|
||||
}
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
namespace RobotNet10.CustomConfiguration.DTOs;
|
||||
|
||||
/// <summary>
|
||||
/// DTO cho ConfigFileMetadata (list view)
|
||||
/// </summary>
|
||||
public class ConfigFileMetadataDto
|
||||
{
|
||||
public Guid Id { get; set; }
|
||||
public string ConfigType { get; set; } = string.Empty; // Tên định danh duy nhất
|
||||
public DateTime CreatedAt { get; set; }
|
||||
public DateTime UpdatedAt { get; set; }
|
||||
public string? Description { get; set; }
|
||||
}
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
namespace RobotNet10.CustomConfiguration.DTOs;
|
||||
|
||||
/// <summary>
|
||||
/// DTO cho ConfigVariable
|
||||
/// </summary>
|
||||
public class ConfigVariableDto
|
||||
{
|
||||
public string Name { get; set; } = string.Empty;
|
||||
public string Type { get; set; } = string.Empty; // "string", "int", "double", "bool", "object", "array", "enum"
|
||||
public object? Value { get; set; }
|
||||
|
||||
// Optional properties
|
||||
public double? Min { get; set; } // Cho int và double (0 nếu không dùng)
|
||||
public double? Max { get; set; } // Cho int và double (0 nếu không dùng)
|
||||
public string Roles { get; set; } = string.Empty; // Roles string (có thể empty)
|
||||
public List<string>? EnumValues { get; set; } // Cho type Enum - danh sách các giá trị cho phép
|
||||
}
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
using RobotNet10.CustomConfiguration.DTOs;
|
||||
|
||||
namespace RobotNet10.CustomConfiguration.DTOs.Requests;
|
||||
|
||||
/// <summary>
|
||||
/// Request DTO cho tạo config mới
|
||||
/// </summary>
|
||||
public class CreateConfigRequest
|
||||
{
|
||||
public string ConfigType { get; set; } = string.Empty; // Tên định danh duy nhất (ví dụ: MQTTBrokerConfig)
|
||||
public List<ConfigVariableDto> Variables { get; set; } = [];
|
||||
public string? Description { get; set; }
|
||||
}
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
using RobotNet10.CustomConfiguration.DTOs;
|
||||
|
||||
namespace RobotNet10.CustomConfiguration.DTOs.Requests;
|
||||
|
||||
/// <summary>
|
||||
/// Request DTO cho update config
|
||||
/// </summary>
|
||||
public class UpdateConfigRequest
|
||||
{
|
||||
public List<ConfigVariableDto>? Variables { get; set; }
|
||||
public string? Description { get; set; }
|
||||
}
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
namespace RobotNet10.CustomConfiguration.DTOs.Requests;
|
||||
|
||||
/// <summary>
|
||||
/// Request DTO cho update variable value
|
||||
/// </summary>
|
||||
public class UpdateVariableRequest
|
||||
{
|
||||
public object? Value { get; set; }
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
namespace RobotNet10.CustomConfiguration.Events;
|
||||
|
||||
/// <summary>
|
||||
/// Event arguments cho ConfigChanged event
|
||||
/// </summary>
|
||||
public class ConfigChangedEventArgs : EventArgs
|
||||
{
|
||||
/// <summary>
|
||||
/// ConfigType của config đã thay đổi
|
||||
/// </summary>
|
||||
public string ConfigType { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// ID của config đã thay đổi
|
||||
/// </summary>
|
||||
public Guid ConfigId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Loại thay đổi: Created, Updated, Deleted, VariableAdded, VariableUpdated, VariableRemoved
|
||||
/// </summary>
|
||||
public ConfigChangeType ChangeType { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Tên variable nếu thay đổi liên quan đến variable (optional)
|
||||
/// </summary>
|
||||
public string? VariableName { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Loại thay đổi của config
|
||||
/// </summary>
|
||||
public enum ConfigChangeType
|
||||
{
|
||||
/// <summary>
|
||||
/// Config được tạo mới
|
||||
/// </summary>
|
||||
Created,
|
||||
|
||||
/// <summary>
|
||||
/// Config được cập nhật (metadata hoặc variables)
|
||||
/// </summary>
|
||||
Updated,
|
||||
|
||||
/// <summary>
|
||||
/// Config bị xóa
|
||||
/// </summary>
|
||||
Deleted,
|
||||
|
||||
/// <summary>
|
||||
/// Variable được thêm vào config
|
||||
/// </summary>
|
||||
VariableAdded,
|
||||
|
||||
/// <summary>
|
||||
/// Variable được cập nhật
|
||||
/// </summary>
|
||||
VariableUpdated,
|
||||
|
||||
/// <summary>
|
||||
/// Variable bị xóa khỏi config
|
||||
/// </summary>
|
||||
VariableRemoved
|
||||
}
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using RobotNet10.CustomConfiguration.Services;
|
||||
using RobotNet10.StorageManager;
|
||||
|
||||
namespace RobotNet10.CustomConfiguration.Extensions;
|
||||
|
||||
/// <summary>
|
||||
/// Extension methods cho dependency injection
|
||||
/// </summary>
|
||||
public static class ServiceCollectionExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Đăng ký các services cho CustomConfiguration
|
||||
/// StorageConfig sẽ được inject từ project sử dụng thư viện này
|
||||
/// </summary>
|
||||
public static IServiceCollection AddCustomConfiguration(
|
||||
this IServiceCollection services,
|
||||
StorageConfig storageConfig)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(storageConfig);
|
||||
|
||||
// Register StorageConfig as named options (matching ConfigService constructor)
|
||||
services.Configure<StorageConfig>("StorageConfigs", options =>
|
||||
{
|
||||
options.UsingLocal = storageConfig.UsingLocal;
|
||||
options.LocalFolder = storageConfig.LocalFolder;
|
||||
options.Bucket = storageConfig.Bucket;
|
||||
options.MinioConfig = storageConfig.MinioConfig;
|
||||
options.RetryCount = storageConfig.RetryCount;
|
||||
});
|
||||
|
||||
// Register ConfigService (creates StorageManager internally)
|
||||
services.AddSingleton<IConfigService, ConfigService>();
|
||||
|
||||
// Register ConfigManager (depends on IConfigService)
|
||||
services.AddSingleton<IConfigManager, ConfigManager>();
|
||||
|
||||
return services;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Đăng ký các services cho CustomConfiguration với StorageConfig từ IConfiguration
|
||||
/// </summary>
|
||||
public static IServiceCollection AddCustomConfiguration(
|
||||
this IServiceCollection services,
|
||||
IConfiguration configuration,
|
||||
string configSectionName = "StorageConfig")
|
||||
{
|
||||
services.Configure<StorageConfig>("StorageConfigs", options =>
|
||||
{
|
||||
configuration.GetSection(configSectionName).Bind(options);
|
||||
});
|
||||
|
||||
// Register ConfigService (creates StorageManager internally)
|
||||
services.AddSingleton<IConfigService, ConfigService>();
|
||||
|
||||
// Register ConfigManager (depends on IConfigService)
|
||||
services.AddSingleton<IConfigManager, ConfigManager>();
|
||||
return services;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,281 @@
|
||||
using RobotNet10.CustomConfiguration.Models;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace RobotNet10.CustomConfiguration.Helpers;
|
||||
|
||||
/// <summary>
|
||||
/// Helper class cho parse và serialize JSON config files
|
||||
/// Format JSON mới: Object với metadata và variables
|
||||
/// Format: {"id": "guid", "configType": "...", "createdAt": "...", "updatedAt": "...", "description": "...", "variables": [...]}
|
||||
/// </summary>
|
||||
public static class JsonConfigParser
|
||||
{
|
||||
private static readonly JsonSerializerOptions JsonOptions = new()
|
||||
{
|
||||
Encoder = System.Text.Encodings.Web.JavaScriptEncoder.UnsafeRelaxedJsonEscaping,
|
||||
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
|
||||
PropertyNameCaseInsensitive = true,
|
||||
DefaultIgnoreCondition = JsonIgnoreCondition.Never,
|
||||
WriteIndented = true
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Parse JSON stream thành ConfigFile (format mới với metadata)
|
||||
/// </summary>
|
||||
public static ConfigFile ParseConfigFile(Stream jsonStream)
|
||||
{
|
||||
using var reader = new StreamReader(jsonStream);
|
||||
var json = reader.ReadToEnd();
|
||||
return ParseConfigFile(json);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse JSON string thành ConfigFile (format mới với metadata)
|
||||
/// </summary>
|
||||
public static ConfigFile ParseConfigFile(string json)
|
||||
{
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(json, nameof(json));
|
||||
|
||||
using var jsonDoc = JsonDocument.Parse(json);
|
||||
var root = jsonDoc.RootElement;
|
||||
|
||||
if (root.ValueKind != JsonValueKind.Object)
|
||||
{
|
||||
throw new ArgumentException("JSON must be an object with metadata and variables");
|
||||
}
|
||||
|
||||
// Parse metadata
|
||||
var id = root.TryGetProperty("id", out var idProp) && idProp.ValueKind == JsonValueKind.String
|
||||
? Guid.Parse(idProp.GetString() ?? throw new ArgumentException("Invalid id format"))
|
||||
: throw new ArgumentException("id is required");
|
||||
|
||||
var configType = root.GetProperty("configType").GetString()
|
||||
?? throw new ArgumentException("configType is required");
|
||||
|
||||
var createdAt = root.TryGetProperty("createdAt", out var createdAtProp) && createdAtProp.ValueKind == JsonValueKind.String
|
||||
? DateTime.Parse(createdAtProp.GetString() ?? throw new ArgumentException("Invalid createdAt format"))
|
||||
: DateTime.UtcNow;
|
||||
|
||||
var updatedAt = root.TryGetProperty("updatedAt", out var updatedAtProp) && updatedAtProp.ValueKind == JsonValueKind.String
|
||||
? DateTime.Parse(updatedAtProp.GetString() ?? throw new ArgumentException("Invalid updatedAt format"))
|
||||
: DateTime.UtcNow;
|
||||
|
||||
var description = root.TryGetProperty("description", out var descProp) && descProp.ValueKind == JsonValueKind.String
|
||||
? descProp.GetString()
|
||||
: null;
|
||||
|
||||
// Parse variables
|
||||
if (!root.TryGetProperty("variables", out var variablesProp) || variablesProp.ValueKind != JsonValueKind.Array)
|
||||
{
|
||||
throw new ArgumentException("variables array is required");
|
||||
}
|
||||
|
||||
var variables = ParseVariablesArray(variablesProp);
|
||||
|
||||
return new ConfigFile
|
||||
{
|
||||
Id = id,
|
||||
ConfigType = configType,
|
||||
Variables = variables,
|
||||
CreatedAt = createdAt,
|
||||
UpdatedAt = updatedAt,
|
||||
Description = description,
|
||||
FilePath = $"configs/{configType}.config.json"
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse variables array từ JSON element
|
||||
/// </summary>
|
||||
private static List<ConfigVariable> ParseVariablesArray(JsonElement variablesElement)
|
||||
{
|
||||
var variables = new List<ConfigVariable>();
|
||||
|
||||
foreach (var element in variablesElement.EnumerateArray())
|
||||
{
|
||||
var variable = new ConfigVariable
|
||||
{
|
||||
Name = element.GetProperty("name").GetString() ?? throw new ArgumentException("Variable name is required"),
|
||||
Type = VariableTypeConverter.ParseType(element.GetProperty("type").GetString() ?? throw new ArgumentException("Variable type is required")),
|
||||
Value = ParseValue(element),
|
||||
Min = element.TryGetProperty("min", out var minProp) && minProp.ValueKind != JsonValueKind.Null
|
||||
? minProp.GetDouble()
|
||||
: null,
|
||||
Max = element.TryGetProperty("max", out var maxProp) && maxProp.ValueKind != JsonValueKind.Null
|
||||
? maxProp.GetDouble()
|
||||
: null,
|
||||
Roles = element.TryGetProperty("roles", out var rolesProp)
|
||||
? rolesProp.GetString() ?? string.Empty
|
||||
: string.Empty,
|
||||
EnumValues = element.TryGetProperty("enumValues", out var enumProp) && enumProp.ValueKind == JsonValueKind.Array
|
||||
? [.. enumProp.EnumerateArray().Select(e => e.GetString() ?? string.Empty)]
|
||||
: null
|
||||
};
|
||||
|
||||
variables.Add(variable);
|
||||
}
|
||||
|
||||
return variables;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse JSON stream thành list of ConfigVariable (backward compatibility cho import từ format cũ)
|
||||
/// Format cũ: [{"name": "port", "type": "int", "value": 8080, "Min": 0, "Max": 65535, "Roles": ""}, ...]
|
||||
/// </summary>
|
||||
public static List<ConfigVariable> ParseVariables(Stream jsonStream)
|
||||
{
|
||||
using var reader = new StreamReader(jsonStream);
|
||||
var json = reader.ReadToEnd();
|
||||
return ParseVariables(json);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse JSON string thành list of ConfigVariable (backward compatibility cho import từ format cũ)
|
||||
/// </summary>
|
||||
public static List<ConfigVariable> ParseVariables(string json)
|
||||
{
|
||||
using var jsonDoc = JsonDocument.Parse(json);
|
||||
var root = jsonDoc.RootElement;
|
||||
|
||||
// Check if it's new format (object) or old format (array)
|
||||
if (root.ValueKind == JsonValueKind.Object)
|
||||
{
|
||||
// New format - extract variables
|
||||
if (root.TryGetProperty("variables", out var variablesProp) && variablesProp.ValueKind == JsonValueKind.Array)
|
||||
{
|
||||
return ParseVariablesArray(variablesProp);
|
||||
}
|
||||
throw new ArgumentException("JSON object must contain 'variables' array");
|
||||
}
|
||||
|
||||
if (root.ValueKind != JsonValueKind.Array)
|
||||
{
|
||||
throw new ArgumentException("JSON must be an array of variables (old format) or object with metadata and variables (new format)");
|
||||
}
|
||||
|
||||
// Old format - array of variables
|
||||
return ParseVariablesArray(root);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse value từ JSON element theo type
|
||||
/// </summary>
|
||||
private static object? ParseValue(JsonElement element)
|
||||
{
|
||||
if (!element.TryGetProperty("value", out var valueProp))
|
||||
{
|
||||
throw new ArgumentException("Variable value is required");
|
||||
}
|
||||
|
||||
var typeStr = element.GetProperty("type").GetString()?.ToLower();
|
||||
return typeStr switch
|
||||
{
|
||||
"string" => valueProp.GetString(),
|
||||
"int" => valueProp.GetInt32(),
|
||||
"double" => valueProp.GetDouble(),
|
||||
"bool" => valueProp.GetBoolean(),
|
||||
"object" => ParseJsonObject(valueProp),
|
||||
"array" => ParseJsonArray(valueProp),
|
||||
"enum" => valueProp.GetString(), // Enum values are strings
|
||||
_ => throw new ArgumentException($"Unsupported type: {typeStr}")
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse JSON object thành Dictionary<string, object>
|
||||
/// </summary>
|
||||
private static Dictionary<string, object> ParseJsonObject(JsonElement element)
|
||||
{
|
||||
if (element.ValueKind != JsonValueKind.Object)
|
||||
{
|
||||
throw new ArgumentException("Value must be a JSON object");
|
||||
}
|
||||
|
||||
return JsonSerializer.Deserialize<Dictionary<string, object>>( element.GetRawText()) ?? [];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse JSON array thành List<object>
|
||||
/// </summary>
|
||||
private static List<object> ParseJsonArray(JsonElement element)
|
||||
{
|
||||
if (element.ValueKind != JsonValueKind.Array)
|
||||
{
|
||||
throw new ArgumentException("Value must be a JSON array");
|
||||
}
|
||||
|
||||
var result = new List<object>();
|
||||
foreach (var item in element.EnumerateArray())
|
||||
{
|
||||
result.Add(item.GetRawText());
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Serialize ConfigFile thành JSON string (format mới với metadata và variables)
|
||||
/// </summary>
|
||||
public static string SerializeConfigFile(ConfigFile config)
|
||||
{
|
||||
var jsonObject = new
|
||||
{
|
||||
id = config.Id,
|
||||
configType = config.ConfigType,
|
||||
createdAt = config.CreatedAt.ToString("O"), // ISO 8601 format
|
||||
updatedAt = config.UpdatedAt.ToString("O"), // ISO 8601 format
|
||||
description = config.Description,
|
||||
variables = config.Variables.Select(v => new
|
||||
{
|
||||
name = v.Name,
|
||||
type = v.Type.ToString().ToLower(),
|
||||
value = SerializeValue(v.Value, v.Type),
|
||||
min = v.Min,
|
||||
max = v.Max,
|
||||
roles = v.Roles ?? string.Empty,
|
||||
enumValues = v.EnumValues
|
||||
}).ToArray()
|
||||
};
|
||||
|
||||
return JsonSerializer.Serialize(jsonObject, JsonOptions);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Serialize list of ConfigVariable thành JSON string (chỉ variables, dùng cho export backward compatibility)
|
||||
/// </summary>
|
||||
public static string SerializeVariables(List<ConfigVariable> variables)
|
||||
{
|
||||
var jsonArray = variables.Select(v => new
|
||||
{
|
||||
name = v.Name,
|
||||
type = v.Type.ToString().ToLower(),
|
||||
value = SerializeValue(v.Value, v.Type),
|
||||
min = v.Min,
|
||||
max = v.Max,
|
||||
roles = v.Roles ?? string.Empty,
|
||||
enumValues = v.EnumValues
|
||||
}).ToArray();
|
||||
|
||||
return JsonSerializer.Serialize(jsonArray, JsonOptions);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Serialize value theo type (đặc biệt cho Object và Array)
|
||||
/// </summary>
|
||||
private static object SerializeValue(object? value, ConfigVariableType type)
|
||||
{
|
||||
if (value == null) return null!;
|
||||
|
||||
return type switch
|
||||
{
|
||||
ConfigVariableType.Object => value is Dictionary<string, object> dict
|
||||
? dict
|
||||
: System.Text.Json.JsonSerializer.Deserialize<Dictionary<string, object>>(value.ToString() ?? "{}", JsonOptions) ?? [],
|
||||
ConfigVariableType.Array => value is List<object> list
|
||||
? list
|
||||
: System.Text.Json.JsonSerializer.Deserialize<List<object>>(value.ToString() ?? "[]", JsonOptions) ?? [],
|
||||
_ => value
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,180 @@
|
||||
using RobotNet10.CustomConfiguration.Models;
|
||||
using System.Globalization;
|
||||
|
||||
namespace RobotNet10.CustomConfiguration.Helpers;
|
||||
|
||||
/// <summary>
|
||||
/// Helper class cho convert giữa variable type và value
|
||||
/// </summary>
|
||||
public static class VariableTypeConverter
|
||||
{
|
||||
/// <summary>
|
||||
/// Convert string type name thành ConfigVariableType enum
|
||||
/// </summary>
|
||||
public static ConfigVariableType ParseType(string typeName)
|
||||
{
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(typeName, nameof(typeName));
|
||||
|
||||
return typeName.ToLower() switch
|
||||
{
|
||||
"string" => ConfigVariableType.String,
|
||||
"int" => ConfigVariableType.Int,
|
||||
"double" => ConfigVariableType.Double,
|
||||
"bool" => ConfigVariableType.Bool,
|
||||
"object" => ConfigVariableType.Object,
|
||||
"array" => ConfigVariableType.Array,
|
||||
"enum" => ConfigVariableType.Enum,
|
||||
_ => throw new ArgumentException($"Invalid variable type: {typeName}")
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Convert value sang đúng type
|
||||
/// </summary>
|
||||
public static object ConvertValue(ConfigVariableType type, object? value)
|
||||
{
|
||||
if (value == null)
|
||||
throw new ArgumentNullException(nameof(value), "Value cannot be null");
|
||||
|
||||
return type switch
|
||||
{
|
||||
ConfigVariableType.String => value.ToString() ?? string.Empty,
|
||||
ConfigVariableType.Int => Convert.ToInt32(value, CultureInfo.InvariantCulture),
|
||||
ConfigVariableType.Double => Convert.ToDouble(value, CultureInfo.InvariantCulture),
|
||||
ConfigVariableType.Bool => Convert.ToBoolean(value, CultureInfo.InvariantCulture),
|
||||
ConfigVariableType.Object => ConvertToObject(value),
|
||||
ConfigVariableType.Array => ConvertToArray(value),
|
||||
ConfigVariableType.Enum => value.ToString() ?? string.Empty, // Enum values are strings
|
||||
_ => throw new ArgumentException($"Unsupported type: {type}")
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Convert value thành Dictionary<string, object> (Object type)
|
||||
/// </summary>
|
||||
private static Dictionary<string, object> ConvertToObject(object value)
|
||||
{
|
||||
if (value is Dictionary<string, object> dict)
|
||||
return dict;
|
||||
|
||||
if (value is System.Text.Json.JsonElement jsonElement && jsonElement.ValueKind == System.Text.Json.JsonValueKind.Object)
|
||||
{
|
||||
var result = new Dictionary<string, object>();
|
||||
foreach (var prop in jsonElement.EnumerateObject())
|
||||
{
|
||||
result[prop.Name] = prop.Value.GetRawText();
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
// Try parse as JSON string
|
||||
var jsonString = value.ToString();
|
||||
if (!string.IsNullOrEmpty(jsonString))
|
||||
{
|
||||
using var doc = System.Text.Json.JsonDocument.Parse(jsonString);
|
||||
if (doc.RootElement.ValueKind == System.Text.Json.JsonValueKind.Object)
|
||||
{
|
||||
var result = new Dictionary<string, object>();
|
||||
foreach (var prop in doc.RootElement.EnumerateObject())
|
||||
{
|
||||
result[prop.Name] = prop.Value.GetRawText();
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
throw new ArgumentException("Value cannot be converted to Object type");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Convert value thành List<object> (Array type)
|
||||
/// </summary>
|
||||
private static List<object> ConvertToArray(object value)
|
||||
{
|
||||
if (value is List<object> list)
|
||||
return list;
|
||||
|
||||
if (value is System.Text.Json.JsonElement jsonElement && jsonElement.ValueKind == System.Text.Json.JsonValueKind.Array)
|
||||
{
|
||||
var result = new List<object>();
|
||||
foreach (var item in jsonElement.EnumerateArray())
|
||||
{
|
||||
result.Add(item.GetRawText());
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
// Try parse as JSON string
|
||||
var jsonString = value.ToString();
|
||||
if (!string.IsNullOrEmpty(jsonString))
|
||||
{
|
||||
using var doc = System.Text.Json.JsonDocument.Parse(jsonString);
|
||||
if (doc.RootElement.ValueKind == System.Text.Json.JsonValueKind.Array)
|
||||
{
|
||||
var result = new List<object>();
|
||||
foreach (var item in doc.RootElement.EnumerateArray())
|
||||
{
|
||||
result.Add(item.GetRawText());
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
throw new ArgumentException("Value cannot be converted to Array type");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validate value có đúng type không
|
||||
/// </summary>
|
||||
public static bool IsValidValue(ConfigVariableType type, object? value)
|
||||
{
|
||||
if (value == null) return false;
|
||||
|
||||
return type switch
|
||||
{
|
||||
ConfigVariableType.String => value is string,
|
||||
ConfigVariableType.Int => value is int || int.TryParse(value.ToString(), NumberStyles.Integer, CultureInfo.InvariantCulture, out _),
|
||||
ConfigVariableType.Double => value is double || double.TryParse(value.ToString(), NumberStyles.Float | NumberStyles.AllowThousands, CultureInfo.InvariantCulture, out _),
|
||||
ConfigVariableType.Bool => value is bool || bool.TryParse(value.ToString(), out _),
|
||||
ConfigVariableType.Object => value is Dictionary<string, object> ||
|
||||
(value is System.Text.Json.JsonElement je && je.ValueKind == System.Text.Json.JsonValueKind.Object) ||
|
||||
TryParseJsonObject(value),
|
||||
ConfigVariableType.Array => value is List<object> ||
|
||||
(value is System.Text.Json.JsonElement je && je.ValueKind == System.Text.Json.JsonValueKind.Array) ||
|
||||
TryParseJsonArray(value),
|
||||
ConfigVariableType.Enum => value is string, // Enum values are always strings
|
||||
_ => false
|
||||
};
|
||||
}
|
||||
|
||||
private static bool TryParseJsonObject(object value)
|
||||
{
|
||||
try
|
||||
{
|
||||
var jsonString = value.ToString();
|
||||
if (string.IsNullOrEmpty(jsonString)) return false;
|
||||
using var doc = System.Text.Json.JsonDocument.Parse(jsonString);
|
||||
return doc.RootElement.ValueKind == System.Text.Json.JsonValueKind.Object;
|
||||
}
|
||||
catch (System.Text.Json.JsonException)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private static bool TryParseJsonArray(object value)
|
||||
{
|
||||
try
|
||||
{
|
||||
var jsonString = value.ToString();
|
||||
if (string.IsNullOrEmpty(jsonString)) return false;
|
||||
using var doc = System.Text.Json.JsonDocument.Parse(jsonString);
|
||||
return doc.RootElement.ValueKind == System.Text.Json.JsonValueKind.Array;
|
||||
}
|
||||
catch (System.Text.Json.JsonException)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
namespace RobotNet10.CustomConfiguration.Models;
|
||||
|
||||
/// <summary>
|
||||
/// Model đại diện cho một file config
|
||||
/// ConfigType là tên định danh duy nhất (ví dụ: MQTTBrokerConfig)
|
||||
/// </summary>
|
||||
public class ConfigFile
|
||||
{
|
||||
public Guid Id { get; set; }
|
||||
public string ConfigType { get; set; } = string.Empty; // Tên định danh duy nhất (ví dụ: MQTTBrokerConfig)
|
||||
public string FilePath { get; set; } = string.Empty; // Path trong StorageManager
|
||||
public List<ConfigVariable> Variables { get; set; } = [];
|
||||
public DateTime CreatedAt { get; set; }
|
||||
public DateTime UpdatedAt { get; set; }
|
||||
public string? Description { get; set; }
|
||||
}
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
namespace RobotNet10.CustomConfiguration.Models;
|
||||
|
||||
/// <summary>
|
||||
/// Metadata của config file (không bao gồm nội dung variables)
|
||||
/// </summary>
|
||||
public class ConfigFileMetadata
|
||||
{
|
||||
public Guid Id { get; set; }
|
||||
public string ConfigType { get; set; } = string.Empty; // Tên định danh duy nhất
|
||||
public DateTime CreatedAt { get; set; }
|
||||
public DateTime UpdatedAt { get; set; }
|
||||
public string? Description { get; set; }
|
||||
}
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
namespace RobotNet10.CustomConfiguration.Models;
|
||||
|
||||
/// <summary>
|
||||
/// Model đại diện cho một variable trong config
|
||||
/// </summary>
|
||||
public class ConfigVariable
|
||||
{
|
||||
public string Name { get; set; } = string.Empty;
|
||||
public ConfigVariableType Type { get; set; }
|
||||
public object? Value { get; set; }
|
||||
|
||||
// Optional properties
|
||||
public double? Min { get; set; } // Cho int và double
|
||||
public double? Max { get; set; } // Cho int và double
|
||||
public string Roles { get; set; } = string.Empty; // Roles string (có thể empty)
|
||||
public List<string>? EnumValues { get; set; } // Cho type Enum - danh sách các giá trị cho phép
|
||||
}
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
namespace RobotNet10.CustomConfiguration.Models;
|
||||
|
||||
/// <summary>
|
||||
/// Enum định nghĩa các loại variable type được hỗ trợ
|
||||
/// </summary>
|
||||
public enum ConfigVariableType
|
||||
{
|
||||
String,
|
||||
Int,
|
||||
Double,
|
||||
Bool,
|
||||
Object, // JSON object (Dictionary<string, object>)
|
||||
Array, // JSON array (List<object>)
|
||||
Enum // Enum với các giá trị được định nghĩa trong EnumValues
|
||||
}
|
||||
|
||||
785
srcs/RobotNet10/Commons/RobotNet10.CustomConfiguration/README.md
Normal file
785
srcs/RobotNet10/Commons/RobotNet10.CustomConfiguration/README.md
Normal file
@@ -0,0 +1,785 @@
|
||||
# RobotNet10.CustomConfiguration
|
||||
|
||||
Thư viện quản lý cấu hình động cho ứng dụng RobotNet10, cho phép import/export, chỉnh sửa và quản lý các file cấu hình dạng JSON với hỗ trợ nhiều kiểu dữ liệu.
|
||||
|
||||
## 📋 Mục lục
|
||||
|
||||
- [Tính năng](#tính-năng)
|
||||
- [Cấu trúc Project](#cấu-trúc-project)
|
||||
- [Cài đặt và Cấu hình](#cài-đặt-và-cấu-hình)
|
||||
- [Hướng dẫn sử dụng Backend](#hướng-dẫn-sử-dụng-backend)
|
||||
- [Hướng dẫn sử dụng Frontend](#hướng-dẫn-sử-dụng-frontend)
|
||||
- [Format JSON Config](#format-json-config)
|
||||
- [API Endpoints](#api-endpoints)
|
||||
- [Ví dụ sử dụng](#ví-dụ-sử-dụng)
|
||||
- [Troubleshooting](#troubleshooting)
|
||||
|
||||
## ✨ Tính năng
|
||||
|
||||
- ✅ **Import/Export Config**: Import và export các file cấu hình dạng JSON
|
||||
- ✅ **Quản lý Config Files**: Tạo, đọc, cập nhật, xóa các file cấu hình
|
||||
- ✅ **Quản lý Variables**: Thêm, sửa, xóa các biến trong config
|
||||
- ✅ **Hỗ trợ nhiều kiểu dữ liệu**: String, Int, Double, Bool, Object, Array, Enum
|
||||
- ✅ **Validation**: Kiểm tra tính hợp lệ của dữ liệu theo type và constraints
|
||||
- ✅ **Tìm kiếm**: Tìm kiếm config theo tên, loại
|
||||
- ✅ **UI Component**: Component Blazor sẵn có để quản lý config qua giao diện
|
||||
- ✅ **Storage Manager**: Tích hợp với RobotNet10.StorageManager (Local hoặc MinIO)
|
||||
|
||||
## 📁 Cấu trúc Project
|
||||
|
||||
```
|
||||
RobotNet10.CustomConfiguration/
|
||||
├── Controllers/
|
||||
│ └── ConfigController.cs # REST API Controller
|
||||
├── DTOs/
|
||||
│ ├── ConfigFileDto.cs
|
||||
│ ├── ConfigFileMetadataDto.cs
|
||||
│ ├── ConfigVariableDto.cs
|
||||
│ └── Requests/
|
||||
│ ├── CreateConfigRequest.cs
|
||||
│ ├── UpdateConfigRequest.cs
|
||||
│ └── UpdateVariableRequest.cs
|
||||
├── Extensions/
|
||||
│ └── ServiceCollectionExtensions.cs # DI Extension methods
|
||||
├── Helpers/
|
||||
│ ├── JsonConfigParser.cs # Parse/Serialize JSON
|
||||
│ └── VariableTypeConverter.cs # Convert variable types
|
||||
├── Models/
|
||||
│ ├── ConfigFile.cs
|
||||
│ ├── ConfigFileMetadata.cs
|
||||
│ ├── ConfigVariable.cs
|
||||
│ └── ConfigVariableType.cs
|
||||
├── Services/
|
||||
│ ├── IConfigService.cs
|
||||
│ ├── ConfigService.cs
|
||||
│ ├── ConfigService.Implementation.cs
|
||||
│ └── ConfigService.Metadata.cs
|
||||
└── Validators/
|
||||
└── ConfigValidator.cs # Validation logic
|
||||
```
|
||||
|
||||
## 🚀 Cài đặt và Cấu hình
|
||||
|
||||
### 1. Thêm Project Reference
|
||||
|
||||
Thêm reference vào project của bạn:
|
||||
|
||||
```xml
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Commons\RobotNet10.CustomConfiguration\RobotNet10.CustomConfiguration.csproj" />
|
||||
</ItemGroup>
|
||||
```
|
||||
|
||||
### 2. Cấu hình Backend (ASP.NET Core)
|
||||
|
||||
#### Bước 1: Thêm using trong `Program.cs`
|
||||
|
||||
```csharp
|
||||
using RobotNet10.CustomConfiguration.Extensions;
|
||||
using RobotNet10.StorageManager;
|
||||
```
|
||||
|
||||
#### Bước 2: Đăng ký Services
|
||||
|
||||
**Cách 1: Từ appsettings.json (Khuyến nghị)**
|
||||
|
||||
```csharp
|
||||
// Trong Program.cs
|
||||
builder.Services.AddCustomConfiguration(builder.Configuration, "StorageConfig");
|
||||
```
|
||||
|
||||
Thêm vào `appsettings.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"StorageConfig": {
|
||||
"UsingLocal": true,
|
||||
"LocalFolder": "Configs",
|
||||
"Bucket": "",
|
||||
"RetryCount": 3,
|
||||
"MinioConfig": {
|
||||
"Endpoint": "localhost:9000",
|
||||
"User": "minioadmin",
|
||||
"Password": "minioadmin",
|
||||
"EnableSSL": false
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Cách 2: Trực tiếp trong code**
|
||||
|
||||
```csharp
|
||||
var storageConfig = new StorageConfig
|
||||
{
|
||||
UsingLocal = true,
|
||||
LocalFolder = "Configs",
|
||||
Bucket = "",
|
||||
RetryCount = 3
|
||||
};
|
||||
|
||||
builder.Services.AddCustomConfiguration(storageConfig);
|
||||
```
|
||||
|
||||
#### Bước 3: Đảm bảo đã map Controllers
|
||||
|
||||
```csharp
|
||||
var app = builder.Build();
|
||||
|
||||
// ... middleware ...
|
||||
|
||||
app.MapControllers(); // Đảm bảo có dòng này
|
||||
|
||||
app.Run();
|
||||
```
|
||||
|
||||
### 3. Cấu hình Frontend (Blazor)
|
||||
|
||||
#### Bước 1: Thêm Project Reference
|
||||
|
||||
```xml
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Components\RobotNet10.CustomConfigurationEditor\RobotNet10.CustomConfigurationEditor.csproj" />
|
||||
</ItemGroup>
|
||||
```
|
||||
|
||||
#### Bước 2: Đăng ký Services trong `Program.cs` hoặc `Client/Program.cs`
|
||||
|
||||
```csharp
|
||||
using RobotNet10.CustomConfigurationEditor.Services.API;
|
||||
using RobotNet10.CustomConfigurationEditor.Services.State;
|
||||
|
||||
// HttpClient (nếu chưa có)
|
||||
builder.Services.AddScoped(sp => new HttpClient
|
||||
{
|
||||
BaseAddress = new Uri(builder.HostEnvironment.BaseAddress)
|
||||
});
|
||||
|
||||
// MudBlazor (nếu chưa có)
|
||||
builder.Services.AddMudServices();
|
||||
|
||||
// CustomConfiguration Services
|
||||
builder.Services.AddScoped<ConfigApiService>();
|
||||
builder.Services.AddScoped<ConfigManagerState>();
|
||||
```
|
||||
|
||||
#### Bước 3: Copy JavaScript file
|
||||
|
||||
Copy file `downloadFile.js` từ `RobotNet10.CustomConfigurationEditor/wwwroot/js/downloadFile.js` vào `wwwroot/js/` của project frontend.
|
||||
|
||||
Thêm vào `index.html` hoặc `App.razor`:
|
||||
|
||||
```html
|
||||
<script src="js/downloadFile.js"></script>
|
||||
```
|
||||
|
||||
#### Bước 4: Thêm using trong `_Imports.razor`
|
||||
|
||||
```razor
|
||||
@using RobotNet10.CustomConfigurationEditor.Components.ConfigManager
|
||||
```
|
||||
|
||||
## 📖 Hướng dẫn sử dụng Backend
|
||||
|
||||
### Sử dụng IConfigService
|
||||
|
||||
Inject `IConfigService` vào service hoặc controller của bạn:
|
||||
|
||||
```csharp
|
||||
public class MyService
|
||||
{
|
||||
private readonly IConfigService _configService;
|
||||
|
||||
public MyService(IConfigService configService)
|
||||
{
|
||||
_configService = configService;
|
||||
}
|
||||
|
||||
public async Task<ConfigFile> GetMqttConfigAsync()
|
||||
{
|
||||
return await _configService.GetConfigByTypeAsync("MQTTBrokerConfig");
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Các phương thức chính
|
||||
|
||||
```csharp
|
||||
// Lấy tất cả configs (metadata)
|
||||
var configs = await _configService.GetAllConfigsAsync();
|
||||
|
||||
// Lấy config theo ID
|
||||
var config = await _configService.GetConfigByIdAsync(id);
|
||||
|
||||
// Lấy config theo ConfigType
|
||||
var mqttConfig = await _configService.GetConfigByTypeAsync("MQTTBrokerConfig");
|
||||
|
||||
// Tạo config mới
|
||||
var newConfig = await _configService.CreateConfigAsync(
|
||||
configType: "MyConfig",
|
||||
variables: variables,
|
||||
description: "My configuration"
|
||||
);
|
||||
|
||||
// Cập nhật config
|
||||
await _configService.UpdateConfigAsync(id, variables, description);
|
||||
|
||||
// Xóa config
|
||||
await _configService.DeleteConfigAsync(id);
|
||||
|
||||
// Import từ file
|
||||
using var stream = File.OpenRead("config.json");
|
||||
var imported = await _configService.ImportConfigAsync(stream, "config.json", "MyConfig");
|
||||
|
||||
// Export ra file
|
||||
var exportStream = await _configService.ExportConfigAsync(id);
|
||||
|
||||
// Cập nhật variable
|
||||
await _configService.UpdateVariableAsync(id, "port", 8080);
|
||||
|
||||
// Thêm variable
|
||||
await _configService.AddVariableAsync(id, newVariable);
|
||||
|
||||
// Xóa variable
|
||||
await _configService.RemoveVariableAsync(id, "variableName");
|
||||
```
|
||||
|
||||
## 🎨 Hướng dẫn sử dụng Frontend
|
||||
|
||||
### Sử dụng Component
|
||||
|
||||
Tạo page mới hoặc thêm vào page hiện có:
|
||||
|
||||
```razor
|
||||
@page "/config-manager"
|
||||
@using RobotNet10.CustomConfigurationEditor.Components.ConfigManager
|
||||
|
||||
<ConfigManagerComponent />
|
||||
```
|
||||
|
||||
### Sử dụng ConfigManagerState
|
||||
|
||||
Inject `ConfigManagerState` vào component của bạn:
|
||||
|
||||
```razor
|
||||
@inject ConfigManagerState State
|
||||
|
||||
<MudButton OnClick="LoadConfigs">Load Configs</MudButton>
|
||||
|
||||
@code {
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
await State.LoadConfigsAsync();
|
||||
}
|
||||
|
||||
private async Task LoadConfigs()
|
||||
{
|
||||
await State.LoadConfigsAsync();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Các phương thức State
|
||||
|
||||
```csharp
|
||||
// Load tất cả configs
|
||||
await State.LoadConfigsAsync();
|
||||
|
||||
// Load với search query
|
||||
await State.LoadConfigsAsync("MQTT");
|
||||
|
||||
// Load config theo ID
|
||||
await State.LoadConfigByIdAsync(id);
|
||||
|
||||
// Load config theo ConfigType
|
||||
await State.LoadConfigByTypeAsync("MQTTBrokerConfig");
|
||||
|
||||
// Select config
|
||||
await State.SelectConfigAsync(configMetadata);
|
||||
|
||||
// Tạo config mới
|
||||
var config = await State.CreateConfigAsync(
|
||||
configType: "MyConfig",
|
||||
variables: variables,
|
||||
description: "My config"
|
||||
);
|
||||
|
||||
// Cập nhật config
|
||||
await State.UpdateConfigAsync(variables, description);
|
||||
|
||||
// Xóa config
|
||||
await State.DeleteConfigAsync(id);
|
||||
|
||||
// Import config
|
||||
using var stream = file.OpenReadStream();
|
||||
var imported = await State.ImportConfigAsync(stream, file.Name, "MyConfig");
|
||||
|
||||
// Export config
|
||||
var stream = await State.ExportConfigAsync(id);
|
||||
|
||||
// Cập nhật variable
|
||||
await State.UpdateVariableAsync("port", 8080);
|
||||
|
||||
// Thêm variable
|
||||
await State.AddVariableAsync(newVariable);
|
||||
|
||||
// Xóa variable
|
||||
await State.RemoveVariableAsync("variableName");
|
||||
```
|
||||
|
||||
## 📄 Format JSON Config
|
||||
|
||||
### Cấu trúc cơ bản
|
||||
|
||||
File config là một mảng JSON chứa các variable:
|
||||
|
||||
```json
|
||||
[
|
||||
{
|
||||
"name": "port",
|
||||
"type": "int",
|
||||
"value": 8080,
|
||||
"Min": 0,
|
||||
"Max": 65535,
|
||||
"Roles": ""
|
||||
},
|
||||
{
|
||||
"name": "host",
|
||||
"type": "string",
|
||||
"value": "localhost",
|
||||
"Roles": ""
|
||||
},
|
||||
{
|
||||
"name": "enableSSL",
|
||||
"type": "bool",
|
||||
"value": true,
|
||||
"Roles": ""
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
### Các kiểu dữ liệu hỗ trợ
|
||||
|
||||
#### 1. String
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "host",
|
||||
"type": "string",
|
||||
"value": "localhost",
|
||||
"Roles": ""
|
||||
}
|
||||
```
|
||||
|
||||
#### 2. Int
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "port",
|
||||
"type": "int",
|
||||
"value": 8080,
|
||||
"Min": 0,
|
||||
"Max": 65535,
|
||||
"Roles": ""
|
||||
}
|
||||
```
|
||||
|
||||
#### 3. Double
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "timeout",
|
||||
"type": "double",
|
||||
"value": 30.5,
|
||||
"Min": 0.0,
|
||||
"Max": 100.0,
|
||||
"Roles": ""
|
||||
}
|
||||
```
|
||||
|
||||
#### 4. Bool
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "enableSSL",
|
||||
"type": "bool",
|
||||
"value": true,
|
||||
"Roles": ""
|
||||
}
|
||||
```
|
||||
|
||||
#### 5. Enum
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "logLevel",
|
||||
"type": "enum",
|
||||
"value": "Info",
|
||||
"EnumValues": ["Debug", "Info", "Warning", "Error"],
|
||||
"Roles": ""
|
||||
}
|
||||
```
|
||||
|
||||
#### 6. Object
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "database",
|
||||
"type": "object",
|
||||
"value": {
|
||||
"host": "localhost",
|
||||
"port": 5432,
|
||||
"name": "mydb"
|
||||
},
|
||||
"Roles": ""
|
||||
}
|
||||
```
|
||||
|
||||
#### 7. Array
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "allowedIPs",
|
||||
"type": "array",
|
||||
"value": ["192.168.1.1", "192.168.1.2", "10.0.0.1"],
|
||||
"Roles": ""
|
||||
}
|
||||
```
|
||||
|
||||
### Các thuộc tính
|
||||
|
||||
| Thuộc tính | Bắt buộc | Mô tả |
|
||||
|-----------|----------|-------|
|
||||
| `name` | ✅ | Tên của variable (duy nhất trong config) |
|
||||
| `type` | ✅ | Kiểu dữ liệu: `string`, `int`, `double`, `bool`, `enum`, `object`, `array` |
|
||||
| `value` | ✅ | Giá trị của variable |
|
||||
| `Min` | ❌ | Giá trị tối thiểu (cho `int` và `double`) |
|
||||
| `Max` | ❌ | Giá trị tối đa (cho `int` và `double`) |
|
||||
| `EnumValues` | ❌ | Danh sách giá trị cho phép (cho `enum`) |
|
||||
| `Roles` | ❌ | Roles string (có thể để trống) |
|
||||
|
||||
## 🔌 API Endpoints
|
||||
|
||||
### Config File Management
|
||||
|
||||
#### GET `/api/configs`
|
||||
Lấy tất cả configs (metadata only)
|
||||
|
||||
**Query Parameters:**
|
||||
- `search` (optional): Tìm kiếm theo tên hoặc ConfigType
|
||||
|
||||
**Response:** `200 OK`
|
||||
```json
|
||||
[
|
||||
{
|
||||
"id": "guid",
|
||||
"configType": "MQTTBrokerConfig",
|
||||
"createdAt": "2024-01-01T00:00:00Z",
|
||||
"updatedAt": "2024-01-01T00:00:00Z",
|
||||
"description": "MQTT Broker Configuration"
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
#### GET `/api/configs/{id}`
|
||||
Lấy config theo ID
|
||||
|
||||
**Response:** `200 OK`
|
||||
```json
|
||||
{
|
||||
"id": "guid",
|
||||
"configType": "MQTTBrokerConfig",
|
||||
"variables": [...],
|
||||
"createdAt": "2024-01-01T00:00:00Z",
|
||||
"updatedAt": "2024-01-01T00:00:00Z",
|
||||
"description": "MQTT Broker Configuration"
|
||||
}
|
||||
```
|
||||
|
||||
#### GET `/api/configs/by-type/{configType}`
|
||||
Lấy config theo ConfigType
|
||||
|
||||
**Response:** `200 OK` (same as GET by ID)
|
||||
|
||||
#### GET `/api/configs/exists/{configType}`
|
||||
Kiểm tra ConfigType có tồn tại không
|
||||
|
||||
**Response:** `200 OK`
|
||||
```json
|
||||
true
|
||||
```
|
||||
|
||||
#### POST `/api/configs`
|
||||
Tạo config mới
|
||||
|
||||
**Request Body:**
|
||||
```json
|
||||
{
|
||||
"configType": "MyConfig",
|
||||
"variables": [
|
||||
{
|
||||
"name": "port",
|
||||
"type": "int",
|
||||
"value": 8080
|
||||
}
|
||||
],
|
||||
"description": "My configuration"
|
||||
}
|
||||
```
|
||||
|
||||
**Response:** `201 Created`
|
||||
|
||||
#### PUT `/api/configs/{id}`
|
||||
Cập nhật config
|
||||
|
||||
**Request Body:**
|
||||
```json
|
||||
{
|
||||
"variables": [...],
|
||||
"description": "Updated description"
|
||||
}
|
||||
```
|
||||
|
||||
**Response:** `200 OK`
|
||||
|
||||
#### DELETE `/api/configs/{id}`
|
||||
Xóa config
|
||||
|
||||
**Response:** `204 No Content`
|
||||
|
||||
### Import/Export
|
||||
|
||||
#### POST `/api/configs/import`
|
||||
Import config từ JSON file
|
||||
|
||||
**Request:** `multipart/form-data`
|
||||
- `file`: JSON file
|
||||
- `configType`: ConfigType name
|
||||
|
||||
**Response:** `200 OK`
|
||||
|
||||
#### GET `/api/configs/{id}/export`
|
||||
Export config ra JSON file
|
||||
|
||||
**Response:** `200 OK` (application/json)
|
||||
|
||||
### Variable Management
|
||||
|
||||
#### PUT `/api/configs/{id}/variables/{variableName}`
|
||||
Cập nhật giá trị variable
|
||||
|
||||
**Request Body:**
|
||||
```json
|
||||
{
|
||||
"variableName": "port",
|
||||
"value": 8080
|
||||
}
|
||||
```
|
||||
|
||||
**Response:** `200 OK`
|
||||
|
||||
#### POST `/api/configs/{id}/variables`
|
||||
Thêm variable mới
|
||||
|
||||
**Request Body:**
|
||||
```json
|
||||
{
|
||||
"name": "newVariable",
|
||||
"type": "string",
|
||||
"value": "value"
|
||||
}
|
||||
```
|
||||
|
||||
**Response:** `200 OK`
|
||||
|
||||
#### DELETE `/api/configs/{id}/variables/{variableName}`
|
||||
Xóa variable
|
||||
|
||||
**Response:** `200 OK`
|
||||
|
||||
## 💡 Ví dụ sử dụng
|
||||
|
||||
### Ví dụ 1: Tạo MQTT Broker Config
|
||||
|
||||
```csharp
|
||||
var variables = new List<ConfigVariable>
|
||||
{
|
||||
new ConfigVariable
|
||||
{
|
||||
Name = "host",
|
||||
Type = ConfigVariableType.String,
|
||||
Value = "localhost"
|
||||
},
|
||||
new ConfigVariable
|
||||
{
|
||||
Name = "port",
|
||||
Type = ConfigVariableType.Int,
|
||||
Value = 1883,
|
||||
Min = 0,
|
||||
Max = 65535
|
||||
},
|
||||
new ConfigVariable
|
||||
{
|
||||
Name = "enableSSL",
|
||||
Type = ConfigVariableType.Bool,
|
||||
Value = false
|
||||
}
|
||||
};
|
||||
|
||||
var config = await _configService.CreateConfigAsync(
|
||||
configType: "MQTTBrokerConfig",
|
||||
variables: variables,
|
||||
description: "MQTT Broker Configuration"
|
||||
);
|
||||
```
|
||||
|
||||
### Ví dụ 2: Import Config từ File
|
||||
|
||||
```csharp
|
||||
using var stream = File.OpenRead("mqtt-config.json");
|
||||
var config = await _configService.ImportConfigAsync(
|
||||
stream: stream,
|
||||
fileName: "mqtt-config.json",
|
||||
configType: "MQTTBrokerConfig"
|
||||
);
|
||||
```
|
||||
|
||||
### Ví dụ 3: Sử dụng Config trong Service
|
||||
|
||||
```csharp
|
||||
public class MqttService
|
||||
{
|
||||
private readonly IConfigService _configService;
|
||||
|
||||
public MqttService(IConfigService configService)
|
||||
{
|
||||
_configService = configService;
|
||||
}
|
||||
|
||||
public async Task ConnectAsync()
|
||||
{
|
||||
var config = await _configService.GetConfigByTypeAsync("MQTTBrokerConfig");
|
||||
|
||||
var host = config.Variables.First(v => v.Name == "host").Value?.ToString();
|
||||
var port = (int)config.Variables.First(v => v.Name == "port").Value!;
|
||||
|
||||
// Connect to MQTT broker using host and port
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Ví dụ 4: Cập nhật Variable qua API
|
||||
|
||||
```csharp
|
||||
// C# HttpClient
|
||||
var client = new HttpClient();
|
||||
var request = new
|
||||
{
|
||||
variableName = "port",
|
||||
value = 8883
|
||||
};
|
||||
|
||||
var response = await client.PutAsJsonAsync(
|
||||
"https://api.example.com/api/configs/{id}/variables/port",
|
||||
request
|
||||
);
|
||||
```
|
||||
|
||||
### Ví dụ 5: Frontend - Sử dụng Component
|
||||
|
||||
```razor
|
||||
@page "/settings/config"
|
||||
@using RobotNet10.CustomConfigurationEditor.Components.ConfigManager
|
||||
|
||||
<PageTitle>Configuration Manager</PageTitle>
|
||||
|
||||
<MudContainer MaxWidth="MaxWidth.ExtraLarge" Class="mt-4">
|
||||
<ConfigManagerComponent />
|
||||
</MudContainer>
|
||||
```
|
||||
|
||||
## 🔧 Troubleshooting
|
||||
|
||||
### Lỗi: "ConfigType already exists"
|
||||
|
||||
**Nguyên nhân:** ConfigType đã tồn tại trong hệ thống.
|
||||
|
||||
**Giải pháp:**
|
||||
- Sử dụng ConfigType khác
|
||||
- Xóa config cũ trước khi tạo mới
|
||||
- Kiểm tra bằng `ConfigTypeExistsAsync()` trước khi tạo
|
||||
|
||||
### Lỗi: "Invalid variable type"
|
||||
|
||||
**Nguyên nhân:** Type của variable không hợp lệ.
|
||||
|
||||
**Giải pháp:**
|
||||
- Kiểm tra type là một trong: `string`, `int`, `double`, `bool`, `enum`, `object`, `array`
|
||||
- Đảm bảo value phù hợp với type
|
||||
|
||||
### Lỗi: "Value out of range"
|
||||
|
||||
**Nguyên nhân:** Giá trị vượt quá Min/Max.
|
||||
|
||||
**Giải pháp:**
|
||||
- Kiểm tra giá trị nằm trong khoảng Min và Max
|
||||
- Cập nhật Min/Max nếu cần
|
||||
|
||||
### Lỗi: "Invalid JSON format"
|
||||
|
||||
**Nguyên nhân:** File JSON không đúng format.
|
||||
|
||||
**Giải pháp:**
|
||||
- Kiểm tra file là mảng JSON hợp lệ
|
||||
- Đảm bảo mỗi variable có `name`, `type`, `value`
|
||||
- Validate JSON trước khi import
|
||||
|
||||
### Lỗi: "StorageManager not initialized"
|
||||
|
||||
**Nguyên nhân:** StorageConfig chưa được cấu hình.
|
||||
|
||||
**Giải pháp:**
|
||||
- Đảm bảo đã gọi `AddCustomConfiguration()` trong `Program.cs`
|
||||
- Kiểm tra `StorageConfig` trong `appsettings.json`
|
||||
|
||||
### Frontend: Component không hiển thị
|
||||
|
||||
**Nguyên nhân:** Services chưa được đăng ký.
|
||||
|
||||
**Giải pháp:**
|
||||
- Kiểm tra đã đăng ký `ConfigApiService` và `ConfigManagerState`
|
||||
- Đảm bảo có `HttpClient` với `BaseAddress`
|
||||
- Kiểm tra đã có `MudBlazor` services
|
||||
|
||||
### Frontend: Export không hoạt động
|
||||
|
||||
**Nguyên nhân:** JavaScript file chưa được thêm.
|
||||
|
||||
**Giải pháp:**
|
||||
- Copy `downloadFile.js` vào `wwwroot/js/`
|
||||
- Thêm script tag vào `index.html` hoặc `App.razor`
|
||||
|
||||
## 📝 Lưu ý
|
||||
|
||||
1. **ConfigType là duy nhất**: Mỗi ConfigType chỉ có thể tồn tại một lần trong hệ thống
|
||||
2. **Validation**: Tất cả dữ liệu đều được validate trước khi lưu
|
||||
3. **Storage**: Config files được lưu trong `configs/{ConfigType}.json` trong StorageManager
|
||||
4. **Metadata**: Metadata được lưu trong `configs/_metadata.json`
|
||||
5. **Thread Safety**: Services được đăng ký là `Scoped`, phù hợp cho web applications
|
||||
|
||||
## 📚 Tài liệu tham khảo
|
||||
|
||||
- [RobotNet10.StorageManager Documentation](../RobotNet10.StorageManager/README.md)
|
||||
- [MudBlazor Documentation](https://mudblazor.com/)
|
||||
- [ASP.NET Core Documentation](https://docs.microsoft.com/aspnet/core)
|
||||
|
||||
## 🤝 Đóng góp
|
||||
|
||||
Nếu bạn phát hiện lỗi hoặc có đề xuất cải thiện, vui lòng tạo issue hoặc pull request.
|
||||
|
||||
## 📄 License
|
||||
|
||||
[Thêm thông tin license nếu có]
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.AspNetCore.Mvc" Version="2.3.9" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\RobotNet10.StorageManager\RobotNet10.StorageManager.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,194 @@
|
||||
using RobotNet10.CustomConfiguration.Events;
|
||||
using RobotNet10.CustomConfiguration.Models;
|
||||
|
||||
namespace RobotNet10.CustomConfiguration.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Implementation của IConfigManager
|
||||
/// Service cho quản lý và truy vấn configuration variables
|
||||
/// </summary>
|
||||
public class ConfigManager : IConfigManager, IDisposable
|
||||
{
|
||||
private readonly IConfigService _configService;
|
||||
|
||||
// ==========================================
|
||||
// EVENTS
|
||||
// ==========================================
|
||||
|
||||
public event EventHandler<ConfigChangedEventArgs>? ConfigChanged;
|
||||
|
||||
/// <summary>
|
||||
/// Constructor - Subscribe vào ConfigChanged event từ IConfigService
|
||||
/// </summary>
|
||||
public ConfigManager(IConfigService configService)
|
||||
{
|
||||
_configService = configService ?? throw new ArgumentNullException(nameof(configService));
|
||||
|
||||
// Forward events from IConfigService to IConfigManager
|
||||
_configService.ConfigChanged += OnConfigServiceChanged;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Forward ConfigChanged event from IConfigService to IConfigManager subscribers
|
||||
/// </summary>
|
||||
private void OnConfigServiceChanged(object? sender, ConfigChangedEventArgs args)
|
||||
{
|
||||
ConfigChanged?.Invoke(this, args);
|
||||
}
|
||||
|
||||
// ==========================================
|
||||
// CONFIG TYPE OPERATIONS
|
||||
// ==========================================
|
||||
|
||||
public async Task<ConfigFile?> GetConfigByTypeAsync(string configType)
|
||||
{
|
||||
return await _configService.GetConfigByTypeAsync(configType);
|
||||
}
|
||||
|
||||
public async Task<bool> ConfigTypeExistsAsync(string configType)
|
||||
{
|
||||
return await _configService.ConfigTypeExistsAsync(configType);
|
||||
}
|
||||
|
||||
// ==========================================
|
||||
// VARIABLE EXISTENCE CHECK
|
||||
// ==========================================
|
||||
|
||||
public async Task<bool> VariableExistsAsync(string configType, string variableName)
|
||||
{
|
||||
var config = await _configService.GetConfigByTypeAsync(configType);
|
||||
if (config == null)
|
||||
return false;
|
||||
|
||||
return config.Variables.Any(v => v.Name.Equals(variableName, StringComparison.OrdinalIgnoreCase));
|
||||
}
|
||||
|
||||
public async Task<bool> VariableExistsAsync(Guid configId, string variableName)
|
||||
{
|
||||
var config = await _configService.GetConfigByIdAsync(configId);
|
||||
if (config == null)
|
||||
return false;
|
||||
|
||||
return config.Variables.Any(v => v.Name.Equals(variableName, StringComparison.OrdinalIgnoreCase));
|
||||
}
|
||||
|
||||
// ==========================================
|
||||
// GET VARIABLE VALUE
|
||||
// ==========================================
|
||||
|
||||
public async Task<object?> GetVariableValueAsync(string configType, string variableName)
|
||||
{
|
||||
var variable = await GetVariableAsync(configType, variableName);
|
||||
return variable?.Value;
|
||||
}
|
||||
|
||||
public async Task<object?> GetVariableValueAsync(Guid configId, string variableName)
|
||||
{
|
||||
var variable = await GetVariableAsync(configId, variableName);
|
||||
return variable?.Value;
|
||||
}
|
||||
|
||||
// ==========================================
|
||||
// GET VARIABLE(S)
|
||||
// ==========================================
|
||||
|
||||
public async Task<ConfigVariable?> GetVariableAsync(string configType, string variableName)
|
||||
{
|
||||
var config = await _configService.GetConfigByTypeAsync(configType);
|
||||
if (config == null)
|
||||
return null;
|
||||
|
||||
return config.Variables.FirstOrDefault(v => v.Name.Equals(variableName, StringComparison.OrdinalIgnoreCase));
|
||||
}
|
||||
|
||||
public async Task<List<ConfigVariable>> GetVariablesAsync(string configType)
|
||||
{
|
||||
var config = await _configService.GetConfigByTypeAsync(configType);
|
||||
return config?.Variables ?? [];
|
||||
}
|
||||
|
||||
public async Task<ConfigVariable?> GetVariableAsync(Guid configId, string variableName)
|
||||
{
|
||||
var config = await _configService.GetConfigByIdAsync(configId);
|
||||
if (config == null)
|
||||
return null;
|
||||
|
||||
return config.Variables.FirstOrDefault(v => v.Name.Equals(variableName, StringComparison.OrdinalIgnoreCase));
|
||||
}
|
||||
|
||||
public async Task<List<ConfigVariable>> GetVariablesAsync(Guid configId)
|
||||
{
|
||||
var config = await _configService.GetConfigByIdAsync(configId);
|
||||
return config?.Variables ?? [];
|
||||
}
|
||||
|
||||
// ==========================================
|
||||
// GET VARIABLE TYPE
|
||||
// ==========================================
|
||||
|
||||
public async Task<string?> GetVariableTypeAsync(string configType, string variableName)
|
||||
{
|
||||
var variable = await GetVariableAsync(configType, variableName);
|
||||
return variable != null ? ConvertTypeToString(variable.Type) : null;
|
||||
}
|
||||
|
||||
public async Task<string?> GetVariableTypeAsync(Guid configId, string variableName)
|
||||
{
|
||||
var variable = await GetVariableAsync(configId, variableName);
|
||||
return variable != null ? ConvertTypeToString(variable.Type) : null;
|
||||
}
|
||||
|
||||
// ==========================================
|
||||
// EVENT TRIGGERS
|
||||
// ==========================================
|
||||
|
||||
/// <summary>
|
||||
/// Trigger ConfigChanged event
|
||||
/// Method này có thể được gọi từ bên ngoài hoặc từ các service khác khi có thay đổi
|
||||
/// </summary>
|
||||
public void OnConfigChanged(string configType, Guid configId, ConfigChangeType changeType, string? variableName = null)
|
||||
{
|
||||
var args = new ConfigChangedEventArgs
|
||||
{
|
||||
ConfigType = configType,
|
||||
ConfigId = configId,
|
||||
ChangeType = changeType,
|
||||
VariableName = variableName
|
||||
};
|
||||
|
||||
ConfigChanged?.Invoke(this, args);
|
||||
}
|
||||
|
||||
// ==========================================
|
||||
// DISPOSE
|
||||
// ==========================================
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_configService.ConfigChanged -= OnConfigServiceChanged;
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
// ==========================================
|
||||
// HELPER METHODS
|
||||
// ==========================================
|
||||
|
||||
/// <summary>
|
||||
/// Convert ConfigVariableType enum thành string
|
||||
/// </summary>
|
||||
private static string ConvertTypeToString(ConfigVariableType type)
|
||||
{
|
||||
return type switch
|
||||
{
|
||||
ConfigVariableType.String => "string",
|
||||
ConfigVariableType.Int => "int",
|
||||
ConfigVariableType.Double => "double",
|
||||
ConfigVariableType.Bool => "bool",
|
||||
ConfigVariableType.Object => "object",
|
||||
ConfigVariableType.Array => "array",
|
||||
ConfigVariableType.Enum => "enum",
|
||||
_ => throw new ArgumentException($"Unknown variable type: {type}")
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,375 @@
|
||||
using RobotNet10.CustomConfiguration.Events;
|
||||
using RobotNet10.CustomConfiguration.Helpers;
|
||||
using RobotNet10.CustomConfiguration.Models;
|
||||
using RobotNet10.CustomConfiguration.Validators;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace RobotNet10.CustomConfiguration.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Implementation của các methods trong ConfigService
|
||||
/// </summary>
|
||||
public partial class ConfigService
|
||||
{
|
||||
public async Task<ConfigFile> CreateConfigAsync(string configType, List<ConfigVariable> variables, string? description = null)
|
||||
{
|
||||
// Validate ConfigType
|
||||
if (string.IsNullOrWhiteSpace(configType))
|
||||
{
|
||||
throw new ArgumentException("ConfigType cannot be empty", nameof(configType));
|
||||
}
|
||||
|
||||
// Check if ConfigType already exists
|
||||
var existing = await GetMetadataByTypeAsync(configType);
|
||||
if (existing != null)
|
||||
{
|
||||
throw new InvalidOperationException($"Config with type '{configType}' already exists");
|
||||
}
|
||||
|
||||
// Create config file
|
||||
var config = new ConfigFile
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
ConfigType = configType,
|
||||
Variables = variables,
|
||||
Description = description,
|
||||
CreatedAt = DateTime.UtcNow,
|
||||
UpdatedAt = DateTime.UtcNow,
|
||||
FilePath = $"{ConfigPath}/{configType}{ConfigFileExtension}"
|
||||
};
|
||||
|
||||
// Validate config
|
||||
var validationResult = ConfigValidator.ValidateConfig(config);
|
||||
if (!validationResult.IsValid)
|
||||
{
|
||||
throw new ArgumentException($"Invalid config: {string.Join("; ", validationResult.Errors)}");
|
||||
}
|
||||
|
||||
// Save config file (includes metadata)
|
||||
await SaveConfigFileAsync(config);
|
||||
|
||||
// Trigger event
|
||||
OnConfigChanged(config, ConfigChangeType.Created);
|
||||
|
||||
return config;
|
||||
}
|
||||
|
||||
public async Task<ConfigFile?> GetConfigByIdAsync(Guid id)
|
||||
{
|
||||
// Load all configs and find by ID
|
||||
var allMetadata = await LoadAllMetadataAsync();
|
||||
var metadata = allMetadata.FirstOrDefault(m => m.Id == id);
|
||||
if (metadata == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return await LoadConfigFileAsync(metadata.ConfigType);
|
||||
}
|
||||
|
||||
public async Task<ConfigFile?> GetConfigByTypeAsync(string configType)
|
||||
{
|
||||
return await LoadConfigFileAsync(configType);
|
||||
}
|
||||
|
||||
public async Task<List<ConfigFileMetadata>> GetAllConfigsAsync()
|
||||
{
|
||||
var metadata = await LoadAllMetadataAsync();
|
||||
return [.. metadata.OrderBy(m => m.ConfigType)];
|
||||
}
|
||||
|
||||
public async Task<List<ConfigFileMetadata>> SearchConfigsAsync(string? searchText)
|
||||
{
|
||||
var allMetadata = await GetAllConfigsAsync();
|
||||
|
||||
if (string.IsNullOrWhiteSpace(searchText))
|
||||
{
|
||||
return allMetadata;
|
||||
}
|
||||
|
||||
var searchLower = searchText.ToLower();
|
||||
return [.. allMetadata.Where(m =>
|
||||
m.ConfigType.ToLower().Contains(searchLower) ||
|
||||
(m.Description != null && m.Description.ToLower().Contains(searchLower))
|
||||
)];
|
||||
}
|
||||
|
||||
public async Task<ConfigFile> UpdateConfigAsync(Guid id, List<ConfigVariable>? variables = null, string? description = null)
|
||||
{
|
||||
var config = await GetConfigByIdAsync(id) ?? throw new KeyNotFoundException($"Config with ID '{id}' not found");
|
||||
|
||||
// Update variables if provided
|
||||
if (variables != null)
|
||||
{
|
||||
config.Variables = variables;
|
||||
}
|
||||
|
||||
// Update description if provided
|
||||
if (description != null)
|
||||
{
|
||||
config.Description = description;
|
||||
}
|
||||
|
||||
config.UpdatedAt = DateTime.UtcNow;
|
||||
|
||||
// Validate config
|
||||
var validationResult = ConfigValidator.ValidateConfig(config);
|
||||
if (!validationResult.IsValid)
|
||||
{
|
||||
throw new ArgumentException($"Invalid config: {string.Join("; ", validationResult.Errors)}");
|
||||
}
|
||||
|
||||
// Save config file (includes metadata)
|
||||
await SaveConfigFileAsync(config);
|
||||
|
||||
// Trigger event
|
||||
OnConfigChanged(config, ConfigChangeType.Updated);
|
||||
|
||||
return config;
|
||||
}
|
||||
|
||||
public async Task<bool> DeleteConfigAsync(Guid id)
|
||||
{
|
||||
// Find config by ID
|
||||
var allMetadata = await LoadAllMetadataAsync();
|
||||
var metadata = allMetadata.FirstOrDefault(m => m.Id == id);
|
||||
if (metadata == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// Delete config file
|
||||
// objectName should be {configType}.config so StorageManager adds .json to make {configType}.config.json
|
||||
var objectName = $"{metadata.ConfigType}.config";
|
||||
var exists = await _storageManager.ExistsAsync(ConfigPath, objectName, CancellationToken.None);
|
||||
if (exists)
|
||||
{
|
||||
await _storageManager.DeleteAsync(ConfigPath, objectName, CancellationToken.None);
|
||||
|
||||
// Trigger event
|
||||
var deletedConfig = new ConfigFile
|
||||
{
|
||||
Id = metadata.Id,
|
||||
ConfigType = metadata.ConfigType,
|
||||
CreatedAt = metadata.CreatedAt,
|
||||
UpdatedAt = metadata.UpdatedAt,
|
||||
Description = metadata.Description
|
||||
};
|
||||
OnConfigChanged(deletedConfig, ConfigChangeType.Deleted);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public async Task<bool> ConfigTypeExistsAsync(string configType)
|
||||
{
|
||||
var metadata = await GetMetadataByTypeAsync(configType);
|
||||
return metadata != null;
|
||||
}
|
||||
|
||||
public async Task<ConfigFile> ImportConfigFromJsonAsync(Stream jsonStream, string configType)
|
||||
{
|
||||
return await ImportConfigFromJsonAsync(jsonStream, configType, null);
|
||||
}
|
||||
|
||||
public async Task<ConfigFile> ImportConfigFromJsonAsync(Stream jsonStream, string configType, string? description)
|
||||
{
|
||||
// Validate ConfigType
|
||||
if (string.IsNullOrWhiteSpace(configType))
|
||||
{
|
||||
throw new ArgumentException("ConfigType cannot be empty", nameof(configType));
|
||||
}
|
||||
|
||||
// Check if ConfigType already exists
|
||||
var existing = await GetMetadataByTypeAsync(configType);
|
||||
if (existing != null)
|
||||
{
|
||||
throw new InvalidOperationException($"Config with type '{configType}' already exists");
|
||||
}
|
||||
|
||||
// Try to parse as full config file (new format with metadata)
|
||||
// Reset stream position first
|
||||
jsonStream.Position = 0;
|
||||
string? fileDescription = null;
|
||||
List<ConfigVariable> variables;
|
||||
|
||||
try
|
||||
{
|
||||
// Try to parse as ConfigFile (new format)
|
||||
var configFile = JsonConfigParser.ParseConfigFile(jsonStream);
|
||||
// If successful, use description from file
|
||||
fileDescription = configFile.Description;
|
||||
variables = configFile.Variables;
|
||||
}
|
||||
catch (Exception ex) when (ex is JsonException or ArgumentException)
|
||||
{
|
||||
// If parsing as ConfigFile fails, try old format (array of variables)
|
||||
jsonStream.Position = 0;
|
||||
variables = JsonConfigParser.ParseVariables(jsonStream);
|
||||
}
|
||||
|
||||
// Use description from parameter if provided, otherwise use description from file
|
||||
var finalDescription = !string.IsNullOrWhiteSpace(description) ? description : fileDescription;
|
||||
|
||||
// Create config with description (from parameter or file)
|
||||
var config = await CreateConfigAsync(configType, variables, finalDescription);
|
||||
|
||||
// Note: CreateConfigAsync already triggers Created event, so no need to trigger again
|
||||
|
||||
return config;
|
||||
}
|
||||
|
||||
public async Task<Stream> ExportConfigToJsonAsync(Guid id)
|
||||
{
|
||||
var config = await GetConfigByIdAsync(id);
|
||||
return config == null ? throw new KeyNotFoundException($"Config with ID '{id}' not found") : await ExportConfigToJsonAsync(config);
|
||||
}
|
||||
|
||||
public Task<Stream> ExportConfigToJsonAsync(ConfigFile config)
|
||||
{
|
||||
// Export với format mới (metadata + variables)
|
||||
var json = JsonConfigParser.SerializeConfigFile(config);
|
||||
var bytes = Encoding.UTF8.GetBytes(json);
|
||||
Stream stream = new MemoryStream(bytes)
|
||||
{
|
||||
Position = 0 // Ensure stream is at the beginning
|
||||
};
|
||||
return Task.FromResult(stream);
|
||||
}
|
||||
|
||||
public async Task<ConfigFile> UpdateVariableAsync(Guid configId, string variableName, object? value)
|
||||
{
|
||||
var config = await GetConfigByIdAsync(configId) ??
|
||||
throw new KeyNotFoundException($"Config with ID '{configId}' not found");
|
||||
var variable = config.Variables.FirstOrDefault(v => v.Name.Equals(variableName, StringComparison.OrdinalIgnoreCase)) ??
|
||||
throw new KeyNotFoundException($"Variable '{variableName}' not found in config");
|
||||
variable.Value = value;
|
||||
config.UpdatedAt = DateTime.UtcNow;
|
||||
|
||||
// Validate
|
||||
var validationResult = ConfigValidator.ValidateConfig(config);
|
||||
if (!validationResult.IsValid)
|
||||
{
|
||||
throw new ArgumentException($"Invalid config: {string.Join("; ", validationResult.Errors)}");
|
||||
}
|
||||
|
||||
// Save config file (includes metadata)
|
||||
await SaveConfigFileAsync(config);
|
||||
|
||||
// Trigger event
|
||||
OnConfigChanged(config, ConfigChangeType.VariableUpdated, variableName);
|
||||
|
||||
return config;
|
||||
}
|
||||
|
||||
public async Task<ConfigFile> AddVariableAsync(Guid configId, ConfigVariable variable)
|
||||
{
|
||||
var config = await GetConfigByIdAsync(configId) ?? throw new KeyNotFoundException($"Config with ID '{configId}' not found");
|
||||
|
||||
// Check if variable name already exists
|
||||
if (config.Variables.Any(v => v.Name.Equals(variable.Name, StringComparison.OrdinalIgnoreCase)))
|
||||
{
|
||||
throw new InvalidOperationException($"Variable '{variable.Name}' already exists in config");
|
||||
}
|
||||
|
||||
config.Variables.Add(variable);
|
||||
config.UpdatedAt = DateTime.UtcNow;
|
||||
|
||||
// Validate
|
||||
var validationResult = ConfigValidator.ValidateConfig(config);
|
||||
if (!validationResult.IsValid)
|
||||
{
|
||||
throw new ArgumentException($"Invalid config: {string.Join("; ", validationResult.Errors)}");
|
||||
}
|
||||
|
||||
// Save config file (includes metadata)
|
||||
await SaveConfigFileAsync(config);
|
||||
|
||||
// Trigger event
|
||||
OnConfigChanged(config, ConfigChangeType.VariableAdded, variable.Name);
|
||||
|
||||
return config;
|
||||
}
|
||||
|
||||
public async Task<ConfigFile> RemoveVariableAsync(Guid configId, string variableName)
|
||||
{
|
||||
var config = await GetConfigByIdAsync(configId) ??
|
||||
throw new KeyNotFoundException($"Config with ID '{configId}' not found");
|
||||
var variable = config.Variables.FirstOrDefault(v => v.Name.Equals(variableName, StringComparison.OrdinalIgnoreCase)) ??
|
||||
throw new KeyNotFoundException($"Variable '{variableName}' not found in config");
|
||||
config.Variables.Remove(variable);
|
||||
config.UpdatedAt = DateTime.UtcNow;
|
||||
|
||||
// Validate
|
||||
var validationResult = ConfigValidator.ValidateConfig(config);
|
||||
if (!validationResult.IsValid)
|
||||
{
|
||||
throw new ArgumentException($"Invalid config: {string.Join("; ", validationResult.Errors)}");
|
||||
}
|
||||
|
||||
// Save config file (includes metadata)
|
||||
await SaveConfigFileAsync(config);
|
||||
|
||||
// Trigger event
|
||||
OnConfigChanged(config, ConfigChangeType.VariableRemoved, variableName);
|
||||
|
||||
return config;
|
||||
}
|
||||
|
||||
// ==========================================
|
||||
// PRIVATE HELPER METHODS
|
||||
// ==========================================
|
||||
|
||||
/// <summary>
|
||||
/// Load config file từ StorageManager (format mới với metadata)
|
||||
/// </summary>
|
||||
private async Task<ConfigFile?> LoadConfigFileAsync(string configType)
|
||||
{
|
||||
try
|
||||
{
|
||||
// objectName should be {configType}.config so StorageManager finds {configType}.config.json
|
||||
var objectName = $"{configType}.config";
|
||||
var exists = await _storageManager.ExistsAsync(ConfigPath, objectName, CancellationToken.None);
|
||||
if (!exists)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
// Get file stream from StorageManager (works with both local and remote storage)
|
||||
using var stream = await _storageManager.GetFileAsync(ConfigPath, objectName, CancellationToken.None);
|
||||
|
||||
// Parse config file directly from stream (format mới với metadata)
|
||||
return JsonConfigParser.ParseConfigFile(stream);
|
||||
}
|
||||
catch (JsonException)
|
||||
{
|
||||
// Config file corrupted/invalid format - treat as not found
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Save config file vào StorageManager (format mới với metadata)
|
||||
/// </summary>
|
||||
private async Task SaveConfigFileAsync(ConfigFile config)
|
||||
{
|
||||
// Serialize với format mới (metadata + variables)
|
||||
var json = JsonConfigParser.SerializeConfigFile(config);
|
||||
var bytes = Encoding.UTF8.GetBytes(json);
|
||||
|
||||
// objectName should be {configType}.config so StorageManager adds .json to make {configType}.config.json
|
||||
var objectName = $"{config.ConfigType}.config";
|
||||
|
||||
using var stream = new MemoryStream(bytes);
|
||||
await _storageManager.UploadAsync(
|
||||
ConfigPath,
|
||||
objectName,
|
||||
stream,
|
||||
stream.Length,
|
||||
"application/json",
|
||||
CancellationToken.None
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
using RobotNet10.CustomConfiguration.Helpers;
|
||||
using RobotNet10.CustomConfiguration.Models;
|
||||
using System.IO;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace RobotNet10.CustomConfiguration.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Helper methods cho quản lý metadata (load từ config files)
|
||||
/// </summary>
|
||||
public partial class ConfigService
|
||||
{
|
||||
/// <summary>
|
||||
/// Load tất cả config files và trả về metadata
|
||||
/// </summary>
|
||||
private async Task<List<ConfigFileMetadata>> LoadAllMetadataAsync()
|
||||
{
|
||||
var metadataList = new List<ConfigFileMetadata>();
|
||||
|
||||
try
|
||||
{
|
||||
// List all files in configs directory
|
||||
var files = await _storageManager.ListAsync(ConfigPath, recursive: false, CancellationToken.None);
|
||||
|
||||
// Filter only .config.json files
|
||||
var configFiles = files.Where(f => f.EndsWith(ConfigFileExtension, StringComparison.OrdinalIgnoreCase)).ToList();
|
||||
|
||||
foreach (var fileName in configFiles)
|
||||
{
|
||||
try
|
||||
{
|
||||
// Extract configType from filename: {configType}.config.json
|
||||
var configType = fileName.Replace(ConfigFileExtension, "", StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
// Load config file to get metadata
|
||||
var config = await LoadConfigFileAsync(configType);
|
||||
if (config != null)
|
||||
{
|
||||
metadataList.Add(new ConfigFileMetadata
|
||||
{
|
||||
Id = config.Id,
|
||||
ConfigType = config.ConfigType,
|
||||
CreatedAt = config.CreatedAt,
|
||||
UpdatedAt = config.UpdatedAt,
|
||||
Description = config.Description
|
||||
});
|
||||
}
|
||||
}
|
||||
catch (JsonException)
|
||||
{
|
||||
// Skip corrupted config files
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex) when (ex is IOException or UnauthorizedAccessException)
|
||||
{
|
||||
// Storage unavailable - return what we have so far
|
||||
return metadataList;
|
||||
}
|
||||
|
||||
return metadataList;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get metadata by ConfigType (load từ config file)
|
||||
/// </summary>
|
||||
private async Task<ConfigFileMetadata?> GetMetadataByTypeAsync(string configType)
|
||||
{
|
||||
var config = await LoadConfigFileAsync(configType);
|
||||
if (config == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return new ConfigFileMetadata
|
||||
{
|
||||
Id = config.Id,
|
||||
ConfigType = config.ConfigType,
|
||||
CreatedAt = config.CreatedAt,
|
||||
UpdatedAt = config.UpdatedAt,
|
||||
Description = config.Description
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
using Microsoft.Extensions.Options;
|
||||
using RobotNet10.CustomConfiguration.Events;
|
||||
using RobotNet10.CustomConfiguration.Models;
|
||||
using RobotNet10.StorageManager;
|
||||
|
||||
namespace RobotNet10.CustomConfiguration.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Service implementation cho quản lý configuration files
|
||||
/// Sử dụng StorageManager để lưu trữ file JSON
|
||||
/// </summary>
|
||||
public partial class ConfigService : IConfigService, IDisposable
|
||||
{
|
||||
private readonly StorageManager.StorageManager _storageManager;
|
||||
private const string ConfigPath = "configs"; // Path trong StorageManager
|
||||
private const string ConfigFileExtension = ".config.json"; // Extension cho config files
|
||||
private const string StorageConfigsKey = "StorageConfigs"; // Named options key cho IOptionsMonitor
|
||||
|
||||
// ==========================================
|
||||
// EVENTS
|
||||
// ==========================================
|
||||
|
||||
public event EventHandler<ConfigChangedEventArgs>? ConfigChanged;
|
||||
|
||||
public ConfigService(IOptionsMonitor<StorageConfig> optionsSnapshot)
|
||||
{
|
||||
var config = optionsSnapshot.Get(StorageConfigsKey);
|
||||
|
||||
ArgumentNullException.ThrowIfNull(config);
|
||||
|
||||
_storageManager = new StorageManager.StorageManager(config);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Trigger ConfigChanged event
|
||||
/// </summary>
|
||||
protected virtual void OnConfigChanged(ConfigFile config, ConfigChangeType changeType, string? variableName = null)
|
||||
{
|
||||
var args = new ConfigChangedEventArgs
|
||||
{
|
||||
ConfigType = config.ConfigType,
|
||||
ConfigId = config.Id,
|
||||
ChangeType = changeType,
|
||||
VariableName = variableName
|
||||
};
|
||||
|
||||
ConfigChanged?.Invoke(this, args);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_storageManager?.Dispose();
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,102 @@
|
||||
using RobotNet10.CustomConfiguration.Events;
|
||||
using RobotNet10.CustomConfiguration.Models;
|
||||
|
||||
namespace RobotNet10.CustomConfiguration.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Service interface cho quản lý và truy vấn configuration variables
|
||||
/// </summary>
|
||||
public interface IConfigManager
|
||||
{
|
||||
// ==========================================
|
||||
// EVENTS
|
||||
// ==========================================
|
||||
|
||||
/// <summary>
|
||||
/// Event được trigger khi có thay đổi trong config (tạo mới, cập nhật, xóa, hoặc thay đổi variables)
|
||||
/// </summary>
|
||||
event EventHandler<ConfigChangedEventArgs>? ConfigChanged;
|
||||
|
||||
// ==========================================
|
||||
// CONFIG TYPE OPERATIONS
|
||||
// ==========================================
|
||||
|
||||
/// <summary>
|
||||
/// Lấy config theo ConfigType (đầy đủ thông tin bao gồm variables)
|
||||
/// </summary>
|
||||
Task<ConfigFile?> GetConfigByTypeAsync(string configType);
|
||||
|
||||
/// <summary>
|
||||
/// Kiểm tra ConfigType có tồn tại không
|
||||
/// </summary>
|
||||
Task<bool> ConfigTypeExistsAsync(string configType);
|
||||
|
||||
// ==========================================
|
||||
// VARIABLE EXISTENCE CHECK
|
||||
// ==========================================
|
||||
|
||||
/// <summary>
|
||||
/// Kiểm tra variable có tồn tại không (theo ConfigType)
|
||||
/// </summary>
|
||||
Task<bool> VariableExistsAsync(string configType, string variableName);
|
||||
|
||||
/// <summary>
|
||||
/// Kiểm tra variable có tồn tại không (theo ConfigId)
|
||||
/// </summary>
|
||||
Task<bool> VariableExistsAsync(Guid configId, string variableName);
|
||||
|
||||
// ==========================================
|
||||
// GET VARIABLE VALUE
|
||||
// ==========================================
|
||||
|
||||
/// <summary>
|
||||
/// Lấy giá trị (Value) của variable (theo ConfigType)
|
||||
/// </summary>
|
||||
Task<object?> GetVariableValueAsync(string configType, string variableName);
|
||||
|
||||
/// <summary>
|
||||
/// Lấy giá trị (Value) của variable (theo ConfigId)
|
||||
/// </summary>
|
||||
Task<object?> GetVariableValueAsync(Guid configId, string variableName);
|
||||
|
||||
// ==========================================
|
||||
// GET VARIABLE(S)
|
||||
// ==========================================
|
||||
|
||||
/// <summary>
|
||||
/// Lấy một variable cụ thể (theo ConfigType)
|
||||
/// </summary>
|
||||
Task<ConfigVariable?> GetVariableAsync(string configType, string variableName);
|
||||
|
||||
/// <summary>
|
||||
/// Lấy tất cả variables của một ConfigType
|
||||
/// </summary>
|
||||
Task<List<ConfigVariable>> GetVariablesAsync(string configType);
|
||||
|
||||
/// <summary>
|
||||
/// Lấy một variable cụ thể (theo ConfigId)
|
||||
/// </summary>
|
||||
Task<ConfigVariable?> GetVariableAsync(Guid configId, string variableName);
|
||||
|
||||
/// <summary>
|
||||
/// Lấy tất cả variables của một ConfigId
|
||||
/// </summary>
|
||||
Task<List<ConfigVariable>> GetVariablesAsync(Guid configId);
|
||||
|
||||
// ==========================================
|
||||
// GET VARIABLE TYPE
|
||||
// ==========================================
|
||||
|
||||
/// <summary>
|
||||
/// Lấy kiểu dữ liệu của variable dưới dạng string (theo ConfigType)
|
||||
/// Trả về: "string", "int", "double", "bool", "object", "array", "enum"
|
||||
/// </summary>
|
||||
Task<string?> GetVariableTypeAsync(string configType, string variableName);
|
||||
|
||||
/// <summary>
|
||||
/// Lấy kiểu dữ liệu của variable dưới dạng string (theo ConfigId)
|
||||
/// Trả về: "string", "int", "double", "bool", "object", "array", "enum"
|
||||
/// </summary>
|
||||
Task<string?> GetVariableTypeAsync(Guid configId, string variableName);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
using RobotNet10.CustomConfiguration.Events;
|
||||
using RobotNet10.CustomConfiguration.Models;
|
||||
|
||||
namespace RobotNet10.CustomConfiguration.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Service interface cho quản lý configuration files
|
||||
/// </summary>
|
||||
public interface IConfigService
|
||||
{
|
||||
// ==========================================
|
||||
// EVENTS
|
||||
// ==========================================
|
||||
|
||||
/// <summary>
|
||||
/// Event được trigger khi có thay đổi trong config (tạo mới, cập nhật, xóa, hoặc thay đổi variables)
|
||||
/// </summary>
|
||||
event EventHandler<ConfigChangedEventArgs>? ConfigChanged;
|
||||
|
||||
// ==========================================
|
||||
// CONFIG FILE MANAGEMENT
|
||||
// ==========================================
|
||||
|
||||
Task<ConfigFile> CreateConfigAsync(string configType, List<ConfigVariable> variables, string? description = null);
|
||||
Task<ConfigFile?> GetConfigByIdAsync(Guid id);
|
||||
Task<ConfigFile?> GetConfigByTypeAsync(string configType);
|
||||
Task<List<ConfigFileMetadata>> GetAllConfigsAsync();
|
||||
Task<List<ConfigFileMetadata>> SearchConfigsAsync(string? searchText);
|
||||
Task<ConfigFile> UpdateConfigAsync(Guid id, List<ConfigVariable>? variables = null, string? description = null);
|
||||
Task<bool> DeleteConfigAsync(Guid id);
|
||||
Task<bool> ConfigTypeExistsAsync(string configType);
|
||||
|
||||
// ==========================================
|
||||
// IMPORT/EXPORT
|
||||
// ==========================================
|
||||
|
||||
Task<ConfigFile> ImportConfigFromJsonAsync(Stream jsonStream, string configType);
|
||||
Task<ConfigFile> ImportConfigFromJsonAsync(Stream jsonStream, string configType, string? description);
|
||||
Task<Stream> ExportConfigToJsonAsync(Guid id);
|
||||
Task<Stream> ExportConfigToJsonAsync(ConfigFile config);
|
||||
|
||||
// ==========================================
|
||||
// VARIABLE MANAGEMENT
|
||||
// ==========================================
|
||||
|
||||
Task<ConfigFile> UpdateVariableAsync(Guid configId, string variableName, object? value);
|
||||
Task<ConfigFile> AddVariableAsync(Guid configId, ConfigVariable variable);
|
||||
Task<ConfigFile> RemoveVariableAsync(Guid configId, string variableName);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,198 @@
|
||||
using RobotNet10.CustomConfiguration.Models;
|
||||
using System.Globalization;
|
||||
|
||||
namespace RobotNet10.CustomConfiguration.Validators;
|
||||
|
||||
/// <summary>
|
||||
/// Validator cho config files và variables
|
||||
/// </summary>
|
||||
public static class ConfigValidator
|
||||
{
|
||||
/// <summary>
|
||||
/// Validate một config file
|
||||
/// </summary>
|
||||
public static ValidationResult ValidateConfig(ConfigFile config)
|
||||
{
|
||||
var errors = new List<string>();
|
||||
|
||||
// Validate ConfigType
|
||||
if (string.IsNullOrWhiteSpace(config.ConfigType))
|
||||
{
|
||||
errors.Add("ConfigType cannot be empty");
|
||||
}
|
||||
|
||||
// Validate Variables
|
||||
if (config.Variables != null && config.Variables.Count > 0)
|
||||
{
|
||||
foreach (var variable in config.Variables)
|
||||
{
|
||||
var variableErrors = ValidateVariable(variable);
|
||||
errors.AddRange(variableErrors);
|
||||
}
|
||||
}
|
||||
|
||||
return new ValidationResult
|
||||
{
|
||||
IsValid = errors.Count == 0,
|
||||
Errors = errors
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validate một variable
|
||||
/// </summary>
|
||||
public static List<string> ValidateVariable(ConfigVariable variable)
|
||||
{
|
||||
var errors = new List<string>();
|
||||
|
||||
// Validate Name
|
||||
if (string.IsNullOrWhiteSpace(variable.Name))
|
||||
{
|
||||
errors.Add("Variable name cannot be empty");
|
||||
}
|
||||
|
||||
// Validate Type
|
||||
if (!Enum.IsDefined(variable.Type))
|
||||
{
|
||||
errors.Add($"Invalid variable type: {variable.Type}");
|
||||
}
|
||||
|
||||
// Validate Value theo Type
|
||||
if (variable.Value == null)
|
||||
{
|
||||
errors.Add($"Variable '{variable.Name}' value cannot be null");
|
||||
}
|
||||
else
|
||||
{
|
||||
var valueErrors = ValidateValueByType(variable.Name, variable.Type, variable.Value, variable.Min, variable.Max, variable);
|
||||
errors.AddRange(valueErrors);
|
||||
}
|
||||
|
||||
return errors;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validate value theo type và Min/Max constraints
|
||||
/// </summary>
|
||||
private static List<string> ValidateValueByType(string variableName, ConfigVariableType type, object value, double? min, double? max, ConfigVariable? variable = null)
|
||||
{
|
||||
var errors = new List<string>();
|
||||
|
||||
switch (type)
|
||||
{
|
||||
case ConfigVariableType.Int:
|
||||
// Try parse
|
||||
if (int.TryParse(value.ToString(), NumberStyles.Integer, CultureInfo.InvariantCulture, out var parsedInt))
|
||||
{
|
||||
if (min.HasValue && parsedInt < min.Value)
|
||||
errors.Add($"Variable '{variableName}' value {parsedInt} is less than Min {min.Value}");
|
||||
if (max.HasValue && parsedInt > max.Value)
|
||||
errors.Add($"Variable '{variableName}' value {parsedInt} is greater than Max {max.Value}");
|
||||
}
|
||||
else
|
||||
{
|
||||
errors.Add($"Variable '{variableName}' must be an integer");
|
||||
}
|
||||
break;
|
||||
|
||||
case ConfigVariableType.Double:
|
||||
// Try parse
|
||||
if (double.TryParse(value.ToString(), NumberStyles.Float | NumberStyles.AllowThousands, CultureInfo.InvariantCulture, out var parsedDouble))
|
||||
{
|
||||
if (min.HasValue && parsedDouble < min.Value)
|
||||
errors.Add($"Variable '{variableName}' value {parsedDouble} is less than Min {min.Value}");
|
||||
if (max.HasValue && parsedDouble > max.Value)
|
||||
errors.Add($"Variable '{variableName}' value {parsedDouble} is greater than Max {max.Value}");
|
||||
}
|
||||
else
|
||||
{
|
||||
errors.Add($"Variable '{variableName}' must be a double");
|
||||
}
|
||||
break;
|
||||
|
||||
case ConfigVariableType.Bool:
|
||||
if (!bool.TryParse(value.ToString(), out _))
|
||||
{
|
||||
errors.Add($"Variable '{variableName}' must be a boolean");
|
||||
}
|
||||
break;
|
||||
|
||||
case ConfigVariableType.Object:
|
||||
// Object có thể là Dictionary<string, object> hoặc JsonElement
|
||||
if (value is not Dictionary<string, object> &&
|
||||
value is not System.Text.Json.JsonElement)
|
||||
{
|
||||
// Try parse as JSON object
|
||||
try
|
||||
{
|
||||
var jsonString = value.ToString();
|
||||
if (jsonString != null)
|
||||
{
|
||||
using var doc = System.Text.Json.JsonDocument.Parse(jsonString);
|
||||
if (doc.RootElement.ValueKind != System.Text.Json.JsonValueKind.Object)
|
||||
{
|
||||
errors.Add($"Variable '{variableName}' must be a JSON object");
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (System.Text.Json.JsonException)
|
||||
{
|
||||
errors.Add($"Variable '{variableName}' must be a valid JSON object");
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case ConfigVariableType.Array:
|
||||
// Array có thể là List<object> hoặc JsonElement
|
||||
if (value is not List<object> &&
|
||||
value is not System.Text.Json.JsonElement)
|
||||
{
|
||||
// Try parse as JSON array
|
||||
try
|
||||
{
|
||||
var jsonString = value.ToString();
|
||||
if (jsonString != null)
|
||||
{
|
||||
using var doc = System.Text.Json.JsonDocument.Parse(jsonString);
|
||||
if (doc.RootElement.ValueKind != System.Text.Json.JsonValueKind.Array)
|
||||
{
|
||||
errors.Add($"Variable '{variableName}' must be a JSON array");
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (System.Text.Json.JsonException)
|
||||
{
|
||||
errors.Add($"Variable '{variableName}' must be a valid JSON array");
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case ConfigVariableType.Enum:
|
||||
if (variable == null || variable.EnumValues == null || variable.EnumValues.Count == 0)
|
||||
{
|
||||
errors.Add($"Variable '{variableName}' of type Enum must have EnumValues defined");
|
||||
}
|
||||
else
|
||||
{
|
||||
var valueStr = value?.ToString();
|
||||
if (string.IsNullOrEmpty(valueStr) || !variable.EnumValues.Contains(valueStr))
|
||||
{
|
||||
errors.Add($"Variable '{variableName}' value '{valueStr}' is not in allowed enum values: {string.Join(", ", variable.EnumValues)}");
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
return errors;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Kết quả validation
|
||||
/// </summary>
|
||||
public class ValidationResult
|
||||
{
|
||||
public bool IsValid { get; set; }
|
||||
public List<string> Errors { get; set; } = [];
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user