From 465a5ba6a2dfc73c6232b8537c883fe2c0843bf4 Mon Sep 17 00:00:00 2001 From: Mauro Servienti Date: Sun, 11 Jun 2023 07:17:12 +0200 Subject: [PATCH 01/28] Spike scatter/gather support --- .../ScatterGather/DefaultAggregator.cs | 39 +++++ .../ScatterGather/Gatherer.cs | 7 + .../ScatterGather/Get_with_2_gatherers.cs | 160 ++++++++++++++++++ .../ScatterGather/IAggregator.cs | 11 ++ .../ScatterGatherEndpointBuilderExtensions.cs | 40 +++++ .../ScatterGather/ScatterGatherOptions.cs | 21 +++ .../Utils/DelegateHttpClientFactory.cs | 19 +++ 7 files changed, 297 insertions(+) create mode 100644 src/ServiceComposer.AspNetCore.Tests/ScatterGather/DefaultAggregator.cs create mode 100644 src/ServiceComposer.AspNetCore.Tests/ScatterGather/Gatherer.cs create mode 100644 src/ServiceComposer.AspNetCore.Tests/ScatterGather/Get_with_2_gatherers.cs create mode 100644 src/ServiceComposer.AspNetCore.Tests/ScatterGather/IAggregator.cs create mode 100644 src/ServiceComposer.AspNetCore.Tests/ScatterGather/ScatterGatherEndpointBuilderExtensions.cs create mode 100644 src/ServiceComposer.AspNetCore.Tests/ScatterGather/ScatterGatherOptions.cs create mode 100644 src/ServiceComposer.AspNetCore.Tests/Utils/DelegateHttpClientFactory.cs diff --git a/src/ServiceComposer.AspNetCore.Tests/ScatterGather/DefaultAggregator.cs b/src/ServiceComposer.AspNetCore.Tests/ScatterGather/DefaultAggregator.cs new file mode 100644 index 00000000..3fc74921 --- /dev/null +++ b/src/ServiceComposer.AspNetCore.Tests/ScatterGather/DefaultAggregator.cs @@ -0,0 +1,39 @@ +using System.Collections.Generic; +using System.Net.Http; +using System.Text.Json.Nodes; +using System.Threading.Tasks; + +namespace ServiceComposer.AspNetCore.Tests.ScatterGather; + +class DefaultAggregator : IAggregator +{ + readonly List _responseMessages = new(); + + public void Add(HttpResponseMessage response) + { + _responseMessages.Add(response); + } + + public async Task Aggregate() + { + var responsesArray = new JsonArray(); + foreach (var responseMessage in _responseMessages) + { + var gathererResponsesAsString = await responseMessage.Content.ReadAsStringAsync(); + // default behavior assumes downstream service returns a JSON array + var gathererResponses = JsonNode.Parse(gathererResponsesAsString)?.AsArray(); + if (gathererResponses is { Count: > 0 }) + { + // TODO: this has the side effect of reversing the order of the responses + for (var i = gathererResponses.Count - 1; i >= 0; i--) + { + var nodeAtIndex = gathererResponses[i]; + gathererResponses.Remove(nodeAtIndex); + responsesArray.Add(nodeAtIndex); + } + } + } + + return responsesArray.ToJsonString(); + } +} \ No newline at end of file diff --git a/src/ServiceComposer.AspNetCore.Tests/ScatterGather/Gatherer.cs b/src/ServiceComposer.AspNetCore.Tests/ScatterGather/Gatherer.cs new file mode 100644 index 00000000..b9b113fd --- /dev/null +++ b/src/ServiceComposer.AspNetCore.Tests/ScatterGather/Gatherer.cs @@ -0,0 +1,7 @@ +namespace ServiceComposer.AspNetCore.Tests.ScatterGather; + +public class Gatherer +{ + public string Key { get; set; } + public string Destination { get; set; } +} \ No newline at end of file diff --git a/src/ServiceComposer.AspNetCore.Tests/ScatterGather/Get_with_2_gatherers.cs b/src/ServiceComposer.AspNetCore.Tests/ScatterGather/Get_with_2_gatherers.cs new file mode 100644 index 00000000..6a813a39 --- /dev/null +++ b/src/ServiceComposer.AspNetCore.Tests/ScatterGather/Get_with_2_gatherers.cs @@ -0,0 +1,160 @@ +using System; +using System.Dynamic; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.DependencyInjection; +using Newtonsoft.Json.Linq; +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.Extensions.DependencyInjection.Extensions; +using ServiceComposer.AspNetCore.Tests.Utils; + +namespace ServiceComposer.AspNetCore.Tests.ScatterGather; + +public class Get_with_2_gatherers +{ + // class AGatherer : IGatherRequestsHandler + // { + // [HttpGet("/values")] + // public Task Handle(HttpRequest request) + // { + // var vm = request.GetScatterGatherResponseModel(); + // dynamic item = new ExpandoObject(); + // item.Id = 1; + // vm.Add(item); + // + // return Task.CompletedTask; + // } + // } + // + // class AnotherGatherer : IGatherRequestsHandler + // { + // [HttpGet("/values")] + // public Task Handle(HttpRequest request) + // { + // var vm = request.GetScatterGatherResponseModel(); + // dynamic item = new ExpandoObject(); + // item.Id = 2; + // vm.Add(item); + // + // return Task.CompletedTask; + // } + // } + + [Fact] + public async Task Returns_expected_response() + { + // Arrange + var aSampleSourceClient = new SelfContainedWebApplicationFactoryWithWebHost + ( + configureServices: services => + { + services.AddRouting(); + }, + configure: app => + { + app.UseRouting(); + app.UseEndpoints(builder => + { + builder.MapGet("/samples/ASamplesSource", () => + { + return new []{ new { Value = "ASample" } }; + }); + }); + } + ).CreateClient(); + + var anotherSampleSourceClient = new SelfContainedWebApplicationFactoryWithWebHost + ( + configureServices: services => + { + services.AddRouting(); + }, + configure: app => + { + app.UseRouting(); + app.UseEndpoints(builder => + { + builder.MapGet("/samples/AnotherSamplesSource", () => + { + return new []{ new { Value = "AnotherSample" } }; + }); + }); + } + ).CreateClient(); + + var client = new SelfContainedWebApplicationFactoryWithWebHost + ( + configureServices: services => + { + HttpClient ClientProvider(string name) => + name switch + { + var val when val == "ASamplesSource" => aSampleSourceClient, + var val when val == "AnotherSamplesSource" => anotherSampleSourceClient, + _ => throw new NotSupportedException($"Missing HTTP client for {name}") + }; + + // TODO: does this need to register a default HTTP client? + // services.AddScatterGather(); + services.AddRouting(); + 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 + { + new() + { + Key = "ASamplesSource", + Destination = "/samples/ASamplesSource" + }, + new() + { + Key = "AnotherSamplesSource", + Destination = "/samples/AnotherSamplesSource" + } + } + }); + }); + } + ).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(responseArray.Select(n=>n.ToJsonString())); + + var expectedArray = JsonNode.Parse(JsonSerializer.Serialize( new[] + { + new {Value = "ASample"}, + new {Value = "AnotherSample"} + }, new JsonSerializerOptions + { + PropertyNamingPolicy = JsonNamingPolicy.CamelCase + }))!.AsArray(); + var expectedArrayAsJsonStrings = new HashSet(expectedArray.Select(n=>n.ToJsonString())); + + Assert.Equal(2, responseArray.Count); + Assert.Equivalent(expectedArrayAsJsonStrings, responseArrayAsJsonStrings); + } +} + diff --git a/src/ServiceComposer.AspNetCore.Tests/ScatterGather/IAggregator.cs b/src/ServiceComposer.AspNetCore.Tests/ScatterGather/IAggregator.cs new file mode 100644 index 00000000..bfeb3980 --- /dev/null +++ b/src/ServiceComposer.AspNetCore.Tests/ScatterGather/IAggregator.cs @@ -0,0 +1,11 @@ +using System.Net.Http; +using System.Threading.Tasks; +using Newtonsoft.Json.Linq; + +namespace ServiceComposer.AspNetCore.Tests.ScatterGather; + +public interface IAggregator +{ + void Add(HttpResponseMessage response); + Task Aggregate(); +} \ No newline at end of file diff --git a/src/ServiceComposer.AspNetCore.Tests/ScatterGather/ScatterGatherEndpointBuilderExtensions.cs b/src/ServiceComposer.AspNetCore.Tests/ScatterGather/ScatterGatherEndpointBuilderExtensions.cs new file mode 100644 index 00000000..d2e98750 --- /dev/null +++ b/src/ServiceComposer.AspNetCore.Tests/ScatterGather/ScatterGatherEndpointBuilderExtensions.cs @@ -0,0 +1,40 @@ +using System.Collections.Generic; +using System.Net.Http; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Routing; +using Microsoft.Extensions.DependencyInjection; + +namespace ServiceComposer.AspNetCore.Tests.ScatterGather; + +public static class ScatterGatherEndpointBuilderExtensions +{ + public static void MapScatterGather(this IEndpointRouteBuilder builder, string template, ScatterGatherOptions options) + { + builder.MapGet(template, async context => + { + var aggregator = options.GetAggregator(context); + var factory = ServiceProviderServiceExtensions.GetRequiredService(context.RequestServices); + var tasks = new List(); + foreach (var gatherer in options.Gatherers) + { + var client = factory.CreateClient(gatherer.Key); + var task = client.GetAsync(gatherer.Destination) + .ContinueWith(t => + { + // TODO: how to handle errors? + // t.IsFaulted? + + aggregator.Add(t.Result); + }); + tasks.Add(task); + } + + await Task.WhenAll(tasks); + var responses = await aggregator.Aggregate(); + + await context.Response.WriteAsync(responses); + }); + } +} \ No newline at end of file diff --git a/src/ServiceComposer.AspNetCore.Tests/ScatterGather/ScatterGatherOptions.cs b/src/ServiceComposer.AspNetCore.Tests/ScatterGather/ScatterGatherOptions.cs new file mode 100644 index 00000000..b765f145 --- /dev/null +++ b/src/ServiceComposer.AspNetCore.Tests/ScatterGather/ScatterGatherOptions.cs @@ -0,0 +1,21 @@ +using System; +using System.Collections.Generic; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.DependencyInjection; + +namespace ServiceComposer.AspNetCore.Tests.ScatterGather; + +public class ScatterGatherOptions +{ + public Type CustomAggregator { get; set; } + internal IAggregator GetAggregator(HttpContext httpContext) + { + if(CustomAggregator != null) + { + return (IAggregator)httpContext.RequestServices.GetRequiredService(CustomAggregator); + } + return new DefaultAggregator(); + } + + public IList Gatherers { get; set; } +} \ No newline at end of file diff --git a/src/ServiceComposer.AspNetCore.Tests/Utils/DelegateHttpClientFactory.cs b/src/ServiceComposer.AspNetCore.Tests/Utils/DelegateHttpClientFactory.cs new file mode 100644 index 00000000..ad6ea942 --- /dev/null +++ b/src/ServiceComposer.AspNetCore.Tests/Utils/DelegateHttpClientFactory.cs @@ -0,0 +1,19 @@ +using System; +using System.Net.Http; + +namespace ServiceComposer.AspNetCore.Tests.Utils; + +public class DelegateHttpClientFactory : IHttpClientFactory +{ + private readonly Func _httpClientProvider; + + public DelegateHttpClientFactory(Func httpClientProvider) + { + _httpClientProvider = httpClientProvider; + } + + public HttpClient CreateClient(string name) + { + return _httpClientProvider(name); + } +} \ No newline at end of file From b2b8464a1664775840e283362b7f8b9be8066101 Mon Sep 17 00:00:00 2001 From: Mauro Servienti Date: Sun, 11 Jun 2023 07:21:20 +0200 Subject: [PATCH 02/28] Make properties init only --- .../ScatterGather/Gatherer.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/ServiceComposer.AspNetCore.Tests/ScatterGather/Gatherer.cs b/src/ServiceComposer.AspNetCore.Tests/ScatterGather/Gatherer.cs index b9b113fd..3c6dca68 100644 --- a/src/ServiceComposer.AspNetCore.Tests/ScatterGather/Gatherer.cs +++ b/src/ServiceComposer.AspNetCore.Tests/ScatterGather/Gatherer.cs @@ -2,6 +2,6 @@ namespace ServiceComposer.AspNetCore.Tests.ScatterGather; public class Gatherer { - public string Key { get; set; } - public string Destination { get; set; } + public string Key { get; init; } + public string Destination { get; init; } } \ No newline at end of file From b773ced929f8daafbc12d4ed04d2299a85495b20 Mon Sep 17 00:00:00 2001 From: Mauro Servienti Date: Sun, 11 Jun 2023 07:21:47 +0200 Subject: [PATCH 03/28] Remove not needed using directives --- .../ScatterGather/Get_with_2_gatherers.cs | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/ServiceComposer.AspNetCore.Tests/ScatterGather/Get_with_2_gatherers.cs b/src/ServiceComposer.AspNetCore.Tests/ScatterGather/Get_with_2_gatherers.cs index 6a813a39..8c674ec2 100644 --- a/src/ServiceComposer.AspNetCore.Tests/ScatterGather/Get_with_2_gatherers.cs +++ b/src/ServiceComposer.AspNetCore.Tests/ScatterGather/Get_with_2_gatherers.cs @@ -1,10 +1,7 @@ using System; -using System.Dynamic; using System.Threading.Tasks; using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.DependencyInjection; -using Newtonsoft.Json.Linq; using ServiceComposer.AspNetCore.Testing; using Xunit; using System.Collections.Generic; From ef730ab679a3fea31001254c3b5f4515c1ae4211 Mon Sep 17 00:00:00 2001 From: Mauro Servienti Date: Sun, 11 Jun 2023 07:22:03 +0200 Subject: [PATCH 04/28] Remove commented code --- .../ScatterGather/Get_with_2_gatherers.cs | 28 ------------------- 1 file changed, 28 deletions(-) diff --git a/src/ServiceComposer.AspNetCore.Tests/ScatterGather/Get_with_2_gatherers.cs b/src/ServiceComposer.AspNetCore.Tests/ScatterGather/Get_with_2_gatherers.cs index 8c674ec2..33807771 100644 --- a/src/ServiceComposer.AspNetCore.Tests/ScatterGather/Get_with_2_gatherers.cs +++ b/src/ServiceComposer.AspNetCore.Tests/ScatterGather/Get_with_2_gatherers.cs @@ -16,34 +16,6 @@ namespace ServiceComposer.AspNetCore.Tests.ScatterGather; public class Get_with_2_gatherers { - // class AGatherer : IGatherRequestsHandler - // { - // [HttpGet("/values")] - // public Task Handle(HttpRequest request) - // { - // var vm = request.GetScatterGatherResponseModel(); - // dynamic item = new ExpandoObject(); - // item.Id = 1; - // vm.Add(item); - // - // return Task.CompletedTask; - // } - // } - // - // class AnotherGatherer : IGatherRequestsHandler - // { - // [HttpGet("/values")] - // public Task Handle(HttpRequest request) - // { - // var vm = request.GetScatterGatherResponseModel(); - // dynamic item = new ExpandoObject(); - // item.Id = 2; - // vm.Add(item); - // - // return Task.CompletedTask; - // } - // } - [Fact] public async Task Returns_expected_response() { From 7d0e7b1dfc4dc8f14501c383271a7128b6626385 Mon Sep 17 00:00:00 2001 From: Mauro Servienti Date: Sun, 11 Jun 2023 07:22:21 +0200 Subject: [PATCH 05/28] simplify pattern matching --- .../ScatterGather/Get_with_2_gatherers.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/ServiceComposer.AspNetCore.Tests/ScatterGather/Get_with_2_gatherers.cs b/src/ServiceComposer.AspNetCore.Tests/ScatterGather/Get_with_2_gatherers.cs index 33807771..e540e6e2 100644 --- a/src/ServiceComposer.AspNetCore.Tests/ScatterGather/Get_with_2_gatherers.cs +++ b/src/ServiceComposer.AspNetCore.Tests/ScatterGather/Get_with_2_gatherers.cs @@ -65,8 +65,8 @@ public async Task Returns_expected_response() HttpClient ClientProvider(string name) => name switch { - var val when val == "ASamplesSource" => aSampleSourceClient, - var val when val == "AnotherSamplesSource" => anotherSampleSourceClient, + "ASamplesSource" => aSampleSourceClient, + "AnotherSamplesSource" => anotherSampleSourceClient, _ => throw new NotSupportedException($"Missing HTTP client for {name}") }; From 5ea520a6ed1045f972d917c7f5263ff3e184175e Mon Sep 17 00:00:00 2001 From: Mauro Servienti Date: Sun, 11 Jun 2023 07:25:32 +0200 Subject: [PATCH 06/28] Use extension method --- .../ScatterGather/ScatterGatherEndpointBuilderExtensions.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ServiceComposer.AspNetCore.Tests/ScatterGather/ScatterGatherEndpointBuilderExtensions.cs b/src/ServiceComposer.AspNetCore.Tests/ScatterGather/ScatterGatherEndpointBuilderExtensions.cs index d2e98750..88604be8 100644 --- a/src/ServiceComposer.AspNetCore.Tests/ScatterGather/ScatterGatherEndpointBuilderExtensions.cs +++ b/src/ServiceComposer.AspNetCore.Tests/ScatterGather/ScatterGatherEndpointBuilderExtensions.cs @@ -15,7 +15,7 @@ public static void MapScatterGather(this IEndpointRouteBuilder builder, string t builder.MapGet(template, async context => { var aggregator = options.GetAggregator(context); - var factory = ServiceProviderServiceExtensions.GetRequiredService(context.RequestServices); + var factory = context.RequestServices.GetRequiredService(); var tasks = new List(); foreach (var gatherer in options.Gatherers) { From e313413d823482b27614d9f9ea66ebfc025b0c5f Mon Sep 17 00:00:00 2001 From: Mauro Servienti Date: Sun, 11 Jun 2023 07:53:19 +0200 Subject: [PATCH 07/28] Support customizing the destination url and by default copy over the query string --- .../ScatterGather/Gatherer.cs | 12 ++++++++++++ .../ScatterGatherEndpointBuilderExtensions.cs | 13 ++++++++++++- 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/src/ServiceComposer.AspNetCore.Tests/ScatterGather/Gatherer.cs b/src/ServiceComposer.AspNetCore.Tests/ScatterGather/Gatherer.cs index 3c6dca68..8e2d458f 100644 --- a/src/ServiceComposer.AspNetCore.Tests/ScatterGather/Gatherer.cs +++ b/src/ServiceComposer.AspNetCore.Tests/ScatterGather/Gatherer.cs @@ -1,7 +1,19 @@ +using System; +using Microsoft.AspNetCore.Http; + namespace ServiceComposer.AspNetCore.Tests.ScatterGather; public class Gatherer { + public Gatherer() + { + DestinationUrlMapper = request => request.Query.Count == 0 + ? Destination + : $"{Destination}?{request.QueryString}"; + } + public string Key { get; init; } public string Destination { get; init; } + + public Func DestinationUrlMapper { get; init; } } \ No newline at end of file diff --git a/src/ServiceComposer.AspNetCore.Tests/ScatterGather/ScatterGatherEndpointBuilderExtensions.cs b/src/ServiceComposer.AspNetCore.Tests/ScatterGather/ScatterGatherEndpointBuilderExtensions.cs index 88604be8..20ec11b8 100644 --- a/src/ServiceComposer.AspNetCore.Tests/ScatterGather/ScatterGatherEndpointBuilderExtensions.cs +++ b/src/ServiceComposer.AspNetCore.Tests/ScatterGather/ScatterGatherEndpointBuilderExtensions.cs @@ -20,7 +20,18 @@ public static void MapScatterGather(this IEndpointRouteBuilder builder, string t foreach (var gatherer in options.Gatherers) { var client = factory.CreateClient(gatherer.Key); - var task = client.GetAsync(gatherer.Destination) + + // TODO: template matching? + // e.g., what if Destination is defined as + // /samples/{culture}/ASamplesSource + // and the source template is /samples/{culture} + // or this responsibility could be moved into the gatherer + // and users could pass in a Func to transform the incoming request path + // into the outgoing request path + // e.g. /samples/ASamplesSource -> /samples/ASamplesSource?filter=foo + // or they could define a IDownstreamInvoker that does the entire downstream request + var destination = gatherer.DestinationUrlMapper(context.Request); + var task = client.GetAsync(destination) .ContinueWith(t => { // TODO: how to handle errors? From 8b1f5144212aa674e85e3379816ed2917091f3e1 Mon Sep 17 00:00:00 2001 From: Mauro Servienti Date: Sun, 11 Jun 2023 14:49:35 +0200 Subject: [PATCH 08/28] Introduce DefaultDestinationUrlMapper --- .../ScatterGather/Gatherer.cs | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/src/ServiceComposer.AspNetCore.Tests/ScatterGather/Gatherer.cs b/src/ServiceComposer.AspNetCore.Tests/ScatterGather/Gatherer.cs index 8e2d458f..d1dda3ea 100644 --- a/src/ServiceComposer.AspNetCore.Tests/ScatterGather/Gatherer.cs +++ b/src/ServiceComposer.AspNetCore.Tests/ScatterGather/Gatherer.cs @@ -1,5 +1,8 @@ using System; +using System.Collections.Generic; using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Routing; +using Microsoft.AspNetCore.Routing.Template; namespace ServiceComposer.AspNetCore.Tests.ScatterGather; @@ -7,13 +10,17 @@ public class Gatherer { public Gatherer() { - DestinationUrlMapper = request => request.Query.Count == 0 - ? Destination - : $"{Destination}?{request.QueryString}"; + DefaultDestinationUrlMapper= request => request.Query.Count == 0 + ? Destination + : $"{Destination}{request.QueryString}"; + + DestinationUrlMapper = request => DefaultDestinationUrlMapper(request); } public string Key { get; init; } public string Destination { get; init; } + public Func DefaultDestinationUrlMapper { get; } + public Func DestinationUrlMapper { get; init; } } \ No newline at end of file From 6123ca54a40290e27745a59f94b2c9e7cf7f04a0 Mon Sep 17 00:00:00 2001 From: Mauro Servienti Date: Sun, 11 Jun 2023 14:49:57 +0200 Subject: [PATCH 09/28] Test query string is passed to downstream endpoints --- .../ScatterGather/When_using_query_string.cs | 130 ++++++++++++++++++ 1 file changed, 130 insertions(+) create mode 100644 src/ServiceComposer.AspNetCore.Tests/ScatterGather/When_using_query_string.cs diff --git a/src/ServiceComposer.AspNetCore.Tests/ScatterGather/When_using_query_string.cs b/src/ServiceComposer.AspNetCore.Tests/ScatterGather/When_using_query_string.cs new file mode 100644 index 00000000..4d9773e9 --- /dev/null +++ b/src/ServiceComposer.AspNetCore.Tests/ScatterGather/When_using_query_string.cs @@ -0,0 +1,130 @@ +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.Extensions.DependencyInjection.Extensions; +using ServiceComposer.AspNetCore.Tests.Utils; + +namespace ServiceComposer.AspNetCore.Tests.ScatterGather; + +public class When_using_query_string +{ + [Fact] + public async Task Values_are_propagated_to_downstream_destinations() + { + // Arrange + var aSampleSourceClient = new SelfContainedWebApplicationFactoryWithWebHost + ( + configureServices: services => + { + services.AddRouting(); + }, + configure: app => + { + app.UseRouting(); + app.UseEndpoints(builder => + { + builder.MapGet("/samples/ASamplesSource", (string culture) => + { + return new []{ new { Value = "ASample", Culture = culture } }; + }); + }); + } + ).CreateClient(); + + var anotherSampleSourceClient = new SelfContainedWebApplicationFactoryWithWebHost + ( + configureServices: services => + { + services.AddRouting(); + }, + configure: app => + { + app.UseRouting(); + app.UseEndpoints(builder => + { + builder.MapGet("/samples/AnotherSamplesSource", (string culture) => + { + return new []{ new { Value = "AnotherSample", Culture = culture } }; + }); + }); + } + ).CreateClient(); + + var client = new SelfContainedWebApplicationFactoryWithWebHost + ( + configureServices: services => + { + HttpClient ClientProvider(string name) => + name switch + { + "ASamplesSource" => aSampleSourceClient, + "AnotherSamplesSource" => anotherSampleSourceClient, + _ => throw new NotSupportedException($"Missing HTTP client for {name}") + }; + + // TODO: does this need to register a default HTTP client? + // services.AddScatterGather(); + services.AddRouting(); + 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 + { + new() + { + Key = "ASamplesSource", + Destination = "/samples/ASamplesSource" + }, + new() + { + Key = "AnotherSamplesSource", + Destination = "/samples/AnotherSamplesSource" + } + } + }); + }); + } + ).CreateClient(); + + // Act + var culture = "it-IT"; + var response = await client.GetAsync($"/samples?culture={culture}"); + + // Assert + Assert.True(response.IsSuccessStatusCode); + + var responseString = await response.Content.ReadAsStringAsync(); + var responseArray = JsonNode.Parse(responseString)!.AsArray(); + var responseArrayAsJsonStrings = new HashSet(responseArray.Select(n=>n.ToJsonString())); + + var expectedArray = JsonNode.Parse(JsonSerializer.Serialize( new[] + { + new {Value = "ASample", Culture = culture}, + new {Value = "AnotherSample", Culture = culture} + }, new JsonSerializerOptions + { + PropertyNamingPolicy = JsonNamingPolicy.CamelCase + }))!.AsArray(); + var expectedArrayAsJsonStrings = new HashSet(expectedArray.Select(n=>n.ToJsonString())); + + Assert.Equal(2, responseArray.Count); + Assert.Equivalent(expectedArrayAsJsonStrings, responseArrayAsJsonStrings); + } +} + From bc7a14e25bf0730a30043897c938c9f0eda05f36 Mon Sep 17 00:00:00 2001 From: Mauro Servienti Date: Sun, 11 Jun 2023 14:51:59 +0200 Subject: [PATCH 10/28] Return IEndpointConventionBuilder to match ASP.Net convention --- .../ScatterGather/ScatterGatherEndpointBuilderExtensions.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/ServiceComposer.AspNetCore.Tests/ScatterGather/ScatterGatherEndpointBuilderExtensions.cs b/src/ServiceComposer.AspNetCore.Tests/ScatterGather/ScatterGatherEndpointBuilderExtensions.cs index 20ec11b8..874c96ed 100644 --- a/src/ServiceComposer.AspNetCore.Tests/ScatterGather/ScatterGatherEndpointBuilderExtensions.cs +++ b/src/ServiceComposer.AspNetCore.Tests/ScatterGather/ScatterGatherEndpointBuilderExtensions.cs @@ -10,9 +10,9 @@ namespace ServiceComposer.AspNetCore.Tests.ScatterGather; public static class ScatterGatherEndpointBuilderExtensions { - public static void MapScatterGather(this IEndpointRouteBuilder builder, string template, ScatterGatherOptions options) + public static IEndpointConventionBuilder MapScatterGather(this IEndpointRouteBuilder builder, string template, ScatterGatherOptions options) { - builder.MapGet(template, async context => + return builder.MapGet(template, async context => { var aggregator = options.GetAggregator(context); var factory = context.RequestServices.GetRequiredService(); From dd656419d3f93060cb30637797dafee3cc8d912a Mon Sep 17 00:00:00 2001 From: Mauro Servienti Date: Sun, 11 Jun 2023 14:55:11 +0200 Subject: [PATCH 11/28] Remove TODO --- .../ScatterGatherEndpointBuilderExtensions.cs | 9 --------- 1 file changed, 9 deletions(-) diff --git a/src/ServiceComposer.AspNetCore.Tests/ScatterGather/ScatterGatherEndpointBuilderExtensions.cs b/src/ServiceComposer.AspNetCore.Tests/ScatterGather/ScatterGatherEndpointBuilderExtensions.cs index 874c96ed..555b1d3c 100644 --- a/src/ServiceComposer.AspNetCore.Tests/ScatterGather/ScatterGatherEndpointBuilderExtensions.cs +++ b/src/ServiceComposer.AspNetCore.Tests/ScatterGather/ScatterGatherEndpointBuilderExtensions.cs @@ -21,15 +21,6 @@ public static IEndpointConventionBuilder MapScatterGather(this IEndpointRouteBui { var client = factory.CreateClient(gatherer.Key); - // TODO: template matching? - // e.g., what if Destination is defined as - // /samples/{culture}/ASamplesSource - // and the source template is /samples/{culture} - // or this responsibility could be moved into the gatherer - // and users could pass in a Func to transform the incoming request path - // into the outgoing request path - // e.g. /samples/ASamplesSource -> /samples/ASamplesSource?filter=foo - // or they could define a IDownstreamInvoker that does the entire downstream request var destination = gatherer.DestinationUrlMapper(context.Request); var task = client.GetAsync(destination) .ContinueWith(t => From 80580b56c8439665e96d492dd8bb1ba5dc996764 Mon Sep 17 00:00:00 2001 From: Mauro Servienti Date: Sun, 11 Jun 2023 15:24:13 +0200 Subject: [PATCH 12/28] Move data gathering into gatherers --- .../ScatterGather/DefaultAggregator.cs | 34 +++++------------ .../ScatterGather/Gatherer.cs | 37 ++++++++++++++++++- .../ScatterGather/IAggregator.cs | 8 ++-- .../ScatterGatherEndpointBuilderExtensions.cs | 11 ++---- 4 files changed, 52 insertions(+), 38 deletions(-) diff --git a/src/ServiceComposer.AspNetCore.Tests/ScatterGather/DefaultAggregator.cs b/src/ServiceComposer.AspNetCore.Tests/ScatterGather/DefaultAggregator.cs index 3fc74921..1bdf7a92 100644 --- a/src/ServiceComposer.AspNetCore.Tests/ScatterGather/DefaultAggregator.cs +++ b/src/ServiceComposer.AspNetCore.Tests/ScatterGather/DefaultAggregator.cs @@ -1,5 +1,5 @@ +using System.Collections.Concurrent; using System.Collections.Generic; -using System.Net.Http; using System.Text.Json.Nodes; using System.Threading.Tasks; @@ -7,33 +7,19 @@ namespace ServiceComposer.AspNetCore.Tests.ScatterGather; class DefaultAggregator : IAggregator { - readonly List _responseMessages = new(); + readonly ConcurrentBag allNodes = new(); - public void Add(HttpResponseMessage response) + public void Add(IEnumerable nodes) { - _responseMessages.Add(response); - } - - public async Task Aggregate() - { - var responsesArray = new JsonArray(); - foreach (var responseMessage in _responseMessages) + foreach (var node in nodes) { - var gathererResponsesAsString = await responseMessage.Content.ReadAsStringAsync(); - // default behavior assumes downstream service returns a JSON array - var gathererResponses = JsonNode.Parse(gathererResponsesAsString)?.AsArray(); - if (gathererResponses is { Count: > 0 }) - { - // TODO: this has the side effect of reversing the order of the responses - for (var i = gathererResponses.Count - 1; i >= 0; i--) - { - var nodeAtIndex = gathererResponses[i]; - gathererResponses.Remove(nodeAtIndex); - responsesArray.Add(nodeAtIndex); - } - } + allNodes.Add(node); } + } - return responsesArray.ToJsonString(); + public Task Aggregate() + { + var responsesArray = new JsonArray(allNodes.ToArray()); + return Task.FromResult(responsesArray); } } \ No newline at end of file diff --git a/src/ServiceComposer.AspNetCore.Tests/ScatterGather/Gatherer.cs b/src/ServiceComposer.AspNetCore.Tests/ScatterGather/Gatherer.cs index d1dda3ea..bf08d0df 100644 --- a/src/ServiceComposer.AspNetCore.Tests/ScatterGather/Gatherer.cs +++ b/src/ServiceComposer.AspNetCore.Tests/ScatterGather/Gatherer.cs @@ -1,8 +1,10 @@ using System; using System.Collections.Generic; +using System.Net.Http; +using System.Text.Json.Nodes; +using System.Threading.Tasks; using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Routing; -using Microsoft.AspNetCore.Routing.Template; +using Microsoft.Extensions.DependencyInjection; namespace ServiceComposer.AspNetCore.Tests.ScatterGather; @@ -23,4 +25,35 @@ public Gatherer() public Func DefaultDestinationUrlMapper { get; } public Func DestinationUrlMapper { get; init; } + + protected virtual async Task> TransformResponse(HttpResponseMessage responseMessage) + { + var nodes = new List(); + var gathererResponsesAsString = await responseMessage.Content.ReadAsStringAsync(); + // default behavior assumes downstream service returns a JSON array + var gathererResponses = JsonNode.Parse(gathererResponsesAsString)?.AsArray(); + if (gathererResponses is { Count: > 0 }) + { + // this has the side effect of reversing the order + // of the responses. This is why we reverse below. + for (var i = gathererResponses.Count - 1; i >= 0; i--) + { + var nodeAtIndex = gathererResponses[i]; + gathererResponses.Remove(nodeAtIndex); + nodes.Add(nodeAtIndex); + } + nodes.Reverse(); + } + + return nodes; + } + + public virtual async Task> Gather(HttpContext context) + { + var factory = context.RequestServices.GetRequiredService(); + var client = factory.CreateClient(Key); + var destination = DestinationUrlMapper(context.Request); + var response = await client.GetAsync(destination); + return await TransformResponse(response); + } } \ No newline at end of file diff --git a/src/ServiceComposer.AspNetCore.Tests/ScatterGather/IAggregator.cs b/src/ServiceComposer.AspNetCore.Tests/ScatterGather/IAggregator.cs index bfeb3980..37679bc1 100644 --- a/src/ServiceComposer.AspNetCore.Tests/ScatterGather/IAggregator.cs +++ b/src/ServiceComposer.AspNetCore.Tests/ScatterGather/IAggregator.cs @@ -1,11 +1,11 @@ -using System.Net.Http; +using System.Collections.Generic; +using System.Text.Json.Nodes; using System.Threading.Tasks; -using Newtonsoft.Json.Linq; namespace ServiceComposer.AspNetCore.Tests.ScatterGather; public interface IAggregator { - void Add(HttpResponseMessage response); - Task Aggregate(); + void Add(IEnumerable nodes); + Task Aggregate(); } \ No newline at end of file diff --git a/src/ServiceComposer.AspNetCore.Tests/ScatterGather/ScatterGatherEndpointBuilderExtensions.cs b/src/ServiceComposer.AspNetCore.Tests/ScatterGather/ScatterGatherEndpointBuilderExtensions.cs index 555b1d3c..cdf77f0a 100644 --- a/src/ServiceComposer.AspNetCore.Tests/ScatterGather/ScatterGatherEndpointBuilderExtensions.cs +++ b/src/ServiceComposer.AspNetCore.Tests/ScatterGather/ScatterGatherEndpointBuilderExtensions.cs @@ -1,10 +1,8 @@ using System.Collections.Generic; -using System.Net.Http; using System.Threading.Tasks; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Routing; -using Microsoft.Extensions.DependencyInjection; namespace ServiceComposer.AspNetCore.Tests.ScatterGather; @@ -15,14 +13,11 @@ public static IEndpointConventionBuilder MapScatterGather(this IEndpointRouteBui return builder.MapGet(template, async context => { var aggregator = options.GetAggregator(context); - var factory = context.RequestServices.GetRequiredService(); + var tasks = new List(); foreach (var gatherer in options.Gatherers) { - var client = factory.CreateClient(gatherer.Key); - - var destination = gatherer.DestinationUrlMapper(context.Request); - var task = client.GetAsync(destination) + var task = gatherer.Gather(context) .ContinueWith(t => { // TODO: how to handle errors? @@ -36,7 +31,7 @@ public static IEndpointConventionBuilder MapScatterGather(this IEndpointRouteBui await Task.WhenAll(tasks); var responses = await aggregator.Aggregate(); - await context.Response.WriteAsync(responses); + await context.Response.WriteAsync(responses.ToJsonString()); }); } } \ No newline at end of file From 407af3c9d635fb1281b89fb1b77fa0395bfef479 Mon Sep 17 00:00:00 2001 From: Mauro Servienti Date: Sun, 11 Jun 2023 15:37:46 +0200 Subject: [PATCH 13/28] Move implementation into the main package --- .../API/APIApprovals.Approve_API.verified.txt | 25 +++++++++++++++++++ .../ScatterGather/DefaultAggregator.cs | 2 +- .../ScatterGather/Gatherer.cs | 2 +- .../ScatterGather/IAggregator.cs | 2 +- .../ScatterGatherEndpointBuilderExtensions.cs | 2 +- .../ScatterGather/ScatterGatherOptions.cs | 2 +- ...viceComposer.AspNetCore.csproj.DotSettings | 2 ++ 7 files changed, 32 insertions(+), 5 deletions(-) rename src/{ServiceComposer.AspNetCore.Tests => ServiceComposer.AspNetCore}/ScatterGather/DefaultAggregator.cs (90%) rename src/{ServiceComposer.AspNetCore.Tests => ServiceComposer.AspNetCore}/ScatterGather/Gatherer.cs (97%) rename src/{ServiceComposer.AspNetCore.Tests => ServiceComposer.AspNetCore}/ScatterGather/IAggregator.cs (77%) rename src/{ServiceComposer.AspNetCore.Tests => ServiceComposer.AspNetCore}/ScatterGather/ScatterGatherEndpointBuilderExtensions.cs (95%) rename src/{ServiceComposer.AspNetCore.Tests => ServiceComposer.AspNetCore}/ScatterGather/ScatterGatherOptions.cs (90%) create mode 100644 src/ServiceComposer.AspNetCore/ServiceComposer.AspNetCore.csproj.DotSettings diff --git a/src/ServiceComposer.AspNetCore.Tests/API/APIApprovals.Approve_API.verified.txt b/src/ServiceComposer.AspNetCore.Tests/API/APIApprovals.Approve_API.verified.txt index 526ad6ec..1c6f6edd 100644 --- a/src/ServiceComposer.AspNetCore.Tests/API/APIApprovals.Approve_API.verified.txt +++ b/src/ServiceComposer.AspNetCore.Tests/API/APIApprovals.Approve_API.verified.txt @@ -60,6 +60,16 @@ namespace ServiceComposer.AspNetCore " removed in v3. Use attribute routing based composition, and CompositionEventHan" + "dler.", true)] public delegate System.Threading.Tasks.Task EventHandler(string requestId, [System.Runtime.CompilerServices.Dynamic] object viewModel, TEvent @event, Microsoft.AspNetCore.Routing.RouteData routeData, Microsoft.AspNetCore.Http.HttpRequest httpRequest); + public class Gatherer + { + public Gatherer() { } + public System.Func DefaultDestinationUrlMapper { get; } + public System.Func DestinationUrlMapper { get; init; } + public string Destination { get; init; } + public string Key { get; init; } + public virtual System.Threading.Tasks.Task> Gather(Microsoft.AspNetCore.Http.HttpContext context) { } + protected virtual System.Threading.Tasks.Task> TransformResponse(System.Net.Http.HttpResponseMessage responseMessage) { } + } public static class HttpRequestExtensions { [return: System.Runtime.CompilerServices.Dynamic] @@ -74,6 +84,11 @@ namespace ServiceComposer.AspNetCore public static System.Threading.Tasks.Task Bind(this Microsoft.AspNetCore.Http.HttpRequest request) where T : new() { } } + public interface IAggregator + { + void Add(System.Collections.Generic.IEnumerable nodes); + System.Threading.Tasks.Task Aggregate(); + } public interface ICompositionContext { string RequestId { get; } @@ -159,6 +174,16 @@ namespace ServiceComposer.AspNetCore public bool UseOutputFormatters { get; set; } public void UseCustomJsonSerializerSettings(System.Func jsonSerializerSettingsConfig) { } } + public static class ScatterGatherEndpointBuilderExtensions + { + public static Microsoft.AspNetCore.Builder.IEndpointConventionBuilder MapScatterGather(this Microsoft.AspNetCore.Routing.IEndpointRouteBuilder builder, string template, ServiceComposer.AspNetCore.ScatterGatherOptions options) { } + } + public class ScatterGatherOptions + { + public ScatterGatherOptions() { } + public System.Type CustomAggregator { get; set; } + public System.Collections.Generic.IList Gatherers { get; set; } + } public static class ServiceCollectionExtensions { public static void AddViewModelComposition(this Microsoft.Extensions.DependencyInjection.IServiceCollection services, Microsoft.Extensions.Configuration.IConfiguration configuration = null) { } diff --git a/src/ServiceComposer.AspNetCore.Tests/ScatterGather/DefaultAggregator.cs b/src/ServiceComposer.AspNetCore/ScatterGather/DefaultAggregator.cs similarity index 90% rename from src/ServiceComposer.AspNetCore.Tests/ScatterGather/DefaultAggregator.cs rename to src/ServiceComposer.AspNetCore/ScatterGather/DefaultAggregator.cs index 1bdf7a92..e8e8e368 100644 --- a/src/ServiceComposer.AspNetCore.Tests/ScatterGather/DefaultAggregator.cs +++ b/src/ServiceComposer.AspNetCore/ScatterGather/DefaultAggregator.cs @@ -3,7 +3,7 @@ using System.Text.Json.Nodes; using System.Threading.Tasks; -namespace ServiceComposer.AspNetCore.Tests.ScatterGather; +namespace ServiceComposer.AspNetCore; class DefaultAggregator : IAggregator { diff --git a/src/ServiceComposer.AspNetCore.Tests/ScatterGather/Gatherer.cs b/src/ServiceComposer.AspNetCore/ScatterGather/Gatherer.cs similarity index 97% rename from src/ServiceComposer.AspNetCore.Tests/ScatterGather/Gatherer.cs rename to src/ServiceComposer.AspNetCore/ScatterGather/Gatherer.cs index bf08d0df..c054e7d2 100644 --- a/src/ServiceComposer.AspNetCore.Tests/ScatterGather/Gatherer.cs +++ b/src/ServiceComposer.AspNetCore/ScatterGather/Gatherer.cs @@ -6,7 +6,7 @@ using Microsoft.AspNetCore.Http; using Microsoft.Extensions.DependencyInjection; -namespace ServiceComposer.AspNetCore.Tests.ScatterGather; +namespace ServiceComposer.AspNetCore; public class Gatherer { diff --git a/src/ServiceComposer.AspNetCore.Tests/ScatterGather/IAggregator.cs b/src/ServiceComposer.AspNetCore/ScatterGather/IAggregator.cs similarity index 77% rename from src/ServiceComposer.AspNetCore.Tests/ScatterGather/IAggregator.cs rename to src/ServiceComposer.AspNetCore/ScatterGather/IAggregator.cs index 37679bc1..706b21bd 100644 --- a/src/ServiceComposer.AspNetCore.Tests/ScatterGather/IAggregator.cs +++ b/src/ServiceComposer.AspNetCore/ScatterGather/IAggregator.cs @@ -2,7 +2,7 @@ using System.Text.Json.Nodes; using System.Threading.Tasks; -namespace ServiceComposer.AspNetCore.Tests.ScatterGather; +namespace ServiceComposer.AspNetCore; public interface IAggregator { diff --git a/src/ServiceComposer.AspNetCore.Tests/ScatterGather/ScatterGatherEndpointBuilderExtensions.cs b/src/ServiceComposer.AspNetCore/ScatterGather/ScatterGatherEndpointBuilderExtensions.cs similarity index 95% rename from src/ServiceComposer.AspNetCore.Tests/ScatterGather/ScatterGatherEndpointBuilderExtensions.cs rename to src/ServiceComposer.AspNetCore/ScatterGather/ScatterGatherEndpointBuilderExtensions.cs index cdf77f0a..2558b861 100644 --- a/src/ServiceComposer.AspNetCore.Tests/ScatterGather/ScatterGatherEndpointBuilderExtensions.cs +++ b/src/ServiceComposer.AspNetCore/ScatterGather/ScatterGatherEndpointBuilderExtensions.cs @@ -4,7 +4,7 @@ using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Routing; -namespace ServiceComposer.AspNetCore.Tests.ScatterGather; +namespace ServiceComposer.AspNetCore; public static class ScatterGatherEndpointBuilderExtensions { diff --git a/src/ServiceComposer.AspNetCore.Tests/ScatterGather/ScatterGatherOptions.cs b/src/ServiceComposer.AspNetCore/ScatterGather/ScatterGatherOptions.cs similarity index 90% rename from src/ServiceComposer.AspNetCore.Tests/ScatterGather/ScatterGatherOptions.cs rename to src/ServiceComposer.AspNetCore/ScatterGather/ScatterGatherOptions.cs index b765f145..74d5d4ef 100644 --- a/src/ServiceComposer.AspNetCore.Tests/ScatterGather/ScatterGatherOptions.cs +++ b/src/ServiceComposer.AspNetCore/ScatterGather/ScatterGatherOptions.cs @@ -3,7 +3,7 @@ using Microsoft.AspNetCore.Http; using Microsoft.Extensions.DependencyInjection; -namespace ServiceComposer.AspNetCore.Tests.ScatterGather; +namespace ServiceComposer.AspNetCore; public class ScatterGatherOptions { diff --git a/src/ServiceComposer.AspNetCore/ServiceComposer.AspNetCore.csproj.DotSettings b/src/ServiceComposer.AspNetCore/ServiceComposer.AspNetCore.csproj.DotSettings new file mode 100644 index 00000000..2c9e4ead --- /dev/null +++ b/src/ServiceComposer.AspNetCore/ServiceComposer.AspNetCore.csproj.DotSettings @@ -0,0 +1,2 @@ + + True \ No newline at end of file From 943dc382eca73bc88e09e72bff9ee67955e89303 Mon Sep 17 00:00:00 2001 From: Mauro Servienti Date: Sun, 11 Jun 2023 15:50:40 +0200 Subject: [PATCH 14/28] Make Gatherer API consistent --- .../API/APIApprovals.Approve_API.verified.txt | 1 + .../ScatterGather/Gatherer.cs | 11 ++++++++--- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/src/ServiceComposer.AspNetCore.Tests/API/APIApprovals.Approve_API.verified.txt b/src/ServiceComposer.AspNetCore.Tests/API/APIApprovals.Approve_API.verified.txt index 1c6f6edd..e268a7ac 100644 --- a/src/ServiceComposer.AspNetCore.Tests/API/APIApprovals.Approve_API.verified.txt +++ b/src/ServiceComposer.AspNetCore.Tests/API/APIApprovals.Approve_API.verified.txt @@ -68,6 +68,7 @@ namespace ServiceComposer.AspNetCore public string Destination { get; init; } public string Key { get; init; } public virtual System.Threading.Tasks.Task> Gather(Microsoft.AspNetCore.Http.HttpContext context) { } + protected virtual string MapDestinationUrl(Microsoft.AspNetCore.Http.HttpRequest request) { } protected virtual System.Threading.Tasks.Task> TransformResponse(System.Net.Http.HttpResponseMessage responseMessage) { } } public static class HttpRequestExtensions diff --git a/src/ServiceComposer.AspNetCore/ScatterGather/Gatherer.cs b/src/ServiceComposer.AspNetCore/ScatterGather/Gatherer.cs index c054e7d2..b068553d 100644 --- a/src/ServiceComposer.AspNetCore/ScatterGather/Gatherer.cs +++ b/src/ServiceComposer.AspNetCore/ScatterGather/Gatherer.cs @@ -12,9 +12,7 @@ public class Gatherer { public Gatherer() { - DefaultDestinationUrlMapper= request => request.Query.Count == 0 - ? Destination - : $"{Destination}{request.QueryString}"; + DefaultDestinationUrlMapper = MapDestinationUrl; DestinationUrlMapper = request => DefaultDestinationUrlMapper(request); } @@ -26,6 +24,13 @@ public Gatherer() public Func DestinationUrlMapper { get; init; } + protected virtual string MapDestinationUrl(HttpRequest request) + { + return request.Query.Count == 0 + ? Destination + : $"{Destination}{request.QueryString}"; + } + protected virtual async Task> TransformResponse(HttpResponseMessage responseMessage) { var nodes = new List(); From 800a086ec7e1610b291d890eb23b9d5cbad6757c Mon Sep 17 00:00:00 2001 From: Mauro Servienti Date: Sun, 11 Jun 2023 19:45:16 +0200 Subject: [PATCH 15/28] Fix snippets namespaces --- src/Snippets/ActionResult/UseSetActionResultHandler.cs | 2 +- src/Snippets/BasicUsage/MarketingProductInfo.cs | 2 +- src/Snippets/BasicUsage/SalesProductInfo.cs | 2 +- src/Snippets/BasicUsage/Startup.cs | 2 +- src/Snippets/CompositionOverController.cs | 2 +- src/Snippets/DefaultCasing/Startup.cs | 2 +- src/Snippets/ModelBinding/ConfigureAppForModelBinding.cs | 2 +- src/Snippets/ModelBinding/ModelBindingUsageHandler.cs | 2 +- src/Snippets/ModelBinding/RawBodyUsageHandler.cs | 2 +- src/Snippets/SampleHandler/SampleHandler.cs | 2 +- src/Snippets/Serialization/ResponseSettingsBasedOnCasing.cs | 2 +- src/Snippets/Serialization/Startup.cs | 2 +- src/Snippets/Serialization/UseOutputFormatters.cs | 2 +- src/Snippets/UpgradeGuides/1.x-to-2.0/UpgradeGuide.cs | 2 +- src/Snippets/ViewModelFactory/MarketingProductInfo.cs | 2 +- src/Snippets/ViewModelFactory/ProductViewModel.cs | 2 +- src/Snippets/ViewModelFactory/ProductViewModelFactory.cs | 2 +- src/Snippets/ViewModelFactory/SalesProductInfo.cs | 2 +- src/Snippets/WriteSupport/EnableWriteSupport.cs | 2 +- 19 files changed, 19 insertions(+), 19 deletions(-) diff --git a/src/Snippets/ActionResult/UseSetActionResultHandler.cs b/src/Snippets/ActionResult/UseSetActionResultHandler.cs index 1e2a6b30..f98b1bb4 100644 --- a/src/Snippets/ActionResult/UseSetActionResultHandler.cs +++ b/src/Snippets/ActionResult/UseSetActionResultHandler.cs @@ -5,7 +5,7 @@ using Microsoft.Extensions.DependencyInjection; using ServiceComposer.AspNetCore; -namespace Snippets.NetCore3x.ActionResult +namespace Snippets.ActionResult { // begin-snippet: action-results public class UseSetActionResultHandler : ICompositionRequestsHandler diff --git a/src/Snippets/BasicUsage/MarketingProductInfo.cs b/src/Snippets/BasicUsage/MarketingProductInfo.cs index 274e7d04..bf856b0e 100644 --- a/src/Snippets/BasicUsage/MarketingProductInfo.cs +++ b/src/Snippets/BasicUsage/MarketingProductInfo.cs @@ -3,7 +3,7 @@ using Microsoft.AspNetCore.Mvc; using ServiceComposer.AspNetCore; -namespace Snippets.NetCore3x.BasicUsage +namespace Snippets.BasicUsage { // begin-snippet: basic-usage-marketing-handler public class MarketingProductInfo: ICompositionRequestsHandler diff --git a/src/Snippets/BasicUsage/SalesProductInfo.cs b/src/Snippets/BasicUsage/SalesProductInfo.cs index 3823ba2c..cd2ed270 100644 --- a/src/Snippets/BasicUsage/SalesProductInfo.cs +++ b/src/Snippets/BasicUsage/SalesProductInfo.cs @@ -4,7 +4,7 @@ using Microsoft.AspNetCore.Routing; using ServiceComposer.AspNetCore; -namespace Snippets.NetCore3x.BasicUsage +namespace Snippets.BasicUsage { // begin-snippet: basic-usage-sales-handler public class SalesProductInfo : ICompositionRequestsHandler diff --git a/src/Snippets/BasicUsage/Startup.cs b/src/Snippets/BasicUsage/Startup.cs index c8a88014..448ba367 100644 --- a/src/Snippets/BasicUsage/Startup.cs +++ b/src/Snippets/BasicUsage/Startup.cs @@ -3,7 +3,7 @@ using Microsoft.Extensions.Logging; using ServiceComposer.AspNetCore; -namespace Snippets.NetCore3x.BasicUsage +namespace Snippets.BasicUsage { // begin-snippet: sample-startup public class Startup diff --git a/src/Snippets/CompositionOverController.cs b/src/Snippets/CompositionOverController.cs index 7605df52..417e80b3 100644 --- a/src/Snippets/CompositionOverController.cs +++ b/src/Snippets/CompositionOverController.cs @@ -1,7 +1,7 @@ using Microsoft.Extensions.DependencyInjection; using ServiceComposer.AspNetCore; -namespace Snippets.NetCore3x +namespace Snippets { public class CompositionOverControllers { diff --git a/src/Snippets/DefaultCasing/Startup.cs b/src/Snippets/DefaultCasing/Startup.cs index e0888560..c3808671 100644 --- a/src/Snippets/DefaultCasing/Startup.cs +++ b/src/Snippets/DefaultCasing/Startup.cs @@ -1,7 +1,7 @@ using Microsoft.Extensions.DependencyInjection; using ServiceComposer.AspNetCore; -namespace Snippets.NetCore3x.DefaultCasing +namespace Snippets.DefaultCasing { public class Startup { diff --git a/src/Snippets/ModelBinding/ConfigureAppForModelBinding.cs b/src/Snippets/ModelBinding/ConfigureAppForModelBinding.cs index f8b3d64d..c2b1ae91 100644 --- a/src/Snippets/ModelBinding/ConfigureAppForModelBinding.cs +++ b/src/Snippets/ModelBinding/ConfigureAppForModelBinding.cs @@ -1,7 +1,7 @@ using Microsoft.Extensions.DependencyInjection; using ServiceComposer.AspNetCore; -namespace Snippets.NetCore3x.ModelBinding +namespace Snippets.ModelBinding { public class ConfigureAppForModelBinding { diff --git a/src/Snippets/ModelBinding/ModelBindingUsageHandler.cs b/src/Snippets/ModelBinding/ModelBindingUsageHandler.cs index 4d1a840d..b07f42da 100644 --- a/src/Snippets/ModelBinding/ModelBindingUsageHandler.cs +++ b/src/Snippets/ModelBinding/ModelBindingUsageHandler.cs @@ -3,7 +3,7 @@ using Microsoft.AspNetCore.Mvc; using ServiceComposer.AspNetCore; -namespace Snippets.NetCore3x.ModelBinding +namespace Snippets.ModelBinding { // begin-snippet: model-binding-model class BodyModel diff --git a/src/Snippets/ModelBinding/RawBodyUsageHandler.cs b/src/Snippets/ModelBinding/RawBodyUsageHandler.cs index 10c5a934..0a71561d 100644 --- a/src/Snippets/ModelBinding/RawBodyUsageHandler.cs +++ b/src/Snippets/ModelBinding/RawBodyUsageHandler.cs @@ -7,7 +7,7 @@ using Newtonsoft.Json.Linq; using ServiceComposer.AspNetCore; -namespace Snippets.NetCore3x.ModelBinding +namespace Snippets.ModelBinding { class RawBodyUsageHandler : ICompositionRequestsHandler { diff --git a/src/Snippets/SampleHandler/SampleHandler.cs b/src/Snippets/SampleHandler/SampleHandler.cs index ef6dd2d7..6065043d 100644 --- a/src/Snippets/SampleHandler/SampleHandler.cs +++ b/src/Snippets/SampleHandler/SampleHandler.cs @@ -5,7 +5,7 @@ using Microsoft.AspNetCore.Mvc; using ServiceComposer.AspNetCore; -namespace Snippets.NetCore3x.SampleHandler +namespace Snippets.SampleHandler { // begin-snippet: sample-handler-with-authorization public class SampleHandlerWithAuthorization : ICompositionRequestsHandler diff --git a/src/Snippets/Serialization/ResponseSettingsBasedOnCasing.cs b/src/Snippets/Serialization/ResponseSettingsBasedOnCasing.cs index a0e1104f..edf0e5b5 100644 --- a/src/Snippets/Serialization/ResponseSettingsBasedOnCasing.cs +++ b/src/Snippets/Serialization/ResponseSettingsBasedOnCasing.cs @@ -1,7 +1,7 @@ using Newtonsoft.Json; using Newtonsoft.Json.Serialization; -namespace Snippets.NetCore3x.Serialization +namespace Snippets.Serialization { public class ResponseSettingsBasedOnCasing { diff --git a/src/Snippets/Serialization/Startup.cs b/src/Snippets/Serialization/Startup.cs index 0fe94e83..44bde9ae 100644 --- a/src/Snippets/Serialization/Startup.cs +++ b/src/Snippets/Serialization/Startup.cs @@ -2,7 +2,7 @@ using Newtonsoft.Json; using ServiceComposer.AspNetCore; -namespace Snippets.NetCore3x.Serialization +namespace Snippets.Serialization { public class Startup { diff --git a/src/Snippets/Serialization/UseOutputFormatters.cs b/src/Snippets/Serialization/UseOutputFormatters.cs index a8749975..649c397e 100644 --- a/src/Snippets/Serialization/UseOutputFormatters.cs +++ b/src/Snippets/Serialization/UseOutputFormatters.cs @@ -1,7 +1,7 @@ using Microsoft.Extensions.DependencyInjection; using ServiceComposer.AspNetCore; -namespace Snippets.NetCore3x.Serialization +namespace Snippets.Serialization { public class UseOutputFormatters { diff --git a/src/Snippets/UpgradeGuides/1.x-to-2.0/UpgradeGuide.cs b/src/Snippets/UpgradeGuides/1.x-to-2.0/UpgradeGuide.cs index d990c435..444e9de2 100644 --- a/src/Snippets/UpgradeGuides/1.x-to-2.0/UpgradeGuide.cs +++ b/src/Snippets/UpgradeGuides/1.x-to-2.0/UpgradeGuide.cs @@ -7,7 +7,7 @@ using Microsoft.Extensions.Logging; using ServiceComposer.AspNetCore; -namespace Snippets.NetCore3x.UpgradeGuides._1.x_to_2._0; +namespace Snippets.UpgradeGuides._1.x_to_2._0; public class UpgradeGuide { diff --git a/src/Snippets/ViewModelFactory/MarketingProductInfo.cs b/src/Snippets/ViewModelFactory/MarketingProductInfo.cs index 44eff876..e7076480 100644 --- a/src/Snippets/ViewModelFactory/MarketingProductInfo.cs +++ b/src/Snippets/ViewModelFactory/MarketingProductInfo.cs @@ -3,7 +3,7 @@ using Microsoft.AspNetCore.Mvc; using ServiceComposer.AspNetCore; -namespace Snipptes.ViewModelFactory +namespace Snippets.ViewModelFactory { // begin-snippet: view-model-factory-marketing-handler public class MarketingProductInfo: ICompositionRequestsHandler diff --git a/src/Snippets/ViewModelFactory/ProductViewModel.cs b/src/Snippets/ViewModelFactory/ProductViewModel.cs index e1e44bca..1aa3a24f 100644 --- a/src/Snippets/ViewModelFactory/ProductViewModel.cs +++ b/src/Snippets/ViewModelFactory/ProductViewModel.cs @@ -1,4 +1,4 @@ -namespace Snipptes.ViewModelFactory +namespace Snippets.ViewModelFactory { // begin-snippet: view-model-factory-product-view-model public class ProductViewModel diff --git a/src/Snippets/ViewModelFactory/ProductViewModelFactory.cs b/src/Snippets/ViewModelFactory/ProductViewModelFactory.cs index cd9d6a62..98f8fd07 100644 --- a/src/Snippets/ViewModelFactory/ProductViewModelFactory.cs +++ b/src/Snippets/ViewModelFactory/ProductViewModelFactory.cs @@ -3,7 +3,7 @@ using Microsoft.AspNetCore.Routing; using ServiceComposer.AspNetCore; -namespace Snipptes.ViewModelFactory +namespace Snippets.ViewModelFactory { // begin-snippet: view-model-factory-product-view-model-factory class ProductViewModelFactory : IEndpointScopedViewModelFactory diff --git a/src/Snippets/ViewModelFactory/SalesProductInfo.cs b/src/Snippets/ViewModelFactory/SalesProductInfo.cs index 00a1a3ce..d342e4ca 100644 --- a/src/Snippets/ViewModelFactory/SalesProductInfo.cs +++ b/src/Snippets/ViewModelFactory/SalesProductInfo.cs @@ -3,7 +3,7 @@ using Microsoft.AspNetCore.Mvc; using ServiceComposer.AspNetCore; -namespace Snipptes.ViewModelFactory +namespace Snippets.ViewModelFactory { // begin-snippet: view-model-factory-sales-handler public class SalesProductInfo : ICompositionRequestsHandler diff --git a/src/Snippets/WriteSupport/EnableWriteSupport.cs b/src/Snippets/WriteSupport/EnableWriteSupport.cs index bd49db5c..c752ee96 100644 --- a/src/Snippets/WriteSupport/EnableWriteSupport.cs +++ b/src/Snippets/WriteSupport/EnableWriteSupport.cs @@ -1,7 +1,7 @@ using Microsoft.Extensions.DependencyInjection; using ServiceComposer.AspNetCore; -namespace Snippets.NetCore3x.WriteSupport; +namespace Snippets.WriteSupport; public class EnableWriteSupport { From 2a49a0da293ff3475eff2e56ceb9e15761a0b8f6 Mon Sep 17 00:00:00 2001 From: Mauro Servienti Date: Sun, 11 Jun 2023 19:45:52 +0200 Subject: [PATCH 16/28] snippet: scatter-gather-basic-usage --- src/Snippets/ScatterGather/Startup.cs | 32 +++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) create mode 100644 src/Snippets/ScatterGather/Startup.cs diff --git a/src/Snippets/ScatterGather/Startup.cs b/src/Snippets/ScatterGather/Startup.cs new file mode 100644 index 00000000..09766a24 --- /dev/null +++ b/src/Snippets/ScatterGather/Startup.cs @@ -0,0 +1,32 @@ +using System.Collections.Generic; +using Microsoft.AspNetCore.Builder; +using Microsoft.Extensions.Logging; +using ServiceComposer.AspNetCore; + +namespace Snippets.ScatterGather; + +public class Startup +{ + // begin-snippet: scatter-gather-basic-usage + public void Configure(IApplicationBuilder app, ILoggerFactory loggerFactory) + { + app.UseRouting(); + app.UseEndpoints(builder => builder.MapScatterGather(template: "api/scatter-gather", new ScatterGatherOptions() + { + Gatherers = new List + { + new() + { + Key = "ASamplesSource", + Destination = "https://a.web.server/api/samples/ASamplesSource" + }, + new() + { + Key = "AnotherSamplesSource", + Destination = "https://another.web.server/api/samples/AnotherSamplesSource" + } + } + })); + } + // end-snippet +} \ No newline at end of file From 9ad76398e7be6d088c3568e06a023c95e36c032a Mon Sep 17 00:00:00 2001 From: Mauro Servienti Date: Sun, 11 Jun 2023 20:37:37 +0200 Subject: [PATCH 17/28] More self-explaining gatherers API --- .../API/APIApprovals.Approve_API.verified.txt | 12 ++++----- .../ScatterGather/Get_with_2_gatherers.cs | 12 ++------- .../ScatterGather/When_using_query_string.cs | 12 ++------- .../ScatterGather/Gatherer.cs | 26 ++++++++++--------- 4 files changed, 24 insertions(+), 38 deletions(-) diff --git a/src/ServiceComposer.AspNetCore.Tests/API/APIApprovals.Approve_API.verified.txt b/src/ServiceComposer.AspNetCore.Tests/API/APIApprovals.Approve_API.verified.txt index e268a7ac..92e168c6 100644 --- a/src/ServiceComposer.AspNetCore.Tests/API/APIApprovals.Approve_API.verified.txt +++ b/src/ServiceComposer.AspNetCore.Tests/API/APIApprovals.Approve_API.verified.txt @@ -62,13 +62,13 @@ namespace ServiceComposer.AspNetCore public delegate System.Threading.Tasks.Task EventHandler(string requestId, [System.Runtime.CompilerServices.Dynamic] object viewModel, TEvent @event, Microsoft.AspNetCore.Routing.RouteData routeData, Microsoft.AspNetCore.Http.HttpRequest httpRequest); public class Gatherer { - public Gatherer() { } - public System.Func DefaultDestinationUrlMapper { get; } - public System.Func DestinationUrlMapper { get; init; } - public string Destination { get; init; } - public string Key { get; init; } + public Gatherer(string key, string destination) { } + public System.Func DefaultDestinationUrlMapper { get; } + public string Destination { get; } + public string Key { get; } + public System.Func DestinationUrlMapper { get; init; } public virtual System.Threading.Tasks.Task> Gather(Microsoft.AspNetCore.Http.HttpContext context) { } - protected virtual string MapDestinationUrl(Microsoft.AspNetCore.Http.HttpRequest request) { } + protected virtual string MapDestinationUrl(Microsoft.AspNetCore.Http.HttpRequest request, string destination) { } protected virtual System.Threading.Tasks.Task> TransformResponse(System.Net.Http.HttpResponseMessage responseMessage) { } } public static class HttpRequestExtensions diff --git a/src/ServiceComposer.AspNetCore.Tests/ScatterGather/Get_with_2_gatherers.cs b/src/ServiceComposer.AspNetCore.Tests/ScatterGather/Get_with_2_gatherers.cs index e540e6e2..5d13f5aa 100644 --- a/src/ServiceComposer.AspNetCore.Tests/ScatterGather/Get_with_2_gatherers.cs +++ b/src/ServiceComposer.AspNetCore.Tests/ScatterGather/Get_with_2_gatherers.cs @@ -86,16 +86,8 @@ HttpClient ClientProvider(string name) => { Gatherers = new List { - new() - { - Key = "ASamplesSource", - Destination = "/samples/ASamplesSource" - }, - new() - { - Key = "AnotherSamplesSource", - Destination = "/samples/AnotherSamplesSource" - } + new(key: "ASamplesSource", destination: "/samples/ASamplesSource"), + new(key: "AnotherSamplesSource", destination: "/samples/AnotherSamplesSource") } }); }); diff --git a/src/ServiceComposer.AspNetCore.Tests/ScatterGather/When_using_query_string.cs b/src/ServiceComposer.AspNetCore.Tests/ScatterGather/When_using_query_string.cs index 4d9773e9..7d5f2e5d 100644 --- a/src/ServiceComposer.AspNetCore.Tests/ScatterGather/When_using_query_string.cs +++ b/src/ServiceComposer.AspNetCore.Tests/ScatterGather/When_using_query_string.cs @@ -86,16 +86,8 @@ HttpClient ClientProvider(string name) => { Gatherers = new List { - new() - { - Key = "ASamplesSource", - Destination = "/samples/ASamplesSource" - }, - new() - { - Key = "AnotherSamplesSource", - Destination = "/samples/AnotherSamplesSource" - } + new(key: "ASamplesSource", destination: "/samples/ASamplesSource"), + new(key: "AnotherSamplesSource", destination: "/samples/AnotherSamplesSource") } }); }); diff --git a/src/ServiceComposer.AspNetCore/ScatterGather/Gatherer.cs b/src/ServiceComposer.AspNetCore/ScatterGather/Gatherer.cs index b068553d..cf6bf3f4 100644 --- a/src/ServiceComposer.AspNetCore/ScatterGather/Gatherer.cs +++ b/src/ServiceComposer.AspNetCore/ScatterGather/Gatherer.cs @@ -10,25 +10,27 @@ namespace ServiceComposer.AspNetCore; public class Gatherer { - public Gatherer() + public Gatherer(string key, string destination) { - DefaultDestinationUrlMapper = MapDestinationUrl; + Key = key; + Destination = destination; - DestinationUrlMapper = request => DefaultDestinationUrlMapper(request); + DefaultDestinationUrlMapper = MapDestinationUrl; + DestinationUrlMapper = (request, dest) => DefaultDestinationUrlMapper(request, dest); } + + public string Key { get; } + public string Destination { get; } - public string Key { get; init; } - public string Destination { get; init; } - - public Func DefaultDestinationUrlMapper { get; } + public Func DefaultDestinationUrlMapper { get; } - public Func DestinationUrlMapper { get; init; } + public Func DestinationUrlMapper { get; init; } - protected virtual string MapDestinationUrl(HttpRequest request) + protected virtual string MapDestinationUrl(HttpRequest request, string destination) { return request.Query.Count == 0 - ? Destination - : $"{Destination}{request.QueryString}"; + ? destination + : $"{destination}{request.QueryString}"; } protected virtual async Task> TransformResponse(HttpResponseMessage responseMessage) @@ -57,7 +59,7 @@ public virtual async Task> Gather(HttpContext context) { var factory = context.RequestServices.GetRequiredService(); var client = factory.CreateClient(Key); - var destination = DestinationUrlMapper(context.Request); + var destination = DestinationUrlMapper(context.Request, Destination); var response = await client.GetAsync(destination); return await TransformResponse(response); } From 07bae3d9573258f8b49c19c0571c0cf2e47a0250 Mon Sep 17 00:00:00 2001 From: Mauro Servienti Date: Sun, 11 Jun 2023 20:37:57 +0200 Subject: [PATCH 18/28] Fix basic usage snippet --- src/Snippets/ScatterGather/Startup.cs | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/src/Snippets/ScatterGather/Startup.cs b/src/Snippets/ScatterGather/Startup.cs index 09766a24..261c5d6c 100644 --- a/src/Snippets/ScatterGather/Startup.cs +++ b/src/Snippets/ScatterGather/Startup.cs @@ -15,16 +15,8 @@ public void Configure(IApplicationBuilder app, ILoggerFactory loggerFactory) { Gatherers = new List { - new() - { - Key = "ASamplesSource", - Destination = "https://a.web.server/api/samples/ASamplesSource" - }, - new() - { - Key = "AnotherSamplesSource", - Destination = "https://another.web.server/api/samples/AnotherSamplesSource" - } + new(key: "ASamplesSource", destination: "https://a.web.server/api/samples/ASamplesSource"), + new(key: "AnotherSamplesSource", destination: "https://another.web.server/api/samples/AnotherSamplesSource") } })); } From bd2cb75e2718a94fa5d7572f4d685187b2d50f20 Mon Sep 17 00:00:00 2001 From: Mauro Servienti Date: Sun, 11 Jun 2023 20:39:53 +0200 Subject: [PATCH 19/28] snippet: scatter-gather-customizing-downstream-urls --- .../CustomizingDownstreamURLs.cs | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 src/Snippets/ScatterGather/CustomizingDownstreamURLs.cs diff --git a/src/Snippets/ScatterGather/CustomizingDownstreamURLs.cs b/src/Snippets/ScatterGather/CustomizingDownstreamURLs.cs new file mode 100644 index 00000000..eb05b18b --- /dev/null +++ b/src/Snippets/ScatterGather/CustomizingDownstreamURLs.cs @@ -0,0 +1,27 @@ +using System.Collections.Generic; +using Microsoft.AspNetCore.Builder; +using Microsoft.Extensions.Logging; +using ServiceComposer.AspNetCore; + +namespace Snippets.ScatterGather; + +public class CustomizingDownstreamURLs +{ + // begin-snippet: scatter-gather-customizing-downstream-urls + public void Configure(IApplicationBuilder app, ILoggerFactory loggerFactory) + { + app.UseEndpoints(builder => builder.MapScatterGather(template: "api/scatter-gather", new ScatterGatherOptions() + { + Gatherers = new List + { + new Gatherer("ASamplesSource", "https://a.web.server/api/samples/ASamplesSource") + { + DestinationUrlMapper = (request, destination) => destination.Replace( + "{this-is-contextual}", + request.HttpContext.Request.Query["this-is-contextual"]) + } + } + })); + } + // end-snippet +} \ No newline at end of file From 279516748f71885eb93dbdb7f9b4831e4795be33 Mon Sep 17 00:00:00 2001 From: Mauro Servienti Date: Sun, 11 Jun 2023 20:42:02 +0200 Subject: [PATCH 20/28] Scatter/Gather documentation --- docs/scatter-gather.md | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) create mode 100644 docs/scatter-gather.md diff --git a/docs/scatter-gather.md b/docs/scatter-gather.md new file mode 100644 index 00000000..06d27a91 --- /dev/null +++ b/docs/scatter-gather.md @@ -0,0 +1,31 @@ +# Scatter/Gather + +ServiceCompose natively supports scatter/gather scenarios. Scatter/gather is supported through a fanout approach. Given an incoming HTTP request, ServiceComposer will issue as many downstream HTTP requests to fetch data from downstream endpoints. Once all data has been retrieved, they are composed and returned to the original upstream caller. + +The following configuration configures a scatter/gather endpoint: + +snippet: scatter-gather-basic-usage + +The above configuration snippet configures ServiceComposer to handle HTTP requests matching the template. Each time a matching request is dealt with, ServiceComposer invokes each configured gatherer and merges responses from each one into a response returned to the original issuer. + +The `Key` and `Destination` properties are mandatory. The key uniquely identifies each gatherer in the context of a specific request. The destination is the downstream URL of the endpoint to invoke to retrieve data. + +## Customizing downstream URLs + +If the incoming request contains a query string, the query string and its values are automatically appended to downstream URLs as is. It is possible to override that behavior by setting the `DownstreamUrlMapper` delegate as presented in the following snippet: + +snippet: scatter-gather-customizing-downstream-urls + +The same approach can be used to customize the downstream URL before invocation. + +## Data format + +ServiceComposer scatter/gather support works only with JSON data. Gatherers must return an `IEnumerable`. By default, gatherers assume that the downstream endpoint result can be converted into a `JsonArray`. + +### Transforming returned data + +TODO + +### Taking control of the downstream invocation process + +TODO From b58bf8d53fddeca7f3ddef31a132e33ad5197cae Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Sun, 11 Jun 2023 18:42:41 +0000 Subject: [PATCH 21/28] MarkdownSnippets documentation changes --- docs/scatter-gather.md | 41 +++++++++++++++++++++++++++++++++++++++-- 1 file changed, 39 insertions(+), 2 deletions(-) diff --git a/docs/scatter-gather.md b/docs/scatter-gather.md index 06d27a91..4ba51d23 100644 --- a/docs/scatter-gather.md +++ b/docs/scatter-gather.md @@ -4,7 +4,24 @@ ServiceCompose natively supports scatter/gather scenarios. Scatter/gather is sup The following configuration configures a scatter/gather endpoint: -snippet: scatter-gather-basic-usage + + +```cs +public void Configure(IApplicationBuilder app, ILoggerFactory loggerFactory) +{ + app.UseRouting(); + app.UseEndpoints(builder => builder.MapScatterGather(template: "api/scatter-gather", new ScatterGatherOptions() + { + Gatherers = new List + { + new(key: "ASamplesSource", destination: "https://a.web.server/api/samples/ASamplesSource"), + new(key: "AnotherSamplesSource", destination: "https://another.web.server/api/samples/AnotherSamplesSource") + } + })); +} +``` +snippet source | anchor + The above configuration snippet configures ServiceComposer to handle HTTP requests matching the template. Each time a matching request is dealt with, ServiceComposer invokes each configured gatherer and merges responses from each one into a response returned to the original issuer. @@ -14,7 +31,27 @@ The `Key` and `Destination` properties are mandatory. The key uniquely identifie If the incoming request contains a query string, the query string and its values are automatically appended to downstream URLs as is. It is possible to override that behavior by setting the `DownstreamUrlMapper` delegate as presented in the following snippet: -snippet: scatter-gather-customizing-downstream-urls + + +```cs +public void Configure(IApplicationBuilder app, ILoggerFactory loggerFactory) +{ + app.UseEndpoints(builder => builder.MapScatterGather(template: "api/scatter-gather", new ScatterGatherOptions() + { + Gatherers = new List + { + new Gatherer("ASamplesSource", "https://a.web.server/api/samples/ASamplesSource") + { + DestinationUrlMapper = (request, destination) => destination.Replace( + "{this-is-contextual}", + request.HttpContext.Request.Query["this-is-contextual"]) + } + } + })); +} +``` +snippet source | anchor + The same approach can be used to customize the downstream URL before invocation. From 3e4e25aa48e1b69d1b76406c05c7b3e961ad8cad Mon Sep 17 00:00:00 2001 From: Mauro Servienti Date: Sun, 11 Jun 2023 21:46:35 +0200 Subject: [PATCH 22/28] snippet: scatter-gather-gather-override --- .../ScatterGather/GatherMethodOverride.cs | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 src/Snippets/ScatterGather/GatherMethodOverride.cs diff --git a/src/Snippets/ScatterGather/GatherMethodOverride.cs b/src/Snippets/ScatterGather/GatherMethodOverride.cs new file mode 100644 index 00000000..6d1cb349 --- /dev/null +++ b/src/Snippets/ScatterGather/GatherMethodOverride.cs @@ -0,0 +1,25 @@ +using System.Collections.Generic; +using System.Net.Http; +using System.Text.Json.Nodes; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; +using ServiceComposer.AspNetCore; + +namespace Snippets.ScatterGather; + +public class GatherMethodOverride +{ + // begin-snippet: scatter-gather-gather-override + public class CustomGatherer : Gatherer + { + public CustomGatherer(string key, string destination) : base(key, destination) { } + + public override Task> Gather(HttpContext context) + { + // by overriding this method we can implement custom logic + // to gather the responses from the downstream service. + return base.Gather(context); + } + } + // end-snippet +} \ No newline at end of file From b748502b1710ed61592bfb072cfdca4ad4b837db Mon Sep 17 00:00:00 2001 From: Mauro Servienti Date: Sun, 11 Jun 2023 21:46:47 +0200 Subject: [PATCH 23/28] snippet: scatter-gather-transform-response --- .../ScatterGather/TransformResponse.cs | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) create mode 100644 src/Snippets/ScatterGather/TransformResponse.cs diff --git a/src/Snippets/ScatterGather/TransformResponse.cs b/src/Snippets/ScatterGather/TransformResponse.cs new file mode 100644 index 00000000..eda65af9 --- /dev/null +++ b/src/Snippets/ScatterGather/TransformResponse.cs @@ -0,0 +1,24 @@ +using System.Collections.Generic; +using System.Net.Http; +using System.Text.Json.Nodes; +using System.Threading.Tasks; +using ServiceComposer.AspNetCore; + +namespace Snippets.ScatterGather; + +public class TransformResponseMethodOverride +{ + // begin-snippet: scatter-gather-transform-response + public class CustomGatherer : Gatherer + { + public CustomGatherer(string key, string destination) : base(key, destination) { } + + protected override Task> TransformResponse(HttpResponseMessage responseMessage) + { + // retrieve the response as a string from the HttpResponseMessage + // and parse it as a JsonNode enumerable. + return base.TransformResponse(responseMessage); + } + } + // end-snippet +} \ No newline at end of file From 27c31f5a7ab78b09cbfa30c3638a883352a4027e Mon Sep 17 00:00:00 2001 From: Mauro Servienti Date: Sun, 11 Jun 2023 21:48:47 +0200 Subject: [PATCH 24/28] Complete scatter/gather documentation --- docs/scatter-gather.md | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/docs/scatter-gather.md b/docs/scatter-gather.md index 4ba51d23..b7765a00 100644 --- a/docs/scatter-gather.md +++ b/docs/scatter-gather.md @@ -61,8 +61,12 @@ ServiceComposer scatter/gather support works only with JSON data. Gatherers must ### Transforming returned data -TODO +If there is a need to transform downstream data to respect the expected format, it's possible to create a custom gatherer and override the `TransformResponse` method: + +snippet: scatter-gather-transform-response ### Taking control of the downstream invocation process -TODO +If transforming returned data is not enough, it's possible to take full control over the downstream service invocation process by overriding the `Gather` method: + +snippet: scatter-gather-gather-override From 9bdaf2e383a038d3b7b3d373ddd411ad905e2adb Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Sun, 11 Jun 2023 19:49:02 +0000 Subject: [PATCH 25/28] MarkdownSnippets documentation changes --- docs/scatter-gather.md | 36 ++++++++++++++++++++++++++++++++++-- 1 file changed, 34 insertions(+), 2 deletions(-) diff --git a/docs/scatter-gather.md b/docs/scatter-gather.md index b7765a00..89c5de95 100644 --- a/docs/scatter-gather.md +++ b/docs/scatter-gather.md @@ -63,10 +63,42 @@ ServiceComposer scatter/gather support works only with JSON data. Gatherers must If there is a need to transform downstream data to respect the expected format, it's possible to create a custom gatherer and override the `TransformResponse` method: -snippet: scatter-gather-transform-response + + +```cs +public class CustomGatherer : Gatherer +{ + public CustomGatherer(string key, string destination) : base(key, destination) { } + + protected override Task> TransformResponse(HttpResponseMessage responseMessage) + { + // retrieve the response as a string from the HttpResponseMessage + // and parse it as a JsonNode enumerable. + return base.TransformResponse(responseMessage); + } +} +``` +snippet source | anchor + ### Taking control of the downstream invocation process If transforming returned data is not enough, it's possible to take full control over the downstream service invocation process by overriding the `Gather` method: -snippet: scatter-gather-gather-override + + +```cs +public class CustomGatherer : Gatherer +{ + public CustomGatherer(string key, string destination) : base(key, destination) { } + + public override Task> Gather(HttpContext context) + { + // by overriding this method we can implement custom logic + // to gather the responses from the downstream service. + return base.Gather(context); + } +} +``` +snippet source | anchor + From c1ffe94f820074c97cd0ad05f34ba5e073bb58d1 Mon Sep 17 00:00:00 2001 From: Mauro Servienti Date: Sun, 11 Jun 2023 22:38:51 +0200 Subject: [PATCH 26/28] Make Gatherer abstract and introduce HttpGatherer --- .../API/APIApprovals.Approve_API.verified.txt | 15 +++-- .../ScatterGather/Get_with_2_gatherers.cs | 4 +- .../ScatterGather/When_using_query_string.cs | 4 +- .../ScatterGather/Gatherer.cs | 55 ++-------------- .../ScatterGather/HttpGatherer.cs | 66 +++++++++++++++++++ .../CustomizingDownstreamURLs.cs | 2 +- .../ScatterGather/GatherMethodOverride.cs | 4 +- src/Snippets/ScatterGather/Startup.cs | 5 +- .../ScatterGather/TransformResponse.cs | 5 +- 9 files changed, 93 insertions(+), 67 deletions(-) create mode 100644 src/ServiceComposer.AspNetCore/ScatterGather/HttpGatherer.cs diff --git a/src/ServiceComposer.AspNetCore.Tests/API/APIApprovals.Approve_API.verified.txt b/src/ServiceComposer.AspNetCore.Tests/API/APIApprovals.Approve_API.verified.txt index 92e168c6..6488bb5d 100644 --- a/src/ServiceComposer.AspNetCore.Tests/API/APIApprovals.Approve_API.verified.txt +++ b/src/ServiceComposer.AspNetCore.Tests/API/APIApprovals.Approve_API.verified.txt @@ -60,14 +60,19 @@ namespace ServiceComposer.AspNetCore " removed in v3. Use attribute routing based composition, and CompositionEventHan" + "dler.", true)] public delegate System.Threading.Tasks.Task EventHandler(string requestId, [System.Runtime.CompilerServices.Dynamic] object viewModel, TEvent @event, Microsoft.AspNetCore.Routing.RouteData routeData, Microsoft.AspNetCore.Http.HttpRequest httpRequest); - public class Gatherer + public abstract class Gatherer { - public Gatherer(string key, string destination) { } - public System.Func DefaultDestinationUrlMapper { get; } - public string Destination { get; } + protected Gatherer(string key) { } public string Key { get; } + public abstract System.Threading.Tasks.Task> Gather(Microsoft.AspNetCore.Http.HttpContext context); + } + public class HttpGatherer : ServiceComposer.AspNetCore.Gatherer + { + public HttpGatherer(string key, string destinationUrl) { } + public System.Func DefaultDestinationUrlMapper { get; } + public string DestinationUrl { get; } public System.Func DestinationUrlMapper { get; init; } - public virtual System.Threading.Tasks.Task> Gather(Microsoft.AspNetCore.Http.HttpContext context) { } + public override System.Threading.Tasks.Task> Gather(Microsoft.AspNetCore.Http.HttpContext context) { } protected virtual string MapDestinationUrl(Microsoft.AspNetCore.Http.HttpRequest request, string destination) { } protected virtual System.Threading.Tasks.Task> TransformResponse(System.Net.Http.HttpResponseMessage responseMessage) { } } diff --git a/src/ServiceComposer.AspNetCore.Tests/ScatterGather/Get_with_2_gatherers.cs b/src/ServiceComposer.AspNetCore.Tests/ScatterGather/Get_with_2_gatherers.cs index 5d13f5aa..5a09bb9f 100644 --- a/src/ServiceComposer.AspNetCore.Tests/ScatterGather/Get_with_2_gatherers.cs +++ b/src/ServiceComposer.AspNetCore.Tests/ScatterGather/Get_with_2_gatherers.cs @@ -86,8 +86,8 @@ HttpClient ClientProvider(string name) => { Gatherers = new List { - new(key: "ASamplesSource", destination: "/samples/ASamplesSource"), - new(key: "AnotherSamplesSource", destination: "/samples/AnotherSamplesSource") + new HttpGatherer(key: "ASamplesSource", destinationUrl: "/samples/ASamplesSource"), + new HttpGatherer(key: "AnotherSamplesSource", destinationUrl: "/samples/AnotherSamplesSource") } }); }); diff --git a/src/ServiceComposer.AspNetCore.Tests/ScatterGather/When_using_query_string.cs b/src/ServiceComposer.AspNetCore.Tests/ScatterGather/When_using_query_string.cs index 7d5f2e5d..080ade00 100644 --- a/src/ServiceComposer.AspNetCore.Tests/ScatterGather/When_using_query_string.cs +++ b/src/ServiceComposer.AspNetCore.Tests/ScatterGather/When_using_query_string.cs @@ -86,8 +86,8 @@ HttpClient ClientProvider(string name) => { Gatherers = new List { - new(key: "ASamplesSource", destination: "/samples/ASamplesSource"), - new(key: "AnotherSamplesSource", destination: "/samples/AnotherSamplesSource") + new HttpGatherer(key: "ASamplesSource", destinationUrl: "/samples/ASamplesSource"), + new HttpGatherer(key: "AnotherSamplesSource", destinationUrl: "/samples/AnotherSamplesSource") } }); }); diff --git a/src/ServiceComposer.AspNetCore/ScatterGather/Gatherer.cs b/src/ServiceComposer.AspNetCore/ScatterGather/Gatherer.cs index cf6bf3f4..40690125 100644 --- a/src/ServiceComposer.AspNetCore/ScatterGather/Gatherer.cs +++ b/src/ServiceComposer.AspNetCore/ScatterGather/Gatherer.cs @@ -1,66 +1,19 @@ -using System; using System.Collections.Generic; -using System.Net.Http; using System.Text.Json.Nodes; using System.Threading.Tasks; using Microsoft.AspNetCore.Http; -using Microsoft.Extensions.DependencyInjection; namespace ServiceComposer.AspNetCore; -public class Gatherer +public abstract class Gatherer { - public Gatherer(string key, string destination) + protected Gatherer(string key) { Key = key; - Destination = destination; - - DefaultDestinationUrlMapper = MapDestinationUrl; - DestinationUrlMapper = (request, dest) => DefaultDestinationUrlMapper(request, dest); } public string Key { get; } - public string Destination { get; } - - public Func DefaultDestinationUrlMapper { get; } - - public Func DestinationUrlMapper { get; init; } - protected virtual string MapDestinationUrl(HttpRequest request, string destination) - { - return request.Query.Count == 0 - ? destination - : $"{destination}{request.QueryString}"; - } - - protected virtual async Task> TransformResponse(HttpResponseMessage responseMessage) - { - var nodes = new List(); - var gathererResponsesAsString = await responseMessage.Content.ReadAsStringAsync(); - // default behavior assumes downstream service returns a JSON array - var gathererResponses = JsonNode.Parse(gathererResponsesAsString)?.AsArray(); - if (gathererResponses is { Count: > 0 }) - { - // this has the side effect of reversing the order - // of the responses. This is why we reverse below. - for (var i = gathererResponses.Count - 1; i >= 0; i--) - { - var nodeAtIndex = gathererResponses[i]; - gathererResponses.Remove(nodeAtIndex); - nodes.Add(nodeAtIndex); - } - nodes.Reverse(); - } - - return nodes; - } - - public virtual async Task> Gather(HttpContext context) - { - var factory = context.RequestServices.GetRequiredService(); - var client = factory.CreateClient(Key); - var destination = DestinationUrlMapper(context.Request, Destination); - var response = await client.GetAsync(destination); - return await TransformResponse(response); - } + // TODO: how to use generics to remove the dependency on JSON? + public abstract Task> Gather(HttpContext context); } \ No newline at end of file diff --git a/src/ServiceComposer.AspNetCore/ScatterGather/HttpGatherer.cs b/src/ServiceComposer.AspNetCore/ScatterGather/HttpGatherer.cs new file mode 100644 index 00000000..ed660c13 --- /dev/null +++ b/src/ServiceComposer.AspNetCore/ScatterGather/HttpGatherer.cs @@ -0,0 +1,66 @@ +using System; +using System.Collections.Generic; +using System.Net.Http; +using System.Text.Json.Nodes; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.DependencyInjection; + +namespace ServiceComposer.AspNetCore; + +public class HttpGatherer : Gatherer +{ + public HttpGatherer(string key, string destinationUrl) + : base(key) + { + DestinationUrl = destinationUrl; + + DefaultDestinationUrlMapper = MapDestinationUrl; + DestinationUrlMapper = (request, destination) => DefaultDestinationUrlMapper(request, destination); + } + + public string DestinationUrl { get; } + + public Func DefaultDestinationUrlMapper { get; } + + public Func DestinationUrlMapper { get; init; } + + protected virtual string MapDestinationUrl(HttpRequest request, string destination) + { + return request.Query.Count == 0 + ? destination + : $"{destination}{request.QueryString}"; + } + + protected virtual async Task> TransformResponse(HttpResponseMessage responseMessage) + { + var nodes = new List(); + var gathererResponsesAsString = await responseMessage.Content.ReadAsStringAsync(); + // default behavior assumes downstream service returns a JSON array + var gathererResponses = JsonNode.Parse(gathererResponsesAsString)?.AsArray(); + if (gathererResponses is { Count: > 0 }) + { + // this has the side effect of reversing the order + // of the responses. This is why we reverse below. + for (var i = gathererResponses.Count - 1; i >= 0; i--) + { + var nodeAtIndex = gathererResponses[i]; + gathererResponses.Remove(nodeAtIndex); + nodes.Add(nodeAtIndex); + } + + nodes.Reverse(); + } + + return nodes; + } + + public override async Task> Gather(HttpContext context) + { + var factory = context.RequestServices.GetRequiredService(); + var client = factory.CreateClient(Key); + var destination = DestinationUrlMapper(context.Request, DestinationUrl); + var response = await client.GetAsync(destination); + return await TransformResponse(response); + } +} \ No newline at end of file diff --git a/src/Snippets/ScatterGather/CustomizingDownstreamURLs.cs b/src/Snippets/ScatterGather/CustomizingDownstreamURLs.cs index eb05b18b..e6303e6a 100644 --- a/src/Snippets/ScatterGather/CustomizingDownstreamURLs.cs +++ b/src/Snippets/ScatterGather/CustomizingDownstreamURLs.cs @@ -14,7 +14,7 @@ public void Configure(IApplicationBuilder app, ILoggerFactory loggerFactory) { Gatherers = new List { - new Gatherer("ASamplesSource", "https://a.web.server/api/samples/ASamplesSource") + new HttpGatherer("ASamplesSource", "https://a.web.server/api/samples/ASamplesSource") { DestinationUrlMapper = (request, destination) => destination.Replace( "{this-is-contextual}", diff --git a/src/Snippets/ScatterGather/GatherMethodOverride.cs b/src/Snippets/ScatterGather/GatherMethodOverride.cs index 6d1cb349..251c33f0 100644 --- a/src/Snippets/ScatterGather/GatherMethodOverride.cs +++ b/src/Snippets/ScatterGather/GatherMethodOverride.cs @@ -10,9 +10,9 @@ namespace Snippets.ScatterGather; public class GatherMethodOverride { // begin-snippet: scatter-gather-gather-override - public class CustomGatherer : Gatherer + public class CustomHttpGatherer : HttpGatherer { - public CustomGatherer(string key, string destination) : base(key, destination) { } + public CustomHttpGatherer(string key, string destination) : base(key, destination) { } public override Task> Gather(HttpContext context) { diff --git a/src/Snippets/ScatterGather/Startup.cs b/src/Snippets/ScatterGather/Startup.cs index 261c5d6c..0434c83f 100644 --- a/src/Snippets/ScatterGather/Startup.cs +++ b/src/Snippets/ScatterGather/Startup.cs @@ -1,3 +1,4 @@ +using System; using System.Collections.Generic; using Microsoft.AspNetCore.Builder; using Microsoft.Extensions.Logging; @@ -15,8 +16,8 @@ public void Configure(IApplicationBuilder app, ILoggerFactory loggerFactory) { Gatherers = new List { - new(key: "ASamplesSource", destination: "https://a.web.server/api/samples/ASamplesSource"), - new(key: "AnotherSamplesSource", destination: "https://another.web.server/api/samples/AnotherSamplesSource") + new HttpGatherer(key: "ASamplesSource", destinationUrl: "https://a.web.server/api/samples/ASamplesSource"), + new HttpGatherer(key: "AnotherSamplesSource", destinationUrl: "https://another.web.server/api/samples/AnotherSamplesSource") } })); } diff --git a/src/Snippets/ScatterGather/TransformResponse.cs b/src/Snippets/ScatterGather/TransformResponse.cs index eda65af9..49b7f3de 100644 --- a/src/Snippets/ScatterGather/TransformResponse.cs +++ b/src/Snippets/ScatterGather/TransformResponse.cs @@ -1,3 +1,4 @@ +using System; using System.Collections.Generic; using System.Net.Http; using System.Text.Json.Nodes; @@ -9,9 +10,9 @@ namespace Snippets.ScatterGather; public class TransformResponseMethodOverride { // begin-snippet: scatter-gather-transform-response - public class CustomGatherer : Gatherer + public class CustomHttpGatherer : HttpGatherer { - public CustomGatherer(string key, string destination) : base(key, destination) { } + public CustomHttpGatherer(string key, string destination) : base(key, destination) { } protected override Task> TransformResponse(HttpResponseMessage responseMessage) { From d06a8d2b71a3e29986fc70c00f7235855199f74d Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Sun, 11 Jun 2023 20:39:16 +0000 Subject: [PATCH 27/28] MarkdownSnippets documentation changes --- docs/scatter-gather.md | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/docs/scatter-gather.md b/docs/scatter-gather.md index 89c5de95..516d4d3e 100644 --- a/docs/scatter-gather.md +++ b/docs/scatter-gather.md @@ -14,13 +14,13 @@ public void Configure(IApplicationBuilder app, ILoggerFactory loggerFactory) { Gatherers = new List { - new(key: "ASamplesSource", destination: "https://a.web.server/api/samples/ASamplesSource"), - new(key: "AnotherSamplesSource", destination: "https://another.web.server/api/samples/AnotherSamplesSource") + new HttpGatherer(key: "ASamplesSource", destinationUrl: "https://a.web.server/api/samples/ASamplesSource"), + new HttpGatherer(key: "AnotherSamplesSource", destinationUrl: "https://another.web.server/api/samples/AnotherSamplesSource") } })); } ``` -snippet source | anchor +snippet source | anchor The above configuration snippet configures ServiceComposer to handle HTTP requests matching the template. Each time a matching request is dealt with, ServiceComposer invokes each configured gatherer and merges responses from each one into a response returned to the original issuer. @@ -40,7 +40,7 @@ public void Configure(IApplicationBuilder app, ILoggerFactory loggerFactory) { Gatherers = new List { - new Gatherer("ASamplesSource", "https://a.web.server/api/samples/ASamplesSource") + new HttpGatherer("ASamplesSource", "https://a.web.server/api/samples/ASamplesSource") { DestinationUrlMapper = (request, destination) => destination.Replace( "{this-is-contextual}", @@ -66,9 +66,9 @@ If there is a need to transform downstream data to respect the expected format, ```cs -public class CustomGatherer : Gatherer +public class CustomHttpGatherer : HttpGatherer { - public CustomGatherer(string key, string destination) : base(key, destination) { } + public CustomHttpGatherer(string key, string destination) : base(key, destination) { } protected override Task> TransformResponse(HttpResponseMessage responseMessage) { @@ -78,7 +78,7 @@ public class CustomGatherer : Gatherer } } ``` -snippet source | anchor +snippet source | anchor ### Taking control of the downstream invocation process @@ -88,9 +88,9 @@ If transforming returned data is not enough, it's possible to take full control ```cs -public class CustomGatherer : Gatherer +public class CustomHttpGatherer : HttpGatherer { - public CustomGatherer(string key, string destination) : base(key, destination) { } + public CustomHttpGatherer(string key, string destination) : base(key, destination) { } public override Task> Gather(HttpContext context) { From 92899ce0ccf62e51911e22d9ab53f0122f47d861 Mon Sep 17 00:00:00 2001 From: Mauro Servienti Date: Thu, 15 Jun 2023 16:27:10 +0200 Subject: [PATCH 28/28] Add a TODO to introduce support for output formatters --- .../ScatterGather/ScatterGatherEndpointBuilderExtensions.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/ServiceComposer.AspNetCore/ScatterGather/ScatterGatherEndpointBuilderExtensions.cs b/src/ServiceComposer.AspNetCore/ScatterGather/ScatterGatherEndpointBuilderExtensions.cs index 2558b861..c1a38eb0 100644 --- a/src/ServiceComposer.AspNetCore/ScatterGather/ScatterGatherEndpointBuilderExtensions.cs +++ b/src/ServiceComposer.AspNetCore/ScatterGather/ScatterGatherEndpointBuilderExtensions.cs @@ -31,6 +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()); }); }