Skip to content

Commit

Permalink
- Checkpoint: 1st draft
Browse files Browse the repository at this point in the history
  • Loading branch information
NinjaRocks committed Oct 31, 2024
1 parent 2da7d85 commit d8d26ff
Show file tree
Hide file tree
Showing 10 changed files with 294 additions and 16 deletions.
8 changes: 2 additions & 6 deletions ApiAggregator.sln
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 0 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.


Expand Down
14 changes: 12 additions & 2 deletions src/ApiAggregator/ApiAggregator.Net.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -16,21 +16,31 @@
<PackageTags>api, aggregator, api-aggregator, utility, api-utility, data-aggregator, api-response, api-response-aggregator</PackageTags>
<IncludeSymbols>True</IncludeSymbols>
<SymbolPackageFormat>snupkg</SymbolPackageFormat>
<PackageLicenseFile>LICENSE</PackageLicenseFile>
</PropertyGroup>

<ItemGroup>
<None Include="..\..\Images\ninja-icon-16.png">
<None Include="..\..\Images\ninja-icon-16.png" Link="misc\ninja-icon-16.png">
<Pack>True</Pack>
<PackagePath>\</PackagePath>
</None>
<None Include="..\..\README.md">
<None Include="..\..\LICENSE" Link="misc\LICENSE">
<Pack>True</Pack>
<PackagePath>\</PackagePath>
</None>
<None Include="..\..\README.md" Link="misc\README.md">
<Pack>True</Pack>
<PackagePath>\</PackagePath>
</None>
</ItemGroup>

<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Http" Version="8.0.1" />
<PackageReference Include="Schemio.Core" Version="1.0.0" />
</ItemGroup>

<ItemGroup>
<Folder Include="misc\" />
</ItemGroup>

</Project>
157 changes: 157 additions & 0 deletions src/ApiAggregator/BaseWebQuery.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
using Microsoft.Extensions.Logging;
using System.Text.Json;
using Schemio;

namespace ApiAggregator
{
/// <summary>
/// Implement to create a Web query using api endpoint.
/// </summary>
/// <typeparam name="TParameter">Type of Query parameter</typeparam>
/// <typeparam name="TResult">Type of Query Result</typeparam>
public abstract class BaseWebQuery<TParameter, TResult> : BaseQuery<TParameter, TResult>, IWebQuery, IRootQuery, IChildQuery
where TParameter : IQueryParameter where TResult : IQueryResult
{
protected BaseWebQuery(string baseAddress)

Check warning on line 15 in src/ApiAggregator/BaseWebQuery.cs

View workflow job for this annotation

GitHub Actions / build

Non-nullable property 'Headers' must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring the property as nullable.

Check warning on line 15 in src/ApiAggregator/BaseWebQuery.cs

View workflow job for this annotation

GitHub Actions / build

Non-nullable property 'Url' must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring the property as nullable.

Check warning on line 15 in src/ApiAggregator/BaseWebQuery.cs

View workflow job for this annotation

GitHub Actions / build

Non-nullable property 'Headers' must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring the property as nullable.

Check warning on line 15 in src/ApiAggregator/BaseWebQuery.cs

View workflow job for this annotation

GitHub Actions / build

Non-nullable property 'Url' must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring the property as nullable.
{
BaseAddress = baseAddress;
Url = GetUrl(QueryParameter);

Check warning on line 18 in src/ApiAggregator/BaseWebQuery.cs

View workflow job for this annotation

GitHub Actions / build

Possible null reference assignment.

Check warning on line 18 in src/ApiAggregator/BaseWebQuery.cs

View workflow job for this annotation

GitHub Actions / build

Possible null reference assignment.
Headers = GetHeaders();

Check warning on line 19 in src/ApiAggregator/BaseWebQuery.cs

View workflow job for this annotation

GitHub Actions / build

Possible null reference assignment.

Check warning on line 19 in src/ApiAggregator/BaseWebQuery.cs

View workflow job for this annotation

GitHub Actions / build

Possible null reference assignment.
}

/// <summary>
/// List of Request headers for the api call.
/// </summary>
public List<KeyValuePair<string, string>> Headers { get; protected set; }

/// <summary>
/// Base address for the api call.
/// </summary>
public string BaseAddress { get; protected set; }

/// <summary>
/// Api endpoint - complete or relative.
/// </summary>
public string Url { get; protected set; }

/// <summary>
/// Override to pass custom headers with the api request.
/// </summary>
/// <returns></returns>
protected virtual List<KeyValuePair<string, string>>? GetHeaders()
{ return []; }

/// <summary>
/// Implement to construct the api endpoint.
/// </summary>
/// <param name="queryParameter">Query Parameter</param>
/// <returns></returns>
protected abstract string? GetUrl(TParameter queryParameter);

/// <summary>
/// Implement to resolve query parameter.
/// </summary>
/// <param name="context">root context.</param>
/// <param name="parentQueryResult">query result from parent query (when configured as nested query). Can be null.</param>
protected abstract void ResolveQueryParameter(IDataContext context, IQueryResult parentQueryResult);

/// <summary>
/// Implement to resolve query parameter for nested queries
/// </summary>
/// <param name="context">root context</param>
/// <param name="parentQueryResult">query result from parent query.</param>
public void ResolveChildQueryParameter(IDataContext context, IQueryResult parentQueryResult)
{
ResolveQueryParameter(context, parentQueryResult);
}

/// <summary>
/// Implement to resolve query parameter for first level queries.
/// </summary>
/// <param name="context">root context</param>
public void ResolveRootQueryParameter(IDataContext context)
{
ResolveQueryParameter(context, null);

Check warning on line 74 in src/ApiAggregator/BaseWebQuery.cs

View workflow job for this annotation

GitHub Actions / build

Cannot convert null literal to non-nullable reference type.
}

/// <summary>
/// Run this web query to get results.
/// </summary>
/// <param name="httpClientFactory">HttpClientFactory</param>
/// <param name="logger">Logger</param>
/// <returns></returns>
/// <exception cref="ArgumentNullException">when httpclientfactory is null.</exception>
public virtual async Task<IQueryResult[]> Run(IHttpClientFactory httpClientFactory, ILogger logger)
{
if (httpClientFactory == null)
throw new ArgumentNullException("HttpClientFactory is required");

var localStorage = new List<TResult>();

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<TResult>)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<IQueryResult>().ToArray();
}
}
}
7 changes: 0 additions & 7 deletions src/ApiAggregator/Class1.cs

