Merge pull request #107 from onerain88/leanengine

LeanEngine C# SDK
oneRain 2021-03-25 10:56:59 +08:00 committed by GitHub
commit 6139bdd914
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
31 changed files with 1074 additions and 48 deletions

View File

@ -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);
}

View File

@ -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;
}
}
}

View File

@ -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;
}
}
}

View File

@ -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;
}
}
}

View File

@ -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;
}
}
}

View File

@ -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;
}
}
}

View File

@ -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<string, MethodInfo> ClassHooks => LCEngine.ClassHooks;
[HttpPost("functions/{className}/{hookName}")]
public async Task<object> 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<string, object> data = LCEngine.Decode(body);
LCObjectData objectData = LCObjectData.Decode(data["object"] as Dictionary<string, object>);
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<string, object>));
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}");
}
}
}
}

View File

@ -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<string, MethodInfo> 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<object> 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<string, object> {
{ "result", result }
};
}
}
return body;
} catch (Exception e) {
return StatusCode(500, e.Message);
}
}
[HttpPost("call/{funcName}")]
public async Task<object> 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<string, object> {
{ "result", LCCloud.Encode(result) }
};
}
}
return body;
} catch (Exception e) {
return StatusCode(500, e.Message);
}
}
private static object[] ParseParameters(MethodInfo mi, JsonElement body) {
Dictionary<string, object> parameters = LCEngine.Decode(body);
List<object> ps = new List<object>();
if (mi.GetParameters().Length > 0) {
if (Array.Exists(mi.GetParameters(),
p => p.GetCustomAttribute<LCEngineFunctionParamAttribute>() != null)) {
// 如果包含 LCEngineFunctionParamAttribute 的参数,则按照配对方式传递参数
foreach (ParameterInfo pi in mi.GetParameters()) {
LCEngineFunctionParamAttribute attr = pi.GetCustomAttribute<LCEngineFunctionParamAttribute>();
if (attr != null) {
string paramName = attr.ParamName;
ps.Add(parameters[paramName]);
}
}
} else {
ps.Add(LCDecoder.Decode(LCEngine.Decode(body)));
}
}
return ps.ToArray();
}
}
}

View File

@ -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<string, string> {
{ "runtime", $"dotnet-{Environment.Version}" },
{ "version", LCApplication.SDKVersion }
};
}
}
}

View File

@ -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<string, MethodInfo> UserHooks => LCEngine.UserHooks;
[HttpPost("onVerified/sms")]
public async Task<object> 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<string, object> 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<object> 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<string, object> 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<object> 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<string, object> dict = LCEngine.Decode(body);
return await Invoke(mi, dict);
}
return body;
} catch (Exception e) {
return StatusCode(500, e.Message);
}
}
private static async Task<object> Invoke(MethodInfo mi, Dictionary<string, object> dict) {
LCObjectData objectData = LCObjectData.Decode(dict["object"] as Dictionary<string, object>);
objectData.ClassName = "_User";
LCObject user = LCObject.Create("_User");
user.Merge(objectData);
return await LCEngine.Invoke(mi, new object[] { user }) as LCObject;
}
}
}

20
Engine/Engine.csproj Normal file
View File

@ -0,0 +1,20 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netcoreapp3.1</TargetFramework>
<ReleaseVersion>0.6.4</ReleaseVersion>
<RootNamespace>LeanCloud.Engine</RootNamespace>
<CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\Common\Common\Common.csproj" />
<ProjectReference Include="..\Storage\Storage\Storage.csproj" />
</ItemGroup>
<ItemGroup>
<FrameworkReference Include="Microsoft.AspNetCore.App" />
</ItemGroup>
<ItemGroup>
<Folder Include="Controllers\" />
</ItemGroup>
</Project>

312
Engine/LCEngine.cs Normal file
View File

