Skip to content

Commit d9f3004

Browse files
committed
v3.0 API update and migrate to typed HttpClient
1 parent f23e8de commit d9f3004

File tree

55 files changed

+448
-173
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

55 files changed

+448
-173
lines changed

Directory.Build.targets

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
<Target Name="Versioning" BeforeTargets="MinVer">
44
<PropertyGroup Label="Build">
5-
<MinVerDefaultPreReleasePhase>preview</MinVerDefaultPreReleasePhase>
5+
<MinVerDefaultPreReleaseIdentifiers>preview.0</MinVerDefaultPreReleaseIdentifiers>
66
<MinVerTagPrefix>v</MinVerTagPrefix>
77
<MinVerVerbosity>normal</MinVerVerbosity>
88
</PropertyGroup>

README.md

Lines changed: 56 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -9,27 +9,72 @@ An unofficial .NET API client for the [MSRC CVRF API][1]
99

1010
## Usage
1111

12-
1. `dotnet add package JamieMagee.MicrosoftSecurityUpdates.Client`
13-
2. Create an instance of `MicrosoftSecurityUpdatesClient`:
12+
There are two main ways to use this client:
13+
14+
- Dependency Injection
15+
- Manual Instantiation
16+
17+
### Dependency Injection
18+
19+
To use the client with dependency injection, register it in your `IServiceCollection`:
1420

1521
```csharp
16-
using var client = new MicrosoftSecurityUpdatesClient();
22+
using JamieMagee.MicrosoftSecurityUpdates.Client;
23+
24+
// In Program.cs
25+
builder.Services.AddMicrosoftSecurityUpdatesClient();
26+
27+
// Or with custom HttpClient configuration
28+
builder.Services.AddMicrosoftSecurityUpdatesClient(httpClient =>
29+
{
30+
// Add custom headers, configure policies, etc.
31+
httpClient.Timeout = TimeSpan.FromSeconds(30);
32+
});
1733
```
1834

19-
3. Use the client to get information about security updates:
35+
Then inject `IMicrosoftSecurityUpdatesClient` into your services:
2036

2137
```csharp
22-
// Get all updates
38+
public class SecurityService
39+
{
40+
private readonly IMicrosoftSecurityUpdatesClient _client;
41+
42+
public SecurityService(IMicrosoftSecurityUpdatesClient client)
43+
{
44+
_client = client;
45+
}
46+
47+
public async Task<IEnumerable<Update>> GetLatestUpdatesAsync()
48+
{
49+
return await _client.GetUpdatesAsync();
50+
}
51+
52+
public async Task<CvrfDocument> GetSecurityBulletinAsync(string id)
53+
{
54+
return await _client.GetCvrfByIdAsync(id);
55+
}
56+
}
57+
```
58+
59+
### Manual Instantiation
60+
61+
You can also create instances manually:
62+
63+
```csharp
64+
// Using default HttpClient
65+
using var client = new MicrosoftSecurityUpdatesClient();
66+
67+
// Using your own HttpClient (for advanced scenarios)
68+
using var httpClient = new HttpClient();
69+
httpClient.Timeout = TimeSpan.FromSeconds(30);
70+
var client = new MicrosoftSecurityUpdatesClient(httpClient);
71+
72+
// Get updates
2373
var updates = await client.GetUpdatesAsync();
24-
// Get all updates by year or month
25-
var updates2022 = await client.GetUpdatesAsync("2022");
26-
var updates2022Dec = await client.GetUpdatesAsync("2022-Dec");
27-
// Get CVRF by month
28-
var cvrf2022Dec = await client.GetCvrfByIdAsync("2022-Dec");
2974
```
3075

3176
## License
3277

