* LCFile.cs:

* LCRole.cs:
* LCUser.cs:
* LCCloud.cs:
* LCQuery.cs:
* LCObject.cs:
* LeanCloud.cs:
* LCBatch.cs:
* LCHttpClient.cs:
* LCCompositionalCondition.cs:

* HelloTest.cs: chore: 实现批处理
oneRain 2020-02-24 17:50:31 +08:00
parent 362ef92079
commit c775f612bc
11 changed files with 244 additions and 16 deletions

View File

@ -21,5 +21,32 @@ namespace LeanCloud.Test {
TestContext.WriteLine($"ret: {ret}");
Assert.AreEqual(ret, "hello, world");
}
[Test]
public async Task Query() {
LCQuery<LCObject> query = new LCQuery<LCObject>("Hello");
query.Limit(30);
List<LCObject> 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<string> sl = new List<string> { "a", "a", "b" };
HashSet<string> ss = new HashSet<string>(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);
}
}
}

View File

@ -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<Dictionary<string, object>> Get(string path,
Dictionary<string, object> headers = null,
Dictionary<string, object> queryParams = null) {
string url = $"{server}/{apiVersion}/{path}";
if (queryParams != null) {
IEnumerable<string> 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<Dictionary<string, object>> Post(string path,
internal async Task<T> Post<T>(string path,
Dictionary<string, object> headers = null,
Dictionary<string, object> data = null,
Dictionary<string, object> 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<T>(resultString, new LeanCloudJsonConverter());
return ret;
}
int code = (int)statusCode;
string message = resultString;
try {
// 尝试获取 LeanCloud 返回错误信息
Dictionary<string, object> error = JsonConvert.DeserializeObject<Dictionary<string, object>>(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<Dictionary<string, object>> Put(string path,
Dictionary<string, object> headers = null,
Dictionary<string, object> data = null,
Dictionary<string, object> 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<string, object> ret = JsonConvert.DeserializeObject<Dictionary<string, object>>(resultString, new LeanCloudJsonConverter());

View File

@ -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<LCObject> objects;
internal LCBatch(IEnumerable<LCObject> objs) {
if (objs == null) {
objects = new HashSet<LCObject>();
} else {
objects = new HashSet<LCObject>(objs);
}
}
internal static bool HasCircleReference(object obj, HashSet<LCObject> 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<LCObject> depParents = new HashSet<LCObject>(parents);
if (obj is LCObject) {
depParents.Add(obj as LCObject);
}
if (deps != null) {
foreach (object dep in deps) {
HashSet<LCObject> ps = new HashSet<LCObject>(depParents);
if (HasCircleReference(dep, ps)) {
return true;
}
}
}
return false;
}
internal static Stack<LCBatch> BatchObjects(IEnumerable<LCObject> objects, bool containSelf) {
Stack<LCBatch> batches = new Stack<LCBatch>();
if (containSelf) {
batches.Push(new LCBatch(objects));
}
HashSet<object> deps = new HashSet<object>();
foreach (LCObject obj in objects) {
deps.UnionWith(obj.operationDict.Values.Select(op => op.GetNewObjectList()));
}
do {
HashSet<object> childSet = new HashSet<object>();
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<object>());
}
}
IEnumerable<LCObject> depObjs = deps.Where(item => item is LCObject lcItem && lcItem.ObjectId == null)
.Cast<LCObject>();
if (depObjs != null && depObjs.Count() > 0) {
batches.Push(new LCBatch(depObjs));
}
deps = childSet;
} while (deps != null && deps.Count > 0);
return batches;
}
}
}

View File

@ -164,9 +164,8 @@ namespace LeanCloud.Storage.Internal.Query {
};
}
internal Dictionary<string, object> BuildParams(string className) {
internal Dictionary<string, object> BuildParams() {
Dictionary<string, object> dict = new Dictionary<string, object> {
{ "className", className },
{ "skip", Skip },
{ "limit", Limit }
};

View File

@ -14,7 +14,7 @@ namespace LeanCloud.Storage {
/// <returns></returns>
public static async Task<Dictionary<string, object>> Run(string name, Dictionary<string, object> parameters = null) {
string path = $"functions/{name}";
Dictionary<string, object> response = await LeanCloud.HttpClient.Post(path, data: parameters);
Dictionary<string, object> response = await LeanCloud.HttpClient.Post<Dictionary<string, object>>(path, data: parameters);
return response;
}

View File

@ -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) {
}
}
}