@ -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<string, MethodInfo> Functions = new Dictionary<string, MethodInfo>();
public static Dictionary<string, MethodInfo> ClassHooks = new Dictionary<string, MethodInfo>();
public static Dictionary<string, MethodInfo> UserHooks = new Dictionary<string, MethodInfo>();
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<LCEngineClassHookAttribute>() != null)
.ToDictionary(mi => {
LCEngineClassHookAttribute attr = mi.GetCustomAttribute<LCEngineClassHookAttribute>();
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<LCEngineUserHookAttribute>() != null)
.ToDictionary(mi => {
LCEngineUserHookAttribute attr = mi.GetCustomAttribute<LCEngineUserHookAttribute>();
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<LCEngineFunctionAttribute>() != null)
.ToDictionary(mi => mi.GetCustomAttribute<LCEngineFunctionAttribute>().FunctionName);
assembly.GetTypes()
.SelectMany(t => t.GetMethods())
.Where(m => m.GetCustomAttribute<LCEngineRealtimeHookAttribute>() != null)
.ToDictionary(mi => {
LCEngineRealtimeHookAttribute attr = mi.GetCustomAttribute<LCEngineRealtimeHookAttribute>();
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<object> 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<string, object> {
{ "code", lcEx.Code },
{ "message", lcEx.Message }
}));
}
throw ex;
}
}
internal static async Task<object> 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<string, object> {
{ "code", lcEx.Code },
{ "message", lcEx.Message }
}));
}
throw ex;
}
}
internal static Dictionary<string, object> Decode(JsonElement jsonElement) {
string json = System.Text.Json.JsonSerializer.Serialize(jsonElement);
Dictionary<string, object> dict = JsonConvert.DeserializeObject<Dictionary<string, object>>(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<string> functions = new List<string>();
functions.AddRange(Functions.Keys);
functions.AddRange(ClassHooks.Keys);
functions.AddRange(UserHooks.Keys);
foreach (string func in functions) {
LCLogger.Debug(func);
}
return new Dictionary<string, List<string>> {
{ "result", functions }
};
}
}
}

View File

@ -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<Dictionary<string, object>> requestContext = new ThreadLocal<Dictionary<string, object>>();
public static void Init() {
if (requestContext.IsValueCreated) {
requestContext.Value.Clear();
}
requestContext.Value = new Dictionary<string, object>();
}
public static void Set(string key, object value) {
if (!requestContext.IsValueCreated) {
requestContext.Value = new Dictionary<string, object>();
}
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);
}
}
}
}

View File

@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netcoreapp2.2</TargetFramework>
<TargetFramework>netcoreapp3.1</TargetFramework>
<IsPackable>false</IsPackable>
<ReleaseVersion>0.6.4</ReleaseVersion>

View File

@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netcoreapp2.2</TargetFramework>
<TargetFramework>netcoreapp3.1</TargetFramework>
<IsPackable>false</IsPackable>
<ReleaseVersion>0.6.4</ReleaseVersion>

View File

@ -2,7 +2,7 @@
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>netcoreapp3.0</TargetFramework>
<TargetFramework>netcoreapp3.1</TargetFramework>
<ReleaseVersion>0.6.4</ReleaseVersion>
</PropertyGroup>

View File

@ -2,7 +2,7 @@
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>netcoreapp3.0</TargetFramework>
<TargetFramework>netcoreapp3.1</TargetFramework>
<ReleaseVersion>0.6.4</ReleaseVersion>
</PropertyGroup>

View File

@ -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<float>("averageStars", new Dictionary<string, object> {
{ "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<LCObject> query = new LCQuery<LCObject>("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<string, object> parameters = new Dictionary<string, object> {
{ "limit", 20 }
};
List<object> result = await LCCloud.RPC("getTodos", parameters) as List<object>;
IEnumerable<LCObject> todos = result.Cast<LCObject>();
foreach (LCObject todo in todos) {
TestContext.WriteLine(todo.ObjectId);
}
}
[Test]
public async Task RPCObjectMap() {
Dictionary<string, object> result = await LCCloud.RPC("getTodoMap") as Dictionary<string, object>;
foreach (KeyValuePair<string, object> kv in result) {
LCObject todo = kv.Value as LCObject;
TestContext.WriteLine(todo.ObjectId);
}
}
}
}

View File

@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netcoreapp2.2</TargetFramework>
<TargetFramework>netcoreapp3.1</TargetFramework>
<IsPackable>false</IsPackable>
<ReleaseVersion>0.6.4</ReleaseVersion>

View File

@ -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<string, object> access = kv.Value as Dictionary<string, object>;
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;

View File

