Skip to content
This repository was archived by the owner on Oct 14, 2025. It is now read-only.

Commit b76850a

Browse files
committed
feature(ReversoClientConfig) add base changes for ReversoClientConfig
Added: -Logger wrapper -HttpClient wrapper -Log logic for cover error cases Refactored: -HttpClient realization
1 parent 113b326 commit b76850a

15 files changed

Lines changed: 274 additions & 68 deletions

File tree

ReversoAPI.Web/ConjugationFeature/Domain/Core/Services/ConjugationParserService.cs

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,19 @@
11
using System.IO;
22
using ReversoAPI.Web.ConjugationFeature.Domain.Supporting.Builders;
33
using ReversoAPI.Web.Shared.Domain.Services;
4+
using ReversoAPI.Web.Shared.Infrastructure.Logger;
45

56
namespace ReversoAPI.Web.ConjugationFeature.Domain.Core.Services
67
{
78
public class ConjugationParserService : BaseParser<ConjugationData>
89
{
10+
private readonly ILogger _log;
11+
12+
public ConjugationParserService(ILogger log)
13+
{
14+
_log = log;
15+
}
16+
917
protected override ConjugationData Parse(Stream htmlStream)
1018
{
1119
try
@@ -16,8 +24,9 @@ protected override ConjugationData Parse(Stream htmlStream)
1624
.WithConjugations()
1725
.Build();
1826
}
19-
catch (ParsingException)
27+
catch (ParsingException ex)
2028
{
29+
_log?.Error(ex.Message);
2130
return null;
2231
}
2332
}

ReversoAPI.Web/ContextFeature/Domain/Core/Services/ContextParserService.cs

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,18 @@
11
using System.IO;
22
using ReversoAPI.Web.ContextFeature.Domain.Supporting.Builders;
33
using ReversoAPI.Web.Shared.Domain.Services;
4+
using ReversoAPI.Web.Shared.Infrastructure.Logger;
45

56
namespace ReversoAPI.Web.ContextFeature.Domain.Core.Services
67
{
78
public class ContextParserService : BaseParser<ContextData>
89
{
10+
private readonly ILogger _log;
11+
public ContextParserService(ILogger log)
12+
{
13+
_log = log;
14+
}
15+
916
protected override ContextData Parse(Stream htmlStream)
1017
{
1118
try
@@ -16,8 +23,9 @@ protected override ContextData Parse(Stream htmlStream)
1623
.WithExamples()
1724
.Build();
1825
}
19-
catch (ParsingException)
26+
catch (ParsingException ex)
2027
{
28+
_log?.Error(ex.Message);
2129
return null;
2230
}
2331
}

ReversoAPI.Web/GrammarCheckFeature/Application/Services/SpellingService.cs

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,15 +6,22 @@
66
using ReversoAPI.Web.GrammarCheckFeature.Application.Validators;
77
using ReversoAPI.Web.Shared.Application.Extensions;
88
using ReversoAPI.Web.Shared.Infrastructure.Http.Interfaces;
9+
using ReversoAPI.Web.Shared.Infrastructure.Logger;
910

1011
namespace ReversoAPI.Web.GrammarCheckFeature.Application.Services
1112
{
1213
public class SpellingService : ISpellingService
1314
{
1415
private const string SpellingURL = "https://orthographe.reverso.net/api/v1/Spelling/";
1516

17+
private readonly ILogger _log;
1618
private readonly IAPIConnector _apiConnector;
17-
public SpellingService(IAPIConnector apiConnector) => _apiConnector = apiConnector;
19+
20+
public SpellingService(IAPIConnector apiConnector, ILogger log)
21+
{
22+
_log = log;
23+
_apiConnector = apiConnector;
24+
}
1825

1926
public async Task<SpellingData> GetAsync(string text, Language language, Locale locale = Locale.None, CancellationToken cancellationToken = default)
2027
{
@@ -27,7 +34,11 @@ public async Task<SpellingData> GetAsync(string text, Language language, Locale
2734
var spellingDto = response.Content.Deserialize<SpellingResponse>();
2835

2936
var validationResult = new SpellingResponseValidator(spellingDto).Validate();
30-
if (!validationResult.IsValid) return null;
37+
if (!validationResult.IsValid)
38+
{
39+
_log?.Error(validationResult.Message);
40+
return null;
41+
}
3142

3243
return spellingDto.ToModel();
3344
}

ReversoAPI.Web/ReversoClient.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ public class ReversoClient : IReversoClient
1515
{
1616
public ReversoClient()
1717
{
18-
var apiConnector = APIConnector.Create(HttpClientCacheWrapper.GetInstance());
18+
var apiConnector = new APIConnector(new CachedHttpClient());
1919

2020
Context = new ContextClient(new ContextService(apiConnector, new ContextParserService()));
2121
Synonyms = new SynonymsClient(new SynonymsService(apiConnector, new SynonymsParserService()));
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
using ReversoAPI.Web.ContextFeature.Domain.Core.Services;
2+
using ReversoAPI.Web.Shared.Domain.Interfaces.Services;
3+
using ReversoAPI.Web.Shared.Infrastructure.Http;
4+
using ReversoAPI.Web.Shared.Infrastructure.Http.Interfaces;
5+
using ReversoAPI.Web.Shared.Infrastructure.Logger;
6+
using ReversoAPI.Web.SynonymsFeature.Domain.Core.Services;
7+
using System;
8+
9+
namespace ReversoAPI.Web
10+
{
11+
public class ReversoClientConfig
12+
{
13+
// General
14+
public IHttpClient HttpClient { get; private set; }
15+
public IAPIConnector APIConnector { get; private set; }
16+
17+
// Let's fill up only elementary fields
18+
// complex objects neither will be filled by user or will be setted by default
19+
private IParser<ContextData> _contextParser;
20+
public IParser<ContextData> ContextParser
21+
{
22+
get => _contextParser ?? new ContextParserService(Logger);
23+
private set => _contextParser = value;
24+
}
25+
public IParser<SynonymsData> SynonymsParser { get; private set; }
26+
27+
// Extra
28+
public ILogger Logger { get; private set; }
29+
30+
private ReversoClientConfig(
31+
IHttpClient httpClient,
32+
IAPIConnector apiConnector,
33+
IParser<ContextData> contextParser,
34+
IParser<SynonymsData> synonymsParser,
35+
ILogger logger)
36+
{
37+
HttpClient = httpClient;
38+
APIConnector = apiConnector;
39+
ContextParser = contextParser;
40+
Logger = logger;
41+
}
42+
43+
ReversoClientConfig CreateDefault()
44+
{
45+
var httpClient = new CachedHttpClient();
46+
47+
return new ReversoClientConfig(
48+
httpClient,
49+
new APIConnector(httpClient),
50+
new ContextParserService(null),
51+
new SynonymsParserService(null),
52+
null);
53+
}
54+
55+
ReversoClientConfig WithHttpClient(IHttpClient httpClient)
56+
{
57+
if(httpClient is null) throw new ArgumentNullException(nameof(httpClient));
58+
59+
HttpClient = httpClient;
60+
return this;
61+
}
62+
63+
ReversoClientConfig WithApiConnector(IAPIConnector apiConnector)
64+
{
65+
if (apiConnector is null) throw new ArgumentNullException(nameof(apiConnector));
66+
67+
APIConnector = apiConnector;
68+
return this;
69+
}
70+
71+
ReversoClientConfig WithLogger(ILogger logger)
72+
{
73+
Logger = logger;
74+
return this;
75+
}
76+
}
77+
}
Lines changed: 8 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
using System;
2-
using System.IO;
32
using System.Net;
43
using System.Linq;
54
using System.Text;
@@ -14,7 +13,7 @@ namespace ReversoAPI.Web.Shared.Infrastructure.Http
1413
{
1514
public class APIConnector : IAPIConnector
1615
{
17-
private readonly HttpClient _httpClient;
16+
private readonly IHttpClient _httpClient;
1817

1918
private const int RetryAttemptCount = 4;
2019
private static readonly HttpStatusCode[] _httpStatusCodesWorthRetrying =
@@ -26,36 +25,23 @@ public class APIConnector : IAPIConnector
2625
HttpStatusCode.GatewayTimeout
2726
};
2827

29-
private APIConnector(HttpClient httpClient)
28+
public APIConnector(IHttpClient httpClient)
3029
{
3130
_httpClient = httpClient;
3231
}
3332

34-
public static APIConnector Create(IHttpClientCacheWrapper httpClientCache)
35-
{
36-
return new APIConnector(httpClientCache.GetHttpClient());
37-
}
38-
3933
public async Task<HttpResponse> GetAsync(Uri uri, CancellationToken cancellationToken = default)
4034
{
4135
if (uri == null) throw new ArgumentNullException(nameof(uri));
4236

43-
using var response = await Policy
37+
var response = await Policy
4438
.Handle<HttpRequestException>()
45-
.OrResult<HttpResponseMessage>(r => _httpStatusCodesWorthRetrying.Contains(r.StatusCode))
39+
.OrResult<HttpResponse>(r => _httpStatusCodesWorthRetrying.Contains(r.StatusCode))
4640
.WaitAndRetryAsync(RetryAttemptCount, attempt => TimeSpan.FromSeconds(Math.Pow(2, attempt)))
4741
.ExecuteAsync(() => _httpClient.GetAsync(uri, cancellationToken))
4842
.ConfigureAwait(false);
4943

50-
var content = await response.Content
51-
.ReadAsStreamAsync()
52-
.ConfigureAwait(false);
53-
54-
return new HttpResponse
55-
{
56-
ContentType = response.Content.Headers.ContentType.MediaType,
57-
Content = await MakeACopyAsync(content, cancellationToken).ConfigureAwait(false),
58-
};
44+
return response;
5945
}
6046

6147
public async Task<HttpResponse> PostAsync(Uri uri, object payload, CancellationToken cancellationToken = default)
@@ -66,35 +52,14 @@ public async Task<HttpResponse> PostAsync(Uri uri, object payload, CancellationT
6652
var json = JsonConvert.SerializeObject(payload);
6753
var data = new StringContent(json, Encoding.UTF8, "application/json");
6854

69-
using var response = await Policy
55+
var response = await Policy
7056
.Handle<HttpRequestException>()
71-
.OrResult<HttpResponseMessage>(r => _httpStatusCodesWorthRetrying.Contains(r.StatusCode))
57+
.OrResult<HttpResponse>(r => _httpStatusCodesWorthRetrying.Contains(r.StatusCode))
7258
.WaitAndRetryAsync(RetryAttemptCount, attempt => TimeSpan.FromSeconds(Math.Pow(2, attempt)))
7359
.ExecuteAsync(() => _httpClient.PostAsync(uri, data, cancellationToken))
7460
.ConfigureAwait(false);
7561

76-
var content = await response.Content
77-
.ReadAsStreamAsync()
78-
.ConfigureAwait(false);
79-
80-
return new HttpResponse
81-
{
82-
ContentType = response.Content.Headers.ContentType.MediaType,
83-
Content = await MakeACopyAsync(content, cancellationToken).ConfigureAwait(false),
84-
};
85-
}
86-
87-
// TODO: Rid of this. HttpResponseMessage disposing leads to close Content stream.
88-
private async Task<Stream> MakeACopyAsync(Stream stream, CancellationToken cancellationToken = default)
89-
{
90-
var ms = new MemoryStream();
91-
92-
await stream
93-
.CopyToAsync(ms, cancellationToken)
94-
.ConfigureAwait(false);
95-
96-
ms.Position = 0;
97-
return ms;
62+
return response;
9863
}
9964
}
10065
}
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
using Newtonsoft.Json;
2+
using System;
3+
using System.IO;
4+
using System.Net.Http;
5+
using System.Text;
6+
using System.Threading;
7+
using System.Threading.Tasks;
8+
using ReversoAPI.Web.Shared.Infrastructure.Http.Interfaces;
9+
10+
namespace ReversoAPI.Web.Shared.Infrastructure.Http
11+
{
12+
public class CachedHttpClient : IHttpClient
13+
{
14+
private readonly HttpClient _httpClient;
15+
16+
public CachedHttpClient()
17+
{
18+
_httpClient = HttpClientCacheWrapper
19+
.GetInstance()
20+
.GetHttpClient();
21+
}
22+
23+
public async Task<HttpResponse> GetAsync(Uri uri, CancellationToken cancellationToken = default)
24+
{
25+
if (uri == null) throw new ArgumentNullException(nameof(uri));
26+
27+
var response = await _httpClient
28+
.GetAsync(uri, cancellationToken)
29+
.ConfigureAwait(false);
30+
31+
var content = await response.Content
32+
.ReadAsStreamAsync()
33+
.ConfigureAwait(false);
34+
35+
return new HttpResponse(
36+
contentType: response.Content.Headers.ContentType.MediaType,
37+
content: await CopyAsync(content, cancellationToken).ConfigureAwait(false),
38+
response.StatusCode);
39+
}
40+
41+
public async Task<HttpResponse> PostAsync(Uri uri, object payload, CancellationToken cancellationToken = default)
42+
{
43+
if (uri == null) throw new ArgumentNullException(nameof(uri));
44+
45+
var json = JsonConvert.SerializeObject(payload);
46+
var data = new StringContent(json, Encoding.UTF8, "application/json");
47+
48+
var response = await _httpClient.PostAsync(uri, data, cancellationToken);
49+
50+
var content = await response.Content
51+
.ReadAsStreamAsync()
52+
.ConfigureAwait(false);
53+
54+
return new HttpResponse(
55+
contentType: response.Content.Headers.ContentType.MediaType,
56+
content: await CopyAsync(content, cancellationToken).ConfigureAwait(false),
57+
response.StatusCode);
58+
}
59+
60+
// TODO: Rid of this. HttpResponseMessage disposing leads to close Content stream.
61+
private async Task<Stream> CopyAsync(Stream stream, CancellationToken cancellationToken = default)
62+
{
63+
var ms = new MemoryStream();
64+
65+
await stream
66+
.CopyToAsync(ms, cancellationToken)
67+
.ConfigureAwait(false);
68+
69+
ms.Position = 0;
70+
return ms;
71+
}
72+
}
73+
}

ReversoAPI.Web/Shared/Infrastructure/Http/HttpClientCacheWrapper.cs

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,9 @@
11
using System;
22
using System.Net.Http;
3-
using ReversoAPI.Web.Shared.Infrastructure.Http.Interfaces;
43

54
namespace ReversoAPI.Web.Shared.Infrastructure.Http
65
{
7-
public class HttpClientCacheWrapper : IHttpClientCacheWrapper
6+
public class HttpClientCacheWrapper
87
{
98
private const string RandomUserAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/110.0.0.0 Safari/537.36";
109
private const bool DefaultCacheKey = true;

ReversoAPI.Web/Shared/Infrastructure/Http/HttpResponse.cs

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,21 @@
11
using System;
22
using System.IO;
3+
using System.Net;
34

45
namespace ReversoAPI.Web.Shared.Infrastructure.Http
56
{
67
public class HttpResponse : IDisposable
78
{
8-
public string ContentType { get; set; }
9-
public Stream Content { get; set; }
9+
public HttpResponse(string contentType, Stream content, HttpStatusCode statusCode)
10+
{
11+
ContentType = contentType;
12+
Content = content;
13+
StatusCode = statusCode;
14+
}
15+
16+
public string ContentType { get; }
17+
public Stream Content { get; }
18+
public HttpStatusCode StatusCode { get; }
1019

1120
public void Dispose()
1221
{

0 commit comments

Comments
 (0)