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

在 JavaScript 中处理 ASP.NET Core Web API 的整型类型的大整数

starIconstarIconstarIconstarIconstarIcon

5.00/5 (1投票)

2024 年 2 月 23 日

CPOL

5分钟阅读

viewsIcon

5302

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

背景

在 JavaScript 代码中处理大整数仍然是一个棘手的问题,因为存在 53 位限制,而 BigInt 存在其他问题。

有几个不错的 JavaScript 学习资源

显然,JavaScript 中有很多糟糕的地方,我们应该牢记并与之共存,尽管自 TypeScript 诞生以来,JavaScript 在语法和一些错误/缺陷修复方面发展迅速。

有 lint 工具可以帮助我们。如果您使用 TypeScript,IDE、TS 编译器和 TS lint 工具将为您提供更多便利。

引言

本文旨在介绍如何在与 ASP.NET Core Web API 进行交互时,在不丢失精度的情况下处理大整数。

您使用整数是因为您希望保留一定的精度。C# .NET 提供了丰富的整数类型:

  1. sbyte, byte, short, ushort, int, uint
  2. long, ulong
  3. 大整数
  4. Int128, UInt128(.NET 7 及更高版本可用)

虽然 JavaScript 的 BigInt 可能解决了 53 位限制引起的一些问题,但在数据序列化过程中存在一些问题。

  1. 由于 Google Chrome 和 Mozilla Firefox 可能存在的 bug,导致精度丢失。
  2. “不知道如何序列化 BigInt

如果您搜索“javascript bigint serialize”,您会找到针对各种场景的许多解决方案。正如许多经验丰富的 JavaScript 开发人员所说,没有万能的解决方案。一些相关的帖子:

我看到许多帖子建议以某种方式使用 JavaScript 的 toString(),然而,当数字大于 53 位时,这种方法会丢失精度或遗漏一位。

您可能已经使用了一些下面描述的解决方案,并且可能已经知道没有万能的解决方案来处理 JavaScript 的这些不足。然而,本文试图为常见场景介绍一些解决方案,并通过生成的代码将这些解决方案进行封装。

Using the Code

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

必备组件

  • .NET 7/8
  • Visual Studio 2022

步骤

  1. 切换到分支 "DefaultWay"
  2. 生成 sln 文件。
  3. 在 Test Explorer 或 "DotNet Test" 中运行 "IntegrationTestsCore"。该测试套件将启动 Web API "DemoCoreWeb",并在完成运行测试用例后关闭。
  4. 运行 StartDemoCoreWeb.ps1 来启动 Web API "DemoCoreWeb"。

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

ASP.NET Core Web API

源代码

    [DataContract(Namespace = Constants.DataNamespace)]
    public class BigNumbers
    {
        [DataMember]
        public long Signed64 { get; set; }

        [DataMember]
        public ulong Unsigned64 { get; set; }

        [DataMember]
        public Int128 Signed128 { get; set; }

        [DataMember]
        public UInt128 Unsigned128 { get; set; }

        [DataMember()]
        public BigInteger BigInt { get; set; }
    }

    [Route("api/[controller]")]
    public class NumbersController : ControllerBase
    {
        [HttpPost]
        [Route("BigNumbers")]
        public BigNumbers PostBigNumbers([FromBody] BigNumbers bigNumbers)
        {
            return bigNumbers;
        }

        [HttpPost]
        [Route("int64")]
        public long PostInt64([FromBody] long int64)
        {
            return int64;
        }

        [HttpPost]
        [Route("bigIntegralAsStringForJs")]
        public string PostBigIntegralAsStringForJs([FromBody] string bigIntegral)
        {
            return bigIntegral;
        }

        [HttpPost]
        [Route("uint64")]
        public ulong PostUint64([FromBody] ulong uint64)
        {
            return uint64;
        }

        [HttpPost]
        [Route("int128")]
        public Int128 PostInt128([FromBody] Int128 int128)
        {
            return int128;
        }

        [HttpPost]
        [Route("uint128")]
        public UInt128 PostUint128([FromBody] UInt128 uint128)
        {
            return uint128;
        }

        [HttpPost]
        [Route("bigInteger")]
        public BigInteger PostBigInteger([FromBody] BigInteger bigInteger)
        {
            return bigInteger;
        }

...

        [HttpPost]
        [Route("long")]
        public long Post([FromBody] long d)
        {
            return d;
        }

        [HttpPost]
        [Route("ulong")]
        public ulong Post([FromBody] ulong d)
        {
            return d;
        }

    }

