RentaStore.Sitelink.Domain 2.0.3

dotnet add package RentaStore.Sitelink.Domain --version 2.0.3
                    
NuGet\Install-Package RentaStore.Sitelink.Domain -Version 2.0.3
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="RentaStore.Sitelink.Domain" Version="2.0.3" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="RentaStore.Sitelink.Domain" Version="2.0.3" />
                    
Directory.Packages.props
<PackageReference Include="RentaStore.Sitelink.Domain" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add RentaStore.Sitelink.Domain --version 2.0.3
                    
#r "nuget: RentaStore.Sitelink.Domain, 2.0.3"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package RentaStore.Sitelink.Domain@2.0.3
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=RentaStore.Sitelink.Domain&version=2.0.3
                    
Install as a Cake Addin
#tool nuget:?package=RentaStore.Sitelink.Domain&version=2.0.3
                    
Install as a Cake Tool

RentaStore.SitelinkAPI

A production-ready REST API wrapper over Sitelink SOAP web services. This solution dynamically generates REST endpoints for SOAP service methods using reflection, providing modern REST/JSON interfaces with comprehensive Swagger documentation and JWT authentication.

Overview

RentaStore.SitelinkAPI transforms legacy SOAP web services into modern RESTful APIs without manually writing endpoints for each SOAP method. The system uses reflection to automatically discover SOAP methods and create corresponding REST endpoints, making it easy to add new services or methods as they become available.

Key Features

  • Dynamic Endpoint Generation - Automatically creates REST endpoints from SOAP service methods using reflection
  • JWT Authentication - Secure token-based authentication with embedded Sitelink credentials
  • Swagger/OpenAPI - Auto-generated interactive API documentation
  • Flexible Response Formats - Returns JSON (default) or XML based on request preference
  • Error Handling - Maps Sitelink error codes to appropriate HTTP status codes
  • Extensible Architecture - Easy to add new SOAP services

Technology Stack

  • .NET 10.0 - Target framework
  • ASP.NET Core Minimal APIs - Lightweight endpoint routing
  • Carter - Module-based endpoint organization
  • System.ServiceModel - SOAP client generation and communication
  • JWT Bearer Authentication - Token-based security
  • Swashbuckle - Swagger/OpenAPI documentation
  • System.Text.Json - High-performance JSON serialization with custom XML converters

Solution Structure

The solution consists of 6 projects:

1. RentaStore.SitelinkAPI (Main API Project)

The primary web API application built with ASP.NET Core Minimal APIs.

Key Components:

  • Program.cs - Application entry point and route configuration
  • Program.Services.cs - Service registration using fluent extension methods
  • DynamicRoutes.cs - Core logic for dynamic endpoint generation
  • Services/ - SOAP wrappers and documentation services
  • Extensions/ - Response handling and error mapping
  • Authentication/ - JWT token generation endpoints

2. CallCenterWs (SOAP Client Library)

SOAP client wrapper for the Sitelink CallCenter web service.

Structure:

  • ServiceReference/Reference.cs - Auto-generated SOAP client code
  • ServiceReference/ArrayOfXElement.cs - Response wrapper implementing IArrayOfXElement
  • ServiceReference/dotnet-svcutil.params.json - SOAP client generation configuration

3. ReportingWs (SOAP Client Library)

SOAP client wrapper for the Sitelink Reporting web service.

Structure:

  • ServiceReference/Reference.cs - Auto-generated SOAP client code
  • ServiceReference/ArrayOfXElement.cs - Response wrapper implementing IArrayOfXElement
  • ServiceReference/dotnet-svcutil.params.json - SOAP client generation configuration

4. CallCenterWs.Mock (Mock SOAP Service)

Mock implementation of the SiteLink CallCenter SOAP service for local development and MCP testing.

