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

View File

@ -27,7 +27,7 @@ namespace LeanCloud.Test {
{ "hello", 1 },
{ "world", 2 }
};
await obj.SaveAsync();
await obj.SaveAsync(fetchWhenSave: true);
Assert.NotNull(obj.ObjectId);
Assert.NotNull(obj.CreatedAt);
Assert.NotNull(obj.UpdatedAt);
@ -292,16 +292,22 @@ namespace LeanCloud.Test {
{ "c2", c2 }
};
await p.SaveAsync();
Assert.NotNull(p.ObjectId);
Assert.NotNull(p.CreatedAt);
Assert.NotNull(c1.ObjectId);
Assert.NotNull(c2.ObjectId);
}
[Test]
public async Task SaveWithQuery() {
public async Task SaveWithExistedObject() {
AVObject p = new AVObject("P");
AVObject c1 = AVObject.CreateWithoutData("C1", "5dea05578a84ab00680b7ae5");
AVObject c2 = new AVObject("C2");
p["c"] = c1;
c1["c"] = c2;
await p.SaveAsync();
Assert.NotNull(p.ObjectId);
Assert.NotNull(p.CreatedAt);
}
}
}

View File

@ -93,30 +93,9 @@ namespace LeanCloud.Storage.Internal {
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,
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 判断是否全部失败或者网络错误
}

View File

@ -45,11 +45,6 @@ namespace LeanCloud.Storage.Internal
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)
{
obj.SetIfDifferent<T>(key, value);

View File

@ -1,22 +1,20 @@
using System.Collections.Generic;
using System.Runtime.CompilerServices;
namespace LeanCloud.Storage.Internal
{
namespace LeanCloud.Storage.Internal {
/// <summary>
/// An equality comparer that uses the object identity (i.e. ReferenceEquals)
/// rather than .Equals, allowing identity to be used for checking equality in
/// ISets and IDictionaries.
/// </summary>
public class IdentityEqualityComparer<T> : IEqualityComparer<T>
{
public bool Equals(T x, T y)
{
return object.ReferenceEquals(x, y);
where T : AVObject {
public bool Equals(T x, T y) {
return x.ClassName == y.ClassName &&
x.ObjectId == y.ObjectId;
}
public int GetHashCode(T obj)
{
public int GetHashCode(T 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.
/// </summary>
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>
/// Saves all of the AVObjects in the enumeration. Equivalent to
/// calling
@ -27,9 +18,9 @@ namespace LeanCloud {
/// </summary>
/// <param name="objects">The objects to save.</param>
/// <param name="cancellationToken">The cancellation token.</param>
public static Task SaveAllAsync<T>(
this IEnumerable<T> objects, CancellationToken cancellationToken) where T : AVObject {
return AVObject.SaveAllAsync(objects, cancellationToken);
public static Task SaveAllAsync<T>(this IEnumerable<T> objects, bool fetchWhenSave = false, AVQuery<AVObject> query = null, CancellationToken cancellationToken = default)
where T : AVObject {
return AVObject.SaveAllAsync(objects, fetchWhenSave, query, cancellationToken);
}
/// <summary>

View File

@ -3,7 +3,7 @@ using LeanCloud.Utilities;
using System;
using System.Text;
using System.Collections.Generic;
using System.ComponentModel;
using System.Net.Http;
using System.Linq;
using System.Runtime.CompilerServices;
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";
internal readonly object mutex = new object();
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 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
// the fetched objects into Pointers.
IDictionary<string, AVObject> fetchedObject = CollectFetchedObjects();
//IDictionary<string, AVObject> fetchedObject = CollectFetchedObjects();
foreach (var pair in serverState) {
var value = pair.Value;
if (value is AVObject) {
// Resolve fetched object.
var avObject = value as AVObject;
if (fetchedObject.TryGetValue(avObject.ObjectId, out AVObject obj)) {
value = obj;
}
}
newServerData[pair.Key] = value;
}
//foreach (var pair in serverState) {
// var value = pair.Value;
// if (value is AVObject) {
// // Resolve fetched object.
// var avObject = value as AVObject;
// if (fetchedObject.TryGetValue(avObject.ObjectId, out AVObject obj)) {
// value = obj;
// }
// }
// newServerData[pair.Key] = value;
//}
IsDirty = false;
serverState = serverState.MutatedClone(mutableClone => {
@ -387,76 +435,6 @@ string propertyName
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) {
var result = new Dictionary<string, object>();
foreach (var pair in operations) {
@ -502,7 +480,7 @@ string propertyName
/// Saves each object in the provided list.
/// </summary>
/// <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 {
foreach (T obj in objects) {
if (HasCircleReference(obj, new HashSet<AVObject>())) {
@ -696,7 +674,17 @@ string propertyName
if (ObjectId == null) {
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;
}
@ -709,8 +697,16 @@ string propertyName
var uniqueObjects = new HashSet<AVObject>(objects.OfType<AVObject>().ToList(),
new IdentityEqualityComparer<AVObject>());
var states = uniqueObjects.Select(t => t.state).ToList();
await ObjectController.DeleteAllAsync(states, cancellationToken);
var states = uniqueObjects.Select(t => t.state);
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) {
obj.IsDirty = true;
}
@ -718,42 +714,6 @@ string propertyName
#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>
/// Adds a task to the queue for all of the given objects.
/// </summary>
@ -1123,16 +1083,14 @@ string propertyName
private void CheckGetAccess(string key) {
lock (mutex) {
if (!CheckIsDataAvailable(key)) {
throw new InvalidOperationException(
"AVObject has no data for this key. Call FetchIfNeededAsync() to get the data.");
throw new InvalidOperationException("AVObject has no data for this key. Call FetchIfNeededAsync() to get the data.");
}
}
}
private void CheckKeyIsMutable(string key) {
if (!IsKeyMutable(key)) {
throw new InvalidOperationException(
"Cannot change the `" + key + "` property of a `" + ClassName + "` object.");
throw new InvalidOperationException($"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>
/// Indicates whether this AVObject has unsaved changes.
/// </summary>
@ -1255,25 +1138,11 @@ string propertyName
private bool CheckIsDirty(bool considerChildren) {
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>
/// Sets the objectId without marking dirty.
/// </summary>
@ -1286,15 +1155,6 @@ string propertyName
}
}
/// <summary>
/// Gets the class name for the AVObject.
/// </summary>
public string ClassName {
get {
return state.ClassName;
}
}
/// <summary>
/// Adds a value for the given key, throwing an Exception if the key
/// already has a value.
@ -1335,30 +1195,11 @@ string propertyName
}
}
/// <summary>
/// Gets a <see cref="AVQuery{AVObject}"/> for the type of object specified by
/// <paramref name="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);
public static AVQuery<T> GetQuery<T>(string className)
where T : AVObject {
return new AVQuery<T>(className);
}
#region refactor
static bool HasCircleReference(object obj, HashSet<AVObject> parents) {
@ -1394,7 +1235,7 @@ string propertyName
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 {
HashSet<object> childSets = new HashSet<object>();
foreach (object dep in deps) {
@ -1405,7 +1246,7 @@ string propertyName
children = (dep as IDictionary).Values;
} else if (dep is AVObject && (dep as AVObject).ObjectId == null) {
// 如果依赖是 AVObject 类型并且还没有保存过,则应该遍历其依赖
// TODO 这里应该是从 Operation 中查找新增的对象
// 这里应该是从 Operation 中查找新增的对象
children = (dep as AVObject).estimatedData.Values;
}
if (children != null) {

View File

@ -69,5 +69,23 @@ namespace Common.Test {
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);
}
}
}
}