diff --git a/Storage/Storage.Test/ObjectControllerTests.cs b/Storage/Storage.Test/ObjectControllerTests.cs
new file mode 100644
index 0000000..fc99e8f
--- /dev/null
+++ b/Storage/Storage.Test/ObjectControllerTests.cs
@@ -0,0 +1,31 @@
+using NUnit.Framework;
+using System.Threading;
+using System.Threading.Tasks;
+using System.Runtime.CompilerServices;
+using LeanCloud;
+
+namespace LeanCloudTests {
+ public class ObjectControllerTests {
+ [SetUp]
+ public void SetUp() {
+ AVClient.Initialize(new AVClient.Configuration {
+ ApplicationId = "BMYV4RKSTwo8WSqt8q9ezcWF-gzGzoHsz",
+ ApplicationKey = "pbf6Nk5seyjilexdpyrPwjSp",
+ });
+ AVClient.HttpLog(TestContext.Out.WriteLine);
+ }
+
+ [Test]
+ [AsyncStateMachine(typeof(ObjectControllerTests))]
+ public async Task TestSave() {
+ TestContext.Out.WriteLine($"before at {Thread.CurrentThread.ManagedThreadId}");
+ var obj = AVObject.Create("Foo");
+ obj["content"] = "hello, world";
+ await obj.SaveAsync();
+ TestContext.Out.WriteLine($"saved at {Thread.CurrentThread.ManagedThreadId}");
+ Assert.NotNull(obj.ObjectId);
+ Assert.NotNull(obj.CreatedAt);
+ Assert.NotNull(obj.UpdatedAt);
+ }
+ }
+}
diff --git a/Storage/Storage.Test/ObjectTest.cs b/Storage/Storage.Test/ObjectTest.cs
new file mode 100644
index 0000000..49bb1ca
--- /dev/null
+++ b/Storage/Storage.Test/ObjectTest.cs
@@ -0,0 +1,58 @@
+using NUnit.Framework;
+using LeanCloud;
+using System.Net.Http;
+using System.Threading;
+using System.Threading.Tasks;
+using System.Runtime.CompilerServices;
+
+namespace LeanCloudTests {
+ public class ObjectTests {
+ [SetUp]
+ public void SetUp() {
+ AVClient.Initialize(new AVClient.Configuration {
+ ApplicationId = "BMYV4RKSTwo8WSqt8q9ezcWF-gzGzoHsz",
+ ApplicationKey = "pbf6Nk5seyjilexdpyrPwjSp",
+ RTMServer = "https://router-g0-push.avoscloud.com",
+ });
+ AVClient.HttpLog(TestContext.Out.WriteLine);
+ }
+
+ [Test]
+ public void TestAVObjectConstructor() {
+ AVObject obj = new AVObject("Foo");
+ Assert.AreEqual("Foo", obj.ClassName);
+ Assert.Null(obj.CreatedAt);
+ Assert.True(obj.IsDataAvailable);
+ Assert.True(obj.IsDirty);
+ }
+
+ [Test]
+ public void TestAVObjectCreate() {
+ AVObject obj = AVObject.CreateWithoutData("Foo", "5d356b1cd5de2b00837162ca");
+ Assert.AreEqual("Foo", obj.ClassName);
+ Assert.AreEqual("5d356b1cd5de2b00837162ca", obj.ObjectId);
+ Assert.Null(obj.CreatedAt);
+ Assert.False(obj.IsDataAvailable);
+ Assert.False(obj.IsDirty);
+ }
+
+ [Test]
+ public async Task TestHttp() {
+ if (SynchronizationContext.Current == null) {
+ TestContext.Out.WriteLine("is null");
+ }
+ TestContext.Out.WriteLine($"current {SynchronizationContext.Current}");
+ var client = new HttpClient();
+ TestContext.Out.WriteLine($"request at {Thread.CurrentThread.ManagedThreadId}");
+ string url = $"{AVClient.CurrentConfiguration.RTMServer}/v1/route?appId={AVClient.CurrentConfiguration.ApplicationId}&secure=1";
+ var res = await client.GetAsync(url);
+ TestContext.Out.WriteLine($"get at {Thread.CurrentThread.ManagedThreadId}");
+ var data = await res.Content.ReadAsStringAsync();
+ res.Dispose();
+ TestContext.Out.WriteLine($"response at {Thread.CurrentThread.ManagedThreadId}");
+ TestContext.Out.WriteLine(data);
+ Assert.Pass();
+ }
+
+ }
+}
\ No newline at end of file
diff --git a/Storage/Storage.Test/Storage.Test.csproj b/Storage/Storage.Test/Storage.Test.csproj
index fcae868..b44ec62 100644
--- a/Storage/Storage.Test/Storage.Test.csproj
+++ b/Storage/Storage.Test/Storage.Test.csproj
@@ -1,48 +1,18 @@
-
-
-
+
+
- Debug
- AnyCPU
- {04DA35BB-6473-4D99-8A33-F499D40047E6}
- Library
- Storage.Test
- Storage.Test
- v4.7
- 0.1.0
-
-
- true
- full
- false
- bin\Debug
- DEBUG;
- prompt
- 4
-
-
- true
- bin\Release
- prompt
- 4
+ netcoreapp2.2
+
+ false
+
-
-
- ..\..\packages\NUnit.3.12.0\lib\net45\nunit.framework.dll
-
+
+
+
+
-
+
-
-
-
-
-
- {659D19F0-9A40-42C0-886C-555E64F16848}
- Storage.PCL
-
-
-
-
\ No newline at end of file
+
diff --git a/Storage/Storage.Test/Test.cs b/Storage/Storage.Test/Test.cs
deleted file mode 100644
index e2edb6b..0000000
--- a/Storage/Storage.Test/Test.cs
+++ /dev/null
@@ -1,21 +0,0 @@
-using NUnit.Framework;
-using System;
-using System.Reflection;
-using System.Diagnostics;
-using LeanCloud;
-
-namespace Storage.Test {
- [TestFixture()]
- public class Test {
- [Test()]
- public void TestCase() {
- Assembly assembly = Assembly.GetEntryAssembly();
- var attr = assembly.GetCustomAttribute();
- Console.WriteLine(attr.InformationalVersion);
-
- FileVersionInfo versionInfo = FileVersionInfo.GetVersionInfo(assembly.Location);
- String version = versionInfo.FileVersion;
- Console.WriteLine(version);
- }
- }
-}
diff --git a/Storage/Storage.Test/packages.config b/Storage/Storage.Test/packages.config
deleted file mode 100644
index ded08cf..0000000
--- a/Storage/Storage.Test/packages.config
+++ /dev/null
@@ -1,4 +0,0 @@
-
-
-
-
\ No newline at end of file
diff --git a/Storage/Storage/Internal/AVCorePlugins.cs b/Storage/Storage/Internal/AVCorePlugins.cs
new file mode 100644
index 0000000..3b2fd06
--- /dev/null
+++ b/Storage/Storage/Internal/AVCorePlugins.cs
@@ -0,0 +1,384 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using LeanCloud.Storage.Internal;
+
+namespace LeanCloud.Storage.Internal
+{
+ public class AVPlugins : IAVCorePlugins
+ {
+ private static readonly object instanceMutex = new object();
+ private static IAVCorePlugins instance;
+ public static IAVCorePlugins Instance
+ {
+ get
+ {
+ lock (instanceMutex)
+ {
+ instance = instance ?? new AVPlugins();
+ return instance;
+ }
+ }
+ set
+ {
+ lock (instanceMutex)
+ {
+ instance = value;
+ }
+ }
+ }
+
+ private readonly object mutex = new object();
+
+ #region Server Controllers
+
+ private IHttpClient httpClient;
+ private IAppRouterController appRouterController;
+ private IAVCommandRunner commandRunner;
+ private IStorageController storageController;
+
+ private IAVCloudCodeController cloudCodeController;
+ private IAVConfigController configController;
+ private IAVFileController fileController;
+ private IAVObjectController objectController;
+ private IAVQueryController queryController;
+ private IAVSessionController sessionController;
+ private IAVUserController userController;
+ private IObjectSubclassingController subclassingController;
+
+ #endregion
+
+ #region Current Instance Controller
+
+ private IAVCurrentUserController currentUserController;
+ private IInstallationIdController installationIdController;
+
+ #endregion
+
+ public void Reset()
+ {
+ lock (mutex)
+ {
+ HttpClient = null;
+ AppRouterController = null;
+ CommandRunner = null;
+ StorageController = null;
+
+ CloudCodeController = null;
+ FileController = null;
+ ObjectController = null;
+ SessionController = null;
+ UserController = null;
+ SubclassingController = null;
+
+ CurrentUserController = null;
+ InstallationIdController = null;
+ }
+ }
+
+ public IHttpClient HttpClient
+ {
+ get
+ {
+ lock (mutex)
+ {
+ httpClient = httpClient ?? new HttpClient();
+ return httpClient;
+ }
+ }
+ set
+ {
+ lock (mutex)
+ {
+ httpClient = value;
+ }
+ }
+ }
+
+ public IAppRouterController AppRouterController
+ {
+ get
+ {
+ lock (mutex)
+ {
+ appRouterController = appRouterController ?? new AppRouterController();
+ return appRouterController;
+ }
+ }
+ set
+ {
+ lock (mutex)
+ {
+ appRouterController = value;
+ }
+ }
+ }
+
+ public IAVCommandRunner CommandRunner
+ {
+ get
+ {
+ lock (mutex)
+ {
+ commandRunner = commandRunner ?? new AVCommandRunner(HttpClient, InstallationIdController);
+ return commandRunner;
+ }
+ }
+ set
+ {
+ lock (mutex)
+ {
+ commandRunner = value;
+ }
+ }
+ }
+
+#if !UNITY
+ public IStorageController StorageController
+ {
+ get
+ {
+ lock (mutex)
+ {
+ storageController = storageController ?? new StorageController(AVClient.CurrentConfiguration.ApplicationId);
+ return storageController;
+ }
+ }
+ set
+ {
+ lock (mutex)
+ {
+ storageController = value;
+ }
+ }
+ }
+#endif
+#if UNITY
+ public IStorageController StorageController
+ {
+ get
+ {
+ lock (mutex)
+ {
+ storageController = storageController ?? new StorageController(AVInitializeBehaviour.IsWebPlayer, AVClient.CurrentConfiguration.ApplicationId);
+ return storageController;
+ }
+ }
+ set
+ {
+ lock (mutex)
+ {
+ storageController = value;
+ }
+ }
+ }
+#endif
+
+ public IAVCloudCodeController CloudCodeController
+ {
+ get
+ {
+ lock (mutex)
+ {
+ cloudCodeController = cloudCodeController ?? new AVCloudCodeController(CommandRunner);
+ return cloudCodeController;
+ }
+ }
+ set
+ {
+ lock (mutex)
+ {
+ cloudCodeController = value;
+ }
+ }
+ }
+
+ public IAVFileController FileController
+ {
+ get
+ {
+ lock (mutex)
+ {
+ if (AVClient.CurrentConfiguration.RegionValue == 0)
+ fileController = fileController ?? new QiniuFileController(CommandRunner);
+ else if (AVClient.CurrentConfiguration.RegionValue == 2)
+ fileController = fileController ?? new QCloudCosFileController(CommandRunner);
+ else if (AVClient.CurrentConfiguration.RegionValue == 1)
+ fileController = fileController ?? new AWSS3FileController(CommandRunner);
+
+ return fileController;
+ }
+ }
+ set
+ {
+ lock (mutex)
+ {
+ fileController = value;
+ }
+ }
+ }
+
+ public IAVConfigController ConfigController
+ {
+ get
+ {
+ lock (mutex)
+ {
+ if (configController == null)
+ {
+ configController = new AVConfigController(CommandRunner, StorageController);
+ }
+ return configController;
+ }
+ }
+ set
+ {
+ lock (mutex)
+ {
+ configController = value;
+ }
+ }
+ }
+
+ public IAVObjectController ObjectController
+ {
+ get
+ {
+ lock (mutex)
+ {
+ objectController = objectController ?? new AVObjectController(CommandRunner);
+ return objectController;
+ }
+ }
+ set
+ {
+ lock (mutex)
+ {
+ objectController = value;
+ }
+ }
+ }
+
+ public IAVQueryController QueryController
+ {
+ get
+ {
+ lock (mutex)
+ {
+ if (queryController == null)
+ {
+ queryController = new AVQueryController(CommandRunner);
+ }
+ return queryController;
+ }
+ }
+ set
+ {
+ lock (mutex)
+ {
+ queryController = value;
+ }
+ }
+ }
+
+ public IAVSessionController SessionController
+ {
+ get
+ {
+ lock (mutex)
+ {
+ sessionController = sessionController ?? new AVSessionController(CommandRunner);
+ return sessionController;
+ }
+ }
+ set
+ {
+ lock (mutex)
+ {
+ sessionController = value;
+ }
+ }
+ }
+
+ public IAVUserController UserController
+ {
+ get
+ {
+ lock (mutex)
+ {
+ userController = userController ?? new AVUserController(CommandRunner);
+ return userController;
+ }
+ }
+ set
+ {
+ lock (mutex)
+ {
+ userController = value;
+ }
+ }
+ }
+
+ public IAVCurrentUserController CurrentUserController
+ {
+ get
+ {
+ lock (mutex)
+ {
+ currentUserController = currentUserController ?? new AVCurrentUserController(StorageController);
+ return currentUserController;
+ }
+ }
+ set
+ {
+ lock (mutex)
+ {
+ currentUserController = value;
+ }
+ }
+ }
+
+ public IObjectSubclassingController SubclassingController
+ {
+ get
+ {
+ lock (mutex)
+ {
+ if (subclassingController == null)
+ {
+ subclassingController = new ObjectSubclassingController();
+ subclassingController.AddRegisterHook(typeof(AVUser), () => CurrentUserController.ClearFromMemory());
+ }
+ return subclassingController;
+ }
+ }
+ set
+ {
+ lock (mutex)
+ {
+ subclassingController = value;
+ }
+ }
+ }
+
+ public IInstallationIdController InstallationIdController
+ {
+ get
+ {
+ lock (mutex)
+ {
+ installationIdController = installationIdController ?? new InstallationIdController(StorageController);
+ return installationIdController;
+ }
+ }
+ set
+ {
+ lock (mutex)
+ {
+ installationIdController = value;
+ }
+ }
+ }
+ }
+}
diff --git a/Storage/Storage/Internal/AppRouter/AppRouterController.cs b/Storage/Storage/Internal/AppRouter/AppRouterController.cs
new file mode 100644
index 0000000..1e0c73d
--- /dev/null
+++ b/Storage/Storage/Internal/AppRouter/AppRouterController.cs
@@ -0,0 +1,103 @@
+using System;
+using System.Net;
+using System.Collections.Generic;
+using System.Threading;
+using System.Threading.Tasks;
+using LeanCloud.Storage.Internal;
+
+namespace LeanCloud.Storage.Internal
+{
+ public class AppRouterController : IAppRouterController
+ {
+ private AppRouterState currentState;
+ private object mutex = new object();
+
+ ///
+ /// Get current app's router state
+ ///
+ ///
+ public AppRouterState Get()
+ {
+ if (string.IsNullOrEmpty(AVClient.CurrentConfiguration.ApplicationId))
+ {
+ throw new AVException(AVException.ErrorCode.NotInitialized, "ApplicationId can not be null.");
+ }
+ AppRouterState state;
+ state = AppRouterState.GetInitial(AVClient.CurrentConfiguration.ApplicationId, AVClient.CurrentConfiguration.Region);
+
+ lock (mutex)
+ {
+ if (currentState != null)
+ {
+ if (!currentState.isExpired())
+ state = currentState;
+ }
+ }
+
+ if (state.isExpired())
+ {
+ lock (mutex)
+ {
+ state.FetchedAt = DateTime.Now + TimeSpan.FromMinutes(10);
+ }
+ Task.Factory.StartNew(RefreshAsync);
+ }
+ return state;
+ }
+
+ public Task RefreshAsync()
+ {
+ return QueryAsync(CancellationToken.None).ContinueWith(t =>
+ {
+ if (!t.IsFaulted && !t.IsCanceled)
+ {
+ lock (mutex)
+ {
+ currentState = t.Result;
+ }
+ }
+ });
+ }
+
+ public Task QueryAsync(CancellationToken cancellationToken)
+ {
+ string appId = AVClient.CurrentConfiguration.ApplicationId;
+ string url = string.Format("https://app-router.leancloud.cn/2/route?appId={0}", appId);
+
+ return AVClient.HttpGetAsync(new Uri(url)).ContinueWith(t =>
+ {
+ var tcs = new TaskCompletionSource();
+ if (t.Result.Item1 != HttpStatusCode.OK)
+ {
+ tcs.SetException(new AVException(AVException.ErrorCode.ConnectionFailed, "can not reach router.", null));
+ }
+ else
+ {
+ var result = Json.Parse(t.Result.Item2) as IDictionary;
+ tcs.SetResult(ParseAppRouterState(result));
+ }
+ return tcs.Task;
+ }).Unwrap();
+ }
+
+ private static AppRouterState ParseAppRouterState(IDictionary jsonObj)
+ {
+ var state = new AppRouterState()
+ {
+ TTL = (int)jsonObj["ttl"],
+ StatsServer = jsonObj["stats_server"] as string,
+ RealtimeRouterServer = jsonObj["rtm_router_server"] as string,
+ PushServer = jsonObj["push_server"] as string,
+ EngineServer = jsonObj["engine_server"] as string,
+ ApiServer = jsonObj["api_server"] as string,
+ Source = "network",
+ };
+ return state;
+ }
+
+ public void Clear()
+ {
+ currentState = null;
+ }
+ }
+}
diff --git a/Storage/Storage/Internal/AppRouter/AppRouterState.cs b/Storage/Storage/Internal/AppRouter/AppRouterState.cs
new file mode 100644
index 0000000..b2e61f9
--- /dev/null
+++ b/Storage/Storage/Internal/AppRouter/AppRouterState.cs
@@ -0,0 +1,85 @@
+using System;
+
+namespace LeanCloud.Storage.Internal
+{
+ public class AppRouterState
+ {
+ public long TTL { get; internal set; }
+ public string ApiServer { get; internal set; }
+ public string EngineServer { get; internal set; }
+ public string PushServer { get; internal set; }
+ public string RealtimeRouterServer { get; internal set; }
+ public string StatsServer { get; internal set; }
+ public string Source { get; internal set; }
+
+ public DateTime FetchedAt { get; internal set; }
+
+ private static object mutex = new object();
+
+ public AppRouterState()
+ {
+ FetchedAt = DateTime.Now;
+ }
+
+ ///
+ /// Is this app router state expired.
+ ///
+ public bool isExpired()
+ {
+ return DateTime.Now > FetchedAt + TimeSpan.FromSeconds(TTL);
+ }
+
+ ///
+ /// Get the initial usable router state
+ ///
+ /// Current app's appId
+ /// Current app's region
+ /// Initial app router state
+ public static AppRouterState GetInitial(string appId, AVClient.Configuration.AVRegion region)
+ {
+ var regionValue = (int)region;
+ var prefix = appId.Substring(0, 8).ToLower();
+ switch (regionValue)
+ {
+ case 0:
+ // 华北
+ return new AppRouterState()
+ {
+ TTL = -1,
+ ApiServer = String.Format("{0}.api.lncld.net", prefix),
+ EngineServer = String.Format("{0}.engine.lncld.net", prefix),
+ PushServer = String.Format("{0}.push.lncld.net", prefix),
+ RealtimeRouterServer = String.Format("{0}.rtm.lncld.net", prefix),
+ StatsServer = String.Format("{0}.stats.lncld.net", prefix),
+ Source = "initial",
+ };
+ case 1:
+ // 美国
+ return new AppRouterState()
+ {
+ TTL = -1,
+ ApiServer = string.Format("{0}.api.lncldglobal.com", prefix),
+ EngineServer = string.Format("{0}.engine.lncldglobal.com", prefix),
+ PushServer = string.Format("{0}.push.lncldglobal.com", prefix),
+ RealtimeRouterServer = string.Format("{0}.rtm.lncldglobal.com", prefix),
+ StatsServer = string.Format("{0}.stats.lncldglobal.com", prefix),
+ Source = "initial",
+ };
+ case 2:
+ // 华东
+ return new AppRouterState() {
+ TTL = -1,
+ ApiServer = string.Format("{0}.api.lncldapi.com", prefix),
+ EngineServer = string.Format("{0}.engine.lncldapi.com", prefix),
+ PushServer = string.Format("{0}.push.lncldapi.com", prefix),
+ RealtimeRouterServer = string.Format("{0}.rtm.lncldapi.com", prefix),
+ StatsServer = string.Format("{0}.stats.lncldapi.com", prefix),
+ Source = "initial",
+ };
+ default:
+ throw new AVException(AVException.ErrorCode.OtherCause, "invalid region");
+ }
+ }
+
+ }
+}
\ No newline at end of file
diff --git a/Storage/Storage/Internal/AppRouter/IAppRouterController.cs b/Storage/Storage/Internal/AppRouter/IAppRouterController.cs
new file mode 100644
index 0000000..771ee18
--- /dev/null
+++ b/Storage/Storage/Internal/AppRouter/IAppRouterController.cs
@@ -0,0 +1,22 @@
+using System;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace LeanCloud.Storage.Internal
+{
+ public interface IAppRouterController
+ {
+ AppRouterState Get();
+ ///
+ /// Start refresh the app router.
+ ///
+ ///
+ Task RefreshAsync();
+ void Clear();
+ ///
+ /// Query the app router.
+ ///
+ /// New AppRouterState
+ Task QueryAsync(CancellationToken cancellationToken);
+ }
+}
diff --git a/Storage/Storage/Internal/Authentication/IAVAuthenticationProvider.cs b/Storage/Storage/Internal/Authentication/IAVAuthenticationProvider.cs
new file mode 100644
index 0000000..406e37c
--- /dev/null
+++ b/Storage/Storage/Internal/Authentication/IAVAuthenticationProvider.cs
@@ -0,0 +1,36 @@
+using System.Collections.Generic;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace LeanCloud.Storage.Internal {
+ public interface IAVAuthenticationProvider {
+ ///
+ /// Authenticates with the service.
+ ///
+ /// The cancellation token.
+ Task> AuthenticateAsync(CancellationToken cancellationToken);
+
+ ///
+ /// Deauthenticates (logs out) the user associated with this provider. This
+ /// call may block.
+ ///
+ void Deauthenticate();
+
+ ///
+ /// Restores authentication that has been serialized, such as session keys,
+ /// etc.
+ ///
+ /// The auth data for the provider. This value may be null
+ /// when unlinking an account.
+ /// true iff the authData was successfully synchronized. A false return
+ /// value indicates that the user should no longer be associated because of bad auth
+ /// data.
+ bool RestoreAuthentication(IDictionary authData);
+
+ ///
+ /// Provides a unique name for the type of authentication the provider does.
+ /// For example, the FacebookAuthenticationProvider would return "facebook".
+ ///
+ string AuthType { get; }
+ }
+}
diff --git a/Storage/Storage/Internal/Cloud/Controller/AVCloudCodeController.cs b/Storage/Storage/Internal/Cloud/Controller/AVCloudCodeController.cs
new file mode 100644
index 0000000..c6fa216
--- /dev/null
+++ b/Storage/Storage/Internal/Cloud/Controller/AVCloudCodeController.cs
@@ -0,0 +1,58 @@
+using System;
+using System.Collections.Generic;
+using System.Threading;
+using System.Threading.Tasks;
+using LeanCloud.Utilities;
+using LeanCloud.Storage.Internal;
+
+namespace LeanCloud.Storage.Internal
+{
+ public class AVCloudCodeController : IAVCloudCodeController
+ {
+ private readonly IAVCommandRunner commandRunner;
+
+ public AVCloudCodeController(IAVCommandRunner commandRunner)
+ {
+ this.commandRunner = commandRunner;
+ }
+
+ public Task CallFunctionAsync(String name,
+ IDictionary parameters,
+ string sessionToken,
+ CancellationToken cancellationToken)
+ {
+ var command = new AVCommand(string.Format("functions/{0}", Uri.EscapeUriString(name)),
+ method: "POST",
+ sessionToken: sessionToken,
+ data: NoObjectsEncoder.Instance.Encode(parameters) as IDictionary);
+
+ return commandRunner.RunCommandAsync(command, cancellationToken: cancellationToken).OnSuccess(t =>
+ {
+ var decoded = AVDecoder.Instance.Decode(t.Result.Item2) as IDictionary;
+ if (!decoded.ContainsKey("result"))
+ {
+ return default(T);
+ }
+ return Conversion.To(decoded["result"]);
+ });
+ }
+
+ public Task RPCFunction(string name, IDictionary parameters, string sessionToken, CancellationToken cancellationToken)
+ {
+ var command = new AVCommand(string.Format("call/{0}", Uri.EscapeUriString(name)),
+ method: "POST",
+ sessionToken: sessionToken,
+ data: PointerOrLocalIdEncoder.Instance.Encode(parameters) as IDictionary);
+
+ return commandRunner.RunCommandAsync(command, cancellationToken: cancellationToken).OnSuccess(t =>
+ {
+ var decoded = AVDecoder.Instance.Decode(t.Result.Item2) as IDictionary;
+ if (!decoded.ContainsKey("result"))
+ {
+ return default(T);
+ }
+ return Conversion.To(decoded["result"]);
+ });
+ }
+ }
+}
diff --git a/Storage/Storage/Internal/Cloud/Controller/IAVCloudCodeController.cs b/Storage/Storage/Internal/Cloud/Controller/IAVCloudCodeController.cs
new file mode 100644
index 0000000..ad6138f
--- /dev/null
+++ b/Storage/Storage/Internal/Cloud/Controller/IAVCloudCodeController.cs
@@ -0,0 +1,19 @@
+using System;
+using System.Collections.Generic;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace LeanCloud.Storage.Internal
+{
+ public interface IAVCloudCodeController
+ {
+ Task CallFunctionAsync(String name,
+ IDictionary parameters,
+ string sessionToken,
+ CancellationToken cancellationToken);
+
+ Task RPCFunction(string name, IDictionary parameters,
+ string sessionToken,
+ CancellationToken cancellationToken);
+ }
+}
diff --git a/Storage/Storage/Internal/Command/AVCommand.cs b/Storage/Storage/Internal/Command/AVCommand.cs
new file mode 100644
index 0000000..0d038bc
--- /dev/null
+++ b/Storage/Storage/Internal/Command/AVCommand.cs
@@ -0,0 +1,118 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Text;
+using LeanCloud.Storage.Internal;
+using System.Linq;
+
+namespace LeanCloud.Storage.Internal
+{
+ ///
+ /// AVCommand is an with pre-populated
+ /// headers.
+ ///
+ public class AVCommand : HttpRequest
+ {
+ public IDictionary DataObject { get; private set; }
+ public override Stream Data
+ {
+ get
+ {
+ if (base.Data != null)
+ {
+ return base.Data;
+ }
+
+ return base.Data = (DataObject != null
+ ? new MemoryStream(Encoding.UTF8.GetBytes(Json.Encode(DataObject)))
+ : null);
+ }
+ set { base.Data = value; }
+ }
+
+ public AVCommand(string relativeUri,
+ string method,
+ string sessionToken = null,
+ IList> headers = null,
+ IDictionary data = null) : this(relativeUri: relativeUri,
+ method: method,
+ sessionToken: sessionToken,
+ headers: headers,
+ stream: null,
+ contentType: data != null ? "application/json" : null)
+ {
+ DataObject = data;
+ }
+
+ public AVCommand(string relativeUri,
+ string method,
+ string sessionToken = null,
+ IList> headers = null,
+ Stream stream = null,
+ string contentType = null)
+ {
+ var state = AVPlugins.Instance.AppRouterController.Get();
+ var urlTemplate = "https://{0}/{1}/{2}";
+ AVClient.Configuration configuration = AVClient.CurrentConfiguration;
+ var apiVersion = "1.1";
+ if (relativeUri.StartsWith("push") || relativeUri.StartsWith("installations"))
+ {
+ Uri = new Uri(string.Format(urlTemplate, state.PushServer, apiVersion, relativeUri));
+ if (configuration.PushServer != null)
+ {
+ Uri = new Uri(string.Format("{0}{1}/{2}", configuration.PushServer, apiVersion, relativeUri));
+ }
+ }
+ else if (relativeUri.StartsWith("stats") || relativeUri.StartsWith("always_collect") || relativeUri.StartsWith("statistics"))
+ {
+ Uri = new Uri(string.Format(urlTemplate, state.StatsServer, apiVersion, relativeUri));
+ if (configuration.StatsServer != null)
+ {
+ Uri = new Uri(string.Format("{0}{1}/{2}", configuration.StatsServer, apiVersion, relativeUri));
+ }
+ }
+ else if (relativeUri.StartsWith("functions") || relativeUri.StartsWith("call"))
+ {
+ Uri = new Uri(string.Format(urlTemplate, state.EngineServer, apiVersion, relativeUri));
+
+ if (configuration.EngineServer != null)
+ {
+ Uri = new Uri(string.Format("{0}{1}/{2}", configuration.EngineServer, apiVersion, relativeUri));
+ }
+ }
+ else
+ {
+ Uri = new Uri(string.Format(urlTemplate, state.ApiServer, apiVersion, relativeUri));
+
+ if (configuration.ApiServer != null)
+ {
+ Uri = new Uri(string.Format("{0}{1}/{2}", configuration.ApiServer, apiVersion, relativeUri));
+ }
+ }
+ Method = method;
+ Data = stream;
+ Headers = new List>(headers ?? Enumerable.Empty>());
+
+ string useProduction = AVClient.UseProduction ? "1" : "0";
+ Headers.Add(new KeyValuePair("X-LC-Prod", useProduction));
+
+ if (!string.IsNullOrEmpty(sessionToken))
+ {
+ Headers.Add(new KeyValuePair("X-LC-Session", sessionToken));
+ }
+ if (!string.IsNullOrEmpty(contentType))
+ {
+ Headers.Add(new KeyValuePair("Content-Type", contentType));
+ }
+ }
+
+ public AVCommand(AVCommand other)
+ {
+ this.Uri = other.Uri;
+ this.Method = other.Method;
+ this.DataObject = other.DataObject;
+ this.Headers = new List>(other.Headers);
+ this.Data = other.Data;
+ }
+ }
+}
diff --git a/Storage/Storage/Internal/Command/AVCommandRunner.cs b/Storage/Storage/Internal/Command/AVCommandRunner.cs
new file mode 100644
index 0000000..cfbfc18
--- /dev/null
+++ b/Storage/Storage/Internal/Command/AVCommandRunner.cs
@@ -0,0 +1,167 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Net;
+using System.Threading;
+using System.Threading.Tasks;
+using LeanCloud.Storage.Internal;
+
+namespace LeanCloud.Storage.Internal
+{
+ ///
+ /// Command Runner.
+ ///
+ public class AVCommandRunner : IAVCommandRunner
+ {
+ private readonly IHttpClient httpClient;
+ private readonly IInstallationIdController installationIdController;
+
+ ///
+ ///
+ ///
+ ///
+ ///
+ public AVCommandRunner(IHttpClient httpClient, IInstallationIdController installationIdController)
+ {
+ this.httpClient = httpClient;
+ this.installationIdController = installationIdController;
+ }
+
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ public Task>> RunCommandAsync(AVCommand command,
+ IProgress uploadProgress = null,
+ IProgress downloadProgress = null,
+ CancellationToken cancellationToken = default(CancellationToken))
+ {
+ return PrepareCommand(command).ContinueWith(commandTask =>
+ {
+ var requestLog = commandTask.Result.ToLog();
+ AVClient.PrintLog("http=>" + requestLog);
+
+ return httpClient.ExecuteAsync(commandTask.Result, uploadProgress, downloadProgress, cancellationToken).OnSuccess(t =>
+ {
+ cancellationToken.ThrowIfCancellationRequested();
+
+ var response = t.Result;
+ var contentString = response.Item2;
+ int responseCode = (int)response.Item1;
+
+ var responseLog = responseCode + ";" + contentString;
+ AVClient.PrintLog("http<=" + responseLog);
+
+ if (responseCode >= 500)
+ {
+ // Server error, return InternalServerError.
+ throw new AVException(AVException.ErrorCode.InternalServerError, response.Item2);
+ }
+ else if (contentString != null)
+ {
+ IDictionary contentJson = null;
+ try
+ {
+ if (contentString.StartsWith("["))
+ {
+ var arrayJson = Json.Parse(contentString);
+ contentJson = new Dictionary { { "results", arrayJson } };
+ }
+ else
+ {
+ contentJson = Json.Parse(contentString) as IDictionary;
+ }
+ }
+ catch (Exception e)
+ {
+ throw new AVException(AVException.ErrorCode.OtherCause,
+ "Invalid response from server", e);
+ }
+ if (responseCode < 200 || responseCode > 299)
+ {
+ AVClient.PrintLog("error response code:" + responseCode);
+ int code = (int)(contentJson.ContainsKey("code") ? (int)contentJson["code"] : (int)AVException.ErrorCode.OtherCause);
+ string error = contentJson.ContainsKey("error") ?
+ contentJson["error"] as string : contentString;
+ AVException.ErrorCode ec = (AVException.ErrorCode)code;
+ throw new AVException(ec, error);
+ }
+ return new Tuple>(response.Item1,
+ contentJson);
+ }
+ return new Tuple>(response.Item1, null);
+ });
+ }).Unwrap();
+ }
+
+ private const string revocableSessionTokenTrueValue = "1";
+ private Task PrepareCommand(AVCommand command)
+ {
+ AVCommand newCommand = new AVCommand(command);
+
+ Task installationIdTask = installationIdController.GetAsync().ContinueWith(t =>
+ {
+ newCommand.Headers.Add(new KeyValuePair("X-LC-Installation-Id", t.Result.ToString()));
+ return newCommand;
+ });
+
+ AVClient.Configuration configuration = AVClient.CurrentConfiguration;
+ newCommand.Headers.Add(new KeyValuePair("X-LC-Id", configuration.ApplicationId));
+
+ long timestamp = (long)(DateTime.UtcNow - new DateTime(1970, 1, 1)).TotalMilliseconds;
+ if (!string.IsNullOrEmpty(configuration.MasterKey) && AVClient.UseMasterKey)
+ {
+ string sign = MD5.GetMd5String(timestamp + configuration.MasterKey);
+ newCommand.Headers.Add(new KeyValuePair("X-LC-Sign", string.Format("{0},{1},master", sign, timestamp)));
+ }
+ else
+ {
+ string sign = MD5.GetMd5String(timestamp + configuration.ApplicationKey);
+ newCommand.Headers.Add(new KeyValuePair("X-LC-Sign", string.Format("{0},{1}", sign, timestamp)));
+ }
+
+ newCommand.Headers.Add(new KeyValuePair("X-LC-Client-Version", AVClient.VersionString));
+
+ if (!string.IsNullOrEmpty(configuration.VersionInfo.BuildVersion))
+ {
+ newCommand.Headers.Add(new KeyValuePair("X-LC-App-Build-Version", configuration.VersionInfo.BuildVersion));
+ }
+ if (!string.IsNullOrEmpty(configuration.VersionInfo.DisplayVersion))
+ {
+ newCommand.Headers.Add(new KeyValuePair("X-LC-App-Display-Version", configuration.VersionInfo.DisplayVersion));
+ }
+ if (!string.IsNullOrEmpty(configuration.VersionInfo.OSVersion))
+ {
+ newCommand.Headers.Add(new KeyValuePair("X-LC-OS-Version", configuration.VersionInfo.OSVersion));
+ }
+
+ if (AVUser.IsRevocableSessionEnabled)
+ {
+ newCommand.Headers.Add(new KeyValuePair("X-LeanCloud-Revocable-Session", revocableSessionTokenTrueValue));
+ }
+
+ if (configuration.AdditionalHTTPHeaders != null)
+ {
+ var headersDictionary = newCommand.Headers.ToDictionary(kv => kv.Key, kv => kv.Value);
+ foreach (var header in configuration.AdditionalHTTPHeaders)
+ {
+ if (headersDictionary.ContainsKey(header.Key))
+ {
+ headersDictionary[header.Key] = header.Value;
+ }
+ else
+ {
+ newCommand.Headers.Add(header);
+ }
+ }
+ newCommand.Headers = headersDictionary.ToList();
+ }
+
+ return installationIdTask;
+ }
+ }
+}
diff --git a/Storage/Storage/Internal/Command/IAVCommandRunner.cs b/Storage/Storage/Internal/Command/IAVCommandRunner.cs
new file mode 100644
index 0000000..081623b
--- /dev/null
+++ b/Storage/Storage/Internal/Command/IAVCommandRunner.cs
@@ -0,0 +1,24 @@
+using System;
+using System.Collections.Generic;
+using System.Net;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace LeanCloud.Storage.Internal
+{
+ public interface IAVCommandRunner
+ {
+ ///
+ /// Executes and convert the result into Dictionary.
+ ///
+ /// The command to be run.
+ /// Upload progress callback.
+ /// Download progress callback.
+ /// The cancellation token for the request.
+ ///
+ Task>> RunCommandAsync(AVCommand command,
+ IProgress uploadProgress = null,
+ IProgress downloadProgress = null,
+ CancellationToken cancellationToken = default(CancellationToken));
+ }
+}
diff --git a/Storage/Storage/Internal/Config/Controller/AVConfigController.cs b/Storage/Storage/Internal/Config/Controller/AVConfigController.cs
new file mode 100644
index 0000000..15d34ff
--- /dev/null
+++ b/Storage/Storage/Internal/Config/Controller/AVConfigController.cs
@@ -0,0 +1,40 @@
+using System;
+using System.Threading.Tasks;
+using System.Threading;
+using LeanCloud.Storage.Internal;
+
+namespace LeanCloud.Storage.Internal {
+ ///
+ /// Config controller.
+ ///
+ internal class AVConfigController : IAVConfigController {
+ private readonly IAVCommandRunner commandRunner;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public AVConfigController(IAVCommandRunner commandRunner, IStorageController storageController) {
+ this.commandRunner = commandRunner;
+ CurrentConfigController = new AVCurrentConfigController(storageController);
+ }
+
+ public IAVCommandRunner CommandRunner { get; internal set; }
+ public IAVCurrentConfigController CurrentConfigController { get; internal set; }
+
+ public Task FetchConfigAsync(String sessionToken, CancellationToken cancellationToken) {
+ var command = new AVCommand("config",
+ method: "GET",
+ sessionToken: sessionToken,
+ data: null);
+
+ return commandRunner.RunCommandAsync(command, cancellationToken: cancellationToken).OnSuccess(task => {
+ cancellationToken.ThrowIfCancellationRequested();
+ return new AVConfig(task.Result.Item2);
+ }).OnSuccess(task => {
+ cancellationToken.ThrowIfCancellationRequested();
+ CurrentConfigController.SetCurrentConfigAsync(task.Result);
+ return task;
+ }).Unwrap();
+ }
+ }
+}
diff --git a/Storage/Storage/Internal/Config/Controller/AVCurrentConfigController.cs b/Storage/Storage/Internal/Config/Controller/AVCurrentConfigController.cs
new file mode 100644
index 0000000..41ba8d8
--- /dev/null
+++ b/Storage/Storage/Internal/Config/Controller/AVCurrentConfigController.cs
@@ -0,0 +1,76 @@
+using System;
+using System.Threading.Tasks;
+using System.Threading;
+using System.Collections.Generic;
+using LeanCloud.Storage.Internal;
+
+namespace LeanCloud.Storage.Internal {
+ ///
+ /// LeanCloud current config controller.
+ ///
+ internal class AVCurrentConfigController : IAVCurrentConfigController {
+ private const string CurrentConfigKey = "CurrentConfig";
+
+ private readonly TaskQueue taskQueue;
+ private AVConfig currentConfig;
+
+ private IStorageController storageController;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public AVCurrentConfigController(IStorageController storageController) {
+ this.storageController = storageController;
+
+ taskQueue = new TaskQueue();
+ }
+
+ public Task GetCurrentConfigAsync() {
+ return taskQueue.Enqueue(toAwait => toAwait.ContinueWith(_ => {
+ if (currentConfig == null) {
+ return storageController.LoadAsync().OnSuccess(t => {
+ object tmp;
+ t.Result.TryGetValue(CurrentConfigKey, out tmp);
+
+ string propertiesString = tmp as string;
+ if (propertiesString != null) {
+ var dictionary = AVClient.DeserializeJsonString(propertiesString);
+ currentConfig = new AVConfig(dictionary);
+ } else {
+ currentConfig = new AVConfig();
+ }
+
+ return currentConfig;
+ });
+ }
+
+ return Task.FromResult(currentConfig);
+ }), CancellationToken.None).Unwrap();
+ }
+
+ public Task SetCurrentConfigAsync(AVConfig config) {
+ return taskQueue.Enqueue(toAwait => toAwait.ContinueWith(_ => {
+ currentConfig = config;
+
+ var jsonObject = ((IJsonConvertible)config).ToJSON();
+ var jsonString = AVClient.SerializeJsonString(jsonObject);
+
+ return storageController.LoadAsync().OnSuccess(t => t.Result.AddAsync(CurrentConfigKey, jsonString));
+ }).Unwrap().Unwrap(), CancellationToken.None);
+ }
+
+ public Task ClearCurrentConfigAsync() {
+ return taskQueue.Enqueue(toAwait => toAwait.ContinueWith(_ => {
+ currentConfig = null;
+
+ return storageController.LoadAsync().OnSuccess(t => t.Result.RemoveAsync(CurrentConfigKey));
+ }).Unwrap().Unwrap(), CancellationToken.None);
+ }
+
+ public Task ClearCurrentConfigInMemoryAsync() {
+ return taskQueue.Enqueue(toAwait => toAwait.ContinueWith(_ => {
+ currentConfig = null;
+ }), CancellationToken.None);
+ }
+ }
+}
diff --git a/Storage/Storage/Internal/Config/Controller/IAVConfigController.cs b/Storage/Storage/Internal/Config/Controller/IAVConfigController.cs
new file mode 100644
index 0000000..5cef0ac
--- /dev/null
+++ b/Storage/Storage/Internal/Config/Controller/IAVConfigController.cs
@@ -0,0 +1,21 @@
+using System;
+using System.Threading.Tasks;
+using System.Threading;
+
+namespace LeanCloud.Storage.Internal {
+ public interface IAVConfigController {
+ ///
+ /// Gets the current config controller.
+ ///
+ /// The current config controller.
+ IAVCurrentConfigController CurrentConfigController { get; }
+
+ ///
+ /// Fetches the config from the server asynchronously.
+ ///
+ /// The config async.
+ /// Session token.
+ /// Cancellation token.
+ Task FetchConfigAsync(String sessionToken, CancellationToken cancellationToken);
+ }
+}
diff --git a/Storage/Storage/Internal/Config/Controller/IAVCurrentConfigController.cs b/Storage/Storage/Internal/Config/Controller/IAVCurrentConfigController.cs
new file mode 100644
index 0000000..759329c
--- /dev/null
+++ b/Storage/Storage/Internal/Config/Controller/IAVCurrentConfigController.cs
@@ -0,0 +1,31 @@
+using System;
+using System.Threading.Tasks;
+
+namespace LeanCloud.Storage.Internal {
+ public interface IAVCurrentConfigController {
+ ///
+ /// Gets the current config async.
+ ///
+ /// The current config async.
+ Task GetCurrentConfigAsync();
+
+ ///
+ /// Sets the current config async.
+ ///
+ /// The current config async.
+ /// Config.
+ Task SetCurrentConfigAsync(AVConfig config);
+
+ ///
+ /// Clears the current config async.
+ ///
+ /// The current config async.
+ Task ClearCurrentConfigAsync();
+
+ ///
+ /// Clears the current config in memory async.
+ ///
+ /// The current config in memory async.
+ Task ClearCurrentConfigInMemoryAsync();
+ }
+}
diff --git a/Storage/Storage/Internal/Encoding/AVDecoder.cs b/Storage/Storage/Internal/Encoding/AVDecoder.cs
new file mode 100644
index 0000000..fdb4303
--- /dev/null
+++ b/Storage/Storage/Internal/Encoding/AVDecoder.cs
@@ -0,0 +1,164 @@
+using System;
+using System.Linq;
+using System.Collections.Generic;
+using System.Globalization;
+using LeanCloud.Utilities;
+
+namespace LeanCloud.Storage.Internal
+{
+ public class AVDecoder
+ {
+ // This class isn't really a Singleton, but since it has no state, it's more efficient to get
+ // the default instance.
+ private static readonly AVDecoder instance = new AVDecoder();
+ public static AVDecoder Instance
+ {
+ get
+ {
+ return instance;
+ }
+ }
+
+ // Prevent default constructor.
+ private AVDecoder() { }
+
+ public object Decode(object data)
+ {
+ if (data == null)
+ {
+ return null;
+ }
+
+ var dict = data as IDictionary;
+ if (dict != null)
+ {
+ if (dict.ContainsKey("__op"))
+ {
+ return AVFieldOperations.Decode(dict);
+ }
+
+ object type;
+ dict.TryGetValue("__type", out type);
+ var typeString = type as string;
+
+ if (typeString == null)
+ {
+ var newDict = new Dictionary();
+ foreach (var pair in dict)
+ {
+ newDict[pair.Key] = Decode(pair.Value);
+ }
+ return newDict;
+ }
+
+ if (typeString == "Date")
+ {
+ return ParseDate(dict["iso"] as string);
+ }
+
+ if (typeString == "Bytes")
+ {
+ return Convert.FromBase64String(dict["base64"] as string);
+ }
+
+ if (typeString == "Pointer")
+ {
+ //set a include key to fetch or query.
+ if (dict.Keys.Count > 3)
+ {
+ return DecodeAVObject(dict);
+ }
+ return DecodePointer(dict["className"] as string, dict["objectId"] as string);
+ }
+
+ if (typeString == "File")
+ {
+ return DecodeAVFile(dict);
+ }
+
+ if (typeString == "GeoPoint")
+ {
+ return new AVGeoPoint(Conversion.To(dict["latitude"]),
+ Conversion.To(dict["longitude"]));
+ }
+
+ if (typeString == "Object")
+ {
+ return DecodeAVObject(dict);
+ }
+
+ if (typeString == "Relation")
+ {
+ return AVRelationBase.CreateRelation(null, null, dict["className"] as string);
+ }
+
+ var converted = new Dictionary();
+ foreach (var pair in dict)
+ {
+ converted[pair.Key] = Decode(pair.Value);
+ }
+ return converted;
+ }
+
+ var list = data as IList