diff --git a/Storage/Storage.Test/HelloTest.cs b/Storage/Storage.Test/HelloTest.cs index 12b1f31..7065e0b 100644 --- a/Storage/Storage.Test/HelloTest.cs +++ b/Storage/Storage.Test/HelloTest.cs @@ -21,5 +21,32 @@ namespace LeanCloud.Test { TestContext.WriteLine($"ret: {ret}"); Assert.AreEqual(ret, "hello, world"); } + + [Test] + public async Task Query() { + LCQuery query = new LCQuery("Hello"); + query.Limit(30); + List results = await query.Find(); + TestContext.WriteLine(results.Count); + foreach (LCObject obj in results) { + TestContext.WriteLine(obj.ObjectId); + Assert.NotNull(obj.ObjectId); + } + } + + [Test] + public void InitByNull() { + List sl = new List { "a", "a", "b" }; + HashSet ss = new HashSet(sl); + TestContext.WriteLine(ss.Count); + } + + [Test] + public async Task Save() { + LCObject hello = new LCObject("Hello"); + await hello.Save(); + TestContext.WriteLine($"object id: {hello.ObjectId}"); + Assert.NotNull(hello.ObjectId); + } } } diff --git a/Storage/Storage/Internal/Http/LCHttpClient.cs b/Storage/Storage/Internal/Http/LCHttpClient.cs index 08efd0c..0f0ca0a 100644 --- a/Storage/Storage/Internal/Http/LCHttpClient.cs +++ b/Storage/Storage/Internal/Http/LCHttpClient.cs @@ -1,4 +1,5 @@ using System; +using System.Linq; using System.Collections.Generic; using System.Threading.Tasks; using System.Net; @@ -40,8 +41,16 @@ namespace LeanCloud.Storage.Internal.Http { internal async Task> Get(string path, Dictionary headers = null, Dictionary queryParams = null) { + + string url = $"{server}/{apiVersion}/{path}"; + if (queryParams != null) { + IEnumerable queryPairs = queryParams.Select(kv => $"{kv.Key}={kv.Value}"); + string queries = string.Join("&", queryPairs); + url = $"{url}?{queries}"; + } + HttpRequestMessage request = new HttpRequestMessage { - RequestUri = new Uri($"{server}/{apiVersion}/{path}"), + RequestUri = new Uri(url), Method = HttpMethod.Get }; HttpUtils.PrintRequest(client, request); @@ -71,7 +80,7 @@ namespace LeanCloud.Storage.Internal.Http { } } - internal async Task> Post(string path, + internal async Task Post(string path, Dictionary headers = null, Dictionary data = null, Dictionary queryParams = null) { @@ -90,6 +99,44 @@ namespace LeanCloud.Storage.Internal.Http { response.Dispose(); HttpUtils.PrintResponse(response, resultString); + HttpStatusCode statusCode = response.StatusCode; + if (response.IsSuccessStatusCode) { + T 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> Put(string path, + Dictionary headers = null, + Dictionary data = null, + Dictionary queryParams = null) { + string content = (data != null) ? JsonConvert.SerializeObject(data) : null; + HttpRequestMessage request = new HttpRequestMessage { + RequestUri = new Uri($"{server}/{apiVersion}/{path}"), + Method = HttpMethod.Put, + Content = new StringContent(content) + }; + request.Content.Headers.ContentType = new MediaTypeHeaderValue("application/json"); + HttpUtils.PrintRequest(client, request, content); + 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()); diff --git a/Storage/Storage/Internal/Object/LCBatch.cs b/Storage/Storage/Internal/Object/LCBatch.cs index 8e18ad9..46a7493 100644 --- a/Storage/Storage/Internal/Object/LCBatch.cs +++ b/Storage/Storage/Internal/Object/LCBatch.cs @@ -1,7 +1,78 @@ -using System; +using System.Linq; +using System.Collections; +using System.Collections.Generic; + namespace LeanCloud.Storage.Internal.Object { - public class LCBatch { - public LCBatch() { + internal class LCBatch { + internal HashSet objects; + + internal LCBatch(IEnumerable objs) { + if (objs == null) { + objects = new HashSet(); + } else { + objects = new HashSet(objs); + } + } + + internal static bool HasCircleReference(object obj, HashSet parents) { + if (obj is LCObject lcObj && parents.Contains(lcObj)) { + return true; + } + IEnumerable deps = null; + if (obj is IList list) { + deps = list; + } else if (obj is IDictionary dict) { + deps = dict.Values; + } else if (obj is LCObject lcObject) { + deps = lcObject.estimatedData.Values; + } + HashSet depParents = new HashSet(parents); + if (obj is LCObject) { + depParents.Add(obj as LCObject); + } + if (deps != null) { + foreach (object dep in deps) { + HashSet ps = new HashSet(depParents); + if (HasCircleReference(dep, ps)) { + return true; + } + } + } + return false; + } + + internal static Stack BatchObjects(IEnumerable objects, bool containSelf) { + Stack batches = new Stack(); + if (containSelf) { + batches.Push(new LCBatch(objects)); + } + HashSet deps = new HashSet(); + foreach (LCObject obj in objects) { + deps.UnionWith(obj.operationDict.Values.Select(op => op.GetNewObjectList())); + } + do { + HashSet childSet = new HashSet(); + foreach (object dep in deps) { + IEnumerable children = null; + if (dep is IList list) { + children = list; + } else if (dep is IDictionary dict) { + children = dict; + } else if (dep is LCObject lcDep && lcDep.ObjectId == null) { + children = lcDep.operationDict.Values.Select(op => op.GetNewObjectList()); + } + if (children != null) { + childSet.UnionWith(children.Cast()); + } + } + IEnumerable depObjs = deps.Where(item => item is LCObject lcItem && lcItem.ObjectId == null) + .Cast(); + if (depObjs != null && depObjs.Count() > 0) { + batches.Push(new LCBatch(depObjs)); + } + deps = childSet; + } while (deps != null && deps.Count > 0); + return batches; } } } diff --git a/Storage/Storage/Internal/Query/LCCompositionalCondition.cs b/Storage/Storage/Internal/Query/LCCompositionalCondition.cs index 9f40793..6d103b8 100644 --- a/Storage/Storage/Internal/Query/LCCompositionalCondition.cs +++ b/Storage/Storage/Internal/Query/LCCompositionalCondition.cs @@ -164,9 +164,8 @@ namespace LeanCloud.Storage.Internal.Query { }; } - internal Dictionary BuildParams(string className) { + internal Dictionary BuildParams() { Dictionary dict = new Dictionary { - { "className", className }, { "skip", Skip }, { "limit", Limit } }; diff --git a/Storage/Storage/LCCloud.cs b/Storage/Storage/LCCloud.cs index 25252bc..02a5707 100644 --- a/Storage/Storage/LCCloud.cs +++ b/Storage/Storage/LCCloud.cs @@ -14,7 +14,7 @@ namespace LeanCloud.Storage { /// public static async Task> Run(string name, Dictionary parameters = null) { string path = $"functions/{name}"; - Dictionary response = await LeanCloud.HttpClient.Post(path, data: parameters); + Dictionary response = await LeanCloud.HttpClient.Post>(path, data: parameters); return response; } diff --git a/Storage/Storage/LCFile.cs b/Storage/Storage/LCFile.cs index 4680626..da24ff6 100644 --- a/Storage/Storage/LCFile.cs +++ b/Storage/Storage/LCFile.cs @@ -1,7 +1,9 @@ using System; namespace LeanCloud.Storage { public class LCFile : LCObject { - public LCFile() : base("_File") { + public const string CLASS_NAME = "_File"; + + public LCFile() : base(CLASS_NAME) { } } } diff --git a/Storage/Storage/LCObject.cs b/Storage/Storage/LCObject.cs index d5190a6..365ab09 100644 --- a/Storage/Storage/LCObject.cs +++ b/Storage/Storage/LCObject.cs @@ -2,8 +2,10 @@ 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 { /// @@ -18,12 +20,12 @@ namespace LeanCloud.Storage { /// /// 预算数据 /// - Dictionary estimatedData; + internal Dictionary estimatedData; /// /// 操作字典 /// - Dictionary operationDict; + internal Dictionary operationDict; static readonly Dictionary subclassTypeDict = new Dictionary(); static readonly Dictionary subclassNameDict = new Dictionary(); @@ -60,6 +62,12 @@ namespace LeanCloud.Storage { bool isNew; + bool IsDirty { + get { + return isNew || estimatedData.Count > 0; + } + } + public LCObject(string className) { if (string.IsNullOrEmpty(className)) { throw new ArgumentNullException(nameof(className)); @@ -128,6 +136,72 @@ namespace LeanCloud.Storage { ApplyOperation(key, deleteOp); } + 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 LeanCloud.HttpClient.Post>>("batch", data: data); + List resultList = results.Select(item => { + if (item.TryGetValue("error", out object message)) { + int code = (int)item["code"]; + throw new LCException(code, message as string); + } + return LCObjectData.Decode(item); + }).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 LeanCloud.HttpClient.Post>(path, data: LCEncoder.Encode(operationDict) as Dictionary, queryParams: queryParams) : + await LeanCloud.HttpClient.Put(path, data: LCEncoder.Encode(operationDict) as Dictionary, queryParams: queryParams); + LCObjectData data = LCObjectData.Decode(response); + Merge(data); + return this; + } + public static void RegisterSubclass(string className, Type type, Func constructor) { LCSubclassInfo subclassInfo = new LCSubclassInfo(className, type, constructor); subclassNameDict[className] = subclassInfo; diff --git a/Storage/Storage/LCQuery.cs b/Storage/Storage/LCQuery.cs index f070c2b..34ad342 100644 --- a/Storage/Storage/LCQuery.cs +++ b/Storage/Storage/LCQuery.cs @@ -354,10 +354,10 @@ namespace LeanCloud.Storage { } Dictionary BuildParams() { - return condition.BuildParams(ClassName); + return condition.BuildParams(); } - string BuildWhere() { + internal string BuildWhere() { return condition.BuildWhere(); } } diff --git a/Storage/Storage/LCRole.cs b/Storage/Storage/LCRole.cs index 90061fe..53d955e 100644 --- a/Storage/Storage/LCRole.cs +++ b/Storage/Storage/LCRole.cs @@ -2,7 +2,9 @@ namespace LeanCloud.Storage { public class LCRole : LCObject { - public LCRole() : base("_Role") { + public const string CLASS_NAME = "_Role"; + + public LCRole() : base(CLASS_NAME) { } } } diff --git a/Storage/Storage/LCUser.cs b/Storage/Storage/LCUser.cs index 1e28cc7..e4a5fa1 100644 --- a/Storage/Storage/LCUser.cs +++ b/Storage/Storage/LCUser.cs @@ -2,7 +2,9 @@ namespace LeanCloud.Storage { public class LCUser : LCObject { - public LCUser() : base("_User") { + public const string CLASS_NAME = "_User"; + + public LCUser() : base(CLASS_NAME) { } } diff --git a/Storage/Storage/LeanCloud.cs b/Storage/Storage/LeanCloud.cs index b988ad2..a413456 100644 --- a/Storage/Storage/LeanCloud.cs +++ b/Storage/Storage/LeanCloud.cs @@ -1,4 +1,5 @@ using System; +using LeanCloud.Storage; using LeanCloud.Storage.Internal.Http; namespace LeanCloud { @@ -27,7 +28,10 @@ namespace LeanCloud { if (string.IsNullOrEmpty(appKey)) { throw new ArgumentException(nameof(appKey)); } - // TODO 注册 LeanCloud 内部子类化类型 + // 注册 LeanCloud 内部子类化类型 + LCObject.RegisterSubclass(LCUser.CLASS_NAME, typeof(LCUser), () => new LCUser()); + LCObject.RegisterSubclass(LCRole.CLASS_NAME, typeof(LCRole), () => new LCRole()); + LCObject.RegisterSubclass(LCFile.CLASS_NAME, typeof(LCFile), () => new LCFile()); HttpClient = new LCHttpClient(appId, appKey, server, SDKVersion, APIVersion); }