A small YARP reverse proxy that
makes JSON REST APIs consumable by the SSRS / Power BI Report Server XML
data source. It forwards GET requests to a configured upstream and
rewrites the JSON response as a flat, tabular XML document. Auth (API keys,
bearer tokens, etc.) is configured declaratively using YARP's built-in
transforms — no custom code.
Deployment model: this proxy is intended to run locally on the report server (the SSRS / PBIRS box) and listen on
localhostonly. SSRS data sources point athttp://localhost:5080/.... The proxy injects API keys from its own config so the keys never appear in report definitions or shared data source connection strings. Do not expose it on a routable interface — it has no authentication of its own. By default it binds only to the loopback interface (127.0.0.1and::1).
SSRS and Power BI Report Server ship an "XML" data source that can hit any
URL and bind the response as a dataset — but it expects XML, not JSON, and
its query language is finicky about element shapes and types. Most modern
REST APIs return JSON. This proxy sits between them and emits a single
predictable XML shape (<Root><Row>...</Row>...</Root>) so that:
- No SSRS dataset query is required (auto-detect finds
<Row>). - The same one-line query works for every endpoint.
- A
?_query=1debug URL returns a paste-ready<Query>document with inferred field types (Integer,Decimal,Boolean,DateTime).
- A YARP route maps an incoming proxy path to an upstream cluster
(
appsettings.json→ReverseProxy:Routes/Clusters). - Auth and any other request shaping uses YARP's built-in transforms
(
RequestHeader,QueryValueParameter,PathPattern, …). - A response transform buffers the upstream JSON and emits a tabular XML
document — always the same shape, so the SSRS dataset needs no query at
all (auto-detect picks
<Row>), or one boilerplate query that works for every endpoint:<Query><ElementPath IgnoreNamespaces="true">Root/Row</ElementPath></Query>
- Appending
?_query=1to any proxied URL returns a generated<Query>document with type-inferred fields, ready to paste into Report Builder.
<Root>
<Row>...scalar fields...</Row>
<Row>...</Row>
</Root>- If the route has
Metadata.RowPathconfigured, that dotted JSON path is resolved; if it's an array, its items are the rows. - Else if the top-level JSON value is an array, its items are the rows.
- Else if the top-level value is an object with exactly one record-like array property (array of objects), that array's items are the rows.
- Else the whole top-level object is a single row.
- A scalar top-level value becomes one row with a
<Value>field.
- Nested objects collapse with
_:address.geo.lat→<address_geo_lat>. - Nested arrays are serialized as a compact JSON string in one element (so the row stays tabular and the column set is fixed per endpoint).
null→ empty element. Numbers/booleans → text form.- XML-illegal characters in JSON property names are sanitized
(
@context→_context, leadingxml*is prefixed with_).
Add a route + cluster under ReverseProxy. Authentication is just another
transform — no custom config section needed.
| Goal | Transform |
|---|---|
| API key in a header | { "RequestHeader": "X-API-Key", "Set": "KEY" } |
| Bearer token | { "RequestHeader": "Authorization", "Set": "Bearer KEY" } |
| API key in a query parameter | { "QueryValueParameter": "api_key", "Set": "KEY" } |
| Basic auth | { "RequestHeader": "Authorization", "Set": "Basic <base64(user:pass)>"} |
| Custom User-Agent (e.g. NWS) | { "RequestHeader": "User-Agent", "Set": "MyApp (you@example.com)" } |
Layered .NET config means env vars or User Secrets override the file. The
Transforms array is positional; the key is the transform's index:
# Override the "Set" value of the 2nd transform on route "myapi"
$env:ReverseProxy__Routes__myapi__Transforms__1__Set = "real-secret-key"Or in dev:
dotnet user-secrets set "ReverseProxy:Routes:myapi:Transforms:1:Set" "real-secret-key"Run the proxy and try:
| Proxy URL | Upstream |
|---|---|
/jsonplaceholder/posts/1 |
https://jsonplaceholder.typicode.com/posts/1 |
/jsonplaceholder/users |
https://jsonplaceholder.typicode.com/users |
/openlibrary/search.json?q=tolkien |
https://openlibrary.org/search.json?q=tolkien |
/weather/points/39.7456,-97.0892 |
https://api.weather.gov/points/39.7456,-97.0892 |
/echo/headers (demo: API key in header) |
https://httpbin.org/headers |
/echo/query (demo: API key in query) |
https://httpbin.org/get |
Append ?_query=1 to any of these to get a paste-ready <Query> document.
dotnet runListens on http://localhost:5080 by default (loopback only). Override with
--urls or the ASPNETCORE_URLS environment variable if you really must
bind elsewhere — but see the warning at the top.
The app uses Microsoft.Extensions.Hosting.WindowsServices, so it can be
launched directly by the Service Control Manager.
From an elevated PowerShell prompt in the cloned repo:
.\install.ps1That will:
dotnet publisha self-containedwin-x64build intoC:\Program Files\XmlDataSourceProxy(override with-InstallDir).- Create a service named
XmlDataSourceProxyrunning under theNT SERVICE\XmlDataSourceProxyvirtual account (auto-created by SCM, no password to manage). - Grant that account read/execute on the install directory.
- Configure auto-start and a restart-on-failure policy.
- Start the service and verify it's listening on
127.0.0.1:5080.
Re-running install.ps1 performs an in-place upgrade (stop, overwrite,
restart).
To remove:
.\uninstall.ps1 # stop + delete service, leave files
.\uninstall.ps1 -RemoveFiles # also delete C:\Program Files\XmlDataSourceProxydotnet publish -c Release -r win-x64 --self-contained `
-o "C:\Program Files\XmlDataSourceProxy"
sc.exe create XmlDataSourceProxy `
binPath= "\"C:\Program Files\XmlDataSourceProxy\XmlDataSourceProxy.exe\"" `
start= auto `
obj= "NT SERVICE\XmlDataSourceProxy" `
DisplayName= "XML Data Source Proxy for SSRS/PBIRS"
sc.exe start XmlDataSourceProxyLogs go to the Windows Application event log under source
XmlDataSourceProxy by default. For richer/structured logs, drop a
Serilog/Seq sink into Program.cs — not included to keep the project
minimal.
The published folder (C:\Program Files\XmlDataSourceProxy) contains the
default appsettings.json from this repo. Either edit it in place or
override individual settings with environment variables (preferred for
secrets — env vars don't get checked into source control, and the virtual
service account can read them without anyone seeing them in JSON):
# Per-service environment variable (PowerShell, elevated).
New-ItemProperty -Path "HKLM:\SYSTEM\CurrentControlSet\Services\XmlDataSourceProxy" `
-Name Environment -PropertyType MultiString `
-Value @("ReverseProxy__Routes__myapi__Transforms__1__Set=real-secret-key")
Restart-Service XmlDataSourceProxy- Data source type: XML. Connection string: the proxy URL.
- Create a dataset. Either leave the query empty (auto-detect finds
<Row>) or paste the boilerplate<Query><ElementPath IgnoreNamespaces="true">Root/Row</ElementPath></Query>. - For real types instead of
String, hit the URL with?_query=1, copy the returned<Query>...</Query>block, and paste that into the dataset Query box. It includes inferredInteger/Decimal/Boolean/DateTimeannotations. - Refresh Fields, drag, render.
Program.cs -- wires YARP + middleware
DesignTimeQueryMiddleware.cs -- snoops/strips ?_query=1
JsonToXmlTransformProvider.cs -- registers the response transform per route
JsonToXmlResponseTransform.cs -- JSON -> tabular XML
QueryDocumentBuilder.cs -- builds the <Query> document
appsettings.json -- routes, clusters, transforms