diff --git a/ApiAggregator.sln b/ApiAggregator.sln index 6bdaaf4..0a7fbcf 100644 --- a/ApiAggregator.sln +++ b/ApiAggregator.sln @@ -5,17 +5,13 @@ VisualStudioVersion = 17.9.34723.18 MinimumVisualStudioVersion = 10.0.40219.1 Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{4338FF70-3C81-4370-ACFB-00E14545BA99}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ApiAggregator.Net", "src\ApiAggregator\ApiAggregator.Net.csproj", "{8250784C-5415-47C2-9FE4-9E54FA4672B6}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ApiAggregator.Net", "src\ApiAggregator\ApiAggregator.Net.csproj", "{8250784C-5415-47C2-9FE4-9E54FA4672B6}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{31E7A02C-167D-46FB-A90A-F3995FD5682D}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ApiAggregator.Tests", "tests\ApiAggregator.Tests\ApiAggregator.Tests.csproj", "{C9ED08F3-F754-4D7A-8034-8FC180EA7F7A}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ApiAggregator.Tests", "tests\ApiAggregator.Tests\ApiAggregator.Tests.csproj", "{C9ED08F3-F754-4D7A-8034-8FC180EA7F7A}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "misc", "misc", "{D6340772-5767-4604-9E64-04078C0C2CAC}" - ProjectSection(SolutionItems) = preProject - LICENSE = LICENSE - README.md = README.md - EndProjectSection EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "github", "github", "{BCE2D3FE-6CF1-4932-9DEE-5B761A132C5E}" ProjectSection(SolutionItems) = preProject diff --git a/README.md b/README.md index 119e167..3774166 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,6 @@ #### Extends `Schemio` for APIs ApiAggregator uses `Schemio` to extend support for apis to configure hierarchical graph of `query`/`transformer` pairs to return aggregated data in a single response. > You can read on [Schemio](https://github.com/CodeShayk/Schemio) for more details on the core functionality. - Please see appendix for schemio implementation in ApiAggregator. diff --git a/src/ApiAggregator/ApiAggregator.Net.csproj b/src/ApiAggregator/ApiAggregator.Net.csproj index 96e0d8d..fa1ca35 100644 --- a/src/ApiAggregator/ApiAggregator.Net.csproj +++ b/src/ApiAggregator/ApiAggregator.Net.csproj @@ -16,21 +16,31 @@ api, aggregator, api-aggregator, utility, api-utility, data-aggregator, api-response, api-response-aggregator True snupkg + LICENSE - + True \ - + + True + \ + + True \ + + + + + diff --git a/src/ApiAggregator/BaseWebQuery.cs b/src/ApiAggregator/BaseWebQuery.cs new file mode 100644 index 0000000..faf6204 --- /dev/null +++ b/src/ApiAggregator/BaseWebQuery.cs @@ -0,0 +1,157 @@ +using Microsoft.Extensions.Logging; +using System.Text.Json; +using Schemio; + +namespace ApiAggregator +{ + /// + /// Implement to create a Web query using api endpoint. + /// + /// Type of Query parameter + /// Type of Query Result + public abstract class BaseWebQuery : BaseQuery, IWebQuery, IRootQuery, IChildQuery + where TParameter : IQueryParameter where TResult : IQueryResult + { + protected BaseWebQuery(string baseAddress) + { + BaseAddress = baseAddress; + Url = GetUrl(QueryParameter); + Headers = GetHeaders(); + } + + /// + /// List of Request headers for the api call. + /// + public List> Headers { get; protected set; } + + /// + /// Base address for the api call. + /// + public string BaseAddress { get; protected set; } + + /// + /// Api endpoint - complete or relative. + /// + public string Url { get; protected set; } + + /// + /// Override to pass custom headers with the api request. + /// + /// + protected virtual List>? GetHeaders() + { return []; } + + /// + /// Implement to construct the api endpoint. + /// + /// Query Parameter + /// + protected abstract string? GetUrl(TParameter queryParameter); + + /// + /// Implement to resolve query parameter. + /// + /// root context. + /// query result from parent query (when configured as nested query). Can be null. + protected abstract void ResolveQueryParameter(IDataContext context, IQueryResult parentQueryResult); + + /// + /// Implement to resolve query parameter for nested queries + /// + /// root context + /// query result from parent query. + public void ResolveChildQueryParameter(IDataContext context, IQueryResult parentQueryResult) + { + ResolveQueryParameter(context, parentQueryResult); + } + + /// + /// Implement to resolve query parameter for first level queries. + /// + /// root context + public void ResolveRootQueryParameter(IDataContext context) + { + ResolveQueryParameter(context, null); + } + + /// + /// Run this web query to get results. + /// + /// HttpClientFactory + /// Logger + /// + /// when httpclientfactory is null. + public virtual async Task Run(IHttpClientFactory httpClientFactory, ILogger logger) + { + if (httpClientFactory == null) + throw new ArgumentNullException("HttpClientFactory is required"); + + var localStorage = new List(); + + logger?.LogInformation($"Run query: {GetType().Name}"); + + using (var client = httpClientFactory.CreateClient()) + { + logger?.LogInformation($"Executing web queries on thread {Thread.CurrentThread.ManagedThreadId} (task {Task.CurrentId})"); + + try + { + HttpResponseMessage result; + + try + { + if (!string.IsNullOrEmpty(BaseAddress)) + client.BaseAddress = new Uri(BaseAddress); + + if (Headers != null && Headers.Any()) + foreach (var header in Headers) + client.DefaultRequestHeaders.Add(header.Key, header.Value); + + result = await client.GetAsync(Url); + + if (!result.IsSuccessStatusCode) + { + logger?.LogInformation($"Result of executing web query {Url} is not success status code"); + } + + var raw = result.Content.ReadAsStringAsync().Result; + + if (string.IsNullOrWhiteSpace(raw)) + logger?.LogInformation($"Result.Content of executing web query {Url} is null or whitespace"); + + if (ResultType.IsArray) + { + var arrObject = JsonSerializer.Deserialize(raw, ResultType); + if (arrObject != null) + localStorage.AddRange((IEnumerable)arrObject); + } + else + { + var obj = JsonSerializer.Deserialize(raw, ResultType); + if (obj != null) + localStorage.Add((TResult)obj); + } + } + catch (TaskCanceledException ex) + { + logger?.LogWarning(ex, $"An error occurred while sending the request. Query URL: {Url}"); + } + catch (HttpRequestException ex) + { + logger?.LogWarning(ex, $"An error occurred while sending the request. Query URL: {Url}"); + } + } + catch (AggregateException ex) + { + logger?.LogInformation($"Web query {GetType().Name} failed"); + foreach (var e in ex.InnerExceptions) + { + logger?.LogError(e, ""); + } + } + } + + return localStorage.Cast().ToArray(); + } + } +} \ No newline at end of file diff --git a/src/ApiAggregator/Class1.cs b/src/ApiAggregator/Class1.cs deleted file mode 100644 index 82aca5f..0000000 --- a/src/ApiAggregator/Class1.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace ApiAggregator -{ - public class Class1 - { - - } -} diff --git a/src/ApiAggregator/ColonSeparatedMatcher.cs b/src/ApiAggregator/ColonSeparatedMatcher.cs new file mode 100644 index 0000000..be4c6ea --- /dev/null +++ b/src/ApiAggregator/ColonSeparatedMatcher.cs @@ -0,0 +1,13 @@ +using Schemio; +using Schemio.Helpers; + +namespace ApiAggregator +{ + public class ColonSeparatedMatcher : ISchemaPathMatcher + { + public bool IsMatch(string inputXPath, ISchemaPaths configuredXPaths) => + // Does the template xpath contain any of the mapping xpaths? + inputXPath.IsNotNullOrEmpty() + && configuredXPaths.Paths.Any(x => inputXPath.ToLower().Contains(x.ToLower())); + } +} \ No newline at end of file diff --git a/src/ApiAggregator/EnumerableExtensions.cs b/src/ApiAggregator/EnumerableExtensions.cs new file mode 100644 index 0000000..fcbaf52 --- /dev/null +++ b/src/ApiAggregator/EnumerableExtensions.cs @@ -0,0 +1,13 @@ +using Schemio; + +namespace ApiAggregator +{ + public static class EnumerableExtensions + { + public static IEnumerable GetByType(this IEnumerable list) where T : class, IQuery + { + var filtered = list.Where(q => (q as T) != null); + return filtered.Cast(); + } + } +} \ No newline at end of file diff --git a/src/ApiAggregator/IWebQuery.cs b/src/ApiAggregator/IWebQuery.cs new file mode 100644 index 0000000..f89d5ae --- /dev/null +++ b/src/ApiAggregator/IWebQuery.cs @@ -0,0 +1,14 @@ +using Microsoft.Extensions.Logging; +using Schemio; + +namespace ApiAggregator +{ + public interface IWebQuery : IQuery + { + List> Headers { get; } + string BaseAddress { get; } + string Url { get; } + + Task Run(IHttpClientFactory httpClientFactory, ILogger logger); + } +} \ No newline at end of file diff --git a/src/ApiAggregator/QueryEngine.cs b/src/ApiAggregator/QueryEngine.cs new file mode 100644 index 0000000..7e0562a --- /dev/null +++ b/src/ApiAggregator/QueryEngine.cs @@ -0,0 +1,47 @@ +using Microsoft.Extensions.Logging; +using Schemio; + +namespace ApiAggregator +{ + public class QueryEngine : IQueryEngine + { + private readonly ILogger logger; + private readonly IHttpClientFactory httpClientFactory; + + public QueryEngine(IHttpClientFactory httpClientFactory, ILogger logger) + { + this.httpClientFactory = httpClientFactory; + this.logger = logger; + } + + public bool CanExecute(IQuery query) => query is IWebQuery; + + public IEnumerable Execute(IEnumerable queries) + { + if (queries == null || !queries.Any()) + return []; + + var webQueries = queries.GetByType(); + + if (!webQueries.Any()) + return []; + + logger.LogInformation($"Total web queries to execute: {webQueries.Count()}"); + + var tasks = webQueries + .Select(q => q.Run(httpClientFactory, logger)) + .ToArray(); + + Task.WhenAll(tasks); + + var result = new List(); + + foreach (var task in tasks) + { + result.AddRange(task.Result); + } + + return result.ToArray(); + } + } +} \ No newline at end of file diff --git a/src/ApiAggregator/ServicesExtensions.cs b/src/ApiAggregator/ServicesExtensions.cs new file mode 100644 index 0000000..27e6c3c --- /dev/null +++ b/src/ApiAggregator/ServicesExtensions.cs @@ -0,0 +1,36 @@ +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Schemio; +using Schemio.Impl; + +namespace ApiAggregator +{ + public static class ServicesExtensions + { + public static IServiceCollection UseApiAggregator(this IServiceCollection services, Func> schemas) + { + services.AddTransient(typeof(IQueryBuilder<>), typeof(QueryBuilder<>)); + services.AddTransient(typeof(ITransformExecutor<>), typeof(TransformExecutor<>)); + services.AddTransient(typeof(IDataProvider<>), typeof(DataProvider<>)); + //services.AddTransient(typeof(IEntitySchema<>), typeof(BaseEntitySchema<>)); + + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + + //services.AddTransient((c) => schema); + + return services; + } + + public static IServiceCollection AddEntitySchema(this IServiceCollection services, IEntitySchema schema) + where TEntity : IEntity + { + if (schema != null) + services.AddTransient(c => (IEntitySchema)schema); + + return services; + } + } +} \ No newline at end of file