3378
All packages in this repository are licensed under [the MIT license](https://opensource.org/licenses/MIT).
3479

35-
[1]: https://api.msrc.microsoft.com/cvrf/v2.0/swagger/index
80+
[1]: https://msrc.microsoft.com/update-guide

src/JamieMagee.MicrosoftSecurityUpdates.Client/IMicrosoftSecurityUpdatesClient.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,9 @@ namespace JamieMagee.MicrosoftSecurityUpdates.Client;
22

33
public interface IMicrosoftSecurityUpdatesClient
44
{
5-
Task<CvrfDocument> GetCvrfByIdAsync(string id, CancellationToken cancellationToken = default);
5+
public Task<CvrfDocument> GetCvrfByIdAsync(string id, CancellationToken cancellationToken = default);
66

7-
Task<IEnumerable<Update>> GetUpdatesAsync(CancellationToken cancellationToken = default);
7+
public Task<IEnumerable<Update>> GetUpdatesAsync(CancellationToken cancellationToken = default);
88

9-
Task<IEnumerable<Update>> GetUpdateByIdAsync(string id, CancellationToken cancellationToken = default);
9+
public Task<IEnumerable<Update>> GetUpdateByIdAsync(string id, CancellationToken cancellationToken = default);
1010
}
Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
<Project Sdk="Microsoft.NET.Sdk">
22

33
<PropertyGroup>
4-
<TargetFramework>net6.0</TargetFramework>
4+
<TargetFramework>net8.0</TargetFramework>
55
<ImplicitUsings>enable</ImplicitUsings>
66
<Nullable>enable</Nullable>
77
</PropertyGroup>
@@ -11,7 +11,8 @@
1111
</ItemGroup>
1212

1313
<ItemGroup Label="Package References">
14-
<PackageReference Include="Restsharp" Version="108.0.3" />
14+
<PackageReference Include="Microsoft.Extensions.Http" Version="8.0.0" />
15+
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="8.0.0" />
1516
</ItemGroup>
1617

1718
</Project>
Lines changed: 65 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,63 +1,97 @@
11
namespace JamieMagee.MicrosoftSecurityUpdates.Client;
22

33
using System.Reflection;
4+
using System.Text.Json;
45
using JamieMagee.MicrosoftSecurityUpdates.Client.Models;
56

67
public sealed class MicrosoftSecurityUpdatesClient : IMicrosoftSecurityUpdatesClient, IDisposable
78
{
8-
private readonly RestClient client;
9+
private readonly HttpClient httpClient;
10+
private readonly bool shouldDisposeHttpClient;
11+
private readonly JsonSerializerOptions jsonSerializerOptions;
912

10-
public MicrosoftSecurityUpdatesClient(string baseUri)
13+
public MicrosoftSecurityUpdatesClient(HttpClient httpClient)
1114
{
12-
var version = typeof(MicrosoftSecurityUpdatesClient).GetTypeInfo().Assembly.GetName().Version;
13-
14-
var clientOptions = new RestClientOptions(baseUri)
15-
{
16-
UserAgent = $"JamieMagee.MicrosoftSecurityUpdates.Client/{version}",
17-
};
15+
this.httpClient = httpClient ?? throw new ArgumentNullException(nameof(httpClient));
16+
this.shouldDisposeHttpClient = false;
17+
this.jsonSerializerOptions = CreateJsonSerializerOptions();
1818

19-
this.client = new RestClient(clientOptions)
20-
.AddDefaultHeader("Accept", "application/json");
19+
this.ConfigureHttpClient();
2120
}
2221

23-
public MicrosoftSecurityUpdatesClient()
24-
: this("https://api.msrc.microsoft.com/cvrf/v2.0")
22+
public MicrosoftSecurityUpdatesClient(string baseUri = "https://api.msrc.microsoft.com/cvrf/v3.0/")
2523
{
24+
this.httpClient = new HttpClient();
25+
this.shouldDisposeHttpClient = true;
26+
this.jsonSerializerOptions = CreateJsonSerializerOptions();
27+
28+
this.httpClient.BaseAddress = new Uri(baseUri);
29+
this.ConfigureHttpClient();
2630
}
2731

28-
public void Dispose() => this.client.Dispose();
32+
public void Dispose()
33+
{
34+
if (this.shouldDisposeHttpClient)
35+
{
36+
this.httpClient.Dispose();
37+
}
38+
}
2939

30-
public Task<CvrfDocument> GetCvrfByIdAsync(string id, CancellationToken cancellationToken = default)
40+
public async Task<CvrfDocument> GetCvrfByIdAsync(string id, CancellationToken cancellationToken = default)
3141
{
32-
var request = new RestRequest($"cvrf/{id}");
33-
return this.ExecuteAsync<CvrfDocument>(request, cancellationToken);
42+
if (string.IsNullOrWhiteSpace(id))
43+
{
44+
throw new ArgumentException("ID cannot be null or whitespace.", nameof(id));
45+
}
46+
47+
var response = await this.httpClient.GetAsync($"cvrf/{id}", cancellationToken).ConfigureAwait(false);
48+
response.EnsureSuccessStatusCode();
49+
50+
var jsonContent = await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false);
51+
return JsonSerializer.Deserialize<CvrfDocument>(jsonContent, this.jsonSerializerOptions)
52+
?? throw new InvalidOperationException("Failed to deserialize response.");
3453
}
3554

3655
public async Task<IEnumerable<Update>> GetUpdatesAsync(CancellationToken cancellationToken = default)
3756
{
38-
var request = new RestRequest("updates");
39-
return (await this.ExecuteAsync<OdataWrapper<Update>>(request, cancellationToken)).Value;
57+
var response = await this.httpClient.GetAsync("updates", cancellationToken).ConfigureAwait(false);
58+
response.EnsureSuccessStatusCode();
59+
60+
var jsonContent = await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false);
61+
var wrapper = JsonSerializer.Deserialize<OdataWrapper<Update>>(jsonContent, this.jsonSerializerOptions)
62+
?? throw new InvalidOperationException("Failed to deserialize response.");
63+
64+
return wrapper.Value;
4065
}
4166

