Use the codes generated by OpenApiClientGen in real world applications: handling authentication and non-200 HTTP statuses. And build comprehensive integration test suites.
Background
As an application developer consuming JSON based Web APIs, you have been using some Swagger / OpenAPI tools (https://openapi.tools/) to generate client API codes if the service vendor does not provided a client API library but an OpenAPI definition file.
As the Web API and the respective OpenAPI definition files may evolve overtime, you need to update your client programs.
"Strongly Typed OpenAPI Client Generators" (OpenApiClientGen) is optimized for C# clients and TypeScript clients targeting jQuery, Angular 2+, Aurelia, AXIOS and Fetch API.
And this article is a complementary to the following articles:
Hopefully, such complementary may assist you to design clean and maintainable architecture of your real world solutions regarding security, load balancing, User Experience and Developer Experience.
Introduction
"Strongly Typed OpenAPI Client Generators" (OpenApiClientGen) has the following intentional limitations and by design by specification:
- Focus on strongly typed data models.
- For HTTP responses of API operations, generate codes only for
"HTTP Status Code"=200
and "Content Type"="application/json
". - Skip definitions of HTTP headers of operation parameters.
- Skip definitions of Authentication and Authorization.
This article introduces a few intended solutions for these intentional limitations for the sake of writing clean application codes of complex business applications, with complex data models and workflows.
If the Web API or respective Swagger / OpenAPI definition is with very dynamic, versatile or weak-typed data, or the API operations support only text/xml, forms and binary payload, OpenApiClientGen
is not for you, while there are plenty of Swagger / OpenAPI client codgen tools (https://tools.openapis.org/) that can handle such scenarios.
Comparing with other tools, OpenApiClientGen
provides the following benefits to application programming:
- Strongly typed data matching to the server side data binding as much as possible. Since the initial release in 2020, it has supported decimal/currency/monetary data types, while Swagger / OpenAPI specifications prior to v3.1 had not provided inherent support for such data types.
- The footprints of generated source codes and the built images are much smaller.
- For TypeScript programmers, the generated codes satisfy TypeScript strict mode and Angular strict mode.
- For TypeScript programmers, generated HTTP client operations utilize what HTTP client request classes you have been using:
jQuery.ajax
of jQuery, HttpClient
of Angular 2+, HttpClient
of Aurelia, AXIOS and Fetch API. - For Angular programmers, the
NG2FormGroup
plugin can generate strictly typed forms codes with validators.
And the intended limitations won't compromise comprehensive error handling needed in real world applications, for the sake of robust error handling, technical logging and User Experience, while the Internet is inherently unreliable.
Using the Code
The following TypeScript and C# code examples are to describe conceptual ideas of application programming and how the design intent of OpenApiClientGen
may assist you to write clean application codes. For concrete solutions, you might still need to further study and refer to many well-written tutorial articles by the other more experienced programmers.
For Authentication
And it is presumed that you are familiar with HTTP interception in application programming for authentication.
Bearer Token
In this section about handling bearer token, an extended "Tour of Heroes" of Angular demonstrate how to centralize the handling of bearer token for each request made by the client API codes.
Generated Angular TypeScript Codes
@Injectable()
export class Heroes {
constructor(@Inject('baseUri') private baseUri:
string = window.location.protocol + '//' + window.location.hostname +
(window.location.port ? ':' + window.location.port : '') + '/',
private http: HttpClient) {
}
getHero(id?: number | null, headersHandler?: () =>
HttpHeaders): Observable<DemoWebApi_Controllers_Client.Hero | null> {
return this.http.get<DemoWebApi_Controllers_Client.Hero | null>
(this.baseUri + 'api/Heroes/' + id,
{ headers: headersHandler ? headersHandler() : undefined });
}
post(name?: string | null, headersHandler?: () => HttpHeaders):
Observable<DemoWebApi_Controllers_Client.Hero> {
return this.http.post<DemoWebApi_Controllers_Client.Hero>
(this.baseUri + 'api/Heroes', JSON.stringify(name),
{ headers: headersHandler ? headersHandler().append
('Content-Type', 'application/json;charset=UTF-8') :
new HttpHeaders({ 'Content-Type':
'application/json;charset=UTF-8' }) });
}
...
export interface Hero {
address?: DemoWebApi_DemoData_Client.Address;
death?: Date | null;
dob?: Date | null;
emailAddress?: string | null;
id?: number | null;
name?: string | null;
phoneNumbers?: Array<DemoWebApi_DemoData_Client.PhoneNumber>;
webAddress?: string | null;
}
export interface HeroFormProperties {
death: FormControl<Date | null | undefined>,
dob: FormControl<Date | null | undefined>,
emailAddress: FormControl<string | null | undefined>,
id: FormControl<number | null | undefined>,
name: FormControl<string | null | undefined>,
webAddress: FormControl<string | null | undefined>,
}
export function CreateHeroFormGroup() {
return new FormGroup<HeroFormProperties>({
death: new FormControl<Date | null | undefined>(undefined),
dob: new FormControl<Date | null | undefined>(undefined),
emailAddress: new FormControl<string | null | undefined>
(undefined, [Validators.email]),
id: new FormControl<number | null | undefined>(undefined),
name: new FormControl<string | null | undefined>
(undefined, [Validators.required,
Validators.maxLength(120), Validators.minLength(2)]),
webAddress: new FormControl<string | null | undefined>
(undefined, [Validators.minLength(6),
Validators.pattern('https?:\\/\\/(www\\.)?
[-a-zA-Z0-9@:%._\\+~#=]{1,256}\\.[a-zA-Z0-9()]{1,6}\\b
([-a-zA-Z0-9()@:%_\\+.~#?&//=]*)')]),
});
}
HTTP Interceptor
Major JavaScript/TypeScript frameworks like Angular and Aurelia have provided built-in way of HTTP interception so you can have a wholesale and predefined way of creating a HTTP client instance with updated credential. Though other JavaScript libraries like React or frameworks like VueJs do not provide built-in HTTP classes nor HTTP interception, it shouldn't be hard for you to craft one with the generated APIs utilizing AXIOS or Fetch
API, as many tutorials online are available.
In tokeninterceptor.ts:
@Injectable()
export class TokenInterceptor implements HttpInterceptor {
constructor(@Inject('BACKEND_URLS') private backendUrls: string[],
@Inject('IAuthService') private authService: IAuthService) {
console.debug('TokenInterceptor created.');
}
intercept(request: HttpRequest<any>, httpHandler: HttpHandler):
Observable<HttpEvent<any>> {
var requestNeedsInterceptor = this.backendUrls.find
(d => request.url.indexOf(d) >= 0);
if (!requestNeedsInterceptor) {
return httpHandler.handle(request);
}
let refinedRequest: HttpRequest<any>;
if (AUTH_STATUSES.access_token) {
refinedRequest = request.clone({
setHeaders: {
Authorization: `Bearer ${AUTH_STATUSES.access_token}`,
Accept: 'application/json,text/html;q=0.9,*/*;q=0.8',
}
});
} else {
refinedRequest = request.clone({
setHeaders: {
Accept: 'application/json,text/html;q=0.9,*/*;q=0.8',
}
});
}
return httpHandler.handle(refinedRequest).pipe(catchError(err => {
if ([401, 403].includes(err.status)) {
console.debug('401 403');
if (AUTH_STATUSES.refreshToken) {
return AuthFunctions.getNewAuthToken
(this.authService, refinedRequest, httpHandler);
}
}
return Promise.reject(err);
}));
}
...
Typically, you would provide the generated API DemoWebApi_Controllers_Client.Heroes
and the TokenInterceptor
in httpServices.module.ts:
export function clientFactory(http: HttpClient) {
return new namespaces.DemoWebApi_Controllers_Client.Heroes
(SiteConfigConstants.apiBaseuri, http);
}
@NgModule({})
export class HttpServicesModule {
static forRoot(): ModuleWithProviders<HttpServicesModule> {
return {
ngModule: HttpServicesModule,
providers: [
{
provide: HTTP_INTERCEPTORS,
useClass: TokenInterceptor,
multi: true
},
{
provide: DemoWebApi_Controllers_Client.Heroes,
useFactory: clientFactory,
deps: [HttpClient],
},
Application Codes
In hero-detail.component.ts:
@Component({
selector: 'app-hero-detail',
templateUrl: './hero-detail.component.html'
})
export class HeroDetailComponent implements OnInit {
hero?: DemoWebApi_Controllers_Client.Hero;
heroForm: FormGroup<HeroWithNestedFormProperties>;
constructor(
private heroService: DemoWebApi_Controllers_Client.Heroes,
...
) {
this.heroForm = CreateHeroWithNestedFormGroup();
}
save(): void {
const raw: DemoWebApi_Controllers_Client.Hero =
this.heroForm.getRawValue();
this.heroService.put(raw).subscribe(
{
next: d => {
...
},
error: error => alert(error)
}
);
}
In your application codes, you don't see explicit codes of handing authentication, since authentication headers are handled by the provided TokenInterceptor
.
PRODA and Medicare Online
Medicare Online is an Australian federal government's Web service launched in 2021 for Medicare, replacing the legacy adaptor way. It uses PRODA for basic authentication and authorization. This section is not intended to become a programming tutorial for Medicare Online or PRODA, but to inspire you for handling similar scenarios of complex key exchanges and dynamic header info in your application codes.
Remarks
- It had taken me days to understand and setup a PRODA instance for my company as a client vendor, while PRODA itself had kept evolving before the official launch. Therefore, don't expect yourself to understand every technical details here, but seek inspiration for your own similar scenarios.
YAML for Bulk Bill Store Forward
parameters:
Authorization:
name: Authorization
type: string
required: true
in: header
description: JWT header for authorization
default: Bearer REPLACE_THIS_KEY
dhs-auditId:
name: dhs-auditId
type: string
required: true
in: header
description: DHS Audit ID
default: LOC00001
dhs-subjectId:
name: dhs-subjectId
type: string
required: true
in: header
description: DHS Subject ID
default: '2509999891'
...
...
/mcp/bulkbillstoreforward/specialist/v1:
post:
summary: This is the request
parameters:
- name: body
required: true
in: body
schema:
$ref: '#/definitions/BulkBillStoreForwardRequestType'
responses:
"200":
description: successful operation
schema:
$ref: '#/definitions/BulkBillStoreForwardResponseType'
"400":
description: server cannot or will not process the request
schema:
$ref: '#/definitions/ServiceMessagesType'
operationId: mcp-bulk-bill-store-forward-specialist@1-eigw
parameters:
- $ref: '#/parameters/Authorization'
- $ref: '#/parameters/dhs-auditId'
- $ref: '#/parameters/dhs-subjectId'
- $ref: '#/parameters/dhs-messageId'
- $ref: '#/parameters/dhs-auditIdType'
- $ref: '#/parameters/dhs-correlationId'
- $ref: '#/parameters/dhs-productId'
- $ref: '#/parameters/dhs-subjectIdType'
Generated C# Client API Codes
public async Task<BulkBillStoreForwardResponseType> BulkBillStoreForwardSpecialistAsync
(BulkBillStoreForwardRequestType requestBody,
Action<System.Net.Http.Headers.HttpRequestHeaders> handleHeaders = null)
{
var requestUri = "mcp/bulkbillstoreforward/specialist/v1";
using (var httpRequestMessage = new System.Net.Http.HttpRequestMessage
(System.Net.Http.HttpMethod.Post, requestUri))
{
using (var requestWriter = new System.IO.StringWriter())
{
var requestSerializer = JsonSerializer.Create(jsonSerializerSettings);
requestSerializer.Serialize(requestWriter, requestBody);
var content = new System.Net.Http.StringContent
(requestWriter.ToString(), System.Text.Encoding.UTF8, "application/json");
httpRequestMessage.Content = content;
if (handleHeaders != null)
{
handleHeaders(httpRequestMessage.Headers);
}
var responseMessage = await httpClient.SendAsync(httpRequestMessage);
try
{
responseMessage.EnsureSuccessStatusCodeEx();
var responseMessageStream = await responseMessage.Content.ReadAsStreamAsync();
using (JsonReader jsonReader = new JsonTextReader
(new System.IO.StreamReader(responseMessageStream)))
{
var serializer = JsonSerializer.Create(jsonSerializerSettings);
return serializer.Deserialize<BulkBillStoreForwardResponseType>(jsonReader);
}
}
finally
{
responseMessage.Dispose();
}
}
}
}
As you can see, the generated codes skips the definitions of header parameters, and take care of only the business data payloads.
Application Codes
BulkBillStoreForwardRequestType requestBody;
...
try
{
var response = await NewClientWithNewIds(ids).BulkBillStoreForwardSpecialistAsync
(requestBody, (headers) =>
{
headers.TryAddWithoutValidation("dhs-subjectId", medicareNumber);
headers.TryAddWithoutValidation("dhs-subjectIdType", "Medicare Card");
});
...
}
catch (Fonlow.Net.Http.WebApiRequestException ex)
{
HandleBadRequest(ids.TraceIdentity, ex);
throw;
}
The generated API codes provide a custom headers handler, so you can adjust the HTTP headers for your app logic according to the published Medicare Web API manual. And in this case, you can easily provide a Medicare card number.
Wholesale Handling of Authentication
If a Web service is designed properly, a set of Web API functions should share the same way of providing credential as well as info of load balancing to the backend.
NewClientWithNewIds()
encapsulates helper functions and factories for handing auth info on wholesale.
...
override protected McpClient NewClientWithNewIds(McpTransactionIds ids)
{
var client = NewHttpClientClientWithNewIds(ids, "mcp");
return new McpClient(client, HttpClientSerializerSettings.Create());
}
override protected McpClient NewClientWithCorrelationId
(McpIdentity ids, string correlationId)
{
var client = NewHttpClientWithCorrelationId(ids, correlationId, "mcp");
return new McpClient(client, HttpClientSerializerSettings.Create());
}
protected HttpClient NewHttpClientWithCorrelationId
(McpIdentity ids, string correlationId, string httpClientName)
{
var client = httpClientFactory.CreateClient(httpClientName);
client.DefaultRequestHeaders.TryAddWithoutValidation("orgId", ids.OrgId);
client.DefaultRequestHeaders.TryAddWithoutValidation
("dhs-correlationId", correlationId);
client.DefaultRequestHeaders.TryAddWithoutValidation
("proda-clientId", ids.ProdaClientId);
return client;
}
A typical HTTP request is like this:
POST https://healthclaiming.api.humanservices.gov.au/claiming/ext-prd/mcp/patientverification/medicare/v1 HTTP/1.1
Host: healthclaiming.api.humanservices.gov.au
dhs-subjectId: 4253263768
dhs-subjectIdType: Medicare Card Number
Accept: application/json
orgId: 7125128193
dhs-correlationId: urn:uuid:MLK412708F2C7DDCE2598138
Authorization: Bearer eyJraWQiOiJCb...elkFglNxQ
dhs-productId: My company healthcare product 1.0
dhs-auditId: KKK41270
X-IBM-Client-Id: d58ab88eac7d983adb24ef00519faf61
dhs-auditIdType: Location Id
dhs-messageId: urn:uuid:3b39c9a6-c220-458c-bd5d-32dd47ec7738
traceparent: 00-fdc2b432189e291b7eb537c3a4777c4d-b0617b3e1b9fafcb-00
Content-Type: application/json; charset=utf-8
Content-Length: 275
{"patient":{"medicare":{"memberNumber":"4453263778","memberRefNumber":"2"},
"identity":{"dateOfBirth":"1980-10-28","familyName":"Bean","givenName":"Tom","sex":"1"},
"residentialAddress":{"locality":"Warner","postcode":"4500"}},
"dateOfService":"2022-01-23","typeCode":"PVM"}
For Non-200 HTTP Responses
Application Codes
try
{
var response = await NewClientWithNewIds(ids).BulkBillStoreForwardSpecialistAsync
(requestBody, (headers) =>
{
headers.TryAddWithoutValidation("dhs-subjectId", medicareNumber);
headers.TryAddWithoutValidation("dhs-subjectIdType", "Medicare Card");
});
...
}
catch (Fonlow.Net.Http.WebApiRequestException ex)
{
HandleBadRequest(ids.TraceIdentity, ex);
throw;
}
Upon non-2xx HTTP responses, the generated client API function may throw System.Net.Http.HttpRequestException
or Fonlow.Net.Http.WebApiRequestException
which exposes more response info for detailed error handling.
Points of Interest
While the integration test suites of OpenApiClientGen
have covered 2,000 Swagger / OpenAPI definition files with over 5,000 test cases, I have inspected only a few dozen definition files that contain authentication meta. I doubt whether Swagger / OpenAPI v2, v3, vX could possibly cover wide variety of "standard" authentication mechanisms, leaving alone special behaviours of each implementation on the service side, therefore I doubt whether a code generator could generate practically useful auth codes that could be used directly in application programming. It is better-off to let application programmer write the auth codes, as long as the overall design makes each client API calls in the app code look simple and clean.
Those Swagger / OpenAPI Definitions Unfriendly to C# and TypeScript
There are at least a few dozen popular programming languages for developing Web API. Backend programmers may have declared identifiers of the service side unfriendly to C# and TypeScript on the client side:
- Conflict or collision against reserved words of C# and TypeScript
- Versatile literal string for
enum
- Names inappropriate for type name, parameters and function names, etc.
- ...
OpenApiClientGen
renames those identifiers without impacting the request payload and the handling of the typed responses.
Hints
- Handling
enum
is rather tricky. ASP.NET (Core) Web API by default can handle enum
as string
or as integer. If you are sure that the Web API expects enum
as string
only, just turn on option "EnumToString". - Some yaml files may have multiple
operationId
s not unique, or the combination of operationId
+tags
not unique, when OpenApiClientGen
, by default, use operationId
+tags
for client API function names, then you may use option "ActionNameStrategy.PathMethodQueryParameters
".
Non "application/json" Payloads
Currently, OpenApiClientGen
v3 still generates client API codes, however obviously the client functions won't be talking to the backend correctly, so you need to be careful not to use those client functions in your app codes. And in v3.1, there will be a switch for skipping generating codes for such operations.
Doggy Swagger / OpenAPI Definitions
The quality of your client application depends on the quality of the Web API and the quality of the generated codes. To ensure the basic quality of the generated codes, the integration test suites cover over 1,700 OpenAPI v3 definitions and over 615 Swagger v2 definitions: generate codes, compile with C# compiler and TypeScript/Angular compiler.
However, I have come across over 100 doggy swagger / OpenAPI definitions, some of which have come from some prominent Web service vendors. Typical dirty or doggy designs in some YAML files:
- In inherited data models, a property is declared multiple times. C# compiler or TypeScript code analysis may raise a warning. However, you understand the nasty effect of having such inheritance. And for Angular Reactive Forms which does not support inheritance, this will result in duplicated Form Controls and error.
- Wrong data constraints. For example, type integer with default "
false
"; using maximum rather than maxItems
for array size limit. - ...
If you happen to be using these Web services, you may two approaches:
- Use the other Swagger / OpenAPI tools that may tolerate the dirty or doggy designs.
- Talk to the service vendor.
Remarks
- Obviously, having the generated codes being successfully compiled is far from enough. It is important to write an integration test suite to assure the generated codes can talk correctly to the backend.
Integration Test Suite for the Generated Codes
In addition to writing integration test suites for your application codes, you should write an integration test suite upon the Web API, using the generated client API codes and serving such purposes:
- Understand and learn the behaviour and the quality of the Web API.
- Isolate the problem areas when having a bug report during development and production.
History
- 1st February, 2024: Initial version