@ -84,19 +84,19 @@ namespace LeanCloud.Storage.Internal.Codec {
public static object EncodeACL(LCACL acl) {
HashSet<string> keys = new HashSet<string>();
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<string, object> result = new Dictionary<string, object>();
foreach (string key in keys) {
Dictionary<string, bool> access = new Dictionary<string, bool>();
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;

View File

@ -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<string, string> 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) {

View File

@ -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) {

View File

@ -1,5 +1,4 @@
using System.Collections;
using System.Collections.Generic;
namespace LeanCloud.Storage.Internal.Operation {
internal interface ILCOperation {

View File

@ -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<string> ignoreHooks;
internal LCIgnoreHookOperation(IEnumerable<string> hooks) {
ignoreHooks = new HashSet<string>(hooks);
}
public object Apply(object oldValue, string key) {
HashSet<object> set = new HashSet<object>();
if (oldValue != null) {
set.UnionWith(oldValue as IEnumerable<object>);
}
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.");
}
}
}

View File

@ -10,8 +10,13 @@ namespace LeanCloud.Storage {
const string RoleKeyPrefix = "role:";
internal Dictionary<string, bool> readAccess = new Dictionary<string, bool>();
internal Dictionary<string, bool> writeAccess = new Dictionary<string, bool>();
public Dictionary<string, bool> ReadAccess {
get;
} = new Dictionary<string, bool>();
public Dictionary<string, bool> WriteAccess {
get;
} = new Dictionary<string, bool>();
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<string, bool> access, string key) {

View File

@ -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 {
/// </summary>
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<string, string> AdditionalHeaders {
get;
} = new Dictionary<string, string>();
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);
}
}
}

View File

@ -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 {
/// <summary>
@ -35,6 +36,15 @@ namespace LeanCloud.Storage {
return response;
}
public static async Task<T> Run<T>(string name,
Dictionary<string, object> parameters = null) {
Dictionary<string, object> response = await Run(name, parameters);
if (response.TryGetValue("result", out object result)) {
return (T)result;
}
return default;
}
/// <summary>
/// Invokes a cloud function as a remote procedure call.
/// </summary>
@ -46,11 +56,48 @@ namespace LeanCloud.Storage {
Dictionary<string, object> headers = new Dictionary<string, object> {
{ PRODUCTION_KEY, IsProduction ? 1 : 0 }
};
object encodeParams = LCEncoder.Encode(parameters);
object encodeParams = Encode(parameters);
Dictionary<string, object> response = await LCApplication.HttpClient.Post<Dictionary<string, object>>(path,
headers: headers,
data: encodeParams);
return LCDecoder.Decode(response["result"]);
}
public static object Encode(object parameters) {
if (parameters == null) {
return new Dictionary<string, object>();
}
if (parameters is LCObject lcObj) {
return EncodeLCObject(lcObj);
}
if (parameters is IList<LCObject> list) {
List<object> l = new List<object>();
foreach (LCObject obj in list) {
l.Add(EncodeLCObject(obj));
}
return l;
}
if (parameters is IDictionary<string, LCObject> dict) {
Dictionary<string, object> d = new Dictionary<string, object>();
foreach (KeyValuePair<string, LCObject> item in dict) {
d[item.Key] = EncodeLCObject(item.Value);
}
return d;
}
return parameters;
}
static object EncodeLCObject(LCObject obj) {
Dictionary<string, object> dict = LCObjectData.Encode(obj.data);
dict["__type"] = "Object";
foreach (KeyValuePair<string, object> kv in obj.estimatedData) {
dict[kv.Key] = kv.Value;
}
return dict;
}
}
}

View File

@ -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<string> GetUpdatedKeys() {
if (this["_updatedKeys"] == null) {
return null;
}
return (this["_updatedKeys"] as List<object>)
.Cast<string>()
.ToList()
.AsReadOnly();
}
}
}

View File

@ -12,11 +12,11 @@ namespace LeanCloud.Storage {
/// <summary>
/// LeanCloud Object
/// </summary>
public class LCObject {
public partial class LCObject {
/// <summary>
/// Last synced data.
/// </summary>
LCObjectData data;
internal LCObjectData data;
/// <summary>
/// Estimated data.
@ -349,18 +349,18 @@ namespace LeanCloud.Storage {
return this;
}
public static async Task<List<LCObject>> SaveAll(List<LCObject> objectList) {
if (objectList == null) {
throw new ArgumentNullException(nameof(objectList));
public static async Task<List<LCObject>> SaveAll(IEnumerable<LCObject> 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<LCObject>())) {
throw new ArgumentException("Found a circle dependency when save.");
}
}
Stack<LCBatch> batches = LCBatch.BatchObjects(objectList, true);
Stack<LCBatch> 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<LCObject> objectList) {
if (objectList == null || objectList.Count == 0) {
throw new ArgumentNullException(nameof(objectList));
public static async Task DeleteAll(IEnumerable<LCObject> objects) {
if (objects == null || objects.Count() == 0) {
throw new ArgumentNullException(nameof(objects));
}
IEnumerable<LCObject> objects = objectList.Where(item => item.ObjectId != null);
HashSet<LCObject> objectSet = new HashSet<LCObject>(objects);
HashSet<LCObject> objectSet = new HashSet<LCObject>(objects.Where(item => item.ObjectId != null));
List<Dictionary<string, object>> requestList = objectSet.Select(item => {
string path = $"/{LCApplication.APIVersion}/classes/{item.ClassName}/{item.ObjectId}";
return new Dictionary<string, object> {

View File

@ -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