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

Feature / Communication Beta #100

Merged
merged 27 commits into from
Feb 14, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
54b9db0
Update unreleased.md
dncsvr Feb 9, 2024
d1bf30f
add `HttpClientLayer`
dncsvr Feb 9, 2024
114ffa9
fix failing build
dncsvr Feb 9, 2024
3aa93b2
add `HttpCommunication` abstractions
dncsvr Feb 9, 2024
918f7d9
implement `HttpCommunication` feature !!! TESTS FAILING !!!
dncsvr Feb 9, 2024
7cb6b5d
fix failing tests
dncsvr Feb 9, 2024
2b8ee57
add `MockCommunicatio` feature implementation - incomplete
dncsvr Feb 9, 2024
e1ee294
refactor `MockCommunication` feature implementation
dncsvr Feb 9, 2024
e1d25d9
refactoring Communication Features
dncsvr Feb 11, 2024
b38c5fb
fix failing build
dncsvr Feb 11, 2024
d0a10d5
minor edits
dncsvr Feb 11, 2024
ca6144b
add `HttpClient` layer and `Communication` feature documentations
dncsvr Feb 12, 2024
d69f494
minor edits
dncsvr Feb 12, 2024
c5bcd74
add test endpoint for testing request exception handling !!! TESTS FA…
dncsvr Feb 12, 2024
b0ea19c
remove test http request exception handler test
dncsvr Feb 12, 2024
9e4d416
minor edits
dncsvr Feb 12, 2024
0c031bc
refactoring
dncsvr Feb 12, 2024
21ac857
minor edit
dncsvr Feb 12, 2024
d493e5d
review changes
dncsvr Feb 13, 2024
35d2954
review changes
dncsvr Feb 13, 2024
659d395
review changes
dncsvr Feb 14, 2024
0b862f5
refactor mock communication feature
dncsvr Feb 14, 2024
fe867b4
update `MockCommunicationFeature` docs
dncsvr Feb 14, 2024
4f11ec7
review changes
dncsvr Feb 14, 2024
86ae23f
edit MockCommunicationFeature example docs
dncsvr Feb 14, 2024
f18042e
fix failing build
dncsvr Feb 14, 2024
79e965f
minor edits
dncsvr Feb 14, 2024
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
3 changes: 2 additions & 1 deletion Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="17.8.0" />
<PackageVersion Include="Moq" Version="4.20.70" />
<PackageVersion Include="MySql.Data" Version="8.3.0" />
<PackageVersion Include="Newtonsoft.Json" Version="13.0.3" />
<PackageVersion Include="NHibernate" Version="5.5.0" />
<PackageVersion Include="NUnit" Version="4.0.1" />
<PackageVersion Include="NUnit3TestAdapter" Version="4.5.0" />
Expand All @@ -28,5 +29,5 @@
<PackageVersion Include="Swashbuckle.AspNetCore" Version="6.5.0" />
<PackageVersion Include="System.Data.SQLite" Version="1.0.118" />
</ItemGroup>

</Project>
2 changes: 2 additions & 0 deletions docs/blueprints/service.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ Layers in this blueprint are;
| Data Access | + | + |
| Dependency Injection | + | + |
| Domain | + | + |
| Http Client | + | |
| Http Server | + | |
| Monitoring | + | + |
| Rest Api | + | + |
Expand All @@ -40,6 +41,7 @@ Features with default options are;
| ------------------ | ------------- | --------------- | -------- |
| Business | Default | Default | Yes |
| Caching | Scoped Memory | Scoped Memory | |
| Communication | Http | Mock | |
| Core | Dotnet | Mock | |
| Database | Sqlite | InMemory | Yes |
| Documentation | Default | | |
Expand Down
40 changes: 40 additions & 0 deletions docs/features/communication.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
# Communication

Add this feature using `AddCommunication()` extension;

```csharp
app.Features.AddCommunication(...);
```

## Http

This feature provides an `IClient<>` implementation and adds descriptors for
configured named clients from _app.settings_

```csharp
c => c.Http()
```
```json
"Communication": {
"Http": {
"MyService": {
"BaseAddress": "http://api.backend.com",
"DefaultHeaders": {
"User-Agent": ".NET Http Client"
}
}
}
}
```

