Dynamic Javascript Client for API Services #98

pull/112/head
Halil İbrahim Kalkan 8 years ago
parent 67fca93d0f
commit 5ac3369a7f

@ -15,12 +15,12 @@ using Microsoft.AspNetCore.Mvc.Controllers;
using Microsoft.AspNetCore.Mvc.Infrastructure;
using Microsoft.AspNetCore.Mvc.ViewComponents;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Volo.Abp.Http;
namespace Volo.Abp.AspNetCore.Mvc
{
[DependsOn(
typeof(AbpAspNetCoreModule)
)]
[DependsOn(typeof(AbpHttpModule))]
[DependsOn(typeof(AbpAspNetCoreModule))]
public class AbpAspNetCoreMvcModule : AbpModule
{
public override void ConfigureServices(IServiceCollection services)

@ -1,7 +1,7 @@
using Microsoft.AspNetCore.Mvc;
using Volo.Abp.Http.Modeling;
namespace Volo.Abp.AspNetCore.Mvc.Controllers
namespace Volo.Abp.AspNetCore.Mvc.ApiExploring
{
[Area("abp")]
public class AbpApiDefinitionController : AbpController
@ -17,7 +17,7 @@ namespace Volo.Abp.AspNetCore.Mvc.Controllers
[Route("api/abp/api-description")]
public ApplicationApiDescriptionModel Get()
{
return _modelProvider.CreateModel();
return _modelProvider.CreateApiModel();
}
}
}

@ -32,7 +32,7 @@ namespace Volo.Abp.AspNetCore.Mvc
Logger = NullLogger<AspNetCoreApiDescriptionModelProvider>.Instance;
}
public ApplicationApiDescriptionModel CreateModel()
public ApplicationApiDescriptionModel CreateApiModel()
{
var model = ApplicationApiDescriptionModel.Create();

@ -0,0 +1,24 @@
using Microsoft.AspNetCore.Mvc;
using Volo.Abp.Http.ProxyScripting;
namespace Volo.Abp.AspNetCore.Mvc.ProxyScripting
{
//TODO: [DontWrapResult]
//TODO: [DisableAuditing]
public class AbpServiceProxiesController : AbpController
{
private readonly IProxyScriptManager _proxyScriptManager;
public AbpServiceProxiesController(IProxyScriptManager proxyScriptManager)
{
_proxyScriptManager = proxyScriptManager;
}
[Produces("text/javascript", "text/plain")]
public string GetAll(ServiceProxyGenerationModel model)
{
model.Normalize();
return _proxyScriptManager.GetScript(model.CreateOptions());
}
}
}

@ -0,0 +1,55 @@
using System;
using System.Linq;
using Volo.Abp.Http.ProxyScripting;
using Volo.Abp.Http.ProxyScripting.Generators.JQuery;
namespace Volo.Abp.AspNetCore.Mvc.ProxyScripting
{
public class ServiceProxyGenerationModel //: TODO: IShouldNormalize
{
public string Type { get; set; }
public bool UseCache { get; set; }
public string Modules { get; set; }
public string Controllers { get; set; }
public string Actions { get; set; }
public ServiceProxyGenerationModel()
{
UseCache = true;
}
public void Normalize()
{
if (Type.IsNullOrEmpty())
{
Type = JQueryProxyScriptGenerator.Name;
}
}
public ProxyScriptingModel CreateOptions()
{
var options = new ProxyScriptingModel(Type, UseCache);
if (!Modules.IsNullOrEmpty())
{
options.Modules = Modules.Split('|').Select(m => m.Trim()).ToArray();
}
if (!Controllers.IsNullOrEmpty())
{
options.Controllers = Controllers.Split('|').Select(m => m.Trim()).ToArray();
}
if (!Actions.IsNullOrEmpty())
{
options.Actions = Actions.Split('|').Select(m => m.Trim()).ToArray();
}
return options;
}
}
}

@ -7,6 +7,7 @@ using Microsoft.Extensions.Options;
using Volo.Abp.DependencyInjection;
using Volo.Abp.DynamicProxy;
using Volo.Abp.Http.Modeling;
using Volo.Abp.Http.ProxyScripting.Generators;
using Volo.Abp.Json;
using Volo.Abp.Threading;
@ -104,7 +105,7 @@ namespace Volo.Abp.Http.Client.DynamicProxying
private static void AddHeaders(IAbpMethodInvocation invocation, ActionApiDescriptionModel action, HttpRequestMessage requestMessage)
{
foreach (var headerParameter in action.Parameters.Where(p => p.BindingSourceId == "Header"))
foreach (var headerParameter in action.Parameters.Where(p => p.BindingSourceId == ParameterBindingSources.Header))
{
var value = HttpActionParameterHelper.FindParameterValue(invocation.ArgumentsDictionary, headerParameter);
if (value != null)

@ -4,6 +4,7 @@ using System.Net.Http;
using System.Text;
using JetBrains.Annotations;
using Volo.Abp.Http.Modeling;
using Volo.Abp.Http.ProxyScripting.Generators;
using Volo.Abp.Json;
namespace Volo.Abp.Http.Client.DynamicProxying
@ -33,7 +34,7 @@ namespace Volo.Abp.Http.Client.DynamicProxying
{
var parameters = action
.Parameters
.Where(p => p.BindingSourceId == "Body")
.Where(p => p.BindingSourceId == ParameterBindingSources.Body)
.ToArray();
if (parameters.Length <= 0)
@ -61,7 +62,7 @@ namespace Volo.Abp.Http.Client.DynamicProxying
{
var parameters = action
.Parameters
.Where(p => p.BindingSourceId == "Form")
.Where(p => p.BindingSourceId == ParameterBindingSources.Form)
.ToArray();
if (!parameters.Any())

@ -3,6 +3,7 @@ using System.Collections.Generic;
using System.Linq;
using System.Text;
using Volo.Abp.Http.Modeling;
using Volo.Abp.Http.ProxyScripting.Generators;
namespace Volo.Abp.Http.Client.DynamicProxying
{
@ -21,7 +22,7 @@ namespace Volo.Abp.Http.Client.DynamicProxying
private static void ReplacePathVariables(StringBuilder urlBuilder, IList<ParameterApiDescriptionModel> actionParameters, IReadOnlyDictionary<string, object> methodArguments)
{
var pathParameters = actionParameters
.Where(p => p.BindingSourceId == "Path")
.Where(p => p.BindingSourceId == ParameterBindingSources.Path)
.ToArray();
if (!pathParameters.Any())
@ -57,7 +58,7 @@ namespace Volo.Abp.Http.Client.DynamicProxying
private static void AddQueryStringParameters(StringBuilder urlBuilder, IList<ParameterApiDescriptionModel> actionParameters, IReadOnlyDictionary<string, object> methodArguments)
{
var queryStringParameters = actionParameters
.Where(p => p.BindingSourceId.IsIn("ModelBinding", "Query"))
.Where(p => p.BindingSourceId.IsIn(ParameterBindingSources.ModelBinding, ParameterBindingSources.Query))
.ToArray();
if (!queryStringParameters.Any())

@ -1,4 +1,6 @@
using Microsoft.Extensions.DependencyInjection;
using Volo.Abp.Http.ProxyScripting.Configuration;
using Volo.Abp.Http.ProxyScripting.Generators.JQuery;
using Volo.Abp.Modularity;
namespace Volo.Abp.Http
@ -8,6 +10,11 @@ namespace Volo.Abp.Http
public override void ConfigureServices(IServiceCollection services)
{
services.AddAssemblyOf<AbpHttpModule>();
services.Configure<AbpApiProxyScriptingOptions>(options =>
{
options.Generators[JQueryProxyScriptGenerator.Name] = typeof(JQueryProxyScriptGenerator);
});
}
}
}

@ -2,6 +2,6 @@ namespace Volo.Abp.Http.Modeling
{
public interface IApiDescriptionModelProvider
{
ApplicationApiDescriptionModel CreateModel();
ApplicationApiDescriptionModel CreateApiModel();
}
}

@ -0,0 +1,15 @@
using System;
using System.Collections.Generic;
namespace Volo.Abp.Http.ProxyScripting.Configuration
{
public class AbpApiProxyScriptingOptions
{
public IDictionary<string, Type> Generators { get; }
public AbpApiProxyScriptingOptions()
{
Generators = new Dictionary<string, Type>();
}
}
}

@ -0,0 +1,9 @@
using Volo.Abp.Http.Modeling;
namespace Volo.Abp.Http.ProxyScripting.Generators
{
public interface IProxyScriptGenerator
{
string CreateScript(ApplicationApiDescriptionModel model);
}
}

@ -0,0 +1,121 @@
using System;
using System.Text;
using Volo.Abp.DependencyInjection;
using Volo.Abp.Http.Modeling;
namespace Volo.Abp.Http.ProxyScripting.Generators.JQuery
{
public class JQueryProxyScriptGenerator : IProxyScriptGenerator, ITransientDependency
{
/// <summary>
/// "jquery".
/// </summary>
public const string Name = "jquery";
public string CreateScript(ApplicationApiDescriptionModel model)
{
var script = new StringBuilder();
script.AppendLine("/* This file is automatically generated by ABP framework to use MVC Controllers from javascript. */");
script.AppendLine();
script.AppendLine("var abp = abp || {};");
script.AppendLine("abp.services = abp.services || {};");
foreach (var module in model.Modules.Values)
{
script.AppendLine();
AddModuleScript(script, module);
}
return script.ToString();
}
private static void AddModuleScript(StringBuilder script, ModuleApiDescriptionModel module)
{
script.AppendLine($"// module '{module.Name.ToCamelCase()}'");
script.AppendLine("(function(){");
script.AppendLine();
script.AppendLine($" abp.services.{module.Name.ToCamelCase()} = abp.services.{module.Name.ToCamelCase()} || {{}};");
foreach (var controller in module.Controllers.Values)
{
script.AppendLine();
AddControllerScript(script, module, controller);
}
script.AppendLine();
script.AppendLine("})();");
}
private static void AddControllerScript(StringBuilder script, ModuleApiDescriptionModel module, ControllerApiDescriptionModel controller)
{
script.AppendLine($" // controller '{controller.ControllerName.ToCamelCase()}'");
script.AppendLine(" (function(){");
script.AppendLine();
script.AppendLine($" abp.services.{module.Name.ToCamelCase()}.{controller.ControllerName.ToCamelCase()} = abp.services.{module.Name.ToCamelCase()}.{controller.ControllerName.ToCamelCase()} || {{}};");
foreach (var action in controller.Actions.Values)
{
script.AppendLine();
AddActionScript(script, module, controller, action);
}
script.AppendLine();
script.AppendLine(" })();");
}
private static void AddActionScript(StringBuilder script, ModuleApiDescriptionModel module, ControllerApiDescriptionModel controller, ActionApiDescriptionModel action)
{
var parameterList = ProxyScriptingJsFuncHelper.GenerateJsFuncParameterList(action, "ajaxParams");
script.AppendLine($" // action '{action.NameOnClass.ToCamelCase()}'");
script.AppendLine($" abp.services.{module.Name.ToCamelCase()}.{controller.ControllerName.ToCamelCase()}{ProxyScriptingJsFuncHelper.WrapWithBracketsOrWithDotPrefix(action.NameOnClass.ToCamelCase())} = function({parameterList}) {{");
script.AppendLine(" return abp.ajax($.extend(true, {");
AddAjaxCallParameters(script, controller, action);
script.AppendLine(" }, ajaxParams));;");
script.AppendLine(" };");
}
private static void AddAjaxCallParameters(StringBuilder script, ControllerApiDescriptionModel controller, ActionApiDescriptionModel action)
{
var httpMethod = action.HttpMethod?.ToUpperInvariant() ?? "POST";
script.AppendLine(" url: abp.appPath + '" + ProxyScriptingHelper.GenerateUrlWithParameters(action) + "',");
script.Append(" type: '" + httpMethod + "'");
if (action.ReturnValue.Type == typeof(void))
{
script.AppendLine(",");
script.Append(" dataType: null");
}
var headers = ProxyScriptingHelper.GenerateHeaders(action, 8);
if (headers != null)
{
script.AppendLine(",");
script.Append(" headers: " + headers);
}
var body = ProxyScriptingHelper.GenerateBody(action);
if (!body.IsNullOrEmpty())
{
script.AppendLine(",");
script.Append(" data: JSON.stringify(" + body + ")");
}
else
{
var formData = ProxyScriptingHelper.GenerateFormPostData(action, 8);
if (!formData.IsNullOrEmpty())
{
script.AppendLine(",");
script.Append(" data: " + formData);
}
}
script.AppendLine();
}
}
}

@ -0,0 +1,14 @@
namespace Volo.Abp.Http.ProxyScripting.Generators
{
public static class ParameterBindingSources
{
public const string ModelBinding = "ModelBinding";
public const string Query = "Query";
public const string Body = "Body";
public const string Path = "Path";
public const string Form = "Form";
public const string Header = "Header";
public const string Custom = "Custom";
public const string Services = "Services";
}
}

@ -0,0 +1,144 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Volo.Abp.Http.Modeling;
namespace Volo.Abp.Http.ProxyScripting.Generators
{
internal static class ProxyScriptingHelper
{
public const string DefaultHttpVerb = "POST";
public static string GenerateUrlWithParameters(ActionApiDescriptionModel action)
{
//TODO: Can be optimized using StringBuilder?
var url = ReplacePathVariables(action.Url, action.Parameters);
url = AddQueryStringParameters(url, action.Parameters);
return url;
}
public static string GenerateHeaders(ActionApiDescriptionModel action, int indent = 0)
{
var parameters = action
.Parameters
.Where(p => p.BindingSourceId == ParameterBindingSources.Header)
.ToArray();
if (!parameters.Any())
{
return null;
}
return ProxyScriptingJsFuncHelper.CreateJsObjectLiteral(parameters, indent);
}
public static string GenerateBody(ActionApiDescriptionModel action)
{
var parameters = action
.Parameters
.Where(p => p.BindingSourceId == ParameterBindingSources.Body)
.ToArray();
if (parameters.Length <= 0)
{
return null;
}
if (parameters.Length > 1)
{
throw new AbpException(
$"Only one complex type allowed as argument to a controller action that's binding source is 'Body'. But {action.UniqueName} ({action.Url}) contains more than one!"
);
}
return ProxyScriptingJsFuncHelper.GetParamNameInJsFunc(parameters[0]);
}
public static string GenerateFormPostData(ActionApiDescriptionModel action, int indent = 0)
{
var parameters = action
.Parameters
.Where(p => p.BindingSourceId == ParameterBindingSources.Form)
.ToArray();
if (!parameters.Any())
{
return null;
}
return ProxyScriptingJsFuncHelper.CreateJsObjectLiteral(parameters, indent);
}
private static string ReplacePathVariables(string url, IList<ParameterApiDescriptionModel> actionParameters)
{
var pathParameters = actionParameters
.Where(p => p.BindingSourceId == ParameterBindingSources.Path)
.ToArray();
if (!pathParameters.Any())
{
return url;
}
foreach (var pathParameter in pathParameters)
{
url = url.Replace($"{{{pathParameter.Name}}}", $"' + {ProxyScriptingJsFuncHelper.GetParamNameInJsFunc(pathParameter)} + '");
}
return url;
}
private static string AddQueryStringParameters(string url, IList<ParameterApiDescriptionModel> actionParameters)
{
var queryStringParameters = actionParameters
.Where(p => p.BindingSourceId.IsIn(ParameterBindingSources.ModelBinding, ParameterBindingSources.Query))
.ToArray();
if (!queryStringParameters.Any())
{
return url;
}
var qsBuilderParams = queryStringParameters
.Select(p => $"{{ name: '{p.Name.ToCamelCase()}', value: {ProxyScriptingJsFuncHelper.GetParamNameInJsFunc(p)} }}")
.JoinAsString(", ");
return url + $"' + abp.utils.buildQueryString([{qsBuilderParams}]) + '";
}
public static string GetConventionalVerbForMethodName(string methodName)
{
if (methodName.StartsWith("Get", StringComparison.OrdinalIgnoreCase))
{
return "GET";
}
if (methodName.StartsWith("Put", StringComparison.OrdinalIgnoreCase) ||
methodName.StartsWith("Update", StringComparison.OrdinalIgnoreCase))
{
return "PUT";
}
if (methodName.StartsWith("Delete", StringComparison.OrdinalIgnoreCase) ||
methodName.StartsWith("Remove", StringComparison.OrdinalIgnoreCase))
{
return "DELETE";
}
if (methodName.StartsWith("Patch", StringComparison.OrdinalIgnoreCase))
{
return "PATCH";
}
if (methodName.StartsWith("Post", StringComparison.OrdinalIgnoreCase) ||
methodName.StartsWith("Create", StringComparison.OrdinalIgnoreCase) ||
methodName.StartsWith("Insert", StringComparison.OrdinalIgnoreCase))
{
return "POST";
}
//Default
return DefaultHttpVerb;
}
}
}

@ -0,0 +1,141 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Volo.Abp.Http.Modeling;
namespace Volo.Abp.Http.ProxyScripting.Generators
{
internal static class ProxyScriptingJsFuncHelper
{
private const string ValidJsVariableNameChars = "abcdefghijklmnopqrstuxwvyzABCDEFGHIJKLMNOPQRSTUXWVYZ0123456789_";
private static readonly HashSet<string> ReservedWords = new HashSet<string> {
"abstract",
"else",
"instanceof",
"super",
"boolean",
"enum",
"int",
"switch",
"break",
"export",
"interface",
"synchronized",
"byte",
"extends",
"let",
"this",
"case",
"false",
"long",
"throw",
"catch",
"final",
"native",
"throws",
"char",
"finally",
"new",
"transient",
"class",
"float",
"null",
"true",
"const",
"for",
"package",
"try",
"continue",
"function",
"private",
"typeof",
"debugger",
"goto",
"protected",
"var",
"default",
"if",
"public",
"void",
"delete",
"implements",
"return",
"volatile",
"do",
"import",
"short",
"while",
"double",
"in",
"static",
"with"
};
public static string NormalizeJsVariableName(string name, string additionalChars = "")
{
var validChars = ValidJsVariableNameChars + additionalChars;
var sb = new StringBuilder(name);
sb.Replace('-', '_');
//Delete invalid chars
foreach (var c in name)
{
if (!validChars.Contains(c))
{
sb.Replace(c.ToString(), "");
}
}
if (sb.Length == 0)
{
return "_" + Guid.NewGuid().ToString("N").Left(8);
}
return sb.ToString();
}
public static string WrapWithBracketsOrWithDotPrefix(string name)
{
if (!ReservedWords.Contains(name))
{
return "." + name;
}
return "['" + name + "']";
}
public static string GetParamNameInJsFunc(ParameterApiDescriptionModel parameterInfo)
{
return parameterInfo.Name == parameterInfo.NameOnMethod
? NormalizeJsVariableName(parameterInfo.Name.ToCamelCase(), ".")
: NormalizeJsVariableName(parameterInfo.NameOnMethod.ToCamelCase()) + "." + NormalizeJsVariableName(parameterInfo.Name.ToCamelCase(), ".");
}
public static string CreateJsObjectLiteral(ParameterApiDescriptionModel[] parameters, int indent = 0)
{
var sb = new StringBuilder();
sb.AppendLine("{");
foreach (var prm in parameters)
{
sb.AppendLine($"{new string(' ', indent)} '{prm.Name}': {GetParamNameInJsFunc(prm)}");
}
sb.Append(new string(' ', indent) + "}");
return sb.ToString();
}
public static string GenerateJsFuncParameterList(ActionApiDescriptionModel action, string ajaxParametersName)
{
var methodParamNames = action.Parameters.Select(p => p.NameOnMethod).Distinct().ToList();
methodParamNames.Add(ajaxParametersName);
return methodParamNames.Select(prmName => NormalizeJsVariableName(prmName.ToCamelCase())).JoinAsString(", ");
}
}
}

@ -0,0 +1,7 @@
namespace Volo.Abp.Http.ProxyScripting
{
public interface IProxyScriptManager
{
string GetScript(ProxyScriptingModel scriptingModel);
}
}

@ -0,0 +1,73 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
using Volo.Abp.DependencyInjection;
using Volo.Abp.Http.Modeling;
using Volo.Abp.Http.ProxyScripting.Configuration;
using Volo.Abp.Http.ProxyScripting.Generators;
using Volo.Abp.Json;
namespace Volo.Abp.Http.ProxyScripting
{
public class ProxyScriptManager : IProxyScriptManager, ISingletonDependency
{
private readonly IApiDescriptionModelProvider _modelProvider;
private readonly AbpApiProxyScriptingOptions _options;
private readonly IServiceProvider _serviceProvider;
private readonly IJsonSerializer _jsonSerializer;
private readonly ConcurrentDictionary<string, string> _cache;
public ProxyScriptManager(
IApiDescriptionModelProvider modelProvider,
IOptions<AbpApiProxyScriptingOptions> options,
IServiceProvider serviceProvider,
IJsonSerializer jsonSerializer)
{
_modelProvider = modelProvider;
_options = options.Value;
_serviceProvider = serviceProvider;
_jsonSerializer = jsonSerializer;
_cache = new ConcurrentDictionary<string, string>();
}
public string GetScript(ProxyScriptingModel scriptingModel)
{
if (scriptingModel.UseCache)
{
return _cache.GetOrAdd(CreateCacheKey(scriptingModel), (key) => CreateScript(scriptingModel));
}
return _cache[CreateCacheKey(scriptingModel)] = CreateScript(scriptingModel);
}
private string CreateScript(ProxyScriptingModel scriptingModel)
{
var apiModel = _modelProvider.CreateApiModel();
if (scriptingModel.IsPartialRequest())
{
apiModel = apiModel.CreateSubModel(scriptingModel.Modules, scriptingModel.Controllers, scriptingModel.Actions);
}
var generatorType = _options.Generators.GetOrDefault(scriptingModel.GeneratorType);
if (generatorType == null)
{
throw new AbpException($"Could not find a proxy script generator with given name: {scriptingModel.GeneratorType}");
}
using (var scope = _serviceProvider.CreateScope())
{
return scope.ServiceProvider.GetRequiredService(generatorType).As<IProxyScriptGenerator>().CreateScript(apiModel);
}
}
private string CreateCacheKey(ProxyScriptingModel model)
{
return _jsonSerializer.Serialize(model).ToMd5();
}
}
}

@ -0,0 +1,32 @@
using System.Collections.Generic;
namespace Volo.Abp.Http.ProxyScripting
{
public class ProxyScriptingModel
{
public string GeneratorType { get; set; }
public bool UseCache { get; set; }
public string[] Modules { get; set; }
public string[] Controllers { get; set; }
public string[] Actions { get; set; }
public IDictionary<string, string> Properties { get; set; }
public ProxyScriptingModel(string generatorType, bool useCache = true)
{
GeneratorType = generatorType;
UseCache = useCache;
Properties = new Dictionary<string, string>();
}
public bool IsPartialRequest()
{
return !(Modules.IsNullOrEmpty() && Controllers.IsNullOrEmpty() && Actions.IsNullOrEmpty());
}
}
}

@ -0,0 +1,16 @@
using System.Threading.Tasks;
using Shouldly;
using Xunit;
namespace Volo.Abp.AspNetCore.Mvc.ProxyScripting
{
public class AbpServiceProxiesController_Tests : AspNetCoreMvcTestBase
{
[Fact]
public async Task GetAll()
{
var script = await GetResponseAsStringAsync("/AbpServiceProxies/GetAll");
script.Length.ShouldBeGreaterThan(0);
}
}
}
Loading…
Cancel
Save