Skip to content

Commit 42d7e31

Browse files
authored
Merge pull request #53 from joadan/copilot/get-http-response-message-options
Add builder-level tests for `ExecuteResponseAsync` response metadata
2 parents 15ff609 + 4e3c6ae commit 42d7e31

10 files changed

Lines changed: 609 additions & 11 deletions

File tree

src/Linq2OData.Core/Builders/CreateBuilder.cs

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1-
namespace Linq2OData.Core.Builders;
1+
using Linq2OData.Core.ODataResponse;
2+
3+
namespace Linq2OData.Core.Builders;
24

35

46

@@ -21,6 +23,15 @@ public async Task<T> ExecuteAsync(ODataInputBase input)
2123
return await odataClient.CreateEntityAsync<T>(entityPath, input);
2224
}
2325

26+
/// <summary>
27+
/// Executes the create request and returns the full <see cref="ODataResponse{T}"/> including
28+
/// HTTP status code and response headers.
29+
/// </summary>
30+
public async Task<ODataResponse<T>> ExecuteResponseAsync(ODataInputBase input)
31+
{
32+
return await odataClient.CreateEntityWithResponseAsync<T>(entityPath, input);
33+
}
34+
2435

2536

2637

src/Linq2OData.Core/Builders/DeleteBuilder.cs

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1-
namespace Linq2OData.Core.Builders;
1+
using Linq2OData.Core.ODataResponse;
2+
3+
namespace Linq2OData.Core.Builders;
24

35

46

@@ -23,6 +25,15 @@ public async Task<bool> ExecuteAsync()
2325
return await odataClient.DeleteEntityAsync(entityPath, keyExpression);
2426
}
2527

28+
/// <summary>
29+
/// Executes the delete request and returns the full <see cref="ODataResponse{T}"/> including
30+
/// HTTP status code and response headers.
31+
/// </summary>
32+
public async Task<ODataResponse<bool>> ExecuteResponseAsync()
33+
{
34+
return await odataClient.DeleteEntityWithResponseAsync(entityPath, keyExpression);
35+
}
36+
2637

2738

2839

src/Linq2OData.Core/Builders/GetBuilder.cs

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
using Linq2OData.Core.Expressions;
2+
using Linq2OData.Core.ODataResponse;
23
using System.Linq.Expressions;
3-
using System.Reflection.PortableExecutable;
4-
using static System.Net.WebRequestMethods;
54

65
namespace Linq2OData.Core.Builders;
76

@@ -39,6 +38,15 @@ public GetBuilder(ODataClient odataClient, Action<T> keySetter)
3938
return result.Data;
4039
}
4140

41+
/// <summary>
42+
/// Executes the request and returns the full <see cref="ODataResponse{T}"/> including
43+
/// HTTP status code and response headers.
44+
/// </summary>
45+
public async Task<ODataResponse<T>?> ExecuteResponseAsync(CancellationToken cancellationToken = default)
46+
{
47+
return await odataClient.QueryEntityAsync<T>(EntityPath, keyExpression, select, expand, cancellationToken);
48+
}
49+
4250
internal QueryNode MergeExpression<TResult>(Expression<Func<T, TResult>> selector)
4351
{
4452
var visitor = new QueryNodeVisitor();

src/Linq2OData.Core/Builders/OrderByBuilder.cs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
using Linq2OData.Core.Expressions;
2+
using Linq2OData.Core.ODataResponse;
23
using System;
34
using System.Linq.Expressions;
45

@@ -126,6 +127,15 @@ public QueryBuilder<TEntity> Expand(string? expand = null)
126127
return queryBuilder.ExecuteAsync(cancellationToken);
127128
}
128129

130+
/// <summary>
131+
/// Executes the query and returns the full <see cref="ODataResponse{T}"/> including
132+
/// HTTP status code and response headers.
133+
/// </summary>
134+
public Task<ODataResponse<List<TEntity>>> ExecuteResponseAsync(CancellationToken cancellationToken = default)
135+
{
136+
return queryBuilder.ExecuteResponseAsync(cancellationToken);
137+
}
138+
129139
/// <summary>
130140
/// Configures the select clause with a custom selector expression.
131141
/// </summary>