4267
public async Task<IEnumerable<Update>> GetUpdateByIdAsync(string id, CancellationToken cancellationToken = default)
4368
{
44-
var request = new RestRequest($"updates('{id}')");
45-
return (await this.ExecuteAsync<OdataWrapper<Update>>(request, cancellationToken)).Value;
69+
if (string.IsNullOrWhiteSpace(id))
70+
{
71+
throw new ArgumentException("ID cannot be null or whitespace.", nameof(id));
72+
}
73+
74+
var response = await this.httpClient.GetAsync($"updates/{id}", cancellationToken).ConfigureAwait(false);
75+
response.EnsureSuccessStatusCode();
76+
77+
var jsonContent = await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false);
78+
var wrapper = JsonSerializer.Deserialize<OdataWrapper<Update>>(jsonContent, this.jsonSerializerOptions)
79+
?? throw new InvalidOperationException("Failed to deserialize response.");
80+
81+
return wrapper.Value;
4682
}
4783

48-
private async Task<T> ExecuteAsync<T>(RestRequest request, CancellationToken cancellationToken = default)
49-
where T : new()
84+
private static JsonSerializerOptions CreateJsonSerializerOptions() => new()
5085
{
51-
T data;
52-
53-
var response = await this.client.ExecuteAsync<T>(request, cancellationToken).ConfigureAwait(false);
54-
if (!response.IsSuccessful)
55-
{
56-
throw new Exception(null);
57-
}
86+
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
87+
PropertyNameCaseInsensitive = true,
88+
};
5889

59-
data = response.ThrowIfError().Data!;
90+
private void ConfigureHttpClient()
91+
{
92+
var version = typeof(MicrosoftSecurityUpdatesClient).GetTypeInfo().Assembly.GetName().Version;
6093

61-
return data;
94+
this.httpClient.DefaultRequestHeaders.Add("Accept", "application/json");
95+
this.httpClient.DefaultRequestHeaders.Add("User-Agent", $"JamieMagee.MicrosoftSecurityUpdates.Client/{version}");
6296
}
6397
}

src/JamieMagee.MicrosoftSecurityUpdates.Client/Models/OdataWrapper.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
namespace JamieMagee.MicrosoftSecurityUpdates.Client.Models;
22

3-
internal record OdataWrapper<T>
3+
internal sealed record OdataWrapper<T>
44
where T : class
55
{
66
[JsonPropertyName("@odata.context")]
Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,2 @@
11
global using System.Text.Json.Serialization;
22
global using JamieMagee.MicrosoftSecurityUpdates.Schema;
3-
global using RestSharp;

0 commit comments

Comments
 (0)