Key Components:

  • Program.cs - SoapCore host serving SOAP 1.2 at /CCWs_3.5/CallCenterWs.asmx
  • Services/CallCenterWsSoapBase.cs - Base class implementing all 260 interface methods with default responses
  • Services/MockCallCenterService.cs - Overrides 13 operations with in-memory state logic
  • State/MockDataStore.cs - Thread-safe in-memory state (units, tenants, reservations, ledgers)
  • State/MockSeedData.cs - Pre-seeded test units (99901, 99902, 101, 102, 103)
  • Helpers/ResponseBuilder.cs - Builds ArrayOfXElement responses matching SiteLink wire format

5. RentaStore.Sitelink.Domain (Shared Library)

Common interfaces, models, and converters shared across projects.

Key Components:

  • IArrayOfXElement.cs - Interface for standardized SOAP response handling
  • Converters/XmlPrefixedValueJsonConverter.cs - Custom JSON converter for XML-to-JSON transformation

6. RentaStore.Sitelink.Tests (Unit Tests)

Unit tests for shared domain components.

Coverage:

  • XmlPrefixedValueJsonConverter — type conversions, collection handling, prefix stripping, return code serialization

Prerequisites

  • .NET 10.0 SDK or later (Download)
  • Git (for cloning the repository)
  • A code editor (Visual Studio 2022, VS Code, or Rider recommended)
  • Access to Sitelink SOAP web service endpoints

Getting Started

Clone the Repository

git clone <repository-url>
cd RentaStore.SitelinkAPI

Configure the Application

  1. Update RentaStore.SitelinkAPI/appsettings.json with your JWT configuration:
{
  "Jwt": {
    "Key": "your-secure-key-here-minimum-32-characters",
    "Issuer": "RentaStore.SitelinkAPI",
    "Audience": "RentaStore.Application"
  }
}
  1. If needed, update SOAP service endpoint URLs in the generated Reference.cs files or use endpoint configuration.

Build and Run

# Restore dependencies
dotnet restore

# Build the solution
dotnet build

# Run the API
dotnet run --project RentaStore.SitelinkAPI/RentaStore.SitelinkAPI.csproj

The API will start and be accessible at:

  • HTTPS: https://localhost:5001
  • HTTP: http://localhost:5000
  • Swagger UI: https://localhost:5001/swagger

Using the API

1. Obtain a JWT Token

First, authenticate to receive a JWT token:

POST /Auth/Token
Content-Type: application/json

{
  "corpCode": "YOUR_CORP_CODE",
  "locationCode": "YOUR_LOCATION_CODE",
  "corpUserName": "YOUR_USERNAME",
  "corpPassword": "YOUR_PASSWORD"
}

Response:

{
  "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
}

2. Call SOAP Methods via REST

Use the JWT token to call any SOAP method through REST endpoints:

POST /callcenter/{MethodName}
Authorization: Bearer {your-jwt-token}
Content-Type: application/json

{
  "param1": "value1",
  "param2": "value2"
}

Or for reporting:

POST /reports/{MethodName}
Authorization: Bearer {your-jwt-token}
Content-Type: application/json

{
  "param1": "value1"
}

3. Response Format

By default, responses are returned as JSON. To receive XML, add the WantXml query parameter:

POST /callcenter/{MethodName}?WantXml=true

4. Explore with Swagger

Navigate to /swagger to see all available endpoints with interactive documentation.

Extending the Solution

Adding a New SOAP Service

To add a new SOAP web service to the API:

Step 1: Generate SOAP Client

Create a new class library project and generate the SOAP client:

dotnet new classlib -n NewServiceWs
cd NewServiceWs

dotnet add package System.ServiceModel.Duplex
dotnet add package System.ServiceModel.Http
dotnet add package System.ServiceModel.Security

# Generate SOAP client from WSDL
dotnet-svcutil https://your-service-url/Service.asmx?wsdl \
  --outputDir ServiceReference \
  --namespace "*,NewServiceWS"
Step 2: Implement IArrayOfXElement

Create ServiceReference/ArrayOfXElement.cs:

using RentaStore.Sitelink.Domain;
using System.Xml.Linq;

namespace NewServiceWS;

public partial class ArrayOfXElement : IArrayOfXElement
{
    public List<XElement> GetNodes() => this.Nodes.ToList();

