* AVObjectTest.cs: chore: 移除旧代码
* Test.cs: * ObjectTest.cs: * AVObject.cs: * AVExtensions.cs: * AVObjectExtensions.cs: * IdentityEqualityComparer.cs: * AVObjectController.cs:
parent
ebeb1ccf6e
commit
a839ccc96d
|
|
@ -186,4 +186,4 @@ namespace LeanCloud.Test {
|
||||||
Assert.IsFalse(a.HasCircleReference());
|
Assert.IsFalse(a.HasCircleReference());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -27,7 +27,7 @@ namespace LeanCloud.Test {
|
||||||
{ "hello", 1 },
|
{ "hello", 1 },
|
||||||
{ "world", 2 }
|
{ "world", 2 }
|
||||||
};
|
};
|
||||||
await obj.SaveAsync();
|
await obj.SaveAsync(fetchWhenSave: true);
|
||||||
Assert.NotNull(obj.ObjectId);
|
Assert.NotNull(obj.ObjectId);
|
||||||
Assert.NotNull(obj.CreatedAt);
|
Assert.NotNull(obj.CreatedAt);
|
||||||
Assert.NotNull(obj.UpdatedAt);
|
Assert.NotNull(obj.UpdatedAt);
|
||||||
|
|
@ -292,16 +292,22 @@ namespace LeanCloud.Test {
|
||||||
{ "c2", c2 }
|
{ "c2", c2 }
|
||||||
};
|
};
|
||||||
await p.SaveAsync();
|
await p.SaveAsync();
|
||||||
|
Assert.NotNull(p.ObjectId);
|
||||||
|
Assert.NotNull(p.CreatedAt);
|
||||||
|
Assert.NotNull(c1.ObjectId);
|
||||||
|
Assert.NotNull(c2.ObjectId);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
public async Task SaveWithQuery() {
|
public async Task SaveWithExistedObject() {
|
||||||
AVObject p = new AVObject("P");
|
AVObject p = new AVObject("P");
|
||||||
AVObject c1 = AVObject.CreateWithoutData("C1", "5dea05578a84ab00680b7ae5");
|
AVObject c1 = AVObject.CreateWithoutData("C1", "5dea05578a84ab00680b7ae5");
|
||||||
AVObject c2 = new AVObject("C2");
|
AVObject c2 = new AVObject("C2");
|
||||||
p["c"] = c1;
|
p["c"] = c1;
|
||||||
c1["c"] = c2;
|
c1["c"] = c2;
|
||||||
await p.SaveAsync();
|
await p.SaveAsync();
|
||||||
|
Assert.NotNull(p.ObjectId);
|
||||||
|
Assert.NotNull(p.CreatedAt);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -93,30 +93,9 @@ namespace LeanCloud.Storage.Internal {
|
||||||
return list;
|
return list;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task DeleteAsync(IObjectState state, AVQuery<AVObject> query, CancellationToken cancellationToken) {
|
|
||||||
var command = new AVCommand {
|
|
||||||
Path = $"classes/{state.ClassName}/{state.ObjectId}",
|
|
||||||
Method = HttpMethod.Delete
|
|
||||||
};
|
|
||||||
if (query != null) {
|
|
||||||
Dictionary<string, object> where = new Dictionary<string, object> {
|
|
||||||
{ "where", query.BuildWhere() }
|
|
||||||
};
|
|
||||||
command.Path = $"{command.Path}?{AVClient.BuildQueryString(where)}";
|
|
||||||
}
|
|
||||||
await AVPlugins.Instance.CommandRunner.RunCommandAsync<IDictionary<string, object>>(command, cancellationToken);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task DeleteAllAsync(IList<IObjectState> states,
|
public async Task DeleteAllAsync(IList<IObjectState> states,
|
||||||
CancellationToken cancellationToken) {
|
CancellationToken cancellationToken) {
|
||||||
var requests = states
|
|
||||||
.Where(item => item.ObjectId != null)
|
|
||||||
.Select(item => new AVCommand {
|
|
||||||
Path = $"classes/{Uri.EscapeDataString(item.ClassName)}/{Uri.EscapeDataString(item.ObjectId)}",
|
|
||||||
Method = HttpMethod.Delete
|
|
||||||
})
|
|
||||||
.ToList();
|
|
||||||
await AVPlugins.Instance.CommandRunner.ExecuteBatchRequests(requests, cancellationToken);
|
|
||||||
// TODO 判断是否全部失败或者网络错误
|
// TODO 判断是否全部失败或者网络错误
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -45,11 +45,6 @@ namespace LeanCloud.Storage.Internal
|
||||||
return PointerOrLocalIdEncoder.Instance.EncodeAVObject(obj, false);
|
return PointerOrLocalIdEncoder.Instance.EncodeAVObject(obj, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static IEnumerable<object> DeepTraversal(object root, bool traverseAVObjects = false, bool yieldRoot = false)
|
|
||||||
{
|
|
||||||
return AVObject.DeepTraversal(root, traverseAVObjects, yieldRoot);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void SetIfDifferent<T>(this AVObject obj, string key, T value)
|
public static void SetIfDifferent<T>(this AVObject obj, string key, T value)
|
||||||
{
|
{
|
||||||
obj.SetIfDifferent<T>(key, value);
|
obj.SetIfDifferent<T>(key, value);
|
||||||
|
|
|
||||||
|
|
@ -1,22 +1,20 @@
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Runtime.CompilerServices;
|
using System.Runtime.CompilerServices;
|
||||||
|
|
||||||
namespace LeanCloud.Storage.Internal
|
namespace LeanCloud.Storage.Internal {
|
||||||
{
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// An equality comparer that uses the object identity (i.e. ReferenceEquals)
|
/// An equality comparer that uses the object identity (i.e. ReferenceEquals)
|
||||||
/// rather than .Equals, allowing identity to be used for checking equality in
|
/// rather than .Equals, allowing identity to be used for checking equality in
|
||||||
/// ISets and IDictionaries.
|
/// ISets and IDictionaries.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class IdentityEqualityComparer<T> : IEqualityComparer<T>
|
public class IdentityEqualityComparer<T> : IEqualityComparer<T>
|
||||||
{
|
where T : AVObject {
|
||||||
public bool Equals(T x, T y)
|
public bool Equals(T x, T y) {
|
||||||
{
|
return x.ClassName == y.ClassName &&
|
||||||
return object.ReferenceEquals(x, y);
|
x.ObjectId == y.ObjectId;
|
||||||
}
|
}
|
||||||
|
|
||||||
public int GetHashCode(T obj)
|
public int GetHashCode(T obj) {
|
||||||
{
|
|
||||||
return RuntimeHelpers.GetHashCode(obj);
|
return RuntimeHelpers.GetHashCode(obj);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -11,15 +11,6 @@ namespace LeanCloud {
|
||||||
/// of AVObjects so that you can easily save and fetch them in batches.
|
/// of AVObjects so that you can easily save and fetch them in batches.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static class AVExtensions {
|
public static class AVExtensions {
|
||||||
/// <summary>
|
|
||||||
/// Saves all of the AVObjects in the enumeration. Equivalent to
|
|
||||||
/// calling <see cref="AVObject.SaveAllAsync{T}(IEnumerable{T})"/>.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="objects">The objects to save.</param>
|
|
||||||
public static Task SaveAllAsync<T>(this IEnumerable<T> objects) where T : AVObject {
|
|
||||||
return AVObject.SaveAllAsync(objects);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Saves all of the AVObjects in the enumeration. Equivalent to
|
/// Saves all of the AVObjects in the enumeration. Equivalent to
|
||||||
/// calling
|
/// calling
|
||||||
|
|
@ -27,9 +18,9 @@ namespace LeanCloud {
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="objects">The objects to save.</param>
|
/// <param name="objects">The objects to save.</param>
|
||||||
/// <param name="cancellationToken">The cancellation token.</param>
|
/// <param name="cancellationToken">The cancellation token.</param>
|
||||||
public static Task SaveAllAsync<T>(
|
public static Task SaveAllAsync<T>(this IEnumerable<T> objects, bool fetchWhenSave = false, AVQuery<AVObject> query = null, CancellationToken cancellationToken = default)
|
||||||
this IEnumerable<T> objects, CancellationToken cancellationToken) where T : AVObject {
|
where T : AVObject {
|
||||||
return AVObject.SaveAllAsync(objects, cancellationToken);
|
return AVObject.SaveAllAsync(objects, fetchWhenSave, query, cancellationToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@ using LeanCloud.Utilities;
|
||||||
using System;
|
using System;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.ComponentModel;
|
using System.Net.Http;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Runtime.CompilerServices;
|
using System.Runtime.CompilerServices;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
|
|
@ -42,12 +42,60 @@ namespace LeanCloud {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public string ClassName {
|
||||||
|
get {
|
||||||
|
return state.ClassName;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[AVFieldName("objectId")]
|
||||||
|
public string ObjectId {
|
||||||
|
get {
|
||||||
|
return state.ObjectId;
|
||||||
|
}
|
||||||
|
set {
|
||||||
|
IsDirty = true;
|
||||||
|
SetObjectIdInternal(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[AVFieldName("ACL")]
|
||||||
|
public AVACL ACL {
|
||||||
|
get {
|
||||||
|
return GetProperty<AVACL>(null, "ACL");
|
||||||
|
}
|
||||||
|
set {
|
||||||
|
SetProperty(value, "ACL");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[AVFieldName("createdAt")]
|
||||||
|
public DateTime? CreatedAt {
|
||||||
|
get {
|
||||||
|
return state.CreatedAt;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[AVFieldName("updatedAt")]
|
||||||
|
public DateTime? UpdatedAt {
|
||||||
|
get {
|
||||||
|
return state.UpdatedAt;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public ICollection<string> Keys {
|
||||||
|
get {
|
||||||
|
return estimatedData.Keys.Union(serverData.Keys).ToArray();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private static readonly string AutoClassName = "_Automatic";
|
private static readonly string AutoClassName = "_Automatic";
|
||||||
|
|
||||||
internal readonly object mutex = new object();
|
internal readonly object mutex = new object();
|
||||||
|
|
||||||
internal readonly ConcurrentDictionary<string, IAVFieldOperation> operationDict = new ConcurrentDictionary<string, IAVFieldOperation>();
|
internal readonly ConcurrentDictionary<string, IAVFieldOperation> operationDict = new ConcurrentDictionary<string, IAVFieldOperation>();
|
||||||
|
private readonly ConcurrentDictionary<string, object> serverData = new ConcurrentDictionary<string, object>();
|
||||||
private readonly ConcurrentDictionary<string, object> estimatedData = new ConcurrentDictionary<string, object>();
|
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);
|
||||||
|
|
@ -345,19 +393,19 @@ string propertyName
|
||||||
|
|
||||||
// We cache the fetched object because subsequent Save operation might flush
|
// We cache the fetched object because subsequent Save operation might flush
|
||||||
// the fetched objects into Pointers.
|
// the fetched objects into Pointers.
|
||||||
IDictionary<string, AVObject> fetchedObject = CollectFetchedObjects();
|
//IDictionary<string, AVObject> fetchedObject = CollectFetchedObjects();
|
||||||
|
|
||||||
foreach (var pair in serverState) {
|
//foreach (var pair in serverState) {
|
||||||
var value = pair.Value;
|
// var value = pair.Value;
|
||||||
if (value is AVObject) {
|
// if (value is AVObject) {
|
||||||
// Resolve fetched object.
|
// // Resolve fetched object.
|
||||||
var avObject = value as AVObject;
|
// var avObject = value as AVObject;
|
||||||
if (fetchedObject.TryGetValue(avObject.ObjectId, out AVObject obj)) {
|
// if (fetchedObject.TryGetValue(avObject.ObjectId, out AVObject obj)) {
|
||||||
value = obj;
|
// value = obj;
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
newServerData[pair.Key] = value;
|
// newServerData[pair.Key] = value;
|
||||||
}
|
//}
|
||||||
|
|
||||||
IsDirty = false;
|
IsDirty = false;
|
||||||
serverState = serverState.MutatedClone(mutableClone => {
|
serverState = serverState.MutatedClone(mutableClone => {
|
||||||
|
|
@ -387,76 +435,6 @@ string propertyName
|
||||||
RebuildEstimatedData();
|
RebuildEstimatedData();
|
||||||
}
|
}
|
||||||
|
|
||||||
private bool HasDirtyChildren {
|
|
||||||
get {
|
|
||||||
lock (mutex) {
|
|
||||||
return FindUnsavedChildren().FirstOrDefault() != null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Flattens dictionaries and lists into a single enumerable of all contained objects
|
|
||||||
/// that can then be queried over.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="root">The root of the traversal</param>
|
|
||||||
/// <param name="traverseAVObjects">Whether to traverse into AVObjects' children</param>
|
|
||||||
/// <param name="yieldRoot">Whether to include the root in the result</param>
|
|
||||||
/// <returns></returns>
|
|
||||||
internal static IEnumerable<object> DeepTraversal(object root, bool traverseAVObjects = false, bool yieldRoot = false) {
|
|
||||||
var items = DeepTraversalInternal(root,
|
|
||||||
traverseAVObjects,
|
|
||||||
new HashSet<object>(new IdentityEqualityComparer<object>()));
|
|
||||||
if (yieldRoot) {
|
|
||||||
return new[] { root }.Concat(items);
|
|
||||||
} else {
|
|
||||||
return items;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static IEnumerable<object> DeepTraversalInternal(object root, bool traverseAVObjects, ICollection<object> seen) {
|
|
||||||
seen.Add(root);
|
|
||||||
IEnumerable itemsToVisit = null;
|
|
||||||
if (root is IDictionary dict) {
|
|
||||||
itemsToVisit = dict.Values;
|
|
||||||
} else if (root is IList list) {
|
|
||||||
itemsToVisit = list;
|
|
||||||
} else if (traverseAVObjects && root is AVObject obj) {
|
|
||||||
itemsToVisit = obj.Keys.ToList().Select(k => obj[k]);
|
|
||||||
}
|
|
||||||
if (itemsToVisit != null) {
|
|
||||||
foreach (var i in itemsToVisit) {
|
|
||||||
if (!seen.Contains(i)) {
|
|
||||||
yield return i;
|
|
||||||
var children = DeepTraversalInternal(i, traverseAVObjects, seen);
|
|
||||||
foreach (var child in children) {
|
|
||||||
yield return child;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private IEnumerable<AVObject> FindUnsavedChildren() {
|
|
||||||
return DeepTraversal(estimatedData)
|
|
||||||
.OfType<AVObject>()
|
|
||||||
.Where(o => o.IsDirty);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Deep traversal of this object to grab a copy of any object referenced by this object.
|
|
||||||
/// These instances may have already been fetched, and we don't want to lose their data when
|
|
||||||
/// refreshing or saving.
|
|
||||||
/// </summary>
|
|
||||||
/// <returns>Map of objectId to AVObject which have been fetched.</returns>
|
|
||||||
private IDictionary<string, AVObject> CollectFetchedObjects() {
|
|
||||||
return DeepTraversal(estimatedData)
|
|
||||||
.OfType<AVObject>()
|
|
||||||
.Where(o => o.ObjectId != null && o.IsDataAvailable)
|
|
||||||
.GroupBy(o => o.ObjectId)
|
|
||||||
.ToDictionary(group => group.Key, group => group.Last());
|
|
||||||
}
|
|
||||||
|
|
||||||
public static IDictionary<string, object> ToJSONObjectForSaving(IDictionary<string, IAVFieldOperation> operations) {
|
public static IDictionary<string, object> ToJSONObjectForSaving(IDictionary<string, IAVFieldOperation> operations) {
|
||||||
var result = new Dictionary<string, object>();
|
var result = new Dictionary<string, object>();
|
||||||
foreach (var pair in operations) {
|
foreach (var pair in operations) {
|
||||||
|
|
@ -502,7 +480,7 @@ string propertyName
|
||||||
/// Saves each object in the provided list.
|
/// Saves each object in the provided list.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="objects">The objects to save.</param>
|
/// <param name="objects">The objects to save.</param>
|
||||||
public static async Task SaveAllAsync<T>(IEnumerable<T> objects, CancellationToken cancellationToken = default)
|
public static async Task SaveAllAsync<T>(IEnumerable<T> objects, bool fetchWhenSave = false, AVQuery<AVObject> query = null, CancellationToken cancellationToken = default)
|
||||||
where T : AVObject {
|
where T : AVObject {
|
||||||
foreach (T obj in objects) {
|
foreach (T obj in objects) {
|
||||||
if (HasCircleReference(obj, new HashSet<AVObject>())) {
|
if (HasCircleReference(obj, new HashSet<AVObject>())) {
|
||||||
|
|
@ -696,7 +674,17 @@ string propertyName
|
||||||
if (ObjectId == null) {
|
if (ObjectId == null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
await ObjectController.DeleteAsync(State, query, cancellationToken);
|
var command = new AVCommand {
|
||||||
|
Path = $"classes/{state.ClassName}/{state.ObjectId}",
|
||||||
|
Method = HttpMethod.Delete
|
||||||
|
};
|
||||||
|
if (query != null) {
|
||||||
|
Dictionary<string, object> where = new Dictionary<string, object> {
|
||||||
|
{ "where", query.BuildWhere() }
|
||||||
|
};
|
||||||
|
command.Path = $"{command.Path}?{AVClient.BuildQueryString(where)}";
|
||||||
|
}
|
||||||
|
await AVPlugins.Instance.CommandRunner.RunCommandAsync<IDictionary<string, object>>(command, cancellationToken);
|
||||||
IsDirty = true;
|
IsDirty = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -709,8 +697,16 @@ string propertyName
|
||||||
var uniqueObjects = new HashSet<AVObject>(objects.OfType<AVObject>().ToList(),
|
var uniqueObjects = new HashSet<AVObject>(objects.OfType<AVObject>().ToList(),
|
||||||
new IdentityEqualityComparer<AVObject>());
|
new IdentityEqualityComparer<AVObject>());
|
||||||
|
|
||||||
var states = uniqueObjects.Select(t => t.state).ToList();
|
var states = uniqueObjects.Select(t => t.state);
|
||||||
await ObjectController.DeleteAllAsync(states, cancellationToken);
|
var requests = states
|
||||||
|
.Where(item => item.ObjectId != null)
|
||||||
|
.Select(item => new AVCommand {
|
||||||
|
Path = $"classes/{Uri.EscapeDataString(item.ClassName)}/{Uri.EscapeDataString(item.ObjectId)}",
|
||||||
|
Method = HttpMethod.Delete
|
||||||
|
})
|
||||||
|
.ToList();
|
||||||
|
await AVPlugins.Instance.CommandRunner.ExecuteBatchRequests(requests, cancellationToken);
|
||||||
|
|
||||||
foreach (var obj in uniqueObjects) {
|
foreach (var obj in uniqueObjects) {
|
||||||
obj.IsDirty = true;
|
obj.IsDirty = true;
|
||||||
}
|
}
|
||||||
|
|
@ -718,42 +714,6 @@ string propertyName
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
private static void CollectDirtyChildren(object node,
|
|
||||||
IList<AVObject> dirtyChildren,
|
|
||||||
ICollection<AVObject> seen,
|
|
||||||
ICollection<AVObject> seenNew) {
|
|
||||||
foreach (var obj in DeepTraversal(node).OfType<AVObject>()) {
|
|
||||||
ICollection<AVObject> scopedSeenNew;
|
|
||||||
// Check for cycles of new objects. Any such cycle means it will be impossible to save
|
|
||||||
// this collection of objects, so throw an exception.
|
|
||||||
if (obj.ObjectId != null) {
|
|
||||||
scopedSeenNew = new HashSet<AVObject>(new IdentityEqualityComparer<AVObject>());
|
|
||||||
} else {
|
|
||||||
if (seenNew.Contains(obj)) {
|
|
||||||
throw new InvalidOperationException("Found a circular dependency while saving");
|
|
||||||
}
|
|
||||||
scopedSeenNew = new HashSet<AVObject>(seenNew, new IdentityEqualityComparer<AVObject>());
|
|
||||||
scopedSeenNew.Add(obj);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check for cycles of any object. If this occurs, then there's no problem, but
|
|
||||||
// we shouldn't recurse any deeper, because it would be an infinite recursion.
|
|
||||||
if (seen.Contains(obj)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
seen.Add(obj);
|
|
||||||
|
|
||||||
// Recurse into this object's children looking for dirty children.
|
|
||||||
// We only need to look at the child object's current estimated data,
|
|
||||||
// because that's the only data that might need to be saved now.
|
|
||||||
CollectDirtyChildren(obj.estimatedData, dirtyChildren, seen, scopedSeenNew);
|
|
||||||
|
|
||||||
if (obj.CheckIsDirty(false)) {
|
|
||||||
dirtyChildren.Add(obj);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Adds a task to the queue for all of the given objects.
|
/// Adds a task to the queue for all of the given objects.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|
@ -1123,16 +1083,14 @@ string propertyName
|
||||||
private void CheckGetAccess(string key) {
|
private void CheckGetAccess(string key) {
|
||||||
lock (mutex) {
|
lock (mutex) {
|
||||||
if (!CheckIsDataAvailable(key)) {
|
if (!CheckIsDataAvailable(key)) {
|
||||||
throw new InvalidOperationException(
|
throw new InvalidOperationException("AVObject has no data for this key. Call FetchIfNeededAsync() to get the data.");
|
||||||
"AVObject has no data for this key. Call FetchIfNeededAsync() to get the data.");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void CheckKeyIsMutable(string key) {
|
private void CheckKeyIsMutable(string key) {
|
||||||
if (!IsKeyMutable(key)) {
|
if (!IsKeyMutable(key)) {
|
||||||
throw new InvalidOperationException(
|
throw new InvalidOperationException($"Cannot change the `{key}` property of a `{ClassName}` object.");
|
||||||
"Cannot change the `" + key + "` property of a `" + ClassName + "` object.");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1152,81 +1110,6 @@ string propertyName
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets a set view of the keys contained in this object. This does not include createdAt,
|
|
||||||
/// updatedAt, or objectId. It does include things like username and ACL.
|
|
||||||
/// </summary>
|
|
||||||
public ICollection<string> Keys {
|
|
||||||
get {
|
|
||||||
lock (mutex) {
|
|
||||||
return estimatedData.Keys;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets or sets the AVACL governing this object.
|
|
||||||
/// </summary>
|
|
||||||
[AVFieldName("ACL")]
|
|
||||||
public AVACL ACL {
|
|
||||||
get { return GetProperty<AVACL>(null, "ACL"); }
|
|
||||||
set { SetProperty(value, "ACL"); }
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Returns true if this object was created by the LeanCloud server when the
|
|
||||||
/// object might have already been there (e.g. in the case of a Facebook
|
|
||||||
/// login)
|
|
||||||
/// </summary>
|
|
||||||
#if !UNITY
|
|
||||||
public
|
|
||||||
#else
|
|
||||||
internal
|
|
||||||
#endif
|
|
||||||
bool IsNew {
|
|
||||||
get {
|
|
||||||
return state.IsNew;
|
|
||||||
}
|
|
||||||
#if !UNITY
|
|
||||||
internal
|
|
||||||
#endif
|
|
||||||
set {
|
|
||||||
MutateState(mutableClone => {
|
|
||||||
mutableClone.IsNew = value;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets the last time this object was updated as the server sees it, so that if you make changes
|
|
||||||
/// to a AVObject, then wait a while, and then call <see cref="SaveAsync(bool, AVQuery{AVObject}, CancellationToken)"/>, the updated time
|
|
||||||
/// will be the time of the <see cref="SaveAsync(bool, AVQuery{AVObject}, CancellationToken)"/> call rather than the time the object was
|
|
||||||
/// changed locally.
|
|
||||||
/// </summary>
|
|
||||||
[AVFieldName("updatedAt")]
|
|
||||||
public DateTime? UpdatedAt {
|
|
||||||
get {
|
|
||||||
return state.UpdatedAt;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets the first time this object was saved as the server sees it, so that if you create a
|
|
||||||
/// AVObject, then wait a while, and then call <see cref="SaveAsync(bool, AVQuery{AVObject}, CancellationToken)"/>, the
|
|
||||||
/// creation time will be the time of the first <see cref="SaveAsync(bool, AVQuery{AVObject}, CancellationToken)"/> call rather than
|
|
||||||
/// the time the object was created locally.
|
|
||||||
/// </summary>
|
|
||||||
[AVFieldName("createdAt")]
|
|
||||||
public DateTime? CreatedAt {
|
|
||||||
get {
|
|
||||||
return state.CreatedAt;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool FetchWhenSave {
|
|
||||||
get; set;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Indicates whether this AVObject has unsaved changes.
|
/// Indicates whether this AVObject has unsaved changes.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|
@ -1255,25 +1138,11 @@ string propertyName
|
||||||
|
|
||||||
private bool CheckIsDirty(bool considerChildren) {
|
private bool CheckIsDirty(bool considerChildren) {
|
||||||
lock (mutex) {
|
lock (mutex) {
|
||||||
return (dirty || operationDict.Count > 0 || (considerChildren && HasDirtyChildren));
|
return dirty || operationDict.Count > 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets or sets the object id. An object id is assigned as soon as an object is
|
|
||||||
/// saved to the server. The combination of a <see cref="ClassName"/> and an
|
|
||||||
/// <see cref="ObjectId"/> uniquely identifies an object in your application.
|
|
||||||
/// </summary>
|
|
||||||
[AVFieldName("objectId")]
|
|
||||||
public string ObjectId {
|
|
||||||
get {
|
|
||||||
return state.ObjectId;
|
|
||||||
}
|
|
||||||
set {
|
|
||||||
IsDirty = true;
|
|
||||||
SetObjectIdInternal(value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Sets the objectId without marking dirty.
|
/// Sets the objectId without marking dirty.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|
@ -1286,15 +1155,6 @@ string propertyName
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets the class name for the AVObject.
|
|
||||||
/// </summary>
|
|
||||||
public string ClassName {
|
|
||||||
get {
|
|
||||||
return state.ClassName;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Adds a value for the given key, throwing an Exception if the key
|
/// Adds a value for the given key, throwing an Exception if the key
|
||||||
/// already has a value.
|
/// already has a value.
|
||||||
|
|
@ -1335,30 +1195,11 @@ string propertyName
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
public static AVQuery<T> GetQuery<T>(string className)
|
||||||
/// Gets a <see cref="AVQuery{AVObject}"/> for the type of object specified by
|
where T : AVObject {
|
||||||
/// <paramref name="className"/>
|
return new AVQuery<T>(className);
|
||||||
/// </summary>
|
|
||||||
/// <param name="className">The class name of the object.</param>
|
|
||||||
/// <returns>A new <see cref="AVQuery{AVObject}"/>.</returns>
|
|
||||||
public static AVQuery<AVObject> GetQuery(string className) {
|
|
||||||
// Since we can't return a AVQuery<AVUser> (due to strong-typing with
|
|
||||||
// generics), we'll require you to go through subclasses. This is a better
|
|
||||||
// experience anyway, especially with LINQ integration, since you'll get
|
|
||||||
// strongly-typed queries and compile-time checking of property names and
|
|
||||||
// types.
|
|
||||||
if (SubclassingController.GetType(className) != null) {
|
|
||||||
throw new ArgumentException(
|
|
||||||
"Use the class-specific query properties for class " + className, nameof(className));
|
|
||||||
}
|
|
||||||
return new AVQuery<AVObject>(className);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
#region refactor
|
#region refactor
|
||||||
|
|
||||||
static bool HasCircleReference(object obj, HashSet<AVObject> parents) {
|
static bool HasCircleReference(object obj, HashSet<AVObject> parents) {
|
||||||
|
|
@ -1394,7 +1235,7 @@ string propertyName
|
||||||
batches.Push(new Batch(avObjects));
|
batches.Push(new Batch(avObjects));
|
||||||
}
|
}
|
||||||
|
|
||||||
IEnumerable<object> deps = from avObj in avObjects select avObj.estimatedData.Values;
|
IEnumerable<object> deps = avObjects.Select(avObj => avObj.estimatedData.Values);
|
||||||
do {
|
do {
|
||||||
HashSet<object> childSets = new HashSet<object>();
|
HashSet<object> childSets = new HashSet<object>();
|
||||||
foreach (object dep in deps) {
|
foreach (object dep in deps) {
|
||||||
|
|
@ -1405,7 +1246,7 @@ string propertyName
|
||||||
children = (dep as IDictionary).Values;
|
children = (dep as IDictionary).Values;
|
||||||
} else if (dep is AVObject && (dep as AVObject).ObjectId == null) {
|
} else if (dep is AVObject && (dep as AVObject).ObjectId == null) {
|
||||||
// 如果依赖是 AVObject 类型并且还没有保存过,则应该遍历其依赖
|
// 如果依赖是 AVObject 类型并且还没有保存过,则应该遍历其依赖
|
||||||
// TODO 这里应该是从 Operation 中查找新增的对象
|
// 这里应该是从 Operation 中查找新增的对象
|
||||||
children = (dep as AVObject).estimatedData.Values;
|
children = (dep as AVObject).estimatedData.Values;
|
||||||
}
|
}
|
||||||
if (children != null) {
|
if (children != null) {
|
||||||
|
|
|
||||||
|
|
@ -69,5 +69,23 @@ namespace Common.Test {
|
||||||
TestContext.WriteLine($"{delta.Key} : {delta.Value}");
|
TestContext.WriteLine($"{delta.Key} : {delta.Value}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void Union() {
|
||||||
|
Dictionary<string, int> dict1 = new Dictionary<string, int> {
|
||||||
|
{ "a", 1 },
|
||||||
|
{ "b", 2 },
|
||||||
|
{ "c", 3 }
|
||||||
|
};
|
||||||
|
Dictionary<string, string> dict2 = new Dictionary<string, string> {
|
||||||
|
{ "b", "b" },
|
||||||
|
{ "c", "c" },
|
||||||
|
{ "d", "d" }
|
||||||
|
};
|
||||||
|
IEnumerable<string> keys = dict1.Keys.Union(dict2.Keys);
|
||||||
|
foreach (string key in keys) {
|
||||||
|
TestContext.WriteLine(key);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue