Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -337,4 +337,8 @@ ASALocalRun/
.localhistory/

# BeatPulse healthcheck temp database
healthchecksdb
healthchecksdb

# Migration test client local credentials (public repo — never commit real creds)
Bricknode.MigrationTestClient/appsettings.json
Bricknode.MigrationTestClient/appsettings.local.json
23 changes: 23 additions & 0 deletions Bricknode.MigrationTestClient/AppConfig.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
namespace Bricknode.MigrationTestClient;

/// <summary>
/// Credentials + endpoints for the demo run. Bound from appsettings.json / appsettings.local.json
/// / environment variables (prefix BFS_). Never commit real values — see appsettings.example.json.
/// </summary>
internal sealed class AppConfig
{
public string Username { get; set; } = "";
public string Password { get; set; } = "";
public string Identifier { get; set; } = "";

/// <summary>SOAP endpoint, e.g. https://your-instance.bricknode.com/api/BFSApi.asmx</summary>
public string SoapEndpoint { get; set; } = "";

/// <summary>REST API base address, e.g. https://your-instance.bricknode.com/</summary>
public string RestEndpoint { get; set; } = "";

public bool HasCredentials =>
!string.IsNullOrWhiteSpace(Username) &&
!string.IsNullOrWhiteSpace(Password) &&
!string.IsNullOrWhiteSpace(Identifier);
}
46 changes: 46 additions & 0 deletions Bricknode.MigrationTestClient/Bricknode.MigrationTestClient.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<LangVersion>latest</LangVersion>
<RootNamespace>Bricknode.MigrationTestClient</RootNamespace>
<DefineConstants Condition="'$(UseRest)' == 'true'">$(DefineConstants);USE_REST</DefineConstants>
</PropertyGroup>

<!--
The migration in one place: which package backs the (unchanged) application code.

dotnet run -> Bricknode.Soap.Sdk (original SOAP SDK)
dotnet run -p:UseRest=true -> Bricknode.Rest.CompatSdk (REST drop-in)

In a real application this is the <PackageReference> you swap.
-->
<ItemGroup Condition="'$(UseRest)' != 'true'">
<ProjectReference Include="..\Bricknode.Soap.Sdk\Bricknode.Soap.Sdk.csproj" />
</ItemGroup>
<ItemGroup Condition="'$(UseRest)' == 'true'">
<ProjectReference Include="..\Bricknode.Rest.CompatSdk\Bricknode.Rest.CompatSdk.csproj" />
</ItemGroup>

<ItemGroup>
<!-- The SOAP SDK (netstandard2.0) was compiled against Bcl.AsyncInterfaces via DI 3.1;
with DI 8.0 in this app the assembly drops out of the graph unless referenced explicitly. -->
<PackageReference Include="Microsoft.Bcl.AsyncInterfaces" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.Configuration" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.Logging" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.Logging.Console" Version="8.0.0" />
</ItemGroup>

<ItemGroup>
<None Update="appsettings.example.json" CopyToOutputDirectory="PreserveNewest" />
<None Update="appsettings.json" CopyToOutputDirectory="PreserveNewest" Condition="Exists('appsettings.json')" />
</ItemGroup>

</Project>
75 changes: 75 additions & 0 deletions Bricknode.MigrationTestClient/Program.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
using BfsApi;
using Bricknode.MigrationTestClient;
using Bricknode.Soap.Sdk.Extensions;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;

// Local verification tool + migration example.
// dotnet run -> original SOAP SDK
// dotnet run -p:UseRest=true -> REST drop-in (Bricknode.Rest.CompatSdk)
// All code below is ordinary SDK consumer code; only the package reference changes.


var config = LoadConfig();
if (!config.HasCredentials)
{
Console.Error.WriteLine(
"No credentials found. Copy appsettings.example.json to appsettings.json and fill it in, " +
"or set BFS_Username / BFS_Password / BFS_Identifier environment variables.");
return 1;
}