    public XElement? GetDataSet(object result)
    {
        if (result is ArrayOfXElement arrayOfXElement)
        {
            var nodes = arrayOfXElement.GetNodes();
            return nodes.FirstOrDefault(n => n.Name.LocalName == "NewDataSet");
        }
        return null;
    }

    public T? GetObject<T>(object result)
    {
        // Custom deserialization logic if needed
        throw new NotImplementedException();
    }
}
Step 3: Create Wrapper Class

In the main API project, create Services/NewServiceWrapper.cs:

namespace RentaStore.SitelinkAPI.Services;

public class NewServiceWrapper : DynamicSoapWrapper
{
    public NewServiceWrapper(object soapClient, ILogger<DynamicSoapWrapper> logger)
        : base(soapClient, logger)
    {
    }
}
Step 4: Register the Service

In Program.Services.cs, add to the AddAPIServices method:

// Create and register the SOAP client
var newServiceSoapClient = new NewServiceWS.NewServiceWsSoapClient(
    NewServiceWS.NewServiceWsSoapClient.EndpointConfiguration.NewServiceWsSoap);

builder.Services.AddSingleton<NewServiceWS.NewServiceWsSoapClient>(newServiceSoapClient);

builder.Services.AddSingleton<NewServiceWrapper>(sp => {
    var logger = sp.GetRequiredService<ILogger<NewServiceWrapper>>();
    return new NewServiceWrapper(
        sp.GetRequiredService<NewServiceWS.NewServiceWsSoapClient>(),
        logger
    );
});
Step 5: Configure Dynamic Routes

In Program.cs, add route configuration:

var newServiceWrapper = app.Services.GetRequiredService<NewServiceWrapper>();
app.ConfigureDynamicRoutes<NewServiceWS.ArrayOfXElement>(
    newServiceWrapper,
    ApiDocumentationService,
    "newservice",    // URL prefix
    "NewService"     // Swagger tag
);
Step 6: Add Documentation (Optional)

Create wwwroot/NewServiceWs.xml with method documentation:

<?xml version="1.0" encoding="utf-8"?>
<methods>
  <method>
    <name>MethodName</name>
    <description>Description of what this method does</description>
    <returns>
      <return name="FieldName" description="Field description"/>
    </returns>
  </method>
</methods>

Register in Program.Services.cs:

builder.Services.AddApiDocumentation(
    "CallCenterWs.xml",
    "ReportingWs.xml",
    "NewServiceWs.xml"  // Add new documentation
);

Customizing Response Handling

To customize how responses are processed, modify Extensions/ResultExtensions.cs:

public static IResult HandleSoapResponse(XElement? dataset, bool wantXml, ILogger logger)
{
    // Add custom logic here
    // Check for specific elements
    // Transform data
    // Handle errors differently
}

Adding Custom Endpoints

For non-dynamic endpoints, use Carter modules. Create a new class:

using Carter;

namespace RentaStore.SitelinkAPI.CustomEndpoints;

public class CustomEndpoints : ICarterModule
{
    public void AddRoutes(IEndpointRouteBuilder app)
    {
        app.MapGet("/custom/endpoint", () => Results.Ok("Custom response"))
            .WithTags("Custom")
            .RequireAuthorization();
    }
}

Architecture Overview

How Dynamic Routes Work

  1. Service Registration - SOAP clients are registered as singletons
  2. Wrapper Creation - Each SOAP client is wrapped in a DynamicSoapWrapper
  3. Method Discovery - Reflection discovers all public methods on the SOAP client
  4. Route Generation - For each method, a POST endpoint is created at /{prefix}/{MethodName}
  5. Request Handling - Incoming JSON is mapped to SOAP method parameters
  6. Authentication - JWT claims provide Sitelink credentials to SOAP methods
  7. Response Processing - SOAP responses are converted to JSON or XML

Authentication Flow

Client → POST /Auth/Token (with credentials)
       ← JWT token (credentials embedded as claims)

Client → POST /callcenter/Method (with JWT)
       → Extract credentials from JWT claims
       → Invoke SOAP method with credentials
       ← Process SOAP response
