|
|
|
|
using System.Collections.Immutable;
|
|
|
|
|
using System.Reflection;
|
|
|
|
|
using System.Text;
|
|
|
|
|
using Connected.Annotations;
|
|
|
|
|
using Connected.Configuration.Environment;
|
|
|
|
|
using Connected.ServiceModel;
|
|
|
|
|
using Connected.Services;
|
|
|
|
|
using Microsoft.AspNetCore.Http;
|
|
|
|
|
|
|
|
|
|
namespace Connected.Rest;
|
|
|
|
|
|
|
|
|
|
internal class ApiResolutionService : IApiResolutionService
|
|
|
|
|
{
|
|
|
|
|
private readonly Dictionary<string, List<ApiInvokeDescriptor>> _methods;
|
|
|
|
|
private readonly Dictionary<string, List<ArgumentDescriptor>> _arguments;
|
|
|
|
|
|
|
|
|
|
public ApiResolutionService(IEnvironmentService environmentService)
|
|
|
|
|
{
|
|
|
|
|
_methods = new(StringComparer.OrdinalIgnoreCase);
|
|
|
|
|
_arguments = new(StringComparer.OrdinalIgnoreCase);
|
|
|
|
|
|
|
|
|
|
EnvironmentService = environmentService;
|
|
|
|
|
|
|
|
|
|
Initialize();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private Dictionary<string, List<ApiInvokeDescriptor>> Methods => _methods;
|
|
|
|
|
private Dictionary<string, List<ArgumentDescriptor>> Arguments => _arguments;
|
|
|
|
|
private IEnvironmentService EnvironmentService { get; }
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// This method tries to resolve argument implementation type based on the parameter's type interface.
|
|
|
|
|
/// </summary>
|
|
|
|
|
/// <param name="parameter">The implementation parameter of the method which declares the argument</param>
|
|
|
|
|
/// <returns><see cref="Type"/> that implements <paramref name="parameter"/>'s type interface.</returns>
|
|
|
|
|
public Type? ResolveArgument(ParameterInfo parameter)
|
|
|
|
|
{
|
|
|
|
|
if (!Arguments.ContainsKey(ArgumentName(parameter.ParameterType)))
|
|
|
|
|
return null;
|
|
|
|
|
|
|
|
|
|
var items = Arguments[ArgumentName(parameter.ParameterType)];
|
|
|
|
|
/*
|
|
|
|
|
* If the interafce has only one implementation the air is clear.
|
|
|
|
|
*/
|
|
|
|
|
if (items.Count == 1)
|
|
|
|
|
return WrapArgument(items[0].Type, parameter);
|
|
|
|
|
/*
|
|
|
|
|
* We have more than one implementation. We'll try to find the implementation that match
|
|
|
|
|
* the assembly of the invoking method. This is the most probable scenario.
|
|
|
|
|
*/
|
|
|
|
|
foreach (var argument in items)
|
|
|
|
|
{
|
|
|
|
|
if (argument.Type.Assembly == parameter.ParameterType.Assembly)
|
|
|
|
|
return WrapArgument(argument.Type, parameter);
|
|
|
|
|
}
|
|
|
|
|
/*
|
|
|
|
|
* Method's assembly doesn't have an implementation, let's try to look in the
|
|
|
|
|
* interface's assembly.
|
|
|
|
|
*/
|
|
|
|
|
foreach (var argument in items)
|
|
|
|
|
{
|
|
|
|
|
if (argument.Type.Assembly == parameter.ParameterType.Assembly)
|
|
|
|
|
return WrapArgument(argument.Type, parameter);
|
|
|
|
|
}
|
|
|
|
|
/*
|
|
|
|
|
* Nope, there must be some intermediate assembly implementing the argument and it surely must
|
|
|
|
|
* be referenced by the method's assembly.
|
|
|
|
|
*/
|
|
|
|
|
return WrapArgument(items[0].Type, parameter);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private static Type WrapArgument(Type argument, ParameterInfo parameter)
|
|
|
|
|
{
|
|
|
|
|
if (!argument.IsGenericType)
|
|
|
|
|
return argument;
|
|
|
|
|
|
|
|
|
|
return argument.MakeGenericType(parameter.ParameterType.GetGenericArguments());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public ApiInvokeDescriptor? ResolveMethod(HttpContext context)
|
|
|
|
|
{
|
|
|
|
|
var route = context.Request.Path.Value;
|
|
|
|
|
|
|
|
|
|
if (route is null)
|
|
|
|
|
return null;
|
|
|
|
|
|
|
|
|
|
if (!Methods.TryGetValue(route.ToString(), out List<ApiInvokeDescriptor>? descriptor))
|
|
|
|
|
return null;
|
|
|
|
|
|
|
|
|
|
//TODO: map overloads from arguments
|
|
|
|
|
return descriptor[0];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void Initialize()
|
|
|
|
|
{
|
|
|
|
|
foreach (var type in EnvironmentService.Services.Services)
|
|
|
|
|
InitializeApiService(type);
|
|
|
|
|
|
|
|
|
|
foreach (var type in EnvironmentService.Services.Arguments)
|
|
|
|
|
InitializeArgument(type);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void InitializeArgument(Type type)
|
|
|
|
|
{
|
|
|
|
|
if (type.GetImplementedArguments() is not List<Type> arguments || !arguments.Any())
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
|
|
foreach (var argument in arguments)
|
|
|
|
|
{
|
|
|
|
|
var name = ArgumentName(argument);
|
|
|
|
|
|
|
|
|
|
if (Arguments.TryGetValue(name, out _))
|
|
|
|
|
Arguments[name].Add(new ArgumentDescriptor { Type = type });
|
|
|
|
|
else
|
|
|
|
|
Arguments.Add(name, new List<ArgumentDescriptor> { new ArgumentDescriptor { Type = type } });
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void InitializeApiService(Type type)
|
|
|
|
|
{
|
|
|
|
|
if (type.GetImplementedServices() is not List<Type> services || !services.Any())
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
|
|
foreach (var service in services)
|
|
|
|
|
{
|
|
|
|
|
var serviceUrl = ResolveServiceUrl(service);
|
|
|
|
|
var methods = service.GetMethods(BindingFlags.Public | BindingFlags.Instance);
|
|
|
|
|
|
|
|
|
|
foreach (var method in methods)
|
|
|
|
|
{
|
|
|
|
|
if (method.GetCustomAttribute<ServiceMethodAttribute>() is not ServiceMethodAttribute attribute || attribute.Verbs == ServiceMethodVerbs.None)
|
|
|
|
|
continue;
|
|
|
|
|
|
|
|
|
|
InitializeServiceMethod(serviceUrl, service, method, attribute.Verbs);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void InitializeServiceMethod(string serviceUrl, Type serviceType, MethodInfo method, ServiceMethodVerbs verbs)
|
|
|
|
|
{
|
|
|
|
|
var parameterTypes = new List<Type>();
|
|
|
|
|
|
|
|
|
|
foreach (var parameter in method.GetParameters())
|
|
|
|
|
parameterTypes.Add(parameter.ParameterType);
|
|
|
|
|
|
|
|
|
|
var targetMethod = serviceType.GetMethod(method.Name, parameterTypes.ToArray());
|
|
|
|
|
var methodUrl = $"{serviceUrl}/{ResolveMethodUrl(targetMethod)}";
|
|
|
|
|
var descriptor = new ApiInvokeDescriptor { Service = serviceType, Method = targetMethod, Parameters = parameterTypes.ToArray(), Verbs = verbs };
|
|
|
|
|
|
|
|
|
|
if (Methods.TryGetValue(methodUrl, out List<ApiInvokeDescriptor>? items))
|
|
|
|
|
items.Add(descriptor);
|
|
|
|
|
else
|
|
|
|
|
Methods.Add(methodUrl, new List<ApiInvokeDescriptor> { descriptor });
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public ImmutableList<Tuple<string, ServiceMethodVerbs>> QueryRoutes()
|
|
|
|
|
{
|
|
|
|
|
var result = new List<Tuple<string, ServiceMethodVerbs>>();
|
|
|
|
|
|
|
|
|
|
foreach (var method in Methods)
|
|
|
|
|
{
|
|
|
|
|
var verbs = ServiceMethodVerbs.None;
|
|
|
|
|
|
|
|
|
|
foreach (var descriptor in method.Value)
|
|
|
|
|
verbs |= descriptor.Verbs;
|
|
|
|
|
|
|
|
|
|
result.Add(Tuple.Create(method.Key, verbs));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return result.ToImmutableList();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private static string ResolveServiceUrl(Type type)
|
|
|
|
|
{
|
|
|
|
|
if (type.GetCustomAttribute<ServiceUrlAttribute>() is ServiceUrlAttribute attribute)
|
|
|
|
|
return attribute.Template;
|
|
|
|
|
|
|
|
|
|
return $"{PascalNamespace(type.Namespace)}/{type.Name.ToPascalCase()}".Replace('.', '/');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private static string ResolveMethodUrl(MethodInfo method)
|
|
|
|
|
{
|
|
|
|
|
if (method.GetCustomAttribute<ServiceUrlAttribute>() is ServiceUrlAttribute attribute)
|
|
|
|
|
return attribute.Template;
|
|
|
|
|
|
|
|
|
|
return method.Name.ToCamelCase();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private static string ArgumentName(Type argument)
|
|
|
|
|
{
|
|
|
|
|
return $"{argument.Namespace}.{argument.Name}, {argument.Assembly.FullName}";
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private static string? PascalNamespace(string? @namespace)
|
|
|
|
|
{
|
|
|
|
|
if (string.IsNullOrEmpty(@namespace))
|
|
|
|
|
return null;
|
|
|
|
|
|
|
|
|
|
var tokens = @namespace.Split('.');
|
|
|
|
|
var result = new StringBuilder();
|
|
|
|
|
|
|
|
|
|
foreach (var token in tokens)
|
|
|
|
|
result.Append($"{token.ToPascalCase()}.");
|
|
|
|
|
|
|
|
|
|
return result.ToString().TrimEnd('.');
|
|
|
|
|
}
|
|
|
|
|
}
|