Click here to Skip to main content
15,889,909 members
Articles / Programming Languages / Typescript

Dealing with Large Integral Numbers in JavaScript for Integral Types of ASP.NET Core Web API

Rate me:
Please Sign up or sign in to vote.
5.00/5 (1 vote)
23 Feb 2024CPOL5 min read 4.1K   4  
Overcome the 53-bit limitation of number of JavaScript while keeping strongly typed integral types of .NET. Part 1.
How to keep the precision of large integral numbers in JavaScript collaborating with strongly typed ASP.NET Core Web API?

Background

Dealing with large integral numbers in JavaScript codes is still a troublesome area due to the 53-bit limitation, while BigInt has other problems.

There are a few decent sources of studying JavaScript:

Obviously, there are many bad parts in JavaScript, which we should keep in mind and live with, even though JavaScript has been evolving fast since the birth of TypeScript, regarding syntax and some fixes against bugs / defects.

There are lint tools to help us. And if you are using TypeScript, you get more blessing from IDE, TS compiler and TS lint tools.

Introduction

This article is about dealing with large integral numbers without losing precision during the conversations with ASP.NET Core Web API.

You use integral numbers because you want to keep certain digits of precision. C# .NET has provided a rich set of integral types:

  1. sbyte, byte, short, ushort, int, uint
  2. long, ulong
  3. BigInteger
  4. Int128, UInt128 (Available since .NET 7)

While BigInt of JavaScript may resolve some issues caused by the 53-bit limitation, there are problems during data serialization:

  1. Loss of precision due to possible bugs in Google Chrome and Mozilla Firefox.
  2. "Do not know to serialize a BigInt"

If you google "javascript bigint serialize", you will find many solutions for various scenarios. As many seasoned JavaScript developers had said, there's no universal solution. A few relevant posts:

I have come across many posts suggesting using JavaScript toString() one way or the other, however, such ways lose precision or miss a bit when the number is larger than 53-bit.

You might have been using some of the solutions described below, and you probably had known there's no universal solution for dealing with such shortfalls of JavaScript. However, this article tries to introduce some solutions for common scenarios and wrap these solutions through generated codes.

Using the Code

Clone or fork JsLargeIntegralDemo at GitHub to get a local working copy.

Prerequisites

  • .NET 7/8
  • Visual Studio 2022

Steps

  1. Checkout branch "DefaultWay"
  2. Build the sln.
  3. 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.
  4. 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:

ASP.NET Core Web API

Source Codes

C#
    [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;
        }

    }

I would expect the client programs can handle integral numbers without losing precision or missing a bit.

Integration Tests with .NET Clients

Source Codes

C#
[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());
    }

For .NET clients particularly .NET 7 clients, no problem, since both ends match data types exactly with solid data bindings and serialization, for example, the client API:

C#
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();
    }
}

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

Please pay attention to those test case name with suffix "Incorrect" and the comments in codes.

TypeScript
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();
            }
        );
    }
    );

Client API codes:

TypeScript
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' }) });
    }

Have you noticed the following through browser's developer console or Fiddler?

  1. In those test cases with suffix "Incorrect", the JavaScript client uses JSON string object, however if the ASP.NET Web API responds with JSON number object, JavaScript reads large integral number INCORRECTLY. You know, this is the nature of JavaScript.
  2. For Int128 and UInt128, ASP.NET Core Web API responds with JSON string object, then JavaScript BigInt can read correctly.

In test case "bigIntegralAsStringForJs", Web API function "postBigIntegralAsStringForJs" surely can handle large integral numbers, however, C# client developers would likely hate such weakly typed design, though JavaScript developers may not mind.

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

ASP.NET Core Web API returns a string by default as:

Content-Type: text/plain

ABCabc

unless the client accepts only "application/json".

"Universal" Solutions for C# Clients and TypeScript Clients

To keep C# client developers and TypeScript client developers happy and avoid providing two sets of Web API functions for large integral numbers, when designing the Web API, consider the following:

  1. On the JavaScript client end, use string object for 54-bit and greater in the request payload and the response payload.
  2. On the service end, use Int128 or UInt128 if the integral numbers are greater than 53-bit and not larger than 128-bit. In other words, not to use long, ulong at the Web API layer.
  3. For integral numbers larger than 128-bit, you may consider to customize the serialization of BigInteger to make your Web API provide the same behaviour of handling UInt128. If you do write one JsonConverter for BigInteger, you may want to do the same for long and ulong. Then you as a Web API developer will enjoy rich data constraints of integral types, and ignore solution 2. And the good news is, I have developed JSON converters of such serialization, please check the referenced article below.

Remarks

  • Customizing the serialization of Web API is a significant breaking change against existing client apps. You should evaluate your own contexts when attempting to use solutions mentioned in this article.

Points of Interest

If you are using jQuery, AXIOS, Fetch API or Aurelia, you should find the TypeScript test suite above with Angular 5+ and Karma represent the behaviours of JavaScript, as you may check through the following test suites for various JavaScript libraries and search "Numbers API" in the codes.

While the introduction of Int128 and UInt128 is offering more bits for integral numbers, matching other languages like C++, however as you can see in the demo above, ASP.NET Core 7 has provided decent serialization to overcome the shortfalls of JavaScript.

References

History

  • 23rd February, 2024: Initial version

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Software Developer
Australia Australia
I started my IT career in programming on different embedded devices since 1992, such as credit card readers, smart card readers and Palm Pilot.

Since 2000, I have mostly been developing business applications on Windows platforms while also developing some tools for myself and developers around the world, so we developers could focus more on delivering business values rather than repetitive tasks of handling technical details.

Beside technical works, I enjoy reading literatures, playing balls, cooking and gardening.

Comments and Discussions

 
-- There are no messages in this forum --