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
<PackageReference Include="RentaStore.Sitelink.Domain" Version="2.0.3" />
<PackageVersion Include="RentaStore.Sitelink.Domain" Version="2.0.3" />
<PackageReference Include="RentaStore.Sitelink.Domain" />
paket add RentaStore.Sitelink.Domain --version 2.0.3
#r "nuget: RentaStore.Sitelink.Domain, 2.0.3"
#:package RentaStore.Sitelink.Domain@2.0.3
#addin nuget:?package=RentaStore.Sitelink.Domain&version=2.0.3
#tool nuget:?package=RentaStore.Sitelink.Domain&version=2.0.3
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 configurationProgram.Services.cs- Service registration using fluent extension methodsDynamicRoutes.cs- Core logic for dynamic endpoint generationServices/- SOAP wrappers and documentation servicesExtensions/- Response handling and error mappingAuthentication/- 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 codeServiceReference/ArrayOfXElement.cs- Response wrapper implementingIArrayOfXElementServiceReference/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 codeServiceReference/ArrayOfXElement.cs- Response wrapper implementingIArrayOfXElementServiceReference/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.asmxServices/CallCenterWsSoapBase.cs- Base class implementing all 260 interface methods with default responsesServices/MockCallCenterService.cs- Overrides 13 operations with in-memory state logicState/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- BuildsArrayOfXElementresponses 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 handlingConverters/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
- Update
RentaStore.SitelinkAPI/appsettings.jsonwith your JWT configuration:
{
"Jwt": {
"Key": "your-secure-key-here-minimum-32-characters",
"Issuer": "RentaStore.SitelinkAPI",
"Audience": "RentaStore.Application"
}
}
- If needed, update SOAP service endpoint URLs in the generated
Reference.csfiles 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
- Service Registration - SOAP clients are registered as singletons
- Wrapper Creation - Each SOAP client is wrapped in a
DynamicSoapWrapper - Method Discovery - Reflection discovers all public methods on the SOAP client
- Route Generation - For each method, a POST endpoint is created at
/{prefix}/{MethodName} - Request Handling - Incoming JSON is mapped to SOAP method parameters
- Authentication - JWT claims provide Sitelink credentials to SOAP methods
- 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:
-88to-89,-91,-96to-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:
- Strips type prefixes from element names
- Converts to camelCase for JSON-friendly property names
- Applies type conversions based on the prefix
- 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 configurationappsettings.Development.json- Development-specific overrides- All
.csprojfiles - 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
- Fork the repository
- Create a feature branch (
git checkout -b feature/amazing-feature) - Commit your changes (
git commit -m 'Add amazing feature') - Push to the branch (
git push origin feature/amazing-feature) - Open a Pull Request
Mock SiteLink Service
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*Elementhelpers 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.,
GateAccessDataAsyncfor 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) useBuildResultwith 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 | Versions 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. |
-
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.