From 362ef92079a3fdbb0556ce8ffbbe52b020aa595f Mon Sep 17 00:00:00 2001 From: oneRain Date: Fri, 21 Feb 2020 16:44:58 +0800 Subject: [PATCH] =?UTF-8?q?*=20LCDecoder.cs:=20chore:=20=E8=B0=83=E9=80=9A?= =?UTF-8?q?=20Query?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * LCFile.cs: * LCRole.cs: * LCUser.cs: * LCQuery.cs: * LCObject.cs: * LCHttpClient.cs: * LCObjectData.cs: * LCSubClassInfo.cs: * LCCompositionalCondition.cs: --- Storage/Storage.Test/AVObjectTest.cs | 189 --------- Storage/Storage/Internal/Codec/LCDecoder.cs | 4 +- Storage/Storage/Internal/Http/LCHttpClient.cs | 34 +- .../Storage/Internal/Object/LCObjectData.cs | 4 +- .../Storage/Internal/Object/LCSubClassInfo.cs | 20 +- .../Query/LCCompositionalCondition.cs | 4 +- Storage/Storage/LCFile.cs | 2 +- Storage/Storage/LCObject.cs | 154 +++++++- Storage/Storage/LCQuery.cs | 361 +++++++++++++++++- Storage/Storage/LCRole.cs | 2 +- Storage/Storage/LCUser.cs | 4 +- 11 files changed, 571 insertions(+), 207 deletions(-) delete mode 100644 Storage/Storage.Test/AVObjectTest.cs diff --git a/Storage/Storage.Test/AVObjectTest.cs b/Storage/Storage.Test/AVObjectTest.cs deleted file mode 100644 index 070cd9a..0000000 --- a/Storage/Storage.Test/AVObjectTest.cs +++ /dev/null @@ -1,189 +0,0 @@ -using NUnit.Framework; -using System; -using System.Text; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using System.Collections; -using System.Collections.Generic; - -namespace LeanCloud.Test { - public class LCObject { - public string Id { - get; set; - } - - public Dictionary Data { - get; set; - } - - public LCObject(string id) { - Data = new Dictionary(); - Id = id; - } - - public static Stack Batch(IEnumerable objects) { - Stack batches = new Stack(); - - IEnumerable deps = objects; - do { - // 只添加本层依赖的 LCObject - IEnumerable avObjects = deps.OfType(); - if (avObjects.Any()) { - batches.Push(new Batch(avObjects)); - } - - HashSet childSets = new HashSet(); - foreach (object dep in deps) { - IEnumerable children = null; - if (dep is IList) { - children = dep as IList; - } else if (dep is IDictionary) { - children = dep as IDictionary; - } else if (dep is LCObject) { - children = (dep as LCObject).Data.Values; - } - if (children != null) { - foreach (object child in children) { - childSets.Add(child); - } - } - } - deps = childSets; - } while (deps != null && deps.Any()); - - return batches; - } - - public static bool HasCircleReference(object obj, HashSet parents) { - if (parents.Contains(obj)) { - return true; - } - IEnumerable deps = null; - if (obj is IList) { - deps = obj as IList; - } else if (obj is IDictionary) { - deps = (obj as IDictionary).Values; - } else if (obj is LCObject) { - deps = (obj as LCObject).Data.Values; - } - HashSet depParent = new HashSet(parents); - if (obj is LCObject) { - depParent.Add((LCObject) obj); - } - if (deps != null) { - foreach (object dep in deps) { - HashSet set = new HashSet(depParent); - if (HasCircleReference(dep, set)) { - return true; - } - } - } - return false; - } - - public Stack Batch() { - return Batch(new List { this }); - } - - public bool HasCircleReference() { - return HasCircleReference(this, new HashSet()); - } - } - - public class Batch { - HashSet ObjectSet { - get; set; - } - - public Batch() { - ObjectSet = new HashSet(); - } - - public Batch(IEnumerable objects) : this() { - foreach (LCObject obj in objects) { - ObjectSet.Add(obj); - } - } - - public override string ToString() { - StringBuilder sb = new StringBuilder(); - sb.AppendLine("----------------------------"); - foreach (LCObject obj in ObjectSet) { - sb.AppendLine(obj.Id); - } - sb.AppendLine("----------------------------"); - return sb.ToString(); - } - } - - public class AVObjectTest { - void PrintBatches(Stack batches) { - while (batches.Any()) { - Batch batch = batches.Pop(); - TestContext.WriteLine(batch); - } - } - - [Test] - public void Simple() { - LCObject a = new LCObject("a"); - LCObject b = new LCObject("b"); - LCObject c = new LCObject("c"); - a.Data["child"] = b; - b.Data["child"] = c; - - Assert.IsFalse(a.HasCircleReference()); - - Stack batches = a.Batch(); - PrintBatches(batches); - } - - [Test] - public void Array() { - LCObject a = new LCObject("a"); - LCObject b = new LCObject("b"); - LCObject c = new LCObject("c"); - a.Data["children"] = new List { b, c }; - - Assert.IsFalse(a.HasCircleReference()); - - Stack batches = a.Batch(); - PrintBatches(batches); - } - - [Test] - public void SimpleCircleReference() { - LCObject a = new LCObject("a"); - LCObject b = new LCObject("b"); - a.Data["child"] = b; - b.Data["child"] = a; - - Assert.IsTrue(a.HasCircleReference()); - } - - [Test] - public void ComplexCircleReference() { - LCObject a = new LCObject("a"); - LCObject b = new LCObject("b"); - LCObject c = new LCObject("c"); - a.Data["arr"] = new List { 1, b }; - a.Data["child"] = c; - b.Data["arr"] = new List { 2, a }; - - Assert.IsTrue(a.HasCircleReference()); - } - - [Test] - public void ComplexCircleReference2() { - LCObject a = new LCObject("a"); - LCObject b = new LCObject("b"); - List list = new List(); - a.Data["list"] = list; - b.Data["list"] = list; - a.Data["child"] = b; - - Assert.IsFalse(a.HasCircleReference()); - } - } -} \ No newline at end of file diff --git a/Storage/Storage/Internal/Codec/LCDecoder.cs b/Storage/Storage/Internal/Codec/LCDecoder.cs index 661e97e..2561b36 100644 --- a/Storage/Storage/Internal/Codec/LCDecoder.cs +++ b/Storage/Storage/Internal/Codec/LCDecoder.cs @@ -24,8 +24,8 @@ namespace LeanCloud.Storage.Internal.Codec { } } Dictionary d = new Dictionary(); - foreach (KeyValuePair kv in dict) { - string key = kv.Key; + foreach (DictionaryEntry kv in dict) { + string key = kv.Key.ToString(); object value = kv.Value; d[key] = Decode(value); } diff --git a/Storage/Storage/Internal/Http/LCHttpClient.cs b/Storage/Storage/Internal/Http/LCHttpClient.cs index 1df7ca3..08efd0c 100644 --- a/Storage/Storage/Internal/Http/LCHttpClient.cs +++ b/Storage/Storage/Internal/Http/LCHttpClient.cs @@ -37,8 +37,38 @@ namespace LeanCloud.Storage.Internal.Http { client.DefaultRequestHeaders.Add("X-LC-Key", appKey); } - internal Task Get() { - return null; + internal async Task> Get(string path, + Dictionary headers = null, + Dictionary queryParams = null) { + HttpRequestMessage request = new HttpRequestMessage { + RequestUri = new Uri($"{server}/{apiVersion}/{path}"), + Method = HttpMethod.Get + }; + HttpUtils.PrintRequest(client, request); + HttpResponseMessage response = await client.SendAsync(request, HttpCompletionOption.ResponseHeadersRead); + request.Dispose(); + + string resultString = await response.Content.ReadAsStringAsync(); + response.Dispose(); + HttpUtils.PrintResponse(response, resultString); + + HttpStatusCode statusCode = response.StatusCode; + if (response.IsSuccessStatusCode) { + Dictionary ret = JsonConvert.DeserializeObject>(resultString, new LeanCloudJsonConverter()); + return ret; + } + int code = (int)statusCode; + string message = resultString; + try { + // 尝试获取 LeanCloud 返回错误信息 + Dictionary error = JsonConvert.DeserializeObject>(resultString, new LeanCloudJsonConverter()); + code = (int)error["code"]; + message = error["error"].ToString(); + } catch (Exception e) { + Logger.Error(e.Message); + } finally { + throw new LCException(code, message); + } } internal async Task> Post(string path, diff --git a/Storage/Storage/Internal/Object/LCObjectData.cs b/Storage/Storage/Internal/Object/LCObjectData.cs index 318d4bc..964758b 100644 --- a/Storage/Storage/Internal/Object/LCObjectData.cs +++ b/Storage/Storage/Internal/Object/LCObjectData.cs @@ -32,8 +32,8 @@ namespace LeanCloud.Storage.Internal.Object { return null; } LCObjectData objectData = new LCObjectData(); - foreach (KeyValuePair kv in dict) { - string key = kv.Key; + foreach (DictionaryEntry kv in dict) { + string key = kv.Key.ToString(); object value = kv.Value; if (key == "className") { objectData.ClassName = value.ToString(); diff --git a/Storage/Storage/Internal/Object/LCSubClassInfo.cs b/Storage/Storage/Internal/Object/LCSubClassInfo.cs index da96add..d225931 100644 --- a/Storage/Storage/Internal/Object/LCSubClassInfo.cs +++ b/Storage/Storage/Internal/Object/LCSubClassInfo.cs @@ -1,7 +1,23 @@ using System; + namespace LeanCloud.Storage.Internal.Object { - public class LCSubClassInfo { - public LCSubClassInfo() { + internal class LCSubclassInfo { + internal string ClassName { + get; + } + + internal Type Type { + get; + } + + internal Func Constructor { + get; + } + + internal LCSubclassInfo(string className, Type type, Func constructor) { + ClassName = className; + Type = type; + Constructor = constructor; } } } diff --git a/Storage/Storage/Internal/Query/LCCompositionalCondition.cs b/Storage/Storage/Internal/Query/LCCompositionalCondition.cs index 1d10962..9f40793 100644 --- a/Storage/Storage/Internal/Query/LCCompositionalCondition.cs +++ b/Storage/Storage/Internal/Query/LCCompositionalCondition.cs @@ -110,7 +110,7 @@ namespace LeanCloud.Storage.Internal.Query { Add(cond); } - void Add(ILCQueryCondition cond) { + internal void Add(ILCQueryCondition cond) { if (cond == null) { return; } @@ -129,7 +129,7 @@ namespace LeanCloud.Storage.Internal.Query { orderByList.Add(key); } - internal void OrderByDesending(string key) { + internal void OrderByDescending(string key) { OrderBy($"-{key}"); } diff --git a/Storage/Storage/LCFile.cs b/Storage/Storage/LCFile.cs index 8c3d8b1..4680626 100644 --- a/Storage/Storage/LCFile.cs +++ b/Storage/Storage/LCFile.cs @@ -1,7 +1,7 @@ using System; namespace LeanCloud.Storage { public class LCFile : LCObject { - public LCFile() { + public LCFile() : base("_File") { } } } diff --git a/Storage/Storage/LCObject.cs b/Storage/Storage/LCObject.cs index d0cea06..d5190a6 100644 --- a/Storage/Storage/LCObject.cs +++ b/Storage/Storage/LCObject.cs @@ -1,4 +1,7 @@ -using System.Collections.Generic; +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; using LeanCloud.Storage.Internal.Object; using LeanCloud.Storage.Internal.Operation; @@ -22,6 +25,9 @@ namespace LeanCloud.Storage { /// Dictionary operationDict; + static readonly Dictionary subclassTypeDict = new Dictionary(); + static readonly Dictionary subclassNameDict = new Dictionary(); + public string ClassName { get { return data.ClassName; @@ -34,12 +40,154 @@ namespace LeanCloud.Storage { } } - public LCObject() { + public DateTime CreatedAt { + get { + return data.CreatedAt; + } + } + + public DateTime UpdatedAt { + get { + return data.UpdatedAt; + } + } + + public LCACL ACL { + get { + return this["ACL"] as LCACL ; + } + } + + bool isNew; + + 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 null; } internal static LCObject Create(string className) { - // TODO + 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 { + object value = estimatedData[key]; + if (value is LCRelation relation) { + relation.Key = key; + relation.Parent = this; + } + return value; + } + 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 static void RegisterSubclass(string className, Type type, Func constructor) { + LCSubclassInfo subclassInfo = new LCSubclassInfo(className, type, constructor); + subclassNameDict[className] = subclassInfo; + subclassTypeDict[type] = 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 { + object oldValue = estimatedData[key]; + estimatedData[key] = op.Apply(oldValue, 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; + } + } + } } } diff --git a/Storage/Storage/LCQuery.cs b/Storage/Storage/LCQuery.cs index 475ce3f..f070c2b 100644 --- a/Storage/Storage/LCQuery.cs +++ b/Storage/Storage/LCQuery.cs @@ -1,7 +1,364 @@ using System; +using System.Linq; +using System.Collections; +using System.Collections.Generic; +using System.Threading.Tasks; +using LeanCloud.Storage.Internal.Query; +using LeanCloud.Storage.Internal.Object; + namespace LeanCloud.Storage { - public class LCQuery { - public LCQuery() { + /// + /// 查询类 + /// + /// + public class LCQuery where T : LCObject { + public string ClassName { + get; private set; + } + + LCCompositionalCondition condition; + + public LCQuery(string className) { + ClassName = className; + condition = new LCCompositionalCondition(); + } + + /// + /// 等于 + /// + /// + /// + /// + public LCQuery WhereEqualTo(string key, object value) { + condition.WhereEqualTo(key, value); + return this; + } + + /// + /// 不等于 + /// + /// + /// + /// + public LCQuery WhereNotEqualTo(string key, object value) { + condition.WhereNotEqualTo(key, value); + return this; + } + + /// + /// 包含 + /// + /// + /// + /// + public LCQuery WhereContainedIn(string key, IEnumerable values) { + condition.WhereContainedIn(key, values); + return this; + } + + /// + /// 包含全部 + /// + /// + /// + /// + public LCQuery WhereContainsAll(string key, IEnumerable values) { + condition.WhereContainsAll(key, values); + return this; + } + + /// + /// 存在 + /// + /// + /// + public LCQuery WhereExists(string key) { + condition.WhereExists(key); + return this; + } + + /// + /// 不存在 + /// + /// + /// + public LCQuery WhereDoesNotExist(string key) { + condition.WhereDoesNotExist(key); + return this; + } + + /// + /// 长度等于 + /// + /// + /// + /// + public LCQuery WhereSizeEqualTo(string key, int size) { + condition.WhereSizeEqualTo(key, size); + return this; + } + + /// + /// 大于 + /// + /// + /// + /// + public LCQuery WhereGreaterThan(string key, object value) { + condition.WhereGreaterThan(key, value); + return this; + } + + /// + /// 大于等于 + /// + /// + /// + /// + public LCQuery WhereGreaterThanOrEqualTo(string key, object value) { + condition.WhereGreaterThanOrEqualTo(key, value); + return this; + } + + /// + /// 小于 + /// + /// + /// + /// + public LCQuery WhereLessThan(string key, object value) { + condition.WhereLessThan(key, value); + return this; + } + + /// + /// 小于等于 + /// + /// + /// + /// + public LCQuery WhereLessThanOrEqualTo(string key, object value) { + condition.WhereLessThanOrEqualTo(key, value); + return this; + } + + /// + /// 相邻 + /// + /// + /// + /// + public LCQuery WhereNear(string key, LCGeoPoint point) { + condition.WhereNear(key, point); + return this; + } + + /// + /// 在坐标区域内 + /// + /// + /// + /// + /// + public LCQuery WhereWithinGeoBox(string key, LCGeoPoint southwest, LCGeoPoint northeast) { + condition.WhereWithinGeoBox(key, southwest, northeast); + return this; + } + + /// + /// 相关 + /// + /// + /// + /// + public LCQuery WhereRelatedTo(LCObject parent, string key) { + condition.WhereRelatedTo(parent, key); + return this; + } + + /// + /// 前缀 + /// + /// + /// + /// + public LCQuery WhereStartsWith(string key, string prefix) { + condition.WhereStartsWith(key, prefix); + return this; + } + + /// + /// 后缀 + /// + /// + /// + /// + public LCQuery WhereEndsWith(string key, string suffix) { + condition.WhereEndsWith(key, suffix); + return this; + } + + /// + /// 字符串包含 + /// + /// + /// + /// + public LCQuery WhereContains(string key, string subString) { + condition.WhereContains(key, subString); + return this; + } + + /// + /// 按 key 升序 + /// + /// + /// + public LCQuery OrderBy(string key) { + condition.OrderBy(key); + return this; + } + + /// + /// 按 key 降序 + /// + /// + /// + public LCQuery OrderByDescending(string key) { + condition.OrderByDescending(key); + return this; + } + + /// + /// 拉取 key 的完整对象 + /// + /// + /// + public LCQuery Include(string key) { + condition.Include(key); + return this; + } + + /// + /// 包含 key + /// + /// + /// + public LCQuery Select(string key) { + condition.Select(key); + return this; + } + + /// + /// 跳过 + /// + /// + /// + public LCQuery Skip(int value) { + condition.Skip = value; + return this; + } + + /// + /// 限制数量 + /// + /// + /// + public LCQuery Limit(int value) { + condition.Limit = value; + return this; + } + + public async Task Count() { + string path = $"classes/{ClassName}"; + Dictionary parameters = BuildParams(); + parameters["limit"] = 0; + parameters["count"] = 1; + Dictionary ret = await LeanCloud.HttpClient.Get(path, queryParams: parameters); + return (int)ret["count"]; + } + + public async Task Get(string objectId) { + if (string.IsNullOrEmpty(objectId)) { + throw new ArgumentNullException(nameof(objectId)); + } + WhereEqualTo("objectId", objectId); + Limit(1); + List results = await Find(); + if (results != null) { + if (results.Count == 0) { + return null; + } + return results[0]; + } + return null; + } + + public async Task> Find() { + string path = $"classes/{ClassName}"; + Dictionary parameters = BuildParams(); + Dictionary response = await LeanCloud.HttpClient.Get(path, queryParams: parameters); + List results = response["results"] as List; + List list = new List(); + foreach (object item in results) { + LCObjectData objectData = LCObjectData.Decode(item as Dictionary); + T obj = LCObject.Create(ClassName) as T; + obj.Merge(objectData); + list.Add(obj); + } + return list; + } + + public async Task First() { + Limit(1); + List results = await Find(); + if (results != null && results.Count > 0) { + return results[0]; + } + return null; + } + + public static LCQuery And(IEnumerable> queries) { + if (queries == null || queries.Count() < 1) { + throw new ArgumentNullException(nameof(queries)); + } + LCQuery compositionQuery = new LCQuery(null); + string className = null; + foreach (LCQuery query in queries) { + if (className != null && className != query.ClassName) { + throw new Exception("All of the queries in an or query must be on the same class."); + } + className = query.ClassName; + compositionQuery.condition.Add(query.condition); + } + compositionQuery.ClassName = className; + return compositionQuery; + } + + public static LCQuery Or(IEnumerable> queries) { + if (queries == null || queries.Count() < 1) { + throw new ArgumentNullException(nameof(queries)); + } + LCQuery compositionQuery = new LCQuery(null); + compositionQuery.condition = new LCCompositionalCondition(LCCompositionalCondition.Or); + string className = null; + foreach (LCQuery query in queries) { + if (className != null && className != query.ClassName) { + throw new Exception("All of the queries in an or query must be on the same class."); + } + className = query.ClassName; + compositionQuery.condition.Add(query.condition); + } + compositionQuery.ClassName = className; + return compositionQuery; + } + + Dictionary BuildParams() { + return condition.BuildParams(ClassName); + } + + string BuildWhere() { + return condition.BuildWhere(); } } } diff --git a/Storage/Storage/LCRole.cs b/Storage/Storage/LCRole.cs index 6da6d3b..90061fe 100644 --- a/Storage/Storage/LCRole.cs +++ b/Storage/Storage/LCRole.cs @@ -2,7 +2,7 @@ namespace LeanCloud.Storage { public class LCRole : LCObject { - public LCRole() { + public LCRole() : base("_Role") { } } } diff --git a/Storage/Storage/LCUser.cs b/Storage/Storage/LCUser.cs index 2c15a64..1e28cc7 100644 --- a/Storage/Storage/LCUser.cs +++ b/Storage/Storage/LCUser.cs @@ -1,7 +1,9 @@ using System; + namespace LeanCloud.Storage { public class LCUser : LCObject { - public LCUser() { + public LCUser() : base("_User") { + } } }