Initial commit

This commit is contained in:
2026-07-03 16:37:12 +07:00
commit 63b8c1ea8b
1931 changed files with 640587 additions and 0 deletions

View File

@@ -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
};
}
}

View File

@@ -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; }
}

View File

@@ -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; }
}

View File

@@ -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
}

View File

@@ -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; }
}

View File

@@ -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; }
}

View File

@@ -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; }
}

View File

@@ -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
}

View File

@@ -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;
}
}

View File

@@ -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
};
}
}

View File

@@ -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;
}
}
}

View File

@@ -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; }
}

View File

@@ -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; }
}

View File

@@ -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
}

View File

@@ -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
}

View 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``double`) |
| `Max` | ❌ | Giá trị tối đa (cho `int``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``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ó]

View File

@@ -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>

View File

@@ -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}")
};
}
}

View File

@@ -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
);
}
}

View File

@@ -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
};
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}

View File

@@ -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);
}

View File

@@ -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; } = [];
}