diff --git a/Common/Common/Json/LCJsonConverter.cs b/Common/Common/Json/LCJsonConverter.cs index 3e78108..e9b98fd 100644 --- a/Common/Common/Json/LCJsonConverter.cs +++ b/Common/Common/Json/LCJsonConverter.cs @@ -28,6 +28,9 @@ namespace LeanCloud.Common { return Convert.ToInt32(reader.Value); } } + if (reader.TokenType == JsonToken.Float) { + return Convert.ToSingle(reader.Value); + } return serializer.Deserialize(reader); } diff --git a/Engine/Attributes/LCEngineClassHookAttribute.cs b/Engine/Attributes/LCEngineClassHookAttribute.cs new file mode 100644 index 0000000..6fbe6d8 --- /dev/null +++ b/Engine/Attributes/LCEngineClassHookAttribute.cs @@ -0,0 +1,30 @@ +using System; + +namespace LeanCloud.Engine { + public enum LCEngineObjectHookType { + BeforeSave, + AfterSave, + BeforeUpdate, + AfterUpdate, + BeforeDelete, + AfterDelete + } + + public class LCEngineClassHookAttribute : Attribute { + public string ClassName { + get; + } + + public LCEngineObjectHookType HookType { + get; + } + + public LCEngineClassHookAttribute(string className, LCEngineObjectHookType hookType) { + if (string.IsNullOrEmpty(className)) { + throw new ArgumentNullException(nameof(className)); + } + ClassName = className; + HookType = hookType; + } + } +} diff --git a/Engine/Attributes/LCEngineFunctionAttribute.cs b/Engine/Attributes/LCEngineFunctionAttribute.cs new file mode 100644 index 0000000..1f4ddb0 --- /dev/null +++ b/Engine/Attributes/LCEngineFunctionAttribute.cs @@ -0,0 +1,17 @@ +using System; + +namespace LeanCloud.Engine { + [AttributeUsage(AttributeTargets.Method, AllowMultiple = false)] + public class LCEngineFunctionAttribute : Attribute { + public string FunctionName { + get; + } + + public LCEngineFunctionAttribute(string funcName) { + if (string.IsNullOrEmpty(funcName)) { + throw new ArgumentNullException(nameof(funcName)); + } + FunctionName = funcName; + } + } +} diff --git a/Engine/Attributes/LCEngineFunctionParamAttribute.cs b/Engine/Attributes/LCEngineFunctionParamAttribute.cs new file mode 100644 index 0000000..157608c --- /dev/null +++ b/Engine/Attributes/LCEngineFunctionParamAttribute.cs @@ -0,0 +1,17 @@ +using System; + +namespace LeanCloud.Engine { + [AttributeUsage(AttributeTargets.Parameter, AllowMultiple = false)] + public class LCEngineFunctionParamAttribute : Attribute { + public string ParamName { + get; + } + + public LCEngineFunctionParamAttribute(string paramName) { + if (string.IsNullOrEmpty(paramName)) { + throw new ArgumentNullException(nameof(paramName)); + } + ParamName = paramName; + } + } +} diff --git a/Engine/Attributes/LCEngineRealtimeHookAttribute.cs b/Engine/Attributes/LCEngineRealtimeHookAttribute.cs new file mode 100644 index 0000000..8c23d06 --- /dev/null +++ b/Engine/Attributes/LCEngineRealtimeHookAttribute.cs @@ -0,0 +1,32 @@ +using System; + +namespace LeanCloud.Engine { + public enum LCEngineRealtimeHookType { + // 消息 + MessageReceived, + MessageSent, + MessageUpdate, + ReceiversOffline, + // 对话 + ConversationStart, + ConversationStarted, + ConversationAdd, + ConversationAdded, + ConversationRemove, + ConversationRemoved, + ConversationUpdate, + // 客户端 + ClientOnline, + ClientOffline, + } + + public class LCEngineRealtimeHookAttribute : Attribute { + public LCEngineRealtimeHookType HookType { + get; + } + + public LCEngineRealtimeHookAttribute(LCEngineRealtimeHookType hookType) { + HookType = hookType; + } + } +} diff --git a/Engine/Attributes/LCEngineUserHookAttribute.cs b/Engine/Attributes/LCEngineUserHookAttribute.cs new file mode 100644 index 0000000..c9485f1 --- /dev/null +++ b/Engine/Attributes/LCEngineUserHookAttribute.cs @@ -0,0 +1,19 @@ +using System; + +namespace LeanCloud.Engine { + public enum LCEngineUserHookType { + OnSMSVerified, + OnEmailVerified, + OnLogin + } + + public class LCEngineUserHookAttribute : Attribute { + public LCEngineUserHookType HookType { + get; + } + + public LCEngineUserHookAttribute(LCEngineUserHookType hookType) { + HookType = hookType; + } + } +} diff --git a/Engine/Controllers/LCClassHookController.cs b/Engine/Controllers/LCClassHookController.cs new file mode 100644 index 0000000..211b4b4 --- /dev/null +++ b/Engine/Controllers/LCClassHookController.cs @@ -0,0 +1,82 @@ +using System; +using System.Text.Json; +using System.Threading.Tasks; +using System.Collections.Generic; +using System.Reflection; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Cors; +using LeanCloud.Storage.Internal.Object; +using LeanCloud.Storage; + +namespace LeanCloud.Engine { + [ApiController] + [Route("{1,1.1}")] + [EnableCors(LCEngine.LCEngineCORS)] + public class LCClassHookController : ControllerBase { + private Dictionary ClassHooks => LCEngine.ClassHooks; + + [HttpPost("functions/{className}/{hookName}")] + public async Task Hook(string className, string hookName, JsonElement body) { + try { + LCLogger.Debug($"Hook: {className}#{hookName}"); + LCLogger.Debug(body.ToString()); + + LCEngine.CheckHookKey(Request); + + string classHookName = GetClassHookName(className, hookName); + if (ClassHooks.TryGetValue(classHookName, out MethodInfo mi)) { + Dictionary data = LCEngine.Decode(body); + + LCObjectData objectData = LCObjectData.Decode(data["object"] as Dictionary); + objectData.ClassName = className; + LCObject obj = LCObject.Create(className); + obj.Merge(objectData); + + // 避免死循环 + if (hookName.StartsWith("before")) { + obj.DisableBeforeHook(); + } else { + obj.DisableAfterHook(); + } + + LCEngine.InitRequestContext(Request); + + LCUser user = null; + if (data.TryGetValue("user", out object userObj) && + userObj != null) { + user = new LCUser(); + user.Merge(LCObjectData.Decode(userObj as Dictionary)); + LCEngineRequestContext.CurrentUser = user; + } + + LCObject result = await LCEngine.Invoke(mi, new object[] { obj }) as LCObject; + if (result != null) { + return LCCloud.Encode(result); + } + } + return body; + } catch (Exception e) { + return StatusCode(500, e.Message); + } + } + + private static string GetClassHookName(string className, string hookName) { + switch (hookName) { + case "beforeSave": + return $"__before_save_for_{className}"; + case "afterSave": + return $"__after_save_for_{className}"; + case "beforeUpdate": + return $"__before_update_for_{className}"; + case "afterUpdate": + return $"__after_update_for_{className}"; + case "beforeDelete": + return $"__before_delete_for_{className}"; + case "afterDelete": + return $"__after_delete_for_{className}"; + default: + throw new Exception($"Error hook name: {hookName}"); + } + } + } +} diff --git a/Engine/Controllers/LCFunctionController.cs b/Engine/Controllers/LCFunctionController.cs new file mode 100644 index 0000000..f64b533 --- /dev/null +++ b/Engine/Controllers/LCFunctionController.cs @@ -0,0 +1,99 @@ +using System; +using System.Text.Json; +using System.Threading.Tasks; +using System.Collections.Generic; +using System.Reflection; +using System.Linq; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Cors; +using LeanCloud.Storage; +using LeanCloud.Storage.Internal.Codec; + +namespace LeanCloud.Engine { + [ApiController] + [Route("{1,1.1}")] + [EnableCors(LCEngine.LCEngineCORS)] + public class LCFunctionController : ControllerBase { + private Dictionary Functions => LCEngine.Functions; + + [HttpGet("functions/_ops/metadatas")] + public object GetFunctions() { + try { + return LCEngine.GetFunctions(Request); + } catch (Exception e) { + return StatusCode(500, e.Message); + } + } + + [HttpPost("functions/{funcName}")] + public async Task Run(string funcName, JsonElement body) { + try { + LCLogger.Debug($"Run: {funcName}"); + LCLogger.Debug(body.ToString()); + + if (Functions.TryGetValue(funcName, out MethodInfo mi)) { + LCEngine.InitRequestContext(Request); + + object[] ps = ParseParameters(mi, body); + object result = await LCEngine.Invoke(mi, ps.ToArray()); + + if (result != null) { + return new Dictionary { + { "result", result } + }; + } + } + return body; + } catch (Exception e) { + return StatusCode(500, e.Message); + } + } + + [HttpPost("call/{funcName}")] + public async Task RPC(string funcName, JsonElement body) { + try { + LCLogger.Debug($"RPC: {funcName}"); + LCLogger.Debug(body.ToString()); + + if (Functions.TryGetValue(funcName, out MethodInfo mi)) { + LCEngine.InitRequestContext(Request); + + object[] ps = ParseParameters(mi, body); + object result = await LCEngine.Invoke(mi, ps); + + if (result != null) { + return new Dictionary { + { "result", LCCloud.Encode(result) } + }; + } + } + return body; + } catch (Exception e) { + return StatusCode(500, e.Message); + } + } + + private static object[] ParseParameters(MethodInfo mi, JsonElement body) { + Dictionary parameters = LCEngine.Decode(body); + List ps = new List(); + + if (mi.GetParameters().Length > 0) { + if (Array.Exists(mi.GetParameters(), + p => p.GetCustomAttribute() != null)) { + // 如果包含 LCEngineFunctionParamAttribute 的参数,则按照配对方式传递参数 + foreach (ParameterInfo pi in mi.GetParameters()) { + LCEngineFunctionParamAttribute attr = pi.GetCustomAttribute(); + if (attr != null) { + string paramName = attr.ParamName; + ps.Add(parameters[paramName]); + } + } + } else { + ps.Add(LCDecoder.Decode(LCEngine.Decode(body))); + } + } + + return ps.ToArray(); + } + } +} diff --git a/Engine/Controllers/LCPingController.cs b/Engine/Controllers/LCPingController.cs new file mode 100644 index 0000000..8e17193 --- /dev/null +++ b/Engine/Controllers/LCPingController.cs @@ -0,0 +1,21 @@ +using System; +using System.Collections.Generic; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Cors; + +namespace LeanCloud.Engine { + [ApiController] + [Route("__engine/{1,1.1}")] + [EnableCors(LCEngine.LCEngineCORS)] + public class LCPingController : ControllerBase { + [HttpGet("ping")] + public object Get() { + LCLogger.Debug("Ping ~~~"); + + return new Dictionary { + { "runtime", $"dotnet-{Environment.Version}" }, + { "version", LCApplication.SDKVersion } + }; + } + } +} diff --git a/Engine/Controllers/LCUserHookController.cs b/Engine/Controllers/LCUserHookController.cs new file mode 100644 index 0000000..0713635 --- /dev/null +++ b/Engine/Controllers/LCUserHookController.cs @@ -0,0 +1,88 @@ +using System; +using System.Text.Json; +using System.Threading.Tasks; +using System.Collections.Generic; +using System.Reflection; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Cors; +using LeanCloud.Storage.Internal.Object; +using LeanCloud.Storage; + +namespace LeanCloud.Engine { + [ApiController] + [Route("{1,1.1}/functions")] + [EnableCors(LCEngine.LCEngineCORS)] + public class LCUserHookController : ControllerBase { + private Dictionary UserHooks => LCEngine.UserHooks; + + [HttpPost("onVerified/sms")] + public async Task HookSMSVerification(JsonElement body) { + try { + LCLogger.Debug(LCEngine.OnSMSVerified); + LCLogger.Debug(body.ToString()); + + LCEngine.CheckHookKey(Request); + + if (UserHooks.TryGetValue(LCEngine.OnSMSVerified, out MethodInfo mi)) { + LCEngine.InitRequestContext(Request); + + Dictionary dict = LCEngine.Decode(body); + return await Invoke(mi, dict); + } + return body; + } catch (Exception e) { + return StatusCode(500, e.Message); + } + } + + [HttpPost("onVerified/email")] + public async Task HookEmailVerification(JsonElement body) { + try { + LCLogger.Debug(LCEngine.OnEmailVerified); + LCLogger.Debug(body.ToString()); + + LCEngine.CheckHookKey(Request); + + if (UserHooks.TryGetValue(LCEngine.OnEmailVerified, out MethodInfo mi)) { + LCEngine.InitRequestContext(Request); + + Dictionary dict = LCEngine.Decode(body); + return await Invoke(mi, dict); + } + return body; + } catch (Exception e) { + return StatusCode(500, e.Message); + } + } + + [HttpPost("_User/onLogin")] + public async Task HookLogin(JsonElement body) { + try { + LCLogger.Debug(LCEngine.OnLogin); + LCLogger.Debug(body.ToString()); + + LCEngine.CheckHookKey(Request); + + if (UserHooks.TryGetValue(LCEngine.OnLogin, out MethodInfo mi)) { + LCEngine.InitRequestContext(Request); + + Dictionary dict = LCEngine.Decode(body); + return await Invoke(mi, dict); + } + return body; + } catch (Exception e) { + return StatusCode(500, e.Message); + } + } + + private static async Task Invoke(MethodInfo mi, Dictionary dict) { + LCObjectData objectData = LCObjectData.Decode(dict["object"] as Dictionary); + objectData.ClassName = "_User"; + + LCObject user = LCObject.Create("_User"); + user.Merge(objectData); + + return await LCEngine.Invoke(mi, new object[] { user }) as LCObject; + } + } +} diff --git a/Engine/Engine.csproj b/Engine/Engine.csproj new file mode 100644 index 0000000..d6c4b94 --- /dev/null +++ b/Engine/Engine.csproj @@ -0,0 +1,20 @@ + + + + netcoreapp3.1 + 0.6.4 + LeanCloud.Engine + true + + + + + + + + + + + + + diff --git a/Engine/LCEngine.cs b/Engine/LCEngine.cs new file mode 100644 index 0000000..2bf6050 --- /dev/null +++ b/Engine/LCEngine.cs @@ -0,0 +1,312 @@ +using System; +using System.Collections.Generic; +using System.Reflection; +using System.Linq; +using System.Threading.Tasks; +using System.Text.Json; +using Microsoft.AspNetCore.Http; +using Newtonsoft.Json; +using Microsoft.Extensions.Primitives; +using Microsoft.Extensions.DependencyInjection; +using LeanCloud.Common; + +namespace LeanCloud.Engine { + public class LCEngine { + public const string LCEngineCORS = "LCEngineCORS"; + + const string LCMasterKeyName = "x-avoscloud-master-key"; + const string LCHookKeyName = "x-lc-hook-key"; + + const string BeforeSave = "__before_save_for_"; + const string AfterSave = "__after_save_for_"; + const string BeforeUpdate = "__before_update_for_"; + const string AfterUpdate = "__after_update_for_"; + const string BeforeDelete = "__before_delete_for_"; + const string AfterDelete = "__after_delete_for_"; + + internal const string OnSMSVerified = "__on_verified_sms"; + internal const string OnEmailVerified = "__on_verified_email"; + internal const string OnLogin = "__on_login__User"; + + const string ClientOnline = "_clientOnline"; + const string ClientOffline = "_clientOffline"; + + const string MessageSent = "_messageSent"; + const string MessageReceived = "_messageReceived"; + const string ReceiversOffline = "_receiversOffline"; + const string MessageUpdate = "_messageUpdate"; + + const string ConversationStart = "_conversationStart"; + const string ConversationStarted = "_conversationStarted"; + const string ConversationAdd = "_conversationAdd"; + const string ConversationAdded = "_conversationAdded"; + const string ConversationRemove = "_conversationRemove"; + const string ConversationRemoved = "_conversationRemoved"; + const string ConversationUpdate = "_conversationUpdate"; + + static readonly string[] LCEngineCORSMethods = new string[] { + "PUT", + "GET", + "POST", + "DELETE", + "OPTIONS" + }; + static readonly string[] LCEngineCORSHeaders = new string[] { + "Content-Type", + "X-AVOSCloud-Application-Id", + "X-AVOSCloud-Application-Key", + "X-AVOSCloud-Application-Production", + "X-AVOSCloud-Client-Version", + "X-AVOSCloud-Request-Sign", + "X-AVOSCloud-Session-Token", + "X-AVOSCloud-Super-Key", + "X-LC-Hook-Key", + "X-LC-Id", + "X-LC-Key", + "X-LC-Prod", + "X-LC-Session", + "X-LC-Sign", + "X-LC-UA", + "X-Requested-With", + "X-Uluru-Application-Id", + "X-Uluru-Application-Key", + "X-Uluru-Application-Production", + "X-Uluru-Client-Version", + "X-Uluru-Session-Token" + }; + + public static Dictionary Functions = new Dictionary(); + public static Dictionary ClassHooks = new Dictionary(); + public static Dictionary UserHooks = new Dictionary(); + + public static void Initialize(IServiceCollection services) { + // 获取环境变量 + LCLogger.Debug("-------------------------------------------------"); + PrintEnvironmentVar("LEANCLOUD_APP_ID"); + PrintEnvironmentVar("LEANCLOUD_APP_KEY"); + PrintEnvironmentVar("LEANCLOUD_APP_MASTER_KEY"); + PrintEnvironmentVar("LEANCLOUD_APP_HOOK_KEY"); + PrintEnvironmentVar("LEANCLOUD_API_SERVER"); + PrintEnvironmentVar("LEANCLOUD_APP_PROD"); + PrintEnvironmentVar("LEANCLOUD_APP_ENV"); + PrintEnvironmentVar("LEANCLOUD_APP_INSTANCE"); + PrintEnvironmentVar("LEANCLOUD_REGION"); + PrintEnvironmentVar("LEANCLOUD_APP_ID"); + PrintEnvironmentVar("LEANCLOUD_APP_DOMAIN"); + PrintEnvironmentVar("LEANCLOUD_APP_PORT"); + LCLogger.Debug("-------------------------------------------------"); + + LCApplication.Initialize(Environment.GetEnvironmentVariable("LEANCLOUD_APP_ID"), + Environment.GetEnvironmentVariable("LEANCLOUD_APP_KEY"), + Environment.GetEnvironmentVariable("LEANCLOUD_API_SERVER")); + LCApplication.AddHeader(LCHookKeyName, Environment.GetEnvironmentVariable("LEANCLOUD_APP_HOOK_KEY")); + + Assembly assembly = Assembly.GetCallingAssembly(); + ClassHooks = assembly.GetTypes() + .SelectMany(t => t.GetMethods()) + .Where(m => m.GetCustomAttribute() != null) + .ToDictionary(mi => { + LCEngineClassHookAttribute attr = mi.GetCustomAttribute(); + switch (attr.HookType) { + case LCEngineObjectHookType.BeforeSave: + return $"{BeforeSave}{attr.ClassName}"; + case LCEngineObjectHookType.AfterSave: + return $"{AfterSave}{attr.ClassName}"; + case LCEngineObjectHookType.BeforeUpdate: + return $"{BeforeUpdate}{attr.ClassName}"; + case LCEngineObjectHookType.AfterUpdate: + return $"{AfterUpdate}{attr.ClassName}"; + case LCEngineObjectHookType.BeforeDelete: + return $"{BeforeDelete}{attr.ClassName}"; + case LCEngineObjectHookType.AfterDelete: + return $"{AfterDelete}{attr.ClassName}"; + default: + throw new Exception($"Error hook type: {attr.HookType}"); + } + }); + + UserHooks = assembly.GetTypes() + .SelectMany(t => t.GetMethods()) + .Where(m => m.GetCustomAttribute() != null) + .ToDictionary(mi => { + LCEngineUserHookAttribute attr = mi.GetCustomAttribute(); + switch (attr.HookType) { + case LCEngineUserHookType.OnSMSVerified: + return OnSMSVerified; + case LCEngineUserHookType.OnEmailVerified: + return OnEmailVerified; + case LCEngineUserHookType.OnLogin: + return OnLogin; + default: + throw new Exception($"Error hook type: {attr.HookType}"); + } + }); + + Functions = assembly.GetTypes() + .SelectMany(t => t.GetMethods()) + .Where(m => m.GetCustomAttribute() != null) + .ToDictionary(mi => mi.GetCustomAttribute().FunctionName); + + assembly.GetTypes() + .SelectMany(t => t.GetMethods()) + .Where(m => m.GetCustomAttribute() != null) + .ToDictionary(mi => { + LCEngineRealtimeHookAttribute attr = mi.GetCustomAttribute(); + switch (attr.HookType) { + case LCEngineRealtimeHookType.ClientOnline: + return ClientOnline; + case LCEngineRealtimeHookType.ClientOffline: + return ClientOffline; + case LCEngineRealtimeHookType.MessageSent: + return MessageSent; + case LCEngineRealtimeHookType.MessageReceived: + return MessageReceived; + case LCEngineRealtimeHookType.ReceiversOffline: + return ReceiversOffline; + case LCEngineRealtimeHookType.MessageUpdate: + return MessageUpdate; + case LCEngineRealtimeHookType.ConversationStart: + return ConversationStart; + case LCEngineRealtimeHookType.ConversationStarted: + return ConversationStarted; + case LCEngineRealtimeHookType.ConversationAdd: + return ConversationAdd; + case LCEngineRealtimeHookType.ConversationAdded: + return ConversationAdded; + case LCEngineRealtimeHookType.ConversationRemove: + return ConversationRemove; + case LCEngineRealtimeHookType.ConversationRemoved: + return ConversationRemoved; + case LCEngineRealtimeHookType.ConversationUpdate: + return ConversationUpdate; + default: + throw new Exception($"Error hook type: {attr.HookType}"); + } + }) + .ToList() + .ForEach(item => { + Functions.TryAdd(item.Key, item.Value); + }); + + services.AddCors(options => { + options.AddPolicy(LCEngineCORS, builder => { + builder.AllowAnyOrigin() + .WithMethods(LCEngineCORSMethods) + .WithHeaders(LCEngineCORSHeaders) + .SetPreflightMaxAge(TimeSpan.FromSeconds(86400)); + }); + }); + } + + public static void PrintEnvironmentVar(string key) { + LCLogger.Debug($"{key} : {Environment.GetEnvironmentVariable(key)}"); + } + + internal static async Task Invoke(MethodInfo mi, object[] parameters) { + try { + if (mi.ReturnType == typeof(Task) || + (mi.ReturnType.IsGenericType && mi.ReturnType.GetGenericTypeDefinition() == typeof(Task<>))) { + Task task = mi.Invoke(null, parameters) as Task; + await task; + return task.GetType().GetProperty("Result")?.GetValue(task); + } + return mi.Invoke(null, parameters); + } catch (TargetInvocationException e) { + Exception ex = e.InnerException; + if (ex is LCException lcEx) { + throw new Exception(JsonConvert.SerializeObject(new Dictionary { + { "code", lcEx.Code }, + { "message", lcEx.Message } + })); + } + throw ex; + } + } + + internal static async Task Invoke(MethodInfo mi, object request) { + try { + object[] ps = null; + if (mi.GetParameters().Length > 0) { + ps = new object[] { request }; + } + if (mi.ReturnType == typeof(Task) || + (mi.ReturnType.IsGenericType && mi.ReturnType.GetGenericTypeDefinition() == typeof(Task<>))) { + Task task = mi.Invoke(null, ps) as Task; + await task; + return task.GetType().GetProperty("Result")?.GetValue(task); + } + return mi.Invoke(null, ps); + } catch (TargetInvocationException e) { + Exception ex = e.InnerException; + if (ex is LCException lcEx) { + throw new Exception(JsonConvert.SerializeObject(new Dictionary { + { "code", lcEx.Code }, + { "message", lcEx.Message } + })); + } + throw ex; + } + } + + internal static Dictionary Decode(JsonElement jsonElement) { + string json = System.Text.Json.JsonSerializer.Serialize(jsonElement); + Dictionary dict = JsonConvert.DeserializeObject>(json, + LCJsonConverter.Default); + return dict; + } + + internal static void InitRequestContext(HttpRequest request) { + LCEngineRequestContext.Init(); + + LCEngineRequestContext.RemoteAddress = GetIP(request); + + if (request.Headers.TryGetValue("x-lc-session", out StringValues session)) { + LCEngineRequestContext.SessionToken = session; + } + } + + internal static string GetIP(HttpRequest request) { + if (request.Headers.TryGetValue("x-real-ip", out StringValues ip)) { + return ip.ToString(); + } + if (request.Headers.TryGetValue("x-forwarded-for", out StringValues forward)) { + return forward.ToString(); + } + return request.HttpContext.Connection.RemoteIpAddress.ToString(); + } + + internal static void CheckMasterKey(HttpRequest request) { + if (!request.Headers.TryGetValue(LCMasterKeyName, out StringValues masterKey)) { + throw new Exception("No master key"); + } + if (!masterKey.Equals(Environment.GetEnvironmentVariable("LEANCLOUD_APP_MASTER_KEY"))) { + throw new Exception("Mismatch master key"); + } + } + + internal static void CheckHookKey(HttpRequest request) { + if (!request.Headers.TryGetValue(LCHookKeyName, out StringValues hookKey)) { + throw new Exception("No hook key"); + } + if (!hookKey.Equals(Environment.GetEnvironmentVariable("LEANCLOUD_APP_HOOK_KEY"))) { + throw new Exception("Mismatch hook key"); + } + } + + public static object GetFunctions(HttpRequest request) { + CheckMasterKey(request); + + List functions = new List(); + functions.AddRange(Functions.Keys); + functions.AddRange(ClassHooks.Keys); + functions.AddRange(UserHooks.Keys); + foreach (string func in functions) { + LCLogger.Debug(func); + } + + return new Dictionary> { + { "result", functions } + }; + } + } +} diff --git a/Engine/LCEngineRequestContext.cs b/Engine/LCEngineRequestContext.cs new file mode 100644 index 0000000..c04c2f9 --- /dev/null +++ b/Engine/LCEngineRequestContext.cs @@ -0,0 +1,74 @@ +using System; +using System.Collections.Generic; +using System.Threading; +using LeanCloud.Storage; + +namespace LeanCloud.Engine { + public class LCEngineRequestContext { + public const string RemoteAddressKey = "__remoteAddressKey"; + public const string SessionTokenKey = "__sessionToken"; + public const string CurrentUserKey = "__currentUser"; + + private static ThreadLocal> requestContext = new ThreadLocal>(); + + public static void Init() { + if (requestContext.IsValueCreated) { + requestContext.Value.Clear(); + } + requestContext.Value = new Dictionary(); + } + + public static void Set(string key, object value) { + if (!requestContext.IsValueCreated) { + requestContext.Value = new Dictionary(); + } + requestContext.Value[key] = value; + } + + public static object Get(string key) { + if (!requestContext.IsValueCreated) { + return null; + } + return requestContext.Value[key]; + } + + public static string RemoteAddress { + get { + object remoteAddress = Get(RemoteAddressKey); + if (remoteAddress != null) { + return remoteAddress as string; + } + return null; + } + set { + Set(RemoteAddressKey, value); + } + } + + public static string SessionToken { + get { + object sessionToken = Get(SessionTokenKey); + if (sessionToken != null) { + return sessionToken as string; + } + return null; + } + set { + Set(SessionTokenKey, value); + } + } + + public static LCUser CurrentUser { + get { + object currentUser = Get(CurrentUserKey); + if (currentUser != null) { + return currentUser as LCUser; + } + return null; + } + set { + Set(CurrentUserKey, value); + } + } + } +} diff --git a/LiveQuery/LiveQuery.Test/LiveQuery.Test.csproj b/LiveQuery/LiveQuery.Test/LiveQuery.Test.csproj index 1915392..e2572b8 100644 --- a/LiveQuery/LiveQuery.Test/LiveQuery.Test.csproj +++ b/LiveQuery/LiveQuery.Test/LiveQuery.Test.csproj @@ -1,7 +1,7 @@ - netcoreapp2.2 + netcoreapp3.1 false 0.6.4 diff --git a/Realtime/Realtime.Test/Realtime.Test.csproj b/Realtime/Realtime.Test/Realtime.Test.csproj index f72f055..49426a2 100644 --- a/Realtime/Realtime.Test/Realtime.Test.csproj +++ b/Realtime/Realtime.Test/Realtime.Test.csproj @@ -1,7 +1,7 @@ - netcoreapp2.2 + netcoreapp3.1 false 0.6.4 diff --git a/Sample/LiveQueryApp/LiveQueryApp.csproj b/Sample/LiveQueryApp/LiveQueryApp.csproj index 58a668e..3275758 100644 --- a/Sample/LiveQueryApp/LiveQueryApp.csproj +++ b/Sample/LiveQueryApp/LiveQueryApp.csproj @@ -2,7 +2,7 @@ Exe - netcoreapp3.0 + netcoreapp3.1 0.6.4 diff --git a/Sample/RealtimeApp/RealtimeApp.csproj b/Sample/RealtimeApp/RealtimeApp.csproj index 3dabc64..975b1b4 100644 --- a/Sample/RealtimeApp/RealtimeApp.csproj +++ b/Sample/RealtimeApp/RealtimeApp.csproj @@ -2,7 +2,7 @@ Exe - netcoreapp3.0 + netcoreapp3.1 0.6.4 diff --git a/Storage/Storage.Test/CloudTest.cs b/Storage/Storage.Test/CloudTest.cs index 2084a74..53bbaed 100644 --- a/Storage/Storage.Test/CloudTest.cs +++ b/Storage/Storage.Test/CloudTest.cs @@ -10,7 +10,8 @@ namespace Storage.Test { [SetUp] public void SetUp() { LCLogger.LogDelegate += Utils.Print; - LCApplication.Initialize("ikGGdRE2YcVOemAaRbgp1xGJ-gzGzoHsz", "NUKmuRbdAhg1vrb2wexYo1jo", "https://ikggdre2.lc-cn-n1-shared.com"); + //LCApplication.Initialize("ikGGdRE2YcVOemAaRbgp1xGJ-gzGzoHsz", "NUKmuRbdAhg1vrb2wexYo1jo", "https://ikggdre2.lc-cn-n1-shared.com"); + LCApplication.Initialize("8ijVI3gBAnPGynW0rVfh5gHP-gzGzoHsz", "265r8JSHhNYpV0qIJBvUWrQY", "https://8ijvi3gb.lc-cn-n1-shared.com"); } [TearDown] @@ -24,7 +25,16 @@ namespace Storage.Test { { "name", "world" } }); TestContext.WriteLine(response["result"]); - Assert.AreEqual(response["result"], "hello, world"); + Assert.AreEqual(response["result"], "Hello, world!"); + } + + [Test] + public async Task RunAverageScore() { + float score = await LCCloud.Run("averageStars", new Dictionary { + { "movie", "夏洛特烦恼" } + }); + TestContext.WriteLine($"score: {score}"); + Assert.True(score.Equals(3.8f)); } [Test] @@ -41,5 +51,35 @@ namespace Storage.Test { Assert.NotNull(item.ObjectId); } } + + [Test] + public async Task RPCObject() { + LCQuery query = new LCQuery("Todo"); + LCObject todo = await query.Get("6052cd87b725a143ea83dbf8"); + object result = await LCCloud.RPC("getTodo", todo); + LCObject obj = result as LCObject; + TestContext.WriteLine(obj.ToString()); + } + + [Test] + public async Task RPCObjects() { + Dictionary parameters = new Dictionary { + { "limit", 20 } + }; + List result = await LCCloud.RPC("getTodos", parameters) as List; + IEnumerable todos = result.Cast(); + foreach (LCObject todo in todos) { + TestContext.WriteLine(todo.ObjectId); + } + } + + [Test] + public async Task RPCObjectMap() { + Dictionary result = await LCCloud.RPC("getTodoMap") as Dictionary; + foreach (KeyValuePair kv in result) { + LCObject todo = kv.Value as LCObject; + TestContext.WriteLine(todo.ObjectId); + } + } } } diff --git a/Storage/Storage.Test/Storage.Test.csproj b/Storage/Storage.Test/Storage.Test.csproj index c0a1aa8..523782b 100644 --- a/Storage/Storage.Test/Storage.Test.csproj +++ b/Storage/Storage.Test/Storage.Test.csproj @@ -1,7 +1,7 @@ - netcoreapp2.2 + netcoreapp3.1 false 0.6.4 diff --git a/Storage/Storage/Internal/Codec/LCDecoder.cs b/Storage/Storage/Internal/Codec/LCDecoder.cs index b4b9770..ec351ee 100644 --- a/Storage/Storage/Internal/Codec/LCDecoder.cs +++ b/Storage/Storage/Internal/Codec/LCDecoder.cs @@ -15,7 +15,7 @@ namespace LeanCloud.Storage.Internal.Codec { return DecodeBytes(dict); } else if (type == "Object") { return DecodeObject(dict); - } else if (type == "Pointer") { + } else if (type == "Pointer" || type == "Object") { return DecodeObject(dict); } else if (type == "Relation") { return DecodeRelation(dict); @@ -80,10 +80,10 @@ namespace LeanCloud.Storage.Internal.Codec { string key = kv.Key; Dictionary access = kv.Value as Dictionary; if (access.TryGetValue("read", out object ra)) { - acl.readAccess[key] = Convert.ToBoolean(ra); + acl.ReadAccess[key] = Convert.ToBoolean(ra); } if (access.TryGetValue("write", out object wa)) { - acl.writeAccess[key] = Convert.ToBoolean(wa); + acl.WriteAccess[key] = Convert.ToBoolean(wa); } } return acl; diff --git a/Storage/Storage/Internal/Codec/LCEncoder.cs b/Storage/Storage/Internal/Codec/LCEncoder.cs index a216200..453bee8 100644 --- a/Storage/Storage/Internal/Codec/LCEncoder.cs +++ b/Storage/Storage/Internal/Codec/LCEncoder.cs @@ -84,19 +84,19 @@ namespace LeanCloud.Storage.Internal.Codec { public static object EncodeACL(LCACL acl) { HashSet keys = new HashSet(); - if (acl.readAccess.Count > 0) { - keys.UnionWith(acl.readAccess.Keys); + if (acl.ReadAccess.Count > 0) { + keys.UnionWith(acl.ReadAccess.Keys); } - if (acl.writeAccess.Count > 0) { - keys.UnionWith(acl.writeAccess.Keys); + if (acl.WriteAccess.Count > 0) { + keys.UnionWith(acl.WriteAccess.Keys); } Dictionary result = new Dictionary(); foreach (string key in keys) { Dictionary access = new Dictionary(); - if (acl.readAccess.TryGetValue(key, out bool ra)) { + if (acl.ReadAccess.TryGetValue(key, out bool ra)) { access["read"] = ra; } - if (acl.writeAccess.TryGetValue(key, out bool wa)) { + if (acl.WriteAccess.TryGetValue(key, out bool wa)) { access["write"] = wa; } result[key] = access; diff --git a/Storage/Storage/Internal/Http/LCHttpClient.cs b/Storage/Storage/Internal/Http/LCHttpClient.cs index 04e2562..f1f9734 100644 --- a/Storage/Storage/Internal/Http/LCHttpClient.cs +++ b/Storage/Storage/Internal/Http/LCHttpClient.cs @@ -148,6 +148,11 @@ namespace LeanCloud.Storage.Internal.Http { string sign = $"{hash},{timestamp}"; headers.Add("X-LC-Sign", sign); } + if (LCApplication.AdditionalHeaders.Count > 0) { + foreach (KeyValuePair kv in LCApplication.AdditionalHeaders) { + headers.Add(kv.Key, kv.Value); + } + } // 当前用户 Session Token LCUser currentUser = await LCUser.GetCurrent(); if (!headers.Contains("X-LC-Session") && currentUser != null) { diff --git a/Storage/Storage/Internal/Object/LCObjectData.cs b/Storage/Storage/Internal/Object/LCObjectData.cs index bc5e90d..32ea0dd 100644 --- a/Storage/Storage/Internal/Object/LCObjectData.cs +++ b/Storage/Storage/Internal/Object/LCObjectData.cs @@ -65,10 +65,10 @@ namespace LeanCloud.Storage.Internal.Object { if (!string.IsNullOrEmpty(objectData.ObjectId)) { dict["objectId"] = objectData.ObjectId; } - if (objectData.CreatedAt != null) { + if (!objectData.CreatedAt.Equals(default)) { dict["createdAt"] = objectData.CreatedAt.ToUniversalTime(); } - if (objectData.UpdatedAt != null) { + if (!objectData.UpdatedAt.Equals(default)) { dict["updatedAt"] = objectData.UpdatedAt.ToUniversalTime(); } if (objectData.CustomPropertyDict != null) { diff --git a/Storage/Storage/Internal/Operation/ILCOperation.cs b/Storage/Storage/Internal/Operation/ILCOperation.cs index 5c6c375..658a2c8 100644 --- a/Storage/Storage/Internal/Operation/ILCOperation.cs +++ b/Storage/Storage/Internal/Operation/ILCOperation.cs @@ -1,5 +1,4 @@ using System.Collections; -using System.Collections.Generic; namespace LeanCloud.Storage.Internal.Operation { internal interface ILCOperation { diff --git a/Storage/Storage/Internal/Operation/LCIgnoreHookOperation.cs b/Storage/Storage/Internal/Operation/LCIgnoreHookOperation.cs new file mode 100644 index 0000000..992e905 --- /dev/null +++ b/Storage/Storage/Internal/Operation/LCIgnoreHookOperation.cs @@ -0,0 +1,39 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; + +namespace LeanCloud.Storage.Internal.Operation { + internal class LCIgnoreHookOperation : ILCOperation { + internal HashSet ignoreHooks; + + internal LCIgnoreHookOperation(IEnumerable hooks) { + ignoreHooks = new HashSet(hooks); + } + + public object Apply(object oldValue, string key) { + HashSet set = new HashSet(); + if (oldValue != null) { + set.UnionWith(oldValue as IEnumerable); + } + set.UnionWith(ignoreHooks); + return set.ToList(); + } + + public object Encode() { + return ignoreHooks; + } + + public IEnumerable GetNewObjectList() { + return ignoreHooks; + } + + public ILCOperation MergeWithPrevious(ILCOperation previousOp) { + if (previousOp is LCIgnoreHookOperation ignoreHookOp) { + ignoreHooks.UnionWith(ignoreHookOp.ignoreHooks); + return this; + } + throw new ArgumentException("Operation is invalid after previous operation."); + } + } +} diff --git a/Storage/Storage/LCACL.cs b/Storage/Storage/LCACL.cs index d07e5c0..3180eae 100644 --- a/Storage/Storage/LCACL.cs +++ b/Storage/Storage/LCACL.cs @@ -10,8 +10,13 @@ namespace LeanCloud.Storage { const string RoleKeyPrefix = "role:"; - internal Dictionary readAccess = new Dictionary(); - internal Dictionary writeAccess = new Dictionary(); + public Dictionary ReadAccess { + get; + } = new Dictionary(); + + public Dictionary WriteAccess { + get; + } = new Dictionary(); public static LCACL CreateWithOwner(LCUser owner) { if (owner == null) { @@ -25,17 +30,17 @@ namespace LeanCloud.Storage { public bool PublicReadAccess { get { - return GetAccess(readAccess, PublicKey); + return GetAccess(ReadAccess, PublicKey); } set { - SetAccess(readAccess, PublicKey, value); + SetAccess(ReadAccess, PublicKey, value); } } public bool PublicWriteAccess { get { - return GetAccess(writeAccess, PublicKey); + return GetAccess(WriteAccess, PublicKey); } set { - SetAccess(writeAccess, PublicKey, value); + SetAccess(WriteAccess, PublicKey, value); } } @@ -43,28 +48,28 @@ namespace LeanCloud.Storage { if (string.IsNullOrEmpty(userId)) { throw new ArgumentNullException(nameof(userId)); } - return GetAccess(readAccess, userId); + return GetAccess(ReadAccess, userId); } public void SetUserIdReadAccess(string userId, bool value) { if (string.IsNullOrEmpty(userId)) { throw new ArgumentNullException(nameof(userId)); } - SetAccess(readAccess, userId, value); + SetAccess(ReadAccess, userId, value); } public bool GetUserIdWriteAccess(string userId) { if (string.IsNullOrEmpty(userId)) { throw new ArgumentNullException(nameof(userId)); } - return GetAccess(writeAccess, userId); + return GetAccess(WriteAccess, userId); } public void SetUserIdWriteAccess(string userId, bool value) { if (string.IsNullOrEmpty(userId)) { throw new ArgumentNullException(nameof(userId)); } - SetAccess(writeAccess, userId, value); + SetAccess(WriteAccess, userId, value); } public bool GetUserReadAccess(LCUser user) { @@ -100,7 +105,7 @@ namespace LeanCloud.Storage { throw new ArgumentNullException(nameof(role)); } string roleKey = $"{RoleKeyPrefix}{role.ObjectId}"; - return GetAccess(readAccess, roleKey); + return GetAccess(ReadAccess, roleKey); } public void SetRoleReadAccess(LCRole role, bool value) { @@ -108,7 +113,7 @@ namespace LeanCloud.Storage { throw new ArgumentNullException(nameof(role)); } string roleKey = $"{RoleKeyPrefix}{role.ObjectId}"; - SetAccess(readAccess, roleKey, value); + SetAccess(ReadAccess, roleKey, value); } public bool GetRoleWriteAccess(LCRole role) { @@ -116,7 +121,7 @@ namespace LeanCloud.Storage { throw new ArgumentNullException(nameof(role)); } string roleKey = $"{RoleKeyPrefix}{role.ObjectId}"; - return GetAccess(writeAccess, roleKey); + return GetAccess(WriteAccess, roleKey); } public void SetRoleWriteAccess(LCRole role, bool value) { @@ -124,7 +129,7 @@ namespace LeanCloud.Storage { throw new ArgumentNullException(nameof(role)); } string roleKey = $"{RoleKeyPrefix}{role.ObjectId}"; - SetAccess(writeAccess, roleKey, value); + SetAccess(WriteAccess, roleKey, value); } bool GetAccess(Dictionary access, string key) { diff --git a/Storage/Storage/LCApplication.cs b/Storage/Storage/LCApplication.cs index 2a297e9..1b121fa 100644 --- a/Storage/Storage/LCApplication.cs +++ b/Storage/Storage/LCApplication.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using LeanCloud.Common; using LeanCloud.Storage; using LeanCloud.Storage.Internal.Http; @@ -9,7 +10,7 @@ namespace LeanCloud { /// public class LCApplication { // SDK 版本号,用于 User-Agent 统计 - internal const string SDKVersion = "0.6.4"; + public const string SDKVersion = "0.6.4"; // 接口版本号,用于接口版本管理 internal const string APIVersion = "1.1"; @@ -42,6 +43,10 @@ namespace LeanCloud { get; set; } + internal static Dictionary AdditionalHeaders { + get; + } = new Dictionary(); + public static void Initialize(string appId, string appKey, string server = null, @@ -68,5 +73,9 @@ namespace LeanCloud { HttpClient = new LCHttpClient(appId, appKey, server, SDKVersion, APIVersion); } + + public static void AddHeader(string key, string value) { + AdditionalHeaders.Add(key, value); + } } } diff --git a/Storage/Storage/LCCloud.cs b/Storage/Storage/LCCloud.cs index a1cbfd1..99fcdda 100644 --- a/Storage/Storage/LCCloud.cs +++ b/Storage/Storage/LCCloud.cs @@ -1,6 +1,7 @@ using System.Threading.Tasks; using System.Collections.Generic; using LeanCloud.Storage.Internal.Codec; +using LeanCloud.Storage.Internal.Object; namespace LeanCloud.Storage { /// @@ -35,6 +36,15 @@ namespace LeanCloud.Storage { return response; } + public static async Task Run(string name, + Dictionary parameters = null) { + Dictionary response = await Run(name, parameters); + if (response.TryGetValue("result", out object result)) { + return (T)result; + } + return default; + } + /// /// Invokes a cloud function as a remote procedure call. /// @@ -46,11 +56,48 @@ namespace LeanCloud.Storage { Dictionary headers = new Dictionary { { PRODUCTION_KEY, IsProduction ? 1 : 0 } }; - object encodeParams = LCEncoder.Encode(parameters); + object encodeParams = Encode(parameters); Dictionary response = await LCApplication.HttpClient.Post>(path, headers: headers, data: encodeParams); return LCDecoder.Decode(response["result"]); } + + public static object Encode(object parameters) { + if (parameters == null) { + return new Dictionary(); + } + + if (parameters is LCObject lcObj) { + return EncodeLCObject(lcObj); + } + + if (parameters is IList list) { + List l = new List(); + foreach (LCObject obj in list) { + l.Add(EncodeLCObject(obj)); + } + return l; + } + + if (parameters is IDictionary dict) { + Dictionary d = new Dictionary(); + foreach (KeyValuePair item in dict) { + d[item.Key] = EncodeLCObject(item.Value); + } + return d; + } + + return parameters; + } + + static object EncodeLCObject(LCObject obj) { + Dictionary dict = LCObjectData.Encode(obj.data); + dict["__type"] = "Object"; + foreach (KeyValuePair kv in obj.estimatedData) { + dict[kv.Key] = kv.Value; + } + return dict; + } } } diff --git a/Storage/Storage/LCHookObject.cs b/Storage/Storage/LCHookObject.cs new file mode 100644 index 0000000..afb8568 --- /dev/null +++ b/Storage/Storage/LCHookObject.cs @@ -0,0 +1,60 @@ +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Linq; +using LeanCloud.Storage.Internal.Operation; + +namespace LeanCloud.Storage { + public static class LCClassHook { + public const string BeforeSave = "beforeSave"; + public const string AfterSave = "afterSave"; + public const string BeforeUpdate = "beforeUpdate"; + public const string AfterUpdate = "afterUpdate"; + public const string BeforeDelete = "beforeDelete"; + public const string AfterDelete = "afterDelete"; + } + + public partial class LCObject { + internal const string IgnoreHooksKey = "__ignore_hooks"; + + public void DisableBeforeHook() { + Ignore( + LCClassHook.BeforeSave, + LCClassHook.BeforeUpdate, + LCClassHook.BeforeDelete); + } + + public void DisableAfterHook() { + Ignore( + LCClassHook.AfterSave, + LCClassHook.AfterUpdate, + LCClassHook.AfterDelete); + } + + public void IgnoreHook(string hookName) { + if (hookName != LCClassHook.BeforeSave && hookName != LCClassHook.AfterSave && + hookName != LCClassHook.BeforeUpdate && hookName != LCClassHook.AfterUpdate && + hookName != LCClassHook.BeforeDelete && hookName != LCClassHook.AfterDelete) { + throw new ArgumentException($"Invalid {hookName}"); + } + + Ignore(hookName); + } + + private void Ignore(params string[] hooks) { + LCIgnoreHookOperation op = new LCIgnoreHookOperation(hooks); + ApplyOperation(IgnoreHooksKey, op); + } + + public ReadOnlyCollection GetUpdatedKeys() { + if (this["_updatedKeys"] == null) { + return null; + } + + return (this["_updatedKeys"] as List) + .Cast() + .ToList() + .AsReadOnly(); + } + } +} diff --git a/Storage/Storage/LCObject.cs b/Storage/Storage/LCObject.cs index 0c302b2..422aa4b 100644 --- a/Storage/Storage/LCObject.cs +++ b/Storage/Storage/LCObject.cs @@ -12,11 +12,11 @@ namespace LeanCloud.Storage { /// /// LeanCloud Object /// - public class LCObject { + public partial class LCObject { /// /// Last synced data. /// - LCObjectData data; + internal LCObjectData data; /// /// Estimated data. @@ -349,18 +349,18 @@ namespace LeanCloud.Storage { return this; } - public static async Task> SaveAll(List objectList) { - if (objectList == null) { - throw new ArgumentNullException(nameof(objectList)); + public static async Task> SaveAll(IEnumerable objects) { + if (objects == null) { + throw new ArgumentNullException(nameof(objects)); } - foreach (LCObject obj in objectList) { + foreach (LCObject obj in objects) { if (LCBatch.HasCircleReference(obj, new HashSet())) { throw new ArgumentException("Found a circle dependency when save."); } } - Stack batches = LCBatch.BatchObjects(objectList, true); + Stack batches = LCBatch.BatchObjects(objects, true); await SaveBatches(batches); - return objectList; + return objects.ToList(); } public async Task Delete() { @@ -371,12 +371,11 @@ namespace LeanCloud.Storage { await LCApplication.HttpClient.Delete(path); } - public static async Task DeleteAll(List objectList) { - if (objectList == null || objectList.Count == 0) { - throw new ArgumentNullException(nameof(objectList)); + public static async Task DeleteAll(IEnumerable objects) { + if (objects == null || objects.Count() == 0) { + throw new ArgumentNullException(nameof(objects)); } - IEnumerable objects = objectList.Where(item => item.ObjectId != null); - HashSet objectSet = new HashSet(objects); + HashSet objectSet = new HashSet(objects.Where(item => item.ObjectId != null)); List> requestList = objectSet.Select(item => { string path = $"/{LCApplication.APIVersion}/classes/{item.ClassName}/{item.ObjectId}"; return new Dictionary { diff --git a/csharp-sdk.sln b/csharp-sdk.sln index 8697e48..3f7d58c 100644 --- a/csharp-sdk.sln +++ b/csharp-sdk.sln @@ -37,6 +37,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LiveQuery-Unity", "LiveQuer EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LiveQueryApp", "Sample\LiveQueryApp\LiveQueryApp.csproj", "{9D5E6A37-8925-48ED-B7EA-12C89291B59D}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Engine", "Engine", "{8087ABCD-629C-4EE5-9ECE-8BDAE631236F}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Engine", "Engine\Engine.csproj", "{0A6AEBC9-9A36-4EA7-8F58-8B951126092D}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -95,6 +99,10 @@ Global {9D5E6A37-8925-48ED-B7EA-12C89291B59D}.Debug|Any CPU.Build.0 = Debug|Any CPU {9D5E6A37-8925-48ED-B7EA-12C89291B59D}.Release|Any CPU.ActiveCfg = Release|Any CPU {9D5E6A37-8925-48ED-B7EA-12C89291B59D}.Release|Any CPU.Build.0 = Release|Any CPU + {0A6AEBC9-9A36-4EA7-8F58-8B951126092D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {0A6AEBC9-9A36-4EA7-8F58-8B951126092D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {0A6AEBC9-9A36-4EA7-8F58-8B951126092D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {0A6AEBC9-9A36-4EA7-8F58-8B951126092D}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(NestedProjects) = preSolution {26CDAE2A-6D79-4981-8D80-3EA34FDFB134} = {319A9989-3B69-4AD0-9E43-F6D31C1D2A4A} @@ -110,6 +118,7 @@ Global {0F61B6D7-4948-4D98-B6CC-41CF33B55669} = {A1A24E0F-6901-4A9A-9BB8-4F586BC7EE17} {12482E48-C0CF-46B1-8FDD-5885D1B7DC4D} = {A1A24E0F-6901-4A9A-9BB8-4F586BC7EE17} {9D5E6A37-8925-48ED-B7EA-12C89291B59D} = {2D980281-F060-4363-AB7A-D4B6C30ADDBB} + {0A6AEBC9-9A36-4EA7-8F58-8B951126092D} = {8087ABCD-629C-4EE5-9ECE-8BDAE631236F} EndGlobalSection GlobalSection(MonoDevelopProperties) = preSolution version = 0.6.4