* csharp-sdk.sln: chore: 循环引用检测及测试;保存批次算法调整

* Test.cs:
* Utils.cs:
* AVObject.cs:
* AppRouterTest.cs:
* ObjectTest.cs:
* AVObject.cs:
* Common.Test.csproj:
* AVObjectTest.cs:
* AVException.cs:
oneRain 2019-12-05 16:06:54 +08:00
parent 043f8e2d88
commit b85babb38b
10 changed files with 557 additions and 47 deletions

View File

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

View File

@ -3,14 +3,21 @@ using System;
using System.Threading;
using System.Threading.Tasks;
using System.Collections.Generic;
using LeanCloud.Common;
namespace LeanCloud.Test {
public class ObjectTests {
[SetUp]
public void SetUp() {
Logger.LogDelegate += Utils.Print;
Utils.InitNorthChina();
}
[TearDown]
public void TearDown() {
Logger.LogDelegate -= Utils.Print;
}
[Test]
public async Task Save() {
AVObject obj = AVObject.Create("Foo");
@ -20,7 +27,7 @@ namespace LeanCloud.Test {
{ "hello", 1 },
{ "world", 2 }
};
await obj.SaveAsync();
await obj.Save();
Assert.NotNull(obj.ObjectId);
Assert.NotNull(obj.CreatedAt);
Assert.NotNull(obj.UpdatedAt);
@ -38,32 +45,57 @@ namespace LeanCloud.Test {
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]
public async Task SaveWithPointer() {
AVObject comment = new AVObject("Comment") {
{ "content", "Hello, Comment" }
};
AVObject parent = new AVObject("Parent");
AVObject c1 = new AVObject("C1");
AVObject c2 = new AVObject("C2");
parent["c1"] = c1;
parent["c2"] = c2;
await parent.Save();
}
AVObject post = new AVObject("Post") {
{ "name", "New Post" },
{ "category", new AVObject("Category") {
{ "name", "new post category" }
} }
[Test]
public async Task SaveWithPointerArray() {
AVObject parent = new AVObject("Parent");
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]
@ -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());
}
}
}

View File

@ -1,5 +1,6 @@
using System;
using LeanCloud;
using LeanCloud.Common;
using NUnit.Framework;
namespace LeanCloud.Test {
@ -47,5 +48,22 @@ namespace LeanCloud.Test {
AVClient.UseMasterKey = !string.IsNullOrEmpty(masterKey);
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;
}
}
}
}

View File

@ -264,6 +264,11 @@ namespace LeanCloud
/// 按条件更新/删除失败
/// </summary>
NoEffectOnUpdatingOrDeleting = 305,
/// <summary>
/// 循环引用
/// </summary>
CircleReference = 400,
}
internal AVException(ErrorCode code, string message, Exception cause = null)

View File

@ -1,6 +1,7 @@
using LeanCloud.Storage.Internal;
using LeanCloud.Utilities;
using System;
using System.Text;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
@ -8,19 +9,49 @@ using System.Runtime.CompilerServices;
using System.Threading;
using System.Threading.Tasks;
using System.Collections;
using System.Collections.Concurrent;
namespace LeanCloud {
/// <summary>
/// AVObject
/// </summary>
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";
internal readonly object mutex = new object();
private readonly LinkedList<IDictionary<string, IAVFieldOperation>> operationSetQueue =
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);
@ -210,7 +241,7 @@ namespace LeanCloud {
this[GetFieldForPropertyName(ClassName, propertyName)] = value;
}
/// <summary>
/// <summary>
/// Gets a relation for a property based upon its associated AVFieldName attribute.
/// </summary>
/// <returns>The AVRelation for the property.</returns>
@ -974,9 +1005,9 @@ string propertyName
if (oldValue != value) {
converdKeys.Add(key);
}
estimatedData.Remove(key);
estimatedData.TryRemove(key, out _);
}
estimatedData.Add(item);
estimatedData.TryAdd(item.Key, item.Value);
}
changedKeys = converdKeys;
foreach (var operations in operationSetQueue) {
@ -1003,7 +1034,7 @@ string propertyName
if (newValue != AVDeleteOperation.DeleteToken) {
estimatedData[key] = newValue;
} else {
estimatedData.Remove(key);
estimatedData.TryRemove(key, out _);
}
IAVFieldOperation oldOperation;
@ -1622,6 +1653,106 @@ string propertyName
protected virtual void OnCollectionPropertyUpdated(string propertyName, NotifyCollectionUpdatedAction action, IEnumerable oldValues, IEnumerable 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 {

View File

@ -1,9 +1,10 @@
using System;
using System.Threading.Tasks;
using NUnit.Framework;
using LeanCloud.Common;
namespace Common.Test {
public class Tests {
public class AppRouterTest {
static void Print(LogLevel level, string info) {
switch (level) {
case LogLevel.Debug:
@ -23,23 +24,43 @@ namespace Common.Test {
[SetUp]
public void SetUp() {
TestContext.Out.WriteLine("Set up");
Logger.LogDelegate += Print;
}
[TearDown]
public void TearDown() {
TestContext.Out.WriteLine("Tear down");
Logger.LogDelegate -= Print;
}
[Test]
public async Task AppRouter() {
var appRouter = new AppRouterController();
for (int i = 0; i < 100; i++) {
var state = await appRouter.Get("BMYV4RKSTwo8WSqt8q9ezcWF-gzGzoHsz");
TestContext.Out.WriteLine(state.ApiServer);
public void ChineseApp() {
Exception e = Assert.Catch(() => {
string appId = "BMYV4RKSTwo8WSqt8q9ezcWF-gzGzoHsz";
AppRouterController appRouter = new AppRouterController(appId, null);
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");
}
}
}

View File

@ -4,6 +4,7 @@
<TargetFramework>netcoreapp3.0</TargetFramework>
<IsPackable>false</IsPackable>
<ReleaseVersion>0.1.0</ReleaseVersion>
</PropertyGroup>
<ItemGroup>

View File

@ -1,7 +1,73 @@
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 {
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}");
}
}
}
}

View File

@ -31,12 +31,12 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Storage", "Storage\Storage\
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RTM", "RTM\RTM\RTM.csproj", "{D4A30F70-AAED-415D-B940-023B3D7241EE}"
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}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Common.Test", "Test\Common.Test\Common.Test.csproj", "{4DF4E0F4-1013-477F-ADA6-BFAFD9312335}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Common", "Common\Common.csproj", "{758DE75D-37D7-4392-B564-9484348B505C}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
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}.Release|Any CPU.ActiveCfg = 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.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.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
GlobalSection(NestedProjects) = preSolution
{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}
{3251B4D8-D11A-4D90-8626-27FEE266B066} = {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}
{59DA32A0-4CD3-424A-8584-D08B8D1E2B98} = {CD6B6669-1A56-437A-932E-BCE7F5D4CD18}
{D4A30F70-AAED-415D-B940-023B3D7241EE} = {64D8F9A1-BA44-459C-817C-788B4EBC0B9F}
{BE05B492-78CD-47CA-9F48-C3E9B4813AFF} = {C827DA2F-6AB4-48D8-AB5B-6DAB925F8933}
{4DF4E0F4-1013-477F-ADA6-BFAFD9312335} = {C827DA2F-6AB4-48D8-AB5B-6DAB925F8933}
EndGlobalSection
GlobalSection(MonoDevelopProperties) = preSolution