using System; using System.Collections; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using LeanCloud.Storage.Internal.Object; using LeanCloud.Storage.Internal.Operation; using LeanCloud.Storage.Internal.Codec; namespace LeanCloud.Storage { /// /// 对象类 /// public class LCObject { /// /// 最近一次与服务端同步的数据 /// LCObjectData data; /// /// 预算数据 /// internal Dictionary estimatedData; /// /// 操作字典 /// internal Dictionary operationDict; static readonly Dictionary subclassTypeDict = new Dictionary(); static readonly Dictionary subclassNameDict = new Dictionary(); public string ClassName { get { return data.ClassName; } } public string ObjectId { get { return data.ObjectId; } } public DateTime CreatedAt { get { return data.CreatedAt; } } public DateTime UpdatedAt { get { return data.UpdatedAt; } } public LCACL ACL { get { return this["ACL"] as LCACL ; } set { this["ACL"] = value; } } bool isNew; bool IsDirty { get { return isNew || estimatedData.Count > 0; } } public LCObject(string className) { if (string.IsNullOrEmpty(className)) { throw new ArgumentNullException(nameof(className)); } data = new LCObjectData(); estimatedData = new Dictionary(); operationDict = new Dictionary(); data.ClassName = className; isNew = true; } public static LCObject CreateWithoutData(string className, string objectId) { if (string.IsNullOrEmpty(objectId)) { throw new ArgumentNullException(nameof(objectId)); } LCObject obj = new LCObject(className); obj.data.ObjectId = objectId; obj.isNew = false; return obj; } internal static LCObject Create(string className) { if (subclassNameDict.TryGetValue(className, out LCSubclassInfo subclassInfo)) { return subclassInfo.Constructor.Invoke(); } return new LCObject(className); } internal static LCObject Create(Type type) { if (subclassTypeDict.TryGetValue(type, out LCSubclassInfo subclassInfo)) { return subclassInfo.Constructor.Invoke(); } return null; } public object this[string key] { get { if (estimatedData.TryGetValue(key, out object value)) { if (value is LCRelation relation) { relation.Key = key; relation.Parent = this; } return value; } return null; } set { if (string.IsNullOrEmpty(key)) { throw new ArgumentNullException(nameof(key)); } if (key.StartsWith("_")) { throw new ArgumentException("key should not start with '_'"); } if (key == "objectId" || key == "createdAt" || key == "updatedAt") { throw new ArgumentException($"{key} is reserved by LeanCloud"); } LCSetOperation setOp = new LCSetOperation(value); ApplyOperation(key, setOp); } } /// /// 删除字段 /// /// public void Unset(string key) { if (string.IsNullOrEmpty(key)) { throw new ArgumentNullException(nameof(key)); } LCDeleteOperation deleteOp = new LCDeleteOperation(); ApplyOperation(key, deleteOp); } /// /// 增加关联 /// /// /// public void AddRelation(string key, LCObject value) { if (string.IsNullOrEmpty(key)) { throw new ArgumentNullException(nameof(key)); } if (value == null) { throw new ArgumentNullException(nameof(value)); } LCAddRelationOperation op = new LCAddRelationOperation(new List { value }); ApplyOperation(key, op); } /// /// 删除关联 /// /// /// public void RemoveRelation(string key, LCObject value) { if (string.IsNullOrEmpty(key)) { throw new ArgumentNullException(nameof(key)); } if (value == null) { throw new ArgumentNullException(nameof(value)); } LCRemoveRelationOperation op = new LCRemoveRelationOperation(value); ApplyOperation(key, op); } /// /// 增加数字属性值 /// /// /// public void Increment(string key, object value) { if (string.IsNullOrEmpty(key)) { throw new ArgumentNullException(nameof(key)); } if (value == null) { throw new ArgumentNullException(nameof(value)); } LCNumberOperation op = new LCNumberOperation(value); ApplyOperation(key, op); } /// /// 在数组属性中增加一个元素 /// /// /// public void Add(string key, object value) { if (string.IsNullOrEmpty(key)) { throw new ArgumentNullException(nameof(key)); } if (value == null) { throw new ArgumentNullException(nameof(value)); } LCAddOperation op = new LCAddOperation(new List { value }); ApplyOperation(key, op); } /// /// 在数组属性中增加一组元素 /// /// /// public void AddAll(string key, IEnumerable values) { if (string.IsNullOrEmpty(key)) { throw new ArgumentNullException(nameof(key)); } if (values == null) { throw new ArgumentNullException(nameof(values)); } LCAddOperation op = new LCAddOperation(new List(values.Cast())); ApplyOperation(key, op); } /// /// 在数组属性中增加一个唯一元素 /// /// /// public void AddUnique(string key, object value) { if (string.IsNullOrEmpty(key)) { throw new ArgumentNullException(nameof(key)); } if (value == null) { throw new ArgumentNullException(nameof(value)); } LCAddUniqueOperation op = new LCAddUniqueOperation(new List { value }); ApplyOperation(key, op); } /// /// 在数组属性中增加一组唯一元素 /// /// /// public void AddAllUnique(string key, IEnumerable values) { if (string.IsNullOrEmpty(key)) { throw new ArgumentNullException(nameof(key)); } if (values == null) { throw new ArgumentNullException(nameof(values)); } LCAddUniqueOperation op = new LCAddUniqueOperation(new List(values.Cast())); ApplyOperation(key, op); } /// /// 移除某个元素 /// /// /// public void Remove(string key, object value) { if (string.IsNullOrEmpty(key)) { throw new ArgumentNullException(nameof(key)); } if (value == null) { throw new ArgumentNullException(nameof(value)); } LCRemoveOperation op = new LCRemoveOperation(new List { value }); ApplyOperation(key, op); } /// /// 移除一组元素 /// /// /// public void RemoveAll(string key, IEnumerable values) { if (string.IsNullOrEmpty(key)) { throw new ArgumentNullException(nameof(key)); } if (values == null) { throw new ArgumentNullException(nameof(values)); } LCRemoveOperation op = new LCRemoveOperation(new List(values.Cast())); ApplyOperation(key, op); } static async Task SaveBatches(Stack batches) { while (batches.Count > 0) { LCBatch batch = batches.Pop(); List dirtyObjects = batch.objects.Where(item => item.IsDirty) .ToList(); List> requestList = dirtyObjects.Select(item => { string path = item.ObjectId == null ? $"/1.1/classes/{item.ClassName}" : $"/1.1/classes/{item.ClassName}/{item.ClassName}"; string method = item.ObjectId == null ? "POST" : "PUT"; Dictionary body = LCEncoder.Encode(item.operationDict) as Dictionary; return new Dictionary { { "path", path }, { "method", method }, { "body", body } }; }).ToList(); Dictionary data = new Dictionary { { "requests", LCEncoder.Encode(requestList) } }; List> results = await LCApplication.HttpClient.Post>>("batch", data: data); List resultList = results.Select(item => { if (item.TryGetValue("error", out object error)) { Dictionary err = error as Dictionary; int code = (int)err["code"]; string message = (string)err["error"]; throw new LCException(code, message as string); } return LCObjectData.Decode(item["success"] as IDictionary); }).ToList(); for (int i = 0; i < dirtyObjects.Count; i++) { LCObject obj = dirtyObjects[i]; LCObjectData objData = resultList[i]; obj.Merge(objData); } } } public async Task Save(bool fetchWhenSave = false, LCQuery query = null) { if (LCBatch.HasCircleReference(this, new HashSet())) { throw new ArgumentException("Found a circle dependency when save."); } Stack batches = LCBatch.BatchObjects(new List { this }, false); if (batches.Count > 0) { await SaveBatches(batches); } string path = ObjectId == null ? $"classes/{ClassName}" : $"classes/{ClassName}/{ObjectId}"; Dictionary queryParams = new Dictionary(); if (fetchWhenSave) { queryParams["fetchWhenSave"] = true; } if (query != null) { queryParams["where"] = query.BuildWhere(); } Dictionary response = ObjectId == null ? await LCApplication.HttpClient.Post>(path, data: LCEncoder.Encode(operationDict) as Dictionary, queryParams: queryParams) : await LCApplication.HttpClient.Put>(path, data: LCEncoder.Encode(operationDict) as Dictionary, queryParams: queryParams); LCObjectData data = LCObjectData.Decode(response); Merge(data); return this; } public static async Task> SaveAll(List objectList) { if (objectList == null) { throw new ArgumentNullException(nameof(objectList)); } foreach (LCObject obj in objectList) { if (LCBatch.HasCircleReference(obj, new HashSet())) { throw new ArgumentException("Found a circle dependency when save."); } } Stack batches = LCBatch.BatchObjects(objectList, true); await SaveBatches(batches); return objectList; } public async Task Delete() { if (ObjectId == null) { return; } string path = $"classes/{ClassName}/{ObjectId}"; await LCApplication.HttpClient.Delete(path); } public static async Task DeleteAll(List objectList) { if (objectList == null || objectList.Count == 0) { throw new ArgumentNullException(nameof(objectList)); } IEnumerable objects = objectList.Where(item => item.ObjectId != null); HashSet objectSet = new HashSet(objects); List> requestList = objectSet.Select(item => { string path = $"/{LCApplication.APIVersion}/classes/{item.ClassName}/{item.ObjectId}"; return new Dictionary { { "path", path }, { "method", "DELETE" } }; }).ToList(); Dictionary data = new Dictionary { { "requests", LCEncoder.Encode(requestList) } }; await LCApplication.HttpClient.Post>("batch", data: data); } public async Task Fetch(IEnumerable keys = null, IEnumerable includes = null) { Dictionary queryParams = new Dictionary(); if (keys != null) { queryParams["keys"] = string.Join(",", keys); } if (includes != null) { queryParams["include"] = string.Join(",", includes); } string path = $"classes/{ClassName}/{ObjectId}"; Dictionary response = await LCApplication.HttpClient.Get>(path, queryParams: queryParams); LCObjectData objectData = LCObjectData.Decode(response); Merge(objectData); return this; } public static void RegisterSubclass(string className, Func constructor) where T : LCObject { Type classType = typeof(T); LCSubclassInfo subclassInfo = new LCSubclassInfo(className, classType, constructor); subclassNameDict[className] = subclassInfo; subclassTypeDict[classType] = subclassInfo; } void ApplyOperation(string key, ILCOperation op) { if (operationDict.TryGetValue(key, out ILCOperation previousOp)) { operationDict[key] = op.MergeWithPrevious(previousOp); } else { operationDict[key] = op; } if (op is LCDeleteOperation) { estimatedData.Remove(key); } else { if (estimatedData.TryGetValue(key, out object oldValue)) { estimatedData[key] = op.Apply(oldValue, key); } else { estimatedData[key] = op.Apply(null, key); } } } internal void Merge(LCObjectData objectData) { data.ClassName = objectData.ClassName ?? data.ClassName; data.ObjectId = objectData.ObjectId ?? data.ObjectId; data.CreatedAt = objectData.CreatedAt != null ? objectData.CreatedAt : data.CreatedAt; data.UpdatedAt = objectData.UpdatedAt != null ? objectData.UpdatedAt : data.UpdatedAt; // 先将本地的预估数据直接替换 data.CustomPropertyDict = estimatedData; // 再将服务端的数据覆盖 foreach (KeyValuePair kv in objectData.CustomPropertyDict) { string key = kv.Key; object value = kv.Value; data.CustomPropertyDict[key] = value; } // 最后重新生成预估数据,用于后续访问和操作 RebuildEstimatedData(); // 清空操作 operationDict.Clear(); isNew = false; } void RebuildEstimatedData() { estimatedData = new Dictionary(); foreach (KeyValuePair kv in data.CustomPropertyDict) { string key = kv.Key; object value = kv.Value; if (value is IList list) { estimatedData[key] = new List(list.Cast()); } else if (value is IDictionary dict) { Dictionary d = new Dictionary(); foreach (DictionaryEntry entry in dict) { string k = entry.Key.ToString(); object v = entry.Value; d[k] = v; } estimatedData[key] = d; } else { estimatedData[key] = value; } } } } }