src/Linq2OData.Core/Builders/QueryBuilder.cs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
using Linq2OData.Core.Expressions;
2+
using Linq2OData.Core.ODataResponse;
23
using System.Linq.Expressions;
34

45
namespace Linq2OData.Core.Builders;
@@ -161,6 +162,15 @@ internal void AppendOrderBy(string propertyName, bool descending)
161162
return result?.Data;
162163
}
163164

165+
/// <summary>
166+
/// Executes the query and returns the full <see cref="ODataResponse{T}"/> including
167+
/// HTTP status code and response headers.
168+
/// </summary>
169+
public async Task<ODataResponse<List<T>>> ExecuteResponseAsync(CancellationToken cancellationToken = default)
170+
{
171+
return await odataClient.QueryEntitySetAsync<T>(EntityPath, select, expand, filter, count, top, skip, orderby, cancellationToken);
172+
}
173+
164174

165175

166176
public QueryProjectionBuilder<T, TResult> Select<TResult>(Expression<Func<List<T>, TResult>> selector)

src/Linq2OData.Core/Builders/SingletonBuilder.cs

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1-
namespace Linq2OData.Core.Builders;
1+
using Linq2OData.Core.ODataResponse;
2+
3+
namespace Linq2OData.Core.Builders;
24

35
public class SingletonBuilder<T> where T : new()
46
{
@@ -32,4 +34,13 @@ public SingletonBuilder<T> Select(string? select = null)
3234
if (result == null) { return default; }
3335
return result.Data;
3436
}
37+
38+
/// <summary>
39+
/// Executes the request and returns the full <see cref="ODataResponse{T}"/> including
40+
/// HTTP status code and response headers.
41+
/// </summary>
42+
public async Task<ODataResponse<T>?> ExecuteResponseAsync(CancellationToken cancellationToken = default)
43+
{
44+
return await odataClient.QueryEntityAsync<T>(singletonPath, string.Empty, select, expand, cancellationToken);
45+
}
3546
}

src/Linq2OData.Core/Builders/UpdateBuilder.cs

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1-
namespace Linq2OData.Core.Builders;
1+
using Linq2OData.Core.ODataResponse;
2+
3+
namespace Linq2OData.Core.Builders;
24

35

46

@@ -23,6 +25,15 @@ public async Task<bool> ExecuteAsync(ODataInputBase input)
2325
return await odataClient.UpdateEntityAsync(entityPath, keyExpression, input);
2426
}
2527

28+
/// <summary>
29+
/// Executes the update request and returns the full <see cref="ODataResponse{T}"/> including
30+
/// HTTP status code and response headers.
31+
/// </summary>
32+
public async Task<ODataResponse<bool>> ExecuteResponseAsync(ODataInputBase input)
33+
{
34+
return await odataClient.UpdateEntityWithResponseAsync(entityPath, keyExpression, input);
35+
}
36+
2637

2738

2839

src/Linq2OData.Core/ODataClient.cs

Lines changed: 91 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,19 @@ public async Task<bool> DeleteEntityAsync(string entitysetName, string keyExpres
6363
return true;
6464
}
6565

