Introduce systematic approach for utilizing integral types of .NET in JavaScript clients and containing large integral numbers, and references to TypeScript code generation.
Introduction
This article is the sequel of Dealing with large integral numbers in JavaScript for integral types of ASP.NET Core Web API which illustrates some by-design nature of JavaScript when dealing with large integral numbers and suggests some solutions. Please read the first one before reading this one.
This article introduces a systematic approach for utilizing integral types of .NET in JavaScript clients.
- JS Doc comment hinting the data range of a property of a TypeScript interface or function parameter. For example, for .NET
byte
, the document will be Type: byte -128 to 127
. And such doc comment will be generated under some conditions. - On the JavaScript client end, use
string
object for 54-bit and greater in the request payload and the response payload. Such mapping will be generated. - On the ASP.NET Core Web API end, customize the serialization / binding for
Int64
, UInt64
and BigInteger
. - For Angular clients with Reactive Forms, the form codes provide validators to enforce the data constraints as declared on the service side. Such validation can be generated.
Using the Code
Clone or fork JsLargeIntegralDemo at GitHub to get a local working copy.
Prerequisites
Steps
- Stay with the master branch or tag v1.
- Build the sln.
- Run
IntegrationTestsCore
in Test Explorer or "DotNet Test". The test suite will launch Web API DemoCoreWeb
and then close after finishing running the test cases. This is to verify that the Web service is functioning well. - Run StartDemoCoreWeb.ps1 to launch the Web API
DemoCoreWeb
.
Folder HeroesDemo contains a modified "Tour of Heroes", an Angular app talking to DemoCoreWeb
. After installing packages through running npm install
, you run ng test
, then you will see:
Comparing with the code example in article "Dealing with large integral numbers in JavaScript for integral types of ASP.NET Core Web API", this one diffs by:
- Customized data binding for
Int64
, UInt64
and BigInteger
. - JavaScript test suite "Numbers API" adapting the serialization at the service side.
In service start-up codes with IServiceCollection
, the following JSON converters are injected:
.AddNewtonsoftJson(
options =>
{
options.SerializerSettings.Converters.Add(new Int64JsonConverter());
options.SerializerSettings.Converters.Add(new Int64NullableJsonConverter());
options.SerializerSettings.Converters.Add(new UInt64JsonConverter());
options.SerializerSettings.Converters.Add(new UInt64NullableJsonConverter());
options.SerializerSettings.Converters.Add(new BigIntegerJsonConverter());
options.SerializerSettings.Converters.Add(new BigIntegerNullableJsonConverter());
}
);
These converters are imported from NuGet package: Fonlow.IntegralExtensions .
Hints
- The C# client API and the C# integration test suite
IntegrationTestsCore
remains the same, and the results remain the same while the service has altered the way of serialization for Int64
, UInt64
and BigInteger
, since the System.Net.Http.HttpClient
is handling well both ways of serialization: JSON number object or JSON string
object.
Integration Tests with JavaScript / TypeScript Clients
This test suite uses string
for integral numbers of 64-bit, 128-bit and BigInt
when talking to ASP.NET Core Web API which provides decent Web API data binding upon JSON number object and JSON string object that represent a number.
Remarks
- You should find out if your backend developed on PHP, Java, Go or Python, etc. could provide such ability of Web API data binding, probably through a similar test suite.
The following test cases are based on Angular 5+ codes and Karma.
Source codes of types with properties of various integral types:
export interface BigNumbers {
bigInt?: string | null;
signed128?: string | null;
signed64?: string | null;
unsigned128?: string | null;
unsigned64?: string | null;
}
export interface IntegralEntity extends DemoWebApi_DemoData_Base_Client.Entity {
byte?: number | null;
int?: number | null;
itemCount?: number | null;
sByte?: number | null;
short?: number | null;
uInt?: number | null;
uShort?: number | null;
}
Source codes of test suite "Numbers API":
describe('Numbers API', () => {
let service: DemoWebApi_Controllers_Client.Numbers;
beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [HttpClientModule],
providers: [
{
provide: DemoWebApi_Controllers_Client.Numbers,
useFactory: numbersClientFactory,
deps: [HttpClient],
},
]
});
service = TestBed.get(DemoWebApi_Controllers_Client.Numbers);
}));
it('postBigNumbers', (done) => {
const d: DemoWebApi_DemoData_Client.BigNumbers = {
unsigned64: '18446744073709551615',
signed64: '9223372036854775807',
unsigned128: '340282366920938463463374607431768211455',
signed128: '170141183460469231731687303715884105727',
bigInt: '6277101735386680762814942322444851025767571854389858533375',
};
service.postBigNumbers(d).subscribe(
r => {
expect(BigInt(r.unsigned64!)).toBe(BigInt('18446744073709551615'));
expect(BigInt(r.signed64!)).toBe(BigInt('9223372036854775807'));
expect(BigInt(r.unsigned128!)).toBe
(BigInt(340282366920938463463374607431768211455n));
expect(BigInt(r.signed128!)).toEqual
(BigInt(170141183460469231731687303715884105727n));
expect(BigInt(r.bigInt!)).toEqual
(BigInt(6277101735386680762814942322444851025767571854389858533375n));
done();
},
error => {
fail(errorResponseToString(error));
done();
}
);
}
);
it('postIntegralEntity', (done) => {
service.postIntegralEntity
({ name: 'Some one', byte: 255, uShort: 65535 }).subscribe(
r => {
expect(r.byte).toBe(255);
expect(r.uShort).toBe(65535);
done();
},
error => {
fail(errorResponseToString(error));
done();
}
);
}
);
it('postIntegralEntityInvalid', (done) => {
service.postIntegralEntity({ name: 'Some one',
byte: 260, uShort: 65540 }).subscribe(
r => {
expect(r).toBeNull();
done();
},
error => {
fail(errorResponseToString(error));
expect().nothing();
done();
}
);
}
);
it('postIntegralEntityInvalidButBackendCheckNull', (done) => {
service.postIntegralEntityMustBeValid
({ name: 'Some one', byte: 260, uShort: 65540 }).subscribe(
r => {
fail('backend should throw 500')
done();
},
error => {
console.error(errorResponseToString(error));
expect().nothing();
done();
}
);
}
);
it('postUShort', (done) => {
service.postByDOfUInt16(65535).subscribe(
r => {
expect(r).toBe(65535);
done();
},
error => {
fail(errorResponseToString(error));
done();
}
);
}
);
it('postUShortInvalid', (done) => {
service.postByDOfUInt16(65540).subscribe(
r => {
expect(r).toBe(0);
done();
},
error => {
fail(errorResponseToString(error));
done();
}
);
}
);
it('postByte', (done) => {
service.postByDOfByte(255).subscribe(
r => {
expect(r).toBe(255);
done();
},
error => {
fail(errorResponseToString(error));
done();
}
);
}
);
it('postByteInvalid', (done) => {
service.postByDOfByte(258).subscribe(
r => {
fail("backend should throw");
done();
},
error => {
console.error(errorResponseToString(error));
expect().nothing();
done();
}
);
}
);
it('getByte', (done) => {
service.getByte(255).subscribe(
r => {
expect(r).toBe(255);
done();
},
error => {
fail(errorResponseToString(error));
done();
}
);
}
);
it('getByteInvalid', (done) => {
service.getByte(258).subscribe(
r => {
expect(r).toBe(0);
done();
},
error => {
fail(errorResponseToString(error));
done();
}
);
}
);
it('postByteWithNegativeInvalid', (done) => {
service.postByDOfByte(-10).subscribe(
r => {
fail("backend throws")
done();
},
error => {
console.error(errorResponseToString(error));
expect().nothing();
done();
}
);
}
);
it('postSByte', (done) => {
service.postByDOfSByte(127).subscribe(
r => {
expect(r).toBe(127);
done();
},
error => {
fail(errorResponseToString(error));
done();
}
);
}
);
it('postSByteInvalid', (done) => {
service.postByDOfSByte(130).subscribe(
r => {
expect(r).toBe(0);
done();
},
error => {
fail(errorResponseToString(error));
done();
}
);
}
);
it('postInt64', (done) => {
service.postInt64('9223372036854775807').subscribe(
r => {
expect(BigInt(r)).toBe(BigInt('9223372036854775807'));
done();
},
error => {
fail(errorResponseToString(error));
done();
}
);
}
);
it('postUInt64', (done) => {
service.postUint64('18446744073709551615').subscribe(
r => {
expect(BigInt(r)).toBe(BigInt('18446744073709551615'));
done();
},
error => {
fail(errorResponseToString(error));
done();
}
);
}
);
it('postInt64Smaller', (done) => {
service.postInt64('9223372036854775123').subscribe(
r => {
expect(BigInt(r)).toBe(BigInt('9223372036854775123'));
done();
},
error => {
fail(errorResponseToString(error));
done();
}
);
}
);
it('postLongAsBigInt', (done) => {
service.postBigInteger('9223372036854775807').subscribe(
r => {
expect(BigInt(r)).toBe(BigInt('9223372036854775807'));
done();
},
error => {
fail(errorResponseToString(error));
done();
}
);
}
);
it('postLongAsBigIntWithSmallNumber', (done) => {
service.postBigInteger('123').subscribe(
r => {
expect(BigInt(r)).toBe(BigInt(123n));
done();
},
error => {
fail(errorResponseToString(error));
done();
}
);
}
);
it('postReallyBigInt192bits', (done) => {
service.postBigInteger('6277101735386680762814942322444851025767571854389858533375').subscribe(
r => {
expect(BigInt(r)).toBe(BigInt
(6277101735386680762814942322444851025767571854389858533375n));
expect(BigInt(r).valueOf()).toBe(BigInt
('6277101735386680762814942322444851025767571854389858533375'));
done();
},
error => {
fail(errorResponseToString(error));
done();
}
);
}
);
it('postReallyBigInt80bits', (done) => {
service.postBigInteger('604462909807314587353087').subscribe(
r => {
expect(BigInt(r).valueOf()).toBe(604462909807314587353087n);
expect(BigInt(r).valueOf()).toBe(BigInt('604462909807314587353087'));
done();
},
error => {
fail(errorResponseToString(error));
done();
}
);
}
);
it('postReallyBigInt128bits', (done) => {
service.postBigInteger('340282366920938463463374607431768211455').subscribe(
r => {
expect(BigInt(r).valueOf()).toBe(340282366920938463463374607431768211455n);
expect(BigInt(r).valueOf()).toBe(BigInt
('340282366920938463463374607431768211455'));
done();
},
error => {
fail(errorResponseToString(error));
done();
}
);
}
);
it('postInt128', (done) => {
service.postInt128('170141183460469231731687303715884105727').subscribe(
r => {
expect(BigInt(r)).toBe(BigInt('170141183460469231731687303715884105727'));
expect(BigInt(r)).toBe(BigInt(170141183460469231731687303715884105727n));
done();
},
error => {
fail(errorResponseToString(error));
done();
}
);
}
);
it('postUInt128', (done) => {
service.postUint128('340282366920938463463374607431768211455').subscribe(
r => {
expect(BigInt(r)).toBe(BigInt('340282366920938463463374607431768211455'));
expect(BigInt(r)).toBe(BigInt(340282366920938463463374607431768211455n));
expect(BigInt(r).valueOf()).toBe(BigInt
('340282366920938463463374607431768211455'));
expect(BigInt(r).valueOf()).toBe(BigInt
(340282366920938463463374607431768211455n));
done();
},
error => {
fail(errorResponseToString(error));
done();
}
);
}
);
});
As you can see, now the JavaScript / TypeScript codes with the same client API used in article ""Dealing with large integral numbers in JavaScript for integral types of ASP.NET Core Web API" can handle large integral number correctly and comfortably.
Summary
To make the JavaScript / TypeScript clients handle large integral number correctly and comfortably with the least programming efforts on the backend and the JS/TS client frontend, do the following.
Backend
- Import NuGet package: Fonlow.IntegralExtensions and inject respective JSON converters in the start-up codes.
- Check
ModelState
in every Web API function calls. This will avoid the Web API return 0
when the clients have provided invalid integral numbers. This is optional since you may have good reasons not to check ModelState
in every Web API function calls. - To generate TypeScript client API codes conforming to the "
Universal
" solutions for TypeScript clients suggested in the prior article, import NuGet packages: Fonlow.WebApiClientGenCore
v7.2 or above, and Fonlow.WebApiClientGenCore.NG2FormGroup
v1.5 or above if you are using Angular and Reactive Forms. And for other JavaScript libraries, please check the references below.
Hints:
- To check
ModelState
in every Web API function calls, you will find many good posts when googling "ASP.NET Web API ModelState
filter".
TypeScript Code Generators for Various JavaScript Libraries
- jQuery and HttpClient helper library
- AXIOS
- Fetch API
- Aurelia
- Angular 6+
- Angular 6+, plus FormGroup creation for Reactive Forms with Description
Articles / Tutorials
Frontend
With the generated client API codes in TypeScript, application programming becomes straightforward and easy for small and large integral types.
Generated Hints Based on .NET Integral Data Types
export interface IntegralEntity extends DemoWebApi_DemoData_Base_Client.Entity {
byte?: number | null;
int?: number | null;
itemCount?: number | null;
sByte?: number | null;
short?: number | null;
uInt?: number | null;
uShort?: number | null;
}
A decent IDE should display respective doc comment beside your finger tips.
postByDOfUInt16(d?: number | null,
headersHandler?: () => HttpHeaders): Observable<number> {
return this.http.post<number>(this.baseUri + 'api/Numbers/ushort',
JSON.stringify(d), { headers: headersHandler ?
headersHandler().append('Content-Type', 'application/json;charset=UTF-8') :
new HttpHeaders({ 'Content-Type': 'application/json;charset=UTF-8' }) });
}
Hints:
- Hints based on .NET integral data types are generated only when the .NET class property has no user defined doc comment and validation attributes, while the TypeScript code generators may generate JS doc comments based on the .NET class property doc comment and decorated validation attributes.
Contain Large Integral Numbers through BigInt
it('postBigNumbers', (done) => {
const d: DemoWebApi_DemoData_Client.BigNumbers = {
unsigned64: '18446744073709551615',
signed64: '9223372036854775807',
unsigned128: '340282366920938463463374607431768211455',
signed128: '170141183460469231731687303715884105727',
bigInt: '6277101735386680762814942322444851025767571854389858533375',
};
service.postBigNumbers(d).subscribe(
r => {
expect(BigInt(r.unsigned64!)).toBe(BigInt('18446744073709551615'));
expect(BigInt(r.signed64!)).toBe(BigInt('9223372036854775807'));
expect(BigInt(r.unsigned128!)).toBe(BigInt
(340282366920938463463374607431768211455n));
expect(BigInt(r.signed128!)).toEqual(BigInt
(170141183460469231731687303715884105727n));
expect(BigInt(r.bigInt!)).toEqual(BigInt
(6277101735386680762814942322444851025767571854389858533375n));
done();
},
Some bug/defect of JavaScript BigInt is well contained in this landscape.
Runtime Validation with Angular Reactive Forms
export function CreateIntegralEntityFormGroup() {
return new FormGroup<IntegralEntityFormProperties>({
emailAddress: new FormControl<string | null | undefined>(undefined,
[Validators.email, Validators.maxLength(255)]),
id: new FormControl<string | null | undefined>(undefined),
name: new FormControl<string | null | undefined>(undefined,
[Validators.required, Validators.minLength(2), Validators.maxLength(255)]),
web: new FormControl<string | null | undefined>(undefined,
[Validators.pattern('https?:\\/\\/(www\\.)?[-a-zA-Z0-9@:%._\\+~#=]
{1,256}\\.[a-zA-Z0-9()]{1,6}\\b([-a-zA-Z0-9()@:%_\\+.~#?&//=]*)')]),
byte: new FormControl<number | null | undefined>(undefined,
[Validators.min(0), Validators.max(256)]),
int: new FormControl<number | null | undefined>(undefined,
[Validators.min(-2147483648), Validators.max(2147483647)]),
itemCount: new FormControl<number | null | undefined>(undefined,
[Validators.min(-1000), Validators.max(1000000)]),
sByte: new FormControl<number | null | undefined>(undefined,
[Validators.min(-127), Validators.max(127)]),
short: new FormControl<number | null | undefined>(undefined,
[Validators.min(-32768), Validators.max(32767)]),
uInt: new FormControl<number | null | undefined>(undefined,
[Validators.min(0), Validators.max(4294967295)]),
uShort: new FormControl<number | null | undefined>(undefined,
[Validators.min(0), Validators.max(65535)]),
});
}
History
- 23rd February, 2024: Initial version