* AVObjectTest.cs: chore: 移除旧代码

* Test.cs:
* ObjectTest.cs:
* AVObject.cs:
* AVExtensions.cs:
* AVObjectExtensions.cs:
* IdentityEqualityComparer.cs:
* AVObjectController.cs:
oneRain 2019-12-11 11:38:17 +08:00
parent ebeb1ccf6e
commit a839ccc96d
8 changed files with 129 additions and 301 deletions

View File

@ -186,4 +186,4 @@ namespace LeanCloud.Test {
Assert.IsFalse(a.HasCircleReference()); Assert.IsFalse(a.HasCircleReference());
} }
} }
} }

View File

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

View File

@ -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 判断是否全部失败或者网络错误
} }

View File

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

View File

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

View File

@ -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>

View File

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

View File

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