* LCFile.cs:
* LCRole.cs: * LCUser.cs: * LCCloud.cs: * LCQuery.cs: * LCObject.cs: * LeanCloud.cs: * LCBatch.cs: * LCHttpClient.cs: * LCCompositionalCondition.cs: * HelloTest.cs: chore: 实现批处理
parent
362ef92079
commit
c775f612bc
|
@ -21,5 +21,32 @@ namespace LeanCloud.Test {
|
||||||
TestContext.WriteLine($"ret: {ret}");
|
TestContext.WriteLine($"ret: {ret}");
|
||||||
Assert.AreEqual(ret, "hello, world");
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
using System;
|
using System;
|
||||||
|
using System.Linq;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using System.Net;
|
using System.Net;
|
||||||
|
@ -40,8 +41,16 @@ namespace LeanCloud.Storage.Internal.Http {
|
||||||
internal async Task<Dictionary<string, object>> Get(string path,
|
internal async Task<Dictionary<string, object>> Get(string path,
|
||||||
Dictionary<string, object> headers = null,
|
Dictionary<string, object> headers = null,
|
||||||
Dictionary<string, object> queryParams = 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 {
|
HttpRequestMessage request = new HttpRequestMessage {
|
||||||
RequestUri = new Uri($"{server}/{apiVersion}/{path}"),
|
RequestUri = new Uri(url),
|
||||||
Method = HttpMethod.Get
|
Method = HttpMethod.Get
|
||||||
};
|
};
|
||||||
HttpUtils.PrintRequest(client, request);
|
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> headers = null,
|
||||||
Dictionary<string, object> data = null,
|
Dictionary<string, object> data = null,
|
||||||
Dictionary<string, object> queryParams = null) {
|
Dictionary<string, object> queryParams = null) {
|
||||||
|
@ -90,6 +99,44 @@ namespace LeanCloud.Storage.Internal.Http {
|
||||||
response.Dispose();
|
response.Dispose();
|
||||||
HttpUtils.PrintResponse(response, resultString);
|
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;
|
HttpStatusCode statusCode = response.StatusCode;
|
||||||
if (response.IsSuccessStatusCode) {
|
if (response.IsSuccessStatusCode) {
|
||||||
Dictionary<string, object> ret = JsonConvert.DeserializeObject<Dictionary<string, object>>(resultString, new LeanCloudJsonConverter());
|
Dictionary<string, object> ret = JsonConvert.DeserializeObject<Dictionary<string, object>>(resultString, new LeanCloudJsonConverter());
|
||||||
|
|
|
@ -1,7 +1,78 @@
|
||||||
using System;
|
using System.Linq;
|
||||||
|
using System.Collections;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
namespace LeanCloud.Storage.Internal.Object {
|
namespace LeanCloud.Storage.Internal.Object {
|
||||||
public class LCBatch {
|
internal class LCBatch {
|
||||||
public 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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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> {
|
Dictionary<string, object> dict = new Dictionary<string, object> {
|
||||||
{ "className", className },
|
|
||||||
{ "skip", Skip },
|
{ "skip", Skip },
|
||||||
{ "limit", Limit }
|
{ "limit", Limit }
|
||||||
};
|
};
|
||||||
|
|
|
@ -14,7 +14,7 @@ namespace LeanCloud.Storage {
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
public static async Task<Dictionary<string, object>> Run(string name, Dictionary<string, object> parameters = null) {
|
public static async Task<Dictionary<string, object>> Run(string name, Dictionary<string, object> parameters = null) {
|
||||||
string path = $"functions/{name}";
|
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;
|
return response;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,9 @@
|
||||||
using System;
|
using System;
|
||||||
namespace LeanCloud.Storage {
|
namespace LeanCloud.Storage {
|
||||||
public class LCFile : LCObject {
|
public class LCFile : LCObject {
|
||||||
public LCFile() : base("_File") {
|
public const string CLASS_NAME = "_File";
|
||||||
|
|
||||||
|
public LCFile() : base(CLASS_NAME) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,8 +2,10 @@
|
||||||
using System.Collections;
|
using System.Collections;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using System.Threading.Tasks;
|
||||||
using LeanCloud.Storage.Internal.Object;
|
using LeanCloud.Storage.Internal.Object;
|
||||||
using LeanCloud.Storage.Internal.Operation;
|
using LeanCloud.Storage.Internal.Operation;
|
||||||
|
using LeanCloud.Storage.Internal.Codec;
|
||||||
|
|
||||||
namespace LeanCloud.Storage {
|
namespace LeanCloud.Storage {
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
@ -18,12 +20,12 @@ namespace LeanCloud.Storage {
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 预算数据
|
/// 预算数据
|
||||||
/// </summary>
|
/// </summary>
|
||||||
Dictionary<string, object> estimatedData;
|
internal Dictionary<string, object> estimatedData;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 操作字典
|
/// 操作字典
|
||||||
/// </summary>
|
/// </summary>
|
||||||
Dictionary<string, ILCOperation> operationDict;
|
internal Dictionary<string, ILCOperation> operationDict;
|
||||||
|
|
||||||
static readonly Dictionary<Type, LCSubclassInfo> subclassTypeDict = new Dictionary<Type, LCSubclassInfo>();
|
static readonly Dictionary<Type, LCSubclassInfo> subclassTypeDict = new Dictionary<Type, LCSubclassInfo>();
|
||||||
static readonly Dictionary<string, LCSubclassInfo> subclassNameDict = new Dictionary<string, LCSubclassInfo>();
|
static readonly Dictionary<string, LCSubclassInfo> subclassNameDict = new Dictionary<string, LCSubclassInfo>();
|
||||||
|
@ -60,6 +62,12 @@ namespace LeanCloud.Storage {
|
||||||
|
|
||||||
bool isNew;
|
bool isNew;
|
||||||
|
|
||||||
|
bool IsDirty {
|
||||||
|
get {
|
||||||
|
return isNew || estimatedData.Count > 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public LCObject(string className) {
|
public LCObject(string className) {
|
||||||
if (string.IsNullOrEmpty(className)) {
|
if (string.IsNullOrEmpty(className)) {
|
||||||
throw new ArgumentNullException(nameof(className));
|
throw new ArgumentNullException(nameof(className));
|
||||||
|
@ -128,6 +136,72 @@ namespace LeanCloud.Storage {
|
||||||
ApplyOperation(key, deleteOp);
|
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) {
|
public static void RegisterSubclass(string className, Type type, Func<LCObject> constructor) {
|
||||||
LCSubclassInfo subclassInfo = new LCSubclassInfo(className, type, constructor);
|
LCSubclassInfo subclassInfo = new LCSubclassInfo(className, type, constructor);
|
||||||
subclassNameDict[className] = subclassInfo;
|
subclassNameDict[className] = subclassInfo;
|
||||||
|
|
|
@ -354,10 +354,10 @@ namespace LeanCloud.Storage {
|
||||||
}
|
}
|
||||||
|
|
||||||
Dictionary<string, object> BuildParams() {
|
Dictionary<string, object> BuildParams() {
|
||||||
return condition.BuildParams(ClassName);
|
return condition.BuildParams();
|
||||||
}
|
}
|
||||||
|
|
||||||
string BuildWhere() {
|
internal string BuildWhere() {
|
||||||
return condition.BuildWhere();
|
return condition.BuildWhere();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,7 +2,9 @@
|
||||||
|
|
||||||
namespace LeanCloud.Storage {
|
namespace LeanCloud.Storage {
|
||||||
public class LCRole : LCObject {
|
public class LCRole : LCObject {
|
||||||
public LCRole() : base("_Role") {
|
public const string CLASS_NAME = "_Role";
|
||||||
|
|
||||||
|
public LCRole() : base(CLASS_NAME) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,7 +2,9 @@
|
||||||
|
|
||||||
namespace LeanCloud.Storage {
|
namespace LeanCloud.Storage {
|
||||||
public class LCUser : LCObject {
|
public class LCUser : LCObject {
|
||||||
public LCUser() : base("_User") {
|
public const string CLASS_NAME = "_User";
|
||||||
|
|
||||||
|
public LCUser() : base(CLASS_NAME) {
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
using System;
|
using System;
|
||||||
|
using LeanCloud.Storage;
|
||||||
using LeanCloud.Storage.Internal.Http;
|
using LeanCloud.Storage.Internal.Http;
|
||||||
|
|
||||||
namespace LeanCloud {
|
namespace LeanCloud {
|
||||||
|
@ -27,7 +28,10 @@ namespace LeanCloud {
|
||||||
if (string.IsNullOrEmpty(appKey)) {
|
if (string.IsNullOrEmpty(appKey)) {
|
||||||
throw new ArgumentException(nameof(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);
|
HttpClient = new LCHttpClient(appId, appKey, server, SDKVersion, APIVersion);
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue