feat: Use JsonSerializationContext for all types, enable trimming (#14)

This commit is contained in:
Valters Melnalksnis
2022-08-29 22:12:09 +03:00
parent 5666917676
commit 455bebb7b4
30 changed files with 501 additions and 336 deletions
@@ -75,12 +75,32 @@ public static class ServiceCollectionExtensions
.AddSingleton<NordigenTokenCache>()
.AddSingleton<NordigenJsonSerializerOptions>()
.AddTransient<INordigenClient, NordigenClient>()
.AddTransient<IAccountClient, AccountClient>()
.AddTransient<IAgreementClient, AgreementClient>()
.AddTransient<IInstitutionClient, InstitutionClient>()
.AddTransient<IRequisitionClient, RequisitionClient>()
.AddTransient<IAccountClient, AccountClient>(provider =>
{
var httpClient = provider.GetRequiredService<IHttpClientFactory>().CreateClient(NordigenOptions.SectionName);
var options = provider.GetRequiredService<NordigenJsonSerializerOptions>();
return new(httpClient, options);
})
.AddTransient<IAgreementClient, AgreementClient>(provider =>
{
var httpClient = provider.GetRequiredService<IHttpClientFactory>().CreateClient(NordigenOptions.SectionName);
var options = provider.GetRequiredService<NordigenJsonSerializerOptions>();
return new(httpClient, options);
})
.AddTransient<IInstitutionClient, InstitutionClient>(provider =>
{
var httpClient = provider.GetRequiredService<IHttpClientFactory>().CreateClient(NordigenOptions.SectionName);
var options = provider.GetRequiredService<NordigenJsonSerializerOptions>();
return new(httpClient, options);
})
.AddTransient<IRequisitionClient, RequisitionClient>(provider =>
{
var httpClient = provider.GetRequiredService<IHttpClientFactory>().CreateClient(NordigenOptions.SectionName);
var options = provider.GetRequiredService<NordigenJsonSerializerOptions>();
return new(httpClient, options);
})
.AddTransient(provider => provider.GetRequiredService<IOptionsMonitor<NordigenOptions>>().CurrentValue)
.AddHttpClient<NordigenHttpClient>(ConfigureNordigenClient)
.AddHttpClient(NordigenOptions.SectionName, ConfigureNordigenClient)
.AddHttpMessageHandler<TokenDelegatingHandler>();
}
@@ -12,16 +12,25 @@ using VMelnalksnis.NordigenDotNet.Institutions;
namespace VMelnalksnis.NordigenDotNet.Accounts;
/// <summary>Information about the account record, such as the processing status and IBAN.</summary>
/// <param name="Id">The ID of this Account, used to refer to this account in other API calls.</param>
/// <param name="Created">The point in time when the account object was created.</param>
/// <param name="LastAccessed">The point in time when the account object was last accessed.</param>
/// <param name="Iban">The account IBAN.</param>
/// <param name="InstitutionId">The id of the <see cref="Institution"/> associated with the account.</param>
/// <param name="Status">The processing status of this account.</param>
public record Account(
Guid Id,
Instant Created,
[property: JsonPropertyName("last_accessed")] Instant LastAccessed,
string Iban,
[property: JsonPropertyName("institution_id")] string InstitutionId,
AccountStatus Status);
public record Account
{
/// <summary>Gets or sets the id of this Account, used to refer to this account in other API calls.</summary>
public Guid Id { get; set; }
/// <summary>Gets or sets the point in time when the account object was created.</summary>
public Instant Created { get; set; }
/// <summary>Gets or sets the point in time when the account object was last accessed.</summary>
[JsonPropertyName("last_accessed")]
public Instant LastAccessed { get; set; }
/// <summary>Gets or sets the account IBAN.</summary>
public string Iban { get; set; } = null!;
/// <summary>Gets or sets the id of the <see cref="Institution"/> associated with the account.</summary>
[JsonPropertyName("institution_id")]
public string InstitutionId { get; set; } = null!;
/// <summary>Gets or sets the processing status of this account.</summary>
public AccountStatus Status { get; set; }
}
@@ -4,6 +4,9 @@
using System;
using System.Collections.Generic;
using System.Net.Http;
using System.Net.Http.Json;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
@@ -14,30 +17,29 @@ namespace VMelnalksnis.NordigenDotNet.Accounts;
/// <inheritdoc />
public sealed class AccountClient : IAccountClient
{
private readonly NordigenHttpClient _nordigenHttpClient;
private readonly HttpClient _httpClient;
private readonly NordigenSerializationContext _context;
/// <summary>Initializes a new instance of the <see cref="AccountClient"/> class.</summary>
/// <param name="nordigenHttpClient">Http client configured for making requests to the Nordigen API.</param>
public AccountClient(NordigenHttpClient nordigenHttpClient)
/// <param name="httpClient">Http client configured for making requests to the Nordigen API.</param>
/// <param name="serializerOptions">Nordigen specific instance of <see cref="JsonSerializerOptions"/>.</param>
public AccountClient(HttpClient httpClient, NordigenJsonSerializerOptions serializerOptions)
{
_nordigenHttpClient = nordigenHttpClient;
_httpClient = httpClient;
_context = serializerOptions.Context;
}
/// <inheritdoc />
public async Task<Account> Get(Guid id, CancellationToken cancellationToken = default)
public Task<Account> Get(Guid id, CancellationToken cancellationToken = default)
{
var account = await _nordigenHttpClient
.Get<Account>(Routes.Accounts.IdUri(id), cancellationToken)
.ConfigureAwait(false);
return account!;
return _httpClient.GetFromJsonAsync(Routes.Accounts.IdUri(id), _context.Account, cancellationToken)!;
}
/// <inheritdoc />
public async Task<List<Balance>> GetBalances(Guid id, CancellationToken cancellationToken = default)
{
var balances = await _nordigenHttpClient
.Get<BalancesWrapper>(Routes.Accounts.BalancesUri(id), cancellationToken)
var balances = await _httpClient
.GetFromJsonAsync(Routes.Accounts.BalancesUri(id), _context.BalancesWrapper, cancellationToken)
.ConfigureAwait(false);
return balances!.Balances;
@@ -46,8 +48,8 @@ public sealed class AccountClient : IAccountClient
/// <inheritdoc />
public async Task<AccountDetails> GetDetails(Guid id, CancellationToken cancellationToken = default)
{
var details = await _nordigenHttpClient
.Get<AccountDetailsWrapper>(Routes.Accounts.DetailsUri(id), cancellationToken)
var details = await _httpClient
.GetFromJsonAsync(Routes.Accounts.DetailsUri(id), _context.AccountDetailsWrapper, cancellationToken)
.ConfigureAwait(false);
return details!.Account;
@@ -59,10 +61,8 @@ public sealed class AccountClient : IAccountClient
Interval? interval = null,
CancellationToken cancellationToken = default)
{
var uri = Routes.Accounts.TransactionsUri(id, interval);
var transactions = await _nordigenHttpClient
.Get<TransactionsWrapper>(uri, cancellationToken)
var transactions = await _httpClient
.GetFromJsonAsync(Routes.Accounts.TransactionsUri(id, interval), _context.TransactionsWrapper, cancellationToken)
.ConfigureAwait(false);
return transactions!.Transactions;
@@ -5,13 +5,34 @@
namespace VMelnalksnis.NordigenDotNet.Accounts;
/// <summary>Additional details of an account.</summary>
public record AccountDetails(
string ResourceId,
string Iban,
string Currency,
string OwnerName,
string Name,
string Product,
string CashAccountType);
public record AccountDetails
{
/// <summary>Gets or sets the account id of the given account in the financial institution.</summary>
public string ResourceId { get; set; } = null!;
internal record AccountDetailsWrapper(AccountDetails Account);
/// <summary>Gets or sets the IBAN of the account.</summary>
public string? Iban { get; set; }
/// <summary>Gets or sets a ISO 4217 currency code.</summary>
public string Currency { get; set; } = null!;
/// <summary>
/// Gets or sets the name of the legal account owner.
/// If there is more than one owner, then e.g. two names might be noted here.
/// For a corporate account, the corporate name is used for this attribute.</summary>
public string? OwnerName { get; set; }
/// <summary>Gets or sets the name of the account, as assigned by the financial institution.</summary>
public string? Name { get; set; }
/// <summary>Gets or sets product Name of the Bank for this account, proprietary definition.</summary>
public string? Product { get; set; }
/// <summary>Gets or sets externalCashAccountType1Code from ISO 20022.</summary>
public string? CashAccountType { get; set; }
}
internal class AccountDetailsWrapper
{
public AccountDetails Account { get; set; } = null!;
}
@@ -5,6 +5,11 @@
namespace VMelnalksnis.NordigenDotNet.Accounts;
/// <summary>An amount in a specific currency.</summary>
/// <param name="Currency">A ISO 4217 currency code.</param>
/// <param name="Amount">The amount.</param>
public record AmountInCurrency(string Currency, decimal Amount);
public record AmountInCurrency
{
/// <summary>Gets or sets a ISO 4217 currency code.</summary>
public string Currency { get; set; } = null!;
/// <summary>Gets or sets the amount.</summary>
public decimal Amount { get; set; }
}
@@ -6,18 +6,25 @@ using System.Collections.Generic;
using NodaTime;
#pragma warning disable SA1623
namespace VMelnalksnis.NordigenDotNet.Accounts;
/// <summary>Account balance.</summary>
/// <param name="BalanceAmount">The balance amount.</param>
/// <param name="BalanceType">The balance type.</param>
/// <param name="CreditLimitIncluded">Whether the credit limit is included in <paramref name="BalanceAmount"/>.</param>
public record Balance(AmountInCurrency BalanceAmount, string BalanceType, bool CreditLimitIncluded)
public record Balance
{
/// <summary>The date on which the balance was calculated.</summary>
public LocalDate? ReferenceDate { get; init; }
/// <summary>Gets or sets the balance amount.</summary>
public AmountInCurrency BalanceAmount { get; set; } = null!;
/// <summary>Gets or sets the balance type.</summary>
public string BalanceType { get; set; } = null!;
/// <summary>Gets or sets a value indicating whether whether the credit limit is included in <see cref="BalanceAmount"/>.</summary>
public bool CreditLimitIncluded { get; set; }
/// <summary>Gets or sets the date on which the balance was calculated.</summary>
public LocalDate? ReferenceDate { get; set; }
}
internal record BalancesWrapper(List<Balance> Balances);
internal class BalancesWrapper
{
public List<Balance> Balances { get; set; } = null!;
}
@@ -6,50 +6,45 @@ using NodaTime;
using VMelnalksnis.NordigenDotNet.Institutions;
#pragma warning disable SA1623
namespace VMelnalksnis.NordigenDotNet.Accounts;
/// <summary>A transaction that has been booked - posted to an account on the account servicer's books.</summary>
/// <param name="TransactionAmount">The amount transferred in this transaction.</param>
/// <param name="UnstructuredInformation">Unstructured information about the transaction, usually added by the debtor.</param>
/// <param name="TransactionId">A unique transaction id created by the <see cref="Institution"/>.</param>
/// <param name="BookingDate">The date when an entry is posted to an account on the account servicer's books.</param>
public record BookedTransaction(
AmountInCurrency TransactionAmount,
string UnstructuredInformation,
string TransactionId,
LocalDate BookingDate)
: Transaction(TransactionAmount, UnstructuredInformation)
public record BookedTransaction : Transaction
{
/// <summary>The name of the counterparty that sends <see cref="Transaction.TransactionAmount"/> during the transaction.</summary>
public string? DebtorName { get; init; }
/// <summary>Gets or sets a unique transaction id created by the <see cref="Institution"/>.</summary>
public string TransactionId { get; set; } = null!;
/// <summary>The account of the counterparty that sends <see cref="Transaction.TransactionAmount"/> during the transaction.</summary>
public TransactionAccount? DebtorAccount { get; init; }
/// <summary>Gets or sets the date when an entry is posted to an account on the account servicer's books.</summary>
public LocalDate BookingDate { get; set; }
/// <summary>The name of the counterparty that receives <see cref="Transaction.TransactionAmount"/> during the transaction.</summary>
public string? CreditorName { get; init; }
/// <summary>Gets or sets the name of the counterparty that sends <see cref="Transaction.TransactionAmount"/> during the transaction.</summary>
public string? DebtorName { get; set; }
/// <summary>The account of the counterparty that receives <see cref="Transaction.TransactionAmount"/> during the transaction.</summary>
public TransactionAccount? CreditorAccount { get; init; }
/// <summary>Gets or sets the account of the counterparty that sends <see cref="Transaction.TransactionAmount"/> during the transaction.</summary>
public TransactionAccount? DebtorAccount { get; set; }
/// <summary>The ISO 20022 bank transaction code.</summary>
/// <summary>Gets or sets the name of the counterparty that receives <see cref="Transaction.TransactionAmount"/> during the transaction.</summary>
public string? CreditorName { get; set; }
/// <summary>Gets or sets the account of the counterparty that receives <see cref="Transaction.TransactionAmount"/> during the transaction.</summary>
public TransactionAccount? CreditorAccount { get; set; }
/// <summary>Gets or sets the ISO 20022 bank transaction code.</summary>
/// <example>Some example values:
/// <code>
/// PMNT-ICDT-STDO
/// PMNT-IRCT-STDO
/// </code></example>
public string? BankTransactionCode { get; init; }
public string? BankTransactionCode { get; set; }
/// <summary>A transaction id, used both by the transaction and any fees paid to the <see cref="Institution"/> for the transaction.</summary>
public string? EntryReference { get; init; }
/// <summary>Gets or sets a transaction id, used both by the transaction and any fees paid to the <see cref="Institution"/> for the transaction.</summary>
public string? EntryReference { get; set; }
/// <summary>Additional structured information about the transaction from the institution.</summary>
/// <summary>Gets or sets additional structured information about the transaction from the institution.</summary>
/// <example>
/// <code>
/// PURCHASE
/// INWARD TRANSFER
/// </code></example>
public string? AdditionalInformation { get; init; }
public string? AdditionalInformation { get; set; }
}
@@ -5,7 +5,4 @@
namespace VMelnalksnis.NordigenDotNet.Accounts;
/// <summary>A transaction that is still pending.</summary>
/// <param name="TransactionAmount">The amount transferred in this transaction.</param>
/// <param name="UnstructuredInformation">Unstructured information about the transaction, usually added by the debtor.</param>
public record PendingTransaction(AmountInCurrency TransactionAmount, string UnstructuredInformation)
: Transaction(TransactionAmount, UnstructuredInformation);
public record PendingTransaction : Transaction;
@@ -6,17 +6,18 @@ using System.Text.Json.Serialization;
using NodaTime;
#pragma warning disable SA1623
namespace VMelnalksnis.NordigenDotNet.Accounts;
/// <summary>Common information for all transactions.</summary>
/// <param name="TransactionAmount">The amount transferred in this transaction.</param>
/// <param name="UnstructuredInformation">Unstructured information about the transaction, usually added by the debtor.</param>
public abstract record Transaction(
AmountInCurrency TransactionAmount,
[property: JsonPropertyName("remittanceInformationUnstructured")] string UnstructuredInformation)
public abstract record Transaction
{
/// <summary>The date when the transaction was valued at.</summary>
public LocalDate? ValueDate { get; init; }
/// <summary>Gets or sets the amount transferred in this transaction.</summary>
public AmountInCurrency TransactionAmount { get; set; } = null!;
/// <summary>Gets or sets unstructured information about the transaction, usually added by the debtor.</summary>
[JsonPropertyName("remittanceInformationUnstructured")]
public string UnstructuredInformation { get; set; } = null!;
/// <summary>Gets or sets the date when the transaction was valued at.</summary>
public LocalDate? ValueDate { get; set; }
}
@@ -5,5 +5,8 @@
namespace VMelnalksnis.NordigenDotNet.Accounts;
/// <summary>An account that can be referenced from a <see cref="Transaction"/>.</summary>
/// <param name="Iban">The IBAN of the account.</param>
public record TransactionAccount(string Iban);
public record TransactionAccount
{
/// <summary>Gets or sets the IBAN of the account.</summary>
public string Iban { get; set; } = null!;
}
@@ -7,8 +7,16 @@ using System.Collections.Generic;
namespace VMelnalksnis.NordigenDotNet.Accounts;
/// <summary>All transactions of an account.</summary>
/// <param name="Booked">All transactions that have been booked.</param>
/// <param name="Pending">All transaction that are still pending.</param>
public record Transactions(List<BookedTransaction> Booked, List<PendingTransaction> Pending);
public record Transactions
{
/// <summary>Gets or sets all transactions that have been booked.</summary>
public List<BookedTransaction> Booked { get; set; } = null!;
internal record TransactionsWrapper(Transactions Transactions);
/// <summary>Gets or sets all transaction that are still pending.</summary>
public List<PendingTransaction> Pending { get; set; } = null!;
}
internal class TransactionsWrapper
{
public Transactions Transactions { get; set; } = null!;
}
@@ -4,6 +4,9 @@
using System;
using System.Collections.Generic;
using System.Net.Http;
using System.Net.Http.Json;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
@@ -12,54 +15,56 @@ namespace VMelnalksnis.NordigenDotNet.Agreements;
/// <inheritdoc />
public sealed class AgreementClient : IAgreementClient
{
private readonly NordigenHttpClient _nordigenHttpClient;
private readonly HttpClient _httpClient;
private readonly NordigenSerializationContext _context;
/// <summary>Initializes a new instance of the <see cref="AgreementClient"/> class.</summary>
/// <param name="nordigenHttpClient">Http client configured for making requests to the Nordigen API.</param>
public AgreementClient(NordigenHttpClient nordigenHttpClient)
/// <param name="httpClient">Http client configured for making requests to the Nordigen API.</param>
/// <param name="serializerOptions">Nordigen specific instance of <see cref="JsonSerializerOptions"/>.</param>
public AgreementClient(HttpClient httpClient, NordigenJsonSerializerOptions serializerOptions)
{
_nordigenHttpClient = nordigenHttpClient;
_httpClient = httpClient;
_context = serializerOptions.Context;
}
/// <inheritdoc />
public IAsyncEnumerable<EndUserAgreement> Get(int pageSize = 100, CancellationToken cancellationToken = default)
{
return _nordigenHttpClient.GetPaginated<EndUserAgreement>(Routes.Agreements.PaginatedUri(100), cancellationToken);
return _httpClient.GetPaginated(
Routes.Agreements.PaginatedUri(pageSize),
_context.PaginatedListEndUserAgreement,
cancellationToken);
}
/// <inheritdoc />
public async Task<EndUserAgreement> Get(Guid id, CancellationToken cancellationToken = default)
public Task<EndUserAgreement> Get(Guid id, CancellationToken cancellationToken = default)
{
var agreement = await _nordigenHttpClient
.Get<EndUserAgreement>(Routes.Agreements.IdUri(id), cancellationToken)
.ConfigureAwait(false);
return agreement!;
return _httpClient.GetFromJsonAsync(Routes.Agreements.IdUri(id), _context.EndUserAgreement, cancellationToken)!;
}
/// <inheritdoc />
public async Task<EndUserAgreement> Post(EndUserAgreementCreation agreementCreation)
public Task<EndUserAgreement> Post(EndUserAgreementCreation agreementCreation)
{
var agreement = await _nordigenHttpClient
.Post<EndUserAgreementCreation, EndUserAgreement>(Routes.Agreements.Uri, agreementCreation)
.ConfigureAwait(false);
return agreement!;
return _httpClient.Post(
Routes.Agreements.Uri,
agreementCreation,
_context.EndUserAgreementCreation,
_context.EndUserAgreement)!;
}
/// <inheritdoc />
public async Task Delete(Guid id)
public Task Delete(Guid id)
{
await _nordigenHttpClient.Delete(Routes.Agreements.IdUri(id)).ConfigureAwait(false);
return _httpClient.Delete(Routes.Agreements.IdUri(id));
}
/// <inheritdoc />
public async Task<EndUserAgreement> Put(Guid id, EndUserAgreementAcceptance acceptance)
public Task<EndUserAgreement> Put(Guid id, EndUserAgreementAcceptance acceptance)
{
var agreement = await _nordigenHttpClient
.Put<EndUserAgreementAcceptance, EndUserAgreement>(Routes.Agreements.AcceptUri(id), acceptance)
.ConfigureAwait(false);
return agreement!;
return _httpClient.Put(
Routes.Agreements.AcceptUri(id),
acceptance,
_context.EndUserAgreementAcceptance,
_context.EndUserAgreement)!;
}
}
@@ -10,25 +10,33 @@ using NodaTime;
using VMelnalksnis.NordigenDotNet.Institutions;
#pragma warning disable SA1623
namespace VMelnalksnis.NordigenDotNet.Agreements;
/// <summary>Details the end-user has agreement to share from a specific institution.</summary>
/// <param name="Id">The id of the end-user agreement.</param>
/// <param name="Created">The point in time when the agreement was created.</param>
/// <param name="MaxHistoricalDays">Maximum number of days of transaction data to retrieve.</param>
/// <param name="AccessValidForDays">Number of days from acceptance that the access can be used.</param>
/// <param name="AccessScope">Level of information to access.</param>
/// <param name="InstitutionId">An <see cref="Institution"/> ID for the agreement.</param>
public record EndUserAgreement(
Guid Id,
Instant Created,
[property: JsonPropertyName("max_historical_days")] int MaxHistoricalDays,
[property: JsonPropertyName("access_valid_for_days")] int AccessValidForDays,
[property: JsonPropertyName("access_scope")] List<string> AccessScope,
[property: JsonPropertyName("institution_id")] string InstitutionId)
public record EndUserAgreement
{
/// <summary>The point int time when the end user accepted the agreement.</summary>
public Instant? Accepted { get; init; }
/// <summary>Gets or sets the id of the end-user agreement.</summary>
public Guid Id { get; set; }
/// <summary>Gets or sets the point in time when the agreement was created.</summary>
public Instant Created { get; set; }
/// <summary>Gets or sets maximum number of days of transaction data to retrieve.</summary>
[JsonPropertyName("max_historical_days")]
public int MaxHistoricalDays { get; set; }
/// <summary>Gets or sets number of days from acceptance that the access can be used.</summary>
[JsonPropertyName("access_valid_for_days")]
public int AccessValidForDays { get; set; }
/// <summary>Gets or sets level of information to access.</summary>
[JsonPropertyName("access_scope")]
public List<string> AccessScope { get; set; } = null!;
/// <summary>Gets or sets an <see cref="Institution"/> ID for the agreement.</summary>
[JsonPropertyName("institution_id")]
public string InstitutionId { get; set; } = null!;
/// <summary>Gets or sets the point int time when the end user accepted the agreement.</summary>
public Instant? Accepted { get; set; }
}
@@ -7,8 +7,13 @@ using System.Text.Json.Serialization;
namespace VMelnalksnis.NordigenDotNet.Agreements;
/// <summary>Details needed to accept an end-user agreement.</summary>
/// <param name="UserAgent">User agent string for the end user.</param>
/// <param name="IpAddress">End user IP address.</param>
public record EndUserAgreementAcceptance(
[property: JsonPropertyName("user_agent")] string UserAgent,
[property: JsonPropertyName("ip_address")] string IpAddress);
public record EndUserAgreementAcceptance(string UserAgent, string IpAddress)
{
/// <summary>Gets user agent string for the end user.</summary>
[JsonPropertyName("user_agent")]
public string UserAgent { get; } = UserAgent;
/// <summary>Gets end user IP address.</summary>
[JsonPropertyName("ip_address")]
public string IpAddress { get; } = IpAddress;
}
@@ -7,8 +7,6 @@ using System.Text.Json.Serialization;
using VMelnalksnis.NordigenDotNet.Institutions;
#pragma warning disable SA1623
namespace VMelnalksnis.NordigenDotNet.Agreements;
/// <summary>Details needed to create a new end-user agreement.</summary>
@@ -38,19 +36,19 @@ public record EndUserAgreementCreation
{
}
/// <summary>An <see cref="Institution"/> id for this agreement.</summary>
/// <summary>Gets an <see cref="Institution"/> id for this agreement.</summary>
[JsonPropertyName("institution_id")]
public string InstitutionId { get; }
/// <summary>Maximum number of days of transaction data to retrieve.</summary>
/// <summary>Gets maximum number of days of transaction data to retrieve.</summary>
[JsonPropertyName("max_historical_days")]
public int MaxHistoricalDays { get; }
/// <summary>Number of days from acceptance that the access can be used.</summary>
/// <summary>Gets number of days from acceptance that the access can be used.</summary>
[JsonPropertyName("access_valid_for_days")]
public int AccessValidForDays { get; }
/// <summary>Level of information to access.</summary>
/// <summary>Gets level of information to access.</summary>
[JsonPropertyName("access_scope")]
public List<string> AccessScope { get; }
}
@@ -0,0 +1,71 @@
// Copyright 2022 Valters Melnalksnis
// Licensed under the Apache License 2.0.
// See LICENSE file in the project root for full license information.
using System.Collections.Generic;
using System.Net.Http;
using System.Net.Http.Json;
using System.Runtime.CompilerServices;
using System.Text.Json.Serialization.Metadata;
using System.Threading;
using System.Threading.Tasks;
namespace VMelnalksnis.NordigenDotNet;
internal static class HttpClientExtensions
{
internal static async IAsyncEnumerable<TResult> GetPaginated<TResult>(
this HttpClient httpClient,
string requestUri,
JsonTypeInfo<PaginatedList<TResult>> typeInfo,
[EnumeratorCancellation] CancellationToken cancellationToken)
where TResult : class
{
var next = requestUri;
while (next is not null && !cancellationToken.IsCancellationRequested)
{
var paginatedList = await httpClient.GetFromJsonAsync(next, typeInfo, cancellationToken).ConfigureAwait(false);
if (paginatedList?.Results is null)
{
yield break;
}
foreach (var result in paginatedList.Results)
{
yield return result;
}
next = paginatedList.Next?.PathAndQuery;
}
}
internal static async Task<TResult?> Post<TRequest, TResult>(
this HttpClient httpClient,
string requestUri,
TRequest request,
JsonTypeInfo<TRequest> requestTypeInfo,
JsonTypeInfo<TResult> resultTypeInfo)
{
var response = await httpClient.PostAsJsonAsync(requestUri, request, requestTypeInfo).ConfigureAwait(false);
await response.ThrowIfNotSuccessful().ConfigureAwait(false);
return await response.Content.ReadFromJsonAsync(resultTypeInfo).ConfigureAwait(false);
}
internal static async Task<TResult?> Put<TRequest, TResult>(
this HttpClient httpClient,
string requestUri,
TRequest request,
JsonTypeInfo<TRequest> requestTypeInfo,
JsonTypeInfo<TResult> resultTypeInfo)
{
var response = await httpClient.PutAsJsonAsync(requestUri, request, requestTypeInfo).ConfigureAwait(false);
await response.ThrowIfNotSuccessful().ConfigureAwait(false);
return await response.Content.ReadFromJsonAsync(resultTypeInfo).ConfigureAwait(false);
}
internal static async Task Delete(this HttpClient httpClient, string requestUri)
{
var response = await httpClient.DeleteAsync(requestUri).ConfigureAwait(false);
await response.ThrowIfNotSuccessful().ConfigureAwait(false);
}
}
@@ -9,16 +9,24 @@ using System.Text.Json.Serialization;
namespace VMelnalksnis.NordigenDotNet.Institutions;
/// <summary>Account servicing payment service provider details.</summary>
/// <param name="Id">The id of the service provider.</param>
/// <param name="Name">The name of the service provider.</param>
/// <param name="Bic">The BIC of the service provider.</param>
/// <param name="TransactionTotalDays">The maximum number of days in the past the service provider will return transactions for.</param>
/// <param name="Countries">All the countries supported by the service provider.</param>
/// <param name="Logo">URI to the service provider's logo.</param>
public record Institution(
string Id,
string Name,
string Bic,
[property: JsonPropertyName("transaction_total_days")] int TransactionTotalDays,
List<string> Countries,
Uri Logo);
public record Institution
{
/// <summary>Gets or sets the id of the service provider.</summary>
public string Id { get; set; } = null!;
/// <summary>Gets or sets the name of the service provider.</summary>
public string Name { get; set; } = null!;
/// <summary>Gets or sets the BIC of the service provider.</summary>
public string Bic { get; set; } = null!;
/// <summary>Gets or sets the maximum number of days in the past the service provider will return transactions for.</summary>
[JsonPropertyName("transaction_total_days")]
public int TransactionTotalDays { get; set; }
/// <summary>Gets or sets all the countries supported by the service provider.</summary>
public List<string> Countries { get; set; } = null!;
/// <summary>Gets or sets uRI to the service provider's logo.</summary>
public Uri Logo { get; set; } = null!;
}
@@ -3,6 +3,9 @@
// See LICENSE file in the project root for full license information.
using System.Collections.Generic;
using System.Net.Http;
using System.Net.Http.Json;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
@@ -11,32 +14,33 @@ namespace VMelnalksnis.NordigenDotNet.Institutions;
/// <inheritdoc />
public sealed class InstitutionClient : IInstitutionClient
{
private readonly NordigenHttpClient _nordigenHttpClient;
private readonly HttpClient _httpClient;
private readonly NordigenSerializationContext _context;
/// <summary>Initializes a new instance of the <see cref="InstitutionClient"/> class.</summary>
/// <param name="nordigenHttpClient">Http client configured for making requests to the Nordigen API.</param>
public InstitutionClient(NordigenHttpClient nordigenHttpClient)
/// <param name="httpClient">Http client configured for making requests to the Nordigen API.</param>
/// <param name="serializerOptions">Nordigen specific instance of <see cref="JsonSerializerOptions"/>.</param>
public InstitutionClient(HttpClient httpClient, NordigenJsonSerializerOptions serializerOptions)
{
_nordigenHttpClient = nordigenHttpClient;
_httpClient = httpClient;
_context = serializerOptions.Context;
}
/// <inheritdoc />
public async Task<List<Institution>> GetByCountry(string countryCode, CancellationToken cancellationToken = default)
public Task<List<Institution>> GetByCountry(string countryCode, CancellationToken cancellationToken = default)
{
var institutions = await _nordigenHttpClient
.Get<List<Institution>>(Routes.Institutions.CountryUri(countryCode), cancellationToken)
.ConfigureAwait(false);
return institutions!;
return _httpClient.GetFromJsonAsync(
Routes.Institutions.CountryUri(countryCode),
_context.ListInstitution,
cancellationToken)!;
}
/// <inheritdoc />
public async Task<Institution> Get(string id, CancellationToken cancellationToken = default)
public Task<Institution> Get(string id, CancellationToken cancellationToken = default)
{
var institution = await _nordigenHttpClient
.Get<Institution>(Routes.Institutions.IdUri(id), cancellationToken)
.ConfigureAwait(false);
return institution!;
return _httpClient.GetFromJsonAsync(
Routes.Institutions.IdUri(id),
_context.Institution,
cancellationToken)!;
}
}
@@ -1,77 +0,0 @@
// Copyright 2022 Valters Melnalksnis
// Licensed under the Apache License 2.0.
// See LICENSE file in the project root for full license information.
using System.Collections.Generic;
using System.Net.Http;
using System.Net.Http.Json;
using System.Runtime.CompilerServices;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
namespace VMelnalksnis.NordigenDotNet;
/// <summary><see cref="HttpClient"/> wrapper that uses the correct serialization settings for all requests.</summary>
public sealed class NordigenHttpClient
{
private readonly HttpClient _httpClient;
private readonly JsonSerializerOptions _serializerOptions;
/// <summary>Initializes a new instance of the <see cref="NordigenHttpClient"/> class.</summary>
/// <param name="httpClient">Http client configured for making requests to the Nordigen API.</param>
/// <param name="nordigenJsonSerializerOptions">Nordigen specific instance of <see cref="JsonSerializerOptions"/>.</param>
public NordigenHttpClient(HttpClient httpClient, NordigenJsonSerializerOptions nordigenJsonSerializerOptions)
{
_httpClient = httpClient;
_serializerOptions = nordigenJsonSerializerOptions.Options;
}
internal async Task<TResult?> Get<TResult>(string requestUri, CancellationToken cancellationToken)
{
return await _httpClient.GetFromJsonAsync<TResult>(requestUri, _serializerOptions, cancellationToken).ConfigureAwait(false);
}
internal async IAsyncEnumerable<TResult> GetPaginated<TResult>(
string requestUri,
[EnumeratorCancellation] CancellationToken cancellationToken)
where TResult : class
{
var next = requestUri;
while (next is not null && !cancellationToken.IsCancellationRequested)
{
var paginatedList = await Get<PaginatedList<TResult>>(next, cancellationToken).ConfigureAwait(false);
if (paginatedList?.Results is null)
{
yield break;
}
foreach (var result in paginatedList.Results)
{
yield return result;
}
next = paginatedList.Next?.PathAndQuery;
}
}
internal async Task<TResult?> Post<TRequest, TResult>(string requestUri, TRequest request)
{
var response = await _httpClient.PostAsJsonAsync(requestUri, request, _serializerOptions).ConfigureAwait(false);
await response.ThrowIfNotSuccessful().ConfigureAwait(false);
return await response.Content.ReadFromJsonAsync<TResult>(_serializerOptions).ConfigureAwait(false);
}
internal async Task<TResult?> Put<TRequest, TResult>(string requestUri, TRequest request)
{
var response = await _httpClient.PutAsJsonAsync(requestUri, request, _serializerOptions).ConfigureAwait(false);
await response.ThrowIfNotSuccessful().ConfigureAwait(false);
return await response.Content.ReadFromJsonAsync<TResult>(_serializerOptions).ConfigureAwait(false);
}
internal async Task Delete(string requestUri)
{
var response = await _httpClient.DeleteAsync(requestUri).ConfigureAwait(false);
await response.ThrowIfNotSuccessful().ConfigureAwait(false);
}
}
@@ -10,18 +10,20 @@ using NodaTime.Serialization.SystemTextJson;
namespace VMelnalksnis.NordigenDotNet;
/// <summary><see cref="JsonSerializerOptions"/> for <see cref="NordigenHttpClient"/>.</summary>
/// <summary><see cref="JsonSerializerOptions"/> for <see cref="INordigenClient"/>.</summary>
public sealed class NordigenJsonSerializerOptions
{
/// <summary>Initializes a new instance of the <see cref="NordigenJsonSerializerOptions"/> class.</summary>
/// <param name="dateTimeZoneProvider">Time zone provider for date and time serialization.</param>
public NordigenJsonSerializerOptions(IDateTimeZoneProvider dateTimeZoneProvider)
{
Options = new JsonSerializerOptions(JsonSerializerDefaults.Web)
var options = new JsonSerializerOptions(JsonSerializerDefaults.Web)
{
Converters = { new JsonStringEnumConverter() },
}.ConfigureForNodaTime(dateTimeZoneProvider);
Context = new(options);
}
internal JsonSerializerOptions Options { get; }
internal NordigenSerializationContext Context { get; }
}
@@ -0,0 +1,32 @@
// Copyright 2022 Valters Melnalksnis
// Licensed under the Apache License 2.0.
// See LICENSE file in the project root for full license information.
using System.Collections.Generic;
using System.Text.Json.Serialization;
using VMelnalksnis.NordigenDotNet.Accounts;
using VMelnalksnis.NordigenDotNet.Agreements;
using VMelnalksnis.NordigenDotNet.Institutions;
using VMelnalksnis.NordigenDotNet.Requisitions;
namespace VMelnalksnis.NordigenDotNet;
/// <inheritdoc />
[JsonSourceGenerationOptions(GenerationMode = JsonSourceGenerationMode.Metadata, IgnoreReadOnlyProperties = true)]
[JsonSerializable(typeof(Account))]
[JsonSerializable(typeof(AccountDetailsWrapper))]
[JsonSerializable(typeof(BalancesWrapper))]
[JsonSerializable(typeof(TransactionsWrapper))]
[JsonSerializable(typeof(EndUserAgreement))]
[JsonSerializable(typeof(PaginatedList<EndUserAgreement>))]
[JsonSerializable(typeof(EndUserAgreementAcceptance))]
[JsonSerializable(typeof(EndUserAgreementCreation))]
[JsonSerializable(typeof(Institution))]
[JsonSerializable(typeof(List<Institution>))]
[JsonSerializable(typeof(Requisition))]
[JsonSerializable(typeof(PaginatedList<Requisition>))]
[JsonSerializable(typeof(RequisitionCreation))]
internal partial class NordigenSerializationContext : JsonSerializerContext
{
}
@@ -7,5 +7,14 @@ using System.Collections.Generic;
namespace VMelnalksnis.NordigenDotNet;
internal sealed record PaginatedList<TResult>(int? Count, Uri? Next, Uri? Previous, List<TResult>? Results)
where TResult : class;
internal sealed class PaginatedList<TResult>
where TResult : class
{
public int? Count { get; set; }
public Uri? Next { get; set; }
public Uri? Previous { get; set; }
public List<TResult>? Results { get; set; }
}
@@ -8,45 +8,56 @@ using System.Text.Json.Serialization;
using NodaTime;
#pragma warning disable SA1623
namespace VMelnalksnis.NordigenDotNet.Requisitions;
/// <summary>A request to access a user's account details from a single institution.</summary>
/// <param name="Id">The id of the requisition.</param>
/// <param name="Created">The point in time when the requisition was created.</param>
/// <param name="Redirect">The URI to which the user will be redirected after authorizing access.</param>
/// <param name="Status">The status of the requisition.</param>
/// <param name="InstitutionId">The ID of the institution for which this requisition was made for.</param>
/// <param name="Reference">Client specified reference for this requisition.</param>
/// <param name="Accounts">Accounts retrieved within the scope of this requisition.</param>
/// <param name="Link">URI to initiate the authorization with the institution.</param>
/// <param name="AccountSelection">Whether to enable account selection view for the end user (if the institution supports it).</param>
/// <param name="RedirectImmediate">Whether to enable redirect back to the client after account list is received.</param>
public record Requisition(
Guid Id,
Instant Created,
Uri Redirect,
RequisitionStatus Status,
[property: JsonPropertyName("institution_id")] string InstitutionId,
string Reference,
List<Guid> Accounts,
Uri Link,
[property: JsonPropertyName("account_selection")] bool AccountSelection,
[property: JsonPropertyName("redirect_immediate")] bool RedirectImmediate)
public record Requisition
{
/// <summary>The raw value of <see cref="Agreement"/>.</summary>
[JsonPropertyName("agreement")]
public string? AgreementValue { get; init; }
/// <summary>Gets or sets the id of the requisition.</summary>
public Guid Id { get; set; }
/// <summary>The end-user-agreement of this requisition.</summary>
/// <summary>Gets or sets the point in time when the requisition was created.</summary>
public Instant Created { get; set; }
/// <summary>Gets or sets the URI to which the user will be redirected after authorizing access.</summary>
public Uri Redirect { get; set; } = null!;
/// <summary>Gets or sets the status of the requisition.</summary>
public RequisitionStatus Status { get; set; }
/// <summary>Gets or sets the ID of the institution for which this requisition was made for.</summary>
[JsonPropertyName("institution_id")]
public string InstitutionId { get; set; } = null!;
/// <summary>Gets or sets client specified reference for this requisition.</summary>
public string Reference { get; set; } = null!;
/// <summary>Gets or sets accounts retrieved within the scope of this requisition.</summary>
public List<Guid> Accounts { get; set; } = null!;
/// <summary>Gets or sets uRI to initiate the authorization with the institution.</summary>
public Uri Link { get; set; } = null!;
/// <summary>Gets or sets a value indicating whether whether to enable account selection view for the end user (if the institution supports it).</summary>
[JsonPropertyName("account_selection")]
public bool AccountSelection { get; set; }
/// <summary>Gets or sets a value indicating whether whether to enable redirect back to the client after account list is received.</summary>
[JsonPropertyName("redirect_immediate")]
public bool RedirectImmediate { get; set; }
/// <summary>Gets or sets the raw value of <see cref="Agreement"/>.</summary>
[JsonPropertyName("agreement")]
public string? AgreementValue { get; set; }
/// <summary>Gets the end-user-agreement of this requisition.</summary>
[JsonIgnore]
public Guid? Agreement => string.IsNullOrWhiteSpace(AgreementValue) ? null : Guid.Parse(AgreementValue);
/// <summary>A two-letter country code (ISO 639-1).</summary>
/// <summary>Gets or sets a two-letter country code (ISO 639-1).</summary>
[JsonPropertyName("user_language")]
public string? UserLanguage { get; init; }
public string? UserLanguage { get; set; }
/// <summary>Social security number for account ownership verification.</summary>
public string? Ssn { get; init; }
/// <summary>Gets or sets social security number for account ownership verification.</summary>
public string? Ssn { get; set; }
}
@@ -4,6 +4,9 @@
using System;
using System.Collections.Generic;
using System.Net.Http;
using System.Net.Http.Json;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
@@ -12,44 +15,49 @@ namespace VMelnalksnis.NordigenDotNet.Requisitions;
/// <inheritdoc />
public sealed class RequisitionClient : IRequisitionClient
{
private readonly NordigenHttpClient _nordigenHttpClient;
private readonly HttpClient _httpClient;
private readonly NordigenSerializationContext _context;
/// <summary>Initializes a new instance of the <see cref="RequisitionClient"/> class.</summary>
/// <param name="nordigenHttpClient">Http client configured for making requests to the Nordigen API.</param>
public RequisitionClient(NordigenHttpClient nordigenHttpClient)
/// <param name="httpClient">Http client configured for making requests to the Nordigen API.</param>
/// <param name="serializerOptions">Nordigen specific instance of <see cref="JsonSerializerOptions"/>.</param>
public RequisitionClient(HttpClient httpClient, NordigenJsonSerializerOptions serializerOptions)
{
_nordigenHttpClient = nordigenHttpClient;
_httpClient = httpClient;
_context = serializerOptions.Context;
}
/// <inheritdoc />
public IAsyncEnumerable<Requisition> Get(int pageSize = 100, CancellationToken cancellationToken = default)
{
return _nordigenHttpClient.GetPaginated<Requisition>(Routes.Requisitions.PaginatedUri(pageSize), cancellationToken);
return _httpClient.GetPaginated(
Routes.Requisitions.PaginatedUri(pageSize),
_context.PaginatedListRequisition,
cancellationToken);
}
/// <inheritdoc />
public async Task<Requisition> Get(Guid id, CancellationToken cancellationToken = default)
public Task<Requisition> Get(Guid id, CancellationToken cancellationToken = default)
{
var requisition = await _nordigenHttpClient
.Get<Requisition>(Routes.Requisitions.IdUri(id), cancellationToken)
.ConfigureAwait(false);
return requisition!;
return _httpClient.GetFromJsonAsync(
Routes.Requisitions.IdUri(id),
_context.Requisition,
cancellationToken)!;
}
/// <inheritdoc />
public async Task<Requisition> Post(RequisitionCreation requisitionCreation)
public Task<Requisition> Post(RequisitionCreation requisitionCreation)
{
var requisition = await _nordigenHttpClient
.Post<RequisitionCreation, Requisition>(Routes.Requisitions.Uri, requisitionCreation)
.ConfigureAwait(false);
return requisition!;
return _httpClient.Post(
Routes.Requisitions.Uri,
requisitionCreation,
_context.RequisitionCreation,
_context.Requisition)!;
}
/// <inheritdoc />
public async Task Delete(Guid id)
{
await _nordigenHttpClient.Delete(Routes.Requisitions.IdUri(id)).ConfigureAwait(false);
await _httpClient.Delete(Routes.Requisitions.IdUri(id)).ConfigureAwait(false);
}
}
@@ -7,43 +7,46 @@ using System.Text.Json.Serialization;
using VMelnalksnis.NordigenDotNet.Institutions;
#pragma warning disable SA1623
namespace VMelnalksnis.NordigenDotNet.Requisitions;
/// <summary>Information needed to create a new <see cref="Requisition"/>.</summary>
/// <param name="Redirect">Redirect URL to your application after end-user authorization with ASPSP.</param>
/// <param name="InstitutionId">The id of the <see cref="Institution"/> for which to create a requisition for.</param>
public record RequisitionCreation(Uri Redirect, [property: JsonPropertyName("institution_id")] string InstitutionId)
public record RequisitionCreation(Uri Redirect, string InstitutionId)
{
/// <summary>The end-user-agreement of this requisition.</summary>
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public Guid? Agreement { get; init; }
/// <summary>Gets redirect URL to your application after end-user authorization with ASPSP.</summary>
public Uri Redirect { get; } = Redirect;
/// <summary>A unique ID for internal referencing.</summary>
/// <summary>Gets the id of the <see cref="Institution"/> for which to create a requisition for.</summary>
[JsonPropertyName("institution_id")]
public string InstitutionId { get; } = InstitutionId;
/// <summary>Gets or sets the end-user-agreement of this requisition.</summary>
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public string? Reference { get; init; }
public Guid? Agreement { get; set; }
/// <summary>Gets or sets a unique ID for internal referencing.</summary>
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public string? Reference { get; set; }
/// <summary>
/// The language for all end users steps hosted by Nordigen in ISO 639-1 format.
/// Gets or sets the language for all end users steps hosted by Nordigen in ISO 639-1 format.
/// If not defined, the language set in the browser will be used instead.
/// </summary>
/// <seealso href="https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes"/>
[JsonPropertyName("user_language")]
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public string? UserLanguage { get; init; }
public string? UserLanguage { get; set; }
/// <summary>Optional SSN field to verify ownership of the account.</summary>
/// <summary>Gets or sets optional SSN field to verify ownership of the account.</summary>
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public string? Ssn { get; init; }
public string? Ssn { get; set; }
/// <summary>Option to enable account selection view for the end user.</summary>
/// <summary>Gets or sets option to enable account selection view for the end user.</summary>
[JsonPropertyName("account_selection")]
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public bool? AccountSelection { get; init; }
public bool? AccountSelection { get; set; }
/// <summary>Enable redirect back to the client after account list received.</summary>
/// <summary>Gets or sets enable redirect back to the client after account list received.</summary>
[JsonPropertyName("redirect_immediate")]
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public bool? RedirectImmediate { get; init; }
public bool? RedirectImmediate { get; set; }
}
@@ -6,7 +6,7 @@ using NodaTime;
namespace VMelnalksnis.NordigenDotNet.Tokens;
/// <summary>Caches refresh and access tokens for <see cref="NordigenHttpClient"/>.</summary>
/// <summary>Caches refresh and access tokens for <see cref="INordigenClient"/>.</summary>
public sealed class NordigenTokenCache
{
private readonly NordigenOptions _nordigenOptions;
@@ -11,7 +11,7 @@ using System.Threading.Tasks;
namespace VMelnalksnis.NordigenDotNet.Tokens;
/// <summary>Handles authorization tokens for <see cref="NordigenHttpClient"/>.</summary>
/// <summary>Handles authorization tokens for <see cref="INordigenClient"/>.</summary>
public sealed class TokenDelegatingHandler : DelegatingHandler
{
private static readonly TokenSerializationContext _context = new(new(JsonSerializerDefaults.Web));
@@ -0,0 +1,11 @@
// Copyright 2022 Valters Melnalksnis
// Licensed under the Apache License 2.0.
// See LICENSE file in the project root for full license information.
// ReSharper disable once CheckNamespace
namespace NodaTime;
[System.Obsolete("Proxy type for System.Text.Json to work around https://github.com/dotnet/runtime/issues/66679#issuecomment-1189027602")]
internal readonly struct YearMonthDayCalendar
{
}