Table of Contents

Batch Request Mapper

The batch request mapper translates an entire list of request models into a single provider DTO representing the whole batch, not a list of individual DTOs. Implement IBatchApiRequestMapper<TRequest, TApiRequest> with a single Map method.

[SuppressMessage("Performance", "CA1812:Avoid uninstantiated internal classes", Justification = "Instantiated by DI container")]
internal sealed class BatchAddressValidationRequestMapper : IBatchApiRequestMapper<MyAddressValidationRequest, ApiRequest>
{
    public ApiRequest Map(IReadOnlyList<MyAddressValidationRequest> requests)
    {
        ArgumentNullException.ThrowIfNull(requests);

        return new ApiRequest
        {
            Addresses = [.. requests.Select(MyAddressPayloadMapper.Map),],
        };
    }
}
Note

If mapping a single request to its payload shape is shared between the singular and batch request mappers, factor it into an internal static helper (MyAddressPayloadMapper.Map above) and call it from both. This is how the FedEx integration avoids duplicating per-address mapping logic.

Note

Some providers accept only one value for fields that would otherwise be per-request, such as a transaction or correlation identifier. When that happens, take the value from the first request and document the behavior on the affected request property, as the FedEx integration does on CustomerTransactionId:

/// <remarks>
///     When submitted via <see cref="IBatchAddressValidationService{TRequest}" />, the provider accepts only
///     one transaction identifier for the entire batch call, so only the value set on the first request is
///     transmitted.
/// </remarks>
public string? TransactionId { get; set; }

Do not assume a per-item identifier submitted on the request (such as FedEx's ClientReferenceId) is echoed back on the provider's response unless you have confirmed it against the provider's actual response schema. If it isn't, say so on that property's doc comment, since the batch response mapper has no way to use it and must rely on positional correlation instead.

Batch Response Mapper

The batch response mapper converts one item out of the provider's batch response into a unified IAddressValidationResponse, selected by position. Implement IBatchApiResponseMapper<TApiResponse> with two methods: GetSharedCustomResponseData and Map.

[SuppressMessage("Performance", "CA1812:Avoid uninstantiated internal classes", Justification = "Instantiated by DI container")]
internal sealed class BatchAddressValidationResponseMapper : IBatchApiResponseMapper<ApiResponse>
{
    public IReadOnlyDictionary<string, object?> GetSharedCustomResponseData(ApiResponse response)
    {
        ArgumentNullException.ThrowIfNull(response);

        return response.GetCustomResponseData();
    }

    public IAddressValidationResponse Map(ApiResponse response, int index, IReadOnlyDictionary<string, object?> sharedCustomResponseData, IValidationResult? validationResult = null)
    {
        ArgumentNullException.ThrowIfNull(response);
        ArgumentNullException.ThrowIfNull(sharedCustomResponseData);
        ArgumentOutOfRangeException.ThrowIfNegative(index);

        if ( response.Results is null || index >= response.Results.Length )
        {
            return new EmptyAddressValidationResponse(validationResult);
        }

        ApiResponse.ResultPayload result = response.Results[index];
        Dictionary<string, object?> customResponseData = new(sharedCustomResponseData, StringComparer.OrdinalIgnoreCase);
        customResponseData.Merge(result.GetCustomResponseData());

        return new AddressValidationResponse(response, validationResult)
        {
            AddressLines = result.AddressLines is not null
                               ? result.AddressLines.ToFrozenSet(StringComparer.OrdinalIgnoreCase)
                               : FrozenSet<string>.Empty,
            CityOrTown = result.City,
            StateOrProvince = result.State,
            PostalCode = result.PostalCode,
            CustomResponseData = customResponseData.AsReadOnly(),
        };
    }
}
Note

index is the position of the item within the provider's response, not the original caller-facing index of the request. The batch validation service only calls Map for items that were actually sent to the provider (locally-invalid requests are already resolved to an EmptyAddressValidationResponse before the API call), so index always lines up with the provider response array.

Note

response is the same instance for every item in a batch call, so any response-level data it exposes is invariant across the whole batch. The batch validation service calls GetSharedCustomResponseData exactly once per batch — not once per item — and passes the result into every Map call as sharedCustomResponseData. If the response type has no response-level custom data (see Data Models), return an empty dictionary.

Note

When response fields are shared between the singular and batch response DTOs, factor the per-item mapping logic (including GetCustomResponseData() handling, see Data Models) into an internal static helper and call it from both the singular and batch response mappers.

Note

Mark both batch mappers internal. This reduces the public API surface if you redistribute the integration as a library.