Skip to content

Commit d8d26ff

Browse files
committed
- Checkpoint: 1st draft
1 parent 2da7d85 commit d8d26ff

10 files changed

+294
-16
lines changed

ApiAggregator.sln

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,17 +5,13 @@ VisualStudioVersion = 17.9.34723.18
55
MinimumVisualStudioVersion = 10.0.40219.1
66
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{4338FF70-3C81-4370-ACFB-00E14545BA99}"
77
EndProject
8-
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ApiAggregator.Net", "src\ApiAggregator\ApiAggregator.Net.csproj", "{8250784C-5415-47C2-9FE4-9E54FA4672B6}"
8+
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ApiAggregator.Net", "src\ApiAggregator\ApiAggregator.Net.csproj", "{8250784C-5415-47C2-9FE4-9E54FA4672B6}"
99
EndProject
1010
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{31E7A02C-167D-46FB-A90A-F3995FD5682D}"
1111
EndProject
12-
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ApiAggregator.Tests", "tests\ApiAggregator.Tests\ApiAggregator.Tests.csproj", "{C9ED08F3-F754-4D7A-8034-8FC180EA7F7A}"
12+
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ApiAggregator.Tests", "tests\ApiAggregator.Tests\ApiAggregator.Tests.csproj", "{C9ED08F3-F754-4D7A-8034-8FC180EA7F7A}"
1313
EndProject
1414
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "misc", "misc", "{D6340772-5767-4604-9E64-04078C0C2CAC}"
15-
ProjectSection(SolutionItems) = preProject
16-
LICENSE = LICENSE
17-
README.md = README.md
18-
EndProjectSection
1915
EndProject
2016
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "github", "github", "{BCE2D3FE-6CF1-4932-9DEE-5B761A132C5E}"
2117
ProjectSection(SolutionItems) = preProject