#if USE_REST
const string sdk = "REST (Bricknode.Rest.CompatSdk)";
var endpoint = config.RestEndpoint;
#else
const string sdk = "SOAP (Bricknode.Soap.Sdk)";
var endpoint = config.SoapEndpoint;
#endif

if (string.IsNullOrWhiteSpace(endpoint))
{
Console.Error.WriteLine($"No endpoint configured for {sdk}.");
return 1;
}

Console.WriteLine($"Bricknode migration test client — {sdk}");
Console.WriteLine($"Endpoint: {endpoint}");

var services = new ServiceCollection();
services.AddLogging(builder => builder.AddSimpleConsole(o => o.SingleLine = true));
services.AddBfsApiClient(cfg =>
{
cfg.Credentials = new Credentials { UserName = config.Username, Password = config.Password };
cfg.Identifier = config.Identifier;
cfg.EndpointAddress = endpoint;
});
services.AddTransient<TestRunner>();

await using var provider = services.BuildServiceProvider();

try
{
await provider.GetRequiredService<TestRunner>().RunAsync();
return 0;
}
catch (Exception ex)
{
Console.Error.WriteLine($"Failed: {ex.GetType().Name}: {ex.Message}");
for (var inner = ex.InnerException; inner is not null; inner = inner.InnerException)
Console.Error.WriteLine($" inner: {inner.GetType().Name}: {inner.Message}");
return 1;
}

static AppConfig LoadConfig()
{
var root = new ConfigurationBuilder()
.SetBasePath(Directory.GetCurrentDirectory())
.AddJsonFile("appsettings.json", optional: true)
.AddJsonFile("appsettings.local.json", optional: true)
.AddEnvironmentVariables("BFS_")
.Build();

return root.Get<AppConfig>() ?? new AppConfig();
}
52 changes: 52 additions & 0 deletions Bricknode.MigrationTestClient/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
# Bricknode Migration Test Client

A small console app for **local verification** of the SOAP → REST migration, written exactly the
way an SDK consumer writes code: services (`IBfsAccountService`, `IBfsCurrencyService`) are
constructor-injected via DI, DTOs come from `BfsApi`. The same code runs against either package —
the only thing that changes is the package reference.

## Run

```bash
dotnet run # original SOAP SDK (Bricknode.Soap.Sdk)
dotnet run -p:UseRest=true # REST drop-in (Bricknode.Rest.CompatSdk)
```

The switch swaps the `<ProjectReference>` in the csproj. In a real application that is the
`<PackageReference>` you change — `Program.cs` and `TestRunner.cs` are untouched. That is the
whole migration.

> **Note:** the flavor is baked in at **build** time — don't combine the switch with `--no-build`,
> or you'll run whichever flavor was built last.

## Known differences when migrating

- `EndpointAddress` must point at the REST base URL instead of the SOAP `.asmx`
(this client reads `SoapEndpoint`/`RestEndpoint` from config accordingly).
- **Dates:** none — `DateTime` values (UTC `Kind=Utc`, unset dates as `DateTime.MinValue`) are
identical to what the SOAP SDK produces, guaranteed by `SoapRestWireParityTests` which runs the
same server data through the real `XmlSerializer` and the REST pipeline and requires
byte-identical results.

## Configure credentials (never committed)

Copy the template and fill it in:

```bash
cp appsettings.example.json appsettings.json
```

`appsettings.json` and `appsettings.local.json` are git-ignored. Alternatively use environment
variables: `BFS_Username`, `BFS_Password`, `BFS_Identifier`, `BFS_SoapEndpoint`, `BFS_RestEndpoint`.

## What it runs

Four read-only cases in `TestRunner.cs`, each printing the response message, result count and a
small sample so you can compare SOAP vs REST output side by side:

- `IBfsAccountService.GetAccountsAsync`
- `IBfsAccountService.GetAccountTypesAsync`
- `IBfsCurrencyService.GetCurrenciesAsync`
- `IBfsAccountService.GetAccountsAsync` with a `CreatedDateFrom` filter — prints the date sent and
the dates received (value + `Kind`), proving the server applies the date and both SDKs see
identical values in both directions.
92 changes: 92 additions & 0 deletions Bricknode.MigrationTestClient/TestRunner.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
using BfsApi;
using Bricknode.Soap.Sdk.Services;

