You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
Connected.Framework/Connected.Rest/Api/ApiResolutionService.cs

208 lines
6.2 KiB

2 years ago
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('.');
}
}