我希望客户端程序能够处理整数而不丢失精度或遗漏一位。

与 .NET 客户端的集成测试

源代码

[Collection(TestConstants.LaunchWebApiAndInit)]
public partial class NumbersApiIntegration : IClassFixture<NumbersFixture>
{
    public NumbersApiIntegration(NumbersFixture fixture)
    {
        api = fixture.Api;
    }

    readonly DemoWebApi.Controllers.Client.Numbers api;

    [Fact]
    public void TestPostBigNumbers()
    {
        var d = new DemoWebApi.DemoData.Client.BigNumbers
        {
            Signed64 = 9223372036854775807,    // long.MaxValue,
            Unsigned64 = 18446744073709551615, // ulong.MaxValue,
            Signed128 = new Int128
                 (0x7FFF_FFFF_FFFF_FFFF, 0xFFFF_FFFF_FFFF_FFFF),    // Int128.MaxValue,
            Unsigned128 = new UInt128
                 (0xFFFF_FFFF_FFFF_FFFF, 0xFFFF_FFFF_FFFF_FFFF),    // UInt128.MaxValue
            BigInt = new BigInteger(18446744073709551615) * 
                     new BigInteger(18446744073709551615) * 
                     new BigInteger(18446744073709551615),
        };

        // {"BigInt":6277101735386680762814942322444851025767571854389858533375,
        // "Signed128":"170141183460469231731687303715884105727",
        // "Signed64":9223372036854775807,"Unsigned128":
        // "340282366920938463463374607431768211455","Unsigned64":18446744073709551615}

        var r = api.PostBigNumbers(d);

        // {"signed64":9223372036854775807,"unsigned64":18446744073709551615,
        // "signed128":"170141183460469231731687303715884105727",
        // "unsigned128":"340282366920938463463374607431768211455",
        // "bigInt":6277101735386680762814942322444851025767571854389858533375}
        Assert.Equal(d.Signed64, r.Signed64);
        Assert.Equal(d.Unsigned64, r.Unsigned64);
        Assert.Equal(d.Signed128, r.Signed128);
        Assert.Equal(d.Unsigned128, r.Unsigned128);
        Assert.Equal(d.BigInt, r.BigInt);
        Assert.NotEqual(d.BigInt, r.BigInt -1);
    }

    [Fact]
    public void TestPostLong()
    {
        var r = api.PostInt64(long.MaxValue);
        Assert.Equal(long.MaxValue, r);
    }

    [Fact]
    public void TestPostULong()
    {
        var r = api.PostUint64(ulong.MaxValue);
        Assert.Equal(ulong.MaxValue, r);
    }

    [Fact]
    public void TestPostInt128()
    {
        var r = api.PostInt128(Int128.MaxValue);
        Assert.Equal(Int128.MaxValue, r);
    }

    [Fact]
    public void TestPostUInt128()
    {
        var r = api.PostUint128(UInt128.MaxValue);
        Assert.Equal(UInt128.MaxValue, r);
    }

    [Fact]
    public void TestPostBigIntegerWith128bits()
    {
        BigInteger bigInt = new BigInteger(18446744073709551615) * 
                            new BigInteger(18446744073709551615); // 128-bit unsigned
        Assert.Equal("340282366920938463426481119284349108225", bigInt.ToString());
        var r = api.PostBigInteger(bigInt);
        Assert.Equal(bigInt, r);
        Assert.Equal("340282366920938463426481119284349108225", r.ToString());
    }

    [Fact]
    public void TestPostBigIntegerWith192bits()
    {
        BigInteger bigInt = new BigInteger(18446744073709551615) * 
                            new BigInteger(18446744073709551615) * 
                            new BigInteger(18446744073709551615); // 192-bit unsigned
        Assert.Equal("6277101735386680762814942322444851025767571854389858533375", 
                      bigInt.ToString());
        var r = api.PostBigInteger(bigInt);
        Assert.Equal(bigInt, r);
        Assert.Equal("6277101735386680762814942322444851025767571854389858533375", 
                      r.ToString());
    }

