Click here to Skip to main content
15,887,083 members
Articles / Web Development / ASP.NET / ASP.NET Core

Enjoy Rich Integral Types of .NET and Overcome the 53-bit Limitation of JavaScript

Rate me:
Please Sign up or sign in to vote.
5.00/5 (1 vote)
23 Feb 2024CPOL5 min read 5.3K   4  
Overcome the 53-bit limitation of number of JavaScript while keeping strongly typed integral types of .NET. Part 2.
Introduce systematic approach for utilizing integral types of .NET in JavaScript clients and containing large integral numbers, and references to TypeScript code generation.

Introduction

This article is the sequel of Dealing with large integral numbers in JavaScript for integral types of ASP.NET Core Web API which illustrates some by-design nature of JavaScript when dealing with large integral numbers and suggests some solutions. Please read the first one before reading this one.

This article introduces a systematic approach for utilizing integral types of .NET in JavaScript clients.

  1. JS Doc comment hinting the data range of a property of a TypeScript interface or function parameter. For example, for .NET byte, the document will be Type: byte -128 to 127. And such doc comment will be generated under some conditions.
  2. On the JavaScript client end, use string object for 54-bit and greater in the request payload and the response payload. Such mapping will be generated.
  3. On the ASP.NET Core Web API end, customize the serialization / binding for Int64, UInt64 and BigInteger.
  4. For Angular clients with Reactive Forms, the form codes provide validators to enforce the data constraints as declared on the service side. Such validation can be generated.

Using the Code

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

Prerequisites

Steps

  1. Stay with the master branch or tag v1.
  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. This is to verify that the Web service is functioning well.
  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:

Comparing with the code example in article "Dealing with large integral numbers in JavaScript for integral types of ASP.NET Core Web API", this one diffs by:

  1. Customized data binding for Int64, UInt64 and BigInteger.
  2. JavaScript test suite "Numbers API" adapting the serialization at the service side.

In service start-up codes with IServiceCollection, the following JSON converters are injected:

C#
.AddNewtonsoftJson(
    options =>
    {
        options.SerializerSettings.Converters.Add(new Int64JsonConverter());
        options.SerializerSettings.Converters.Add(new Int64NullableJsonConverter());
        options.SerializerSettings.Converters.Add(new UInt64JsonConverter());
        options.SerializerSettings.Converters.Add(new UInt64NullableJsonConverter());
        options.SerializerSettings.Converters.Add(new BigIntegerJsonConverter());
        options.SerializerSettings.Converters.Add(new BigIntegerNullableJsonConverter());
    }
);

These converters are imported from NuGet package: Fonlow.IntegralExtensions .

Hints

  • The C# client API and the C# integration test suite IntegrationTestsCore remains the same, and the results remain the same while the service has altered the way of serialization for Int64, UInt64 and BigInteger, since the System.Net.Http.HttpClient is handling well both ways of serialization: JSON number object or JSON string object.

Integration Tests with JavaScript / TypeScript Clients

This test suite uses string for integral numbers of 64-bit, 128-bit and BigInt when talking to ASP.NET Core Web API which provides decent Web API data binding upon JSON number object and JSON string object that represent a number.

Remarks

  • You should find out if your backend developed on PHP, Java, Go or Python, etc. could provide such ability of Web API data binding, probably through a similar test suite.

The following test cases are based on Angular 5+ codes and Karma.

Source codes of types with properties of various integral types:

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

Source codes of test suite "Numbers API":

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

As you can see, now the JavaScript / TypeScript codes with the same client API used in article ""Dealing with large integral numbers in JavaScript for integral types of ASP.NET Core Web API" can handle large integral number correctly and comfortably.

Summary

To make the JavaScript / TypeScript clients handle large integral number correctly and comfortably with the least programming efforts on the backend and the JS/TS client frontend, do the following.

Backend

  1. Import NuGet package: Fonlow.IntegralExtensions and inject respective JSON converters in the start-up codes.
  2. Check ModelState in every Web API function calls. This will avoid the Web API return 0 when the clients have provided invalid integral numbers. This is optional since you may have good reasons not to check ModelState in every Web API function calls.
  3. To generate TypeScript client API codes conforming to the "Universal" solutions for TypeScript clients suggested in the prior article, import NuGet packages: Fonlow.WebApiClientGenCore v7.2 or above, and Fonlow.WebApiClientGenCore.NG2FormGroup v1.5 or above if you are using Angular and Reactive Forms. And for other JavaScript libraries, please check the references below.

Hints:

  • To check ModelState in every Web API function calls, you will find many good posts when googling "ASP.NET Web API ModelState filter".

TypeScript Code Generators for Various JavaScript Libraries

  1. jQuery and HttpClient helper library
  2. AXIOS
  3. Fetch API
  4. Aurelia
  5. Angular 6+
  6. Angular 6+, plus FormGroup creation for Reactive Forms with Description
Articles / Tutorials

Frontend

With the generated client API codes in TypeScript, application programming becomes straightforward and easy for small and large integral types.

Generated Hints Based on .NET Integral Data Types

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

A decent IDE should display respective doc comment beside your finger tips.

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

Hints:

  • Hints based on .NET integral data types are generated only when the .NET class property has no user defined doc comment and validation attributes, while the TypeScript code generators may generate JS doc comments based on the .NET class property doc comment and decorated validation attributes.

Contain Large Integral Numbers through BigInt

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

Some bug/defect of JavaScript BigInt is well contained in this landscape.

Runtime Validation with Angular Reactive Forms

TypeScript
export function CreateIntegralEntityFormGroup() {
    return new FormGroup<IntegralEntityFormProperties>({
        emailAddress: new FormControl<string | null | undefined>(undefined, 
                      [Validators.email, Validators.maxLength(255)]),
        id: new FormControl<string | null | undefined>(undefined),
        name: new FormControl<string | null | undefined>(undefined, 
         [Validators.required, Validators.minLength(2), Validators.maxLength(255)]),
        web: new FormControl<string | null | undefined>(undefined, 
         [Validators.pattern('https?:\\/\\/(www\\.)?[-a-zA-Z0-9@:%._\\+~#=]
         {1,256}\\.[a-zA-Z0-9()]{1,6}\\b([-a-zA-Z0-9()@:%_\\+.~#?&//=]*)')]),
        byte: new FormControl<number | null | undefined>(undefined, 
         [Validators.min(0), Validators.max(256)]),
        int: new FormControl<number | null | undefined>(undefined, 
         [Validators.min(-2147483648), Validators.max(2147483647)]),
        itemCount: new FormControl<number | null | undefined>(undefined, 
         [Validators.min(-1000), Validators.max(1000000)]),
        sByte: new FormControl<number | null | undefined>(undefined, 
         [Validators.min(-127), Validators.max(127)]),
        short: new FormControl<number | null | undefined>(undefined, 
         [Validators.min(-32768), Validators.max(32767)]),
        uInt: new FormControl<number | null | undefined>(undefined, 
         [Validators.min(0), Validators.max(4294967295)]),
        uShort: new FormControl<number | null | undefined>(undefined, 
         [Validators.min(0), Validators.max(65535)]),
    });
}

History

  • 23rd February, 2024: Initial version

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 --