Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Use object to not couple to Json and allow output formatters #515

Draft
wants to merge 7 commits into
base: scatter-gather
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 5 additions & 6 deletions docs/scatter-gather.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ public void Configure(IApplicationBuilder app, ILoggerFactory loggerFactory)
app.UseRouting();
app.UseEndpoints(builder => builder.MapScatterGather(template: "api/scatter-gather", new ScatterGatherOptions()
{
Gatherers = new List<Gatherer>
Gatherers = new List<IGatherer>
{
new HttpGatherer(key: "ASamplesSource", destinationUrl: "https://a.web.server/api/samples/ASamplesSource"),
new HttpGatherer(key: "AnotherSamplesSource", destinationUrl: "https://another.web.server/api/samples/AnotherSamplesSource")
Expand All @@ -38,7 +38,7 @@ public void Configure(IApplicationBuilder app, ILoggerFactory loggerFactory)
{
app.UseEndpoints(builder => builder.MapScatterGather(template: "api/scatter-gather", new ScatterGatherOptions()
{
Gatherers = new List<Gatherer>
Gatherers = new List<IGatherer>
{
new HttpGatherer("ASamplesSource", "https://a.web.server/api/samples/ASamplesSource")
{
Expand Down Expand Up @@ -78,7 +78,7 @@ public class CustomHttpGatherer : HttpGatherer
}
}
```
<sup><a href='/src/Snippets/ScatterGather/TransformResponse.cs#L12-L24' title='Snippet source file'>snippet source</a> | <a href='#snippet-scatter-gather-transform-response' title='Start of snippet'>anchor</a></sup>
<sup><a href='/src/Snippets/ScatterGather/TransformResponse.cs#L13-L25' title='Snippet source file'>snippet source</a> | <a href='#snippet-scatter-gather-transform-response' title='Start of snippet'>anchor</a></sup>
<!-- endSnippet -->

### Taking control of the downstream invocation process
Expand All @@ -94,11 +94,10 @@ public class CustomHttpGatherer : HttpGatherer

public override Task<IEnumerable<JsonNode>> Gather(HttpContext context)
{
// by overriding this method we can implement custom logic
// to gather the responses from the downstream service.

return base.Gather(context);
}
}
```
<sup><a href='/src/Snippets/ScatterGather/GatherMethodOverride.cs#L12-L24' title='Snippet source file'>snippet source</a> | <a href='#snippet-scatter-gather-gather-override' title='Start of snippet'>anchor</a></sup>
<sup><a href='/src/Snippets/ScatterGather/GatherMethodOverride.cs#L12-L23' title='Snippet source file'>snippet source</a> | <a href='#snippet-scatter-gather-gather-override' title='Start of snippet'>anchor</a></sup>
<!-- endSnippet -->
Original file line number Diff line number Diff line change
Expand Up @@ -60,13 +60,14 @@ namespace ServiceComposer.AspNetCore
" removed in v3. Use attribute routing based composition, and CompositionEventHan" +
"dler<TEvent>.", true)]
public delegate System.Threading.Tasks.Task EventHandler<TEvent>(string requestId, [System.Runtime.CompilerServices.Dynamic] object viewModel, TEvent @event, Microsoft.AspNetCore.Routing.RouteData routeData, Microsoft.AspNetCore.Http.HttpRequest httpRequest);
public abstract class Gatherer
public abstract class Gatherer<T> : ServiceComposer.AspNetCore.IGatherer
where T : class
{
protected Gatherer(string key) { }
public string Key { get; }
public abstract System.Threading.Tasks.Task<System.Collections.Generic.IEnumerable<System.Text.Json.Nodes.JsonNode>> Gather(Microsoft.AspNetCore.Http.HttpContext context);
public abstract System.Threading.Tasks.Task<System.Collections.Generic.IEnumerable<T>> Gather(Microsoft.AspNetCore.Http.HttpContext context);
}
public class HttpGatherer : ServiceComposer.AspNetCore.Gatherer
public class HttpGatherer : ServiceComposer.AspNetCore.Gatherer<System.Text.Json.Nodes.JsonNode>
{
public HttpGatherer(string key, string destinationUrl) { }
public System.Func<Microsoft.AspNetCore.Http.HttpRequest, string, string> DefaultDestinationUrlMapper { get; }
Expand All @@ -92,8 +93,8 @@ namespace ServiceComposer.AspNetCore
}
public interface IAggregator
{
void Add(System.Collections.Generic.IEnumerable<System.Text.Json.Nodes.JsonNode> nodes);
System.Threading.Tasks.Task<System.Text.Json.Nodes.JsonArray> Aggregate();
void Add(System.Collections.Generic.IEnumerable<object> nodes);
System.Threading.Tasks.Task<object> Aggregate();
}
public interface ICompositionContext
{
Expand All @@ -117,6 +118,11 @@ namespace ServiceComposer.AspNetCore
System.Threading.Tasks.Task Handle(Microsoft.AspNetCore.Http.HttpRequest request);
}
public interface IEndpointScopedViewModelFactory : ServiceComposer.AspNetCore.IViewModelFactory { }
public interface IGatherer
{
string Key { get; }
System.Threading.Tasks.Task<System.Collections.Generic.IEnumerable<object>> Gather(Microsoft.AspNetCore.Http.HttpContext context);
}
[System.Obsolete("IHandleRequests is obsoleted and will be treated as an error starting v2 and remo" +
"ved in v3. Use attribute routing based composition and ICompositionRequestsHandl" +
"er.", true)]
Expand Down Expand Up @@ -188,7 +194,7 @@ namespace ServiceComposer.AspNetCore
{
public ScatterGatherOptions() { }
public System.Type CustomAggregator { get; set; }
public System.Collections.Generic.IList<ServiceComposer.AspNetCore.Gatherer> Gatherers { get; set; }
public System.Collections.Generic.IList<ServiceComposer.AspNetCore.IGatherer> Gatherers { get; set; }
}
public static class ServiceCollectionExtensions
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ HttpClient ClientProvider(string name) =>
// TODO: does this need to register a default HTTP client?
// services.AddScatterGather();
services.AddRouting();
services.AddControllers();
services.Replace(
new ServiceDescriptor(typeof(IHttpClientFactory),
new DelegateHttpClientFactory(ClientProvider)));
Expand All @@ -84,7 +85,7 @@ HttpClient ClientProvider(string name) =>
{
builder.MapScatterGather(template: "/samples", new ScatterGatherOptions
{
Gatherers = new List<Gatherer>
Gatherers = new List<IGatherer>
{
new HttpGatherer(key: "ASamplesSource", destinationUrl: "/samples/ASamplesSource"),
new HttpGatherer(key: "AnotherSamplesSource", destinationUrl: "/samples/AnotherSamplesSource")
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
using System;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.DependencyInjection;
using ServiceComposer.AspNetCore.Testing;
using Xunit;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Text.Json;
using System.Text.Json.Nodes;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection.Extensions;
using ServiceComposer.AspNetCore.Tests.Utils;

namespace ServiceComposer.AspNetCore.Tests.ScatterGather;

public class Get_with_HttpGatherer_and_CustomGatherer
{
class CustomGatherer : IGatherer
{
public string Key { get; } = "CustomGatherer";
public Task<IEnumerable<object>> Gather(HttpContext context)
{
var data = (IEnumerable<object>)(new []{ new { Value = "ACustomSample" } });
return Task.FromResult(data);
}
}

[Fact]
public async Task Returns_expected_response()
{
// Arrange
var aSampleSourceClient = new SelfContainedWebApplicationFactoryWithWebHost<Dummy>
(
configureServices: services =>
{
services.AddRouting();
},
configure: app =>
{
app.UseRouting();
app.UseEndpoints(builder =>
{
builder.MapGet("/samples/ASamplesSource", () =>
{
return new []{ new { Value = "ASample" } };
});
});
}
).CreateClient();

var client = new SelfContainedWebApplicationFactoryWithWebHost<Dummy>
(
configureServices: services =>
{
HttpClient ClientProvider(string name) =>
name switch
{
"ASamplesSource" => aSampleSourceClient,
_ => throw new NotSupportedException($"Missing HTTP client for {name}")
};

// TODO: does this need to register a default HTTP client?
// services.AddScatterGather();
services.AddRouting();
services.AddControllers();
services.Replace(
new ServiceDescriptor(typeof(IHttpClientFactory),
new DelegateHttpClientFactory(ClientProvider)));
},
configure: app =>
{
app.UseRouting();
app.UseEndpoints(builder =>
{
builder.MapScatterGather(template: "/samples", new ScatterGatherOptions
{
Gatherers = new List<IGatherer>
{
new HttpGatherer(key: "ASamplesSource", destinationUrl: "/samples/ASamplesSource"),
new CustomGatherer()
}
});
});
}
).CreateClient();

// Act
var response = await client.GetAsync("/samples");

// Assert
Assert.True(response.IsSuccessStatusCode);

var responseString = await response.Content.ReadAsStringAsync();
var responseArray = JsonNode.Parse(responseString)!.AsArray();
var responseArrayAsJsonStrings = new HashSet<string>(responseArray.Select(n=>n.ToJsonString()));

var expectedArray = JsonNode.Parse(JsonSerializer.Serialize( new[]
{
new {Value = "ASample"},
new {Value = "ACustomSample"}
}, new JsonSerializerOptions
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
}))!.AsArray();
var expectedArrayAsJsonStrings = new HashSet<string>(expectedArray.Select(n=>n.ToJsonString()));

Assert.Equal(2, responseArray.Count);
Assert.Equivalent(expectedArrayAsJsonStrings, responseArrayAsJsonStrings);
}
}

Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ HttpClient ClientProvider(string name) =>
{
builder.MapScatterGather(template: "/samples", new ScatterGatherOptions
{
Gatherers = new List<Gatherer>
Gatherers = new List<IGatherer>
{
new HttpGatherer(key: "ASamplesSource", destinationUrl: "/samples/ASamplesSource"),
new HttpGatherer(key: "AnotherSamplesSource", destinationUrl: "/samples/AnotherSamplesSource")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,17 +9,17 @@ class DefaultAggregator : IAggregator
{
readonly ConcurrentBag<JsonNode> allNodes = new();

public void Add(IEnumerable<JsonNode> nodes)
public void Add(IEnumerable<object> nodes)
{
foreach (var node in nodes)
{
allNodes.Add(node);
allNodes.Add((JsonNode)node);
}
}

public Task<JsonArray> Aggregate()
public Task<object> Aggregate()
{
var responsesArray = new JsonArray(allNodes.ToArray());
return Task.FromResult(responsesArray);
return Task.FromResult((object)responsesArray);
}
}
11 changes: 7 additions & 4 deletions src/ServiceComposer.AspNetCore/ScatterGather/Gatherer.cs
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
using System.Collections.Generic;
using System.Text.Json.Nodes;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;

namespace ServiceComposer.AspNetCore;

public abstract class Gatherer
public abstract class Gatherer<T> : IGatherer where T : class
{
protected Gatherer(string key)
{
Expand All @@ -14,6 +13,10 @@ protected Gatherer(string key)

public string Key { get; }

// TODO: how to use generics to remove the dependency on JSON?
public abstract Task<IEnumerable<JsonNode>> Gather(HttpContext context);
Task<IEnumerable<object>> IGatherer.Gather(HttpContext context)
{
return Gather(context).ContinueWith(t => (IEnumerable<object>)t.Result);
}

public abstract Task<IEnumerable<T>> Gather(HttpContext context);
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@

namespace ServiceComposer.AspNetCore;

public class HttpGatherer : Gatherer
public class HttpGatherer : Gatherer<JsonNode>
{
public HttpGatherer(string key, string destinationUrl)
: base(key)
Expand Down
4 changes: 2 additions & 2 deletions src/ServiceComposer.AspNetCore/ScatterGather/IAggregator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,6 @@ namespace ServiceComposer.AspNetCore;

public interface IAggregator
{
void Add(IEnumerable<JsonNode> nodes);
Task<JsonArray> Aggregate();
void Add(IEnumerable<object> nodes);
Task<object> Aggregate();
}
11 changes: 11 additions & 0 deletions src/ServiceComposer.AspNetCore/ScatterGather/IGatherer.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
using System.Collections.Generic;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;

namespace ServiceComposer.AspNetCore;

public interface IGatherer
{
string Key { get; }
Task<IEnumerable<object>> Gather(HttpContext context);
}
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,8 @@ public static IEndpointConventionBuilder MapScatterGather(this IEndpointRouteBui
await Task.WhenAll(tasks);
var responses = await aggregator.Aggregate();

// TODO: support output formatters by using the WriteModelAsync extension method.
// It must be under a setting flag, because it requires a dependency on MVC.
await context.Response.WriteAsync(responses.ToJsonString());
// TODO: support output formatters by using the WriteModelAsync extension method. It must be under a setting flag, because it requires a dependency on MVC.
await context.Response.WriteAsync( System.Text.Json.JsonSerializer.Serialize(responses));
});
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,9 @@ internal IAggregator GetAggregator(HttpContext httpContext)
{
return (IAggregator)httpContext.RequestServices.GetRequiredService(CustomAggregator);
}

return new DefaultAggregator();
}

public IList<Gatherer> Gatherers { get; set; }
public IList<IGatherer> Gatherers { get; set; }
}
2 changes: 1 addition & 1 deletion src/Snippets/ScatterGather/CustomizingDownstreamURLs.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ public void Configure(IApplicationBuilder app, ILoggerFactory loggerFactory)
{
app.UseEndpoints(builder => builder.MapScatterGather(template: "api/scatter-gather", new ScatterGatherOptions()
{
Gatherers = new List<Gatherer>
Gatherers = new List<IGatherer>
{
new HttpGatherer("ASamplesSource", "https://a.web.server/api/samples/ASamplesSource")
{
Expand Down
3 changes: 1 addition & 2 deletions src/Snippets/ScatterGather/GatherMethodOverride.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,7 @@ public CustomHttpGatherer(string key, string destination) : base(key, destinatio

public override Task<IEnumerable<JsonNode>> Gather(HttpContext context)
{
// by overriding this method we can implement custom logic
// to gather the responses from the downstream service.

return base.Gather(context);
}
}
Expand Down
2 changes: 1 addition & 1 deletion src/Snippets/ScatterGather/Startup.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ public void Configure(IApplicationBuilder app, ILoggerFactory loggerFactory)
app.UseRouting();
app.UseEndpoints(builder => builder.MapScatterGather(template: "api/scatter-gather", new ScatterGatherOptions()
{
Gatherers = new List<Gatherer>
Gatherers = new List<IGatherer>
{
new HttpGatherer(key: "ASamplesSource", destinationUrl: "https://a.web.server/api/samples/ASamplesSource"),
new HttpGatherer(key: "AnotherSamplesSource", destinationUrl: "https://another.web.server/api/samples/AnotherSamplesSource")
Expand Down
1 change: 1 addition & 0 deletions src/Snippets/ScatterGather/TransformResponse.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
using System.Net.Http;
using System.Text.Json.Nodes;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using ServiceComposer.AspNetCore;

namespace Snippets.ScatterGather;
Expand Down