66+
public async Task<ODataResponse<bool>> DeleteEntityWithResponseAsync(string entitysetName, string keyExpression)
67+
{
68+
var response = await httpClient.DeleteAsync($"{entitysetName}({keyExpression})");
69+
var headers = CopyResponseHeaders(response);
70+
var statusCode = response.StatusCode;
71+
if (statusCode == System.Net.HttpStatusCode.NotFound)
72+
{
73+
return new ODataResponse<bool> { Data = false, StatusCode = statusCode, ResponseHeaders = headers };
74+
}
75+
await ValidateResponseAsync(response);
76+
return new ODataResponse<bool> { Data = true, StatusCode = statusCode, ResponseHeaders = headers };
77+
}
78+
6679
public async Task<T> CreateEntityAsync<T>(string entitysetName, ODataInputBase input)
6780
{
6881
string json = JsonSerializer.Serialize(input, jsonOptions);
@@ -82,6 +95,29 @@ public async Task<T> CreateEntityAsync<T>(string entitysetName, ODataInputBase i
8295
return odataResponse!.Data!;
8396
}
8497

98+
public async Task<ODataResponse<T>> CreateEntityWithResponseAsync<T>(string entitysetName, ODataInputBase input)
99+
{
100+
string json = JsonSerializer.Serialize(input, jsonOptions);
101+
var content = new StringContent(json, Encoding.UTF8, "application/json");
102+
var response = await httpClient.PostAsync($"{entitysetName}", content);
103+
104+
await ValidateResponseAsync(response);
105+
106+
var headers = CopyResponseHeaders(response);
107+
var statusCode = response.StatusCode;
108+
var rawResponse = await response.Content.ReadAsStringAsync();
109+
var odataResponse = ProcessQueryResponse<T>(rawResponse);
110+
111+
if (odataResponse == null || odataResponse.Data == null)
112+
{
113+
throw new Exception("Create entity reported success but the response is null.");
114+
}
115+
116+
odataResponse.StatusCode = statusCode;
117+
odataResponse.ResponseHeaders = headers;
118+
return odataResponse;
119+
}
120+
85121
public async Task InvokeActionAsync(string actionPath, object? parameters = null, CancellationToken token = default)
86122
{
87123
string json = parameters != null ? JsonSerializer.Serialize(parameters, jsonOptions) : "{}";
@@ -96,17 +132,27 @@ public async Task InvokeActionAsync(string actionPath, object? parameters = null
96132
var content = new StringContent(json, Encoding.UTF8, "application/json");
97133
var response = await httpClient.PostAsync(actionPath, content, token);
98134
await ValidateResponseAsync(response);
135+
var headers = CopyResponseHeaders(response);
136+
var statusCode = response.StatusCode;
99137
var rawResponse = await response.Content.ReadAsStringAsync(token);
100-
return ProcessQueryResponse<T>(rawResponse);
138+
var result = ProcessQueryResponse<T>(rawResponse);
139+
result.StatusCode = statusCode;
140+
result.ResponseHeaders = headers;
141+
return result;
101142
}
102143

103144
public async Task<ODataResponse<T>?> InvokeFunctionAsync<T>(string functionPath, CancellationToken token = default)
104145
{
105146
using var response = await httpClient.GetAsync(functionPath, token);
106147
if (response.StatusCode == System.Net.HttpStatusCode.NotFound) { return null; }
107148
await ValidateResponseAsync(response);
149+
var headers = CopyResponseHeaders(response);
150+
var statusCode = response.StatusCode;
108151
var rawResponse = await response.Content.ReadAsStringAsync(token);
109-
return ProcessQueryResponse<T>(rawResponse);
152+
var result = ProcessQueryResponse<T>(rawResponse);
153+
result.StatusCode = statusCode;
154+
result.ResponseHeaders = headers;
155+
return result;
110156
}
111157

112158
public async Task<bool> UpdateEntityAsync(string entitysetName, string keyExpression, ODataInputBase input)
@@ -126,6 +172,27 @@ public async Task<bool> UpdateEntityAsync(string entitysetName, string keyExpres
126172
return true;
127173
}
128174

175+
public async Task<ODataResponse<bool>> UpdateEntityWithResponseAsync(string entitysetName, string keyExpression, ODataInputBase input)
176+
{
177+
string json = JsonSerializer.Serialize(input, jsonOptions);
178+
var request = new HttpRequestMessage(HttpMethod.Patch, $"{entitysetName}({keyExpression})")
179+
{
180+
Content = new StringContent(json, Encoding.UTF8, "application/json")
181+
};
182+
183+
var response = await httpClient.SendAsync(request);
184+
var headers = CopyResponseHeaders(response);
185+
var statusCode = response.StatusCode;
186+
if (statusCode == System.Net.HttpStatusCode.NotFound)
187+
{
188+
return new ODataResponse<bool> { Data = false, StatusCode = statusCode, ResponseHeaders = headers };
189+
}
190+
191+
await ValidateResponseAsync(response);
192+
193+
return new ODataResponse<bool> { Data = true, StatusCode = statusCode, ResponseHeaders = headers };
194+
}
195+
129196

130197
public async Task<ODataResponse<List<T>>> QueryEntitySetAsync<T>(string entitySetName, string? select, string? expand = null, string? filter = null, bool? count = null, int? top = null, int? skip = null, string? orderby = null, CancellationToken token = default)
131198
{
@@ -138,8 +205,13 @@ public async Task<ODataResponse<List<T>>> QueryEntitySetAsync<T>(string entitySe
138205
await ValidateResponseAsync(response);
139206
}
140207

208+
var headers = CopyResponseHeaders(response);
209+
var statusCode = response.StatusCode;
141210
var rawResponse = await response.Content.ReadAsStringAsync(token);
142-
return ProcessQueryResponse<List<T>>(rawResponse);
211+
var result = ProcessQueryResponse<List<T>>(rawResponse);
212+
result.StatusCode = statusCode;
213+
result.ResponseHeaders = headers;
214+
return result;
143215
}
144216

145217

@@ -159,8 +231,13 @@ public async Task<ODataResponse<List<T>>> QueryEntitySetAsync<T>(string entitySe
159231
await ValidateResponseAsync(response);
160232
}
161233

234+
var headers = CopyResponseHeaders(response);
235+
var statusCode = response.StatusCode;
162236
var rawResponse = await response.Content.ReadAsStringAsync(token);
163-
return ProcessQueryResponse<T>(rawResponse);
237+
var result = ProcessQueryResponse<T>(rawResponse);
238+
result.StatusCode = statusCode;
239+
result.ResponseHeaders = headers;
240+
return result;
164241
}
165242

166243
private string GenerateUrl(string entitySetName, string? keyString = null, string? select = null, string? expand = null, string? filter = null, bool? count = null, int? top = null, int? skip = null, string? orderby = null)
@@ -392,6 +469,16 @@ private async Task ValidateResponseAsync(HttpResponseMessage response)
392469

393470
throw new ODataRequestException($"OData request failed. Status code:{(int)response.StatusCode}. Error:{odataError?.Message}", response.RequestMessage, odataError);
394471
}
472+
473+
private static IDictionary<string, IEnumerable<string>> CopyResponseHeaders(HttpResponseMessage response)
474+
{
475+
var headers = new Dictionary<string, IEnumerable<string>>(StringComparer.OrdinalIgnoreCase);
476+
foreach (var header in response.Headers)
477+
headers[header.Key] = header.Value.ToList();
478+
foreach (var header in response.Content.Headers)
479+
headers[header.Key] = header.Value.ToList();
480+
return headers;
481+
}
395482
}
396483

397484
}

src/Linq2OData.Core/ODataResponse/ODataResponse.cs

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
using System.Diagnostics;
1+
using System.Net;
22
using System.Text.Json.Serialization;
33

44
namespace Linq2OData.Core.ODataResponse;
@@ -11,5 +11,17 @@ public class ODataResponse<T>
1111
[JsonPropertyName("__count")]
1212
public long? Count { get; set; }
1313

14+
/// <summary>
15+
/// The HTTP status code returned by the server.
16+
/// Populated when using <c>ExecuteResponseAsync</c> or the lower-level <c>ODataClient</c> methods.
17+
/// </summary>
18+
public HttpStatusCode? StatusCode { get; set; }
19+
20+
/// <summary>
21+
/// Combined response and content headers returned by the server.
22+
/// Populated when using <c>ExecuteResponseAsync</c> or the lower-level <c>ODataClient</c> methods.
23+
/// Values are captured before the <see cref="HttpResponseMessage"/> is disposed.
24+
/// </summary>
25+
public IDictionary<string, IEnumerable<string>>? ResponseHeaders { get; set; }
1426
}
1527

0 commit comments

Comments
 (0)