This file was deleted.

13 changes: 13 additions & 0 deletions src/ApiAggregator/ColonSeparatedMatcher.cs
Original file line number Diff line number Diff line change
@@ -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()));
}
}
13 changes: 13 additions & 0 deletions src/ApiAggregator/EnumerableExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
using Schemio;

namespace ApiAggregator
{
public static class EnumerableExtensions
{
public static IEnumerable<T> GetByType<T>(this IEnumerable<IQuery> list) where T : class, IQuery
{
var filtered = list.Where(q => (q as T) != null);
return filtered.Cast<T>();
}
}
}
14 changes: 14 additions & 0 deletions src/ApiAggregator/IWebQuery.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
using Microsoft.Extensions.Logging;
using Schemio;

namespace ApiAggregator
{
public interface IWebQuery : IQuery
{
List<KeyValuePair<string, string>> Headers { get; }
string BaseAddress { get; }
string Url { get; }

Task<IQueryResult[]> Run(IHttpClientFactory httpClientFactory, ILogger logger);
}
}
47 changes: 47 additions & 0 deletions src/ApiAggregator/QueryEngine.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
using Microsoft.Extensions.Logging;
using Schemio;

namespace ApiAggregator
{
public class QueryEngine : IQueryEngine
{
private readonly ILogger<QueryEngine> logger;
private readonly IHttpClientFactory httpClientFactory;

public QueryEngine(IHttpClientFactory httpClientFactory, ILogger<QueryEngine> logger)
{
this.httpClientFactory = httpClientFactory;
this.logger = logger;
}

public bool CanExecute(IQuery query) => query is IWebQuery;

public IEnumerable<IQueryResult> Execute(IEnumerable<IQuery> queries)
{
if (queries == null || !queries.Any())
return [];

var webQueries = queries.GetByType<IWebQuery>();

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<IQueryResult>();

foreach (var task in tasks)
{
result.AddRange(task.Result);
}

return result.ToArray();
}
}
}
36 changes: 36 additions & 0 deletions src/ApiAggregator/ServicesExtensions.cs
Original file line number Diff line number Diff line change
@@ -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<IEntity, IEntitySchema<IEntity>> 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<IQueryExecutor, QueryExecutor>();
services.AddTransient<ISchemaPathMatcher, ColonSeparatedMatcher>();
services.AddTransient<IQueryEngine, QueryEngine>();

//services.AddTransient((c) => schema);

return services;
}

public static IServiceCollection AddEntitySchema<TEntity>(this IServiceCollection services, IEntitySchema<IEntity> schema)
where TEntity : IEntity
{
if (schema != null)
services.AddTransient(c => (IEntitySchema<TEntity>)schema);

return services;
}
}
}

0 comments on commit d8d26ff

Please sign in to comment.