* csharp-sdk.sln: chore: 循环引用检测及测试;保存批次算法调整
* Test.cs: * Utils.cs: * AVObject.cs: * AppRouterTest.cs: * ObjectTest.cs: * AVObject.cs: * Common.Test.csproj: * AVObjectTest.cs: * AVException.cs:
parent
043f8e2d88
commit
b85babb38b
|
@ -1181,7 +1181,7 @@ string propertyName
|
||||||
// The task produced by taskStart. By running this immediately, we allow everything prior
|
// The task produced by taskStart. By running this immediately, we allow everything prior
|
||||||
// to toAwait to run before waiting for all of the queues on all of the objects.
|
// to toAwait to run before waiting for all of the queues on all of the objects.
|
||||||
Task<T> fullTask = taskStart(readyToStart.Task);
|
Task<T> fullTask = taskStart(readyToStart.Task);
|
||||||
|
|
||||||
// Add fullTask to each of the objects' queues.
|
// Add fullTask to each of the objects' queues.
|
||||||
var childTasks = new List<Task>();
|
var childTasks = new List<Task>();
|
||||||
foreach (AVObject obj in objects)
|
foreach (AVObject obj in objects)
|
||||||
|
|
|
@ -0,0 +1,189 @@
|
||||||
|
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<string, object> Data {
|
||||||
|
get; set;
|
||||||
|
}
|
||||||
|
|
||||||
|
public LCObject(string id) {
|
||||||
|
Data = new Dictionary<string, object>();
|
||||||
|
Id = id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Stack<Batch> Batch(IEnumerable<LCObject> objects) {
|
||||||
|
Stack<Batch> batches = new Stack<Batch>();
|
||||||
|
|
||||||
|
IEnumerable<object> deps = objects;
|
||||||
|
do {
|
||||||
|
// 只添加本层依赖的 LCObject
|
||||||
|
IEnumerable<LCObject> avObjects = deps.OfType<LCObject>();
|
||||||
|
if (avObjects.Any()) {
|
||||||
|
batches.Push(new Batch(avObjects));
|
||||||
|
}
|
||||||
|
|
||||||
|
HashSet<object> childSets = new HashSet<object>();
|
||||||
|
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<LCObject> 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<LCObject> depParent = new HashSet<LCObject>(parents);
|
||||||
|
if (obj is LCObject) {
|
||||||
|
depParent.Add((LCObject) obj);
|
||||||
|
}
|
||||||
|
if (deps != null) {
|
||||||
|
foreach (object dep in deps) {
|
||||||
|
HashSet<LCObject> set = new HashSet<LCObject>(depParent);
|
||||||
|
if (HasCircleReference(dep, set)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Stack<Batch> Batch() {
|
||||||
|
return Batch(new List<LCObject> { this });
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool HasCircleReference() {
|
||||||
|
return HasCircleReference(this, new HashSet<LCObject>());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class Batch {
|
||||||
|
HashSet<LCObject> ObjectSet {
|
||||||
|
get; set;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Batch() {
|
||||||
|
ObjectSet = new HashSet<LCObject>();
|
||||||
|
}
|
||||||
|
|
||||||
|
public Batch(IEnumerable<LCObject> 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<Batch> 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<Batch> 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<LCObject> { b, c };
|
||||||
|
|
||||||
|
Assert.IsFalse(a.HasCircleReference());
|
||||||
|
|
||||||
|
Stack<Batch> 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<object> { 1, b };
|
||||||
|
a.Data["child"] = c;
|
||||||
|
b.Data["arr"] = new List<object> { 2, a };
|
||||||
|
|
||||||
|
Assert.IsTrue(a.HasCircleReference());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void ComplexCircleReference2() {
|
||||||
|
LCObject a = new LCObject("a");
|
||||||
|
LCObject b = new LCObject("b");
|
||||||
|
List<object> list = new List<object>();
|
||||||
|
a.Data["list"] = list;
|
||||||
|
b.Data["list"] = list;
|
||||||
|
a.Data["child"] = b;
|
||||||
|
|
||||||
|
Assert.IsFalse(a.HasCircleReference());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -3,14 +3,21 @@ using System;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using LeanCloud.Common;
|
||||||
|
|
||||||
namespace LeanCloud.Test {
|
namespace LeanCloud.Test {
|
||||||
public class ObjectTests {
|
public class ObjectTests {
|
||||||
[SetUp]
|
[SetUp]
|
||||||
public void SetUp() {
|
public void SetUp() {
|
||||||
|
Logger.LogDelegate += Utils.Print;
|
||||||
Utils.InitNorthChina();
|
Utils.InitNorthChina();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[TearDown]
|
||||||
|
public void TearDown() {
|
||||||
|
Logger.LogDelegate -= Utils.Print;
|
||||||
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
public async Task Save() {
|
public async Task Save() {
|
||||||
AVObject obj = AVObject.Create("Foo");
|
AVObject obj = AVObject.Create("Foo");
|
||||||
|
@ -20,7 +27,7 @@ namespace LeanCloud.Test {
|
||||||
{ "hello", 1 },
|
{ "hello", 1 },
|
||||||
{ "world", 2 }
|
{ "world", 2 }
|
||||||
};
|
};
|
||||||
await obj.SaveAsync();
|
await obj.Save();
|
||||||
Assert.NotNull(obj.ObjectId);
|
Assert.NotNull(obj.ObjectId);
|
||||||
Assert.NotNull(obj.CreatedAt);
|
Assert.NotNull(obj.CreatedAt);
|
||||||
Assert.NotNull(obj.UpdatedAt);
|
Assert.NotNull(obj.UpdatedAt);
|
||||||
|
@ -38,32 +45,57 @@ namespace LeanCloud.Test {
|
||||||
TestContext.Out.WriteLine($"balance: {account["balance"]}");
|
TestContext.Out.WriteLine($"balance: {account["balance"]}");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//[Test]
|
||||||
|
//public async Task SaveWithPointer() {
|
||||||
|
// AVObject comment = new AVObject("Comment") {
|
||||||
|
// { "content", "Hello, Comment" }
|
||||||
|
// };
|
||||||
|
|
||||||
|
// AVObject post = new AVObject("Post") {
|
||||||
|
// { "name", "New Post" },
|
||||||
|
// { "category", new AVObject("Category") {
|
||||||
|
// { "name", "new post category" }
|
||||||
|
// } }
|
||||||
|
// };
|
||||||
|
// comment["post"] = post;
|
||||||
|
|
||||||
|
// AVObject testPost = new AVObject("Post") {
|
||||||
|
// { "name", "Test Post" },
|
||||||
|
// { "category", new AVObject("Category") {
|
||||||
|
// { "name", "test post category" }
|
||||||
|
// } }
|
||||||
|
// };
|
||||||
|
// comment["test_post"] = testPost;
|
||||||
|
|
||||||
|
// await comment.Save();
|
||||||
|
// TestContext.Out.WriteLine(post);
|
||||||
|
// TestContext.Out.WriteLine(testPost);
|
||||||
|
// TestContext.Out.WriteLine(comment);
|
||||||
|
//}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
public async Task SaveWithPointer() {
|
public async Task SaveWithPointer() {
|
||||||
AVObject comment = new AVObject("Comment") {
|
AVObject parent = new AVObject("Parent");
|
||||||
{ "content", "Hello, Comment" }
|
AVObject c1 = new AVObject("C1");
|
||||||
};
|
AVObject c2 = new AVObject("C2");
|
||||||
|
parent["c1"] = c1;
|
||||||
|
parent["c2"] = c2;
|
||||||
|
await parent.Save();
|
||||||
|
}
|
||||||
|
|
||||||
AVObject post = new AVObject("Post") {
|
[Test]
|
||||||
{ "name", "New Post" },
|
public async Task SaveWithPointerArray() {
|
||||||
{ "category", new AVObject("Category") {
|
AVObject parent = new AVObject("Parent");
|
||||||
{ "name", "new post category" }
|
AVObject c1 = new AVObject("C1");
|
||||||
} }
|
AVObject c2 = new AVObject("C2");
|
||||||
|
parent["iList"] = new List<int> { 1, 1, 2, 3 };
|
||||||
|
parent["cList"] = new List<AVObject> { c1, c2 };
|
||||||
|
parent["cDict"] = new Dictionary<string, AVObject> {
|
||||||
|
{ "c1", c1 },
|
||||||
|
{ "c2", c2 }
|
||||||
};
|
};
|
||||||
comment["post"] = post;
|
await parent.SaveAsync();
|
||||||
|
|
||||||
AVObject testPost = new AVObject("Post") {
|
|
||||||
{ "name", "Test Post" },
|
|
||||||
{ "category", new AVObject("Category") {
|
|
||||||
{ "name", "test post category" }
|
|
||||||
} }
|
|
||||||
};
|
|
||||||
comment["test_post"] = testPost;
|
|
||||||
|
|
||||||
await comment.SaveAsync();
|
|
||||||
TestContext.Out.WriteLine(post);
|
|
||||||
TestContext.Out.WriteLine(testPost);
|
|
||||||
TestContext.Out.WriteLine(comment);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
|
@ -199,5 +231,54 @@ namespace LeanCloud.Test {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void SimpleCircleReference() {
|
||||||
|
AVObject a = new AVObject("A");
|
||||||
|
AVObject b = new AVObject("B");
|
||||||
|
a["b"] = b;
|
||||||
|
b["a"] = a;
|
||||||
|
|
||||||
|
Assert.ThrowsAsync<AVException>(async () => await a.Save());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void IndirectCircleReference() {
|
||||||
|
AVObject a = new AVObject("A");
|
||||||
|
AVObject b = new AVObject("B");
|
||||||
|
AVObject c = new AVObject("C");
|
||||||
|
a["b"] = b;
|
||||||
|
b["c"] = c;
|
||||||
|
c["a"] = a;
|
||||||
|
|
||||||
|
Assert.ThrowsAsync<AVException>(async () => await a.Save());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void SimpleCollectionPointerCircleReference() {
|
||||||
|
AVObject a = new AVObject("A");
|
||||||
|
AVObject b = new AVObject("B");
|
||||||
|
a["children"] = new List<object> { 1, b };
|
||||||
|
b["children"] = new Dictionary<string, object> {
|
||||||
|
{ "c", a }
|
||||||
|
};
|
||||||
|
|
||||||
|
Assert.ThrowsAsync<AVException>(async () => await a.Save());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void IndirectCollectionPointerCircleReference() {
|
||||||
|
AVObject a = new AVObject("A");
|
||||||
|
AVObject b = new AVObject("B");
|
||||||
|
AVObject c = new AVObject("C");
|
||||||
|
|
||||||
|
a["children"] = new List<object> { 1, b };
|
||||||
|
b["children"] = new List<object> { 2, c };
|
||||||
|
c["children"] = new Dictionary<string, object> {
|
||||||
|
{ "c", a }
|
||||||
|
};
|
||||||
|
|
||||||
|
Assert.ThrowsAsync<AVException>(async () => await a.Save());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -1,5 +1,6 @@
|
||||||
using System;
|
using System;
|
||||||
using LeanCloud;
|
using LeanCloud;
|
||||||
|
using LeanCloud.Common;
|
||||||
using NUnit.Framework;
|
using NUnit.Framework;
|
||||||
|
|
||||||
namespace LeanCloud.Test {
|
namespace LeanCloud.Test {
|
||||||
|
@ -47,5 +48,22 @@ namespace LeanCloud.Test {
|
||||||
AVClient.UseMasterKey = !string.IsNullOrEmpty(masterKey);
|
AVClient.UseMasterKey = !string.IsNullOrEmpty(masterKey);
|
||||||
AVClient.HttpLog(TestContext.Out.WriteLine);
|
AVClient.HttpLog(TestContext.Out.WriteLine);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
internal static void Print(LogLevel level, string info) {
|
||||||
|
switch (level) {
|
||||||
|
case LogLevel.Debug:
|
||||||
|
TestContext.Out.WriteLine($"[DEBUG] {info}");
|
||||||
|
break;
|
||||||
|
case LogLevel.Warn:
|
||||||
|
TestContext.Out.WriteLine($"[WARNING] {info}");
|
||||||
|
break;
|
||||||
|
case LogLevel.Error:
|
||||||
|
TestContext.Out.WriteLine($"[ERROR] {info}");
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
TestContext.Out.WriteLine(info);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -264,6 +264,11 @@ namespace LeanCloud
|
||||||
/// 按条件更新/删除失败
|
/// 按条件更新/删除失败
|
||||||
/// </summary>
|
/// </summary>
|
||||||
NoEffectOnUpdatingOrDeleting = 305,
|
NoEffectOnUpdatingOrDeleting = 305,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 循环引用
|
||||||
|
/// </summary>
|
||||||
|
CircleReference = 400,
|
||||||
}
|
}
|
||||||
|
|
||||||
internal AVException(ErrorCode code, string message, Exception cause = null)
|
internal AVException(ErrorCode code, string message, Exception cause = null)
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
using LeanCloud.Storage.Internal;
|
using LeanCloud.Storage.Internal;
|
||||||
using LeanCloud.Utilities;
|
using LeanCloud.Utilities;
|
||||||
using System;
|
using System;
|
||||||
|
using System.Text;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.ComponentModel;
|
using System.ComponentModel;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
@ -8,19 +9,49 @@ using System.Runtime.CompilerServices;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using System.Collections;
|
using System.Collections;
|
||||||
|
using System.Collections.Concurrent;
|
||||||
|
|
||||||
namespace LeanCloud {
|
namespace LeanCloud {
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// AVObject
|
/// AVObject
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class AVObject : IEnumerable<KeyValuePair<string, object>>, INotifyPropertyChanged, INotifyPropertyUpdated, INotifyCollectionPropertyUpdated {
|
public class AVObject : IEnumerable<KeyValuePair<string, object>>, INotifyPropertyChanged, INotifyPropertyUpdated, INotifyCollectionPropertyUpdated {
|
||||||
|
internal class Batch {
|
||||||
|
internal HashSet<AVObject> Objects {
|
||||||
|
get; set;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Batch() {
|
||||||
|
Objects = new HashSet<AVObject>();
|
||||||
|
}
|
||||||
|
|
||||||
|
public Batch(IEnumerable<AVObject> objects) : this() {
|
||||||
|
foreach (AVObject obj in objects) {
|
||||||
|
Objects.Add(obj);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public override string ToString() {
|
||||||
|
StringBuilder sb = new StringBuilder();
|
||||||
|
sb.AppendLine("----------------------------");
|
||||||
|
foreach (AVObject obj in Objects) {
|
||||||
|
sb.AppendLine(obj.ClassName);
|
||||||
|
}
|
||||||
|
sb.AppendLine("----------------------------");
|
||||||
|
return sb.ToString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
private static readonly string AutoClassName = "_Automatic";
|
private static readonly string AutoClassName = "_Automatic";
|
||||||
|
|
||||||
internal readonly object mutex = new object();
|
internal readonly object mutex = new object();
|
||||||
|
|
||||||
private readonly LinkedList<IDictionary<string, IAVFieldOperation>> operationSetQueue =
|
private readonly LinkedList<IDictionary<string, IAVFieldOperation>> operationSetQueue =
|
||||||
new LinkedList<IDictionary<string, IAVFieldOperation>>();
|
new LinkedList<IDictionary<string, IAVFieldOperation>>();
|
||||||
private readonly IDictionary<string, object> estimatedData = new Dictionary<string, object>();
|
|
||||||
|
private readonly ConcurrentDictionary<string, IAVFieldOperation> operationDict = new ConcurrentDictionary<string, IAVFieldOperation>();
|
||||||
|
private readonly ConcurrentDictionary<string, object> estimatedData = new ConcurrentDictionary<string, object>();
|
||||||
|
|
||||||
private static readonly ThreadLocal<bool> isCreatingPointer = new ThreadLocal<bool>(() => false);
|
private static readonly ThreadLocal<bool> isCreatingPointer = new ThreadLocal<bool>(() => false);
|
||||||
|
|
||||||
|
@ -210,7 +241,7 @@ namespace LeanCloud {
|
||||||
this[GetFieldForPropertyName(ClassName, propertyName)] = value;
|
this[GetFieldForPropertyName(ClassName, propertyName)] = value;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summar、y>
|
||||||
/// Gets a relation for a property based upon its associated AVFieldName attribute.
|
/// Gets a relation for a property based upon its associated AVFieldName attribute.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <returns>The AVRelation for the property.</returns>
|
/// <returns>The AVRelation for the property.</returns>
|
||||||
|
@ -974,9 +1005,9 @@ string propertyName
|
||||||
if (oldValue != value) {
|
if (oldValue != value) {
|
||||||
converdKeys.Add(key);
|
converdKeys.Add(key);
|
||||||
}
|
}
|
||||||
estimatedData.Remove(key);
|
estimatedData.TryRemove(key, out _);
|
||||||
}
|
}
|
||||||
estimatedData.Add(item);
|
estimatedData.TryAdd(item.Key, item.Value);
|
||||||
}
|
}
|
||||||
changedKeys = converdKeys;
|
changedKeys = converdKeys;
|
||||||
foreach (var operations in operationSetQueue) {
|
foreach (var operations in operationSetQueue) {
|
||||||
|
@ -1003,7 +1034,7 @@ string propertyName
|
||||||
if (newValue != AVDeleteOperation.DeleteToken) {
|
if (newValue != AVDeleteOperation.DeleteToken) {
|
||||||
estimatedData[key] = newValue;
|
estimatedData[key] = newValue;
|
||||||
} else {
|
} else {
|
||||||
estimatedData.Remove(key);
|
estimatedData.TryRemove(key, out _);
|
||||||
}
|
}
|
||||||
|
|
||||||
IAVFieldOperation oldOperation;
|
IAVFieldOperation oldOperation;
|
||||||
|
@ -1622,6 +1653,106 @@ string propertyName
|
||||||
protected virtual void OnCollectionPropertyUpdated(string propertyName, NotifyCollectionUpdatedAction action, IEnumerable oldValues, IEnumerable newValues) {
|
protected virtual void OnCollectionPropertyUpdated(string propertyName, NotifyCollectionUpdatedAction action, IEnumerable oldValues, IEnumerable newValues) {
|
||||||
collectionUpdated?.Invoke(this, new CollectionPropertyUpdatedEventArgs(propertyName, action, oldValues, newValues));
|
collectionUpdated?.Invoke(this, new CollectionPropertyUpdatedEventArgs(propertyName, action, oldValues, newValues));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
#region refactor
|
||||||
|
|
||||||
|
static bool HasCircleReference(object obj, HashSet<AVObject> 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 AVObject) {
|
||||||
|
deps = (obj as AVObject).estimatedData.Values;
|
||||||
|
}
|
||||||
|
HashSet<AVObject> depParent = new HashSet<AVObject>(parents);
|
||||||
|
if (obj is AVObject) {
|
||||||
|
depParent.Add(obj as AVObject);
|
||||||
|
}
|
||||||
|
if (deps != null) {
|
||||||
|
foreach (object dep in deps) {
|
||||||
|
HashSet<AVObject> p = new HashSet<AVObject>(depParent);
|
||||||
|
if (HasCircleReference(dep, p)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
static Stack<Batch> BatchObjects(IEnumerable<AVObject> avObjects) {
|
||||||
|
Stack<Batch> batches = new Stack<Batch>();
|
||||||
|
|
||||||
|
IEnumerable<object> deps = avObjects;
|
||||||
|
do {
|
||||||
|
// 只添加本层依赖的 LCObject
|
||||||
|
IEnumerable<AVObject> depAVObjs = deps.OfType<AVObject>();
|
||||||
|
if (depAVObjs.Any()) {
|
||||||
|
batches.Push(new Batch(depAVObjs));
|
||||||
|
}
|
||||||
|
|
||||||
|
HashSet<object> childSets = new HashSet<object>();
|
||||||
|
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).Values;
|
||||||
|
} else if (dep is AVObject && (dep as AVObject).ObjectId == null) {
|
||||||
|
// 如果依赖是 AVObject 类型并且还没有保存过,则应该遍历其依赖
|
||||||
|
children = (dep as AVObject).estimatedData.Values;
|
||||||
|
}
|
||||||
|
if (children != null) {
|
||||||
|
foreach (object child in children) {
|
||||||
|
childSets.Add(child);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
deps = childSets;
|
||||||
|
} while (deps != null && deps.Any());
|
||||||
|
|
||||||
|
return batches;
|
||||||
|
}
|
||||||
|
|
||||||
|
public virtual async Task Save() {
|
||||||
|
if (HasCircleReference(this, new HashSet<AVObject>())) {
|
||||||
|
throw new AVException(AVException.ErrorCode.CircleReference, "Found a circle dependency when save");
|
||||||
|
}
|
||||||
|
Stack<Batch> batches = BatchObjects(new List<AVObject> { this });
|
||||||
|
while (batches.Any()) {
|
||||||
|
Batch batch = batches.Pop();
|
||||||
|
IList<AVObject> dirtyObjects = batch.Objects.Where(o => o.IsDirty).ToList();
|
||||||
|
IList<IObjectState> states = (from item in dirtyObjects
|
||||||
|
select item.state).ToList();
|
||||||
|
IList<IDictionary<string, IAVFieldOperation>> operationList = (from item in dirtyObjects
|
||||||
|
select item.StartSave()).ToList();
|
||||||
|
var serverStates = await ObjectController.SaveAllAsync(states, operationList, CancellationToken.None);
|
||||||
|
|
||||||
|
try {
|
||||||
|
foreach (var pair in dirtyObjects.Zip(serverStates, (item, state) => new { item, state })) {
|
||||||
|
pair.item.HandleSave(pair.state);
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
foreach (var pair in dirtyObjects.Zip(operationList, (item, ops) => new { item, ops })) {
|
||||||
|
pair.item.HandleFailedSave(pair.ops);
|
||||||
|
}
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
}
|
}
|
||||||
|
|
||||||
public interface INotifyPropertyUpdated {
|
public interface INotifyPropertyUpdated {
|
||||||
|
|
|
@ -1,9 +1,10 @@
|
||||||
|
using System;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using NUnit.Framework;
|
using NUnit.Framework;
|
||||||
using LeanCloud.Common;
|
using LeanCloud.Common;
|
||||||
|
|
||||||
namespace Common.Test {
|
namespace Common.Test {
|
||||||
public class Tests {
|
public class AppRouterTest {
|
||||||
static void Print(LogLevel level, string info) {
|
static void Print(LogLevel level, string info) {
|
||||||
switch (level) {
|
switch (level) {
|
||||||
case LogLevel.Debug:
|
case LogLevel.Debug:
|
||||||
|
@ -23,23 +24,43 @@ namespace Common.Test {
|
||||||
|
|
||||||
[SetUp]
|
[SetUp]
|
||||||
public void SetUp() {
|
public void SetUp() {
|
||||||
TestContext.Out.WriteLine("Set up");
|
|
||||||
Logger.LogDelegate += Print;
|
Logger.LogDelegate += Print;
|
||||||
}
|
}
|
||||||
|
|
||||||
[TearDown]
|
[TearDown]
|
||||||
public void TearDown() {
|
public void TearDown() {
|
||||||
TestContext.Out.WriteLine("Tear down");
|
|
||||||
Logger.LogDelegate -= Print;
|
Logger.LogDelegate -= Print;
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
public async Task AppRouter() {
|
public void ChineseApp() {
|
||||||
var appRouter = new AppRouterController();
|
Exception e = Assert.Catch(() => {
|
||||||
for (int i = 0; i < 100; i++) {
|
string appId = "BMYV4RKSTwo8WSqt8q9ezcWF-gzGzoHsz";
|
||||||
var state = await appRouter.Get("BMYV4RKSTwo8WSqt8q9ezcWF-gzGzoHsz");
|
AppRouterController appRouter = new AppRouterController(appId, null);
|
||||||
TestContext.Out.WriteLine(state.ApiServer);
|
TestContext.WriteLine("init done");
|
||||||
}
|
});
|
||||||
|
TestContext.WriteLine(e.Message);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public async Task ChineseAppWithDomain() {
|
||||||
|
string appId = "BMYV4RKSTwo8WSqt8q9ezcWF-gzGzoHsz";
|
||||||
|
string server = "https://bmyv4rks.lc-cn-n1-shared.com";
|
||||||
|
AppRouterController appRouterController = new AppRouterController(appId, server);
|
||||||
|
AppRouter appRouterState = await appRouterController.Get();
|
||||||
|
Assert.AreEqual(appRouterState.ApiServer, server);
|
||||||
|
Assert.AreEqual(appRouterState.EngineServer, server);
|
||||||
|
Assert.AreEqual(appRouterState.PushServer, server);
|
||||||
|
Assert.AreEqual(appRouterState.RTMServer, server);
|
||||||
|
Assert.AreEqual(appRouterState.StatsServer, server);
|
||||||
|
Assert.AreEqual(appRouterState.PlayServer, server);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void InternationalApp() {
|
||||||
|
string appId = "BMYV4RKSTwo8WSqt8q9ezcWF-MdYXbMMI";
|
||||||
|
_ = new AppRouterController(appId, null);
|
||||||
|
TestContext.WriteLine("International app init done");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -4,6 +4,7 @@
|
||||||
<TargetFramework>netcoreapp3.0</TargetFramework>
|
<TargetFramework>netcoreapp3.0</TargetFramework>
|
||||||
|
|
||||||
<IsPackable>false</IsPackable>
|
<IsPackable>false</IsPackable>
|
||||||
|
<ReleaseVersion>0.1.0</ReleaseVersion>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|
|
@ -1,7 +1,73 @@
|
||||||
using System;
|
using System;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Collections;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Collections.Concurrent;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using NUnit.Framework;
|
||||||
|
|
||||||
namespace Common.Test {
|
namespace Common.Test {
|
||||||
public class Test {
|
public class Test {
|
||||||
public Test() {
|
[Test]
|
||||||
|
public async Task AsyncFor() {
|
||||||
|
for (int i = 0; i < 5; i++) {
|
||||||
|
await Task.Delay(1000);
|
||||||
|
TestContext.WriteLine($"{i} done at {DateTimeOffset.UtcNow}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void ConcurrentCollection() {
|
||||||
|
List<int> list = new List<int>();
|
||||||
|
for (int i = 0; i < 1000; i++) {
|
||||||
|
Task.Run(() => {
|
||||||
|
list.Add(i);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
TestContext.WriteLine($"{list.Count}");
|
||||||
|
|
||||||
|
ConcurrentQueue<int> queue = new ConcurrentQueue<int>();
|
||||||
|
for (int i = 0; i < 1000; i++) {
|
||||||
|
Task.Run(() => {
|
||||||
|
queue.Enqueue(i);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
TestContext.WriteLine($"{queue.Count}");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void ObjectType() {
|
||||||
|
List<object> list = new List<object> { 1, "hello", 2, "world" };
|
||||||
|
TestContext.WriteLine(list is IList);
|
||||||
|
object[] objs = { 1, "hi", 3 };
|
||||||
|
TestContext.WriteLine(objs is IList);
|
||||||
|
List<object> subList = list.OfType<string>().ToList<object>();
|
||||||
|
foreach (object obj in subList) {
|
||||||
|
TestContext.WriteLine(obj);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void CollectionExcept() {
|
||||||
|
List<int> list1 = new List<int> { 1, 2, 3, 4, 5 };
|
||||||
|
List<int> list2 = new List<int> { 4, 5, 6 };
|
||||||
|
IEnumerable<int> deltaList = list1.Except(list2).ToList();
|
||||||
|
foreach (int delta in deltaList) {
|
||||||
|
TestContext.WriteLine(delta);
|
||||||
|
}
|
||||||
|
|
||||||
|
Dictionary<string, object> dict1 = new Dictionary<string, object> {
|
||||||
|
{ "a", 1 },
|
||||||
|
{ "b", 2 }
|
||||||
|
};
|
||||||
|
Dictionary<string, object> dict2 = new Dictionary<string, object> {
|
||||||
|
{ "b", 2 },
|
||||||
|
{ "c", 3 }
|
||||||
|
};
|
||||||
|
IEnumerable<KeyValuePair<string, object>> deltaDict = dict1.Except(dict2);
|
||||||
|
foreach (KeyValuePair<string, object> delta in deltaDict) {
|
||||||
|
TestContext.WriteLine($"{delta.Key} : {delta.Value}");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -31,12 +31,12 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Storage", "Storage\Storage\
|
||||||
EndProject
|
EndProject
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RTM", "RTM\RTM\RTM.csproj", "{D4A30F70-AAED-415D-B940-023B3D7241EE}"
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RTM", "RTM\RTM\RTM.csproj", "{D4A30F70-AAED-415D-B940-023B3D7241EE}"
|
||||||
EndProject
|
EndProject
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Common", "Common\Common.csproj", "{14EC150A-EF90-4E0B-B6D7-C2CF1945F6E5}"
|
|
||||||
EndProject
|
|
||||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Test", "Test", "{C827DA2F-6AB4-48D8-AB5B-6DAB925F8933}"
|
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Test", "Test", "{C827DA2F-6AB4-48D8-AB5B-6DAB925F8933}"
|
||||||
EndProject
|
EndProject
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Common.Test", "Test\Common.Test\Common.Test.csproj", "{4DF4E0F4-1013-477F-ADA6-BFAFD9312335}"
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Common.Test", "Test\Common.Test\Common.Test.csproj", "{4DF4E0F4-1013-477F-ADA6-BFAFD9312335}"
|
||||||
EndProject
|
EndProject
|
||||||
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Common", "Common\Common.csproj", "{758DE75D-37D7-4392-B564-9484348B505C}"
|
||||||
|
EndProject
|
||||||
Global
|
Global
|
||||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||||
Debug|Any CPU = Debug|Any CPU
|
Debug|Any CPU = Debug|Any CPU
|
||||||
|
@ -87,14 +87,14 @@ Global
|
||||||
{D4A30F70-AAED-415D-B940-023B3D7241EE}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
{D4A30F70-AAED-415D-B940-023B3D7241EE}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
{D4A30F70-AAED-415D-B940-023B3D7241EE}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
{D4A30F70-AAED-415D-B940-023B3D7241EE}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
{D4A30F70-AAED-415D-B940-023B3D7241EE}.Release|Any CPU.Build.0 = Release|Any CPU
|
{D4A30F70-AAED-415D-B940-023B3D7241EE}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
{14EC150A-EF90-4E0B-B6D7-C2CF1945F6E5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
|
||||||
{14EC150A-EF90-4E0B-B6D7-C2CF1945F6E5}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
|
||||||
{14EC150A-EF90-4E0B-B6D7-C2CF1945F6E5}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
|
||||||
{14EC150A-EF90-4E0B-B6D7-C2CF1945F6E5}.Release|Any CPU.Build.0 = Release|Any CPU
|
|
||||||
{4DF4E0F4-1013-477F-ADA6-BFAFD9312335}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
{4DF4E0F4-1013-477F-ADA6-BFAFD9312335}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
{4DF4E0F4-1013-477F-ADA6-BFAFD9312335}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
{4DF4E0F4-1013-477F-ADA6-BFAFD9312335}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
{4DF4E0F4-1013-477F-ADA6-BFAFD9312335}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
{4DF4E0F4-1013-477F-ADA6-BFAFD9312335}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
{4DF4E0F4-1013-477F-ADA6-BFAFD9312335}.Release|Any CPU.Build.0 = Release|Any CPU
|
{4DF4E0F4-1013-477F-ADA6-BFAFD9312335}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{758DE75D-37D7-4392-B564-9484348B505C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{758DE75D-37D7-4392-B564-9484348B505C}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{758DE75D-37D7-4392-B564-9484348B505C}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{758DE75D-37D7-4392-B564-9484348B505C}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
EndGlobalSection
|
EndGlobalSection
|
||||||
GlobalSection(NestedProjects) = preSolution
|
GlobalSection(NestedProjects) = preSolution
|
||||||
{659D19F0-9A40-42C0-886C-555E64F16848} = {CD6B6669-1A56-437A-932E-BCE7F5D4CD18}
|
{659D19F0-9A40-42C0-886C-555E64F16848} = {CD6B6669-1A56-437A-932E-BCE7F5D4CD18}
|
||||||
|
@ -105,9 +105,7 @@ Global
|
||||||
{EA1C601E-D853-41F7-B9EB-276CBF7D1FA5} = {5B895B7A-1F6E-40A5-8081-43B334D2C076}
|
{EA1C601E-D853-41F7-B9EB-276CBF7D1FA5} = {5B895B7A-1F6E-40A5-8081-43B334D2C076}
|
||||||
{3251B4D8-D11A-4D90-8626-27FEE266B066} = {5B895B7A-1F6E-40A5-8081-43B334D2C076}
|
{3251B4D8-D11A-4D90-8626-27FEE266B066} = {5B895B7A-1F6E-40A5-8081-43B334D2C076}
|
||||||
{F907012C-74DF-4575-AFE6-E8DAACC26D24} = {5B895B7A-1F6E-40A5-8081-43B334D2C076}
|
{F907012C-74DF-4575-AFE6-E8DAACC26D24} = {5B895B7A-1F6E-40A5-8081-43B334D2C076}
|
||||||
{BE05B492-78CD-47CA-9F48-C3E9B4813AFF} = {CD6B6669-1A56-437A-932E-BCE7F5D4CD18}
|
{BE05B492-78CD-47CA-9F48-C3E9B4813AFF} = {C827DA2F-6AB4-48D8-AB5B-6DAB925F8933}
|
||||||
{59DA32A0-4CD3-424A-8584-D08B8D1E2B98} = {CD6B6669-1A56-437A-932E-BCE7F5D4CD18}
|
|
||||||
{D4A30F70-AAED-415D-B940-023B3D7241EE} = {64D8F9A1-BA44-459C-817C-788B4EBC0B9F}
|
|
||||||
{4DF4E0F4-1013-477F-ADA6-BFAFD9312335} = {C827DA2F-6AB4-48D8-AB5B-6DAB925F8933}
|
{4DF4E0F4-1013-477F-ADA6-BFAFD9312335} = {C827DA2F-6AB4-48D8-AB5B-6DAB925F8933}
|
||||||
EndGlobalSection
|
EndGlobalSection
|
||||||
GlobalSection(MonoDevelopProperties) = preSolution
|
GlobalSection(MonoDevelopProperties) = preSolution
|
||||||
|
|
Loading…
Reference in New Issue