Integrate from .NET Framework 2.0 / 4.5
Source
Mirrored from the suite checkout docs/DOTNET_FRAMEWORK_INTEGRATION.md. Prefer updating that file, then re-copy here when contracts change.
How-to for legacy .NET Framework apps that must call OxyKit.Mail or OxyKit.Render over HTTP.
Do not reference OxyKit.Mail.Client, OxyKit.Render.Client, or the Contracts NuGet packages from Framework. Those SDKs target modern .NET (net10.0 today). Call the JSON APIs directly. Prefer upgrading the host to .NET 10+ and using the typed clients when you can.
Authoritative shapes: live OpenAPI on each API in Development (/openapi/v1.json, Scalar /scalar). This doc is a minimal cookbook, not a second OpenAPI catalog.
Which sample covers which Framework?
Two HTTP stacks cover essentially every supported Framework app:
| Your project targets | Use this sample | Why |
|---|---|---|
| 2.0, 3.0, 3.5, 4.0 | 2.0 (HttpWebRequest) | No HttpClient in the BCL until 4.5 |
| 4.5, 4.6, 4.7, 4.8 | 4.5 (HttpClient) | Same pattern through the end of Framework |
Framework 1.x is out of scope. You do not need separate docs for 3.x or 4.6/4.7/4.8 — pick the row above.
| Service | Default local base | Auth header |
|---|---|---|
| OxyKit.Mail | http://localhost:5026 | X-Api-Key: {key} (or Authorization: Bearer {jwt}) |
| OxyKit.Render | http://localhost:5066 | X-OxyKit-Render-Key: {key} |
Shared notes
- Build JSON with
string.Format/ concatenation unless you already ship a JSON library. Do not assumeSystem.Text.Json. - Prefer UTF-8 bodies and
Content-Type: application/json; charset=utf-8. - Escape JSON string values (
\,", control chars). The 2.0 samples include a tiny helper; reuse it from 4.5 if needed. - HTTPS + TLS 1.2: enable before the first request (once per AppDomain) on older runtimes:
// Prefer the enum on 4.5+. On 2.0 / 4.0 use the numeric cast if the enum value is missing.
ServicePointManager.SecurityProtocol =
(SecurityProtocolType)3072; // TLS 1.2- Keep API keys out of source control.
OxyKit.Mail — send email
POST /api/v1/email/send
Success means the message was accepted for Mail-owned delivery — do not re-send when status is sent, accepted, or scheduled.
| HTTP | Typical status | Meaning |
|---|---|---|
| 200 | sent | SMTP succeeded immediately |
| 202 | accepted | Queued for Mail retry / delivery |
| 202 | scheduled | Stored for later send |
Example body:
{
"to": "user@example.com",
"cc": "",
"bcc": "",
"subject": "Hello",
"body": "Plain text or HTML",
"isHtml": false
}Example success body:
{
"emailId": "11111111-1111-1111-1111-111111111111",
"status": "accepted",
"message": "..."
}Sample — Framework 2.0 (also 3.x / 4.0)
using System;
using System.IO;
using System.Net;
using System.Text;
public static class OxyKitMailSendFw20
{
public static string Send(
string baseUrl,
string apiKey,
string to,
string subject,
string body,
bool isHtml)
{
string json = string.Format(
"{{\"to\":{0},\"cc\":\"\",\"bcc\":\"\",\"subject\":{1},\"body\":{2},\"isHtml\":{3}}}",
JsonString(to),
JsonString(subject),
JsonString(body),
isHtml ? "true" : "false");
Uri uri = new Uri(Combine(baseUrl, "api/v1/email/send"));
HttpWebRequest req = (HttpWebRequest)WebRequest.Create(uri);
req.Method = "POST";
req.ContentType = "application/json; charset=utf-8";
req.Headers["X-Api-Key"] = apiKey;
byte[] bytes = Encoding.UTF8.GetBytes(json);
req.ContentLength = bytes.Length;
using (Stream s = req.GetRequestStream())
s.Write(bytes, 0, bytes.Length);
HttpWebResponse resp;
try
{
resp = (HttpWebResponse)req.GetResponse();
}
catch (WebException ex)
{
if (ex.Response == null)
throw;
resp = (HttpWebResponse)ex.Response;
}
using (resp)
using (StreamReader reader = new StreamReader(resp.GetResponseStream(), Encoding.UTF8))
{
string responseBody = reader.ReadToEnd();
int code = (int)resp.StatusCode;
if (code != 200 && code != 202)
throw new ApplicationException(
"OxyKit.Mail send failed: HTTP " + code + " " + responseBody);
// Treat sent / accepted / scheduled as success — do not re-send.
if (responseBody.IndexOf("\"status\":\"sent\"") < 0
&& responseBody.IndexOf("\"status\":\"accepted\"") < 0
&& responseBody.IndexOf("\"status\":\"scheduled\"") < 0)
{
throw new ApplicationException(
"OxyKit.Mail returned unexpected body: " + responseBody);
}
return responseBody;
}
}
private static string Combine(string baseUrl, string path)
{
if (baseUrl.EndsWith("/"))
return baseUrl + path;
return baseUrl + "/" + path;
}
private static string JsonString(string value)
{
if (value == null)
value = "";
StringBuilder sb = new StringBuilder();
sb.Append('"');
for (int i = 0; i < value.Length; i++)
{
char c = value[i];
switch (c)
{
case '\\': sb.Append("\\\\"); break;
case '"': sb.Append("\\\""); break;
case '\n': sb.Append("\\n"); break;
case '\r': sb.Append("\\r"); break;
case '\t': sb.Append("\\t"); break;
default:
if (c < 0x20)
sb.AppendFormat("\\u{0:x4}", (int)c);
else
sb.Append(c);
break;
}
}
sb.Append('"');
return sb.ToString();
}
}Sample — Framework 4.5+ (also 4.6 / 4.7 / 4.8)
Reference System.Net.Http (BCL on 4.5+). Same JSON / status rules as the 2.0 sample — share JsonString in one helper if you maintain both.
using System;
using System.Net.Http;
using System.Text;
using System.Threading.Tasks;
public static class OxyKitMailSendFw45
{
private static readonly HttpClient Http = new HttpClient();
public static async Task<string> SendAsync(
string baseUrl,
string apiKey,
string to,
string subject,
string body,
bool isHtml)
{
string json = string.Format(
"{{\"to\":{0},\"cc\":\"\",\"bcc\":\"\",\"subject\":{1},\"body\":{2},\"isHtml\":{3}}}",
JsonString(to),
JsonString(subject),
JsonString(body),
isHtml ? "true" : "false");
using (var content = new StringContent(json, Encoding.UTF8, "application/json"))
using (var request = new HttpRequestMessage(HttpMethod.Post, Combine(baseUrl, "api/v1/email/send")))
{
request.Headers.TryAddWithoutValidation("X-Api-Key", apiKey);
request.Content = content;
using (HttpResponseMessage response = await Http.SendAsync(request).ConfigureAwait(false))
{
string responseBody = await response.Content.ReadAsStringAsync().ConfigureAwait(false);
int code = (int)response.StatusCode;
if (code != 200 && code != 202)
throw new ApplicationException(
"OxyKit.Mail send failed: HTTP " + code + " " + responseBody);
if (responseBody.IndexOf("\"status\":\"sent\"", StringComparison.Ordinal) < 0
&& responseBody.IndexOf("\"status\":\"accepted\"", StringComparison.Ordinal) < 0
&& responseBody.IndexOf("\"status\":\"scheduled\"", StringComparison.Ordinal) < 0)
{
throw new ApplicationException(
"OxyKit.Mail returned unexpected body: " + responseBody);
}
return responseBody;
}
}
}
private static string Combine(string baseUrl, string path)
{
if (!baseUrl.EndsWith("/"))
baseUrl += "/";
return baseUrl + path;
}
private static string JsonString(string value)
{
if (value == null)
value = "";
StringBuilder sb = new StringBuilder();
sb.Append('"');
for (int i = 0; i < value.Length; i++)
{
char c = value[i];
switch (c)
{
case '\\': sb.Append("\\\\"); break;
case '"': sb.Append("\\\""); break;
case '\n': sb.Append("\\n"); break;
case '\r': sb.Append("\\r"); break;
case '\t': sb.Append("\\t"); break;
default:
if (c < 0x20)
sb.AppendFormat("\\u{0:x4}", (int)c);
else
sb.Append(c);
break;
}
}
sb.Append('"');
return sb.ToString();
}
}Sync call site only if needed: SendAsync(...).GetAwaiter().GetResult().
Create a sender API key in Mail with a role that may send (Admin or Sender).
OxyKit.Render — render a document
Example: table → HTML (binary formats: write the response stream to a file).
POST /render/table/html (also under /v1/render/...)
Example body:
{
"sheets": [
{
"name": "Orders",
"headers": ["Id", "Customer", "Total"],
"rows": [[1, "Acme", 120.5], [2, "Beta", 80]]
}
]
}Errors return JSON { "error": "..." } with 4xx/5xx. Success returns the document bytes (text/html, application/pdf, etc.).
Sample — Framework 2.0 (also 3.x / 4.0)
using System;
using System.IO;
using System.Net;
using System.Text;
public static class OxyKitRenderFw20
{
public static void RenderTableHtml(
string baseUrl,
string apiKey,
string jsonBody,
string outputFilePath)
{
Uri uri = new Uri(Combine(baseUrl, "render/table/html"));
HttpWebRequest req = (HttpWebRequest)WebRequest.Create(uri);
req.Method = "POST";
req.ContentType = "application/json; charset=utf-8";
req.Headers["X-OxyKit-Render-Key"] = apiKey;
byte[] bytes = Encoding.UTF8.GetBytes(jsonBody);
req.ContentLength = bytes.Length;
using (Stream s = req.GetRequestStream())
s.Write(bytes, 0, bytes.Length);
HttpWebResponse resp;
try
{
resp = (HttpWebResponse)req.GetResponse();
}
catch (WebException ex)
{
if (ex.Response == null)
throw;
resp = (HttpWebResponse)ex.Response;
}
using (resp)
{
int code = (int)resp.StatusCode;
using (Stream stream = resp.GetResponseStream())
{
if (code < 200 || code >= 300)
{
using (StreamReader reader = new StreamReader(stream, Encoding.UTF8))
throw new ApplicationException(
"OxyKit.Render failed: HTTP " + code + " " + reader.ReadToEnd());
}
using (FileStream file = File.Create(outputFilePath))
CopyTo(stream, file);
}
}
}
private static void CopyTo(Stream input, Stream output)
{
byte[] buffer = new byte[8192];
int read;
while ((read = input.Read(buffer, 0, buffer.Length)) > 0)
output.Write(buffer, 0, read);
}
private static string Combine(string baseUrl, string path)
{
if (baseUrl.EndsWith("/"))
return baseUrl + path;
return baseUrl + "/" + path;
}
}Sample — Framework 4.5+ (also 4.6 / 4.7 / 4.8)
using System;
using System.IO;
using System.Net.Http;
using System.Text;
using System.Threading.Tasks;
public static class OxyKitRenderFw45
{
private static readonly HttpClient Http = new HttpClient();
public static async Task RenderTableHtmlAsync(
string baseUrl,
string apiKey,
string jsonBody,
string outputFilePath)
{
using (var content = new StringContent(jsonBody, Encoding.UTF8, "application/json"))
using (var request = new HttpRequestMessage(HttpMethod.Post, Combine(baseUrl, "render/table/html")))
{
request.Headers.TryAddWithoutValidation("X-OxyKit-Render-Key", apiKey);
request.Content = content;
using (HttpResponseMessage response = await Http.SendAsync(request).ConfigureAwait(false))
{
if (!response.IsSuccessStatusCode)
{
string err = await response.Content.ReadAsStringAsync().ConfigureAwait(false);
throw new ApplicationException(
"OxyKit.Render failed: HTTP " + (int)response.StatusCode + " " + err);
}
using (Stream stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false))
using (FileStream file = File.Create(outputFilePath))
await stream.CopyToAsync(file).ConfigureAwait(false);
}
}
}
private static string Combine(string baseUrl, string path)
{
if (!baseUrl.EndsWith("/"))
baseUrl += "/";
return baseUrl + path;
}
}Usage (local Dev key from Render appsettings.Development.json):
string body =
"{\"sheets\":[{\"name\":\"Orders\",\"headers\":[\"Id\",\"Customer\",\"Total\"],"
+ "\"rows\":[[1,\"Acme\",120.5],[2,\"Beta\",80]]}]}";
// 2.0–4.0:
OxyKitRenderFw20.RenderTableHtml(
"http://localhost:5066", "dev-local-key", body, @"C:\temp\orders.html");
// 4.5+:
await OxyKitRenderFw45.RenderTableHtmlAsync(
"http://localhost:5066", "dev-local-key", body, @"C:\temp\orders.html");Other transforms: change the path (render/html/pdf, render/table/pdf, render/template/pdf, …) and the JSON shape. See each product README / OpenAPI.
When to use the typed clients instead
| Host | Recommendation |
|---|---|
| .NET Framework 2.0–4.0 | This HOW-TO — 2.0 sample |
| .NET Framework 4.5–4.8 | This HOW-TO — 4.5 sample (raw HTTP; Client NuGet still not for Framework) |
| .NET Framework 4.6.2+ later | Possible same-package netstandard2.0 multi-target — not shipped yet |
| .NET 10+ | OxyKit.Mail.Client / OxyKit.Render.Client |
Smoke without code
Import each product’s hoppscotch/ collection + Local env, select OxyKit.Mail Local or OxyKit.Render Local, run folders in order. Match headers and bodies from those requests when debugging Framework callers.