View File

@ -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 {
/// <summary>
@ -18,12 +20,12 @@ namespace LeanCloud.Storage {
/// <summary>
/// 预算数据
/// </summary>
Dictionary<string, object> estimatedData;
internal Dictionary<string, object> estimatedData;
/// <summary>
/// 操作字典
/// </summary>
Dictionary<string, ILCOperation> operationDict;
internal Dictionary<string, ILCOperation> operationDict;
static readonly Dictionary<Type, LCSubclassInfo> subclassTypeDict = new Dictionary<Type, LCSubclassInfo>();
static readonly Dictionary<string, LCSubclassInfo> subclassNameDict = new Dictionary<string, LCSubclassInfo>();
@ -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<LCBatch> batches) {
while (batches.Count > 0) {
LCBatch batch = batches.Pop();
List<LCObject> dirtyObjects = batch.objects.Where(item => item.IsDirty)
.ToList();
List<Dictionary<string, object>> 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<string, object> body = LCEncoder.Encode(item.operationDict) as Dictionary<string, object>;
return new Dictionary<string, object> {
{ "path", path },
{ "method", method },
{ "body", body }
};
}).ToList();
Dictionary<string, object> data = new Dictionary<string, object> {
{ "requests", LCEncoder.Encode(requestList) }
};
List<Dictionary<string, object>> results = await LeanCloud.HttpClient.Post<List<Dictionary<string, object>>>("batch", data: data);
List<LCObjectData> 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<LCObject> Save(bool fetchWhenSave = false, LCQuery<LCObject> query = null) {
if (LCBatch.HasCircleReference(this, new HashSet<LCObject>())) {
throw new ArgumentException("Found a circle dependency when save.");
}
Stack<LCBatch> batches = LCBatch.BatchObjects(new List<LCObject> { this }, false);
if (batches.Count > 0) {
await SaveBatches(batches);
}
string path = ObjectId == null ? $"classes/{ClassName}" : $"classes/{ClassName}/{ObjectId}";
Dictionary<string, object> queryParams = new Dictionary<string, object>();
if (fetchWhenSave) {
queryParams["fetchWhenSave"] = true;
}
if (query != null) {
queryParams["where"] = query.BuildWhere();
}
Dictionary<string, object> response = ObjectId == null ?
await LeanCloud.HttpClient.Post<Dictionary<string, object>>(path, data: LCEncoder.Encode(operationDict) as Dictionary<string, object>, queryParams: queryParams) :
await LeanCloud.HttpClient.Put(path, data: LCEncoder.Encode(operationDict) as Dictionary<string, object>, queryParams: queryParams);
LCObjectData data = LCObjectData.Decode(response);
Merge(data);
return this;
}
public static void RegisterSubclass(string className, Type type, Func<LCObject> constructor) {
LCSubclassInfo subclassInfo = new LCSubclassInfo(className, type, constructor);
subclassNameDict[className] = subclassInfo;

View File

@ -354,10 +354,10 @@ namespace LeanCloud.Storage {
}
Dictionary<string, object> BuildParams() {
return condition.BuildParams(ClassName);
return condition.BuildParams();
}
string BuildWhere() {
internal string BuildWhere() {
return condition.BuildWhere();
}
}

View File

@ -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) {
}
}
}

View File

@ -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) {
}
}

View File

@ -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);
}