* ObjectControllerTests.cs: test: 测试批量操作

* AVObject.cs:
* AVFileExtensions.cs:
* MutableObjectState.cs:
* AVObjectController.cs:
* ObjectSubclassInfo.cs:
* ObjectSubclassingController.cs:
oneRain 2019-09-02 14:01:28 +08:00
parent 7f29ee2cbf
commit 7980d47168
7 changed files with 98 additions and 106 deletions

View File

@ -13,11 +13,9 @@ namespace LeanCloudTests {
[Test] [Test]
public async Task Save() { public async Task Save() {
TestContext.Out.WriteLine($"before at {Thread.CurrentThread.ManagedThreadId}");
AVObject obj = AVObject.Create("Foo"); AVObject obj = AVObject.Create("Foo");
obj["content"] = "hello, world"; obj["content"] = "hello, world";
await obj.SaveAsync(); await obj.SaveAsync();
TestContext.Out.WriteLine($"{obj.ObjectId} saved at {Thread.CurrentThread.ManagedThreadId}");
Assert.NotNull(obj.ObjectId); Assert.NotNull(obj.ObjectId);
Assert.NotNull(obj.CreatedAt); Assert.NotNull(obj.CreatedAt);
Assert.NotNull(obj.UpdatedAt); Assert.NotNull(obj.UpdatedAt);
@ -26,6 +24,8 @@ namespace LeanCloudTests {
[Test] [Test]
public async Task SaveWithOptions() { public async Task SaveWithOptions() {
AVObject account = AVObject.CreateWithoutData("Account", "5d65fa5330863b008065e476"); AVObject account = AVObject.CreateWithoutData("Account", "5d65fa5330863b008065e476");
account["balance"] = 100;
await account.SaveAsync();
AVQuery<AVObject> query = new AVQuery<AVObject>("Account"); AVQuery<AVObject> query = new AVQuery<AVObject>("Account");
query.WhereGreaterThan("balance", 80); query.WhereGreaterThan("balance", 80);
account["balance"] = 50; account["balance"] = 50;
@ -33,6 +33,20 @@ namespace LeanCloudTests {
TestContext.Out.WriteLine($"balance: {account["balance"]}"); TestContext.Out.WriteLine($"balance: {account["balance"]}");
} }
[Test]
public async Task SaveBatch() {
List<AVObject> objList = new List<AVObject>();
for (int i = 0; i < 5; i++) {
AVObject obj = AVObject.Create("Foo");
obj["content"] = "batch object";
objList.Add(obj);
}
await objList.SaveAllAsync();
objList.ForEach(obj => {
Assert.NotNull(obj.ObjectId);
});
}
[Test] [Test]
public async Task Fetch() { public async Task Fetch() {
AVObject obj = AVObject.CreateWithoutData("Todo", "5d5f6039d5de2b006cf29c8f"); AVObject obj = AVObject.CreateWithoutData("Todo", "5d5f6039d5de2b006cf29c8f");
@ -56,5 +70,40 @@ namespace LeanCloudTests {
AVObject tag = obj["tag"] as AVObject; AVObject tag = obj["tag"] as AVObject;
TestContext.Out.WriteLine($"{tag["name"]}"); TestContext.Out.WriteLine($"{tag["name"]}");
} }
[Test]
public async Task FetchAll() {
List<AVObject> objList = new List<AVObject> {
AVObject.CreateWithoutData("Tag", "5d64e5ebc05a8000730340ba"),
AVObject.CreateWithoutData("Tag", "5d64e5eb12215f0073db271c"),
AVObject.CreateWithoutData("Tag", "5d64e57f43e78c0068a14315")
};
await objList.FetchAllAsync();
objList.ForEach(obj => {
Assert.NotNull(obj.ObjectId);
TestContext.Out.WriteLine($"{obj.ObjectId}, {obj["name"]}");
});
}
[Test]
public async Task Delete() {
AVObject obj = AVObject.Create("Foo");
obj["content"] = "hello, world";
await obj.SaveAsync();
Assert.NotNull(obj);
await obj.DeleteAsync();
}
[Test]
public async Task DeleteAll() {
List<AVObject> objList = new List<AVObject>();
for (int i = 0; i < 5; i++) {
AVObject obj = AVObject.Create("Foo");
obj["content"] = "batch object";
objList.Add(obj);
}
await objList.SaveAllAsync();
await AVObject.DeleteAllAsync(objList);
}
} }
} }

View File

@ -15,7 +15,7 @@ namespace LeanCloud.Storage.Internal {
Path = $"classes/{Uri.EscapeDataString(state.ClassName)}/{Uri.EscapeDataString(state.ObjectId)}?{AVClient.BuildQueryString(queryString)}", Path = $"classes/{Uri.EscapeDataString(state.ClassName)}/{Uri.EscapeDataString(state.ObjectId)}?{AVClient.BuildQueryString(queryString)}",
Method = HttpMethod.Get Method = HttpMethod.Get
}; };
return AVPlugins.Instance.CommandRunner.RunCommandAsync<IDictionary<string, object>>(command, cancellationToken: cancellationToken).OnSuccess(t => { return AVPlugins.Instance.CommandRunner.RunCommandAsync<IDictionary<string, object>>(command, cancellationToken).OnSuccess(t => {
return AVObjectCoder.Instance.Decode(t.Result.Item2, AVDecoder.Instance); return AVObjectCoder.Instance.Decode(t.Result.Item2, AVDecoder.Instance);
}); });
} }
@ -44,7 +44,7 @@ namespace LeanCloud.Storage.Internal {
string encode = AVClient.BuildQueryString(args); string encode = AVClient.BuildQueryString(args);
command.Path = $"{command.Path}?{encode}"; command.Path = $"{command.Path}?{encode}";
} }
return AVPlugins.Instance.CommandRunner.RunCommandAsync<IDictionary<string, object>>(command, cancellationToken: cancellationToken).OnSuccess(t => { return AVPlugins.Instance.CommandRunner.RunCommandAsync<IDictionary<string, object>>(command, cancellationToken).OnSuccess(t => {
var serverState = AVObjectCoder.Instance.Decode(t.Result.Item2, AVDecoder.Instance); var serverState = AVObjectCoder.Instance.Decode(t.Result.Item2, AVDecoder.Instance);
serverState = serverState.MutatedClone(mutableClone => { serverState = serverState.MutatedClone(mutableClone => {
mutableClone.IsNew = t.Result.Item1 == System.Net.HttpStatusCode.Created; mutableClone.IsNew = t.Result.Item1 == System.Net.HttpStatusCode.Created;
@ -82,7 +82,7 @@ namespace LeanCloud.Storage.Internal {
Path = $"classes/{state.ClassName}/{state.ObjectId}", Path = $"classes/{state.ClassName}/{state.ObjectId}",
Method = HttpMethod.Delete Method = HttpMethod.Delete
}; };
return AVPlugins.Instance.CommandRunner.RunCommandAsync<IDictionary<string, object>>(command, cancellationToken: cancellationToken); return AVPlugins.Instance.CommandRunner.RunCommandAsync<IDictionary<string, object>>(command, cancellationToken);
} }
public IList<Task> DeleteAllAsync(IList<IObjectState> states, public IList<Task> DeleteAllAsync(IList<IObjectState> states,
@ -131,8 +131,8 @@ namespace LeanCloud.Storage.Internal {
var encodedRequests = requests.Select(r => { var encodedRequests = requests.Select(r => {
var results = new Dictionary<string, object> { var results = new Dictionary<string, object> {
{ "method", r.Method }, { "method", r.Method.Method },
{ "path", r.Path }, { "path", $"/{AVClient.APIVersion}/{r.Path}" },
}; };
if (r.Content != null) { if (r.Content != null) {
@ -147,7 +147,7 @@ namespace LeanCloud.Storage.Internal {
{ "requests", encodedRequests } { "requests", encodedRequests }
} }
}; };
AVPlugins.Instance.CommandRunner.RunCommandAsync<IDictionary<string, object>>(command, cancellationToken: cancellationToken).ContinueWith(t => { AVPlugins.Instance.CommandRunner.RunCommandAsync<IList<object>>(command, cancellationToken).ContinueWith(t => {
if (t.IsFaulted || t.IsCanceled) { if (t.IsFaulted || t.IsCanceled) {
foreach (var tcs in tcss) { foreach (var tcs in tcss) {
if (t.IsFaulted) { if (t.IsFaulted) {
@ -159,7 +159,7 @@ namespace LeanCloud.Storage.Internal {
return; return;
} }
var resultsArray = Conversion.As<IList<object>>(t.Result.Item2["results"]); var resultsArray = t.Result.Item2;
int resultLength = resultsArray.Count; int resultLength = resultsArray.Count;
if (resultLength != batchSize) { if (resultLength != batchSize) {
foreach (var tcs in tcss) { foreach (var tcs in tcss) {

View File

@ -2,10 +2,8 @@
using System.Linq; using System.Linq;
using System.Collections.Generic; using System.Collections.Generic;
namespace LeanCloud.Storage.Internal namespace LeanCloud.Storage.Internal {
{ public class MutableObjectState : IObjectState {
public class MutableObjectState : IObjectState
{
public bool IsNew { get; set; } public bool IsNew { get; set; }
public string ClassName { get; set; } public string ClassName { get; set; }
public string ObjectId { get; set; } public string ObjectId { get; set; }
@ -14,84 +12,65 @@ namespace LeanCloud.Storage.Internal
// Initialize serverData to avoid further null checking. // Initialize serverData to avoid further null checking.
private IDictionary<string, object> serverData = new Dictionary<string, object>(); private IDictionary<string, object> serverData = new Dictionary<string, object>();
public IDictionary<string, object> ServerData public IDictionary<string, object> ServerData {
{ get {
get
{
return serverData; return serverData;
} }
set set {
{
serverData = value; serverData = value;
} }
} }
public object this[string key] public object this[string key] {
{ get {
get
{
return ServerData[key]; return ServerData[key];
} }
} }
public bool ContainsKey(string key) public bool ContainsKey(string key) {
{
return ServerData.ContainsKey(key); return ServerData.ContainsKey(key);
} }
public void Apply(IDictionary<string, IAVFieldOperation> operationSet) public void Apply(IDictionary<string, IAVFieldOperation> operationSet) {
{
// Apply operationSet // Apply operationSet
foreach (var pair in operationSet) foreach (var pair in operationSet) {
{
object oldValue; object oldValue;
ServerData.TryGetValue(pair.Key, out oldValue); ServerData.TryGetValue(pair.Key, out oldValue);
var newValue = pair.Value.Apply(oldValue, pair.Key); var newValue = pair.Value.Apply(oldValue, pair.Key);
if (newValue != AVDeleteOperation.DeleteToken) if (newValue != AVDeleteOperation.DeleteToken) {
{
ServerData[pair.Key] = newValue; ServerData[pair.Key] = newValue;
} } else {
else
{
ServerData.Remove(pair.Key); ServerData.Remove(pair.Key);
} }
} }
} }
public void Apply(IObjectState other) public void Apply(IObjectState other) {
{
IsNew = other.IsNew; IsNew = other.IsNew;
if (other.ObjectId != null) if (other.ObjectId != null) {
{
ObjectId = other.ObjectId; ObjectId = other.ObjectId;
} }
if (other.UpdatedAt != null) if (other.UpdatedAt != null) {
{
UpdatedAt = other.UpdatedAt; UpdatedAt = other.UpdatedAt;
} }
if (other.CreatedAt != null) if (other.CreatedAt != null) {
{
CreatedAt = other.CreatedAt; CreatedAt = other.CreatedAt;
} }
foreach (var pair in other) foreach (var pair in other) {
{
ServerData[pair.Key] = pair.Value; ServerData[pair.Key] = pair.Value;
} }
} }
public IObjectState MutatedClone(Action<MutableObjectState> func) public IObjectState MutatedClone(Action<MutableObjectState> func) {
{
var clone = MutableClone(); var clone = MutableClone();
func(clone); func(clone);
return clone; return clone;
} }
protected virtual MutableObjectState MutableClone() protected virtual MutableObjectState MutableClone() {
{ return new MutableObjectState {
return new MutableObjectState
{
IsNew = IsNew, IsNew = IsNew,
ClassName = ClassName, ClassName = ClassName,
ObjectId = ObjectId, ObjectId = ObjectId,
@ -101,13 +80,11 @@ namespace LeanCloud.Storage.Internal
}; };
} }
IEnumerator<KeyValuePair<string, object>> IEnumerable<KeyValuePair<string, object>>.GetEnumerator() IEnumerator<KeyValuePair<string, object>> IEnumerable<KeyValuePair<string, object>>.GetEnumerator() {
{
return ServerData.GetEnumerator(); return ServerData.GetEnumerator();
} }
System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() {
{
return ((IEnumerable<KeyValuePair<string, object>>)this).GetEnumerator(); return ((IEnumerable<KeyValuePair<string, object>>)this).GetEnumerator();
} }
} }

View File

@ -1,18 +1,11 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Reflection; using System.Reflection;
using LeanCloud.Storage.Internal;
namespace LeanCloud.Storage.Internal namespace LeanCloud.Storage.Internal {
{ internal class ObjectSubclassInfo {
internal class ObjectSubclassInfo public ObjectSubclassInfo(Type type, ConstructorInfo constructor) {
{
public ObjectSubclassInfo(Type type, ConstructorInfo constructor)
{
TypeInfo = type.GetTypeInfo(); TypeInfo = type.GetTypeInfo();
ClassName = GetClassName(TypeInfo); ClassName = GetClassName(TypeInfo);
Constructor = constructor; Constructor = constructor;
@ -28,15 +21,13 @@ namespace LeanCloud.Storage.Internal
public IDictionary<string, string> PropertyMappings { get; private set; } public IDictionary<string, string> PropertyMappings { get; private set; }
private ConstructorInfo Constructor { get; set; } private ConstructorInfo Constructor { get; set; }
public AVObject Instantiate() public AVObject Instantiate() {
{
return (AVObject)Constructor.Invoke(null); return (AVObject)Constructor.Invoke(null);
} }
internal static string GetClassName(TypeInfo type) internal static string GetClassName(TypeInfo type) {
{
var attribute = type.GetCustomAttribute<AVClassNameAttribute>(); var attribute = type.GetCustomAttribute<AVClassNameAttribute>();
return attribute != null ? attribute.ClassName : null; return attribute?.ClassName;
} }
} }
} }

View File

@ -11,7 +11,7 @@ namespace LeanCloud.Storage.Internal {
private readonly ReaderWriterLockSlim mutex; private readonly ReaderWriterLockSlim mutex;
private readonly IDictionary<string, ObjectSubclassInfo> registeredSubclasses; private readonly IDictionary<string, ObjectSubclassInfo> registeredSubclasses;
private Dictionary<string, Action> registerActions; private readonly Dictionary<string, Action> registerActions;
public ObjectSubclassingController() { public ObjectSubclassingController() {
mutex = new ReaderWriterLockSlim(); mutex = new ReaderWriterLockSlim();
@ -38,10 +38,8 @@ namespace LeanCloud.Storage.Internal {
} }
public bool IsTypeValid(string className, Type type) { public bool IsTypeValid(string className, Type type) {
ObjectSubclassInfo subclassInfo = null;
mutex.EnterReadLock(); mutex.EnterReadLock();
registeredSubclasses.TryGetValue(className, out subclassInfo); registeredSubclasses.TryGetValue(className, out ObjectSubclassInfo subclassInfo);
mutex.ExitReadLock(); mutex.ExitReadLock();
return subclassInfo == null return subclassInfo == null
@ -63,12 +61,12 @@ namespace LeanCloud.Storage.Internal {
// TOCTTOU bug. // TOCTTOU bug.
mutex.EnterWriteLock(); mutex.EnterWriteLock();
ObjectSubclassInfo previousInfo = null; if (registeredSubclasses.TryGetValue(className, out ObjectSubclassInfo previousInfo)) {
if (registeredSubclasses.TryGetValue(className, out previousInfo)) {
if (typeInfo.IsAssignableFrom(previousInfo.TypeInfo)) { if (typeInfo.IsAssignableFrom(previousInfo.TypeInfo)) {
// Previous subclass is more specific or equal to the current type, do nothing. // Previous subclass is more specific or equal to the current type, do nothing.
return; return;
} else if (previousInfo.TypeInfo.IsAssignableFrom(typeInfo)) { }
if (previousInfo.TypeInfo.IsAssignableFrom(typeInfo)) {
// Previous subclass is parent of new child, fallthrough and actually register // Previous subclass is parent of new child, fallthrough and actually register
// this class. // this class.
/* Do nothing */ /* Do nothing */
@ -91,10 +89,9 @@ namespace LeanCloud.Storage.Internal {
mutex.ExitWriteLock(); mutex.ExitWriteLock();
} }
Action toPerform;
mutex.EnterReadLock(); mutex.EnterReadLock();
registerActions.TryGetValue(className, out toPerform); registerActions.TryGetValue(className, out Action toPerform);
mutex.ExitReadLock(); mutex.ExitReadLock();
toPerform?.Invoke(); toPerform?.Invoke();
@ -113,10 +110,9 @@ namespace LeanCloud.Storage.Internal {
} }
public AVObject Instantiate(string className) { public AVObject Instantiate(string className) {
ObjectSubclassInfo info = null;
mutex.EnterReadLock(); mutex.EnterReadLock();
registeredSubclasses.TryGetValue(className, out info); registeredSubclasses.TryGetValue(className, out ObjectSubclassInfo info);
mutex.ExitReadLock(); mutex.ExitReadLock();
return info != null return info != null

View File

@ -1,20 +0,0 @@
using System;
using System.Collections.Generic;
namespace LeanCloud.Storage.Internal {
/// <summary>
/// So here's the deal. We have a lot of internal APIs for AVObject, AVUser, etc.
///
/// These cannot be 'internal' anymore if we are fully modularizing things out, because
/// they are no longer a part of the same library, especially as we create things like
/// Installation inside push library.
///
/// So this class contains a bunch of extension methods that can live inside another
/// namespace, which 'wrap' the intenral APIs that already exist.
/// </summary>
public static class AVFileExtensions {
public static AVFile Create(string name, Uri uri, string mimeType = null) {
return new AVFile(name, uri, mimeType);
}
}
}

View File

@ -192,7 +192,7 @@ namespace LeanCloud {
#endregion #endregion
public static IDictionary<String, String> GetPropertyMappings(string className) { public static IDictionary<string, string> GetPropertyMappings(string className) {
return SubclassingController.GetPropertyMappings(className); return SubclassingController.GetPropertyMappings(className);
} }
@ -246,7 +246,7 @@ string propertyName
string propertyName string propertyName
#endif #endif
) { ) {
return GetProperty<T>(default(T), propertyName); return GetProperty<T>(default, propertyName);
} }
/// <summary> /// <summary>
@ -262,9 +262,8 @@ string propertyName
#else #else
string propertyName string propertyName
#endif #endif
) { ) {
T result; if (TryGetValue(GetFieldForPropertyName(ClassName, propertyName), out T result)) {
if (TryGetValue<T>(GetFieldForPropertyName(ClassName, propertyName), out result)) {
return result; return result;
} }
return defaultValue; return defaultValue;