1158 lines
49 KiB
C#
1158 lines
49 KiB
C#
#nullable enable
|
|
|
|
using Newtonsoft.Json.Linq;
|
|
using System;
|
|
using System.Collections;
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
using System.Threading;
|
|
using Cysharp.Threading.Tasks;
|
|
using UVC.Extention;
|
|
|
|
namespace UVC.Data.Core
|
|
{
|
|
/// <summary>
|
|
/// 키-값 쌍의 데이터를 관리하고 변경 사항을 추적하는 동적 데이터 객체입니다.
|
|
/// SortedDictionary를 상속하여 키를 기준으로 정렬된 데이터를 제공합니다.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// 이 클래스는 JSON과 호환되는 데이터 구조를 표현하며, 데이터 변경 추적 기능을 통해
|
|
/// 효율적인 데이터 동기화를 지원합니다.
|
|
/// </remarks>
|
|
/// <example>
|
|
/// <code>
|
|
/// // DataMask 생성 및 설정
|
|
/// var mask = new DataMask();
|
|
/// mask.ObjectIdKey = "Id";
|
|
/// mask.ObjectName = "users";
|
|
///
|
|
/// // 필드 이름 변환 설정
|
|
/// mask.NamesForReplace = new Dictionary<string, string>
|
|
/// {
|
|
/// { "userName", "name" },
|
|
/// { "userEmail", "email" }
|
|
/// };
|
|
///
|
|
/// // JObject 속성으로 마스킹 규칙 추가
|
|
/// mask["include"] = new JArray("name", "email", "age");
|
|
/// mask["exclude"] = new JArray("password", "token");
|
|
///
|
|
/// // DataObject에 마스크 적용하는 예시
|
|
/// var dataObject = new DataObject();
|
|
/// dataObject["userName"] = "홍길동";
|
|
/// dataObject["userEmail"] = "hong@example.com";
|
|
/// dataObject["password"] = "secret123";
|
|
///
|
|
/// // 마스크 적용 결과 (개념적 예시):
|
|
/// // - userName은 name으로 변환됨
|
|
/// // - userEmail은 email로 변환됨
|
|
/// // - password는 제외됨
|
|
/// </code>
|
|
/// </example>
|
|
public partial class DataObject : OrderedDictionary<string, object?>, IDataObject
|
|
{
|
|
private bool isInPool = false;
|
|
/// <summary>
|
|
/// 이 객체가 객체 풀에 있는지 여부를 나타냅니다.
|
|
/// 중복 반환을 방지하기 위해 DataObjectPool에서 내부적으로 사용됩니다.
|
|
/// </summary>
|
|
internal bool IsInPool
|
|
{
|
|
get => isInPool;
|
|
set
|
|
{
|
|
isInPool = value;
|
|
foreach (var item in this)
|
|
{
|
|
if (item.Value is DataObject dataObject)
|
|
{
|
|
dataObject.isInPool = value; // 내부 DataObject도 풀에 있다고 표시합니다.
|
|
}
|
|
else if (item.Value is DataArray dataArray)
|
|
{
|
|
dataArray.IsInPool = value; // 내부 DataArray도 풀에 있다고 표시합니다.
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private bool createdFromPool = false;
|
|
|
|
/// <summary>
|
|
/// 객체가 풀에서 생성되었는지 여부를 나타냅니다.
|
|
/// </summary>
|
|
internal bool CreatedFromPool
|
|
{
|
|
get => createdFromPool;
|
|
set
|
|
{
|
|
createdFromPool = value;
|
|
foreach (var item in this)
|
|
{
|
|
if (item.Value is DataObject dataObject)
|
|
{
|
|
dataObject.CreatedFromPool = value; // 내부 DataObject도 풀에 있다고 표시합니다.
|
|
}
|
|
else if (item.Value is DataArray dataArray)
|
|
{
|
|
dataArray.CreatedFromPool = value; // 내부 DataArray도 풀에 있다고 표시합니다.
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// 객체의 고유 식별자를 나타내는 속성입니다. DataArray에서 데이터를 식별하는 데 사용됩니다.
|
|
/// </summary>
|
|
public string Id
|
|
{
|
|
get
|
|
{
|
|
if (IdKey != null && ContainsKey(IdKey))
|
|
{
|
|
return this[IdKey]?.ToString() ?? string.Empty;
|
|
}
|
|
if (Count > 0)
|
|
{
|
|
return this.First().Value?.ToString() ?? string.Empty;
|
|
}
|
|
return string.Empty;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Id에 해당하는 key 문자열
|
|
/// </summary>
|
|
public string? IdKey { get; set; } = null;
|
|
|
|
/// <summary>
|
|
/// DataObject의 이름을 나타내는 속성입니다.
|
|
/// </summary>
|
|
public string Name { get; internal set; } = string.Empty;
|
|
|
|
|
|
/// <summary>
|
|
/// 직접적인 변경이 있었던 키를 저장하는 리스트입니다.
|
|
/// </summary>
|
|
protected HashSet<string> changedProperies = new HashSet<string>();
|
|
|
|
/// <summary>
|
|
/// 변경된 속성의 키 목록을 읽기 전용으로 반환합니다.
|
|
/// </summary>
|
|
public IReadOnlyCollection<string> ChangedProperies => changedProperies;
|
|
|
|
// 객체 생성 중에는 변경 추적을 비활성화하기 위한 플래그
|
|
private bool _isInitializing = false;
|
|
|
|
/// <summary>
|
|
/// 기본 생성자입니다. 빈 데이터 객체를 생성합니다.
|
|
/// </summary>
|
|
public DataObject() { }
|
|
|
|
public DataObject(string jsonString) : this(JObject.Parse(jsonString))
|
|
{
|
|
}
|
|
|
|
public DataObject(System.IO.Stream jsonStream)
|
|
{
|
|
if (jsonStream == null)
|
|
throw new ArgumentNullException(nameof(jsonStream));
|
|
|
|
// 스트림 처리 최적화를 위해 청크 단위로 읽을 수 있지만,
|
|
// 현재는 Newtonsoft.Json의 기본 역직렬화 사용
|
|
using (var reader = new Newtonsoft.Json.JsonTextReader(new System.IO.StreamReader(jsonStream)))
|
|
{
|
|
// 청크 읽기 설정 - 메모리 사용량 최적화
|
|
reader.SupportMultipleContent = true;
|
|
|
|
var serializer = new Newtonsoft.Json.JsonSerializer();
|
|
var sourceObject = serializer.Deserialize<JObject>(reader);
|
|
|
|
// 수정된 코드: 생성자를 호출하는 대신 FromJObject 메서드를 사용
|
|
if (sourceObject != null) FromJObject(sourceObject);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// JObject로부터 데이터 객체를 생성합니다.
|
|
/// </summary>
|
|
/// <param name="other">복사할 JObject 객체</param>
|
|
public DataObject(JObject other)
|
|
{
|
|
_isInitializing = true;
|
|
try
|
|
{
|
|
// JObject로부터 속성을 복사
|
|
foreach (var prop in other.Properties())
|
|
{
|
|
// 인덱서를 사용하여 JToken을 객체로 변환하고 추가
|
|
this[prop.Name] = ConvertJTokenToObject(prop.Value);
|
|
}
|
|
}
|
|
finally
|
|
{
|
|
_isInitializing = false;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Dictionary로 데이터 객체를 초기화합니다.
|
|
/// </summary>
|
|
/// <param name="dictionary">초기화에 사용할 Dictionary 객체</param>
|
|
public DataObject(Dictionary<string, object?> dictionary) : base()
|
|
{
|
|
_isInitializing = true;
|
|
try
|
|
{
|
|
// 생성자에서 초기 속성들을 등록
|
|
foreach (var item in dictionary)
|
|
{
|
|
this[item.Key] = item.Value;
|
|
}
|
|
}
|
|
finally
|
|
{
|
|
_isInitializing = false;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// 지정된 <see cref="JObject"/>의 속성과 값으로 현재 객체를 채웁니다.
|
|
/// </summary>
|
|
/// <remarks>이 메서드는 제공된 <see cref="JObject"/>의 모든 속성을 반복하고
|
|
/// 해당 값을 현재 객체의 해당 속성에 할당합니다. 현재 객체에 속성이 없는 경우
|
|
/// 동적으로 추가됩니다. 이 메서드는 채우기 프로세스 중 적절한 처리를 보장하기 위해 내부 초기화
|
|
/// 플래그를 임시로 설정합니다.</remarks>
|
|
/// <param name="other">복사할 속성과 값이 포함된 <see cref="JObject"/>입니다.</param>
|
|
public DataObject FromJObject(JObject other)
|
|
{
|
|
IdKey = null; // IdKey를 초기화합니다.
|
|
Name = string.Empty; // Name을 초기화합니다.
|
|
_isInitializing = true;
|
|
try
|
|
{
|
|
// JObject로부터 속성을 복사
|
|
foreach (var prop in other.Properties())
|
|
{
|
|
// 인덱서를 사용하여 JToken을 객체로 변환하고 추가
|
|
this[prop.Name] = ConvertJTokenToObject(prop.Value);
|
|
}
|
|
}
|
|
finally
|
|
{
|
|
_isInitializing = false;
|
|
}
|
|
return this;
|
|
}
|
|
|
|
/// <summary>
|
|
/// 지정된 사전의 키-값 쌍으로 현재 객체를 채웁니다.
|
|
/// </summary>
|
|
/// <remarks>이 메서드는 제공된 사전을 기반으로 객체의 속성을 업데이트합니다.
|
|
/// 사전의 키가 속성 이름과 일치하면 속성 값이 해당 값으로 설정됩니다.
|
|
/// 사전에 예상되는 속성 유형과 일치하는 유효한 키와 값이 포함되어 있는지 확인합니다.</remarks>
|
|
/// <param name="dictionary">객체의 속성을 초기화하는 키-값 쌍을 포함하는 사전입니다. 키는 속성
|
|
/// 이름을 나타내고 값은 해당 속성 값을 나타냅니다.</param>
|
|
public DataObject FromDictionary(Dictionary<string, object?> dictionary)
|
|
{
|
|
IdKey = null; // IdKey를 초기화합니다.
|
|
Name = string.Empty; // Name을 초기화합니다.
|
|
_isInitializing = true;
|
|
try
|
|
{
|
|
// 생성자에서 초기 속성들을 등록
|
|
foreach (var item in dictionary)
|
|
{
|
|
this[item.Key] = item.Value;
|
|
}
|
|
}
|
|
finally
|
|
{
|
|
_isInitializing = false;
|
|
}
|
|
return this;
|
|
}
|
|
|
|
/// <summary>
|
|
/// JSON 문자열 표현에서 <see cref="DataObject"/> 인스턴스를 생성합니다.
|
|
/// </summary>
|
|
/// <remarks>이 메서드를 사용하여 JSON 문자열을 <see cref="DataObject"/>로 역직렬화합니다.
|
|
/// 입력 문자열이 예상 객체 구조의 유효한 JSON 표현인지 확인합니다.</remarks>
|
|
/// <param name="jsonString"><see cref="DataObject"/>로 파싱할 JSON 문자열입니다. null이거나 비어 있으면 안 됩니다.</param>
|
|
/// <returns>JSON 문자열의 데이터로 채워진 새 <see cref="DataObject"/> 인스턴스를 반환합니다. <paramref name="jsonString"/>이 null이거나 비어 있으면 현재 인스턴스를 반환합니다.</returns>
|
|
public DataObject FromJsonString(string jsonString)
|
|
{
|
|
if (!string.IsNullOrEmpty(jsonString))
|
|
{
|
|
return FromJObject(JObject.Parse(jsonString));
|
|
}
|
|
return this;
|
|
}
|
|
|
|
|
|
/// <summary>
|
|
/// UniTask 기반 비차단 방식으로 JObject로부터 데이터를 채웁니다. WebGL 같은 싱글스레드 환경에서
|
|
/// UI 블로킹을 피하기 위해 일정 수의 키마다 await로 제어권을 양보합니다.
|
|
/// await myDataObject.FromJObjectIncrementalAsync(jObject, 200, cancellationToken);
|
|
/// </summary>
|
|
/// <param name="other">JObject 소스</param>
|
|
/// <param name="batchSize">한 번에 처리할 속성 수</param>
|
|
/// <param name="cancellationToken">취소 토큰</param>
|
|
public async UniTask FromJObjectIncrementalAsync(JObject other, int batchSize =50, CancellationToken cancellationToken = default)
|
|
{
|
|
if (other == null) return;
|
|
IdKey = null;
|
|
Name = string.Empty;
|
|
_isInitializing = true;
|
|
int count =0;
|
|
try
|
|
{
|
|
foreach (var prop in other.Properties())
|
|
{
|
|
cancellationToken.ThrowIfCancellationRequested();
|
|
#if UNITY_WEBGL && !UNITY_EDITOR
|
|
var value = await ConvertJTokenToObjectAsync(prop.Value, batchSize, cancellationToken);
|
|
this[prop.Name] = value;
|
|
#else
|
|
this[prop.Name] = ConvertJTokenToObject(prop.Value);
|
|
#endif
|
|
count++;
|
|
if (count % batchSize ==0)
|
|
{
|
|
await UniTask.Yield(cancellationToken);
|
|
}
|
|
}
|
|
}
|
|
finally
|
|
{
|
|
_isInitializing = false;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// UniTask 기반 비차단 방식으로 JSON 문자열을 스트리밍하여 파싱합니다.
|
|
/// JsonTextReader를 사용해 토큰 단위로 읽고, 일정 수의 항목마다 await로 제어권을 양보합니다.
|
|
/// await myDataObject.FromJsonStringIncrementalAsync(jsonString, 200, cancellationToken);
|
|
/// </summary>
|
|
/// <param name="jsonString">JSON 문자열</param>
|
|
/// <param name="batchSize">한 번에 처리할 속성 수</param>
|
|
/// <param name="cancellationToken">취소 토큰</param>
|
|
public async UniTask FromJsonStringIncrementalAsync(string jsonString, int batchSize =50, CancellationToken cancellationToken = default)
|
|
{
|
|
if (string.IsNullOrEmpty(jsonString)) return;
|
|
|
|
using (var sr = new System.IO.StringReader(jsonString))
|
|
using (var reader = new Newtonsoft.Json.JsonTextReader(sr))
|
|
{
|
|
reader.SupportMultipleContent = false;
|
|
|
|
// 기대: StartObject
|
|
if (!reader.Read() || reader.TokenType != Newtonsoft.Json.JsonToken.StartObject) return;
|
|
|
|
IdKey = null;
|
|
Name = string.Empty;
|
|
_isInitializing = true;
|
|
int count =0;
|
|
|
|
while (reader.Read())
|
|
{
|
|
cancellationToken.ThrowIfCancellationRequested();
|
|
|
|
if (reader.TokenType == Newtonsoft.Json.JsonToken.PropertyName)
|
|
{
|
|
string propName = (string)reader.Value!;
|
|
|
|
// 다음 토큰은 값의 시작이어야 함. JToken.ReadFrom를 사용하여 값 전체를 읽는다.
|
|
if (!reader.Read()) break;
|
|
|
|
JToken token = JToken.ReadFrom(reader);
|
|
#if UNITY_WEBGL && !UNITY_EDITOR
|
|
var value = await ConvertJTokenToObjectAsync(token, batchSize, cancellationToken);
|
|
this[propName] = value;
|
|
#else
|
|
this[propName] = ConvertJTokenToObject(token);
|
|
#endif
|
|
|
|
count++;
|
|
if (count % batchSize ==0)
|
|
{
|
|
await UniTask.Yield(cancellationToken);
|
|
}
|
|
}
|
|
else if (reader.TokenType == Newtonsoft.Json.JsonToken.EndObject)
|
|
{
|
|
break;
|
|
}
|
|
}
|
|
|
|
_isInitializing = false;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// JToken을 적절한 C# 객체 타입으로 변환하는 헬퍼 메서드입니다.
|
|
/// </summary>
|
|
/// <param name="token">변환할 JToken 객체</param>
|
|
/// <returns>변환된 C# 객체</returns>
|
|
private async UniTask<object?> MaterializeJTokenAsync(JToken token, int batchSize =50, CancellationToken cancellationToken = default)
|
|
{
|
|
switch (token.Type)
|
|
{
|
|
case JTokenType.String:
|
|
return token.ToString();
|
|
case JTokenType.Integer:
|
|
return token.ToObject<int>();
|
|
case JTokenType.Float:
|
|
return token.ToObject<float>();
|
|
case JTokenType.Boolean:
|
|
return token.ToObject<bool>();
|
|
case JTokenType.Object:
|
|
// 점진적으로 DataObject로 채우기
|
|
var dataObject = DataObjectPool.Get();
|
|
await dataObject.FromJObjectIncrementalAsync((JObject)token, batchSize, cancellationToken);
|
|
return dataObject;
|
|
case JTokenType.Array:
|
|
JArray array = (JArray)token;
|
|
var dataArray = DataArrayPool.Get();
|
|
await dataArray.FromJArrayIncrementalAsync(array, batchSize, cancellationToken);
|
|
return dataArray;
|
|
default:
|
|
return token.ToString();
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// 기존 ConvertJTokenToObject는 동기 API를 유지하되, 호출자가 비동기 전환을 원하면
|
|
/// MaterializeJTokenAsync를 직접 호출하도록 합니다. 기본 동작은 기존과 동일합니다.
|
|
/// </summary>
|
|
private object? ConvertJTokenToObject(JToken token)
|
|
{
|
|
switch (token.Type)
|
|
{
|
|
case JTokenType.String:
|
|
return token.ToString();
|
|
case JTokenType.Integer:
|
|
return token.ToObject<int>();
|
|
case JTokenType.Float:
|
|
return token.ToObject<float>();
|
|
case JTokenType.Boolean:
|
|
return token.ToObject<bool>();
|
|
case JTokenType.Object:
|
|
var dataObject = DataObjectPool.Get();
|
|
dataObject.FromJObject((JObject)token);
|
|
return dataObject;
|
|
case JTokenType.Array:
|
|
JArray array = (JArray)token;
|
|
return DataArrayPool.Get().FromJArray(array);
|
|
default:
|
|
return token.ToString();
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// 새 비동기 전환용 헬퍼: 동기 ConvertJTokenToObject 대신 사용하여 비동기적으로 토큰을 재질화합니다.
|
|
/// 메서드 이름: ConvertJTokenToObjectAsync
|
|
/// </summary>
|
|
public UniTask<object?> ConvertJTokenToObjectAsync(JToken token, int batchSize =50, CancellationToken cancellationToken = default)
|
|
{
|
|
return MaterializeJTokenAsync(token, batchSize, cancellationToken);
|
|
}
|
|
|
|
/// <summary>
|
|
/// 모든 프로퍼티를 변경된 것으로 표시합니다.
|
|
/// 전체 데이터가 갱신되었을 때 사용합니다.
|
|
/// </summary>
|
|
public void MarkAllAsUpdated()
|
|
{
|
|
changedProperies.Clear();
|
|
changedProperies.UnionWith(this.Keys);
|
|
}
|
|
|
|
/// <summary>
|
|
/// 새 속성을 추가하고 변경 사항을 추적합니다.
|
|
/// </summary>
|
|
/// <param name="propertyName">추가할 속성의 이름</param>
|
|
/// <param name="value">속성의 값</param>
|
|
public new void Add(string propertyName, object value)
|
|
{
|
|
// 인덱서를 사용하여 일관된 변경 추적 로직을 보장합니다.
|
|
this[propertyName] = value;
|
|
}
|
|
|
|
/// <summary>
|
|
/// 인덱서를 통해 속성값에 접근하고 설정합니다.
|
|
/// 속성값이 변경될 때마다 변경 사항을 자동으로 추적합니다.
|
|
/// </summary>
|
|
/// <param name="key">접근할 속성의 키</param>
|
|
/// <returns>속성값</returns>
|
|
public new object? this[string key]
|
|
{
|
|
get => base[key];
|
|
set
|
|
{
|
|
// 키가 새로 추가되거나 기존 값이 변경되었는지 확인합니다.
|
|
bool hasChanged = !TryGetValue(key, out object? oldValue) || !Equals(oldValue, value);
|
|
|
|
if (hasChanged)
|
|
{
|
|
// 기존에 풀링 가능한 객체가 있었다면 풀에 반환합니다.
|
|
if (oldValue is DataObject oldDataObject)
|
|
{
|
|
if (oldDataObject.IsInPool) oldDataObject.ReturnToPool();
|
|
}
|
|
else if (oldValue is DataArray oldDataArray)
|
|
{
|
|
if (oldDataArray.IsInPool) oldDataArray.ReturnToPool();
|
|
}
|
|
|
|
// 기본 딕셔너리에 값을 설정합니다.
|
|
base[key] = value;
|
|
|
|
// 객체 초기화 중이 아닐 때만 변경 사항을 추적합니다.
|
|
if (!_isInitializing)
|
|
{
|
|
changedProperies.Add(key);
|
|
}
|
|
}
|
|
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// 지정된 속성의 값을 정수(int)로 변환하여 반환합니다.
|
|
/// </summary>
|
|
/// <param name="propertyName">속성 이름</param>
|
|
/// <param name="defaultValue">속성이 없거나 변환할 수 없는 경우 반환할 기본값</param>
|
|
/// <returns>변환된 정수 값 또는 기본값</returns>
|
|
public int? GetInt(string propertyName, int? defaultValue = null)
|
|
{
|
|
if (TryGetValue(propertyName, out object? value) && value != null)
|
|
{
|
|
if (value is int intValue)
|
|
return intValue;
|
|
return Convert.ToInt32(value);
|
|
}
|
|
return defaultValue;
|
|
}
|
|
|
|
/// <summary>
|
|
/// 지정된 속성의 값을 문자열(string)로 변환하여 반환합니다.
|
|
/// </summary>
|
|
/// <param name="propertyName">속성 이름</param>
|
|
/// <param name="defaultValue">속성이 없거나 null인 경우 반환할 기본값</param>
|
|
/// <returns>변환된 문자열 값 또는 기본값</returns>
|
|
public string? GetString(string propertyName, string? defaultValue = null)
|
|
{
|
|
if (TryGetValue(propertyName, out object? value) && value != null)
|
|
{
|
|
return value.ToString();
|
|
}
|
|
return defaultValue;
|
|
}
|
|
|
|
/// <summary>
|
|
/// 지정된 속성의 값을 불리언(bool)으로 변환하여 반환합니다.
|
|
/// </summary>
|
|
/// <param name="propertyName">속성 이름</param>
|
|
/// <param name="defaultValue">속성이 없거나 변환할 수 없는 경우 반환할 기본값</param>
|
|
/// <returns>변환된 불리언 값 또는 기본값</returns>
|
|
public bool? GetBool(string propertyName, bool? defaultValue = false)
|
|
{
|
|
if (TryGetValue(propertyName, out object? value) && value != null)
|
|
{
|
|
if (value is bool boolValue)
|
|
return boolValue;
|
|
return Convert.ToBoolean(value);
|
|
}
|
|
return defaultValue;
|
|
}
|
|
|
|
/// <summary>
|
|
/// 지정된 속성의 값을 부동 소수점(float)으로 변환하여 반환합니다.
|
|
/// </summary>
|
|
/// <param name="propertyName">속성 이름</param>
|
|
/// <param name="defaultValue">속성이 없거나 변환할 수 없는 경우 반환할 기본값</param>
|
|
/// <returns>변환된 부동 소수점 값 또는 기본값</returns>
|
|
public float? GetFloat(string propertyName, float? defaultValue = null)
|
|
{
|
|
if (TryGetValue(propertyName, out object? value) && value != null)
|
|
{
|
|
if (value is float floatValue)
|
|
return floatValue;
|
|
return Convert.ToSingle(value);
|
|
}
|
|
return defaultValue;
|
|
}
|
|
|
|
/// <summary>
|
|
/// 지정된 속성의 값을 더블 정밀도 부동 소수점(double)으로 변환하여 반환합니다.
|
|
/// </summary>
|
|
/// <param name="propertyName">속성 이름</param>
|
|
/// <param name="defaultValue">속성이 없거나 변환할 수 없는 경우 반환할 기본값</param>
|
|
/// <returns>변환된 더블 값 또는 기본값</returns>
|
|
public double? GetDouble(string propertyName, double? defaultValue = null)
|
|
{
|
|
if (TryGetValue(propertyName, out object? value) && value != null)
|
|
{
|
|
if (value is double doubleValue)
|
|
return doubleValue;
|
|
return Convert.ToDouble(value);
|
|
}
|
|
return defaultValue;
|
|
}
|
|
|
|
/// <summary>
|
|
/// 지정된 속성의 값을 long으로 변환하여 반환합니다.
|
|
/// </summary>
|
|
/// <param name="propertyName">속성 이름</param>
|
|
/// <param name="defaultValue">속성이 없거나 변환할 수 없는 경우 반환할 기본값</param>
|
|
/// <returns>변환된 long 값 또는 기본값</returns>
|
|
public long? GetLong(string propertyName, long? defaultValue = null)
|
|
{
|
|
if (TryGetValue(propertyName, out object? value) && value != null)
|
|
{
|
|
if (value is long longValue)
|
|
return longValue;
|
|
return Convert.ToInt64(value);
|
|
}
|
|
return defaultValue;
|
|
}
|
|
|
|
/// <summary>
|
|
/// 지정된 속성의 값을 DateTime으로 변환하여 반환합니다.
|
|
/// </summary>
|
|
/// <param name="propertyName">속성 이름</param>
|
|
/// <param name="defaultValue">속성이 없거나 변환할 수 없는 경우 반환할 기본값</param>
|
|
/// <returns>변환된 DateTime 값 또는 기본값</returns>
|
|
public DateTime? GetDateTime(string propertyName, DateTime? defaultValue = null)
|
|
{
|
|
if (TryGetValue(propertyName, out object? value) && value != null)
|
|
{
|
|
if (value is DateTime dateTime)
|
|
return dateTime;
|
|
return Convert.ToDateTime(value);
|
|
}
|
|
return defaultValue;
|
|
}
|
|
|
|
/// <summary>
|
|
/// 지정된 속성의 값을 열거형(Enum)으로 변환하여 반환합니다.
|
|
/// </summary>
|
|
/// <typeparam name="T">변환할 열거형 타입</typeparam>
|
|
/// <param name="propertyName">속성 이름</param>
|
|
/// <param name="defaultValue">속성이 없거나 변환할 수 없는 경우 반환할 기본값</param>
|
|
/// <returns>변환된 열거형 값 또는 기본값</returns>
|
|
public T? GetEnum<T>(string propertyName, T? defaultValue = default) where T : Enum
|
|
{
|
|
if (TryGetValue(propertyName, out object? value) && value != null)
|
|
{
|
|
if (value is T enumValue)
|
|
return enumValue;
|
|
return (T)Enum.Parse(typeof(T), value.ToString());
|
|
}
|
|
return defaultValue;
|
|
}
|
|
|
|
/// <summary>
|
|
/// 지정된 속성의 값을 IDataObject로 반환합니다.
|
|
/// </summary>
|
|
/// <param name="propertyName">속성 이름</param>
|
|
/// <param name="defaultValue">속성이 없거나 IDataObject가 아닌 경우 반환할 기본값</param>
|
|
/// <returns>IDataObject 인터페이스를 구현한 객체 또는 기본값</returns>
|
|
public IDataObject? Get(string propertyName, IDataObject? defaultValue = null)
|
|
{
|
|
if (TryGetValue(propertyName, out object? value) && value != null)
|
|
{
|
|
if (value is IDataObject dataObject) return dataObject;
|
|
}
|
|
return defaultValue;
|
|
}
|
|
|
|
/// <summary>
|
|
/// 지정된 속성의 값을 DataArray로 변환하여 반환합니다.
|
|
/// </summary>
|
|
/// <param name="propertyName">속성 이름</param>
|
|
/// <param name="defaultValue">속성이 없거나 변환할 수 없는 경우 반환할 기본값</param>
|
|
/// <returns>DataArray 객체 또는 기본값</returns>
|
|
public DataArray? GetDataArray(string propertyName, DataArray? defaultValue = null)
|
|
{
|
|
if (TryGetValue(propertyName, out object? value) && value != null)
|
|
{
|
|
if (value is DataArray dataArray)
|
|
return dataArray;
|
|
if (value is JArray jArray)
|
|
return DataArrayPool.Get().FromJArray(jArray);
|
|
}
|
|
return defaultValue;
|
|
}
|
|
|
|
/// <summary>
|
|
/// 지정된 속성의 값을 DataObject로 변환하여 반환합니다.
|
|
/// <para>이 메서드는 먼저 현재 객체에서 속성을 찾습니다. 찾지 못하면 중첩된 DataObject를 재귀적으로 탐색합니다.</para>
|
|
/// <para>과도한 재귀로 인한 문제를 방지하기 위해 최대 탐색 깊이가 제한됩니다.</para>
|
|
/// </summary>
|
|
/// <param name="propertyName">속성 이름</param>
|
|
/// <param name="defaultValue">속성이 없거나 변환할 수 없는 경우 반환할 기본값</param>
|
|
/// <param name="maxDepth">재귀 탐색의 최대 깊이</param>
|
|
/// <returns>DataObject 객체 또는 기본값</returns>
|
|
public DataObject? GetDataObject(string propertyName, DataObject? defaultValue = null, int maxDepth = 8)
|
|
{
|
|
// 내부 재귀 탐색을 시작하고, 결과를 찾지 못하면 기본값을 반환합니다.
|
|
return GetDataObjectInternal(propertyName, 0, maxDepth) ?? defaultValue;
|
|
}
|
|
|
|
/// <summary>
|
|
/// 재귀 깊이 제한을 두고 내부적으로 DataObject를 탐색하는 private 헬퍼 메서드입니다.
|
|
/// </summary>
|
|
/// <param name="propertyName">찾을 속성 이름</param>
|
|
/// <param name="currentDepth">현재 재귀 깊이</param>
|
|
/// <param name="maxDepth">최대 재귀 깊이</param>
|
|
private DataObject? GetDataObjectInternal(string propertyName, int currentDepth, int maxDepth)
|
|
{
|
|
// StackOverflow 방지를 위한 최대 재귀 깊이
|
|
if (currentDepth >= maxDepth)
|
|
{
|
|
return null; // 깊이 제한 초과 시 탐색 중단
|
|
}
|
|
|
|
//1. 현재 객체에서 직접 속성 검색
|
|
if (TryGetValue(propertyName, out object? value) && value != null)
|
|
{
|
|
if (value is DataObject dataObject)
|
|
return dataObject;
|
|
if (value is JObject jObject)
|
|
{
|
|
var dataObj = DataObjectPool.Get();
|
|
dataObj.FromJObject(jObject);
|
|
return dataObj;
|
|
}
|
|
if (value is Dictionary<string, object?> dict)
|
|
{
|
|
var dataObj = DataObjectPool.Get();
|
|
dataObj.FromDictionary(dict);
|
|
return dataObj;
|
|
}
|
|
}
|
|
|
|
//2. 중첩된 DataObject에서 재귀적으로 검색
|
|
foreach (KeyValuePair<string, object?> keyValue in this)
|
|
{
|
|
if (keyValue.Value is DataObject nestedDataObject)
|
|
{
|
|
// 재귀 호출 시 깊이를1 증가
|
|
DataObject? foundInNested = nestedDataObject.GetDataObjectInternal(propertyName, currentDepth + 1, maxDepth);
|
|
if (foundInNested != null)
|
|
{
|
|
return foundInNested;
|
|
}
|
|
}
|
|
}
|
|
|
|
return null; // 이 경로에서 객체를 찾지 못함
|
|
}
|
|
|
|
/// <summary>
|
|
/// 지정된 경로를 사용하여 중첩된 DataObject를 찾습니다.
|
|
/// 경로는 점(.)으로 구분된 속성 이름과 배열 인덱스로 구성됩니다.
|
|
/// </summary>
|
|
/// <example>
|
|
/// <code>
|
|
/// var userProfile = data.FindDataObjectByPath("users.0.profile");
|
|
/// </code>
|
|
/// </example>
|
|
/// <param name="path">"data.users.0.profile"과 같은 객체 경로</param>
|
|
/// <param name="defaultValue">경로를 찾을 수 없는 경우 반환할 기본값</param>
|
|
/// <returns>찾은 DataObject 또는 기본값</returns>
|
|
public DataObject? FindDataObjectByPath(string path, DataObject? defaultValue = null)
|
|
{
|
|
var segments = path.Split('.');
|
|
object? current = this;
|
|
|
|
foreach (var segment in segments)
|
|
{
|
|
if (current == null) return defaultValue;
|
|
|
|
switch (current)
|
|
{
|
|
case DataObject currentObject:
|
|
if (!currentObject.TryGetValue(segment, out current))
|
|
{
|
|
return defaultValue;
|
|
}
|
|
break;
|
|
case DataArray currentArray:
|
|
if (int.TryParse(segment, out int index) && index >= 0 && index < currentArray.Count)
|
|
{
|
|
current = currentArray[index];
|
|
}
|
|
else
|
|
{
|
|
return defaultValue;
|
|
}
|
|
break;
|
|
default:
|
|
// Path leads to a primitive value, cannot traverse further.
|
|
return defaultValue;
|
|
}
|
|
}
|
|
|
|
return current as DataObject ?? defaultValue;
|
|
}
|
|
|
|
|
|
/// <summary>
|
|
/// 속성이 제거될 때 기존 속성 목록에서도 제거합니다.
|
|
/// </summary>
|
|
/// <param name="key">제거할 속성의 키</param>
|
|
/// <returns>제거 성공 여부</returns>
|
|
public new bool Remove(string key)
|
|
{
|
|
// 제거하기 전에 이전 값을 가져옵니다.
|
|
TryGetValue(key, out object? oldValue);
|
|
|
|
bool result = base.Remove(key);
|
|
if (result)
|
|
{
|
|
// 제거된 객체가 풀링 가능한 경우 풀에 반환합니다.
|
|
if (oldValue is DataObject dataObject)
|
|
{
|
|
if (dataObject.IsInPool) dataObject.ReturnToPool();
|
|
}
|
|
else if (oldValue is DataArray dataArray)
|
|
{
|
|
if (dataArray.IsInPool) dataArray.ReturnToPool();
|
|
}
|
|
|
|
// 변경 추적 목록에서도 제거합니다.
|
|
changedProperies.Remove(key);
|
|
}
|
|
return result;
|
|
}
|
|
|
|
/// <summary>
|
|
/// 모든 속성을 제거하고 추적 리스트를 초기화합니다.
|
|
/// </summary>
|
|
public void RemoveAll()
|
|
{
|
|
foreach (var value in Values.ToList())
|
|
{
|
|
if (value is DataObject dataObject)
|
|
{
|
|
dataObject.ReturnToPool(); // DataObject를 풀에 반환합니다.
|
|
}
|
|
else if (value is DataArray dataArray)
|
|
{
|
|
dataArray.ReturnToPool(); // DataArray를 풀에 반환합니다.
|
|
}
|
|
}
|
|
base.Clear();
|
|
changedProperies.Clear();
|
|
}
|
|
|
|
/// <summary>
|
|
/// 객체를 풀에 반환하기 전에 초기 상태로 리셋합니다.
|
|
/// 모든 속성을 제거하고, 이름과 ID 키를 기본값으로 설정합니다.
|
|
/// </summary>
|
|
public void Reset()
|
|
{
|
|
RemoveAll();
|
|
Name = string.Empty;
|
|
IdKey = null;
|
|
isUpdateImmediately = false;
|
|
}
|
|
|
|
/// <summary>
|
|
/// 동일한 상태와 값을 가진 현재 데이터 객체의 새 인스턴스를 생성합니다.
|
|
/// </summary>
|
|
/// <remarks>복제된 객체는 원본 객체와 독립적이므로, 한 객체를 변경해도 다른 객체에는 영향을 미치지 않습니다.
|
|
///</remarks>
|
|
/// <param name="fromPool">복제된 객체가 풀에 있는지 여부를 나타냅니다. 기본값은 true입니다.</param>
|
|
/// <returns>현재 객체의 복사본인 새 <see cref="IDataObject"/> 인스턴스를 반환합니다.</returns>
|
|
public IDataObject Clone(bool fromPool = true)
|
|
{
|
|
return Copy(fromPool);
|
|
}
|
|
|
|
/// <summary>
|
|
/// 모든 키-값 쌍을 포함하여 현재 <see cref="DataObject"/> 인스턴스의 깊은 복사본을 생성합니다.
|
|
/// </summary>
|
|
/// <remarks>이 메서드는 <see cref="DataObject"/>와 그 내용의 깊은 복사본을 수행합니다.
|
|
/// 키-값 쌍의 값이 다른 <see cref="DataObject"/> 또는 <see cref="DataArray"/>인 경우,
|
|
/// 메서드는 해당 객체를 재귀적으로 복제합니다. 기본 유형 및 기타 복제 불가능한 값은
|
|
/// 직접 복사됩니다.</remarks>
|
|
/// <param name="fromPool">복제된 객체가 풀에 있는지 여부를 나타냅니다. 기본값은 true입니다.</param>
|
|
/// <returns>현재 객체의 깊은 복사본인 새로운 <see cref="DataObject"/> 인스턴스를 반환합니다.</returns>
|
|
public DataObject Copy(bool fromPool = true)
|
|
{
|
|
DataObject clone;
|
|
if (fromPool) clone = DataObjectPool.Get();
|
|
else clone = new DataObject();
|
|
clone.Name = Name;
|
|
clone.IdKey = IdKey;
|
|
|
|
// 모든 키-값 쌍을 순회하며 깊은 복사를 수행합니다.
|
|
foreach (var pair in this)
|
|
{
|
|
object? clonedValue;
|
|
switch (pair.Value)
|
|
{
|
|
// 값이 DataObject인 경우, 재귀적으로 Copy을 호출합니다.
|
|
case DataObject dataObjectValue:
|
|
clonedValue = dataObjectValue.Copy(fromPool);
|
|
break;
|
|
// 값이 DataArray인 경우, 재귀적으로 Clone을 호출합니다.
|
|
case DataArray dataArrayValue:
|
|
clonedValue = dataArrayValue.Copy(fromPool);
|
|
break;
|
|
// 그 외의 경우 (primitive 타입 등)는 그대로 복사합니다.
|
|
default:
|
|
clonedValue = pair.Value;
|
|
break;
|
|
}
|
|
// 복제된 값을 새 DataObject에 추가합니다.
|
|
clone.Add(pair.Key, clonedValue);
|
|
}
|
|
|
|
clone.changedProperies = new HashSet<string>(changedProperies);
|
|
return clone;
|
|
}
|
|
|
|
/// <summary>
|
|
/// 현재 DataObject를 JObject로 변환합니다.
|
|
/// </summary>
|
|
/// <returns>현재 객체의 데이터를 담은 JObject</returns>
|
|
public JObject ToJObject()
|
|
{
|
|
JObject result = new JObject();
|
|
foreach (var kvp in this)
|
|
{
|
|
result[kvp.Key] = JToken.FromObject(kvp.Value);
|
|
}
|
|
return result;
|
|
}
|
|
|
|
/// <summary>
|
|
/// 변경된 것으로 표시된 속성 목록을 지웁니다.
|
|
/// </summary>
|
|
/// <remarks>이 메서드는 변경된 속성의 내부 추적을 재설정하여
|
|
/// 기록된 모든 수정 사항을 효과적으로 지웁니다. 모든 변경 내용 추적을 삭제하고
|
|
/// 새로 시작하려면 이 메서드를 사용하세요.</remarks>
|
|
public void ClearChangedProperties()
|
|
{
|
|
// 변경 추적 목록을 초기화합니다.
|
|
changedProperies.Clear();
|
|
}
|
|
|
|
/// <summary>
|
|
/// 다른 DataObject와 현재 객체를 비교하여 다른 부분만 설정합니다.
|
|
/// 변경된 키는 자동으로 추적됩니다.
|
|
/// </summary>
|
|
/// <param name="other">비교할 DataObject</param>
|
|
/// <param name="updatedDataOnly">true인 경우, 변경된 속성만 업데이트합니다. false인 경우, 모든 속성을 동기화합니다.</param>
|
|
public void UpdateDifferent(IDataObject other, bool updatedDataOnly)
|
|
{
|
|
if (other == null || other is not DataObject otherDataObject) return;
|
|
|
|
// 변경 추적 목록을 초기화하여 'other' 객체의 상태를 기준으로 새로 설정합니다.
|
|
changedProperies.Clear();
|
|
|
|
foreach (var keyValue in otherDataObject)
|
|
{
|
|
if (!this.ContainsKey(keyValue.Key) || (this[keyValue.Key] == null && keyValue.Value != null)
|
|
|| (this[keyValue.Key] != null && keyValue.Value == null)
|
|
|| (this[keyValue.Key] != null && keyValue.Value != null && !this[keyValue.Key]!.Equals(keyValue.Value)))
|
|
{
|
|
//참조 타입과 값 타입 구분하여 복사
|
|
object? valueToSet;
|
|
switch (keyValue.Value)
|
|
{
|
|
// DataObject나 DataArray는 풀을 사용하지 않는 깊은 복사를 수행합니다.
|
|
case DataObject dataObjectValue:
|
|
valueToSet = dataObjectValue.Copy(fromPool: false);
|
|
break;
|
|
case DataArray dataArrayValue:
|
|
valueToSet = dataArrayValue.Copy(fromPool: false);
|
|
break;
|
|
// 그 외 타입은 값을 그대로 할당합니다.
|
|
default:
|
|
valueToSet = keyValue.Value;
|
|
break;
|
|
}
|
|
this[keyValue.Key] = valueToSet;
|
|
changedProperies.Add(keyValue.Key);
|
|
}
|
|
}
|
|
|
|
if (updatedDataOnly) return;
|
|
|
|
// 현재 객체에만 있는 속성은 제거합니다.
|
|
var keysToRemove = this.Keys.Except(otherDataObject.Keys).ToList();
|
|
foreach (var key in keysToRemove)
|
|
{
|
|
this.Remove(key);
|
|
changedProperies.Remove(key);
|
|
}
|
|
|
|
}
|
|
|
|
/// <summary>
|
|
/// 현재 객체의 상태를 기반으로 업데이트된 <see cref="IDataObject"/> 인스턴스를 생성하고 반환합니다.
|
|
/// </summary>
|
|
/// <remarks>이 메서드는 <see cref="DataObject"/> 및 <see
|
|
/// cref="DataArray"/> 유형의 속성은 깊이 복사되고, 다른 유형의 속성은 직접 복사되도록 합니다. <c>IdKey</c>가 없으면
|
|
/// 현재 객체의 첫 번째 키-값 쌍이 업데이트된 객체에 포함됩니다.
|
|
///</remarks>
|
|
/// <param name="fromPool">업데이트된 객체가 풀에서 검색해야 하는지 여부를 나타내는 값입니다. <see langword="true"/>인 경우
|
|
/// <see cref="DataObjectPool"/>에서 객체를 가져옵니다. 그렇지 않으면 새 <see cref="DataObject"/>
|
|
/// 인스턴스가 생성됩니다.</param>
|
|
/// <returns>현재 객체에서 복사된 속성과 값을 포함하는 업데이트된 <see cref="IDataObject"/> 인스턴스입니다.
|
|
/// 반환된 객체에는 변경된 모든 속성, <c>IdKey</c> 및 <c>Name</c>이 포함됩니다.
|
|
/// <c>IdKey</c>가 현재 객체에 있는 경우 해당 값도 업데이트된 객체에 포함됩니다.</returns>
|
|
public IDataObject GetUpdatedObject(bool fromPool = true)
|
|
{
|
|
DataObject updated = fromPool ? DataObjectPool.Get() : new DataObject();
|
|
foreach (var key in changedProperies)
|
|
{
|
|
if (this.ContainsKey(key))
|
|
{
|
|
if (this[key] is DataObject dataObject)
|
|
{
|
|
updated[key] = dataObject.Copy(fromPool); // DataObject는 복사합니다.
|
|
}
|
|
else if (this[key] is DataArray dataArray)
|
|
{
|
|
updated[key] = dataArray.Copy(fromPool); // DataArray는 복사합니다.
|
|
}
|
|
else
|
|
{
|
|
updated[key] = this[key]; // 그 외의 값은 그대로 복사합니다.
|
|
}
|
|
}
|
|
}
|
|
updated.IdKey = IdKey; // ID 키를 복사합니다.
|
|
updated.Name = Name; // 이름을 복사합니다.
|
|
|
|
//id에 해당하는 속성이 있다면, 업데이트된 객체에도 포함시킵니다.
|
|
if (IdKey != null && ContainsKey(IdKey))
|
|
{
|
|
updated[IdKey] = this[IdKey];
|
|
}
|
|
else if (Count > 0)
|
|
{
|
|
var keyValue = this.First();
|
|
updated[keyValue.Key] = keyValue.Value;
|
|
}
|
|
|
|
return updated;
|
|
}
|
|
|
|
public void ReturnToPool()
|
|
{
|
|
if (CreatedFromPool)
|
|
{
|
|
Reset();
|
|
}
|
|
else
|
|
{
|
|
DataObjectPool.Return(this);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// 업데이트 된 속성의 수.
|
|
/// </summary>
|
|
/// <returns>업데이트된 속성의 총 개수입니다. 업데이트된 속성이 없으면0을 반환합니다.</returns>
|
|
public int UpdatedCount { get => changedProperies.Count; }
|
|
|
|
private bool isUpdateImmediately = false;
|
|
/// <summary>
|
|
/// 업데이트가 즉시 적용되어야 하는지 여부를 나타냅니다.
|
|
/// </summary>
|
|
/// <returns></returns>
|
|
public bool IsUpdateImmediately
|
|
{
|
|
get => isUpdateImmediately;
|
|
set
|
|
{
|
|
if (isUpdateImmediately == value) return;
|
|
isUpdateImmediately = value;
|
|
|
|
foreach (var item in this)
|
|
{
|
|
if (item.Value is DataObject dataObject)
|
|
{
|
|
dataObject.IsUpdateImmediately = value;
|
|
}
|
|
else if (item.Value is DataArray dataArray)
|
|
{
|
|
dataArray.IsUpdateImmediately = value;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// DataObject의 내용을 문자열로 반환합니다.
|
|
/// 각 키-값 쌍이 "키:값" 형식으로 쉼표로 구분되어 표시됩니다.
|
|
/// </summary>
|
|
/// <returns>DataObject의 내용을 나타내는 문자열</returns>
|
|
public override string ToString()
|
|
{
|
|
return string.Join(", ", this.Select(kvp => $"{kvp.Key}:{kvp.Value}"));
|
|
}
|
|
|
|
/// <summary>
|
|
/// 현재 객체를 JSON 문자열 표현으로 변환합니다.
|
|
/// </summary>
|
|
/// <returns>현재 객체를 나타내는 JSON 문자열입니다.</returns>
|
|
public string ToJson()
|
|
{
|
|
return ToJObject().ToString(Newtonsoft.Json.Formatting.None);
|
|
}
|
|
|
|
/// <summary>
|
|
/// 비동기 팩토리: JObject로부터 완전히 점진 재질화된 DataObject를 생성합니다.
|
|
/// </summary>
|
|
public static async UniTask<DataObject> CreateFromJObjectAsync(JObject other, int batchSize =50, CancellationToken cancellationToken = default)
|
|
{
|
|
var obj = new DataObject();
|
|
await obj.FromJObjectIncrementalAsync(other, batchSize, cancellationToken);
|
|
return obj;
|
|
}
|
|
|
|
/// <summary>
|
|
/// 비동기 팩토리: JSON 문자열로부터 완전히 점진 재질화된 DataObject를 생성합니다.
|
|
/// </summary>
|
|
public static async UniTask<DataObject> CreateFromJsonStringAsync(string jsonString, int batchSize =50, CancellationToken cancellationToken = default)
|
|
{
|
|
if (string.IsNullOrEmpty(jsonString)) return new DataObject();
|
|
#if UNITY_WEBGL && !UNITY_EDITOR
|
|
var obj = new DataObject();
|
|
await obj.FromJsonStringIncrementalAsync(jsonString, batchSize, cancellationToken);
|
|
return obj;
|
|
// WebGL: 스레드가 없으므로 incremental 파서를 사용해 메인스레드에서 점진 처리
|
|
#else
|
|
// non-WebGL: 스레드풀에서 전체 파싱 후 비동기 재질화
|
|
var jObj = await UniTask.RunOnThreadPool(() => JObject.Parse(jsonString));
|
|
return await CreateFromJObjectAsync(jObj, batchSize, cancellationToken);
|
|
#endif
|
|
}
|
|
|
|
/// <summary>
|
|
/// 비동기 팩토리: 스트림으로부터 완전히 점진 재질화된 DataObject를 생성합니다.
|
|
/// </summary>
|
|
public static async UniTask<DataObject> CreateFromStreamAsync(System.IO.Stream jsonStream, int batchSize =50, CancellationToken cancellationToken = default)
|
|
{
|
|
if (jsonStream == null) throw new ArgumentNullException(nameof(jsonStream));
|
|
using (var sr = new System.IO.StreamReader(jsonStream))
|
|
using (var reader = new Newtonsoft.Json.JsonTextReader(sr))
|
|
{
|
|
reader.SupportMultipleContent = true;
|
|
var serializer = new Newtonsoft.Json.JsonSerializer();
|
|
var jObj = serializer.Deserialize<JObject>(reader);
|
|
if (jObj == null) return new DataObject();
|
|
return await CreateFromJObjectAsync(jObj, batchSize, cancellationToken);
|
|
}
|
|
}
|
|
}
|
|
} |