Skip to content

Commit d81cb40

Browse files
committed
Add C# HttpClient snippet copy feature
1 parent 946527f commit d81cb40

4 files changed

Lines changed: 134 additions & 19 deletions

File tree

DebugProbe.AspNetCore.Tests/Rendering/HtmlRendererTests.cs

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -204,6 +204,44 @@ public void Details_page_renders_curl_copy_attributes_and_buttons()
204204
Assert.Equal(2, occurrences);
205205
}
206206

207+
[Fact]
208+
public void Details_page_renders_csharp_copy_attributes_and_buttons()
209+
{
210+
var entry = CreateEntry();
211+
entry.OutgoingRequests.Add(new DebugOutgoingRequest
212+
{
213+
Method = "PUT",
214+
Url = "https://external-api.test/v1/update",
215+
StatusCode = 200,
216+
DurationMs = 120,
217+
RequestBody = "{\"name\":\"John\"}",
218+
RequestHeaders = new Dictionary<string, string> { ["Authorization"] = "Bearer token" }
219+
});
220+
221+
var html = HtmlRenderer.RenderDetailsPage(
222+
entry,
223+
CreateEnvironment(),
224+
"{\"request\":true}",
225+
"{\"response\":true}");
226+
227+
// 1. Verify Incoming Request Card attributes and button
228+
Assert.Contains("data-method=\"POST\"", html);
229+
Assert.Contains("data-url=\"http://example.test/orders?id=10\"", html);
230+
Assert.Contains("data-headers=\"{&quot;X-Test&quot;:&quot;yes&quot;}\"", html);
231+
Assert.Contains("data-body=\"{&quot;request&quot;:true}\"", html);
232+
Assert.Contains("class=\"csharp-copy-btn\"", html);
233+
234+
// 2. Verify Outgoing Request Card attributes and button
235+
Assert.Contains("data-method=\"PUT\"", html);
236+
Assert.Contains("data-url=\"https://external-api.test/v1/update\"", html);
237+
Assert.Contains("data-headers=\"{&quot;Authorization&quot;:&quot;Bearer token&quot;}\"", html);
238+
Assert.Contains("data-body=\"{&quot;name&quot;:&quot;John&quot;}\"", html);
239+
240+
// 3. Verify Response Card has no C# copy button (only 2 copy C# buttons in total should exist in HTML markup)
241+
var occurrences = (html.Length - html.Replace("class=\"csharp-copy-btn\"", "").Length) / "class=\"csharp-copy-btn\"".Length;
242+
Assert.Equal(2, occurrences);
243+
}
244+
207245
private static DebugEntry CreateEntry()
208246
{
209247
return new DebugEntry

DebugProbe.AspNetCore/Assets/css/debugprobe.css

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1293,7 +1293,8 @@ pre {
12931293
/* =========================
12941294
cURL Copy Button & Tooltip
12951295
========================= */
1296-
.curl-copy-btn {
1296+
.curl-copy-btn,
1297+
.csharp-copy-btn {
12971298
display: inline-flex;
12981299
align-items: center;
12991300
justify-content: center;
@@ -1308,13 +1309,15 @@ pre {
13081309
transition: all 0.15s ease;
13091310
}
13101311

1311-
.curl-copy-btn:hover {
1312+
.curl-copy-btn:hover,
1313+
.csharp-copy-btn:hover {
13121314
background: #f9fafb;
13131315
border-color: #d1d5db;
13141316
color: #111827;
13151317
}
13161318

1317-
.curl-copy-btn svg {
1319+
.curl-copy-btn svg,
1320+
.csharp-copy-btn svg {
13181321
width: 14px;
13191322
height: 14px;
13201323
stroke: currentColor;

DebugProbe.AspNetCore/Assets/js/debugprobe-ui.js

Lines changed: 77 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,82 @@ function buildCurlCommand(method, url, headers, body, isWindows) {
4343
return curlCmd;
4444
}
4545

46+
function showCopiedTooltip(btn) {
47+
const tooltip = document.createElement("div");
48+
tooltip.className = "copied-tooltip";
49+
tooltip.textContent = "Copied!";
50+
document.body.appendChild(tooltip);
51+
52+
const rect = btn.getBoundingClientRect();
53+
tooltip.style.left = (rect.left + window.scrollX + rect.width / 2) + "px";
54+
tooltip.style.top = (rect.top + window.scrollY) + "px";
55+
56+
setTimeout(() => {
57+
tooltip.remove();
58+
}, 1500);
59+
}
60+
61+
function escapeCSharpString(str) {
62+
if (!str) return "";
63+
return str
64+
.replace(/\\/g, "\\\\")
65+
.replace(/"/g, '\\"')
66+
.replace(/\r/g, '\\r')
67+
.replace(/\n/g, '\\n');
68+
}
69+
70+
function buildCSharpSnippet(method, url, headers, body) {
71+
const escapedUrl = escapeCSharpString(url);
72+
const methodFormatted = method.charAt(0).toUpperCase() + method.slice(1).toLowerCase();
73+
let snippet = `var request = new HttpRequestMessage(HttpMethod.${methodFormatted}, "${escapedUrl}");\n`;
74+
75+
for (const [key, value] of Object.entries(headers)) {
76+
if (!key || !value) continue;
77+
const trimmedVal = value.trim();
78+
if (trimmedVal === "[REDACTED]" || trimmedVal === "") continue;
79+
80+
snippet += `request.Headers.Add("${escapeCSharpString(key)}", "${escapeCSharpString(value)}");\n`;
81+
}
82+
83+
if (body && body.trim() !== "" && body !== "[Body too large]") {
84+
let contentType = "application/json";
85+
for (const [key, value] of Object.entries(headers)) {
86+
if (key.toLowerCase() === "content-type" && value && value.trim() !== "" && value.trim() !== "[REDACTED]") {
87+
contentType = value.split(";")[0].trim();
88+
break;
89+
}
90+
}
91+
snippet += `request.Content = new StringContent("${escapeCSharpString(body)}", Encoding.UTF8, "${escapeCSharpString(contentType)}");\n`;
92+
}
93+
94+
snippet += `var response = await httpClient.SendAsync(request);`;
95+
return snippet;
96+
}
97+
98+
function copyAsCSharp(btn) {
99+
const card = btn.closest(".trace-card");
100+
if (!card) return;
101+
102+
const method = card.dataset.method;
103+
const url = card.dataset.url;
104+
if (!method || !url) return;
105+
106+
let headers = {};
107+
try {
108+
headers = JSON.parse(card.dataset.headers || '{}');
109+
} catch (e) {
110+
// Fallback or ignore
111+
}
112+
113+
const body = card.dataset.body;
114+
115+
const snippet = buildCSharpSnippet(method, url, headers, body);
116+
117+
navigator.clipboard.writeText(snippet);
118+
119+
showCopiedTooltip(btn);
120+
}
121+
46122
function copyAsCurl(btn) {
47123
const card = btn.closest(".trace-card");
48124
if (!card) return;
@@ -67,19 +143,7 @@ function copyAsCurl(btn) {
67143

68144
navigator.clipboard.writeText(curlCmd);
69145

70-
// Show temporary "Copied!" tooltip
71-
const tooltip = document.createElement("div");
72-
tooltip.className = "copied-tooltip";
73-
tooltip.textContent = "Copied!";
74-
document.body.appendChild(tooltip);
75-
76-
const rect = btn.getBoundingClientRect();
77-
tooltip.style.left = (rect.left + window.scrollX + rect.width / 2) + "px";
78-
tooltip.style.top = (rect.top + window.scrollY) + "px";
79-
80-
setTimeout(() => {
81-
tooltip.remove();
82-
}, 1500);
146+
showCopiedTooltip(btn);
83147
}
84148

85149

DebugProbe.AspNetCore/Internal/Rendering/HtmlRenderer.cs

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -318,10 +318,10 @@ private static string BuildTraceCard(
318318
if (!string.IsNullOrWhiteSpace(dataHeaders)) dataAttrs += $" data-headers=\"{Encode(dataHeaders)}\"";
319319
if (!string.IsNullOrWhiteSpace(dataBody)) dataAttrs += $" data-body=\"{Encode(dataBody)}\"";
320320

321-
var copyCurlBtn = "";
321+
var copyBtns = "";
322322
if (!string.IsNullOrWhiteSpace(dataMethod))
323323
{
324-
copyCurlBtn = $@"
324+
copyBtns = $@"
325325
<button class=""curl-copy-btn""
326326
type=""button""
327327
title=""Copy as cURL""
@@ -331,6 +331,16 @@ private static string BuildTraceCard(
331331
<path d=""M16 4h2a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h2""></path>
332332
<rect x=""8"" y=""2"" width=""8"" height=""4"" rx=""1"" ry=""1""></rect>
333333
</svg>
334+
</button>
335+
<button class=""csharp-copy-btn""
336+
type=""button""
337+
title=""Copy as C#""
338+
aria-label=""Copy as C#""
339+
onclick=""copyAsCSharp(this)"">
340+
<svg viewBox=""0 0 24 24"" aria-hidden=""true"">
341+
<path d=""M16 18l6-6-6-6""></path>
342+
<path d=""M8 6l-6 6 6 6""></path>
343+
</svg>
334344
</button>";
335345
}
336346

@@ -347,7 +357,7 @@ private static string BuildTraceCard(
347357
<div class=""trace-card-meta"">
348358
{status}
349359
{duration}
350-
{copyCurlBtn}
360+
{copyBtns}
351361
</div>
352362
</div>
353363
<div class=""trace-details"">

0 commit comments

Comments
 (0)