Skip to content

Commit a4dad56

Browse files
authored
Wrapped service responses in an object making available the deserialized response content and metadata derived from response headers. (#53)
* Wrapped service responses in an object making available the deserialized response content and metadata derived from response headers. * Added the raw JSON response to the AuthorizedServiceResponse. * Added the raw headers to AuthorizedServiceResponse.
1 parent 9119ee0 commit a4dad56

File tree

15 files changed

+369
-99
lines changed

15 files changed

+369
-99
lines changed

README.md

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -296,7 +296,7 @@ To make a call to an authorized service, you first need to obtain an instance of
296296
If making a request where all information is provided via the path and querystring, such as GET requests, the following method should be invoked:
297297

298298
```csharp
299-
Task<Attempt<TResponse?>> SendRequestAsync<TResponse>(string serviceAlias, string path, HttpMethod httpMethod);
299+
Task<Attempt<AuthorizedServiceResponse<TResponse>>> SendRequestAsync<TResponse>(string serviceAlias, string path, HttpMethod httpMethod);
300300
```
301301

302302
The parameters for the request are as follows:
@@ -311,7 +311,7 @@ There is also a type parameter:
311311
If you need to provide data in the request, as is usually the case for POST or PUT requests that required the creation or update of a resource, an overload is available:
312312

313313
```csharp
314-
Task<Attempt<TResponse>> SendRequestAsync<TRequest, TResponse>(string serviceAlias, string path, HttpMethod httpMethod, TRequest? requestContent = null)
314+
Task<Attempt<AuthorizedServiceResponse<TResponse>>> SendRequestAsync<TRequest, TResponse>(string serviceAlias, string path, HttpMethod httpMethod, TRequest? requestContent = null)
315315
where TRequest : class;
316316
```
317317

@@ -326,18 +326,24 @@ And additional type parameter:
326326
If you need to work with the raw JSON response, there are equivalent methods for both of these that omit the deserialization step:
327327

328328
```csharp
329-
Task<Attempt<string?>> SendRequestRawAsync(string serviceAlias, string path, HttpMethod httpMethod);
329+
Task<Attempt<AuthorizedServiceResponse<string>>> SendRequestRawAsync(string serviceAlias, string path, HttpMethod httpMethod);
330330

331-
Task<<Attempt<string?>> SendRequestRawAsync<TRequest>(string serviceAlias, string path, HttpMethod httpMethod, TRequest? requestContent = null)
331+
Task<Attempt<AuthorizedServiceResponse<string>>> SendRequestRawAsync<TRequest>(string serviceAlias, string path, HttpMethod httpMethod, TRequest? requestContent = null)
332332
where TRequest : class;
333333
```
334334

335335
Finally, there are convenience extension methods available for each of the common HTTP verbs, allowing you to simplify the requests and omit the `HttpMethod` parameter, e.g.
336336

337337
```csharp
338-
Task<Attempt<TResponse?>> GetRequestAsync<TResponse>(string serviceAlias, string path);
338+
Task<Attempt<AuthorizedServiceResponse<TResponse>>> GetRequestAsync<TResponse>(string serviceAlias, string path);
339339
```
340340

341+
The response is received wrapped in an instance of `AuthorizedServiceResponse` which has three properties:
342+
343+
- `Data` - the response data deserialized into an instance of the provided `TResponse` type.
344+
- `Raw` - the raw JSON response string.
345+
- `Metadata` - various metadata from the service response, provided in headers and parsed into an instance of `ServiceResponseMetadata`.
346+
341347
## Providers
342348

343349
The list of providers for which the package has been verified is maintained at the [Umbraco Documentation website](https://docs.umbraco.com/umbraco-dxp/packages/authorized-services#verified-providers).
@@ -448,6 +454,10 @@ Switching the encryption engine to for example `AesSecretEncryptor` can be done
448454
builder.Services.AddUnique<ISecretEncryptor, AesSecretEncryptor>();
449455
```
450456

457+
#### IServiceResponseMetadataParser
458+
459+
Responsible for parsing header values from the response received when calling an authorized service into an instance of `ServiceResponseMetadata`.
460+
451461
#### ITokenFactory
452462

453463
Responsible for instantiating a new strongly typed `Token` instance from the service response. Implemented by `TokenFactory`.

examples/Umbraco.AuthorizedServices.TestSite/Controllers/HubspotContactsController.cs

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
using Microsoft.AspNetCore.Mvc;
22
using Umbraco.AuthorizedServices.Extensions;
3+
using Umbraco.AuthorizedServices.Models;
34
using Umbraco.AuthorizedServices.Services;
45
using Umbraco.AuthorizedServices.TestSite.Models.Dtos;
56
using Umbraco.AuthorizedServices.TestSite.Models.ServiceResponses;
@@ -21,15 +22,15 @@ public HubspotContactsController(IAuthorizedServiceCaller authorizedServiceCalle
2122
[HttpGet]
2223
public async Task<IActionResult> Get()
2324
{
24-
Attempt<HubspotContactResponse?> responseAttempt = await AuthorizedServiceCaller.GetRequestAsync<HubspotContactResponse>(
25+
Attempt<AuthorizedServiceResponse<HubspotContactResponse>> responseAttempt = await AuthorizedServiceCaller.GetRequestAsync<HubspotContactResponse>(
2526
ServiceAlias,
2627
BasePath);
2728
if (!responseAttempt.Success || responseAttempt.Result is null)
2829
{
2930
return HandleFailedRequest(responseAttempt.Exception, "Could not retrieve contacts.");
3031
}
3132

32-
HubspotContactResponse response = responseAttempt.Result;
33+
HubspotContactResponse response = responseAttempt.Result.Data!;
3334
return Ok(
3435
response.Results
3536
.Select(MapToDto)
@@ -40,7 +41,7 @@ public async Task<IActionResult> Get()
4041
[Route("{id}")]
4142
public async Task<IActionResult> Get(string id)
4243
{
43-
Attempt<HubspotContactResponse.Result?> responseAttempt = await AuthorizedServiceCaller.GetRequestAsync<HubspotContactResponse.Result>(
44+
Attempt<AuthorizedServiceResponse<HubspotContactResponse.Result>> responseAttempt = await AuthorizedServiceCaller.GetRequestAsync<HubspotContactResponse.Result>(
4445
ServiceAlias,
4546
$"{BasePath}{id}");
4647

@@ -49,14 +50,14 @@ public async Task<IActionResult> Get(string id)
4950
return HandleFailedRequest(responseAttempt.Exception, "Could not retrieve contact.");
5051
}
5152

52-
HubspotContactResponse.Result response = responseAttempt.Result;
53+
HubspotContactResponse.Result response = responseAttempt.Result.Data!;
5354
return Ok(MapToDto(response));
5455
}
5556

5657
[HttpPost]
5758
public async Task<IActionResult> Create([FromBody] ContactDto contact)
5859
{
59-
Attempt<HubspotContactResponse.Result?> responseAttempt = await AuthorizedServiceCaller.PostRequestAsync<HubspotContactResponse.Result, HubspotContactResponse.Result>(
60+
Attempt<AuthorizedServiceResponse<HubspotContactResponse.Result>> responseAttempt = await AuthorizedServiceCaller.PostRequestAsync<HubspotContactResponse.Result, HubspotContactResponse.Result>(
6061
ServiceAlias,
6162
BasePath,
6263
MapToRequest(contact));
@@ -65,14 +66,14 @@ public async Task<IActionResult> Create([FromBody] ContactDto contact)
6566
return HandleFailedRequest(responseAttempt.Exception, "Could not create contact.");
6667
}
6768

68-
HubspotContactResponse.Result response = responseAttempt.Result;
69+
HubspotContactResponse.Result response = responseAttempt.Result.Data!;
6970
return CreatedAtAction(nameof(Get), new { id = response.Id }, MapToDto(response));
7071
}
7172

7273
[HttpPut]
7374
public async Task<IActionResult> Update([FromBody] ContactDto contact)
7475
{
75-
Attempt<HubspotContactResponse.Result?> responseAttempt = await AuthorizedServiceCaller.PatchRequestAsync<HubspotContactResponse.Result, HubspotContactResponse.Result>(
76+
Attempt<AuthorizedServiceResponse<HubspotContactResponse.Result>> responseAttempt = await AuthorizedServiceCaller.PatchRequestAsync<HubspotContactResponse.Result, HubspotContactResponse.Result>(
7677
ServiceAlias,
7778
$"{BasePath}{contact.Id}",
7879
MapToRequest(contact));
@@ -81,7 +82,7 @@ public async Task<IActionResult> Update([FromBody] ContactDto contact)
8182
return HandleFailedRequest(responseAttempt.Exception, "Could not update contact.");
8283
}
8384

84-
HubspotContactResponse.Result response = responseAttempt.Result;
85+
HubspotContactResponse.Result response = responseAttempt.Result.Data!;
8586
return Ok(MapToDto(response));
8687
}
8788

examples/Umbraco.AuthorizedServices.TestSite/Controllers/TestAuthorizedServicesController.cs

Lines changed: 28 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
using Microsoft.AspNetCore.Mvc;
22
using Umbraco.AuthorizedServices.Extensions;
3+
using Umbraco.AuthorizedServices.Models;
34
using Umbraco.AuthorizedServices.Services;
45
using Umbraco.AuthorizedServices.TestSite.Models.ServiceResponses;
56
using Umbraco.Cms.Core;
@@ -15,29 +16,29 @@ public TestAuthorizedServicesController(IAuthorizedServiceCaller authorizedServi
1516

1617
public async Task<IActionResult> GetUmbracoContributorsFromGitHub()
1718
{
18-
Attempt<List<GitHubContributorResponse>?> responseAttempt = await AuthorizedServiceCaller.GetRequestAsync<List<GitHubContributorResponse>>(
19+
Attempt<AuthorizedServiceResponse<List<GitHubContributorResponse>>> responseAttempt = await AuthorizedServiceCaller.GetRequestAsync<List<GitHubContributorResponse>>(
1920
"github",
2021
"/repos/Umbraco/Umbraco-CMS/contributors");
2122
if (!responseAttempt.Success || responseAttempt.Result is null)
2223
{
2324
return HandleFailedRequest(responseAttempt.Exception, "Could not retrieve contributors.");
2425
}
2526

26-
List<GitHubContributorResponse> response = responseAttempt.Result;
27+
List<GitHubContributorResponse> response = responseAttempt.Result.Data!;
2728
return Content(string.Join(", ", response.Select(x => x.Login)));
2829
}
2930

3031
public async Task<IActionResult> GetContactsFromHubspot()
3132
{
32-
Attempt<HubspotContactResponse?> responseAttempt = await AuthorizedServiceCaller.GetRequestAsync<HubspotContactResponse>(
33+
Attempt<AuthorizedServiceResponse<HubspotContactResponse>> responseAttempt = await AuthorizedServiceCaller.GetRequestAsync<HubspotContactResponse>(
3334
"hubspot",
3435
"/crm/v3/objects/contacts?limit=10&archived=false");
3536
if (!responseAttempt.Success || responseAttempt.Result is null)
3637
{
3738
return HandleFailedRequest(responseAttempt.Exception, "Could not retrieve contacts.");
3839
}
3940

40-
HubspotContactResponse response = responseAttempt.Result;
41+
HubspotContactResponse response = responseAttempt.Result.Data!;
4142
return Content(
4243
string.Join(
4344
", ",
@@ -48,7 +49,7 @@ public async Task<IActionResult> GetContactsFromHubspot()
4849
public async Task<IActionResult> GetMeetupSelfUserInfo()
4950
{
5051
// This makes a GraphQL query
51-
Attempt<string?> responseAttempt = await AuthorizedServiceCaller.SendRequestRawAsync(
52+
Attempt<AuthorizedServiceResponse<string>> responseAttempt = await AuthorizedServiceCaller.SendRequestRawAsync(
5253
"meetup",
5354
"/gql",
5455
HttpMethod.Post,
@@ -62,13 +63,13 @@ public async Task<IActionResult> GetMeetupSelfUserInfo()
6263
return HandleFailedRequest(responseAttempt.Exception, "Could not retrieve user info.");
6364
}
6465

65-
var response = responseAttempt.Result;
66-
return Content(response);
66+
AuthorizedServiceResponse<string> response = responseAttempt.Result;
67+
return Content(response.Data ?? string.Empty);
6768
}
6869

6970
public async Task<IActionResult> GetFormsFromDynamics()
7071
{
71-
Attempt<DynamicsFormResponse?> responseAttempt = await AuthorizedServiceCaller.GetRequestAsync<DynamicsFormResponse>(
72+
Attempt<AuthorizedServiceResponse<DynamicsFormResponse>> responseAttempt = await AuthorizedServiceCaller.GetRequestAsync<DynamicsFormResponse>(
7273
"dynamics",
7374
"/msdyncrm_marketingforms");
7475

@@ -77,13 +78,13 @@ public async Task<IActionResult> GetFormsFromDynamics()
7778
return HandleFailedRequest(responseAttempt.Exception, "Could not retrieve forms.");
7879
}
7980

80-
DynamicsFormResponse response = responseAttempt.Result;
81+
DynamicsFormResponse response = responseAttempt.Result.Data!;
8182
return Content(string.Join(", ", response.Results.Select(x => x.Name)));
8283
}
8384

8485
public async Task<IActionResult> GetSearchResultsFromGoogle()
8586
{
86-
Attempt<string?> responseAttempt = await AuthorizedServiceCaller.SendRequestRawAsync(
87+
Attempt<AuthorizedServiceResponse<string>> responseAttempt = await AuthorizedServiceCaller.SendRequestRawAsync(
8788
"google",
8889
"/v1/urlInspection/index:inspect",
8990
HttpMethod.Post,
@@ -99,12 +100,12 @@ public async Task<IActionResult> GetSearchResultsFromGoogle()
99100
}
100101

101102
var response = responseAttempt.Result;
102-
return Content(response);
103+
return Content(response.Data ?? string.Empty);
103104
}
104105

105106
public async Task<IActionResult> GetFoldersFromDropbox()
106107
{
107-
Attempt<string?> responseAttempt = await AuthorizedServiceCaller.SendRequestRawAsync(
108+
Attempt<AuthorizedServiceResponse<string>> responseAttempt = await AuthorizedServiceCaller.SendRequestRawAsync(
108109
"dropbox",
109110
"/2/files/list_folder",
110111
HttpMethod.Post,
@@ -119,13 +120,13 @@ public async Task<IActionResult> GetFoldersFromDropbox()
119120
return HandleFailedRequest(responseAttempt.Exception, "Could not retrieve folders.");
120121
}
121122

122-
var response = responseAttempt.Result;
123-
return Content(response);
123+
var response = responseAttempt.Result.Data;
124+
return Content(response ?? string.Empty);
124125
}
125126

126127
public async Task<IActionResult> GetAssetsFromAssetBank(string assetIds)
127128
{
128-
Attempt<AssetBankSearchResponse?> responseAttempt = await AuthorizedServiceCaller.GetRequestAsync<AssetBankSearchResponse>(
129+
Attempt<AuthorizedServiceResponse<AssetBankSearchResponse>> responseAttempt = await AuthorizedServiceCaller.GetRequestAsync<AssetBankSearchResponse>(
129130
"assetBank",
130131
"/assetbank-rya-assets-test/rest/asset-search?assetIds=" + assetIds);
131132

@@ -134,13 +135,13 @@ public async Task<IActionResult> GetAssetsFromAssetBank(string assetIds)
134135
return HandleFailedRequest(responseAttempt.Exception, "Could not retrieve assets.");
135136
}
136137

137-
AssetBankSearchResponse response = responseAttempt.Result;
138+
AssetBankSearchResponse response = responseAttempt.Result.Data!;
138139
return Content(string.Join(", ", response.Select(x => x.ToString())));
139140
}
140141

141142
public async Task<IActionResult> GetVideoDetailsFromYouTube(string videoId)
142143
{
143-
Attempt<string?> responseAttempt = await AuthorizedServiceCaller.SendRequestRawAsync(
144+
Attempt<AuthorizedServiceResponse<string>> responseAttempt = await AuthorizedServiceCaller.SendRequestRawAsync(
144145
"youtube",
145146
$"/v3/videos?id={videoId}&part=snippet,contentDetails,statistics,status",
146147
HttpMethod.Get);
@@ -150,13 +151,13 @@ public async Task<IActionResult> GetVideoDetailsFromYouTube(string videoId)
150151
return HandleFailedRequest(responseAttempt.Exception, "Could not retrieve video details.");
151152
}
152153

153-
var response = responseAttempt.Result;
154-
return Content(response);
154+
var response = responseAttempt.Result.Data;
155+
return Content(response ?? string.Empty);
155156
}
156157

157158
public async Task<IActionResult> GetInstagramProfile()
158159
{
159-
Attempt<InstagramProfileResponse?> responseAttempt = await AuthorizedServiceCaller.GetRequestAsync<InstagramProfileResponse>(
160+
Attempt<AuthorizedServiceResponse<InstagramProfileResponse>> responseAttempt = await AuthorizedServiceCaller.GetRequestAsync<InstagramProfileResponse>(
160161
"instagram",
161162
$"/v3.0/me?fields=username");
162163

@@ -165,12 +166,12 @@ public async Task<IActionResult> GetInstagramProfile()
165166
return HandleFailedRequest(responseAttempt.Exception, "Could not retrieve account details.");
166167
}
167168

168-
return Content(responseAttempt.Result.Username);
169+
return Content(responseAttempt.Result.Data!.Username);
169170
}
170171

171172
public async Task<IActionResult> GetTwitterProfileUsingOAuth1()
172173
{
173-
Attempt<string?> responseAttempt = await AuthorizedServiceCaller.SendRequestRawAsync(
174+
Attempt<AuthorizedServiceResponse<string>> responseAttempt = await AuthorizedServiceCaller.SendRequestRawAsync(
174175
"twitter",
175176
"/1.1/account/settings.json",
176177
HttpMethod.Get);
@@ -180,13 +181,13 @@ public async Task<IActionResult> GetTwitterProfileUsingOAuth1()
180181
return HandleFailedRequest(responseAttempt.Exception, "Could not retrieve account details.");
181182
}
182183

183-
var response = responseAttempt.Result;
184-
return Content(response);
184+
var response = responseAttempt.Result.Data;
185+
return Content(response ?? string.Empty);
185186
}
186187

187188
public async Task<IActionResult> GetTwitterProfileUsingOAuth2()
188189
{
189-
Attempt<string?> responseAttempt = await AuthorizedServiceCaller.SendRequestRawAsync(
190+
Attempt<AuthorizedServiceResponse<string>> responseAttempt = await AuthorizedServiceCaller.SendRequestRawAsync(
190191
"twitter_oauth2",
191192
"/2/users/me",
192193
HttpMethod.Get);
@@ -196,8 +197,8 @@ public async Task<IActionResult> GetTwitterProfileUsingOAuth2()
196197
return HandleFailedRequest(responseAttempt.Exception, "Could not retrieve account details.");
197198
}
198199

199-
var response = responseAttempt.Result;
200-
return Content(response);
200+
var response = responseAttempt.Result.Data;
201+
return Content(response ?? string.Empty);
201202
}
202203

203204
public async Task<IActionResult> GetApiKey(string serviceAlias)

examples/Umbraco.AuthorizedServices.TestSite/Properties/launchSettings.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@
2323
"applicationUrl": "https://localhost:44344;http://localhost:63430",
2424
"environmentVariables": {
2525
"ASPNETCORE_ENVIRONMENT": "Development"
26-
}
26+
}
2727
}
2828
}
2929
}

src/Umbraco.AuthorizedServices/AuthorizedServicesComposer.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ private static void RegisterServices(IUmbracoBuilder builder)
4949
builder.Services.AddUnique<IAuthorizedRequestBuilder, AuthorizedRequestBuilder>();
5050

5151
builder.Services.AddUnique<IAuthorizedServiceCaller, AuthorizedServiceCaller>();
52+
builder.Services.AddUnique<IServiceResponseMetadataParser, ServiceResponseMetadataParser>();
5253
builder.Services.AddUnique<IDateTimeProvider, DateTimeProvider>();
5354
builder.Services.AddUnique<IRefreshTokenParametersBuilder, RefreshTokenParametersBuilder>();
5455

src/Umbraco.AuthorizedServices/Controllers/AuthorizedServiceController.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -151,10 +151,10 @@ public async Task<IActionResult> SendSampleRequest(string alias)
151151
{
152152
ServiceDetail serviceDetail = _serviceDetailOptions.Get(alias);
153153

154-
Attempt<string?> responseAttempt = await _authorizedServiceCaller.SendRequestRawAsync(alias, serviceDetail.SampleRequest ?? string.Empty, HttpMethod.Get);
154+
Attempt<AuthorizedServiceResponse<string>> responseAttempt = await _authorizedServiceCaller.SendRequestRawAsync(alias, serviceDetail.SampleRequest ?? string.Empty, HttpMethod.Get);
155155
if (responseAttempt.Success && responseAttempt.Result is not null)
156156
{
157-
return Ok(responseAttempt.Result);
157+
return Ok(responseAttempt.Result.Data ?? string.Empty);
158158
}
159159

160160
if (responseAttempt.Exception is not null)

0 commit comments

Comments
 (0)