A:添加测试项目
This commit is contained in:
1 parent
25c4a663a5
commit
859814514b
12 files changed
+562
-3
No files matched your search
@@ -14,9 +14,7 @@
|
|||||||
<PackageReference Include="Microsoft.Extensions.Configuration.Ini" Version="8.0.0" />
|
<PackageReference Include="Microsoft.Extensions.Configuration.Ini" Version="8.0.0" />
|
||||||
<PackageReference Include="Microsoft.Extensions.FileProviders.Physical" Version="8.0.0" />
|
<PackageReference Include="Microsoft.Extensions.FileProviders.Physical" Version="8.0.0" />
|
||||||
<PackageReference Include="Tomlyn" Version="0.16.2" />
|
<PackageReference Include="Tomlyn" Version="0.16.2" />
|
||||||
<PackageReference Include="Tomlyn.Extensions.Configuration" Version="0.16.2" />
|
<PackageReference Include="Tomlyn.Extensions.Configuration" Version="1.0.0" />
|
||||||
<PackageReference Include="IniParser" Version="2.5.2" />
|
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
using Microsoft.Extensions.FileProviders;
|
||||||
|
|
||||||
|
namespace Sharp.Config;
|
||||||
|
|
||||||
|
public static class ConfigBuilder
|
||||||
|
{
|
||||||
|
public static ConfigManager Build(Action<ConfigOptions> configure)
|
||||||
|
{
|
||||||
|
var options = new ConfigOptions();
|
||||||
|
configure(options);
|
||||||
|
return ConfigManager.Build(options);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static ConfigManager BuildFromFiles(IEnumerable<string> files,
|
||||||
|
bool reloadOnChange = true,
|
||||||
|
bool includeEnvironment = true,
|
||||||
|
string? environmentPrefix = null,
|
||||||
|
string? basePath = null,
|
||||||
|
IFileProvider? fileProvider = null)
|
||||||
|
{
|
||||||
|
return Build(opt =>
|
||||||
|
{
|
||||||
|
opt.ReloadOnChange = reloadOnChange;
|
||||||
|
opt.IncludeEnvironmentVariables = includeEnvironment;
|
||||||
|
opt.EnvironmentPrefix = environmentPrefix;
|
||||||
|
opt.BasePath = basePath;
|
||||||
|
opt.FileProvider = fileProvider;
|
||||||
|
|
||||||
|
foreach (var f in files)
|
||||||
|
{
|
||||||
|
if (!ConfigSource.TryGuess(f, out var src))
|
||||||
|
{
|
||||||
|
throw new NotSupportedException($"Unsupported config extension: {f}");
|
||||||
|
}
|
||||||
|
opt.Sources.Add(src);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
namespace Sharp.Config;
|
||||||
|
|
||||||
|
public enum ConfigFormat
|
||||||
|
{
|
||||||
|
Json,
|
||||||
|
Ini,
|
||||||
|
Xml,
|
||||||
|
Toml
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,183 @@
|
|||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
using System.Text;
|
||||||
|
using Microsoft.Extensions.Configuration;
|
||||||
|
using Microsoft.Extensions.FileProviders;
|
||||||
|
using Microsoft.Extensions.Primitives;
|
||||||
|
using Sharp.Config.Serialization;
|
||||||
|
using Tomlyn.Extensions.Configuration;
|
||||||
|
|
||||||
|
namespace Sharp.Config;
|
||||||
|
|
||||||
|
public sealed class ConfigManager
|
||||||
|
{
|
||||||
|
public IConfigurationRoot Configuration { get; }
|
||||||
|
public ConfigOptions Options { get; }
|
||||||
|
|
||||||
|
private ConfigManager(IConfigurationRoot configuration, ConfigOptions options)
|
||||||
|
{
|
||||||
|
Configuration = configuration;
|
||||||
|
Options = options;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static ConfigManager Build(ConfigOptions options)
|
||||||
|
{
|
||||||
|
var builder = new ConfigurationBuilder();
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(options.BasePath))
|
||||||
|
{
|
||||||
|
builder.SetBasePath(options.BasePath!);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options.FileProvider is not null)
|
||||||
|
{
|
||||||
|
builder.SetFileProvider(options.FileProvider);
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var src in options.Sources)
|
||||||
|
{
|
||||||
|
switch (src.Format)
|
||||||
|
{
|
||||||
|
case ConfigFormat.Json:
|
||||||
|
builder.AddJsonFile(src.Path, src.Optional, options.ReloadOnChange);
|
||||||
|
break;
|
||||||
|
case ConfigFormat.Ini:
|
||||||
|
builder.AddIniFile(src.Path, src.Optional, options.ReloadOnChange);
|
||||||
|
break;
|
||||||
|
case ConfigFormat.Xml:
|
||||||
|
builder.AddXmlFile(src.Path, src.Optional, options.ReloadOnChange);
|
||||||
|
break;
|
||||||
|
case ConfigFormat.Toml:
|
||||||
|
builder.AddToml(src.Path, src.Optional, options.ReloadOnChange);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw new NotSupportedException($"Unsupported format: {src.Format}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options.IncludeEnvironmentVariables)
|
||||||
|
{
|
||||||
|
if (!string.IsNullOrWhiteSpace(options.EnvironmentPrefix))
|
||||||
|
{
|
||||||
|
builder.AddEnvironmentVariables(options.EnvironmentPrefix);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
builder.AddEnvironmentVariables();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options.ConfigureBuilderHook is not null)
|
||||||
|
{
|
||||||
|
builder = (ConfigurationBuilder)options.ConfigureBuilderHook(builder);
|
||||||
|
}
|
||||||
|
|
||||||
|
var configuration = builder.Build();
|
||||||
|
return new ConfigManager(configuration, options);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Strongly-typed binding helpers
|
||||||
|
public T Get<T>(string? section = null, bool validate = false)
|
||||||
|
where T : new()
|
||||||
|
{
|
||||||
|
var target = new T();
|
||||||
|
if (string.IsNullOrWhiteSpace(section))
|
||||||
|
{
|
||||||
|
Configuration.Bind(target);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Configuration.GetSection(section).Bind(target);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (validate)
|
||||||
|
{
|
||||||
|
ValidateObject(target, throwOnError: true);
|
||||||
|
}
|
||||||
|
|
||||||
|
return target;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool TryGet<T>(out T instance, string? section = null, bool validate = false) where T : new()
|
||||||
|
{
|
||||||
|
instance = Get<T>(section, validate: false);
|
||||||
|
if (!validate) return true;
|
||||||
|
var results = ValidateObject(instance, throwOnError: false);
|
||||||
|
return results.Count == 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
public IList<ValidationResult> ValidateObject(object instance, bool throwOnError)
|
||||||
|
{
|
||||||
|
var context = new ValidationContext(instance);
|
||||||
|
var results = new List<ValidationResult>();
|
||||||
|
// validate top-level
|
||||||
|
Validator.TryValidateObject(instance, context, results, validateAllProperties: true);
|
||||||
|
|
||||||
|
// validate nested properties with DataAnnotations recursively
|
||||||
|
foreach (var prop in instance.GetType().GetProperties())
|
||||||
|
{
|
||||||
|
var value = prop.GetValue(instance);
|
||||||
|
if (value is null) continue;
|
||||||
|
if (IsSimple(prop.PropertyType)) continue;
|
||||||
|
var nestedContext = new ValidationContext(value);
|
||||||
|
Validator.TryValidateObject(value, nestedContext, results, validateAllProperties: true);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (throwOnError && results.Count > 0)
|
||||||
|
{
|
||||||
|
throw new ConfigurationValidationException(results);
|
||||||
|
}
|
||||||
|
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool IsSimple(Type t)
|
||||||
|
{
|
||||||
|
t = Nullable.GetUnderlyingType(t) ?? t;
|
||||||
|
return t.IsPrimitive || t.IsEnum || t == typeof(string) || t == typeof(decimal) || t == typeof(DateTime) || t == typeof(Guid) || t == typeof(DateTimeOffset) || t == typeof(TimeSpan);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save APIs
|
||||||
|
public void Save<T>(T instance, string path, ConfigFormat format, bool indented = true, Encoding? encoding = null)
|
||||||
|
{
|
||||||
|
Directory.CreateDirectory(System.IO.Path.GetDirectoryName(System.IO.Path.GetFullPath(path)) ?? ".");
|
||||||
|
var serializer = SerializerFactory.Get(format);
|
||||||
|
serializer.SerializeToFile(path, instance!, indented, encoding);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read file to typed object directly (bypassing IConfiguration)
|
||||||
|
public T? LoadFromFile<T>(string path, ConfigFormat format)
|
||||||
|
{
|
||||||
|
var serializer = SerializerFactory.Get(format);
|
||||||
|
return serializer.DeserializeFromFile<T>(path);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reload notifications
|
||||||
|
public IDisposable OnReload(Action callback)
|
||||||
|
{
|
||||||
|
IChangeToken token = Configuration.GetReloadToken();
|
||||||
|
return ChangeToken.OnChange(() => token, callback);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class SerializerFactory
|
||||||
|
{
|
||||||
|
public static IConfigSerializer Get(ConfigFormat format) => format switch
|
||||||
|
{
|
||||||
|
ConfigFormat.Json => new JsonConfigSerializer(),
|
||||||
|
ConfigFormat.Xml => new XmlConfigSerializer(),
|
||||||
|
ConfigFormat.Ini => new IniConfigSerializer(),
|
||||||
|
ConfigFormat.Toml => new TomlConfigSerializer(),
|
||||||
|
_ => throw new NotSupportedException($"Unsupported format: {format}")
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed class ConfigurationValidationException : Exception
|
||||||
|
{
|
||||||
|
public IReadOnlyList<ValidationResult> Errors { get; }
|
||||||
|
public ConfigurationValidationException(IReadOnlyList<ValidationResult> errors)
|
||||||
|
: base($"Configuration validation failed: {string.Join("; ", errors.Select(e => e.ErrorMessage))}")
|
||||||
|
{
|
||||||
|
Errors = errors;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
using Microsoft.Extensions.Configuration;
|
||||||
|
using Microsoft.Extensions.FileProviders;
|
||||||
|
|
||||||
|
namespace Sharp.Config;
|
||||||
|
|
||||||
|
public sealed class ConfigOptions
|
||||||
|
{
|
||||||
|
public string? BasePath { get; init; }
|
||||||
|
public IList<ConfigSource> Sources { get; } = new List<ConfigSource>();
|
||||||
|
|
||||||
|
public bool ReloadOnChange { get; init; } = true;
|
||||||
|
public bool IncludeEnvironmentVariables { get; init; } = true;
|
||||||
|
public string? EnvironmentPrefix { get; init; }
|
||||||
|
|
||||||
|
public IFileProvider? FileProvider { get; init; }
|
||||||
|
|
||||||
|
// Hook: allow external modules (如 Common) 自定义构建
|
||||||
|
public Func<IConfigurationBuilder, IConfigurationBuilder>? ConfigureBuilderHook { get; init; }
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
using System.Diagnostics.CodeAnalysis;
|
||||||
|
|
||||||
|
namespace Sharp.Config;
|
||||||
|
|
||||||
|
public sealed class ConfigSource
|
||||||
|
{
|
||||||
|
public required string Path { get; init; }
|
||||||
|
public required ConfigFormat Format { get; init; }
|
||||||
|
public bool Optional { get; init; } = true;
|
||||||
|
|
||||||
|
public override string ToString() => $"{Format}:{Path} (Optional={Optional})";
|
||||||
|
|
||||||
|
public static bool TryGuess(string path, [NotNullWhen(true)] out ConfigSource? source)
|
||||||
|
{
|
||||||
|
source = null;
|
||||||
|
var ext = System.IO.Path.GetExtension(path).ToLowerInvariant();
|
||||||
|
var fmt = ext switch
|
||||||
|
{
|
||||||
|
".json" => ConfigFormat.Json,
|
||||||
|
".ini" => ConfigFormat.Ini,
|
||||||
|
".xml" => ConfigFormat.Xml,
|
||||||
|
".toml" => ConfigFormat.Toml,
|
||||||
|
_ => (ConfigFormat?)null
|
||||||
|
};
|
||||||
|
|
||||||
|
if (fmt is null) return false;
|
||||||
|
source = new ConfigSource { Path = path, Format = fmt.Value };
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
using System.Text;
|
||||||
|
|
||||||
|
namespace Sharp.Config.Serialization;
|
||||||
|
|
||||||
|
public interface IConfigSerializer
|
||||||
|
{
|
||||||
|
string Serialize<T>(T instance, bool indented = true, Encoding? encoding = null);
|
||||||
|
T? Deserialize<T>(string content);
|
||||||
|
void SerializeToFile<T>(string path, T instance, bool indented = true, Encoding? encoding = null);
|
||||||
|
T? DeserializeFromFile<T>(string path, Encoding? encoding = null);
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,126 @@
|
|||||||
|
using System.Text;
|
||||||
|
|
||||||
|
namespace Sharp.Config.Serialization;
|
||||||
|
|
||||||
|
public sealed class IniConfigSerializer : IConfigSerializer
|
||||||
|
{
|
||||||
|
public string Serialize<T>(T instance, bool indented = true, Encoding? encoding = null)
|
||||||
|
{
|
||||||
|
var (root, sections) = ObjectToIniMaps(instance);
|
||||||
|
var sb = new StringBuilder();
|
||||||
|
foreach (var kv in root)
|
||||||
|
{
|
||||||
|
sb.Append(kv.Key).Append('=').Append(kv.Value).Append('\n');
|
||||||
|
}
|
||||||
|
foreach (var section in sections)
|
||||||
|
{
|
||||||
|
sb.Append('\n').Append('[').Append(section.Key).Append(']').Append('\n');
|
||||||
|
foreach (var kv in section.Value)
|
||||||
|
{
|
||||||
|
sb.Append(kv.Key).Append('=').Append(kv.Value).Append('\n');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return sb.ToString();
|
||||||
|
}
|
||||||
|
|
||||||
|
public T? Deserialize<T>(string content)
|
||||||
|
{
|
||||||
|
var dict = ParseIniToDictionary(content);
|
||||||
|
var json = System.Text.Json.JsonSerializer.Serialize(dict);
|
||||||
|
return System.Text.Json.JsonSerializer.Deserialize<T>(json);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void SerializeToFile<T>(string path, T instance, bool indented = true, Encoding? encoding = null)
|
||||||
|
{
|
||||||
|
var text = Serialize(instance, indented, encoding);
|
||||||
|
encoding ??= new UTF8Encoding(false);
|
||||||
|
File.WriteAllText(path, text, encoding);
|
||||||
|
}
|
||||||
|
|
||||||
|
public T? DeserializeFromFile<T>(string path, Encoding? encoding = null)
|
||||||
|
{
|
||||||
|
encoding ??= new UTF8Encoding(false);
|
||||||
|
var text = File.ReadAllText(path, encoding);
|
||||||
|
return Deserialize<T>(text);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static (Dictionary<string, string> root, Dictionary<string, Dictionary<string, string>> sections) ObjectToIniMaps<T>(T instance)
|
||||||
|
{
|
||||||
|
var root = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
|
||||||
|
var sections = new Dictionary<string, Dictionary<string, string>>(StringComparer.OrdinalIgnoreCase);
|
||||||
|
if (instance is null) return (root, sections);
|
||||||
|
|
||||||
|
var type = instance.GetType();
|
||||||
|
foreach (var prop in type.GetProperties())
|
||||||
|
{
|
||||||
|
var value = prop.GetValue(instance);
|
||||||
|
if (value is null) continue;
|
||||||
|
|
||||||
|
if (IsSimple(prop.PropertyType))
|
||||||
|
{
|
||||||
|
root[prop.Name] = Convert.ToString(value) ?? string.Empty;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var sec = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
|
||||||
|
foreach (var inner in prop.PropertyType.GetProperties())
|
||||||
|
{
|
||||||
|
var innerVal = inner.GetValue(value);
|
||||||
|
if (innerVal is null) continue;
|
||||||
|
if (IsSimple(inner.PropertyType))
|
||||||
|
{
|
||||||
|
sec[inner.Name] = Convert.ToString(innerVal) ?? string.Empty;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (sec.Count > 0)
|
||||||
|
{
|
||||||
|
sections[prop.Name] = sec;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (root, sections);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Dictionary<string, object?> ParseIniToDictionary(string content)
|
||||||
|
{
|
||||||
|
var dict = new Dictionary<string, object?>(StringComparer.OrdinalIgnoreCase);
|
||||||
|
var currentSection = (Dictionary<string, object?>?)null;
|
||||||
|
|
||||||
|
foreach (var rawLine in content.Split(new[] { "\r\n", "\n" }, StringSplitOptions.None))
|
||||||
|
{
|
||||||
|
var line = rawLine.Trim();
|
||||||
|
if (string.IsNullOrEmpty(line)) continue;
|
||||||
|
if (line.StartsWith(";") || line.StartsWith("#")) continue;
|
||||||
|
if (line.StartsWith("[") && line.EndsWith("]"))
|
||||||
|
{
|
||||||
|
var name = line.Substring(1, line.Length - 2).Trim();
|
||||||
|
currentSection = new Dictionary<string, object?>(StringComparer.OrdinalIgnoreCase);
|
||||||
|
dict[name] = currentSection;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var idx = line.IndexOf('=');
|
||||||
|
if (idx <= 0) continue;
|
||||||
|
var key = line.Substring(0, idx).Trim();
|
||||||
|
var value = line.Substring(idx + 1).Trim();
|
||||||
|
|
||||||
|
if (currentSection is null)
|
||||||
|
{
|
||||||
|
dict[key] = value;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
currentSection[key] = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return dict;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool IsSimple(Type t)
|
||||||
|
{
|
||||||
|
t = Nullable.GetUnderlyingType(t) ?? t;
|
||||||
|
return t.IsPrimitive || t.IsEnum || t == typeof(string) || t == typeof(decimal) || t == typeof(DateTime) || t == typeof(Guid) || t == typeof(DateTimeOffset) || t == typeof(TimeSpan);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
using System.Text;
|
||||||
|
using System.Text.Json;
|
||||||
|
|
||||||
|
namespace Sharp.Config.Serialization;
|
||||||
|
|
||||||
|
public sealed class JsonConfigSerializer : IConfigSerializer
|
||||||
|
{
|
||||||
|
private readonly JsonSerializerOptions _options;
|
||||||
|
|
||||||
|
public JsonConfigSerializer(JsonSerializerOptions? options = null)
|
||||||
|
{
|
||||||
|
_options = options ?? new JsonSerializerOptions
|
||||||
|
{
|
||||||
|
WriteIndented = true,
|
||||||
|
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
|
||||||
|
ReadCommentHandling = JsonCommentHandling.Skip,
|
||||||
|
AllowTrailingCommas = true
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public string Serialize<T>(T instance, bool indented = true, Encoding? encoding = null)
|
||||||
|
{
|
||||||
|
_options.WriteIndented = indented;
|
||||||
|
return JsonSerializer.Serialize(instance, _options);
|
||||||
|
}
|
||||||
|
|
||||||
|
public T? Deserialize<T>(string content)
|
||||||
|
=> JsonSerializer.Deserialize<T>(content, _options);
|
||||||
|
|
||||||
|
public void SerializeToFile<T>(string path, T instance, bool indented = true, Encoding? encoding = null)
|
||||||
|
{
|
||||||
|
var text = Serialize(instance, indented, encoding);
|
||||||
|
encoding ??= new UTF8Encoding(false);
|
||||||
|
File.WriteAllText(path, text, encoding);
|
||||||
|
}
|
||||||
|
|
||||||
|
public T? DeserializeFromFile<T>(string path, Encoding? encoding = null)
|
||||||
|
{
|
||||||
|
encoding ??= new UTF8Encoding(false);
|
||||||
|
var text = File.ReadAllText(path, encoding);
|
||||||
|
return Deserialize<T>(text);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
using System.Text;
|
||||||
|
using Tomlyn;
|
||||||
|
|
||||||
|
namespace Sharp.Config.Serialization;
|
||||||
|
|
||||||
|
public sealed class TomlConfigSerializer : IConfigSerializer
|
||||||
|
{
|
||||||
|
public string Serialize<T>(T instance, bool indented = true, Encoding? encoding = null)
|
||||||
|
{
|
||||||
|
// Tomlyn writes with indentation by default
|
||||||
|
return Toml.FromModel(instance!, new TomlModelOptions { ConvertIndentation = true });
|
||||||
|
}
|
||||||
|
|
||||||
|
public T? Deserialize<T>(string content)
|
||||||
|
{
|
||||||
|
return Toml.ToModel<T>(content);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void SerializeToFile<T>(string path, T instance, bool indented = true, Encoding? encoding = null)
|
||||||
|
{
|
||||||
|
var text = Serialize(instance, indented, encoding);
|
||||||
|
encoding ??= new UTF8Encoding(false);
|
||||||
|
File.WriteAllText(path, text, encoding);
|
||||||
|
}
|
||||||
|
|
||||||
|
public T? DeserializeFromFile<T>(string path, Encoding? encoding = null)
|
||||||
|
{
|
||||||
|
encoding ??= new UTF8Encoding(false);
|
||||||
|
var text = File.ReadAllText(path, encoding);
|
||||||
|
return Deserialize<T>(text);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
using System.Text;
|
||||||
|
using System.Xml.Serialization;
|
||||||
|
|
||||||
|
namespace Sharp.Config.Serialization;
|
||||||
|
|
||||||
|
public sealed class XmlConfigSerializer : IConfigSerializer
|
||||||
|
{
|
||||||
|
public string Serialize<T>(T instance, bool indented = true, Encoding? encoding = null)
|
||||||
|
{
|
||||||
|
var ns = new XmlSerializerNamespaces();
|
||||||
|
ns.Add(string.Empty, string.Empty);
|
||||||
|
var serializer = new XmlSerializer(typeof(T));
|
||||||
|
using var sw = new StringWriter();
|
||||||
|
serializer.Serialize(sw, instance, ns);
|
||||||
|
return sw.ToString();
|
||||||
|
}
|
||||||
|
|
||||||
|
public T? Deserialize<T>(string content)
|
||||||
|
{
|
||||||
|
var serializer = new XmlSerializer(typeof(T));
|
||||||
|
using var sr = new StringReader(content);
|
||||||
|
return (T?)serializer.Deserialize(sr);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void SerializeToFile<T>(string path, T instance, bool indented = true, Encoding? encoding = null)
|
||||||
|
{
|
||||||
|
var text = Serialize(instance, indented, encoding);
|
||||||
|
encoding ??= new UTF8Encoding(false);
|
||||||
|
File.WriteAllText(path, text, encoding);
|
||||||
|
}
|
||||||
|
|
||||||
|
public T? DeserializeFromFile<T>(string path, Encoding? encoding = null)
|
||||||
|
{
|
||||||
|
encoding ??= new UTF8Encoding(false);
|
||||||
|
var text = File.ReadAllText(path, encoding);
|
||||||
|
return Deserialize<T>(text);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net8.0</TargetFramework>
|
||||||
|
<LangVersion>latest</LangVersion>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.12.0"/>
|
||||||
|
<PackageReference Include="MSTest" Version="3.6.4"/>
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<Using Include="Microsoft.VisualStudio.TestTools.UnitTesting"/>
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\..\src\Sharp\Common\Common.csproj"/>
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
Reference in new issue
Block a user