README.md

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@
1212
#### Extends `Schemio` for APIs
1313
ApiAggregator uses `Schemio` to extend support for apis to configure hierarchical graph of `query`/`transformer` pairs to return aggregated data in a single response.
1414
> You can read on [Schemio](https://github.com/CodeShayk/Schemio) for more details on the core functionality.
15-
1615
Please see appendix for schemio implementation in ApiAggregator.
1716

1817

src/ApiAggregator/ApiAggregator.Net.csproj

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,21 +16,31 @@
1616
<PackageTags>api, aggregator, api-aggregator, utility, api-utility, data-aggregator, api-response, api-response-aggregator</PackageTags>
1717
<IncludeSymbols>True</IncludeSymbols>
1818
<SymbolPackageFormat>snupkg</SymbolPackageFormat>
19+
<PackageLicenseFile>LICENSE</PackageLicenseFile>
1920
</PropertyGroup>
2021

2122
<ItemGroup>
22-
<None Include="..\..\Images\ninja-icon-16.png">
23+
<None Include="..\..\Images\ninja-icon-16.png" Link="misc\ninja-icon-16.png">
2324
<Pack>True</Pack>
2425
<PackagePath>\</PackagePath>
2526
</None>
26-
<None Include="..\..\README.md">
27+
<None Include="..\..\LICENSE" Link="misc\LICENSE">
28+
<Pack>True</Pack>
29+
<PackagePath>\</PackagePath>
30+
</None>
31+
<None Include="..\..\README.md" Link="misc\README.md">
2732
<Pack>True</Pack>
2833
<PackagePath>\</PackagePath>
2934
</None>
3035
</ItemGroup>
3136

3237
<ItemGroup>
38+
<PackageReference Include="Microsoft.Extensions.Http" Version="8.0.1" />
3339
<PackageReference Include="Schemio.Core" Version="1.0.0" />
3440
</ItemGroup>
3541

42+
<ItemGroup>
43+
<Folder Include="misc\" />
44+
</ItemGroup>
45+
3646
</Project>

src/ApiAggregator/BaseWebQuery.cs

Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
using Microsoft.Extensions.Logging;
2+
using System.Text.Json;
3+
using Schemio;
4+
5+
namespace ApiAggregator
6+
{
7+
/// <summary>
8+
/// Implement to create a Web query using api endpoint.
9+
/// </summary>
10+
/// <typeparam name="TParameter">Type of Query parameter</typeparam>
11+
/// <typeparam name="TResult">Type of Query Result</typeparam>
12+
public abstract class BaseWebQuery<TParameter, TResult> : BaseQuery<TParameter, TResult>, IWebQuery, IRootQuery, IChildQuery
13+
where TParameter : IQueryParameter where TResult : IQueryResult
14+
{
15+
protected BaseWebQuery(string baseAddress)
16+
{
17+
BaseAddress = baseAddress;
18+
Url = GetUrl(QueryParameter);
19+
Headers = GetHeaders();
20+
}
21+
22+
/// <summary>
23+
/// List of Request headers for the api call.
24+
/// </summary>
25+
public List<KeyValuePair<string, string>> Headers { get; protected set; }
26+
27+
/// <summary>
28+
/// Base address for the api call.
29+
/// </summary>
30+
public string BaseAddress { get; protected set; }
31+
32+
/// <summary>
33+
/// Api endpoint - complete or relative.
34+
/// </summary>
35+
public string Url { get; protected set; }
36+
37+
/// <summary>
38+
/// Override to pass custom headers with the api request.
39+
/// </summary>
40+
/// <returns></returns>
41+
protected virtual List<KeyValuePair<string, string>>? GetHeaders()
42+
{ return []; }
43+
44+
/// <summary>
45+
/// Implement to construct the api endpoint.
46+
/// </summary>
47+
/// <param name="queryParameter">Query Parameter</param>
48+
/// <returns></returns>
49+
protected abstract string? GetUrl(TParameter queryParameter);
50+
51+
/// <summary>
52+
/// Implement to resolve query parameter.
53+
/// </summary>
54+
/// <param name="context">root context.</param>
55+
/// <param name="parentQueryResult">query result from parent query (when configured as nested query). Can be null.</param>
56+
protected abstract void ResolveQueryParameter(IDataContext context, IQueryResult parentQueryResult);
57+
58+
/// <summary>
59+
/// Implement to resolve query parameter for nested queries
60+
/// </summary>
61+
/// <param name="context">root context</param>
62+
/// <param name="parentQueryResult">query result from parent query.</param>
63+
public void ResolveChildQueryParameter(IDataContext context, IQueryResult parentQueryResult)
64+
{
65+
ResolveQueryParameter(context, parentQueryResult);
66+
}
67+
68+
/// <summary>
69+
/// Implement to resolve query parameter for first level queries.
70+
/// </summary>
71+
/// <param name="context">root context</param>
72+
public void ResolveRootQueryParameter(IDataContext context)
73+
{
74+
ResolveQueryParameter(context, null);
75+
}
76+
77+
/// <summary>
78+
/// Run this web query to get results.
79+
/// </summary>
80+
/// <param name="httpClientFactory">HttpClientFactory</param>
81+
/// <param name="logger">Logger</param>
82+
/// <returns></returns>
83+
/// <exception cref="ArgumentNullException">when httpclientfactory is null.</exception>
84+
public virtual async Task<IQueryResult[]> Run(IHttpClientFactory httpClientFactory, ILogger logger)
85+
{
86+
if (httpClientFactory == null)
87+
throw new ArgumentNullException("HttpClientFactory is required");
88+
89+
var localStorage = new List<TResult>();
90+
91+
logger?.LogInformation($"Run query: {GetType().Name}");
92+
93+
using (var client = httpClientFactory.CreateClient())
94+
{
95+
logger?.LogInformation($"Executing web queries on thread {Thread.CurrentThread.ManagedThreadId} (task {Task.CurrentId})");
96+
97+
try
98+
{
99+
HttpResponseMessage result;
100+
101+
try
102+
{
103+
if (!string.IsNullOrEmpty(BaseAddress))
104+
client.BaseAddress = new Uri(BaseAddress);
105+
106+
if (Headers != null && Headers.Any())
107+
foreach (var header in Headers)
108+
client.DefaultRequestHeaders.Add(header.Key, header.Value);
109+
110+
result = await client.GetAsync(Url);
111+
112+
if (!result.IsSuccessStatusCode)
113+
{
114+
logger?.LogInformation($"Result of executing web query {Url} is not success status code");
115+
}
116+
117+
var raw = result.Content.ReadAsStringAsync().Result;
118+
119+
if (string.IsNullOrWhiteSpace(raw))
120+
logger?.LogInformation($"Result.Content of executing web query {Url} is null or whitespace");
121+
122+
if (ResultType.IsArray)
123+
{
124+
var arrObject = JsonSerializer.Deserialize(raw, ResultType);
125+
if (arrObject != null)
126+
localStorage.AddRange((IEnumerable<TResult>)arrObject);
127+
}
128+
else
129+
{
130+
var obj = JsonSerializer.Deserialize(raw, ResultType);
131+
if (obj != null)
132+
localStorage.Add((TResult)obj);
133+
}
134+
}
135+
catch (TaskCanceledException ex)
136+
{
137+
logger?.LogWarning(ex, $"An error occurred while sending the request. Query URL: {Url}");
138+
}
139+
catch (HttpRequestException ex)
140+
{
141+
logger?.LogWarning(ex, $"An error occurred while sending the request. Query URL: {Url}");
142+
}
143+
}
144+
catch (AggregateException ex)
145+
{
146+
logger?.LogInformation($"Web query {GetType().Name} failed");
147+
foreach (var e in ex.InnerExceptions)
148+
{
149+
logger?.LogError(e, "");
150+
}
151+
}
152+
}
153+
154+
return localStorage.Cast<IQueryResult>().ToArray();
155+
}
156+
}
157+
}

src/ApiAggregator/Class1.cs

Lines changed: 0 additions & 7 deletions
This file was deleted.
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
using Schemio;
2+
using Schemio.Helpers;
3+
4+
namespace ApiAggregator
5+
{
6+
public class ColonSeparatedMatcher : ISchemaPathMatcher
7+
{
8+
public bool IsMatch(string inputXPath, ISchemaPaths configuredXPaths) =>
9+
// Does the template xpath contain any of the mapping xpaths?
10+
inputXPath.IsNotNullOrEmpty()
11+
&& configuredXPaths.Paths.Any(x => inputXPath.ToLower().Contains(x.ToLower()));
12+
}
13+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
using Schemio;
2+
3+
namespace ApiAggregator
4+
{
5+
public static class EnumerableExtensions
6+
{
7+
public static IEnumerable<T> GetByType<T>(this IEnumerable<IQuery> list) where T : class, IQuery
8+
{
9+
var filtered = list.Where(q => (q as T) != null);
10+
return filtered.Cast<T>();
11+
}
12+
}
13+
}

src/ApiAggregator/IWebQuery.cs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
using Microsoft.Extensions.Logging;
2+
using Schemio;
3+
4+
namespace ApiAggregator
5+
{
6+
public interface IWebQuery : IQuery
7+
{
8+
List<KeyValuePair<string, string>> Headers { get; }
9+
string BaseAddress { get; }
10+
string Url { get; }
11+
12+
Task<IQueryResult[]> Run(IHttpClientFactory httpClientFactory, ILogger logger);
13+
}
14+
}

src/ApiAggregator/QueryEngine.cs

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
using Microsoft.Extensions.Logging;
2+
using Schemio;
3+
4+
namespace ApiAggregator
5+
{
6+
public class QueryEngine : IQueryEngine
7+
{
8+
private readonly ILogger<QueryEngine> logger;
9+
private readonly IHttpClientFactory httpClientFactory;
10+
11+
public QueryEngine(IHttpClientFactory httpClientFactory, ILogger<QueryEngine> logger)
12+
{
13+
this.httpClientFactory = httpClientFactory;
14+
this.logger = logger;
15+
}
16+
17+
public bool CanExecute(IQuery query) => query is IWebQuery;
18+
19+
public IEnumerable<IQueryResult> Execute(IEnumerable<IQuery> queries)
20+
{
21+
if (queries == null || !queries.Any())
22+
return [];
23+
24+
var webQueries = queries.GetByType<IWebQuery>();
25+
26+
if (!webQueries.Any())
27+
return [];
28+
29+
logger.LogInformation($"Total web queries to execute: {webQueries.Count()}");
30+
31+
var tasks = webQueries
32+
.Select(q => q.Run(httpClientFactory, logger))
33+
.ToArray();
34+
35+
Task.WhenAll(tasks);
36+
37+
var result = new List<IQueryResult>();
38+
39+
foreach (var task in tasks)
40+
{
41+
result.AddRange(task.Result);
42+
}
43+
44+
return result.ToArray();
45+
}
46+
}
47+
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
using Microsoft.Extensions.Configuration;
2+
using Microsoft.Extensions.DependencyInjection;
3+
using Microsoft.Extensions.Logging;
4+
using Schemio;
5+
using Schemio.Impl;
6+
7+
namespace ApiAggregator
8+
{
9+
public static class ServicesExtensions
10+
{
11+
public static IServiceCollection UseApiAggregator(this IServiceCollection services, Func<IEntity, IEntitySchema<IEntity>> schemas)
12+
{
13+
services.AddTransient(typeof(IQueryBuilder<>), typeof(QueryBuilder<>));
14+
services.AddTransient(typeof(ITransformExecutor<>), typeof(TransformExecutor<>));
15+
services.AddTransient(typeof(IDataProvider<>), typeof(DataProvider<>));
16+
//services.AddTransient(typeof(IEntitySchema<>), typeof(BaseEntitySchema<>));
17+
18+
services.AddTransient<IQueryExecutor, QueryExecutor>();
19+
services.AddTransient<ISchemaPathMatcher, ColonSeparatedMatcher>();
20+
services.AddTransient<IQueryEngine, QueryEngine>();
21+
22+
//services.AddTransient((c) => schema);
23+
24+
return services;
25+
}
26+
27+
public static IServiceCollection AddEntitySchema<TEntity>(this IServiceCollection services, IEntitySchema<IEntity> schema)
28+
where TEntity : IEntity
29+
{
30+
if (schema != null)
31+
services.AddTransient(c => (IEntitySchema<TEntity>)schema);
32+
33+
return services;
34+
}
35+
}
36+
}

0 commit comments

Comments
 (0)