享受 .NET 丰富的整数类型并克服 JavaScript 的 53 位限制
克服 JavaScript 数字的 53 位限制,同时保留 .NET 强类型的整数类型。第二部分。
引言
本文是 处理 ASP.NET Core Web API 整数类型的 JavaScript 中的大整数 的续篇,该文阐述了 JavaScript 在处理大整数时的一些设计特性并提出了一些解决方案。请先阅读第一篇再阅读本文。
本文介绍了一种在 JavaScript 客户端利用 .NET 整数类型的系统化方法。
- JS 文档注释提示 TypeScript 接口属性或函数参数的数据范围。例如,对于 .NET
byte
,文档将是Type: byte -128 to 127
。在某些条件下将生成此类文档注释。 - 在 JavaScript 客户端,使用
string
对象来表示请求和响应负载中的 54 位及以上的数字。将生成此类映射。 - 在 ASP.NET Core Web API 端,为
Int64
、UInt64
和BigInteger
自定义序列化/绑定。 - 对于带有 Reactive Forms 的 Angular 客户端,表单代码提供了验证器来强制执行服务端声明的数据约束。此类验证可以生成。
Using the Code
克隆或 fork GitHub 上的 JsLargeIntegralDemo 以获取本地工作副本。
必备组件
- .NET 7/8
- Visual Studio 2022
- NuGet 包:Fonlow.IntegralExtensions
- WebApiClientGen 的 NuGet 包
步骤
- 保持与 master 分支或 tag v1 一致。
- 构建 sln。
- 在 Test Explorer 或 "DotNet Test" 中运行
IntegrationTestsCore
。该测试套件将启动 Web APIDemoCoreWeb
,并在运行完测试用例后关闭。这是为了验证 Web 服务是否运行良好。 - 运行 StartDemoCoreWeb.ps1 来启动 Web API
DemoCoreWeb
。
HeroesDemo 文件夹包含一个修改过的 "Tour of Heroes",一个与 DemoCoreWeb
通信的 Angular 应用。在通过运行 npm install
安装完包后,运行 ng test
,您将看到
与文章 "处理 ASP.NET Core Web API 整数类型的 JavaScript 中的大整数" 中的代码示例相比,此示例的不同之处在于
- 为
Int64
、UInt64
和BigInteger
自定义数据绑定。 - JavaScript 测试套件 "Numbers API" 适配了服务端的序列化。
在带有 IServiceCollection
的服务启动代码中,注入了以下 JSON 转换器
.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());
}
);
这些转换器是从 NuGet 包:Fonlow.IntegralExtensions 导入的。
提示
- C# 客户端 API 和 C# 集成测试套件
IntegrationTestsCore
保持不变,结果也保持不变,尽管服务改变了Int64
、UInt64
和BigInteger
的序列化方式,因为System.Net.Http.HttpClient
能够很好地处理 JSON 数字对象或 JSONstring
对象这两种序列化方式。
集成测试与 JavaScript / TypeScript 客户端
该测试套件在使用 64 位、128 位整数和 BigInt
与 ASP.NET Core Web API 通信时使用 string
,ASP.NET Core Web API 提供了良好的 Web API 数据绑定,能够处理表示数字的 JSON 数字对象和 JSON 字符串对象。
备注
- 您应该找出您的后端(例如使用 PHP、Java、Go 或 Python 等开发)是否能够提供此类 Web API 数据绑定能力,也许可以通过类似的测试套件。
以下测试用例基于 Angular 5+ 代码和 Karma。
包含各种整数类型属性的类型的源代码
export interface BigNumbers {
/** Type: BigInteger */
bigInt?: string | null;
/** Type: Int128, -170141183460469231731687303715884105728 to
170141183460469231731687303715884105727 */
signed128?: string | null;
/** Type: long, -9,223,372,036,854,775,808 to 9,223,372,036,854,775,807 */
signed64?: string | null;
/** Type: UInt128, 0 to 340282366920938463463374607431768211455 */
unsigned128?: string | null;
/** Type: ulong, 0 to 18,446,744,073,709,551,615 */
unsigned64?: string | null;
}
export interface IntegralEntity extends DemoWebApi_DemoData_Base_Client.Entity {
/** Type: byte, 0 to 255 */
byte?: number | null;
/** Type: int, -2,147,483,648 to 2,147,483,647 */
int?: number | null;
/** Range: inclusive between -1000 and 1000000 */
itemCount?: number | null;
/** Type: sbyte, -128 to 127 */
sByte?: number | null;
/** Type: short, -32,768 to 32,767 */
short?: number | null;
/** Type: uint, 0 to 4,294,967,295 */
uInt?: number | null;
/** Type: ushort, 0 to 65,535 */
uShort?: number | null;
}
"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', //2 ^ 64 -1,
signed64: '9223372036854775807', //2 ^ 63 -1,
unsigned128: '340282366920938463463374607431768211455',
signed128: '170141183460469231731687303715884105727',
bigInt: '6277101735386680762814942322444851025767571854389858533375', // 3
// unsigned64, 192bits
};
/**
request:
{
"unsigned64":"18446744073709551615",
"signed64":"9223372036854775807",
"unsigned128":"340282366920938463463374607431768211455",
"signed128":"170141183460469231731687303715884105727",
"bigInt":"6277101735386680762814942322444851025767571854389858533375"
}
response:
{
"signed64": 9223372036854775807,
"unsigned64": 18446744073709551615,
"signed128": "170141183460469231731687303715884105727",
"unsigned128": "340282366920938463463374607431768211455",
"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();
}
);
}
);
/**
* ASP.NET will validate complex object by default,
and make it null if a property is invalid.
*/
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();
}
);
}
);
/**
* Backend checks if the data is null, likely due to invalid properties.
* And throw error.
*/
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();
}
);
}
);
/**
* ASP.NET Web API just give 0 back
*/
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();
}
);
}
);
/**
* ASP.NET Web API check ModelState and throw
*/
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();
}
);
}
);
/**
* ASP.NET Web API just give 0 back, if the API does not check ModelState and throw
*/
it('getByteInvalid', (done) => {
service.getByte(258).subscribe(
r => {
expect(r).toBe(0);
done();
},
error => {
fail(errorResponseToString(error));
done();
}
);
}
);
/**
* ASP.NET Web API just give 0 back
*/
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();
}
);
}
);
/**
* ASP.NET Web API just give 0 back
*/
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) => {
// request: "9223372036854775807"
// response: "9223372036854775807"
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) => {
// request: "6277101735386680762814942322444851025767571854389858533375"
// response: "6277101735386680762814942322444851025767571854389858533375"
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();
}
);
}
);
/**
* Correct.
* Request as string: "170141183460469231731687303715884105727",
* Response: "170141183460469231731687303715884105727" ,
Content-Type: application/json; charset=utf-8
*/
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();
}
);
}
);
/**
* Correct.
* Request as string: "340282366920938463463374607431768211455",
* Response: "340282366920938463463374607431768211455" ,
Content-Type: application/json; charset=utf-8
*/
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();
}
);
}
);
});
正如您所见,现在使用与文章 "处理 ASP.NET Core Web API 整数类型的 JavaScript 中的大整数" 中相同的客户端 API 的 JavaScript / TypeScript 代码,可以正确且舒适地处理大整数。
摘要
为了让 JavaScript / TypeScript 客户端能够以最少的后端和 JS/TS 客户端前端编程工作来正确且舒适地处理大整数,请执行以下操作。
后端
- 导入 NuGet 包:Fonlow.IntegralExtensions 并在启动代码中注入相应的 JSON 转换器。
- 在所有 Web API 函数调用中检查
ModelState
。这将避免在客户端提供无效整数时 Web API 返回0
。此项可选,因为您可能有充分的理由不在所有 Web API 函数调用中检查ModelState
。 - 要生成符合先前文章中建议的 TypeScript 客户端的 "
Universal
" 解决方案的 TypeScript 客户端 API 代码,请导入 NuGet 包:Fonlow.WebApiClientGenCore
v7.2 或更高版本,如果您使用 Angular 和 Reactive Forms,则导入Fonlow.WebApiClientGenCore.NG2FormGroup
v1.5 或更高版本。对于其他 JavaScript 库,请参阅下面的参考资料。
提示
- 要检查所有 Web API 函数调用中的
ModelState
,您可以通过谷歌搜索 "ASP.NET Web APIModelState
filter" 找到许多好的文章。
适用于各种 JavaScript 库的 TypeScript 代码生成器
- jQuery 和 HttpClient 辅助库
- AXIOS
- Fetch API
- Aurelia
- Angular 6+
- Angular 6+,以及用于 Reactive Forms 的 FormGroup 创建,带有说明
- 为 ASP.NET Core Web API 生成 TypeScript 客户端 API
- 使用 ASP.NET Core Web API 为 Angular Reactive Forms 生成类型化的 FormGroup
- 为强类型 OpenAPI 客户端生成器的意图限制提供预期解决方案
- 英雄之旅:React,带有 ASP.NET Core 后端
- 英雄之旅:Aurelia,带有 ASP.NET Core 后端
前端
通过生成的 TypeScript 客户端 API 代码,应用程序编程对于小型和大型整数类型都变得直接而轻松。
基于 .NET 整数数据类型的生成提示
export interface IntegralEntity extends DemoWebApi_DemoData_Base_Client.Entity {
/** Type: byte, 0 to 255 */
byte?: number | null;
/** Type: int, -2,147,483,648 to 2,147,483,647 */
int?: number | null;
/** Range: inclusive between -1000 and 1000000 */
itemCount?: number | null;
/** Type: sbyte, -128 to 127 */
sByte?: number | null;
/** Type: short, -32,768 to 32,767 */
short?: number | null;
/** Type: uint, 0 to 4,294,967,295 */
uInt?: number | null;
/** Type: ushort, 0 to 65,535 */
uShort?: number | null;
}
一个不错的 IDE 应该在您指尖旁边显示相应的文档注释。
/**
* POST api/Numbers/ushort
* @param {number} d Type: ushort, 0 to 65,535
* @return {number} Type: ushort, 0 to 65,535
*/
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' }) });
}
提示
- 基于 .NET 整数数据类型的提示仅在 .NET 类属性没有用户定义的文档注释和验证属性时生成,而 TypeScript 代码生成器可能会根据 .NET 类属性的文档注释和装饰的验证属性生成 JS 文档注释。
通过 BigInt 处理大整数
it('postBigNumbers', (done) => {
const d: DemoWebApi_DemoData_Client.BigNumbers = {
unsigned64: '18446744073709551615', //2 ^ 64 -1,
signed64: '9223372036854775807', //2 ^ 63 -1,
unsigned128: '340282366920938463463374607431768211455',
signed128: '170141183460469231731687303715884105727',
bigInt: '6277101735386680762814942322444851025767571854389858533375', // 3
// unsigned64, 192bits
};
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();
},
JavaScript BigInt 的一些错误/缺陷在此场景下得到了很好的处理。
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)]),
});
}
历史
- 2024 年 2 月 23 日:初始版本