diff --git a/Common/Common-Unity/Common-Unity.csproj b/Common/Common-Unity/Common-Unity.csproj index 63689bf..c8928d6 100644 --- a/Common/Common-Unity/Common-Unity.csproj +++ b/Common/Common-Unity/Common-Unity.csproj @@ -27,6 +27,9 @@ Task\LCTaskExtensions.cs + + Json\LCJsonConverter.cs + diff --git a/Common/Common/Common.csproj b/Common/Common/Common.csproj index 6c06788..dc91fba 100644 --- a/Common/Common/Common.csproj +++ b/Common/Common/Common.csproj @@ -3,7 +3,7 @@ netstandard2.0 0.1.0 - LeanCloud.Common + Common diff --git a/Storage/Storage/Internal/Http/LCJsonConverter.cs b/Common/Common/Json/LCJsonConverter.cs similarity index 91% rename from Storage/Storage/Internal/Http/LCJsonConverter.cs rename to Common/Common/Json/LCJsonConverter.cs index d79bd9d..3e78108 100644 --- a/Storage/Storage/Internal/Http/LCJsonConverter.cs +++ b/Common/Common/Json/LCJsonConverter.cs @@ -2,7 +2,7 @@ using System.Collections.Generic; using Newtonsoft.Json; -namespace LeanCloud.Storage.Internal { +namespace LeanCloud.Common { public class LCJsonConverter : JsonConverter { public override bool CanConvert(Type objectType) { return objectType == typeof(object); @@ -31,5 +31,7 @@ namespace LeanCloud.Storage.Internal { return serializer.Deserialize(reader); } + + public readonly static LCJsonConverter Default = new LCJsonConverter(); } } diff --git a/LiveQuery/LiveQuery-Unity/LiveQuery-Unity.csproj b/LiveQuery/LiveQuery-Unity/LiveQuery-Unity.csproj new file mode 100644 index 0000000..05fe632 --- /dev/null +++ b/LiveQuery/LiveQuery-Unity/LiveQuery-Unity.csproj @@ -0,0 +1,29 @@ + + + + netstandard2.0 + + + + + + + + Internal\LCLiveQueryHeartBeat.cs + + + Internal\LCLiveQueryConnection.cs + + + LCLiveQuery.cs + + + LCQueryExtension.cs + + + + + ..\..\UnityLibs\Newtonsoft.Json.dll + + + diff --git a/LiveQuery/LiveQuery.Test/LiveQuery.Test.csproj b/LiveQuery/LiveQuery.Test/LiveQuery.Test.csproj new file mode 100644 index 0000000..70a5ff8 --- /dev/null +++ b/LiveQuery/LiveQuery.Test/LiveQuery.Test.csproj @@ -0,0 +1,19 @@ + + + + netcoreapp2.2 + + false + 0.1.0 + + + + + + + + + + + + diff --git a/LiveQuery/LiveQuery.Test/LiveQuery.cs b/LiveQuery/LiveQuery.Test/LiveQuery.cs new file mode 100644 index 0000000..9689daf --- /dev/null +++ b/LiveQuery/LiveQuery.Test/LiveQuery.cs @@ -0,0 +1,180 @@ +using NUnit.Framework; +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using System.Net.Http; +using System.Net.Http.Headers; +using Newtonsoft.Json; +using LeanCloud; +using LeanCloud.Storage; +using LeanCloud.LiveQuery; + +using static NUnit.Framework.TestContext; + +namespace LiveQuery.Test { + internal class Account : LCObject { + internal int Balance { + get { + return (int)this["balance"]; + } + set { + this["balance"] = value; + } + } + + internal Account() : base("Account") { } + } + + public class LiveQuery { + private LCLiveQuery liveQuery; + + private Account account; + + [SetUp] + public async Task Setup() { + LCLogger.LogDelegate += Print; + LCApplication.Initialize("ikGGdRE2YcVOemAaRbgp1xGJ-gzGzoHsz", + "NUKmuRbdAhg1vrb2wexYo1jo", + "https://ikggdre2.lc-cn-n1-shared.com"); + + LCObject.RegisterSubclass("Account", () => new Account()); + + LCQuery query = new LCQuery("Account"); + query.WhereGreaterThan("balance", 100); + liveQuery = await query.Subscribe(); + } + + [TearDown] + public void TearDown() { + LCLogger.LogDelegate -= Print; + } + + [Test] + [Order(0)] + public async Task Create() { + TaskCompletionSource tcs = new TaskCompletionSource(); + + liveQuery.OnCreate = (obj) => { + WriteLine($"******** create: {obj}"); + tcs.SetResult(null); + }; + account = new Account { + Balance = 110 + }; + await account.Save(); + + await tcs.Task; + } + + [Test] + [Order(1)] + public async Task Update() { + TaskCompletionSource tcs = new TaskCompletionSource(); + + liveQuery.OnUpdate = (obj, updatedKeys) => { + WriteLine($"******** update: {obj}"); + tcs.SetResult(null); + }; + account.Balance = 120; + await account.Save(); + + await tcs.Task; + } + + [Test] + [Order(2)] + public async Task Leave() { + TaskCompletionSource tcs = new TaskCompletionSource(); + + liveQuery.OnLeave = (obj, updatedKeys) => { + WriteLine($"******** level: {obj}"); + tcs.SetResult(null); + }; + account.Balance = 80; + await account.Save(); + + await tcs.Task; + } + + [Test] + [Order(3)] + public async Task Enter() { + TaskCompletionSource tcs = new TaskCompletionSource(); + + liveQuery.OnEnter = (obj, updatedKeys) => { + WriteLine($"******** enter: {obj}"); + tcs.SetResult(null); + }; + account.Balance = 120; + await account.Save(); + + await tcs.Task; + } + + [Test] + [Order(4)] + public async Task Delete() { + TaskCompletionSource tcs = new TaskCompletionSource(); + + liveQuery.OnDelete = (objId) => { + WriteLine($"******** delete: {objId}"); + tcs.SetResult(null); + }; + await account.Delete(); + + await tcs.Task; + } + + [Test] + [Order(5)] + public async Task Login() { + TaskCompletionSource tcs = new TaskCompletionSource(); + + await LCUser.Login("hello", "world"); + LCQuery userQuery = LCUser.GetQuery(); + userQuery.WhereEqualTo("username", "hello"); + LCLiveQuery userLiveQuery = await userQuery.Subscribe(); + userLiveQuery.OnLogin = (user) => { + WriteLine($"login: {user}"); + tcs.SetResult(null); + }; + + // 模拟 REST API + string url = "https://ikggdre2.lc-cn-n1-shared.com/1.1/login"; + HttpRequestMessage request = new HttpRequestMessage { + RequestUri = new Uri(url), + Method = HttpMethod.Post + }; + request.Headers.Add("X-LC-Id", "ikGGdRE2YcVOemAaRbgp1xGJ-gzGzoHsz"); + request.Headers.Add("X-LC-Key", "NUKmuRbdAhg1vrb2wexYo1jo"); + string content = JsonConvert.SerializeObject(new Dictionary { + { "username", "hello" }, + { "password", "world" } + }); + StringContent requestContent = new StringContent(content); + requestContent.Headers.ContentType = new MediaTypeHeaderValue("application/json"); + request.Content = requestContent; + HttpClient client = new HttpClient(); + await client.SendAsync(request, HttpCompletionOption.ResponseHeadersRead); + + await tcs.Task; + } + + private static void Print(LCLogLevel level, string info) { + switch (level) { + case LCLogLevel.Debug: + WriteLine($"[DEBUG] {info}\n"); + break; + case LCLogLevel.Warn: + WriteLine($"[WARNING] {info}\n"); + break; + case LCLogLevel.Error: + WriteLine($"[ERROR] {info}\n"); + break; + default: + WriteLine(info); + break; + } + } + } +} \ No newline at end of file diff --git a/LiveQuery/LiveQuery/Internal/LCLiveQueryConnection.cs b/LiveQuery/LiveQuery/Internal/LCLiveQueryConnection.cs new file mode 100644 index 0000000..7203eac --- /dev/null +++ b/LiveQuery/LiveQuery/Internal/LCLiveQueryConnection.cs @@ -0,0 +1,230 @@ +using System; +using System.Text; +using System.Collections.Generic; +using System.Threading.Tasks; +using Newtonsoft.Json; +using LeanCloud.Realtime.Internal.Router; +using LeanCloud.Realtime.Internal.WebSocket; +using LeanCloud.Common; +using LeanCloud.Storage; + +namespace LeanCloud.LiveQuery.Internal { + public class LCLiveQueryConnection { + /// + /// 发送超时 + /// + private const int SEND_TIMEOUT = 10000; + + /// + /// 最大重连次数,超过后重置 Router 缓存后再次尝试重连 + /// + private const int MAX_RECONNECT_TIMES = 10; + + /// + /// 重连间隔 + /// + private const int RECONNECT_INTERVAL = 10000; + + /// + /// 子协议 + /// + private const string SUB_PROTOCOL = "lc.json.3"; + + /// + /// 通知事件 + /// + internal Action> OnNotification; + + /// + /// 断线事件 + /// + internal Action OnDisconnect; + + /// + /// 重连成功事件 + /// + internal Action OnReconnected; + + internal string id; + + /// + /// 请求回调缓存 + /// + private readonly Dictionary>> responses; + + private int requestI = 1; + + private LCRTMRouter router; + + private LCLiveQueryHeartBeat heartBeat; + + private LCWebSocketClient client; + + public LCLiveQueryConnection(string id) { + this.id = id; + responses = new Dictionary>>(); + heartBeat = new LCLiveQueryHeartBeat(this); + router = new LCRTMRouter(); + client = new LCWebSocketClient { + OnMessage = OnClientMessage, + OnClose = OnClientDisconnect + }; + } + + public async Task Connect() { + try { + LCRTMServer rtmServer = await router.GetServer(); + try { + LCLogger.Debug($"Primary Server"); + await client.Connect(rtmServer.Primary, SUB_PROTOCOL); + } catch (Exception e) { + LCLogger.Error(e); + LCLogger.Debug($"Secondary Server"); + await client.Connect(rtmServer.Secondary, SUB_PROTOCOL); + } + } catch (Exception e) { + throw e; + } + } + + /// + /// 重置连接 + /// + /// + internal async Task Reset() { + // 关闭就连接 + await client.Close(); + // 重新创建连接组件 + heartBeat = new LCLiveQueryHeartBeat(this); + router = new LCRTMRouter(); + client = new LCWebSocketClient { + OnMessage = OnClientMessage, + OnClose = OnClientDisconnect + }; + await Reconnect(); + } + + /// + /// 发送请求,会在收到应答后返回 + /// + /// + /// + internal async Task> SendRequest(Dictionary request) { + TaskCompletionSource> tcs = new TaskCompletionSource>(); + int requestIndex = requestI++; + request["i"] = requestIndex; + responses.Add(requestIndex, tcs); + try { + string json = JsonConvert.SerializeObject(request); + await SendText(json); + } catch (Exception e) { + tcs.TrySetException(e); + } + return await tcs.Task; + } + + /// + /// 发送文本消息 + /// + /// + /// + internal async Task SendText(string text) { + LCLogger.Debug($"{id} => {text}"); + Task sendTask = client.Send(text); + if (await Task.WhenAny(sendTask, Task.Delay(SEND_TIMEOUT)) == sendTask) { + await sendTask; + } else { + throw new TimeoutException("Send request time out"); + } + } + + /// + /// 关闭连接 + /// + /// + internal async Task Close() { + OnNotification = null; + OnDisconnect = null; + OnReconnected = null; + heartBeat.Stop(); + await client.Close(); + } + + private void OnClientMessage(byte[] bytes) { + _ = heartBeat.Refresh(OnPingTimeout); + try { + string json = Encoding.UTF8.GetString(bytes); + Dictionary msg = JsonConvert.DeserializeObject>(json, + LCJsonConverter.Default); + LCLogger.Debug($"{id} <= {json}"); + if (msg.TryGetValue("i", out object i)) { + int requestIndex = Convert.ToInt32(i); + if (responses.TryGetValue(requestIndex, out TaskCompletionSource> tcs)) { + if (msg.TryGetValue("error", out object error)) { + // 错误 + if (error is Dictionary dict) { + int code = Convert.ToInt32(dict["code"]); + string detail = dict["detail"] as string; + tcs.SetException(new LCException(code, detail)); + } else { + tcs.SetException(new Exception(error as string)); + } + } else { + tcs.SetResult(msg); + } + responses.Remove(requestIndex); + } else { + LCLogger.Error($"No request for {requestIndex}"); + } + } else { + // 通知 + OnNotification?.Invoke(msg); + } + } catch (Exception e) { + LCLogger.Error(e); + } + } + + private void OnClientDisconnect() { + heartBeat.Stop(); + OnDisconnect?.Invoke(); + // 重连 + _ = Reconnect(); + } + + private async void OnPingTimeout() { + await client.Close(); + OnClientDisconnect(); + } + + private async Task Reconnect() { + while (true) { + int reconnectCount = 0; + // 重连策略 + while (reconnectCount < MAX_RECONNECT_TIMES) { + try { + LCLogger.Debug($"Reconnecting... {reconnectCount}"); + await Connect(); + break; + } catch (Exception e) { + reconnectCount++; + LCLogger.Error(e); + LCLogger.Debug($"Reconnect after {RECONNECT_INTERVAL}ms"); + await Task.Delay(RECONNECT_INTERVAL); + } + } + if (reconnectCount < MAX_RECONNECT_TIMES) { + // 重连成功 + LCLogger.Debug("Reconnected"); + client.OnMessage = OnClientMessage; + client.OnClose = OnClientDisconnect; + OnReconnected?.Invoke(); + break; + } else { + // 重置 Router,继续尝试重连 + router = new LCRTMRouter(); + } + } + } + } +} diff --git a/LiveQuery/LiveQuery/Internal/LCLiveQueryHeartBeat.cs b/LiveQuery/LiveQuery/Internal/LCLiveQueryHeartBeat.cs new file mode 100644 index 0000000..a0fec18 --- /dev/null +++ b/LiveQuery/LiveQuery/Internal/LCLiveQueryHeartBeat.cs @@ -0,0 +1,55 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using System.Collections.Generic; + +namespace LeanCloud.LiveQuery.Internal { + /// + /// LiveQuery 心跳控制器 + /// + internal class LCLiveQueryHeartBeat { + private const int PING_INTERVAL = 5000; + private const int PONG_INTERVAL = 5000; + + private readonly LCLiveQueryConnection connection; + + private CancellationTokenSource pingCTS; + private CancellationTokenSource pongCTS; + + internal LCLiveQueryHeartBeat(LCLiveQueryConnection connection) { + this.connection = connection; + } + + internal async Task Refresh(Action onTimeout) { + LCLogger.Debug("LiveQuery HeartBeat refresh"); + Stop(); + + pingCTS = new CancellationTokenSource(); + Task delayTask = Task.Delay(PING_INTERVAL, pingCTS.Token); + await delayTask; + if (delayTask.IsCanceled) { + return; + } + // 发送 Ping 包 + LCLogger.Debug("Ping ~~~"); + _ = connection.SendText("{}"); + + // 等待 Pong + pongCTS = new CancellationTokenSource(); + Task timeoutTask = Task.Delay(PONG_INTERVAL, pongCTS.Token); + await timeoutTask; + if (timeoutTask.IsCanceled) { + return; + } + + // 超时 + LCLogger.Debug("Ping timeout"); + onTimeout?.Invoke(); + } + + internal void Stop() { + pingCTS?.Cancel(); + pongCTS?.Cancel(); + } + } +} diff --git a/LiveQuery/LiveQuery/LCLiveQuery.cs b/LiveQuery/LiveQuery/LCLiveQuery.cs new file mode 100644 index 0000000..c6f96a6 --- /dev/null +++ b/LiveQuery/LiveQuery/LCLiveQuery.cs @@ -0,0 +1,265 @@ +using System; +using System.Linq; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Threading.Tasks; +using LeanCloud.Storage; +using LeanCloud.Storage.Internal.Object; +using LeanCloud.LiveQuery.Internal; + +namespace LeanCloud.LiveQuery { + /// + /// LiveQuery + /// + public class LCLiveQuery { + /// + /// 新对象创建事件 + /// + public Action OnCreate; + /// + /// 对象更新事件 + /// + public Action> OnUpdate; + /// + /// 对象被删除 + /// + public Action OnDelete; + /// + /// 有新的满足条件的对象产生 + /// + public Action> OnEnter; + /// + /// 不再满足条件 + /// + public Action> OnLeave; + /// + /// 当一个用户登录成功 + /// + public Action OnLogin; + + public string Id { + get; private set; + } + + public LCQuery Query { + get; internal set; + } + + private static LCLiveQueryConnection connection; + + private static Dictionary> liveQueries = new Dictionary>(); + + internal LCLiveQuery() { + + } + + private static readonly string DeviceId = Guid.NewGuid().ToString(); + + /// + /// 订阅 + /// + /// + public async Task Subscribe() { + // TODO 判断当前连接情况 + if (connection == null) { + connection = new LCLiveQueryConnection(DeviceId) { + OnReconnected = OnReconnected, + OnNotification = OnNotification + }; + await connection.Connect(); + await Login(); + } + Dictionary queryData = new Dictionary { + { "className", Query.ClassName }, + { "where", Query.Condition.Encode() } + }; + Dictionary data = new Dictionary { + { "query", queryData }, + { "id", DeviceId }, + { "clientTimestamp", DateTimeOffset.Now.ToUnixTimeMilliseconds() } + }; + LCUser user = await LCUser.GetCurrent(); + if (user != null && !string.IsNullOrEmpty(user.SessionToken)) { + data.Add("sessionToken", user.SessionToken); + } + string path = "LiveQuery/subscribe"; + Dictionary result = await LCApplication.HttpClient.Post>(path, + data: data); + if (result.TryGetValue("query_id", out object id)) { + Id = id as string; + WeakReference weakRef = new WeakReference(this); + liveQueries[Id] = weakRef; + } + } + + /// + /// 取消订阅 + /// + /// + public async Task Unsubscribe() { + Dictionary data = new Dictionary { + { "id", DeviceId }, + { "query_id", Id } + }; + string path = "LiveQuery/unsubscribe"; + await LCApplication.HttpClient.Post>(path, + data: data); + // 移除 + liveQueries.Remove(Id); + } + + private static async Task Login() { + Dictionary data = new Dictionary { + { "cmd", "login" }, + { "appId", LCApplication.AppId }, + { "installationId", DeviceId }, + { "clientTs", DateTimeOffset.Now.ToUnixTimeMilliseconds() }, + { "service", 1 } + }; + await connection.SendRequest(data); + } + + private static async void OnReconnected() { + await Login(); + Dictionary> oldLiveQueries = liveQueries; + liveQueries = new Dictionary>(); + foreach (WeakReference weakRef in oldLiveQueries.Values) { + if (weakRef.TryGetTarget(out LCLiveQuery liveQuery)) { + await liveQuery.Subscribe(); + } + } + } + + private static void OnNotification(Dictionary notification) { + if (!notification.TryGetValue("cmd", out object cmd) || + !"data".Equals(cmd)) { + return; + } + if (!notification.TryGetValue("msg", out object msg) || + !(msg is IEnumerable list)) { + return; + } + + foreach (object item in list) { + if (item is Dictionary dict) { + if (!dict.TryGetValue("op", out object op)) { + continue; + } + switch (op as string) { + case "create": + OnCreateNotification(dict); + break; + case "update": + OnUpdateNotification(dict); + break; + case "enter": + OnEnterNotification(dict); + break; + case "leave": + OnLeaveNotification(dict); + break; + case "delete": + OnDeleteNotification(dict); + break; + case "login": + OnLoginNotification(dict); + break; + default: + LCLogger.Debug($"Not support: {op}"); + break; + } + } + } + } + + private static void OnCreateNotification(Dictionary data) { + if (TryGetLiveQuery(data, out LCLiveQuery liveQuery) && + TryGetObject(data, out LCObject obj)) { + liveQuery.OnCreate?.Invoke(obj); + } + } + + private static void OnUpdateNotification(Dictionary data) { + if (TryGetLiveQuery(data, out LCLiveQuery liveQuery) && + TryGetObject(data, out LCObject obj) && + TryGetUpdatedKeys(data, out ReadOnlyCollection keys)) { + liveQuery.OnUpdate?.Invoke(obj, keys); + } + } + + private static void OnEnterNotification(Dictionary data) { + if (TryGetLiveQuery(data, out LCLiveQuery liveQuery) && + TryGetObject(data, out LCObject obj) && + TryGetUpdatedKeys(data, out ReadOnlyCollection keys)) { + liveQuery.OnEnter?.Invoke(obj, keys); + } + } + + private static void OnLeaveNotification(Dictionary data) { + if (TryGetLiveQuery(data, out LCLiveQuery liveQuery) && + TryGetObject(data, out LCObject obj) && + TryGetUpdatedKeys(data, out ReadOnlyCollection keys)) { + liveQuery.OnLeave?.Invoke(obj, keys); + } + } + + private static void OnDeleteNotification(Dictionary data) { + if (TryGetLiveQuery(data, out LCLiveQuery liveQuery) && + TryGetObject(data, out LCObject obj)) { + liveQuery.OnDelete?.Invoke(obj.ObjectId); + } + } + + private static void OnLoginNotification(Dictionary data) { + if (TryGetLiveQuery(data, out LCLiveQuery liveQuery) && + data.TryGetValue("object", out object obj) && + obj is Dictionary dict) { + LCObjectData objectData = LCObjectData.Decode(dict); + LCUser user = new LCUser(objectData); + liveQuery.OnLogin?.Invoke(user); + } + } + + private static bool TryGetLiveQuery(Dictionary data, out LCLiveQuery liveQuery) { + if (!data.TryGetValue("query_id", out object i) || + !(i is string id)) { + liveQuery = null; + return false; + } + + if (!liveQueries.TryGetValue(id, out WeakReference weakRef) || + !weakRef.TryGetTarget(out LCLiveQuery lq)) { + liveQuery = null; + return false; + } + + liveQuery = lq; + return true; + } + + private static bool TryGetObject(Dictionary data, out LCObject obj) { + if (!data.TryGetValue("object", out object o) || + !(o is Dictionary dict)) { + obj = null; + return false; + } + + LCObjectData objectData = LCObjectData.Decode(dict); + obj = LCObject.Create(dict["className"] as string); + obj.Merge(objectData); + return true; + } + + private static bool TryGetUpdatedKeys(Dictionary data, out ReadOnlyCollection keys) { + if (!data.TryGetValue("updatedKeys", out object uks) || + !(uks is List list)) { + keys = null; + return false; + } + + keys = list.Cast().ToList() + .AsReadOnly(); + return true; + } + } +} diff --git a/LiveQuery/LiveQuery/LCQueryExtension.cs b/LiveQuery/LiveQuery/LCQueryExtension.cs new file mode 100644 index 0000000..66c7ec9 --- /dev/null +++ b/LiveQuery/LiveQuery/LCQueryExtension.cs @@ -0,0 +1,14 @@ +using System.Threading.Tasks; +using LeanCloud.Storage; + +namespace LeanCloud.LiveQuery { + public static class LCQueryExtension { + public static async Task Subscribe(this LCQuery query) { + LCLiveQuery liveQuery = new LCLiveQuery { + Query = query + }; + await liveQuery.Subscribe(); + return liveQuery; + } + } +} diff --git a/LiveQuery/LiveQuery/LiveQuery.csproj b/LiveQuery/LiveQuery/LiveQuery.csproj new file mode 100644 index 0000000..37bce0c --- /dev/null +++ b/LiveQuery/LiveQuery/LiveQuery.csproj @@ -0,0 +1,14 @@ + + + + netstandard2.0 + 0.1.0 + + + + + + + + + diff --git a/Realtime/Realtime/Internal/Connection/LCConnection.cs b/Realtime/Realtime/Internal/Connection/LCConnection.cs index 2add3be..c52a2d9 100644 --- a/Realtime/Realtime/Internal/Connection/LCConnection.cs +++ b/Realtime/Realtime/Internal/Connection/LCConnection.cs @@ -33,6 +33,11 @@ namespace LeanCloud.Realtime.Internal.Connection { /// private const int HEART_BEAT_INTERVAL = 30000; + /// + /// 子协议 + /// + private const string SUB_PROTOCOL = "lc.protobuf2.3"; + /// /// 通知事件 /// @@ -79,11 +84,11 @@ namespace LeanCloud.Realtime.Internal.Connection { LCRTMServer rtmServer = await router.GetServer(); try { LCLogger.Debug($"Primary Server"); - await client.Connect(rtmServer.Primary); + await client.Connect(rtmServer.Primary, SUB_PROTOCOL); } catch (Exception e) { LCLogger.Error(e); LCLogger.Debug($"Secondary Server"); - await client.Connect(rtmServer.Secondary); + await client.Connect(rtmServer.Secondary, SUB_PROTOCOL); } } catch (Exception e) { throw e; diff --git a/Realtime/Realtime/Internal/Controller/LCIMConversationController.cs b/Realtime/Realtime/Internal/Controller/LCIMConversationController.cs index cd7836a..af0455f 100644 --- a/Realtime/Realtime/Internal/Controller/LCIMConversationController.cs +++ b/Realtime/Realtime/Internal/Controller/LCIMConversationController.cs @@ -5,7 +5,6 @@ using System.Collections.ObjectModel; using System.Threading.Tasks; using Newtonsoft.Json; using LeanCloud.Realtime.Internal.Protocol; -using LeanCloud.Storage.Internal; using LeanCloud.Storage.Internal.Codec; using LeanCloud.Common; @@ -441,7 +440,8 @@ namespace LeanCloud.Realtime.Internal.Controller { command.ConvMessage = convMessage; GenericCommand response = await Connection.SendRequest(command); JsonObjectMessage results = response.ConvMessage.Results; - List convs = JsonConvert.DeserializeObject>(results.Data, new LCJsonConverter()); + List convs = JsonConvert.DeserializeObject>(results.Data, + LCJsonConverter.Default); return convs.Select(item => { Dictionary conv = item as Dictionary; string convId = conv["objectId"] as string; @@ -478,7 +478,8 @@ namespace LeanCloud.Realtime.Internal.Controller { request.ConvMessage = convMessage; GenericCommand response = await Connection.SendRequest(request); JsonObjectMessage results = response.ConvMessage.Results; - List convs = JsonConvert.DeserializeObject>(results.Data, new LCJsonConverter()); + List convs = JsonConvert.DeserializeObject>(results.Data, + LCJsonConverter.Default); List convList = convs.Select(item => { LCIMTemporaryConversation temporaryConversation = new LCIMTemporaryConversation(Client); temporaryConversation.MergeFrom(item as Dictionary); @@ -800,7 +801,7 @@ namespace LeanCloud.Realtime.Internal.Controller { private async Task OnPropertiesUpdated(ConvCommand conv) { LCIMConversation conversation = await Client.GetOrQueryConversation(conv.Cid); Dictionary updatedAttr = JsonConvert.DeserializeObject>(conv.AttrModified.Data, - new LCJsonConverter()); + LCJsonConverter.Default); // 更新内存数据 conversation.MergeInfo(updatedAttr); Client.OnConversationInfoUpdated?.Invoke(conversation, diff --git a/Realtime/Realtime/Internal/Router/LCRTMRouter.cs b/Realtime/Realtime/Internal/Router/LCRTMRouter.cs index 418ddb7..c1207b0 100644 --- a/Realtime/Realtime/Internal/Router/LCRTMRouter.cs +++ b/Realtime/Realtime/Internal/Router/LCRTMRouter.cs @@ -1,7 +1,6 @@ using System; using System.Threading.Tasks; using System.Net.Http; -using LeanCloud.Storage.Internal; using LeanCloud.Common; using Newtonsoft.Json; @@ -9,7 +8,7 @@ namespace LeanCloud.Realtime.Internal.Router { /// /// RTM Router /// - internal class LCRTMRouter { + public class LCRTMRouter { /// /// 请求超时 /// @@ -17,14 +16,14 @@ namespace LeanCloud.Realtime.Internal.Router { private LCRTMServer rtmServer; - internal LCRTMRouter() { + public LCRTMRouter() { } /// /// 获取服务器地址 /// /// - internal async Task GetServer() { + public async Task GetServer() { if (rtmServer == null || !rtmServer.IsValid) { await Fetch(); } @@ -53,7 +52,7 @@ namespace LeanCloud.Realtime.Internal.Router { response.Dispose(); LCHttpUtils.PrintResponse(response, resultString); - rtmServer = JsonConvert.DeserializeObject(resultString, new LCJsonConverter()); + rtmServer = JsonConvert.DeserializeObject(resultString, LCJsonConverter.Default); return rtmServer; } diff --git a/Realtime/Realtime/Internal/Router/LCRTMServer.cs b/Realtime/Realtime/Internal/Router/LCRTMServer.cs index 45a5176..0e6ad07 100644 --- a/Realtime/Realtime/Internal/Router/LCRTMServer.cs +++ b/Realtime/Realtime/Internal/Router/LCRTMServer.cs @@ -2,38 +2,38 @@ using Newtonsoft.Json; namespace LeanCloud.Realtime.Internal.Router { - internal class LCRTMServer { + public class LCRTMServer { [JsonProperty("groupId")] - internal string GroupId { + public string GroupId { get; set; } [JsonProperty("groupUrl")] - internal string GroupUrl { + public string GroupUrl { get; set; } [JsonProperty("server")] - internal string Primary { + public string Primary { get; set; } [JsonProperty("secondary")] - internal string Secondary { + public string Secondary { get; set; } [JsonProperty("ttl")] - internal int Ttl { + public int Ttl { get; set; } DateTimeOffset createdAt; - internal LCRTMServer() { + public LCRTMServer() { createdAt = DateTimeOffset.Now; } - internal bool IsValid => DateTimeOffset.Now < createdAt + TimeSpan.FromSeconds(Ttl); + public bool IsValid => DateTimeOffset.Now < createdAt + TimeSpan.FromSeconds(Ttl); } } diff --git a/Realtime/Realtime/Internal/WebSocket/LCWebSocketClient.cs b/Realtime/Realtime/Internal/WebSocket/LCWebSocketClient.cs index 09d599e..ca42dec 100644 --- a/Realtime/Realtime/Internal/WebSocket/LCWebSocketClient.cs +++ b/Realtime/Realtime/Internal/WebSocket/LCWebSocketClient.cs @@ -1,12 +1,13 @@ using System; using System.Threading.Tasks; using System.Net.WebSockets; +using System.Text; namespace LeanCloud.Realtime.Internal.WebSocket { /// /// WebSocket 客户端,负责底层连接和事件,只与通信协议相关 /// - internal class LCWebSocketClient { + public class LCWebSocketClient { // .net standard 2.0 好像在拼合 Frame 时有 bug,所以将这个值调整大一些 private const int RECV_BUFFER_SIZE = 1024 * 5; @@ -23,12 +24,12 @@ namespace LeanCloud.Realtime.Internal.WebSocket { /// /// 消息事件 /// - internal Action OnMessage; + public Action OnMessage; /// /// 连接关闭 /// - internal Action OnClose; + public Action OnClose; private ClientWebSocket ws; @@ -37,11 +38,14 @@ namespace LeanCloud.Realtime.Internal.WebSocket { /// /// /// - internal async Task Connect(string server) { + public async Task Connect(string server, + string subProtocol = null) { LCLogger.Debug($"Connecting WebSocket: {server}"); Task timeoutTask = Task.Delay(CONNECT_TIMEOUT); ws = new ClientWebSocket(); - ws.Options.AddSubProtocol("lc.protobuf2.3"); + if (!string.IsNullOrEmpty(subProtocol)) { + ws.Options.AddSubProtocol(subProtocol); + } Task connectTask = ws.ConnectAsync(new Uri(server), default); if (await Task.WhenAny(connectTask, timeoutTask) == connectTask) { LCLogger.Debug($"Connected WebSocket: {server}"); @@ -57,7 +61,7 @@ namespace LeanCloud.Realtime.Internal.WebSocket { /// 主动关闭连接 /// /// - internal async Task Close() { + public async Task Close() { LCLogger.Debug("Closing WebSocket"); OnMessage = null; OnClose = null; @@ -81,11 +85,12 @@ namespace LeanCloud.Realtime.Internal.WebSocket { /// /// /// - internal async Task Send(byte[] data) { + public async Task Send(byte[] data, + WebSocketMessageType messageType = WebSocketMessageType.Binary) { ArraySegment bytes = new ArraySegment(data); if (ws.State == WebSocketState.Open) { try { - await ws.SendAsync(bytes, WebSocketMessageType.Binary, true, default); + await ws.SendAsync(bytes, messageType, true, default); } catch (Exception e) { LCLogger.Error(e); throw e; @@ -97,6 +102,15 @@ namespace LeanCloud.Realtime.Internal.WebSocket { } } + /// + /// 发送文本数据 + /// + /// + /// + public async Task Send(string text) { + await Send(Encoding.UTF8.GetBytes(text), WebSocketMessageType.Text); + } + /// /// 接收数据 /// @@ -119,14 +133,12 @@ namespace LeanCloud.Realtime.Internal.WebSocket { HandleExceptionClose(); } } - } else if (result.MessageType == WebSocketMessageType.Binary) { + } else { // 拼合 WebSocket Message int length = result.Count; byte[] data = new byte[length]; Array.Copy(buffer, data, length); OnMessage?.Invoke(data); - } else { - LCLogger.Error($"Error message type: {result.MessageType}"); } } } catch (Exception e) { diff --git a/Realtime/Realtime/Message/LCIMTypedMessage.cs b/Realtime/Realtime/Message/LCIMTypedMessage.cs index d43b40d..39cbb79 100644 --- a/Realtime/Realtime/Message/LCIMTypedMessage.cs +++ b/Realtime/Realtime/Message/LCIMTypedMessage.cs @@ -2,7 +2,7 @@ using System.Collections.Generic; using Newtonsoft.Json; using LeanCloud.Storage.Internal.Codec; -using LeanCloud.Storage.Internal; +using LeanCloud.Common; namespace LeanCloud.Realtime { /// @@ -115,7 +115,7 @@ namespace LeanCloud.Realtime { internal static LCIMTypedMessage Deserialize(string json) { Dictionary msgData = JsonConvert.DeserializeObject>(json, - new LCJsonConverter()); + LCJsonConverter.Default); LCIMTypedMessage message = null; int msgType = (int)msgData[MessageTypeKey]; if (customMessageDict.TryGetValue(msgType, out Func msgConstructor)) { diff --git a/Sample/LiveQueryApp/LiveQueryApp.csproj b/Sample/LiveQueryApp/LiveQueryApp.csproj new file mode 100644 index 0000000..dfe693c --- /dev/null +++ b/Sample/LiveQueryApp/LiveQueryApp.csproj @@ -0,0 +1,12 @@ + + + + Exe + netcoreapp3.0 + 0.1.0 + + + + + + diff --git a/Sample/LiveQueryApp/Program.cs b/Sample/LiveQueryApp/Program.cs new file mode 100644 index 0000000..cc42aab --- /dev/null +++ b/Sample/LiveQueryApp/Program.cs @@ -0,0 +1,69 @@ +using System; +using System.Threading.Tasks; +using LeanCloud; +using LeanCloud.Storage; +using LeanCloud.LiveQuery; + +using static System.Console; + +namespace LiveQueryApp { + class Program { + static void Main(string[] args) { + WriteLine("Hello World!"); + + SingleThreadSynchronizationContext.Run(async () => { + LCLogger.LogDelegate += Print; + LCApplication.Initialize("ikGGdRE2YcVOemAaRbgp1xGJ-gzGzoHsz", + "NUKmuRbdAhg1vrb2wexYo1jo", + "https://ikggdre2.lc-cn-n1-shared.com"); + + await LCUser.Login("hello", "world"); + LCQuery userQuery = LCUser.GetQuery(); + userQuery.WhereEqualTo("username", "hello"); + LCLiveQuery userLiveQuery = await userQuery.Subscribe(); + userLiveQuery.OnLogin = (user) => { + WriteLine($"login: {user.Username}"); + }; + + LCQuery query = new LCQuery("Account"); + query.WhereGreaterThan("balance", 100); + LCLiveQuery liveQuery = await query.Subscribe(); + liveQuery.OnCreate = (obj) => { + WriteLine($"create: {obj}"); + }; + liveQuery.OnUpdate = (obj, keys) => { + WriteLine($"update: {obj}"); + WriteLine(keys.Count); + }; + liveQuery.OnDelete = (objId) => { + WriteLine($"delete: {objId}"); + }; + liveQuery.OnEnter = (obj, keys) => { + WriteLine($"enter: {obj}"); + WriteLine(keys.Count); + }; + liveQuery.OnLeave = (obj, keys) => { + WriteLine($"leave: {obj}"); + WriteLine(keys.Count); + }; + }); + } + + private static void Print(LCLogLevel level, string info) { + switch (level) { + case LCLogLevel.Debug: + WriteLine($"[DEBUG] {info}\n"); + break; + case LCLogLevel.Warn: + WriteLine($"[WARNING] {info}\n"); + break; + case LCLogLevel.Error: + WriteLine($"[ERROR] {info}\n"); + break; + default: + WriteLine(info); + break; + } + } + } +} diff --git a/Sample/LiveQueryApp/SingleThreadSynchronizationContext.cs b/Sample/LiveQueryApp/SingleThreadSynchronizationContext.cs new file mode 100644 index 0000000..d881cca --- /dev/null +++ b/Sample/LiveQueryApp/SingleThreadSynchronizationContext.cs @@ -0,0 +1,43 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using System.Collections.Concurrent; +using System.Collections.Generic; + +namespace LiveQueryApp { + /// + /// 单线程环境,用于控制台应用 await 返回 + /// + public class SingleThreadSynchronizationContext : SynchronizationContext { + private readonly BlockingCollection> queue = new BlockingCollection>(); + + public override void Post(SendOrPostCallback d, object state) { + queue.Add(new KeyValuePair(d, state)); + } + + public void RunOnCurrentThread() { + while (queue.TryTake(out KeyValuePair workItem, Timeout.Infinite)) { + workItem.Key(workItem.Value); + } + } + + public void Complete() { + queue.CompleteAdding(); + } + + public static void Run(Func func) { + SynchronizationContext prevContext = Current; + try { + SingleThreadSynchronizationContext syncContext = new SingleThreadSynchronizationContext(); + SetSynchronizationContext(syncContext); + + Task t = func(); + syncContext.RunOnCurrentThread(); + + t.GetAwaiter().GetResult(); + } finally { + SetSynchronizationContext(prevContext); + } + } + } +} diff --git a/Sample/RealtimeApp/RealtimeApp.csproj b/Sample/RealtimeApp/RealtimeApp.csproj index d96ba13..a6a6535 100644 --- a/Sample/RealtimeApp/RealtimeApp.csproj +++ b/Sample/RealtimeApp/RealtimeApp.csproj @@ -3,6 +3,7 @@ Exe netcoreapp3.0 + 0.1.0 diff --git a/Storage/Storage-Unity/Storage-Unity.csproj b/Storage/Storage-Unity/Storage-Unity.csproj index 7f8ff6f..368a7a3 100644 --- a/Storage/Storage-Unity/Storage-Unity.csproj +++ b/Storage/Storage-Unity/Storage-Unity.csproj @@ -99,9 +99,6 @@ Internal\Http\LCHttpClient.cs - - Internal\Http\LCJsonConverter.cs - Internal\Query\LCEqualCondition.cs diff --git a/Storage/Storage/Internal/Http/LCHttpClient.cs b/Storage/Storage/Internal/Http/LCHttpClient.cs index af4eb76..5a4bb6a 100644 --- a/Storage/Storage/Internal/Http/LCHttpClient.cs +++ b/Storage/Storage/Internal/Http/LCHttpClient.cs @@ -61,7 +61,8 @@ namespace LeanCloud.Storage.Internal.Http { LCHttpUtils.PrintResponse(response, resultString); if (response.IsSuccessStatusCode) { - T ret = JsonConvert.DeserializeObject(resultString, new LCJsonConverter()); + T ret = JsonConvert.DeserializeObject(resultString, + LCJsonConverter.Default); return ret; } throw HandleErrorResponse(response.StatusCode, resultString); @@ -94,7 +95,8 @@ namespace LeanCloud.Storage.Internal.Http { LCHttpUtils.PrintResponse(response, resultString); if (response.IsSuccessStatusCode) { - T ret = JsonConvert.DeserializeObject(resultString, new LCJsonConverter()); + T ret = JsonConvert.DeserializeObject(resultString, + LCJsonConverter.Default); return ret; } throw HandleErrorResponse(response.StatusCode, resultString); @@ -127,7 +129,8 @@ namespace LeanCloud.Storage.Internal.Http { LCHttpUtils.PrintResponse(response, resultString); if (response.IsSuccessStatusCode) { - T ret = JsonConvert.DeserializeObject(resultString, new LCJsonConverter()); + T ret = JsonConvert.DeserializeObject(resultString, + LCJsonConverter.Default); return ret; } throw HandleErrorResponse(response.StatusCode, resultString); @@ -150,7 +153,8 @@ namespace LeanCloud.Storage.Internal.Http { LCHttpUtils.PrintResponse(response, resultString); if (response.IsSuccessStatusCode) { - Dictionary ret = JsonConvert.DeserializeObject>(resultString, new LCJsonConverter()); + Dictionary ret = JsonConvert.DeserializeObject>(resultString, + LCJsonConverter.Default); return; } throw HandleErrorResponse(response.StatusCode, resultString); @@ -161,7 +165,8 @@ namespace LeanCloud.Storage.Internal.Http { string message = responseContent; try { // 尝试获取 LeanCloud 返回错误信息 - Dictionary error = JsonConvert.DeserializeObject>(responseContent, new LCJsonConverter()); + Dictionary error = JsonConvert.DeserializeObject>(responseContent, + LCJsonConverter.Default); code = (int)error["code"]; message = error["error"].ToString(); } catch (Exception e) { diff --git a/Storage/Storage/Internal/Object/LCObjectData.cs b/Storage/Storage/Internal/Object/LCObjectData.cs index d941068..152c445 100644 --- a/Storage/Storage/Internal/Object/LCObjectData.cs +++ b/Storage/Storage/Internal/Object/LCObjectData.cs @@ -4,30 +4,30 @@ using System.Collections.Generic; using LeanCloud.Storage.Internal.Codec; namespace LeanCloud.Storage.Internal.Object { - internal class LCObjectData { - internal string ClassName { + public class LCObjectData { + public string ClassName { get; set; } - internal string ObjectId { + public string ObjectId { get; set; } - internal DateTime CreatedAt { + public DateTime CreatedAt { get; set; } - internal DateTime UpdatedAt { + public DateTime UpdatedAt { get; set; } - internal Dictionary CustomPropertyDict; + public Dictionary CustomPropertyDict; - internal LCObjectData() { + public LCObjectData() { CustomPropertyDict = new Dictionary(); } - internal static LCObjectData Decode(IDictionary dict) { + public static LCObjectData Decode(IDictionary dict) { if (dict == null) { return null; } @@ -50,7 +50,7 @@ namespace LeanCloud.Storage.Internal.Object { return objectData; } - internal static Dictionary Encode(LCObjectData objectData) { + public static Dictionary Encode(LCObjectData objectData) { if (objectData == null) { return null; } diff --git a/Storage/Storage/Internal/Query/LCCompositionalCondition.cs b/Storage/Storage/Internal/Query/LCCompositionalCondition.cs index 54098e3..426a904 100644 --- a/Storage/Storage/Internal/Query/LCCompositionalCondition.cs +++ b/Storage/Storage/Internal/Query/LCCompositionalCondition.cs @@ -118,7 +118,7 @@ namespace LeanCloud.Storage.Internal.Query { public void WhereMatchesQuery(string key, LCQuery query) where K : LCObject { Dictionary inQuery = new Dictionary { - { "where", query.condition }, + { "where", query.Condition }, { "className", query.ClassName } }; AddOperation(key, "$inQuery", inQuery); @@ -126,7 +126,7 @@ namespace LeanCloud.Storage.Internal.Query { public void WhereDoesNotMatchQuery(string key, LCQuery query) where K : LCObject { Dictionary inQuery = new Dictionary { - { "where", query.condition }, + { "where", query.Condition }, { "className", query.ClassName } }; AddOperation(key, "$notInQuery", inQuery); diff --git a/Storage/Storage/LCObject.cs b/Storage/Storage/LCObject.cs index 84ad22b..aacc54f 100644 --- a/Storage/Storage/LCObject.cs +++ b/Storage/Storage/LCObject.cs @@ -93,7 +93,7 @@ namespace LeanCloud.Storage { return obj; } - internal static LCObject Create(string className) { + public static LCObject Create(string className) { if (subclassNameDict.TryGetValue(className, out LCSubclassInfo subclassInfo)) { return subclassInfo.Constructor.Invoke(); } @@ -494,7 +494,7 @@ namespace LeanCloud.Storage { } } - internal void Merge(LCObjectData objectData) { + public void Merge(LCObjectData objectData) { data.ClassName = objectData.ClassName ?? data.ClassName; data.ObjectId = objectData.ObjectId ?? data.ObjectId; data.CreatedAt = objectData.CreatedAt != null ? objectData.CreatedAt : data.CreatedAt; diff --git a/Storage/Storage/LCQuery.cs b/Storage/Storage/LCQuery.cs index f86d203..7d391d9 100644 --- a/Storage/Storage/LCQuery.cs +++ b/Storage/Storage/LCQuery.cs @@ -8,20 +8,37 @@ using LeanCloud.Storage.Internal.Query; using LeanCloud.Storage.Internal.Object; namespace LeanCloud.Storage { + public class LCQuery { + public string ClassName { + get; internal set; + } + + public LCCompositionalCondition Condition { + get; internal set; + } + + public LCQuery(string className) { + ClassName = className; + Condition = new LCCompositionalCondition(); + } + + internal Dictionary BuildParams() { + return Condition.BuildParams(); + } + + internal string BuildWhere() { + return Condition.BuildWhere(); + } + } + /// /// 查询类 /// /// - public class LCQuery where T : LCObject { - public string ClassName { - get; private set; - } + public class LCQuery : LCQuery where T : LCObject { + public LCQuery(string className) : + base(className) { - internal LCCompositionalCondition condition; - - public LCQuery(string className) { - ClassName = className; - condition = new LCCompositionalCondition(); } /// @@ -31,7 +48,7 @@ namespace LeanCloud.Storage { /// /// public LCQuery WhereEqualTo(string key, object value) { - condition.WhereEqualTo(key, value); + Condition.WhereEqualTo(key, value); return this; } @@ -42,7 +59,7 @@ namespace LeanCloud.Storage { /// /// public LCQuery WhereNotEqualTo(string key, object value) { - condition.WhereNotEqualTo(key, value); + Condition.WhereNotEqualTo(key, value); return this; } @@ -53,7 +70,7 @@ namespace LeanCloud.Storage { /// /// public LCQuery WhereContainedIn(string key, IEnumerable values) { - condition.WhereContainedIn(key, values); + Condition.WhereContainedIn(key, values); return this; } @@ -64,7 +81,7 @@ namespace LeanCloud.Storage { /// /// public LCQuery WhereNotContainedIn(string key, IEnumerable values) { - condition.WhereNotContainedIn(key, values); + Condition.WhereNotContainedIn(key, values); return this; } @@ -75,7 +92,7 @@ namespace LeanCloud.Storage { /// /// public LCQuery WhereContainsAll(string key, IEnumerable values) { - condition.WhereContainsAll(key, values); + Condition.WhereContainsAll(key, values); return this; } @@ -85,7 +102,7 @@ namespace LeanCloud.Storage { /// /// public LCQuery WhereExists(string key) { - condition.WhereExists(key); + Condition.WhereExists(key); return this; } @@ -95,7 +112,7 @@ namespace LeanCloud.Storage { /// /// public LCQuery WhereDoesNotExist(string key) { - condition.WhereDoesNotExist(key); + Condition.WhereDoesNotExist(key); return this; } @@ -106,7 +123,7 @@ namespace LeanCloud.Storage { /// /// public LCQuery WhereSizeEqualTo(string key, int size) { - condition.WhereSizeEqualTo(key, size); + Condition.WhereSizeEqualTo(key, size); return this; } @@ -117,7 +134,7 @@ namespace LeanCloud.Storage { /// /// public LCQuery WhereGreaterThan(string key, object value) { - condition.WhereGreaterThan(key, value); + Condition.WhereGreaterThan(key, value); return this; } @@ -128,7 +145,7 @@ namespace LeanCloud.Storage { /// /// public LCQuery WhereGreaterThanOrEqualTo(string key, object value) { - condition.WhereGreaterThanOrEqualTo(key, value); + Condition.WhereGreaterThanOrEqualTo(key, value); return this; } @@ -139,7 +156,7 @@ namespace LeanCloud.Storage { /// /// public LCQuery WhereLessThan(string key, object value) { - condition.WhereLessThan(key, value); + Condition.WhereLessThan(key, value); return this; } @@ -150,7 +167,7 @@ namespace LeanCloud.Storage { /// /// public LCQuery WhereLessThanOrEqualTo(string key, object value) { - condition.WhereLessThanOrEqualTo(key, value); + Condition.WhereLessThanOrEqualTo(key, value); return this; } @@ -161,7 +178,7 @@ namespace LeanCloud.Storage { /// /// public LCQuery WhereNear(string key, LCGeoPoint point) { - condition.WhereNear(key, point); + Condition.WhereNear(key, point); return this; } @@ -173,7 +190,7 @@ namespace LeanCloud.Storage { /// /// public LCQuery WhereWithinGeoBox(string key, LCGeoPoint southwest, LCGeoPoint northeast) { - condition.WhereWithinGeoBox(key, southwest, northeast); + Condition.WhereWithinGeoBox(key, southwest, northeast); return this; } @@ -184,7 +201,7 @@ namespace LeanCloud.Storage { /// /// public LCQuery WhereRelatedTo(LCObject parent, string key) { - condition.WhereRelatedTo(parent, key); + Condition.WhereRelatedTo(parent, key); return this; } @@ -195,7 +212,7 @@ namespace LeanCloud.Storage { /// /// public LCQuery WhereStartsWith(string key, string prefix) { - condition.WhereStartsWith(key, prefix); + Condition.WhereStartsWith(key, prefix); return this; } @@ -206,7 +223,7 @@ namespace LeanCloud.Storage { /// /// public LCQuery WhereEndsWith(string key, string suffix) { - condition.WhereEndsWith(key, suffix); + Condition.WhereEndsWith(key, suffix); return this; } @@ -217,7 +234,7 @@ namespace LeanCloud.Storage { /// /// public LCQuery WhereContains(string key, string subString) { - condition.WhereContains(key, subString); + Condition.WhereContains(key, subString); return this; } @@ -229,7 +246,7 @@ namespace LeanCloud.Storage { /// /// public LCQuery WhereMatches(string key, string regex, string modifiers = null) { - condition.WhereMatches(key, regex, modifiers); + Condition.WhereMatches(key, regex, modifiers); return this; } @@ -240,7 +257,7 @@ namespace LeanCloud.Storage { /// /// public LCQuery WhereMatchesQuery(string key, LCQuery query) where K : LCObject { - condition.WhereMatchesQuery(key, query); + Condition.WhereMatchesQuery(key, query); return this; } @@ -252,7 +269,7 @@ namespace LeanCloud.Storage { /// /// public LCQuery WhereDoesNotMatchQuery(string key, LCQuery query) where K : LCObject { - condition.WhereDoesNotMatchQuery(key, query); + Condition.WhereDoesNotMatchQuery(key, query); return this; } @@ -262,7 +279,7 @@ namespace LeanCloud.Storage { /// /// public LCQuery OrderByAscending(string key) { - condition.OrderByAscending(key); + Condition.OrderByAscending(key); return this; } @@ -272,7 +289,7 @@ namespace LeanCloud.Storage { /// /// public LCQuery OrderByDescending(string key) { - condition.OrderByDescending(key); + Condition.OrderByDescending(key); return this; } @@ -282,7 +299,7 @@ namespace LeanCloud.Storage { /// /// public LCQuery AddAscendingOrder(string key) { - condition.AddAscendingOrder(key); + Condition.AddAscendingOrder(key); return this; } @@ -292,7 +309,7 @@ namespace LeanCloud.Storage { /// /// public LCQuery AddDescendingOrder(string key) { - condition.AddDescendingOrder(key); + Condition.AddDescendingOrder(key); return this; } @@ -302,7 +319,7 @@ namespace LeanCloud.Storage { /// /// public LCQuery Include(string key) { - condition.Include(key); + Condition.Include(key); return this; } @@ -312,7 +329,7 @@ namespace LeanCloud.Storage { /// /// public LCQuery Select(string key) { - condition.Select(key); + Condition.Select(key); return this; } @@ -322,7 +339,7 @@ namespace LeanCloud.Storage { /// /// public LCQuery Skip(int value) { - condition.Skip = value; + Condition.Skip = value; return this; } @@ -332,7 +349,7 @@ namespace LeanCloud.Storage { /// /// public LCQuery Limit(int value) { - condition.Limit = value; + Condition.Limit = value; return this; } @@ -396,7 +413,7 @@ namespace LeanCloud.Storage { throw new Exception("All of the queries in an or query must be on the same class."); } className = query.ClassName; - compositionQuery.condition.Add(query.condition); + compositionQuery.Condition.Add(query.Condition); } compositionQuery.ClassName = className; return compositionQuery; @@ -407,25 +424,17 @@ namespace LeanCloud.Storage { throw new ArgumentNullException(nameof(queries)); } LCQuery compositionQuery = new LCQuery(null); - compositionQuery.condition = new LCCompositionalCondition(LCCompositionalCondition.Or); + compositionQuery.Condition = new LCCompositionalCondition(LCCompositionalCondition.Or); string className = null; foreach (LCQuery query in queries) { if (className != null && className != query.ClassName) { throw new Exception("All of the queries in an or query must be on the same class."); } className = query.ClassName; - compositionQuery.condition.Add(query.condition); + compositionQuery.Condition.Add(query.Condition); } compositionQuery.ClassName = className; return compositionQuery; } - - Dictionary BuildParams() { - return condition.BuildParams(); - } - - internal string BuildWhere() { - return condition.BuildWhere(); - } } } diff --git a/Storage/Storage/LCUser.cs b/Storage/Storage/LCUser.cs index df51104..7a9cc6c 100644 --- a/Storage/Storage/LCUser.cs +++ b/Storage/Storage/LCUser.cs @@ -85,7 +85,7 @@ namespace LeanCloud.Storage { } - internal LCUser(LCObjectData objectData) : this() { + public LCUser(LCObjectData objectData) : this() { Merge(objectData); } diff --git a/csharp-sdk.sln b/csharp-sdk.sln index 4493742..205f883 100644 --- a/csharp-sdk.sln +++ b/csharp-sdk.sln @@ -27,6 +27,16 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Realtime", "Realtime\Realti EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RealtimeApp", "Sample\RealtimeApp\RealtimeApp.csproj", "{A716EFC7-9220-4A9A-9F73-B816A0787F77}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "LiveQuery", "LiveQuery", "{A1A24E0F-6901-4A9A-9BB8-4F586BC7EE17}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LiveQuery", "LiveQuery\LiveQuery\LiveQuery.csproj", "{659BA438-1DA7-4A32-92A4-DD0FAE142259}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LiveQuery.Test", "LiveQuery\LiveQuery.Test\LiveQuery.Test.csproj", "{7F770CE0-593E-486A-96E8-8903BC27C6FB}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LiveQueryApp", "Sample\LiveQueryApp\LiveQueryApp.csproj", "{CF72C053-5DB9-4E9C-BF9D-6664672F4916}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LiveQuery-Unity", "LiveQuery\LiveQuery-Unity\LiveQuery-Unity.csproj", "{FF11B077-93F1-45FD-A3C7-020D316EB5A4}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -69,6 +79,22 @@ Global {A716EFC7-9220-4A9A-9F73-B816A0787F77}.Debug|Any CPU.Build.0 = Debug|Any CPU {A716EFC7-9220-4A9A-9F73-B816A0787F77}.Release|Any CPU.ActiveCfg = Release|Any CPU {A716EFC7-9220-4A9A-9F73-B816A0787F77}.Release|Any CPU.Build.0 = Release|Any CPU + {659BA438-1DA7-4A32-92A4-DD0FAE142259}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {659BA438-1DA7-4A32-92A4-DD0FAE142259}.Debug|Any CPU.Build.0 = Debug|Any CPU + {659BA438-1DA7-4A32-92A4-DD0FAE142259}.Release|Any CPU.ActiveCfg = Release|Any CPU + {659BA438-1DA7-4A32-92A4-DD0FAE142259}.Release|Any CPU.Build.0 = Release|Any CPU + {7F770CE0-593E-486A-96E8-8903BC27C6FB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {7F770CE0-593E-486A-96E8-8903BC27C6FB}.Debug|Any CPU.Build.0 = Debug|Any CPU + {7F770CE0-593E-486A-96E8-8903BC27C6FB}.Release|Any CPU.ActiveCfg = Release|Any CPU + {7F770CE0-593E-486A-96E8-8903BC27C6FB}.Release|Any CPU.Build.0 = Release|Any CPU + {CF72C053-5DB9-4E9C-BF9D-6664672F4916}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {CF72C053-5DB9-4E9C-BF9D-6664672F4916}.Debug|Any CPU.Build.0 = Debug|Any CPU + {CF72C053-5DB9-4E9C-BF9D-6664672F4916}.Release|Any CPU.ActiveCfg = Release|Any CPU + {CF72C053-5DB9-4E9C-BF9D-6664672F4916}.Release|Any CPU.Build.0 = Release|Any CPU + {FF11B077-93F1-45FD-A3C7-020D316EB5A4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {FF11B077-93F1-45FD-A3C7-020D316EB5A4}.Debug|Any CPU.Build.0 = Debug|Any CPU + {FF11B077-93F1-45FD-A3C7-020D316EB5A4}.Release|Any CPU.ActiveCfg = Release|Any CPU + {FF11B077-93F1-45FD-A3C7-020D316EB5A4}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(NestedProjects) = preSolution {26CDAE2A-6D79-4981-8D80-3EA34FDFB134} = {319A9989-3B69-4AD0-9E43-F6D31C1D2A4A} @@ -80,6 +106,10 @@ Global {882A9419-CC5E-4CFB-B076-7561989B0A4A} = {319A9989-3B69-4AD0-9E43-F6D31C1D2A4A} {75A3A4EC-93B8-40C9-AE04-DF14A72525CC} = {319A9989-3B69-4AD0-9E43-F6D31C1D2A4A} {A716EFC7-9220-4A9A-9F73-B816A0787F77} = {2D980281-F060-4363-AB7A-D4B6C30ADDBB} + {659BA438-1DA7-4A32-92A4-DD0FAE142259} = {A1A24E0F-6901-4A9A-9BB8-4F586BC7EE17} + {7F770CE0-593E-486A-96E8-8903BC27C6FB} = {A1A24E0F-6901-4A9A-9BB8-4F586BC7EE17} + {CF72C053-5DB9-4E9C-BF9D-6664672F4916} = {2D980281-F060-4363-AB7A-D4B6C30ADDBB} + {FF11B077-93F1-45FD-A3C7-020D316EB5A4} = {A1A24E0F-6901-4A9A-9BB8-4F586BC7EE17} EndGlobalSection GlobalSection(MonoDevelopProperties) = preSolution version = 0.1.0 diff --git a/script/package.sh b/script/package.sh index 40d3c8a..96dea75 100644 --- a/script/package.sh +++ b/script/package.sh @@ -16,4 +16,8 @@ pack ./Storage/Storage-Unity/bin/Release/netstandard2.0/ ./Plugins LeanCloud-SDK # Realtime pack ./Realtime/Realtime/bin/Release/netstandard2.0/ ./DLLs LeanCloud-SDK-Realtime-Standard.zip -pack ./Realtime/Realtime-Unity/bin/Release/netstandard2.0/ ./Plugins LeanCloud-SDK-Realtime-Unity.zip \ No newline at end of file +pack ./Realtime/Realtime-Unity/bin/Release/netstandard2.0/ ./Plugins LeanCloud-SDK-Realtime-Unity.zip + +# LiveQuery +pack ./LiveQuery/LiveQuery/bin/Release/netstandard2.0/ ./DLLs LeanCloud-SDK-LiveQuery-Standard.zip +pack ./LiveQuery/LiveQuery-Unity/bin/Release/netstandard2.0/ ./Plugins LeanCloud-SDK-LiveQuery-Unity.zip \ No newline at end of file