    [Fact]
    public void TestPostBigIntegerWith80bits()
    {
        byte[] bytes = { 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x7F };
        BigInteger bigInt = new BigInteger(bytes); // 192-bit unsigned
        Assert.Equal("604462909807314587353087", bigInt.ToString());
        var r = api.PostBigInteger(bigInt);
        Assert.Equal(bigInt, r);
        Assert.Equal("604462909807314587353087", r.ToString());
        Assert.True(r.ToByteArray().SequenceEqual(bytes));
    }

    [Fact]
    public void TestPostUIntAsBigInteger()
    {
        BigInteger bigInt = UInt128.MaxValue;
        var r = api.PostBigInteger(bigInt);
        Assert.Equal(bigInt, r);
        Assert.Equal("340282366920938463463374607431768211455", r.ToString());
    }

特别是对于 .NET 7 客户端,没有问题,因为双方通过可靠的数据绑定和序列化精确匹配数据类型,例如 客户端 API

public System.Int128 PostInt128(System.Int128 int128, 
       Action<System.Net.Http.Headers.HttpRequestHeaders> handleHeaders = null)
{
    var requestUri = "api/Numbers/int128";
    using var httpRequestMessage = new HttpRequestMessage(HttpMethod.Post, requestUri);
    using var requestWriter = new System.IO.StringWriter();
    var requestSerializer = JsonSerializer.Create(jsonSerializerSettings);
    requestSerializer.Serialize(requestWriter, int128);
    var content = new StringContent(requestWriter.ToString(), 
                  System.Text.Encoding.UTF8, "application/json");
    httpRequestMessage.Content = content;
    handleHeaders?.Invoke(httpRequestMessage.Headers);
    var responseMessage = client.SendAsync(httpRequestMessage).Result;
    try
    {
        responseMessage.EnsureSuccessStatusCodeEx();
        var stream = responseMessage.Content.ReadAsStreamAsync().Result;
        using JsonReader jsonReader = new JsonTextReader
                                      (new System.IO.StreamReader(stream));
        var serializer = JsonSerializer.Create(jsonSerializerSettings);
        return serializer.Deserialize<System.Int128>(jsonReader);
    }
    finally
    {
        responseMessage.Dispose();
    }
}

与 JavaScript / TypeScript 客户端的集成测试

此测试套件在与 ASP.NET Core Web API 通信时,使用 string 来表示 64 位、128 位整数以及 BigInt,ASP.NET Core Web API 对 JSON 数字对象和表示数字的 JSON string 对象提供了良好的 Web API 数据绑定。

备注

