mirror of
https://github.com/GeWuYou/GFramework.git
synced 2026-05-07 00:39:00 +08:00
feat(data): 添加统一设置数据仓库和JSON序列化器实现
- 实现UnifiedSettingsDataRepository统一管理所有设置数据 - 添加JsonSerializer基于Newtonsoft.Json的序列化功能 - 创建SettingsModel管理设置数据生命周期和迁移 - 添加完整的单元测试验证持久化功能 - 实现数据类型注册和批量保存加载功能 - 支持设置数据的版本迁移和事件通知机制
This commit is contained in:
parent
46ea6f1ffd
commit
21b4c826d4
20
GFramework.Game.Tests/Data/PersistenceTestUtilities.cs
Normal file
20
GFramework.Game.Tests/Data/PersistenceTestUtilities.cs
Normal file
@ -0,0 +1,20 @@
|
||||
using GFramework.Game.Abstractions.Data;
|
||||
using GFramework.Game.Abstractions.Enums;
|
||||
|
||||
namespace GFramework.Game.Tests.Data;
|
||||
|
||||
internal sealed record TestDataLocation(
|
||||
string Key,
|
||||
StorageKinds Kinds = StorageKinds.Local,
|
||||
string? Namespace = null,
|
||||
IReadOnlyDictionary<string, string>? Metadata = null) : IDataLocation;
|
||||
|
||||
internal sealed class TestSaveData : IData
|
||||
{
|
||||
public string Name { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
internal sealed class TestSimpleData : IData
|
||||
{
|
||||
public int Value { get; set; }
|
||||
}
|
||||
95
GFramework.Game.Tests/Data/PersistenceTests.cs
Normal file
95
GFramework.Game.Tests/Data/PersistenceTests.cs
Normal file
@ -0,0 +1,95 @@
|
||||
using System.IO;
|
||||
using GFramework.Game.Abstractions.Data;
|
||||
using GFramework.Game.Data;
|
||||
using GFramework.Game.Serializer;
|
||||
using GFramework.Game.Storage;
|
||||
|
||||
namespace GFramework.Game.Tests.Data;
|
||||
|
||||
[TestFixture]
|
||||
public class PersistenceTests
|
||||
{
|
||||
private static string CreateTempRoot()
|
||||
{
|
||||
var path = Path.Combine(Path.GetTempPath(), "gframework-persistence", Guid.NewGuid().ToString("N"));
|
||||
Directory.CreateDirectory(path);
|
||||
return path;
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task FileStorage_PersistsDataAndRejectsIllegalKeys()
|
||||
{
|
||||
var root = CreateTempRoot();
|
||||
using var storage = new FileStorage(root, new JsonSerializer(), ".json");
|
||||
|
||||
var saved = new TestSimpleData { Value = 5 };
|
||||
await storage.WriteAsync("folder/item", saved);
|
||||
|
||||
var loaded = await storage.ReadAsync<TestSimpleData>("folder/item");
|
||||
Assert.That(loaded.Value, Is.EqualTo(saved.Value));
|
||||
|
||||
Assert.ThrowsAsync<ArgumentException>(async () => await storage.WriteAsync("../escape", new TestSimpleData()));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task SaveRepository_ManagesSlots()
|
||||
{
|
||||
var root = CreateTempRoot();
|
||||
using var storage = new FileStorage(root, new JsonSerializer());
|
||||
var config = new SaveConfiguration
|
||||
{
|
||||
SaveRoot = "saves",
|
||||
SaveSlotPrefix = "slot_",
|
||||
SaveFileName = "save.json"
|
||||
};
|
||||
|
||||
var repository = new SaveRepository<TestSaveData>(storage, config);
|
||||
var data = new TestSaveData { Name = "hero" };
|
||||
|
||||
await repository.SaveAsync(1, data);
|
||||
Assert.That(await repository.ExistsAsync(1));
|
||||
|
||||
var loaded = await repository.LoadAsync(1);
|
||||
Assert.That(loaded.Name, Is.EqualTo(data.Name));
|
||||
|
||||
var slots = await repository.ListSlotsAsync();
|
||||
Assert.That(slots, Is.EqualTo(new[] { 1 }));
|
||||
|
||||
await repository.DeleteAsync(1);
|
||||
Assert.That(await repository.ExistsAsync(1), Is.False);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task UnifiedSettingsDataRepository_RoundTripsDataAndLoadAll()
|
||||
{
|
||||
var root = CreateTempRoot();
|
||||
using var storage = new FileStorage(root, new JsonSerializer());
|
||||
var serializer = new JsonSerializer();
|
||||
var repo = new UnifiedSettingsDataRepository(
|
||||
storage,
|
||||
serializer,
|
||||
new DataRepositoryOptions { EnableEvents = false },
|
||||
"settings.json");
|
||||
|
||||
var location = new TestDataLocation("settings/choice");
|
||||
repo.RegisterDataType(location, typeof(TestSimpleData));
|
||||
|
||||
var data = new TestSimpleData { Value = 42 };
|
||||
await repo.SaveAsync(location, data);
|
||||
|
||||
using var storage2 = new FileStorage(root, new JsonSerializer());
|
||||
var repo2 = new UnifiedSettingsDataRepository(
|
||||
storage2,
|
||||
serializer,
|
||||
new DataRepositoryOptions { EnableEvents = false },
|
||||
"settings.json");
|
||||
repo2.RegisterDataType(location, typeof(TestSimpleData));
|
||||
|
||||
var loaded = await repo2.LoadAsync<TestSimpleData>(location);
|
||||
Assert.That(loaded.Value, Is.EqualTo(data.Value));
|
||||
|
||||
var all = await repo2.LoadAllAsync();
|
||||
Assert.That(all.Keys, Contains.Item(location.Key));
|
||||
Assert.That(all[location.Key], Is.TypeOf<TestSimpleData>());
|
||||
}
|
||||
}
|
||||
140
GFramework.Game.Tests/Serializer/JsonSerializerTests.cs
Normal file
140
GFramework.Game.Tests/Serializer/JsonSerializerTests.cs
Normal file
@ -0,0 +1,140 @@
|
||||
using Newtonsoft.Json;
|
||||
using GameJsonSerializer = GFramework.Game.Serializer.JsonSerializer;
|
||||
|
||||
namespace GFramework.Game.Tests.Serializer;
|
||||
|
||||
[TestFixture]
|
||||
public sealed class JsonSerializerTests
|
||||
{
|
||||
[Test]
|
||||
public void Serialize_And_Deserialize_Should_RoundTrip_Object()
|
||||
{
|
||||
var serializer = new GameJsonSerializer();
|
||||
var original = new PlayerStateStub
|
||||
{
|
||||
Name = "Player1",
|
||||
Level = 7
|
||||
};
|
||||
|
||||
var json = serializer.Serialize(original);
|
||||
var restored = serializer.Deserialize<PlayerStateStub>(json);
|
||||
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.That(restored.Name, Is.EqualTo("Player1"));
|
||||
Assert.That(restored.Level, Is.EqualTo(7));
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Serialize_Should_Honor_Injected_Settings()
|
||||
{
|
||||
var serializer = new GameJsonSerializer(new JsonSerializerSettings
|
||||
{
|
||||
Formatting = Formatting.Indented,
|
||||
NullValueHandling = NullValueHandling.Ignore
|
||||
});
|
||||
|
||||
var json = serializer.Serialize(new OptionalStateStub
|
||||
{
|
||||
Name = "Configured",
|
||||
Description = null
|
||||
});
|
||||
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.That(json, Does.Contain(Environment.NewLine));
|
||||
Assert.That(json, Does.Contain("\"Name\": \"Configured\""));
|
||||
Assert.That(json, Does.Not.Contain("Description"));
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Converters_Should_Be_Used_For_Serialization_And_Deserialization()
|
||||
{
|
||||
var serializer = new GameJsonSerializer();
|
||||
serializer.Converters.Add(new CoordinateStubConverter());
|
||||
|
||||
var json = serializer.Serialize(new CoordinateStub { X = 3, Y = 9 });
|
||||
var restored = serializer.Deserialize<CoordinateStub>(json);
|
||||
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.That(json, Is.EqualTo("\"3:9\""));
|
||||
Assert.That(restored.X, Is.EqualTo(3));
|
||||
Assert.That(restored.Y, Is.EqualTo(9));
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Deserialize_Should_Throw_With_Target_Type_Context_When_Json_Is_Invalid()
|
||||
{
|
||||
var serializer = new GameJsonSerializer();
|
||||
|
||||
var exception =
|
||||
Assert.Throws<InvalidOperationException>(() => serializer.Deserialize<PlayerStateStub>("{invalid json}"));
|
||||
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.That(exception, Is.Not.Null);
|
||||
Assert.That(exception!.Message, Does.Contain(typeof(PlayerStateStub).FullName));
|
||||
Assert.That(exception.InnerException, Is.Not.Null);
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Deserialize_With_Runtime_Type_Should_Return_Target_Runtime_Type()
|
||||
{
|
||||
var serializer = new GameJsonSerializer();
|
||||
|
||||
var restored = serializer.Deserialize("{\"Name\":\"Runtime\",\"Level\":11}", typeof(PlayerStateStub));
|
||||
|
||||
Assert.That(restored, Is.TypeOf<PlayerStateStub>());
|
||||
Assert.That(((PlayerStateStub)restored).Level, Is.EqualTo(11));
|
||||
}
|
||||
|
||||
private sealed class PlayerStateStub
|
||||
{
|
||||
public string Name { get; set; } = string.Empty;
|
||||
|
||||
public int Level { get; set; }
|
||||
}
|
||||
|
||||
private sealed class OptionalStateStub
|
||||
{
|
||||
public string Name { get; set; } = string.Empty;
|
||||
|
||||
public string? Description { get; set; }
|
||||
}
|
||||
|
||||
private sealed class CoordinateStub
|
||||
{
|
||||
public int X { get; set; }
|
||||
|
||||
public int Y { get; set; }
|
||||
}
|
||||
|
||||
private sealed class CoordinateStubConverter : JsonConverter<CoordinateStub>
|
||||
{
|
||||
public override void WriteJson(JsonWriter writer, CoordinateStub? value, JsonSerializer serializer)
|
||||
{
|
||||
writer.WriteValue($"{value?.X}:{value?.Y}");
|
||||
}
|
||||
|
||||
public override CoordinateStub? ReadJson(
|
||||
JsonReader reader,
|
||||
Type objectType,
|
||||
CoordinateStub? existingValue,
|
||||
bool hasExistingValue,
|
||||
JsonSerializer serializer)
|
||||
{
|
||||
var raw = (string?)reader.Value ?? throw new JsonSerializationException("Coordinate value cannot be null.");
|
||||
var parts = raw.Split(':');
|
||||
return new CoordinateStub
|
||||
{
|
||||
X = int.Parse(parts[0]),
|
||||
Y = int.Parse(parts[1])
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
181
GFramework.Game.Tests/Setting/SettingsModelTests.cs
Normal file
181
GFramework.Game.Tests/Setting/SettingsModelTests.cs
Normal file
@ -0,0 +1,181 @@
|
||||
using GFramework.Core.Abstractions.Architectures;
|
||||
using GFramework.Core.Abstractions.Lifecycle;
|
||||
using GFramework.Core.Abstractions.Rule;
|
||||
using GFramework.Game.Abstractions.Data;
|
||||
using GFramework.Game.Abstractions.Enums;
|
||||
using GFramework.Game.Abstractions.Setting;
|
||||
using GFramework.Game.Setting;
|
||||
|
||||
namespace GFramework.Game.Tests.Setting;
|
||||
|
||||
[TestFixture]
|
||||
public sealed class SettingsModelTests
|
||||
{
|
||||
[Test]
|
||||
public void GetData_After_Initialize_Should_Register_New_Type_In_Repository()
|
||||
{
|
||||
var locationProvider = new TestDataLocationProvider();
|
||||
var repository = new FakeSettingsDataRepository();
|
||||
var model = new SettingsModel<FakeSettingsDataRepository>(locationProvider, repository);
|
||||
((IContextAware)model).SetContext(new Mock<IArchitectureContext>(MockBehavior.Loose).Object);
|
||||
|
||||
((IInitializable)model).Initialize();
|
||||
|
||||
_ = model.GetData<TestSettingsData>();
|
||||
|
||||
Assert.That(repository.RegisteredTypes, Contains.Key("TestSettingsData"));
|
||||
Assert.That(repository.RegisteredTypes["TestSettingsData"], Is.EqualTo(typeof(TestSettingsData)));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task RegisterMigration_After_Cache_Warmup_Should_Invalidate_Type_Cache()
|
||||
{
|
||||
var locationProvider = new TestDataLocationProvider();
|
||||
var repository = new FakeSettingsDataRepository();
|
||||
var model = new SettingsModel<FakeSettingsDataRepository>(locationProvider, repository);
|
||||
((IContextAware)model).SetContext(new Mock<IArchitectureContext>(MockBehavior.Loose).Object);
|
||||
|
||||
_ = model.GetData<TestSettingsData>();
|
||||
((IInitializable)model).Initialize();
|
||||
|
||||
repository.Stored["TestSettingsData"] = new TestSettingsData
|
||||
{
|
||||
Version = 1,
|
||||
Value = "legacy"
|
||||
};
|
||||
|
||||
await model.InitializeAsync();
|
||||
Assert.That(model.GetData<TestSettingsData>().Version, Is.EqualTo(1));
|
||||
|
||||
model.RegisterMigration(new TestSettingsMigration());
|
||||
|
||||
repository.Stored["TestSettingsData"] = new TestSettingsData
|
||||
{
|
||||
Version = 1,
|
||||
Value = "legacy"
|
||||
};
|
||||
|
||||
await model.InitializeAsync();
|
||||
|
||||
var current = model.GetData<TestSettingsData>();
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.That(current.Version, Is.EqualTo(2));
|
||||
Assert.That(current.Value, Is.EqualTo("legacy-migrated"));
|
||||
});
|
||||
}
|
||||
|
||||
private sealed class TestSettingsData : ISettingsData
|
||||
{
|
||||
public string Value { get; set; } = "default";
|
||||
|
||||
public int Version { get; set; } = 1;
|
||||
|
||||
public DateTime LastModified { get; } = DateTime.UtcNow;
|
||||
|
||||
public void Reset()
|
||||
{
|
||||
Value = "default";
|
||||
Version = 1;
|
||||
}
|
||||
|
||||
public void LoadFrom(ISettingsData source)
|
||||
{
|
||||
if (source is not TestSettingsData data)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Value = data.Value;
|
||||
Version = data.Version;
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class TestSettingsMigration : ISettingsMigration
|
||||
{
|
||||
public Type SettingsType => typeof(TestSettingsData);
|
||||
|
||||
public int FromVersion => 1;
|
||||
|
||||
public int ToVersion => 2;
|
||||
|
||||
public ISettingsSection Migrate(ISettingsSection oldData)
|
||||
{
|
||||
var data = (TestSettingsData)oldData;
|
||||
return new TestSettingsData
|
||||
{
|
||||
Version = 2,
|
||||
Value = $"{data.Value}-migrated"
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class FakeSettingsDataRepository : ISettingsDataRepository
|
||||
{
|
||||
public Dictionary<string, Type> RegisteredTypes { get; } = new(StringComparer.Ordinal);
|
||||
|
||||
public Dictionary<string, IData> Stored { get; } = new(StringComparer.Ordinal);
|
||||
|
||||
public Task<T> LoadAsync<T>(IDataLocation location) where T : class, IData, new()
|
||||
{
|
||||
return Task.FromResult(Stored.TryGetValue(location.Key, out var data) ? (T)data : new T());
|
||||
}
|
||||
|
||||
public Task SaveAsync<T>(IDataLocation location, T data) where T : class, IData
|
||||
{
|
||||
Stored[location.Key] = data;
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task<bool> ExistsAsync(IDataLocation location)
|
||||
{
|
||||
return Task.FromResult(Stored.ContainsKey(location.Key));
|
||||
}
|
||||
|
||||
public Task DeleteAsync(IDataLocation location)
|
||||
{
|
||||
Stored.Remove(location.Key);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task SaveAllAsync(IEnumerable<(IDataLocation location, IData data)> dataList)
|
||||
{
|
||||
foreach (var (location, data) in dataList)
|
||||
{
|
||||
Stored[location.Key] = data;
|
||||
}
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task<IDictionary<string, IData>> LoadAllAsync()
|
||||
{
|
||||
IDictionary<string, IData> snapshot = new Dictionary<string, IData>(Stored, StringComparer.Ordinal);
|
||||
return Task.FromResult(snapshot);
|
||||
}
|
||||
|
||||
public void RegisterDataType(IDataLocation location, Type type)
|
||||
{
|
||||
RegisteredTypes[location.Key] = type;
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class TestDataLocationProvider : IDataLocationProvider
|
||||
{
|
||||
public IDataLocation GetLocation(Type type)
|
||||
{
|
||||
return new TestDataLocation(type.Name);
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class TestDataLocation(string key) : IDataLocation
|
||||
{
|
||||
public string Key { get; } = key;
|
||||
|
||||
public StorageKinds Kinds => StorageKinds.Memory;
|
||||
|
||||
public string? Namespace => "tests";
|
||||
|
||||
public IReadOnlyDictionary<string, string>? Metadata => null;
|
||||
}
|
||||
}
|
||||
@ -80,11 +80,10 @@ public class UnifiedSettingsDataRepository(
|
||||
public async Task SaveAsync<T>(IDataLocation location, T data)
|
||||
where T : class, IData
|
||||
{
|
||||
await EnsureLoadedAsync();
|
||||
await _lock.WaitAsync();
|
||||
try
|
||||
{
|
||||
await EnsureLoadedAsync();
|
||||
|
||||
var key = location.Key;
|
||||
var serialized = Serializer.Serialize(data);
|
||||
|
||||
@ -211,7 +210,9 @@ public class UnifiedSettingsDataRepository(
|
||||
|
||||
var key = UnifiedKey;
|
||||
|
||||
_file = await Storage.ExistsAsync(key) ? await Storage.ReadAsync<UnifiedSettingsFile>(key) : new UnifiedSettingsFile { Version = 1 };
|
||||
_file = await Storage.ExistsAsync(key)
|
||||
? await Storage.ReadAsync<UnifiedSettingsFile>(key)
|
||||
: new UnifiedSettingsFile { Version = 1 };
|
||||
|
||||
_loaded = true;
|
||||
}
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
using GFramework.Core.Abstractions.Serializer;
|
||||
using Newtonsoft.Json;
|
||||
using NewtonsoftJsonException = Newtonsoft.Json.JsonException;
|
||||
|
||||
namespace GFramework.Game.Serializer;
|
||||
|
||||
@ -9,6 +10,27 @@ namespace GFramework.Game.Serializer;
|
||||
public sealed class JsonSerializer
|
||||
: IRuntimeTypeSerializer
|
||||
{
|
||||
private readonly JsonSerializerSettings _settings;
|
||||
|
||||
/// <summary>
|
||||
/// 初始化 JSON 序列化器。
|
||||
/// </summary>
|
||||
/// <param name="settings">可选的 Newtonsoft.Json 配置;不提供时使用默认配置。</param>
|
||||
public JsonSerializer(JsonSerializerSettings? settings = null)
|
||||
{
|
||||
_settings = settings ?? new JsonSerializerSettings();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取当前序列化器使用的 Newtonsoft.Json 配置实例。
|
||||
/// </summary>
|
||||
public JsonSerializerSettings Settings => _settings;
|
||||
|
||||
/// <summary>
|
||||
/// 获取当前序列化器使用的自定义转换器集合。
|
||||
/// </summary>
|
||||
public IList<JsonConverter> Converters => _settings.Converters;
|
||||
|
||||
/// <summary>
|
||||
/// 将指定类型的对象序列化为JSON字符串
|
||||
/// </summary>
|
||||
@ -17,21 +39,9 @@ public sealed class JsonSerializer
|
||||
/// <returns>序列化后的JSON字符串</returns>
|
||||
public string Serialize<T>(T value)
|
||||
{
|
||||
return JsonConvert.SerializeObject(value);
|
||||
return JsonConvert.SerializeObject(value, _settings);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 将JSON字符串反序列化为指定类型的对象
|
||||
/// </summary>
|
||||
/// <typeparam name="T">要反序列化的目标类型</typeparam>
|
||||
/// <param name="data">要反序列化的JSON字符串数据</param>
|
||||
/// <returns>反序列化后的对象实例</returns>
|
||||
/// <exception cref="ArgumentException">当无法反序列化数据时抛出</exception>
|
||||
public T Deserialize<T>(string data)
|
||||
{
|
||||
return JsonConvert.DeserializeObject<T>(data)
|
||||
?? throw new ArgumentException("Cannot deserialize data");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 将对象序列化为JSON字符串(使用运行时类型)
|
||||
@ -41,7 +51,25 @@ public sealed class JsonSerializer
|
||||
/// <returns>序列化后的JSON字符串</returns>
|
||||
public string Serialize(object obj, Type type)
|
||||
{
|
||||
return JsonConvert.SerializeObject(obj, type, null);
|
||||
ArgumentNullException.ThrowIfNull(obj);
|
||||
ArgumentNullException.ThrowIfNull(type);
|
||||
|
||||
return JsonConvert.SerializeObject(obj, type, _settings);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 将JSON字符串反序列化为指定类型的对象
|
||||
/// </summary>
|
||||
/// <typeparam name="T">要反序列化的目标类型</typeparam>
|
||||
/// <param name="data">要反序列化的JSON字符串数据</param>
|
||||
/// <returns>反序列化后的对象实例</returns>
|
||||
/// <exception cref="InvalidOperationException">当无法反序列化数据时抛出</exception>
|
||||
public T Deserialize<T>(string data)
|
||||
{
|
||||
return (T)DeserializeCore(
|
||||
data,
|
||||
typeof(T),
|
||||
static (json, type, settings) => JsonConvert.DeserializeObject<T>(json, settings));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -50,10 +78,40 @@ public sealed class JsonSerializer
|
||||
/// <param name="data">要反序列化的JSON字符串数据</param>
|
||||
/// <param name="type">反序列化目标类型</param>
|
||||
/// <returns>反序列化后的对象实例</returns>
|
||||
/// <exception cref="ArgumentException">当无法反序列化到指定类型时抛出</exception>
|
||||
/// <exception cref="InvalidOperationException">当无法反序列化到指定类型时抛出</exception>
|
||||
public object Deserialize(string data, Type type)
|
||||
{
|
||||
return JsonConvert.DeserializeObject(data, type)
|
||||
?? throw new ArgumentException($"Cannot deserialize to {type.Name}");
|
||||
return DeserializeCore(
|
||||
data,
|
||||
type,
|
||||
static (json, targetType, settings) => JsonConvert.DeserializeObject(json, targetType, settings));
|
||||
}
|
||||
|
||||
private object DeserializeCore(
|
||||
string data,
|
||||
Type targetType,
|
||||
Func<string, Type, JsonSerializerSettings, object?> deserialize)
|
||||
{
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(data);
|
||||
ArgumentNullException.ThrowIfNull(targetType);
|
||||
ArgumentNullException.ThrowIfNull(deserialize);
|
||||
|
||||
try
|
||||
{
|
||||
var result = deserialize(data, targetType, _settings);
|
||||
if (result == null)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"Deserialization returned null for target type '{targetType.FullName}'.");
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
catch (Exception ex) when (ex is NewtonsoftJsonException or InvalidCastException or ArgumentException)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"Failed to deserialize JSON to target type '{targetType.FullName}'.",
|
||||
ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -59,7 +59,9 @@ public class SettingsModel<TRepository>(IDataLocationProvider? locationProvider,
|
||||
public T GetData<T>() where T : class, ISettingsData, new()
|
||||
{
|
||||
// 使用_data字典获取或添加指定类型的实例,确保唯一性
|
||||
return (T)_data.GetOrAdd(typeof(T), _ => new T());
|
||||
var data = (T)_data.GetOrAdd(typeof(T), _ => new T());
|
||||
TryRegisterDataType(typeof(T));
|
||||
return data;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -84,6 +86,7 @@ public class SettingsModel<TRepository>(IDataLocationProvider? locationProvider,
|
||||
{
|
||||
_applicators[typeof(T)] = applicator;
|
||||
_data[applicator.DataType] = applicator.Data;
|
||||
TryRegisterDataType(applicator.DataType);
|
||||
return this;
|
||||
}
|
||||
|
||||
@ -114,6 +117,7 @@ public class SettingsModel<TRepository>(IDataLocationProvider? locationProvider,
|
||||
public ISettingsModel RegisterMigration(ISettingsMigration migration)
|
||||
{
|
||||
_migrations[(migration.SettingsType, migration.FromVersion)] = migration;
|
||||
_migrationCache.TryRemove(migration.SettingsType, out _);
|
||||
return this;
|
||||
}
|
||||
|
||||
@ -258,11 +262,21 @@ public class SettingsModel<TRepository>(IDataLocationProvider? locationProvider,
|
||||
// 遍历所有已知的数据类型,为其分配位置并注册到数据仓库中
|
||||
foreach (var type in _data.Keys)
|
||||
{
|
||||
var location = _locationProvider.GetLocation(type);
|
||||
DataRepository.RegisterDataType(location, type);
|
||||
TryRegisterDataType(type);
|
||||
}
|
||||
}
|
||||
|
||||
private void TryRegisterDataType(Type type)
|
||||
{
|
||||
if (_repository == null || _locationProvider == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var location = _locationProvider.GetLocation(type);
|
||||
_repository.RegisterDataType(location, type);
|
||||
}
|
||||
|
||||
private ISettingsData MigrateIfNeeded(ISettingsData data)
|
||||
{
|
||||
if (data is not IVersionedData versioned)
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user