1+ using System . Collections . Generic ;
12using System . Net . Http ;
23using System . Net . Http . Headers ;
34using System . Net . Http . Json ;
@@ -18,39 +19,29 @@ public DeployApiClient(IHttpClientFactory httpClientFactory)
1819 _httpClientFactory = httpClientFactory ;
1920 }
2021
21- private readonly JsonSerializerOptions _jsonOptions = new ( )
22- {
23- PropertyNameCaseInsensitive = true ,
24- PropertyNamingPolicy = JsonNamingPolicy . CamelCase ,
25- Converters = { new JsonStringEnumConverter ( JsonNamingPolicy . CamelCase , allowIntegerValues : true ) } ,
26- TypeInfoResolver = new DefaultJsonTypeInfoResolver ( )
27- } ;
28-
29- private readonly JsonSerializerOptions _jsonOptionsNoEnum = new ( )
30- {
31- PropertyNameCaseInsensitive = true ,
32- PropertyNamingPolicy = JsonNamingPolicy . CamelCase ,
33- TypeInfoResolver = new DefaultJsonTypeInfoResolver ( )
34- } ;
35-
3622 public Task < LoginResult > LoginAsync ( DeploySession session , string ? tokenName , CancellationToken cancellationToken = default )
3723 {
3824 return SendAsync < LoginResult > (
3925 session ,
4026 HttpMethod . Post ,
4127 "/cli/login" ,
42- new { name = tokenName } ,
43- cancellationToken ) ;
28+ new LoginRequest { Name = tokenName } ,
29+ GetTypeInfo < LoginResult > ( ) ,
30+ cancellationToken ,
31+ payloadTypeInfo : GetTypeInfo < LoginRequest > ( ) ) ;
4432 }
4533
46- public Task < IReadOnlyList < OrgSummary > > GetOrgsAsync ( DeploySession session , CancellationToken cancellationToken = default )
34+ public async Task < IReadOnlyList < OrgSummary > > GetOrgsAsync ( DeploySession session , CancellationToken cancellationToken = default )
4735 {
48- return SendAsync < IReadOnlyList < OrgSummary > > (
36+ var result = await SendAsync < List < OrgSummary > > (
4937 session ,
5038 HttpMethod . Get ,
5139 "/orgs" ,
5240 null ,
53- cancellationToken ) ;
41+ GetTypeInfo < List < OrgSummary > > ( ) ,
42+ cancellationToken ) . ConfigureAwait ( false ) ;
43+
44+ return result ;
5445 }
5546
5647 public Task < OrgSummary > CreateOrgAsync ( DeploySession session , string name , CancellationToken cancellationToken = default )
@@ -59,19 +50,24 @@ public Task<OrgSummary> CreateOrgAsync(DeploySession session, string name, Cance
5950 session ,
6051 HttpMethod . Post ,
6152 "/orgs" ,
62- new { name } ,
63- cancellationToken ) ;
53+ new CreateOrgRequest { Name = name } ,
54+ GetTypeInfo < OrgSummary > ( ) ,
55+ cancellationToken ,
56+ payloadTypeInfo : GetTypeInfo < CreateOrgRequest > ( ) ) ;
6457 }
6558
66- public Task < IReadOnlyList < ProjectSummary > > GetProjectsAsync ( DeploySession session , string orgName , CancellationToken cancellationToken = default )
59+ public async Task < IReadOnlyList < ProjectSummary > > GetProjectsAsync ( DeploySession session , string orgName , CancellationToken cancellationToken = default )
6760 {
6861 var org = EnsureOrg ( orgName ) ;
69- return SendAsync < IReadOnlyList < ProjectSummary > > (
62+ var result = await SendAsync < List < ProjectSummary > > (
7063 session ,
7164 HttpMethod . Get ,
7265 $ "/orgs/{ Segment ( org ) } /projects",
7366 null ,
74- cancellationToken ) ;
67+ GetTypeInfo < List < ProjectSummary > > ( ) ,
68+ cancellationToken ) . ConfigureAwait ( false ) ;
69+
70+ return result ;
7571 }
7672
7773 public Task < ProjectSummary > CreateProjectAsync ( DeploySession session , string orgName , string name , CancellationToken cancellationToken = default )
@@ -81,8 +77,10 @@ public Task<ProjectSummary> CreateProjectAsync(DeploySession session, string org
8177 session ,
8278 HttpMethod . Post ,
8379 $ "/orgs/{ Segment ( org ) } /projects",
84- new { name } ,
85- cancellationToken ) ;
80+ new CreateProjectRequest { Name = name } ,
81+ GetTypeInfo < ProjectSummary > ( ) ,
82+ cancellationToken ,
83+ payloadTypeInfo : GetTypeInfo < CreateProjectRequest > ( ) ) ;
8684 }
8785
8886 public Task < DeploymentCreated > CreateDeploymentAsync (
@@ -100,16 +98,18 @@ public Task<DeploymentCreated> CreateDeploymentAsync(
10098 session ,
10199 HttpMethod . Post ,
102100 "/cli/deploy" ,
103- new
101+ new CreateDeploymentRequest
104102 {
105- orgName = org ,
106- projectName = project ,
107- environment ,
108- branch ,
109- commitSha ,
110- artifactLocation
103+ OrgName = org ,
104+ ProjectName = project ,
105+ Environment = environment ,
106+ Branch = branch ,
107+ CommitSha = commitSha ,
108+ ArtifactLocation = artifactLocation
111109 } ,
112- cancellationToken ) ;
110+ GetTypeInfo < DeploymentCreated > ( ) ,
111+ cancellationToken ,
112+ payloadTypeInfo : GetTypeInfo < CreateDeploymentRequest > ( ) ) ;
113113 }
114114
115115 public async Task < UploadedArtifact > UploadArtifactAsync (
@@ -166,7 +166,7 @@ public async Task<UploadedArtifact> UploadArtifactAsync(
166166 }
167167
168168 await using var responseStream = await response . Content . ReadAsStreamAsync ( cancellationToken ) . ConfigureAwait ( false ) ;
169- var result = await JsonSerializer . DeserializeAsync < UploadedArtifact > ( responseStream , _jsonOptions , cancellationToken ) . ConfigureAwait ( false ) ;
169+ var result = await JsonSerializer . DeserializeAsync ( responseStream , GetTypeInfo < UploadedArtifact > ( ) , cancellationToken ) . ConfigureAwait ( false ) ;
170170
171171 if ( result is null )
172172 {
@@ -183,6 +183,7 @@ public Task<DeploymentLogInfo> GetLogsAsync(DeploySession session, Guid deployme
183183 HttpMethod . Get ,
184184 $ "/cli/logs/{ deploymentId } ",
185185 null ,
186+ GetTypeInfo < DeploymentLogInfo > ( ) ,
186187 cancellationToken ) ;
187188 }
188189
@@ -195,6 +196,7 @@ public Task<EnvConfigResult> GetEnvironmentAsync(DeploySession session, string o
195196 HttpMethod . Get ,
196197 $ "/cli/env/{ Segment ( org ) } /{ Segment ( project ) } /{ envSegment } ",
197198 null ,
199+ GetTypeInfo < EnvConfigResult > ( ) ,
198200 cancellationToken ) ;
199201 }
200202
@@ -214,13 +216,13 @@ public async Task UpsertConfigAsync(
214216 {
215217 var ( org , project ) = EnsureNames ( orgName , projectName ) ;
216218 var envSegment = environment . ToString ( ) ;
217- await SendAsync < object > (
219+ await SendAsync (
218220 session ,
219221 HttpMethod . Post ,
220222 $ "/orgs/{ Segment ( org ) } /projects/{ Segment ( project ) } /config/{ envSegment } ",
221223 request ,
222224 cancellationToken ,
223- useEnumStrings : false ) . ConfigureAwait ( false ) ;
225+ payloadTypeInfo : GetTypeInfo < ConfigUpsertRequest > ( useEnumStrings : false ) ) . ConfigureAwait ( false ) ;
224226 }
225227
226228 public Task < RollbackResult > RollbackAsync ( DeploySession session , string orgName , string projectName , Guid targetDeploymentId , CancellationToken cancellationToken = default )
@@ -230,13 +232,15 @@ public Task<RollbackResult> RollbackAsync(DeploySession session, string orgName,
230232 session ,
231233 HttpMethod . Post ,
232234 "/cli/rollback" ,
233- new
235+ new RollbackRequest
234236 {
235- orgName = org ,
236- projectName = project ,
237- targetDeploymentId
237+ OrgName = org ,
238+ ProjectName = project ,
239+ TargetDeploymentId = targetDeploymentId
238240 } ,
239- cancellationToken ) ;
241+ GetTypeInfo < RollbackResult > ( ) ,
242+ cancellationToken ,
243+ payloadTypeInfo : GetTypeInfo < RollbackRequest > ( ) ) ;
240244 }
241245
242246 public async Task < RegisterResult > RegisterUserAsync ( string apiBaseUrl , string email , string password , CancellationToken cancellationToken = default )
@@ -250,9 +254,11 @@ public async Task<RegisterResult> RegisterUserAsync(string apiBaseUrl, string em
250254 new DeploySession ( apiBaseUrl , string . Empty ) , // no auth header needed for register
251255 HttpMethod . Post ,
252256 "/users/register" ,
253- new { email , password } ,
254- cancellationToken ,
255- includeUserHeader : false ) . ConfigureAwait ( false ) ;
257+ new RegisterUserRequest { Email = email , Password = password } ,
258+ responseTypeInfo : GetTypeInfo < RegisterResult > ( ) ,
259+ cancellationToken : cancellationToken ,
260+ includeUserHeader : false ,
261+ payloadTypeInfo : GetTypeInfo < RegisterUserRequest > ( ) ) . ConfigureAwait ( false ) ;
256262 }
257263
258264 private async Task < IReadOnlyList < DomainInfo > > SendDomainsAsync ( DeploySession session , string orgName , string projectName , CancellationToken cancellationToken )
@@ -262,12 +268,21 @@ private async Task<IReadOnlyList<DomainInfo>> SendDomainsAsync(DeploySession ses
262268 HttpMethod . Get ,
263269 $ "/cli/domains/{ Segment ( orgName ) } /{ Segment ( projectName ) } ",
264270 null ,
271+ GetTypeInfo < List < DomainInfo > > ( ) ,
265272 cancellationToken ) . ConfigureAwait ( false ) ;
266273
267274 return domains ;
268275 }
269276
270- private async Task < T > SendAsync < T > ( DeploySession session , HttpMethod method , string path , object ? payload , CancellationToken cancellationToken , bool includeUserHeader = true , bool useEnumStrings = true )
277+ private async Task < T > SendAsync < T > (
278+ DeploySession session ,
279+ HttpMethod method ,
280+ string path ,
281+ object ? payload ,
282+ JsonTypeInfo < T > responseTypeInfo ,
283+ CancellationToken cancellationToken ,
284+ bool includeUserHeader = true ,
285+ JsonTypeInfo ? payloadTypeInfo = null )
271286 {
272287 var baseUri = BuildBaseUri ( session . ApiBaseUrl ) ;
273288 var requestUri = new Uri ( baseUri , path ) ;
@@ -280,8 +295,12 @@ private async Task<T> SendAsync<T>(DeploySession session, HttpMethod method, str
280295
281296 if ( payload is not null )
282297 {
283- var opts = useEnumStrings ? _jsonOptions : _jsonOptionsNoEnum ;
284- request . Content = JsonContent . Create ( payload , options : opts ) ;
298+ if ( payloadTypeInfo is null )
299+ {
300+ throw new InvalidOperationException ( $ "No JSON type info registered for payload type '{ payload . GetType ( ) . Name } '.") ;
301+ }
302+
303+ request . Content = JsonContent . Create ( payload , payloadTypeInfo ) ;
285304 }
286305
287306 using var client = _httpClientFactory . CreateClient ( HttpClientNames . Deploy ) ;
@@ -305,7 +324,7 @@ private async Task<T> SendAsync<T>(DeploySession session, HttpMethod method, str
305324 }
306325
307326 await using var stream = await response . Content . ReadAsStreamAsync ( cancellationToken ) . ConfigureAwait ( false ) ;
308- var result = await JsonSerializer . DeserializeAsync < T > ( stream , _jsonOptions , cancellationToken ) . ConfigureAwait ( false ) ;
327+ var result = await JsonSerializer . DeserializeAsync ( stream , responseTypeInfo , cancellationToken ) . ConfigureAwait ( false ) ;
309328
310329 if ( result is null )
311330 {
@@ -315,6 +334,50 @@ private async Task<T> SendAsync<T>(DeploySession session, HttpMethod method, str
315334 return result ;
316335 }
317336
337+ private async Task SendAsync (
338+ DeploySession session ,
339+ HttpMethod method ,
340+ string path ,
341+ object ? payload ,
342+ CancellationToken cancellationToken ,
343+ bool includeUserHeader = true ,
344+ JsonTypeInfo ? payloadTypeInfo = null )
345+ {
346+ var baseUri = BuildBaseUri ( session . ApiBaseUrl ) ;
347+ var requestUri = new Uri ( baseUri , path ) ;
348+
349+ using var request = new HttpRequestMessage ( method , requestUri ) ;
350+ if ( includeUserHeader )
351+ {
352+ request . Headers . Add ( "X-User-Email" , session . UserEmail ) ;
353+ }
354+
355+ if ( payload is not null )
356+ {
357+ if ( payloadTypeInfo is null )
358+ {
359+ throw new InvalidOperationException ( $ "No JSON type info registered for payload type '{ payload . GetType ( ) . Name } '.") ;
360+ }
361+
362+ request . Content = JsonContent . Create ( payload , payloadTypeInfo ) ;
363+ }
364+
365+ using var client = _httpClientFactory . CreateClient ( HttpClientNames . Deploy ) ;
366+ using var response = await client . SendAsync ( request , HttpCompletionOption . ResponseHeadersRead , cancellationToken ) . ConfigureAwait ( false ) ;
367+
368+ if ( ! response . IsSuccessStatusCode )
369+ {
370+ var detail = await SafeReadAsync ( response , cancellationToken ) . ConfigureAwait ( false ) ;
371+ var message = $ "API request failed ({ ( int ) response . StatusCode } { response . ReasonPhrase } ).";
372+ if ( ! string . IsNullOrWhiteSpace ( detail ) )
373+ {
374+ message += $ " { detail } ";
375+ }
376+
377+ throw new InvalidOperationException ( message ) ;
378+ }
379+ }
380+
318381 private static string EnsureOrg ( string orgName )
319382 {
320383 if ( string . IsNullOrWhiteSpace ( orgName ) )
@@ -347,6 +410,13 @@ private static Uri BuildBaseUri(string baseUrl)
347410 return uri ;
348411 }
349412
413+ private static JsonTypeInfo < T > GetTypeInfo < T > ( bool useEnumStrings = true )
414+ {
415+ var context = useEnumStrings ? ( JsonSerializerContext ) DeployJsonContext . Default : DeployJsonNumericContext . Default ;
416+ return ( JsonTypeInfo < T > ? ) context . GetTypeInfo ( typeof ( T ) )
417+ ?? throw new InvalidOperationException ( $ "No JSON type info registered for '{ typeof ( T ) . Name } '.") ;
418+ }
419+
350420 private static async Task < string > SafeReadAsync ( HttpResponseMessage response , CancellationToken cancellationToken )
351421 {
352422 try
@@ -358,4 +428,5 @@ private static async Task<string> SafeReadAsync(HttpResponseMessage response, Ca
358428 return string . Empty ;
359429 }
360430 }
431+
361432}
0 commit comments