  • 您应该找出您用 PHP、Java、Go 或 Python 等开发的后端是否能够提供类似的 Web API 数据绑定能力,可能通过类似的测试套件。

以下测试用例基于 Angular 5+ 代码和 Karma。

源代码

请注意那些后缀为 "Incorrect" 的测试用例以及代码中的注释。

describe('Numbers API without customized serialization', () => {
...
     it('postBigNumbersIncorrect', (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!)).not.toBe
                      (BigInt('18446744073709551615')); // BigInt can not handle the 
                                                        // conversion from json 
                                                        // number form correctly.
                expect(BigInt(r.unsigned64!)).toEqual
                      (BigInt('18446744073709551616')); // actually incorrect 
                                                        // during deserialization

                expect(BigInt(r.signed64!)).not.toBe(BigInt('9223372036854775807'));
                expect(BigInt(r.signed64!)).toEqual(BigInt('9223372036854775808'));

                expect(BigInt(r.unsigned128!)).toBe(BigInt
                             (340282366920938463463374607431768211455n));

                expect(BigInt(r.signed128!)).toEqual(BigInt
                             (170141183460469231731687303715884105727n));

                expect(BigInt(r.bigInt!)).not.toEqual
                (BigInt(6277101735386680762814942322444851025767571854389858533375n));
                expect(BigInt(r.bigInt!)).toEqual
                (BigInt(6277101735386680763835789423207666416102355444464034512896n));// how wrong

                done();
            },
            error => {
                fail(errorResponseToString(error));
                done();
            }
        );
    }
    );

    /**
     * Even though the request payload is 9223372036854776000 
     * (loosing precision, cause of the 53bit issue), 
     * or "9223372036854776123", the response is 0 as shown in Chrome's 
     * console and Fiddler.
     * And the Web API has received actually 0. Not sure if the Web API binding 
     * had turned the request payload into 0 if the client is a Web browser.
     */
    it('postInt64ButIncorrect', (done) => {
        service.postInt64('9223372036854775807').subscribe(
            r => {
                expect(BigInt(9223372036854775807n).toString()).toBe
                                                    ('9223372036854775807');
                expect(BigInt(r)).toBe(BigInt('9223372036854775808'));    //reponse is 
                                                                // 9223372036854775807, 
                                               //but BigInt(r) gives last 3 digits 808
                done();
            },
            error => {
                fail(errorResponseToString(error));
                done();
            }
        );
    }
    );

    /**
          postBigIntegerForJs(bigInteger?: string | null, 
          headersHandler?: () => HttpHeaders): Observable<string> {
            return this.http.post<string>(this.baseUri + 'api/Numbers/bigIntegerForJs', 
            JSON.stringify(bigInteger), 
            { headers: headersHandler ? headersHandler().append
            ('Content-Type', 'application/json;charset=UTF-8') : new HttpHeaders
            ({ 'Content-Type': 'application/json;charset=UTF-8' }) });
        }
     */
    it('postBigIntegralAsStringForJs', (done) => {
        service.postBigIntegralAsStringForJs('9223372036854775807').subscribe(
            r => {
                expect(BigInt(9223372036854775807n).toString()).toBe
                                                    ('9223372036854775807');
                expect(BigInt('9223372036854775807').toString()).toBe
                                                     ('9223372036854775807');
                expect(BigInt(r)).toBe(BigInt('9223372036854775807'));
                expect(BigInt(r)).toBe(BigInt(9223372036854775807n));
                done();
            },
            error => {
                fail(errorResponseToString(error));
                done();
            }
        );
    }
    );

    it('postBigIntegralAsStringForJs2', (done) => {
        service.postBigIntegralAsStringForJs
          ('6277101735386680762814942322444851025767571854389858533375').subscribe(
            r => {
                expect(BigInt
                (6277101735386680762814942322444851025767571854389858533375n).toString()).toBe
                ('6277101735386680762814942322444851025767571854389858533375');
                expect(BigInt
                 ('6277101735386680762814942322444851025767571854389858533375').toString()).
                toBe('6277101735386680762814942322444851025767571854389858533375');
                expect(BigInt(r)).toBe(BigInt
                      ('6277101735386680762814942322444851025767571854389858533375'));
                expect(BigInt(r)).toBe(BigInt
                      (6277101735386680762814942322444851025767571854389858533375n));
                done();
            },
            error => {
                fail(errorResponseToString(error));
                done();
            }
        );
    }
    );

    it('postInt64SmallerInCorrect', (done) => {
        service.postInt64('9223372036854775123').subscribe(
            r => {
                expect(BigInt(r)).not.toBe(BigInt('9223372036854775123')); //reponse is 
                                            // 9223372036854775123, 
                                            //but BigInt(r) gives l9223372036854774784
                expect(BigInt(r)).toBe(BigInt('9223372036854774784'));     //many digits 
                                                                           //wrong
                done();
            },
            error => {
                fail(errorResponseToString(error));
                done();
            }
        );
    }
    );

    it('postLongAsBigIntButIncorrect', (done) => {
        // request: "9223372036854775807"
        // response: 9223372036854775807
        service.postBigInteger('9223372036854775807').subscribe(
            r => {
                expect(BigInt(9223372036854775807n).toString()).toBe
                                                    ('9223372036854775807');
                expect(BigInt(r)).toBe(BigInt('9223372036854775808')); //reponse is 
                                                      9223372036854775807, 
                       // but BigInt(r) gives last 3 digits 808, since the returned 
                       // value does not have the n suffix.
                expect(r.toString()).toBe('9223372036854776000'); //the response 
                                      // is a big int which JS could not handle 
                                      // in toString(), 53bit gets in the way.
                expect(BigInt(r).toString()).toBe('9223372036854775808');
                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('postReallyBigInt192bitsButIncorrect', (done) => {
        // request: "6277101735386680762814942322444851025767571854389858533375"
        // response: 6277101735386680762814942322444851025767571854389858533375
        service.postBigInteger
          ('6277101735386680762814942322444851025767571854389858533375').subscribe(
            r => {
                expect(BigInt(r)).toBe(BigInt
                (6277101735386680762814942322444851025767571854389858533375)); //this 
                                                       // time, it is correct, but...
                expect(BigInt(r).valueOf()).not.toBe
                (6277101735386680762814942322444851025767571854389858533375n); // not 
                                                                               // really,
                expect(BigInt(r).valueOf()).not.toBe(BigInt
                ('6277101735386680762814942322444851025767571854389858533375')); // not 
                                   // really, because what returned is lack of n
                expect(BigInt(r)).toBe
                (6277101735386680763835789423207666416102355444464034512896n); // many 
                                                                // many digits wrong
               done();
            },
            error => {
                fail(errorResponseToString(error));
                done();
            }
        );
    }
    );

    it('postReallyBigInt80bitsButIncorect', (done) => {
        service.postBigInteger('604462909807314587353087').subscribe(
            r => {
                expect(BigInt(r)).toBe(BigInt(604462909807314587353087)); //this time, 
                                                                // it is correct, but...
                expect(BigInt(r).valueOf()).not.toBe(604462909807314587353087n); // not 
                                                                                 // really,
                expect(BigInt(r).valueOf()).not.toBe
                (BigInt('604462909807314587353087')); // not really, because 
                                                      // what returned is lack of n
                expect(BigInt(r).valueOf()).toBe(604462909807314587353088n); // last 
                                                                        // digit wrong
                done();
            },
            error => {
                fail(errorResponseToString(error));
                done();
            }
        );
    }
    );

    it('postReallyBigInt128bitsButIncorect', (done) => {
        service.postBigInteger('340282366920938463463374607431768211455').subscribe(
            r => {
                expect(BigInt(r)).toBe(BigInt
                   (340282366920938463463374607431768211455)); //this time, 
                                                         // it is correct, but...
                expect(BigInt(r).valueOf()).not.toBe
                   (340282366920938463463374607431768211455n); // not really,
                expect(BigInt(r).valueOf()).not.toBe(BigInt
                ('340282366920938463463374607431768211455')); // not really, 
                                            // because what returned is lack of n
                expect(BigInt(r)).toBe(340282366920938463463374607431768211456n); // last 
                                                                  // digit wrong,
                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();
            }
        );
    }
    );

客户端 API 代码:

    export interface BigNumbers {

        /** BigInteger */
        bigInt?: string | null;

        /** Int128, -170141183460469231731687303715884105728 to 
            170141183460469231731687303715884105727 */
        signed128?: string | null;

        /** long, -9,223,372,036,854,775,808 to 9,223,372,036,854,775,807 */
        signed64?: string | null;

        /** UInt128, 0 to 340282366920938463463374607431768211455 */
        unsigned128?: string | null;

        /** ulong, 0 to 18,446,744,073,709,551,615 */
        unsigned64?: string | null;
    }

        /**
         * POST api/Numbers/long
         * @param {string} d long, -9,223,372,036,854,775,808 to 9,223,372,036,854,775,807
         * @return {string} long, -9,223,372,036,854,775,808 to 9,223,372,036,854,775,807
         */
        postByDOfInt64(d?: string | null, headersHandler?: () => 
                           HttpHeaders): Observable<string> {
            return this.http.post<string>(this.baseUri + 'api/Numbers/long', 
                   JSON.stringify(d), { headers: headersHandler ? headersHandler().append
                   ('Content-Type', 'application/json;charset=UTF-8') : 
                   new HttpHeaders({ 'Content-Type': 'application/json;charset=UTF-8' }) });
        }

        /**
         * POST api/Numbers/ulong
         * @param {string} d ulong, 0 to 18,446,744,073,709,551,615
         * @return {string} ulong, 0 to 18,446,744,073,709,551,615
         */
        postByDOfUInt64(d?: string | null, headersHandler?: () => 
            HttpHeaders): Observable<string> {
            return this.http.post<string>(this.baseUri + 'api/Numbers/ulong', 
            JSON.stringify(d), { headers: headersHandler ? headersHandler().append('Content-Type', 
            'application/json;charset=UTF-8') : new HttpHeaders
            ({ 'Content-Type': 'application/json;charset=UTF-8' }) });
        }

        /**
         * POST api/Numbers/bigInteger
         * @param {string} bigInteger BigInteger
         * @return {string} BigInteger
         */
        postBigInteger(bigInteger?: string | null, headersHandler?: () => 
        HttpHeaders): Observable<string> {
            return this.http.post<string>(this.baseUri + 'api/Numbers/bigInteger', 
            JSON.stringify(bigInteger), { headers: headersHandler ? headersHandler().append
            ('Content-Type', 'application/json;charset=UTF-8') : 
            new HttpHeaders({ 'Content-Type': 'application/json;charset=UTF-8' }) });
        }

您是否通过浏览器的开发者控制台或 Fiddler 注意到以下情况?

  1. 在后缀为 "Incorrect" 的测试用例中,JavaScript 客户端使用了 JSON 字符串对象,但是如果 ASP.NET Web API 以 JSON 数字对象响应,JavaScript 会 INCORRECTLY 读取大整数。您知道,这是 JavaScript 的特性。
  2. 对于 Int128UInt128,ASP.NET Core Web API 以 JSON string 对象响应,然后 JavaScript 的 BigInt 可以正确读取。

在测试用例 "bigIntegralAsStringForJs" 中,Web API 函数 "postBigIntegralAsStringForJs" 肯定可以处理大整数,然而,C# 客户端开发人员可能会讨厌这种弱类型设计,尽管 JavaScript 开发人员可能不介意。

[HttpPost]
[Route("bigIntegralAsStringForJs")]
public string PostBigIntegralAsStringForJs([FromBody] string bigIntegral)
{
    return bigIntegral;
}

ASP.NET Core Web API 默认返回一个 string

Content-Type: text/plain

ABCabc

除非客户端只接受 "application/json"。

C# 客户端和 TypeScript 客户端的“通用”解决方案

为了让 C# 客户端开发人员和 TypeScript 客户端开发人员满意,并避免为大整数提供两套 Web API 函数,在设计 Web API 时,请考虑以下几点:

  1. 在 JavaScript 客户端端,在请求负载和响应负载中使用 string 对象来表示 54 位及以上的整数。
  2. 在服务端,如果整数大于 53 位且不大于 128 位,请使用 Int128UInt128。换句话说,不要在 Web API 层使用 longulong
  3. 对于大于 128 位的整数,您可以考虑自定义 BigInteger 的序列化,使您的 Web API 具有与处理 UInt128 相同的行为。如果您编写了 JsonConverter 来处理 BigInteger,您可能也想为 longulong 编写相同的转换器。这样,作为 Web API 开发人员,您将享受丰富的整数类型数据约束,并忽略解决方案 2。好消息是,我已经开发了此类序列化的 JSON 转换器,请参阅下面的相关文章。

备注

  • 自定义 Web API 的序列化是针对现有客户端应用程序的重大破坏性更改。在尝试使用本文中提到的解决方案时,您应评估您自己的具体情况。

关注点

如果您正在使用 jQuery、AXIOS、Fetch API 或 Aurelia,您应该会发现上面带有 Angular 5+ 和 Karma 的 TypeScript 测试套件能够代表 JavaScript 的行为,您可以查看以下各种 JavaScript 库的测试套件,并在代码中搜索 "Numbers API"。

虽然 Int128UInt128 的引入为整数提供了更多位数,与 C++ 等其他语言相匹配,但如上面的演示所示,ASP.NET Core 7 已经提供了良好的序列化来克服 JavaScript 的不足。

参考文献

历史

  • 2024 年 2 月 23 日:初始版本
© . All rights reserved.