Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .editorconfig
Original file line number Diff line number Diff line change
Expand Up @@ -295,4 +295,5 @@ dotnet_style_parentheses_in_relational_binary_operators = always_for_clarity:sil
dotnet_style_parentheses_in_other_operators = never_if_unnecessary:silent
dotnet_style_require_accessibility_modifiers = omit_if_default:suggestion
dotnet_diagnostic.CA1851.severity = none
dotnet_diagnostic.CA1873.severity = none
resharper_possible_multiple_enumeration_highlighting = none
122 changes: 61 additions & 61 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -145,22 +145,14 @@ Objective: create a service for executing HTTP requests via REST interface in JS
To solve this problem, you need to create an interface, implement this interface in a class, and connect the interface and implementation in DI.
In this case, the interface makes it easy to unit test the service that uses the interface.

*ServiceUriOptions.cs*
```csharp
public class ServiceUriOptions
{
public string ServiceUri { get; set; } = default!;
}
```

*RemoteServiceModel.cs*
```csharp
public class RemoteServiceModel
{
public int UserId { get; set; }
public int Id { get; set; }
public string Title { get; set; } = default!;
public string Body { get; set; } = default!;
public string Title { get; set; }
public string Body { get; set; }
}
```

Expand All @@ -172,42 +164,39 @@ public interface IRemoteServiceApiHttpService
}
```

The interface implementation must inherit from the class `RestHttpClient` or from `RestHttpClientFromOptions<TOptions>`.

`RestHttpClientFromOptions<TOptions>` is a base class that provides an out-of-the-box `BaseUri` injection mechanism for `HttpClient`.

`TOptions` is a class that is used to read settings from `asppsettings.json` for base addresses of services and is injected into `ServiceCollection` as `IOptions<TOptions>`
The interface implementation must inherit from the class `RestHttpClient`.

Class implementation:

```csharp
public class DefaultRemoteServiceApiHttpService : RestHttpClientFromOptions<ServiceUriOptions>, IRemoteServiceApiHttpService
public class DefaultRemoteServiceApiHttpService : RestHttpClient, IRemoteServiceApiHttpService
{
public DefaultRemoteServiceApiHttpService(IOptions<ServiceUriOptions> optionsAccessor,
HttpClient httpClient,
ILoggerFactory loggerFactory,
RestHttpClientOptions configuration,
IHttpContextAccessor httpContextAccessor)
: base(optionsAccessor,
httpClient,
loggerFactory,
configuration,
httpContextAccessor,
optionsAccessor.Value.ServiceUri)

public DefaultRemoteServiceApiHttpService(
HttpClient httpClient,
ILoggerFactory loggerFactory,
RestHttpClientOptions configuration,
IHttpContextAccessor httpContextAccessor)
: base(
httpClient,
loggerFactory,
configuration,
httpContextAccessor)
{
_baseUri = optionsAccessor.Value.ServiceUri;
}

public async Task<IList<RemoteServiceModel>> GetAllInstances()
{
var uri = "api/instances";
const string uri = "api/instances";
var result = await Get<IList<RemoteServiceModel>>(uri, TimeSpan.FromSeconds(10));

return result.ResultObject;
}
}
```

Moreover, such services are implemented via DI as HttpClient services and they must be added to DI over `AddHttpClient<>()` method.
Such services are implemented via DI as HttpClient services and they must be added to DI over `AddHttpClient<>()` method.

```csharp
public class Startup
Expand All @@ -216,32 +205,34 @@ public class Startup
{
....
services.AddOptions();
services.Configure<ServiceUriOptions>(Configuration.GetSection("Services"));

services.AddHttpClient<IRemoteServiceApiHttpService, DefaultRemoteServiceApiHttpService>();
services.AddHttpClient<IRemoteServiceApiHttpService, DefaultRemoteServiceApiHttpService>(x =>
{
x.BaseAddress = new("service/uri");
x.Timeout = TimeSpan.FromHours(1);
});
}
}
```

If you need to get access to other instances from the `ServiceCollection` collection inside the http service, then classic dependency injection is implemented.

```csharp
public class CachedRemoteServiceApiHttpService : RestHttpClientFromOptions<ServiceUriOptions>, IRemoteServiceApiHttpService
public class CachedRemoteServiceApiHttpService : RestHttpClient, IRemoteServiceApiHttpService
{
readonly IMemoryCache _memoryCache;

public CachedRemoteServiceApiHttpService(IOptions<ServiceUriOptions> optionsAccessor,
HttpClient httpClient,
ILoggerFactory loggerFactory,
RestHttpClientOptions configuration,
IHttpContextAccessor httpContextAccessor
IMemoryCache memoryCache)
: base(optionsAccessor,
httpClient,
loggerFactory,
configuration,
httpContextAccessor,
optionsAccessor.Value.ServiceUri)
public CachedRemoteServiceApiHttpService(
HttpClient httpClient,
ILoggerFactory loggerFactory,
RestHttpClientOptions configuration,
IHttpContextAccessor httpContextAccessor
IMemoryCache memoryCache)
: base(
httpClient,
loggerFactory,
configuration,
httpContextAccessor)
{
_memoryCache = memoryCache;
}
Expand All @@ -261,7 +252,7 @@ ILogger<DefaultRemoteServiceApiHttpService> log
```csharp
public async Task<RestHttpResponseMessage<IList<RemoteServiceModel>> GetAllInstances()
{
var uri = "api/instances";
const string uri = "api/instances";
var result = await Get<IList<RemoteServiceModel>>(uri, TimeSpan.FromSeconds(10));

return result;
Expand All @@ -276,7 +267,7 @@ public async Task<RestHttpResponseMessage<IList<RemoteServiceModel>> FilterInsta
if (filter is null || filter.Prop is null)
return RestHttpResponseMessageWrapper.Empty<IEnumerable<ConnectorMinimalViewModel>>(); // using the response wrapper.

var uri = "api/instances";
const string uri = "api/instances";
var result = await Get<IList<RemoteServiceModel>>(uri, TimeSpan.FromSeconds(10));

return result;
Expand Down Expand Up @@ -329,17 +320,10 @@ Http services inherited from this class are easy to test.
public class DefaultRemoteServiceApiHttpServiceTests
{
readonly ILogger<DefaultRemoteServiceApiHttpService> _logger;
readonly Mock<IOptions<ServiceUriOptions>> _serviceUriOptionsMock;

public DefaultRemoteServiceApiHttpServiceTests()
{
_logger = new StubLogger<DefaultRemoteServiceApiHttpService>();

_serviceUriOptionsMock = new Mock<IOptions<ServiceUriOptions>>();
_serviceUriOptionsMock.Setup(x => x.Value)
.Returns(new ServiceUriOptions() {
ServiceUri = "https://jsonplaceholder.typicode.com"
});
}

[Fact]
Expand Down Expand Up @@ -369,14 +353,13 @@ public class DefaultRemoteServiceApiHttpServiceTests
Assert.Equal(model.UserId, firstInstance.UserId);
}

DefaultRemoteServiceApiHttpService CreateApiService(HttpClient httpClient, HttpContext? httpContext, IOptions<ServiceOptions> optionsAccessor)
DefaultRemoteServiceApiHttpService CreateApiService(HttpClient httpClient, HttpContext? httpContext = null)
{
return new DefaultRemoteServiceApiHttpService(optionsAccessor ?? _optionsMoq.Object,
httpClient,
_loggerFactory,
null,
new HttpContextAccessorStub(httpContext ?? new DefaultHttpContext()),
optionsAccessor.Value.ServiceUri);
return new DefaultRemoteServiceApiHttpService(
httpClient,
_loggerFactory,
null,
new HttpContextAccessorStub(httpContext ?? new DefaultHttpContext()));
}
}
```
Expand All @@ -396,4 +379,21 @@ In the v5 the library was changed a lot. So you must follow migration steps.
9. In the class methods remove remove change all strings `client.Get()` or `client.Post()` and others to just `Get()` or `Post()`.
10. In the Startup.cs change `services.AddTransient<IService, Service>()` to `servicese.AddHttpClient<IService, Service>()` for all http services inherited from the `RestHttpClient` and `RestHttpClientFromOptions`.
11. In the Startup.cs change `services.AddScoped<IService, Service>()` to `servicese.AddHttpClient<IService, Service>()` for all http services inherited from the `RestHttpClient` and `RestHttpClientFromOptions`.
12. Change all unit tests to the new version described in the [Testing features](#testing-features).
12. Change all unit tests to the new version described in the [Testing features](#testing-features).

### Migration Guide to v7

1. Replace `RestHttpClientFromOptions<T>` with `RestHttpClient` and remove unnecessary injections in implementation class constructor.
2. Move your underlying `HttpClient` configuration to `.AddHttpClient<>()` method.

```c#
services.AddHttpClient<IService, Service>((serviceProvider, client) =>
{
// Get base uri from application options.
var baseUri = serviceProvider.GetRequiredService<IOptions<ServiceUriOptions>>().Value.BaseUri;
client.BaseAddress = new(baseUri);

// Override default HTTP client timeout (100 sec).
client.Timeout = System.Threading.Timeout.InfiniteTimeSpan;
})
```
Original file line number Diff line number Diff line change
@@ -1,24 +1,23 @@
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using System.Threading.Tasks;

namespace Monq.Core.HttpClientExtensions.TestApp.Controllers
namespace Monq.Core.HttpClientExtensions.TestApp.Controllers;

[Route("api/test")]
public class TestController : Controller
{
[Route("api/test")]
public class TestController : Controller
{
readonly ITestService _service;
readonly ITestService _service;

public TestController(ITestService service)
{
_service = service;
}
public TestController(ITestService service)
{
_service = service;
}

[HttpGet]
public async Task<IActionResult> Get()
{
var result = await _service.TestApi();
[HttpGet]
public async Task<IActionResult> Get()
{
var result = await _service.TestApi();

return Ok(result);
}
return Ok(result);
}
}
74 changes: 53 additions & 21 deletions samples/Monq.Core.HttpClientExtensions.TestApp/Program.cs
Original file line number Diff line number Diff line change
@@ -1,25 +1,57 @@
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Hosting;
using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Options;
using Monq.Core.HttpClientExtensions;
using Monq.Core.HttpClientExtensions.TestApp;
using Polly;
using Polly.Extensions.Http;
using System;
using System.Net.Http;
using System.Text;

namespace Monq.Core.HttpClientExtensions.TestApp
{
public class Program
var builder = WebApplication.CreateBuilder(args);
Console.OutputEncoding = Encoding.UTF8;

builder.Host
.ConfigBasicHttpService(opts =>
{
var headerOptions = new RestHttpClientHeaderOptions();
headerOptions.AddForwardedHeader("X-Trace-Event-Id");
headerOptions.AddForwardedHeader("Accept-Language");
opts.ConfigHeaders(headerOptions);
});

builder.Services.AddOptions();
builder.Services.AddHttpContextAccessor();
builder.Services.AddLogging();
builder.Services.Configure<ServiceUriOptions>(x => x.TestServiceUri = "https://jsonplaceholder.typicode.com");

builder.Services
.AddHttpClient<ITestService, TestService>((serviceProvider, client) =>
{
public static void Main(string[] args)
{
CreateHostBuilder(args).Build().Run();
}

public static IHostBuilder CreateHostBuilder(string[] args) =>
Host.CreateDefaultBuilder(args)
.ConfigBasicHttpService(opts =>
{
var headerOptions = new RestHttpClientHeaderOptions();
headerOptions.AddForwardedHeader("X-Trace-Event-Id");
headerOptions.AddForwardedHeader("Accept-Language");
opts.ConfigHeaders(headerOptions);
})
.ConfigureWebHostDefaults(webBuilder => webBuilder.UseStartup<Startup>());
}
var baseUri = serviceProvider.GetRequiredService<IOptions<ServiceUriOptions>>().Value.TestServiceUri;
client.BaseAddress = new(baseUri);
})
.AddPolicyHandler(GetCircuitBreakerPolicy());

builder.Services
.AddControllers()
.AddJsonOptions(options => options.JsonSerializerOptions.PropertyNameCaseInsensitive = true);

var app = builder.Build();

if (app.Environment.IsDevelopment())
app.UseDeveloperExceptionPage();

app.UseRouting();
app.MapControllers();

await app.RunAsync();

static IAsyncPolicy<HttpResponseMessage> GetCircuitBreakerPolicy()
{
return HttpPolicyExtensions
.HandleTransientHttpError()
.CircuitBreakerAsync(2, TimeSpan.FromSeconds(30));
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
namespace Monq.Core.HttpClientExtensions.TestApp
namespace Monq.Core.HttpClientExtensions.TestApp;

public class ServiceUriOptions
{
public class ServiceUriOptions
{
public string TestServiceUri { get; set; }
}
public string TestServiceUri { get; set; }
}
Loading