Skip to content

SDK Design Guidelines

Ben Foster edited this page Oct 20, 2021 · 11 revisions

This page covers our high level design guidelines when contributing to the TrueLayer SDK for .NET. This is not an exhaustive list and in general, the best examples can be taken from the existing code.

For general SDK standards, head over the sdk-standards repo.

Use of record types

Record types benefit from immutable characteristics, support value-based equality and can be easily inspected. Use record types for API response objects and optionally for any immutable value types on requests.

Favour shorthand definitions where possible ensuring that property descriptions are provided and nullable properties are annotated accordingly:

    /// <summary>
    /// Represents a user account within a wallet
    /// </summary>
    /// <param name="AccountId">The account identifier</param>
    /// <param name="Iban">The International Bank Account Number (IBAN) of the account</param>
    /// <param name="Name">The name of the account holder</param>
    public record UserAcccount(Guid AccountId, string Iban, string? Name);

When explicitly declaring record types, use the null forgiving operator to indicate fields that are known to be provided (never null) in API responses and satisfy the compiler when running in a nullable context:

    public record AccountBalance
    {
        /// <summary>
        /// The currency of the account. Accounts can only hold 1 currency each.
        /// </summary>
        public string Currency { get; init; } = null!;

        /// <summary>
        /// The IBAN of the account that can be used to send funds to the account.
        /// </summary>
        public string Iban { get; init; } = null!;
    }

Classes should be used for any mutable types, most commonly API request objects.

Handling mandatory and optional fields

All API request fields that are mandatory should be provided via constructor parameters and validated accordingly. Validation only need cover the presence of a reasonable value. It should not attempt to duplicate complex validation covered by the corresponding API. Mandatory fields should be exposed as public readonly properties.

Optional fields should be exposed as publicly settable properties and should be annotated as nullable.

public sealed record MerchantAccount : IDiscriminated
{
    /// <summary>
    /// Creates a new <see cref="MerchantAccount"/>
    /// </summary>
    /// <param name="id">Your TrueLayer merchant account identifier</param>
    /// <returns></returns>
    public MerchantAccount(string id)
    {
        Id = id.NotNullOrWhiteSpace(nameof(id));
    }

    public string Type => "merchant_account";

    /// <summary>
    /// Gets the TrueLayer merchant account identifier
    /// </summary>
    public string Id { get; }

    /// <summary>
    /// The name of the beneficiary. 
    /// If unspecified, the API will use the account owner name associated to the selected merchant account.
    /// </summary>
    public string? Name { get; init; }
}

Naming Conventions

Where possible, use the same language, terms and descriptors that are used in our APIs and associated public documentation. Someone who is already familiar with our APIs should be able to easily navigate the SDK.

When creating types that represent the request and response schemas of API endpoints:

  • Request types should be named according to the action being attempted or resource that will be created suffixed with Request for example, CreatePaymentRequest or ActivateCustomerRequest or QueryTransactionsRequest
  • Response types should either mirror the name of the request suffixed with Response or in the case of endpoints that return the full representation of a resource, may skip the suffix for example, CreatePaymentResponse, ActivateCustomerResponse or Customer

Nesting types / multiple definitions per file

Multiple type definitions per file can be used when:

  • The types are nested below a parent type that corresponds with the name of the file e.g. CreatePaymentRequest
  • The types are only used within the context of a parent
  • For union type definitions to aid discoverability e.g. PaymentMethods.ExternalAccount

It may be necessary to suffix the name of nested types to avoid conflicts with property names. In these cases you favour prefixing the type with the parent entity name or if more appropriate a Request, Details, Summary or Response suffix, for example:

    public class RegisterCustomerRequest
    {
        public string? Name { get; set; }
        public CustomerAddress? Address { get; set; }

        public class CustomerAddress
        {
            // ...
        }
    }

Enums

TrueLayer APIs return enum values in snake_case format. Due to the current limitations of the System.Text.Json library when round-tripping custom enum formats, enum fields should defined as string properties. This is also less error prone if new enum values are returned by the API that have not yet been defined in the SDK.

In cases where the values need to be specified a set of constants should be provided in a file named [EnumType]s (note the pluralization to avoid conflict with genuine enum types).

    public static class CustomerSegments
    {
        public const string Retail = "retail";
        public const string Business = "business";
        public const string Corporate = "corporate";
    }

Use of Union Types

APIs that expose fields with multiple schemas, denoted by the Open API oneOf keyword should be represented by the OneOf<> type (part of the OneOf library):

    public static class GetPaymentResponse
    {
        public record PaymentDetails
        {
            public string Id { get; init; } = null!;
            public long AmountInMinor { get; init; }
            public string Currency { get; init; } = null!;
            public string Status { get; init; } = null!;
            public DateTime CreatedAt { get; init; }
            public OneOf<MerchantAccount, ExternalAccount> Beneficiary { get; init; }
            public OneOf<BankTransfer> PaymentMethod { get; init; }
        }

In cases where we currently support only a single schema but expect this to be extended in the future you can specify a single type (see PaymentMethod above). This sets the expectations to library consumers that additional types can be expected in the future.

Union types are discriminated by the type JSON field. The discriminator value should be set by decorating the relevant type with the [JsonDiscriminator] attribute:

        [JsonDiscriminator("merchant_account")]
        public sealed record MerchantAccount : IDiscriminated
        {