Client ← Return JSON/XML

Error Handling

SOAP services return error codes in <RT><Ret_Code> elements. The system maps these to HTTP status codes:

  • -88 to -89, -91, -96 to -98 → 401 Unauthorized
  • -92 → 403 Forbidden
  • -93 → 503 Service Unavailable
  • -99 → 500 Internal Server Error
  • Positive codes → 200 OK

XML to JSON Conversion with XmlPrefixedValueJsonConverter

The XmlPrefixedValueJsonConverter is a custom System.Text.Json converter that intelligently transforms Sitelink's prefixed XML element names into clean, typed JSON properties.

How It Works

Sitelink SOAP services use Hungarian notation-style prefixes on XML element names to indicate data types. The converter:

  1. Strips type prefixes from element names
  2. Converts to camelCase for JSON-friendly property names
  3. Applies type conversions based on the prefix
  4. Handles collection elements (ensures arrays for Table, Row, Item)
Prefix Rules and Type Conversions
XML Prefix Strips To Type Conversion Example
lng Remove "lng", camelCase long <lngCustomerID>123456789</lngCustomerID> → "customerID": 123456789
dc Remove "dc", camelCase decimal <dcTotalAmount>100.50</dcTotalAmount> → "totalAmount": 100.50
i Remove "i", camelCase int <iQuantity>5</iQuantity> → "quantity": 5
b Convert to "is" + name boolean <bActive>true</bActive> → "isActive": true
d Remove "d", camelCase string (ISO 8601 UTC) <dCreated>2025-01-15</dCreated> → "created": "2025-01-15T00:00:00.000Z"
s Remove "s", camelCase string <sName>John</sName> → "name": "John"
*ID camelCase int <UnitID>12345</UnitID> → "unitID": 12345
Collection Element Handling

Single-element collections are always serialized as arrays (not objects):


<NewDataSet>
  <Table>
    <dcRate>100.00</dcRate>
    <sDescription>Standard Rate</sDescription>
  </Table>
</NewDataSet>
// JSON output - note the array brackets
{
  "table": [
    {
      "rate": 100.00,
      "description": "Standard Rate"
    }
  ]
}

Collection Elements: Table, Row, Item (case-insensitive)

This ensures consistent deserialization to List<T> regardless of result count, fixing a critical bug where single-row responses would fail to deserialize.

Using the Converter Directly

The converter is available in the RentaStore.Sitelink.Domain package and can be used directly by consuming applications:

Installation:

dotnet add reference /path/to/RentaStore.Sitelink.Domain/RentaStore.Sitelink.Domain.csproj

Basic Usage:

using System.Text.Json;
using System.Xml.Linq;
using RentaStore.Sitelink.Converters;

// Parse XML response
var xmlData = XElement.Parse("<NewDataSet>...</NewDataSet>");

// Configure JsonSerializerOptions with the converter
var options = new JsonSerializerOptions
{
    Converters = { new XmlPrefixedValueJsonConverter() },
    WriteIndented = true
};

// Serialize to JSON
var json = JsonSerializer.Serialize(xmlData, options);

Deserialize to Strongly-Typed Models:

using System.Text.Json;
using System.Xml.Linq;
using RentaStore.Sitelink.Converters;

// Define your model matching the JSON structure
public class UnitRate
{
    public decimal Rate { get; set; }
    public string Description { get; set; }
    public bool IsActive { get; set; }
}

public class Response
{
    public List<UnitRate> Table { get; set; }
}