namespace Bricknode.MigrationTestClient;

/// <summary>
/// The verification cases, written exactly as a consumer of the SDK writes them: services are
/// constructor-injected, DTOs come from <c>BfsApi</c>. This file compiles unchanged against both
/// the SOAP package and the REST drop-in — the only thing that changes is the package reference.
/// </summary>
internal sealed class TestRunner
{
private readonly IBfsAccountService _accounts;
private readonly IBfsCurrencyService _currencies;

public TestRunner(IBfsAccountService accounts, IBfsCurrencyService currencies)
{
_accounts = accounts;
_currencies = currencies;
}

public async Task RunAsync()
{
await GetAccounts();
await GetAccountTypes();
await GetCurrencies();
await GetAccountsCreatedSince();
}

/// <summary>
/// Sends a date to the server (CreatedDateFrom filter) and shows the date sent and the dates
/// received back, so SOAP and REST date handling can be compared end to end.
/// </summary>
private async Task GetAccountsCreatedSince()
{
var from = new DateTime(2026, 5, 1, 0, 0, 0, DateTimeKind.Utc);

var response = await _accounts.GetAccountsAsync(new GetAccountsArgs { CreatedDateFrom = from });

Print("GetAccounts (CreatedDateFrom filter)", response.Message, response.Result?.Length ?? 0,
response.Result?.Take(3).Select(a => a.AccountNo));
Console.WriteLine($" sent : CreatedDateFrom = {Show(from)}");

if (response.Result is { Length: > 0 })
{
var oldest = response.Result.Min(a => a.CreatedDate);
var newest = response.Result.Max(a => a.CreatedDate);
Console.WriteLine($" received: oldest CreatedDate = {Show(oldest)}");
Console.WriteLine($" received: newest CreatedDate = {Show(newest)}");
Console.WriteLine($" filter respected by server : {oldest >= from}");
}
}

private static string Show(DateTime value) => $"{value:yyyy-MM-dd HH:mm:ss.fff} (Kind={value.Kind})";

private async Task GetAccounts()
{
var response = await _accounts.GetAccountsAsync(new GetAccountsArgs());

Print("GetAccounts", response.Message, response.Result?.Length ?? 0,
response.Result?.Take(5).Select(a => a.AccountNo));
}

private async Task GetAccountTypes()
{
var response = await _accounts.GetAccountTypesAsync(new GetAccountTypeArgs());

Print("GetAccountTypes", response.Message, response.Result?.Length ?? 0,
response.Result?.Take(5).Select(t => t.Key));
}

private async Task GetCurrencies()
{
var response = await _currencies.GetCurrenciesAsync(new GetCurrencyArgs());

Print("GetCurrencies", response.Message, response.Result?.Length ?? 0,
response.Result?.Take(5).Select(c => c.Code));
}

private static void Print(string title, string? message, int count, IEnumerable<string>? sample)
{
var ok = message == "OK";
Console.WriteLine();
Console.WriteLine($"[{(ok ? "OK" : "FAIL")}] {title}");
Console.WriteLine($" message : {message}");
Console.WriteLine($" results : {count}");

var items = sample?.Where(s => !string.IsNullOrWhiteSpace(s)).ToList();
if (items is { Count: > 0 })
Console.WriteLine($" sample : {string.Join(", ", items)}");
}
}
8 changes: 8 additions & 0 deletions Bricknode.MigrationTestClient/appsettings.example.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"_comment": "Copy this file to appsettings.json (git-ignored) and fill in real values. Never commit credentials.",
"Username": "REPLACE_ME",
"Password": "REPLACE_ME",
"Identifier": "REPLACE_ME",
"SoapEndpoint": "https://your-instance.bricknode.com/api/BFSApi.asmx",
"RestEndpoint": "https://your-instance.bricknode.com/"
}
Loading