## Mock

Adds a mock implementation to be used in testing with `MockClientConfiguration`

```csharp
communication: c => c.Mock(defaultResponses: response =>
{
response.ForClient<MyService>("""{ "value": "test result" }""");
response.ForClient<MyOtherService>(new { value = "path1 response" }, when: r => r.UrlOrPath.Equals("path1"));
})
```
30 changes: 30 additions & 0 deletions docs/layers/http-client.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# Http Client

DO uses ASP.NET Core's `IHttpClientFactory` and related services for providing
clients for http requests and responses.

```csharp
app.Layers.AddHttpClient();
```

## Configuration Targets

`HttpClient` layer provides `List<HttpClientDescriptor>` which is used to add
named configuration delegates for `IHttpClientBuilder`.

### `List<HttpClientDescriptor>`

This target is provided in `AddServices` phase as the target. To configure it
in a feature;

```csharp
configurator.ConfigureHttpClient(clients =>
{
...
});
```

> :information_source:
>
> Descriptor with name "Default" is added as a default builder delegate for all
> created http clients
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
namespace Do.Communication;

public class CommunicationConfigurator { }
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
using Do.Architecture;
using Do.Communication;

namespace Do;

public static class CommunicationExtensions
{
public static void AddCommunication(this List<IFeature> source, Func<CommunicationConfigurator, IFeature<CommunicationConfigurator>> configure) =>
source.Add(configure(new()));
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
using Microsoft.Extensions.Logging;
using System.Text;

namespace Do.Communication.Http;

public class Client<T>(
ILogger<Client<T>> _logger,
HttpClientFactory<T> _clientFactory
) : IClient<T>
{
public async Task<Response> Send(Request request)
{
var req = new HttpRequestMessage(request.Method, request.UrlOrPath);

if (request.Content is not null && !string.IsNullOrWhiteSpace(request.Content.Body))
{
req.Content = new StringContent(request.Content.Body, Encoding.UTF8, request.Content.Type);
}

foreach (var (name, value) in request.Headers)
{
if (string.IsNullOrWhiteSpace(value)) { continue; }

req.Headers.Add(name, value);
}

var client = _clientFactory.Create();
var res = await client.SendAsync(req);
var content = await res.Content.ReadAsStringAsync();

try
{
res.EnsureSuccessStatusCode();
}
catch (HttpRequestException ex)
{
if (ex.StatusCode is not null && (int)ex.StatusCode >= 500)
{
_logger.LogError(ex, ex.Message);
}

throw new ClientException(content, ex);
}

return new(content);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
namespace Do.Communication.Http;

public record ClientConfig(
Uri? BaseAddress = default,
Dictionary<string, string>? DefaultHeaders = default
);
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
namespace System;

internal static class DictionaryExtensions
{
internal static Dictionary<string, string> Merge(this Dictionary<string, string>? source, Dictionary<string, string>? input)
{
source ??= [];
input ??= [];

foreach (var (key, value) in input)
{
if (!source.ContainsKey(key))
{
source[key] = value;
}
}

return source;
}
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
namespace Do.Communication.Http;

public class HttpClientFactory<T>(IHttpClientFactory _httpClientFactory)
{
public System.Net.Http.HttpClient Create() => _httpClientFactory.CreateClient(typeof(T).Name);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
using Do.Communication;
using Do.Communication.Http;

namespace Do;

public static class HttpCommunicationExtensions
{
public static HttpCommunicationFeature Http(this CommunicationConfigurator _) => new();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
using Do.Architecture;
using Do.HttpClient;
using Microsoft.Extensions.DependencyInjection;

namespace Do.Communication.Http;

public class HttpCommunicationFeature : IFeature<CommunicationConfigurator>
{
public void Configure(LayerConfigurator configurator)
{
configurator.ConfigureTypeCollection(types => types.Add(typeof(IClient<>)));

configurator.ConfigureHttpClients(descriptors =>
{
var configurations = Settings.Optional<Dictionary<string, ClientConfig>>("Communication:Http", []).GetSection() ?? [];
configurations.TryGetValue(HttpClientLayer.DefaultConfigKey, out var defaultSettings);

foreach (var (key, (baseAddress, defaultHeaders)) in configurations)
{
descriptors.Add(
new(
Name: key,
BaseAddress: baseAddress ?? defaultSettings?.BaseAddress,
DefaultHeaders: defaultHeaders.Merge(defaultSettings?.DefaultHeaders)
)
);
}
});

configurator.ConfigureServiceCollection(serviceCollection =>
{
serviceCollection.AddSingleton(typeof(HttpClientFactory<>));
serviceCollection.AddSingleton(typeof(IClient<>), typeof(Client<>));
});
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
using Do.Testing;
using Moq;
using Newtonsoft.Json;
using System.Reflection;

namespace Do.Communication.Mock;

public class DefaultResponseBuilder
{
static readonly MethodInfo _setupClient = typeof(DefaultResponseBuilder).GetMethod(nameof(SetupClient), BindingFlags.Static | BindingFlags.NonPublic)
?? throw new("SetupClient<T> should have existed");

static void SetupClient<T>(Mock<IClient<T>> mock, List<(string response, Func<Request, bool> when)> setups)
where T : class
{
foreach (var (response, when) in setups)
{
mock.Setup(c => c.Send(It.Is<Request>(r => when(r))))
.ReturnsAsync(new Response(response));
}
}

readonly Dictionary<Type, List<(string? response, Func<Request, bool> when)>> _setups = [];

public void ForClient<T>(object response,
Func<Request, bool>? when = default
) where T : class =>
ForClient<T>(JsonConvert.SerializeObject(response), when);

public void ForClient<T>(string responseString,
Func<Request, bool>? when = default
) where T : class
{
when ??= _ => true;

if (!_setups.TryGetValue(typeof(T), out var setups))
{
setups = _setups[typeof(T)] = [];
}

setups.Add((responseString, when));
}

internal IMockCollection BuildMockClients()
{
var result = new MockCollection();

foreach (var (type, setups) in _setups)
{
result.Add(
service: typeof(IClient<>).MakeGenericType(type),
singleton: true,
setup: mock => _setupClient.MakeGenericMethod(type).Invoke(null, [mock, setups])
);
}

return result;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
using Do.Communication;
using Do.Communication.Mock;

namespace Do;

public static class MockCommunicationExtensions
{
public static MockCommunicationFeature Mock(this CommunicationConfigurator _, Action<DefaultResponseBuilder>? defaultResponses = default) => new(defaultResponses ?? (_ => { }));
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
using Do.Architecture;

namespace Do.Communication.Mock;

public class MockCommunicationFeature(Action<DefaultResponseBuilder> _setupDefaultResponses)
: IFeature<CommunicationConfigurator>
{
public void Configure(LayerConfigurator configurator)
{
configurator.ConfigureTestConfiguration(tests =>
{
var builder = new DefaultResponseBuilder();
_setupDefaultResponses(builder);

foreach (var descriptor in builder.BuildMockClients())
{
tests.Mocks.Add(descriptor);
}
});
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,4 +18,8 @@ internal Setting(Func<IConfiguration> getConfiguration, string? key, T? defaultV
public T GetValue() =>
(_key is not null ? _getConfiguration().GetValue(_key, _defaultValue) : _defaultValue) ??
throw new ConfigurationRequiredException($"Config required for {_key}");

public T? GetSection() =>
_key is not null ? _getConfiguration().GetSection(_key).Get<T>() ?? _defaultValue :
throw new ConfigurationRequiredException($"Config required for {_key}");
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Features;
using Microsoft.Extensions.DependencyInjection;
using NHibernate;

Expand All @@ -10,6 +11,14 @@ public class FlatTransactionMiddleware(RequestDelegate _next)
{
public async Task InvokeAsync(HttpContext context)
{
var metadata = context.Features.Get<IEndpointFeature>()?.Endpoint?.Metadata;
if (metadata?.GetMetadata<NoTransactionAttribute>() is not null)
{
await _next(context);

return;
}

using (var session = context.RequestServices.GetRequiredService<ISession>())
{
session.BeginTransaction();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
namespace Do.Database;

public class NoTransactionAttribute : Attribute { }
Loading
Loading