// Parse and convert
var xmlData = XElement.Parse(@"
<NewDataSet>
  <Table>
    <dcRate>100.00</dcRate>
    <sDescription>Standard</sDescription>
    <bActive>true</bActive>
  </Table>
</NewDataSet>");

var options = new JsonSerializerOptions
{
    Converters = { new XmlPrefixedValueJsonConverter() },
    PropertyNameCaseInsensitive = true
};

// Convert to JSON, then deserialize to model
var json = JsonSerializer.Serialize(xmlData, options);
var response = JsonSerializer.Deserialize<Response>(json, options);

// Access typed data
foreach (var rate in response.Table)
{
    Console.WriteLine($"{rate.Description}: ${rate.Rate}");
}

Using with ArrayOfXElement:

The converter is already integrated into ArrayOfXElement classes:

using CallCenterWS;
using ReportingWS;

// CallCenter service
var callCenterClient = new CallCenterWsSoapClient(...);
var result = await callCenterClient.SomeMethodAsync(...);

// Get as JSON string
var json = result.GetJson(result);

// Get as strongly-typed object
var typedResult = result.GetObject<MyResponseModel>(result);
Date/Time Normalization

The converter normalizes date/time values to UTC in ISO 8601 format:

  • Input: <dCreated>2025-01-15T10:30:00-05:00</dCreated>
  • Output: "created": "2025-01-15T15:30:00.000Z"

All dates are converted to UTC with millisecond precision and the Z suffix.

Adding Custom Collection Elements

If you discover additional Sitelink element names that should always be arrays, update the CollectionElements HashSet in XmlPrefixedValueJsonConverter.cs:

private static readonly HashSet<string> CollectionElements = new HashSet<string>(StringComparer.OrdinalIgnoreCase) {
    "Table",
    "Row",
    "Item",
    "YourNewElement",  // Add here
    // Add other known collection elements as discovered
};

Development Tips

Testing Endpoints

Use the Swagger UI at /swagger for interactive testing, or use curl:

# Get token
TOKEN=$(curl -X POST http://localhost:5000/Auth/Token \
  -H "Content-Type: application/json" \
  -d '{"corpCode":"CORP","locationCode":"LOC","corpUserName":"user","corpPassword":"pass"}' \
  | jq -r '.token')

# Call endpoint
curl -X POST http://localhost:5000/callcenter/MethodName \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"param":"value"}'

Debugging SOAP Calls

Enable verbose logging in appsettings.Development.json:

{
  "Logging": {
    "LogLevel": {
      "Default": "Debug",
      "RentaStore.SitelinkAPI.Services": "Trace"
    }
  }
}

Regenerating SOAP Clients

When the SOAP service WSDL changes:

cd CallCenterWs/ServiceReference

# Backup your ArrayOfXElement.cs first!
cp ArrayOfXElement.cs ArrayOfXElement.cs.backup

# Regenerate
dotnet-svcutil https://service-url/Service.asmx?wsdl \
  --outputFile Reference.cs \
  --namespace "*,CallCenterWS"

# Restore your custom implementation
cp ArrayOfXElement.cs.backup ArrayOfXElement.cs

Project Configuration

Key Configuration Files

  • appsettings.json - JWT settings, logging configuration
  • appsettings.Development.json - Development-specific overrides
  • All .csproj files - Version number (<AssemblyVersion>, <FileVersion>, <Version>)
  • ServiceReference/dotnet-svcutil.params.json - SOAP client generation parameters

Environment Variables

You can override settings using environment variables:

export Jwt__Key="your-secure-key"
export Jwt__Issuer="CustomIssuer"

dotnet run

Version History

See git commit history for detailed changes:

git log --oneline

Current version: 2.0.2

For detailed changelog, see CHANGELOG.md

Contributing

  1. Fork the repository
  2. Create a feature branch (git checkout -b feature/amazing-feature)
  3. Commit your changes (git commit -m 'Add amazing feature')
  4. Push to the branch (git push origin feature/amazing-feature)
  5. Open a Pull Request

The CallCenterWs.Mock project provides a local mock of the SiteLink CallCenter SOAP service, enabling end-to-end MCP testing without a live SiteLink connection.

Running the Mock

# Start the mock service (SOAP 1.2 on https://localhost:5291)
dotnet run --project CallCenterWs.Mock/CallCenterWs.Mock.csproj

# Start the API gateway (auto-connects to mock in Development mode)
dotnet run --project RentaStore.SitelinkAPI/RentaStore.SitelinkAPI.csproj

Or use the development stack scripts to start all services at once:

# Bash
./dev.sh start     # Start all 4 services (mock, gateway, API, MCP)
./dev.sh status    # Check running services
./dev.sh stop      # Graceful shutdown

# PowerShell
.\dev.ps1 start
.\dev.ps1 status
.\dev.ps1 stop

Configuration

The API gateway connects to the mock when Sitelink:MockEndpoint is set in appsettings.Development.json:

{
  "Sitelink": {
    "MockEndpoint": "https://localhost:5291/CCWs_3.5/CallCenterWs.asmx"
  }
}

Remove or leave empty to connect to the production SiteLink endpoint.

Implemented Operations

The mock implements 13 SOAP operations with in-memory state:

Category Operations
Units UnitsInformationAvailableUnitsOnly_v2, UnitsInformationByUnitID
Tenants TenantSearchDetailed, TenantNewDetailed_v3, TenantUpdate_v3, TenantInfoByTenantID
Reservations ReservationNewWithSource_v5, ReservationUpdate_v2, ReservationUpdate_v3, ReservationList_v3
Pricing MoveInCostRetrieveWithDiscount_Reservation
Move-In MoveInReservation_v4
Ledgers LedgersByTenantID_v3

All other operations return error code -999 ("not implemented in mock mode").

Known Limitations

The mock service is designed for MCP exploratory testing and local development, not as a full substitute for the real SiteLink SOAP service. Key limitations:

  • Incomplete field coverage — Mock XML responses include only a subset of the fields the real service returns per entity. Downstream code deserializing into strongly-typed classes may encounter null values for fields the mock omits. The ResponseBuilder.Build*Element helpers would need expanding to match the full real response structure.
  • 13 of 260 operations — Only the operations listed above are implemented. The broader RentaStore ecosystem requires additional SiteLink methods (e.g., GateAccessDataAsync for the Gate Access feature). New mock operations should be added as needed, but the real endpoint remains necessary for production and response structure validation.
  • No business rule fidelity — In-memory state does not replicate SiteLink billing cycles, insurance, concession plans, or payment processing. Suitable for happy-path flows and basic error handling only.
  • State is ephemeral — All data resets on restart. No persistence between sessions.
  • Response structure contract — Data-returning methods must use BuildDataOnly (no RT element). Status-only methods (create/update/delete) use BuildResult with RT. Mixing these causes the response pipeline to discard data (see v2.0.1 fix).

Pre-Seeded Data

The mock starts with 5 units. State resets on restart (no persistence).

UnitID Name Size Rate Type Status
99901 MCP_TEST_A001 2x3 R750 MCP_TEST_SmallUnit Available
99902 MCP_TEST_A002 2x3 R750 MCP_TEST_SmallUnit Available
101 A101 3x3 R950 Small Available
102 A102 3x6 R1500 Medium Available
103 B101 6x6 R2800 Large Rented

Troubleshooting

SOAP Endpoint Not Accessible

Check the endpoint configuration in the generated Reference.cs files. You may need to update the service URL.

JWT Token Invalid

Ensure the Jwt:Key in appsettings.json is at least 32 characters and matches between token generation and validation.

Method Not Appearing in Swagger

Check that the method:

  • Is public
  • Is not in the excluded methods list (DynamicRoutes.cs:20)
  • Has a return type compatible with Task<T>

License

This project is licensed under the MIT License.

Product Compatible and additional computed target framework versions.
.NET net10.0 is compatible.  net10.0-android was computed.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-maccatalyst was computed.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.
  • net10.0

    • No dependencies.

NuGet packages (2)

Showing the top 2 NuGet packages that depend on RentaStore.Sitelink.Domain:

Package Downloads
RentaStore.Sitelink.ReportingWs

SOAP client wrapper for the SiteLink Reporting web service. Generated from the SiteLink WSDL and adapted to expose IArrayOfXElement for standardized response handling.

RentaStore.Sitelink.CallCenterWs

SOAP client wrapper for the SiteLink CallCenter web service. Generated from the SiteLink WSDL and adapted to expose IArrayOfXElement for standardized response handling.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
2.0.3 370 5/8/2026
2.0.2 134 5/7/2026