65.9K
CodeProject 正在变化。 阅读更多。
Home

享受 .NET 丰富的整数类型并克服 JavaScript 的 53 位限制

starIconstarIconstarIconstarIconstarIcon

5.00/5 (1投票)

2024 年 2 月 23 日

CPOL

5分钟阅读

viewsIcon

6174

克服 JavaScript 数字的 53 位限制,同时保留 .NET 强类型的整数类型。第二部分。

引言

本文是 处理 ASP.NET Core Web API 整数类型的 JavaScript 中的大整数 的续篇,该文阐述了 JavaScript 在处理大整数时的一些设计特性并提出了一些解决方案。请先阅读第一篇再阅读本文。

本文介绍了一种在 JavaScript 客户端利用 .NET 整数类型的系统化方法。

  1. JS 文档注释提示 TypeScript 接口属性或函数参数的数据范围。例如,对于 .NET byte,文档将是 Type: byte -128 to 127。在某些条件下将生成此类文档注释。
  2. 在 JavaScript 客户端,使用 string 对象来表示请求和响应负载中的 54 位及以上的数字。将生成此类映射。
  3. 在 ASP.NET Core Web API 端,为 Int64UInt64BigInteger 自定义序列化/绑定。
  4. 对于带有 Reactive Forms 的 Angular 客户端,表单代码提供了验证器来强制执行服务端声明的数据约束。此类验证可以生成。

Using the Code

克隆或 fork GitHub 上的 JsLargeIntegralDemo 以获取本地工作副本。

必备组件

步骤

  1. 保持与 master 分支或 tag v1 一致。
  2. 构建 sln。
  3. 在 Test Explorer 或 "DotNet Test" 中运行 IntegrationTestsCore。该测试套件将启动 Web API DemoCoreWeb,并在运行完测试用例后关闭。这是为了验证 Web 服务是否运行良好。
  4. 运行 StartDemoCoreWeb.ps1 来启动 Web API DemoCoreWeb

HeroesDemo 文件夹包含一个修改过的 "Tour of Heroes",一个与 DemoCoreWeb 通信的 Angular 应用。在通过运行 npm install 安装完包后,运行 ng test,您将看到

与文章 "处理 ASP.NET Core Web API 整数类型的 JavaScript 中的大整数" 中的代码示例相比,此示例的不同之处在于

  1. Int64UInt64BigInteger 自定义数据绑定。
  2. 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 保持不变,结果也保持不变,尽管服务改变了 Int64UInt64BigInteger 的序列化方式,因为 System.Net.Http.HttpClient 能够很好地处理 JSON 数字对象或 JSON string 对象这两种序列化方式。

集成测试与 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 客户端前端编程工作来正确且舒适地处理大整数,请执行以下操作。

后端

  1. 导入 NuGet 包:Fonlow.IntegralExtensions 并在启动代码中注入相应的 JSON 转换器。
  2. 在所有 Web API 函数调用中检查 ModelState。这将避免在客户端提供无效整数时 Web API 返回 0。此项可选,因为您可能有充分的理由不在所有 Web API 函数调用中检查 ModelState
  3. 要生成符合先前文章中建议的 TypeScript 客户端的 "Universal" 解决方案的 TypeScript 客户端 API 代码,请导入 NuGet 包:Fonlow.WebApiClientGenCore v7.2 或更高版本,如果您使用 Angular 和 Reactive Forms,则导入 Fonlow.WebApiClientGenCore.NG2FormGroup v1.5 或更高版本。对于其他 JavaScript 库,请参阅下面的参考资料。

提示

  • 要检查所有 Web API 函数调用中的 ModelState,您可以通过谷歌搜索 "ASP.NET Web API ModelState filter" 找到许多好的文章。

适用于各种 JavaScript 库的 TypeScript 代码生成器

  1. jQueryHttpClient 辅助库
  2. AXIOS
  3. Fetch API
  4. Aurelia
  5. Angular 6+
  6. Angular 6+,以及用于 Reactive Forms 的 FormGroup 创建,带有说明
文章 / 教程

前端

通过生成的 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 日:初始版本
© . All rights reserved.