diff --git a/CHANGELOG.md b/CHANGELOG.md
index 3ae4084e..28271b2a 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -17,7 +17,10 @@ This release does not contain security updates.
### Added
+- ⚡️ `OslcRequestParams` class for configuring request options (media types, headers, OSLC Core version). Parameters can be pre-set in the library, set in the `OslcClient` constructor, or overridden on a per-request basis.
+- ⚡️ Graph accumulation mode in `OslcClient` via `EnableGraphAccumulation()` - useful for the initial discovery phase to accumulate all service provider information in a single graph.
- `RootServicesHelper` was added to assist with processing OSLC Root Services documents. It can help with direct lookups (as long as your URI ends with `/rootservices` or `/rootservices.xml`), can look up a standard `/.well-known/oslc/rootservices.xml` location, or fall back to appending `/rootservices` for legacy systems.
+- New overloads for `GetResourceAsync` and `CreateResourceAsync` accepting `OslcRequestParams` for per-request parameter customization.
- ⚡️Samples for IBM Jazz ERM (aka Doors NG), ETM, and EWM were migrated to .NET 10 and tested against Jazz.net. You can run them yourself using `OSLC4Net_SDK\Examples\scripts\test-jazz_net.ps1`.
@@ -25,10 +28,12 @@ This release does not contain security updates.
- `OSLC4Net.Core` requires .NET 10 to be able to use the `[Experimental]` annotation.
- `OSLC4Net.Client` requires .NET 10.
+- `OslcClient` now has a `DefaultRequestParams` property for default request parameters configuration.
- ❗️ `SignedByteNode` (which corresponds to `xsd:byte`) is now parsed as C# `sbyte` (signed byte) instead of `byte`.
### Deprecated
+- 👉 `OslcRestClient` remains deprecated since 0.5.0. Use `OslcClient` instead.
- Getters and setters for the RDF type (both `GetRdfTypes()` and `GetTypes()`)
are deprecated in favor of the `.Types` property.
diff --git a/OSLC4Net_SDK/OSLC4Net.Client/Oslc/OslcClient.cs b/OSLC4Net_SDK/OSLC4Net.Client/Oslc/OslcClient.cs
index 8fe410db..6f5d1116 100644
--- a/OSLC4Net_SDK/OSLC4Net.Client/Oslc/OslcClient.cs
+++ b/OSLC4Net_SDK/OSLC4Net.Client/Oslc/OslcClient.cs
@@ -30,8 +30,16 @@
namespace OSLC4Net.Client.Oslc;
///
-/// An OSLC Client.
+/// An OSLC Client. This is the primary client for OSLC operations.
///
+///
+/// OslcClient supports:
+///
+/// - Configurable request parameters via (can be set in constructor or per-request)
+/// - Access to both unmarshalled POCOs and raw response graphs via
+/// - Accumulating responses in a single graph for discovery phases via
+///
+///
public class OslcClient : IDisposable
{
private readonly ILogger _logger;
@@ -42,6 +50,17 @@ public class OslcClient : IDisposable
protected readonly ISet _formatters;
protected readonly HttpClient _client;
+ ///
+ /// Default request parameters used for all requests unless overridden.
+ ///
+ public OslcRequestParams DefaultRequestParams { get; }
+
+ ///
+ /// When set, all response graphs will be merged into this graph.
+ /// Useful for the initial discovery phase to accumulate all service provider information.
+ ///
+ public Graph? AccumulatingGraph { get; private set; }
+
protected string AcceptHeader { get; } =
"text/turtle;q=1.0, application/rdf+xml;q=0.9, application/n-triples;q=0.8, text/n3;q=0.7";
@@ -52,16 +71,28 @@ public OslcClient(ILogger logger) : this(false, logger)
{
}
+ ///
+ /// Initialize a new OslcClient with custom default request parameters.
+ ///
+ /// Logger instance.
+ /// Default request parameters for all requests.
+ public OslcClient(ILogger logger, OslcRequestParams defaultRequestParams)
+ : this(false, logger, defaultRequestParams)
+ {
+ }
+
///
/// Initialize a new OslcClient using an externally managed HttpClient (e.g. with resilience policies).
///
/// Pre-configured HttpClient instance (lifetime managed by caller).
/// Logger instance.
- public OslcClient(HttpClient client, ILogger logger)
+ /// Optional default request parameters for all requests.
+ public OslcClient(HttpClient client, ILogger logger, OslcRequestParams? defaultRequestParams = null)
{
_logger = logger;
_client = client;
_formatters = new HashSet { new RdfXmlMediaTypeFormatter() };
+ DefaultRequestParams = defaultRequestParams ?? OslcRequestParams.Default;
}
///
@@ -116,14 +147,23 @@ protected OslcClient(Func logger) : this(null,
+ private OslcClient(HttpClientHandler? customHandler, ILogger logger,
+ OslcRequestParams? defaultRequestParams = null) : this(null,
customHandler, logger)
{
+ DefaultRequestParams = defaultRequestParams ?? OslcRequestParams.Default;
}
private OslcClient(bool allowInvalidTlsCerts, ILogger logger)
+ : this(allowInvalidTlsCerts, logger, null)
+ {
+ }
+
+ private OslcClient(bool allowInvalidTlsCerts, ILogger logger,
+ OslcRequestParams? defaultRequestParams)
{
_logger = logger;
var handler = new HttpClientHandler
@@ -146,13 +186,15 @@ private OslcClient(bool allowInvalidTlsCerts, ILogger logger)
_formatters.Add(new RdfXmlMediaTypeFormatter());
_client = new HttpClient(handler);
+ DefaultRequestParams = defaultRequestParams ?? OslcRequestParams.Default;
}
public static OslcClient ForBasicAuth(string username, string password,
ILogger logger,
- HttpClientHandler handler = null)
+ HttpClientHandler? handler = null,
+ OslcRequestParams? defaultRequestParams = null)
{
- var oslcClient = new OslcClient(handler, logger);
+ var oslcClient = new OslcClient(handler, logger, defaultRequestParams);
var client = oslcClient.GetHttpClient();
var credentials = Convert.ToBase64String(Encoding.ASCII.GetBytes($"{username}:{password}"));
client.DefaultRequestHeaders.Authorization =
@@ -164,11 +206,11 @@ public static OslcClient ForBasicAuth(string username, string password,
/// Create an OslcClient for Basic Auth using a pre-configured HttpClient (e.g. with resilience policies).
///
public static OslcClient ForBasicAuth(HttpClient httpClient, string username, string password,
- ILogger logger)
+ ILogger logger, OslcRequestParams? defaultRequestParams = null)
{
var credentials = Convert.ToBase64String(Encoding.ASCII.GetBytes($"{username}:{password}"));
httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Basic", credentials);
- return new OslcClient(httpClient, logger);
+ return new OslcClient(httpClient, logger, defaultRequestParams);
}
///
@@ -180,10 +222,59 @@ public HttpClient GetHttpClient()
return _client;
}
+ ///
+ /// Enables graph accumulation mode. All response graphs will be merged into a single graph.
+ /// Useful for the initial discovery phase to accumulate all service provider information.
+ ///
+ /// The accumulating graph that will contain all merged response data.
+ public Graph EnableGraphAccumulation()
+ {
+ AccumulatingGraph ??= new Graph();
+ return AccumulatingGraph;
+ }
+
+ ///
+ /// Disables graph accumulation mode and optionally returns the accumulated graph.
+ ///
+ /// The accumulated graph, or null if accumulation was not enabled.
+ public Graph? DisableGraphAccumulation()
+ {
+ var result = AccumulatingGraph;
+ AccumulatingGraph = null;
+ return result;
+ }
+
+ ///
+ /// Clears the accumulating graph without disabling accumulation mode.
+ ///
+ public void ClearAccumulatingGraph()
+ {
+ AccumulatingGraph?.Clear();
+ }
+
public async Task> GetResourceAsync(string resourceUri, string? mediaType)
where T : IExtendedResource, new()
{
- var httpResponseMessage = await GetResourceRawAsync(resourceUri, mediaType).ConfigureAwait(false);
+ return await GetResourceAsync(resourceUri, mediaType, null).ConfigureAwait(false);
+ }
+
+ ///
+ /// Get an OSLC resource with request parameter overrides.
+ ///
+ /// The type of resource to retrieve.
+ /// The URI of the resource.
+ /// The media type to accept (deprecated, use requestParams instead).
+ /// Request parameters to override defaults.
+ /// An OslcResponse containing the resource(s) and graph.
+ public async Task> GetResourceAsync(string resourceUri, string? mediaType,
+ OslcRequestParams? requestParams)
+ where T : IExtendedResource, new()
+ {
+ var effectiveParams = DefaultRequestParams.Merge(requestParams);
+ var acceptHeader = mediaType ?? effectiveParams.AcceptHeader ?? AcceptHeader;
+
+ var httpResponseMessage = await GetResourceRawAsync(resourceUri, acceptHeader, effectiveParams)
+ .ConfigureAwait(false);
// REVISIT: according to the spec, non-success codes may also come with a RDF response - should, actually! (@berezovskyi 2024-10)
// consider adding .ErrorResource to the OslcResponse
if (httpResponseMessage.IsSuccessStatusCode && httpResponseMessage.Content is not null)
@@ -205,6 +296,12 @@ public async Task> GetResourceAsync(string resourceUri, strin
var graph = await httpResponseMessage.Content.ReadAsAsync(typeof(Graph), _formatters)
.ConfigureAwait(false) as Graph;
+ // Merge into accumulating graph if enabled
+ if (AccumulatingGraph is not null && graph is not null)
+ {
+ AccumulatingGraph.Merge(graph);
+ }
+
return OslcResponse.WithSuccess(resources?.ToList(), graph, httpResponseMessage);
}
else
@@ -238,12 +335,25 @@ public async Task> GetResourceAsync(string resourceUri, strin
public Task> GetResourceAsync(string resourceUri) where T : IExtendedResource, new()
{
- return GetResourceAsync(resourceUri, null);
+ return GetResourceAsync(resourceUri, null, null);
}
public Task> GetResourceAsync(Uri typeURI) where T : IExtendedResource, new()
{
- return GetResourceAsync(typeURI.ToString(), null);
+ return GetResourceAsync(typeURI.ToString(), null, null);
+ }
+
+ ///
+ /// Get an OSLC resource with request parameter overrides.
+ ///
+ /// The type of resource to retrieve.
+ /// The URI of the resource.
+ /// Request parameters to override defaults.
+ /// An OslcResponse containing the resource(s) and graph.
+ public Task> GetResourceAsync(Uri typeURI, OslcRequestParams? requestParams)
+ where T : IExtendedResource, new()
+ {
+ return GetResourceAsync(typeURI.ToString(), null, requestParams);
}
///
@@ -251,11 +361,37 @@ public async Task> GetResourceAsync(string resourceUri, strin
///
public async Task GetResourceRawAsync(string url, string? mediaType = null)
{
+ return await GetResourceRawAsync(url, mediaType, null).ConfigureAwait(false);
+ }
+
+ ///
+ /// Consider using instead.
+ ///
+ /// The URL to fetch.
+ /// The Accept media type.
+ /// Optional request parameters to override defaults.
+ public async Task GetResourceRawAsync(string url, string? mediaType,
+ OslcRequestParams? requestParams)
+ {
+ var effectiveParams = DefaultRequestParams.Merge(requestParams);
+ var accept = mediaType ?? effectiveParams.AcceptHeader ?? AcceptHeader;
+ var oslcVersion = effectiveParams.OslcCoreVersion ?? OslcRequestParams.DefaultOslcCoreVersion;
+
_client.DefaultRequestHeaders.Accept.Clear();
- // TODO: use uniformly (@berezovskyi 2024-10)
- _client.DefaultRequestHeaders.Accept.ParseAdd(mediaType ?? AcceptHeader);
+ _client.DefaultRequestHeaders.Accept.ParseAdd(accept);
_client.DefaultRequestHeaders.Remove(OSLCConstants.OSLC_CORE_VERSION);
- _client.DefaultRequestHeaders.Add(OSLCConstants.OSLC_CORE_VERSION, "2.0");
+ _client.DefaultRequestHeaders.Add(OSLCConstants.OSLC_CORE_VERSION, oslcVersion);
+
+ // Apply custom headers from request parameters
+ if (effectiveParams.CustomHeaders is not null)
+ {
+ foreach (var header in effectiveParams.CustomHeaders)
+ {
+ _client.DefaultRequestHeaders.Remove(header.Key);
+ _client.DefaultRequestHeaders.Add(header.Key, header.Value);
+ }
+ }
+
HttpResponseMessage response;
bool redirect;
byte redirectCount = 0;
@@ -396,7 +532,28 @@ public async Task DeleteResourceAsync(string url, Cancellat
public async Task> CreateResourceAsync(string url, T artifact, string? mediaType = null)
where T : IExtendedResource, new()
{
- var response = await CreateResourceRawAsync(url, artifact, mediaType).ConfigureAwait(false);
+ return await CreateResourceAsync(url, artifact, mediaType, null).ConfigureAwait(false);
+ }
+
+ ///
+ /// Create an OSLC resource with request parameter overrides.
+ ///
+ /// The type of resource to create.
+ /// The creation factory URL.
+ /// The resource to create.
+ /// The Content-Type media type (deprecated, use requestParams instead).
+ /// Request parameters to override defaults.
+ /// An OslcResponse containing the created resource.
+ public async Task> CreateResourceAsync(string url, T artifact, string? mediaType,
+ OslcRequestParams? requestParams)
+ where T : IExtendedResource, new()
+ {
+ var effectiveParams = DefaultRequestParams.Merge(requestParams);
+ var contentType = mediaType ?? effectiveParams.ContentType ?? OSLCConstants.CT_RDF;
+ var acceptType = effectiveParams.AcceptHeader ?? AcceptHeader;
+
+ var response = await CreateResourceRawAsync(url, artifact, contentType, acceptType, effectiveParams)
+ .ConfigureAwait(false);
// a bit outside the spec, but these should be success statuses
if (response.StatusCode == HttpStatusCode.OK
|| response.StatusCode == HttpStatusCode.Created
@@ -406,7 +563,7 @@ public async Task> CreateResourceAsync(string url, T artifact
// we have two options: the Location header points to a newly created resource or the resource is returned directly
// I think OSLC mandates Location, so let's start with that
var createdUri = response.Headers.Location?.AbsoluteUri;
- return await GetResourceAsync(createdUri, mediaType).ConfigureAwait(false);
+ return await GetResourceAsync(createdUri, acceptType, requestParams).ConfigureAwait(false);
}
else
{
@@ -444,14 +601,41 @@ public async Task CreateResourceRawAsync(Uri uri, IResource
public async Task CreateResourceRawAsync(string url, IResource artifact, string mediaType,
string acceptType)
{
+ return await CreateResourceRawAsync(url, artifact, mediaType, acceptType, null).ConfigureAwait(false);
+ }
+
+ ///
+ /// Create (POST) an artifact to a URL - usually an OSLC Creation Factory
+ ///
+ /// The creation factory URL.
+ /// The resource to create.
+ /// The Content-Type.
+ /// The Accept header.
+ /// Optional request parameters to apply custom headers.
+ /// The HTTP response.
+ public async Task CreateResourceRawAsync(string url, IResource artifact, string mediaType,
+ string acceptType, OslcRequestParams? requestParams)
+ {
+ var effectiveParams = DefaultRequestParams.Merge(requestParams);
+ var oslcVersion = effectiveParams.OslcCoreVersion ?? OslcRequestParams.DefaultOslcCoreVersion;
+
_client.DefaultRequestHeaders.Accept.Clear();
foreach (var acceptSingle in acceptType.Split(','))
{
_client.DefaultRequestHeaders.Accept.Add(MediaTypeWithQualityHeaderValue.Parse(acceptSingle));
}
- //_client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue(acceptType));
_client.DefaultRequestHeaders.Remove(OSLCConstants.OSLC_CORE_VERSION);
- _client.DefaultRequestHeaders.Add(OSLCConstants.OSLC_CORE_VERSION, "2.0");
+ _client.DefaultRequestHeaders.Add(OSLCConstants.OSLC_CORE_VERSION, oslcVersion);
+
+ // Apply custom headers from request parameters
+ if (effectiveParams.CustomHeaders is not null)
+ {
+ foreach (var header in effectiveParams.CustomHeaders)
+ {
+ _client.DefaultRequestHeaders.Remove(header.Key);
+ _client.DefaultRequestHeaders.Add(header.Key, header.Value);
+ }
+ }
var mediaTypeValue = new MediaTypeHeaderValue(mediaType);
var formatter =
diff --git a/OSLC4Net_SDK/OSLC4Net.Client/OslcRequestParams.cs b/OSLC4Net_SDK/OSLC4Net.Client/OslcRequestParams.cs
new file mode 100644
index 00000000..e7ea0585
--- /dev/null
+++ b/OSLC4Net_SDK/OSLC4Net.Client/OslcRequestParams.cs
@@ -0,0 +1,205 @@
+/*******************************************************************************
+ * Copyright (c) 2025 Andrii Berezovskyi and OSLC4Net contributors.
+ *
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * and Eclipse Distribution License v. 1.0 which accompanies this distribution.
+ *
+ * The Eclipse Public License is available at http://www.eclipse.org/legal/epl-v10.html
+ * and the Eclipse Distribution License is available at
+ * http://www.eclipse.org/org/documents/edl-v10.php.
+ *******************************************************************************/
+
+using OSLC4Net.Client.Oslc;
+
+namespace OSLC4Net.Client;
+
+///
+/// Request parameters for OSLC client operations. These can be pre-set in the library,
+/// configured in the OslcClient constructor, or overridden on a per-request basis.
+///
+///
+/// This class allows users to customize:
+///
+/// - Accept header for content negotiation (which media types to request)
+/// - Content-Type header for POST/PUT operations
+/// - Custom headers for specific requests
+/// - OSLC Core version header
+///
+///
+public sealed class OslcRequestParams
+{
+ ///
+ /// Default Accept header value for OSLC requests.
+ /// Prefers Turtle, then RDF/XML, then N-Triples, then N3.
+ ///
+ public const string DefaultAcceptHeader =
+ "text/turtle;q=1.0, application/rdf+xml;q=0.9, application/n-triples;q=0.8, text/n3;q=0.7";
+
+ ///
+ /// Default Content-Type for POST/PUT operations.
+ ///
+ public const string DefaultContentType = OSLCConstants.CT_RDF;
+
+ ///
+ /// Default OSLC Core version.
+ ///
+ public const string DefaultOslcCoreVersion = "2.0";
+
+ ///
+ /// Accept header for content negotiation.
+ ///
+ public string? AcceptHeader { get; init; }
+
+ ///
+ /// Content-Type header for POST/PUT operations.
+ ///
+ public string? ContentType { get; init; }
+
+ ///
+ /// OSLC Core version header value.
+ ///
+ public string? OslcCoreVersion { get; init; }
+
+ ///
+ /// Additional custom headers to include in requests.
+ ///
+ public IDictionary? CustomHeaders { get; init; }
+
+ ///
+ /// Creates default request parameters.
+ ///
+ public static OslcRequestParams Default => new()
+ {
+ AcceptHeader = DefaultAcceptHeader,
+ ContentType = DefaultContentType,
+ OslcCoreVersion = DefaultOslcCoreVersion
+ };
+
+ ///
+ /// Creates request parameters for RDF/XML only requests.
+ /// Useful for root services discovery or legacy servers.
+ ///
+ public static OslcRequestParams RdfXmlOnly => new()
+ {
+ AcceptHeader = OSLCConstants.CT_RDF,
+ ContentType = OSLCConstants.CT_RDF,
+ OslcCoreVersion = DefaultOslcCoreVersion
+ };
+
+ ///
+ /// Creates request parameters for Turtle only requests.
+ ///
+ public static OslcRequestParams TurtleOnly => new()
+ {
+ AcceptHeader = "text/turtle",
+ ContentType = "text/turtle",
+ OslcCoreVersion = DefaultOslcCoreVersion
+ };
+
+ ///
+ /// Merges these parameters with overrides. Override values take precedence.
+ ///
+ /// Override parameters (can be null).
+ /// A new OslcRequestParams with merged values.
+ public OslcRequestParams Merge(OslcRequestParams? overrides)
+ {
+ if (overrides is null)
+ {
+ return this;
+ }
+
+ var mergedHeaders = new Dictionary();
+
+ if (CustomHeaders is not null)
+ {
+ foreach (var kvp in CustomHeaders)
+ {
+ mergedHeaders[kvp.Key] = kvp.Value;
+ }
+ }
+
+ if (overrides.CustomHeaders is not null)
+ {
+ foreach (var kvp in overrides.CustomHeaders)
+ {
+ mergedHeaders[kvp.Key] = kvp.Value;
+ }
+ }
+
+ return new OslcRequestParams
+ {
+ AcceptHeader = overrides.AcceptHeader ?? AcceptHeader,
+ ContentType = overrides.ContentType ?? ContentType,
+ OslcCoreVersion = overrides.OslcCoreVersion ?? OslcCoreVersion,
+ CustomHeaders = mergedHeaders.Count > 0 ? mergedHeaders : null
+ };
+ }
+
+ ///
+ /// Creates a builder for fluent construction of request parameters.
+ ///
+ public static OslcRequestParamsBuilder Builder() => new();
+}
+
+///
+/// Builder for constructing OslcRequestParams in a fluent manner.
+///
+public sealed class OslcRequestParamsBuilder
+{
+ private string? _acceptHeader;
+ private string? _contentType;
+ private string? _oslcCoreVersion;
+ private Dictionary? _customHeaders;
+
+ ///
+ /// Sets the Accept header.
+ ///
+ public OslcRequestParamsBuilder WithAccept(string accept)
+ {
+ _acceptHeader = accept;
+ return this;
+ }
+
+ ///
+ /// Sets the Content-Type header.
+ ///
+ public OslcRequestParamsBuilder WithContentType(string contentType)
+ {
+ _contentType = contentType;
+ return this;
+ }
+
+ ///
+ /// Sets the OSLC Core version.
+ ///
+ public OslcRequestParamsBuilder WithOslcCoreVersion(string version)
+ {
+ _oslcCoreVersion = version;
+ return this;
+ }
+
+ ///
+ /// Adds a custom header.
+ ///
+ public OslcRequestParamsBuilder WithHeader(string name, string value)
+ {
+ _customHeaders ??= [];
+ _customHeaders[name] = value;
+ return this;
+ }
+
+ ///
+ /// Builds the OslcRequestParams.
+ ///
+ public OslcRequestParams Build()
+ {
+ return new OslcRequestParams
+ {
+ AcceptHeader = _acceptHeader,
+ ContentType = _contentType,
+ OslcCoreVersion = _oslcCoreVersion,
+ CustomHeaders = _customHeaders
+ };
+ }
+}
diff --git a/OSLC4Net_SDK/Tests/OSLC4Net.Client.Tests/OslcClientTests.cs b/OSLC4Net_SDK/Tests/OSLC4Net.Client.Tests/OslcClientTests.cs
new file mode 100644
index 00000000..01cd88c0
--- /dev/null
+++ b/OSLC4Net_SDK/Tests/OSLC4Net.Client.Tests/OslcClientTests.cs
@@ -0,0 +1,269 @@
+/*******************************************************************************
+ * Copyright (c) 2025 Andrii Berezovskyi and OSLC4Net contributors.
+ *
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * and Eclipse Distribution License v. 1.0 which accompanies this distribution.
+ *
+ * The Eclipse Public License is available at http://www.eclipse.org/legal/epl-v10.html
+ * and the Eclipse Distribution License is available at
+ * http://www.eclipse.org/org/documents/edl-v10.php.
+ *******************************************************************************/
+
+using System.Net;
+using System.Text;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Hosting;
+using Microsoft.Extensions.Logging;
+using OSLC4Net.Client.Oslc;
+using TUnit.Core;
+
+namespace OSLC4Net.Client.Tests;
+
+public class OslcClientTests
+{
+ private IHost AppHost { get; }
+ private ILoggerFactory LoggerFactory { get; }
+
+ public OslcClientTests()
+ {
+ AppHost = Host.CreateDefaultBuilder()
+ .ConfigureLogging(builder =>
+ {
+ builder.AddConsole();
+ })
+ .Build();
+ LoggerFactory = AppHost.Services.GetRequiredService();
+ }
+
+ [Test]
+ public async Task OslcClient_DefaultConstructor_HasDefaultRequestParams()
+ {
+ var client = new OslcClient(LoggerFactory.CreateLogger());
+
+ await Assert.That(client.DefaultRequestParams).IsNotNull();
+ await Assert.That(client.DefaultRequestParams.AcceptHeader).IsEqualTo(OslcRequestParams.DefaultAcceptHeader);
+ await Assert.That(client.DefaultRequestParams.ContentType).IsEqualTo(OslcRequestParams.DefaultContentType);
+
+ client.Dispose();
+ }
+
+ [Test]
+ public async Task OslcClient_WithCustomRequestParams_UsesProvidedParams()
+ {
+ var customParams = new OslcRequestParams
+ {
+ AcceptHeader = "custom/accept",
+ ContentType = "custom/content",
+ OslcCoreVersion = "3.0"
+ };
+
+ var client = new OslcClient(LoggerFactory.CreateLogger(), customParams);
+
+ await Assert.That(client.DefaultRequestParams.AcceptHeader).IsEqualTo("custom/accept");
+ await Assert.That(client.DefaultRequestParams.ContentType).IsEqualTo("custom/content");
+ await Assert.That(client.DefaultRequestParams.OslcCoreVersion).IsEqualTo("3.0");
+
+ client.Dispose();
+ }
+
+ [Test]
+ public async Task OslcClient_ForBasicAuth_WithCustomRequestParams_UsesProvidedParams()
+ {
+ var customParams = new OslcRequestParams
+ {
+ AcceptHeader = "application/rdf+xml"
+ };
+
+ var client = OslcClient.ForBasicAuth(
+ "testuser",
+ "testpass",
+ LoggerFactory.CreateLogger(),
+ null,
+ customParams
+ );
+
+ await Assert.That(client.DefaultRequestParams.AcceptHeader).IsEqualTo("application/rdf+xml");
+
+ client.Dispose();
+ }
+
+ [Test]
+ public async Task EnableGraphAccumulation_ReturnsGraph()
+ {
+ var client = new OslcClient(LoggerFactory.CreateLogger());
+
+ var graph = client.EnableGraphAccumulation();
+
+ await Assert.That(graph).IsNotNull();
+ await Assert.That(client.AccumulatingGraph).IsNotNull();
+ await Assert.That(client.AccumulatingGraph).IsEqualTo(graph);
+
+ client.Dispose();
+ }
+
+ [Test]
+ public async Task EnableGraphAccumulation_CalledTwice_ReturnsSameGraph()
+ {
+ var client = new OslcClient(LoggerFactory.CreateLogger());
+
+ var graph1 = client.EnableGraphAccumulation();
+ var graph2 = client.EnableGraphAccumulation();
+
+ await Assert.That(graph1).IsEqualTo(graph2);
+
+ client.Dispose();
+ }
+
+ [Test]
+ public async Task DisableGraphAccumulation_ReturnsAccumulatedGraph()
+ {
+ var client = new OslcClient(LoggerFactory.CreateLogger());
+
+ var enabledGraph = client.EnableGraphAccumulation();
+ var disabledGraph = client.DisableGraphAccumulation();
+
+ await Assert.That(disabledGraph).IsEqualTo(enabledGraph);
+ await Assert.That(client.AccumulatingGraph).IsNull();
+
+ client.Dispose();
+ }
+
+ [Test]
+ public async Task DisableGraphAccumulation_WhenNotEnabled_ReturnsNull()
+ {
+ var client = new OslcClient(LoggerFactory.CreateLogger());
+
+ var graph = client.DisableGraphAccumulation();
+
+ await Assert.That(graph).IsNull();
+
+ client.Dispose();
+ }
+
+ [Test]
+ public async Task ClearAccumulatingGraph_ClearsGraphContent()
+ {
+ var client = new OslcClient(LoggerFactory.CreateLogger());
+
+ var graph = client.EnableGraphAccumulation();
+ // Add a triple to the graph
+ var subj = graph.CreateUriNode(new Uri("http://example.org/subject"));
+ var pred = graph.CreateUriNode(new Uri("http://example.org/predicate"));
+ var obj = graph.CreateUriNode(new Uri("http://example.org/object"));
+ graph.Assert(new VDS.RDF.Triple(subj, pred, obj));
+
+ await Assert.That(graph.Triples.Count).IsGreaterThan(0);
+
+ client.ClearAccumulatingGraph();
+
+ await Assert.That(graph.Triples.Count).IsEqualTo(0);
+ await Assert.That(client.AccumulatingGraph).IsNotNull(); // Still enabled
+
+ client.Dispose();
+ }
+
+ [Test]
+ public async Task GetResourceRawAsync_AppliesCustomHeaders()
+ {
+ string? capturedAcceptHeader = null;
+ string? capturedOslcVersionHeader = null;
+ string? capturedCustomHeader = null;
+
+ var handler = new FakeHttpMessageHandler((req, ct) =>
+ {
+ // Capture headers from the request
+ if (req.Headers.TryGetValues("Accept", out var acceptValues))
+ {
+ capturedAcceptHeader = string.Join(", ", acceptValues);
+ }
+ if (req.Headers.TryGetValues("OSLC-Core-Version", out var oslcValues))
+ {
+ capturedOslcVersionHeader = string.Join(", ", oslcValues);
+ }
+ if (req.Headers.TryGetValues("X-Custom-Header", out var customValues))
+ {
+ capturedCustomHeader = string.Join(", ", customValues);
+ }
+
+ return Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK)
+ {
+ Content = new StringContent("",
+ Encoding.UTF8, "application/rdf+xml"),
+ RequestMessage = req
+ });
+ });
+
+ var httpClient = new HttpClient(handler);
+ var customParams = new OslcRequestParams
+ {
+ AcceptHeader = "application/rdf+xml",
+ OslcCoreVersion = "2.0",
+ CustomHeaders = new Dictionary
+ {
+ { "X-Custom-Header", "CustomValue" }
+ }
+ };
+
+ var client = new OslcClient(httpClient, LoggerFactory.CreateLogger(), customParams);
+
+ await client.GetResourceRawAsync("http://example.com/resource");
+
+ await Assert.That(capturedAcceptHeader).Contains("application/rdf+xml");
+ await Assert.That(capturedOslcVersionHeader).IsEqualTo("2.0");
+ await Assert.That(capturedCustomHeader).IsEqualTo("CustomValue");
+ }
+
+ [Test]
+ public async Task GetResourceRawAsync_WithPerRequestOverride_UsesOverride()
+ {
+ string? capturedAcceptHeader = null;
+
+ var handler = new FakeHttpMessageHandler((req, ct) =>
+ {
+ if (req.Headers.TryGetValues("Accept", out var acceptValues))
+ {
+ capturedAcceptHeader = string.Join(", ", acceptValues);
+ }
+
+ return Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK)
+ {
+ Content = new StringContent("",
+ Encoding.UTF8, "application/rdf+xml"),
+ RequestMessage = req
+ });
+ });
+
+ var httpClient = new HttpClient(handler);
+ var defaultParams = new OslcRequestParams
+ {
+ AcceptHeader = "application/rdf+xml"
+ };
+
+ var client = new OslcClient(httpClient, LoggerFactory.CreateLogger(), defaultParams);
+
+ var overrideParams = new OslcRequestParams
+ {
+ AcceptHeader = "text/turtle"
+ };
+
+ await client.GetResourceRawAsync("http://example.com/resource", null, overrideParams);
+
+ await Assert.That(capturedAcceptHeader).Contains("text/turtle");
+ }
+
+ [Test]
+ public async Task HttpClient_Constructor_AcceptsPreConfiguredClient()
+ {
+ using var httpClient = new HttpClient();
+ httpClient.DefaultRequestHeaders.Add("X-Pre-Configured", "true");
+
+ // When using an externally-provided HttpClient, the OslcClient does NOT own it,
+ // so we must ensure the HttpClient is not disposed when the OslcClient is disposed.
+ // In this case, we manage the HttpClient with using and don't dispose the OslcClient.
+ var client = new OslcClient(httpClient, LoggerFactory.CreateLogger());
+
+ await Assert.That(client.GetHttpClient()).IsEqualTo(httpClient);
+ await Assert.That(client.GetHttpClient().DefaultRequestHeaders.Contains("X-Pre-Configured")).IsTrue();
+ }
+}
diff --git a/OSLC4Net_SDK/Tests/OSLC4Net.Client.Tests/OslcRequestParamsTests.cs b/OSLC4Net_SDK/Tests/OSLC4Net.Client.Tests/OslcRequestParamsTests.cs
new file mode 100644
index 00000000..dbab5163
--- /dev/null
+++ b/OSLC4Net_SDK/Tests/OSLC4Net.Client.Tests/OslcRequestParamsTests.cs
@@ -0,0 +1,154 @@
+/*******************************************************************************
+ * Copyright (c) 2025 Andrii Berezovskyi and OSLC4Net contributors.
+ *
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * and Eclipse Distribution License v. 1.0 which accompanies this distribution.
+ *
+ * The Eclipse Public License is available at http://www.eclipse.org/legal/epl-v10.html
+ * and the Eclipse Distribution License is available at
+ * http://www.eclipse.org/org/documents/edl-v10.php.
+ *******************************************************************************/
+
+using TUnit.Core;
+
+namespace OSLC4Net.Client.Tests;
+
+public class OslcRequestParamsTests
+{
+ [Test]
+ public async Task Default_HasExpectedValues()
+ {
+ var defaultParams = OslcRequestParams.Default;
+
+ await Assert.That(defaultParams.AcceptHeader).IsEqualTo(OslcRequestParams.DefaultAcceptHeader);
+ await Assert.That(defaultParams.ContentType).IsEqualTo(OslcRequestParams.DefaultContentType);
+ await Assert.That(defaultParams.OslcCoreVersion).IsEqualTo("2.0");
+ await Assert.That(defaultParams.CustomHeaders).IsNull();
+ }
+
+ [Test]
+ public async Task RdfXmlOnly_HasRdfXmlValues()
+ {
+ var rdfXmlParams = OslcRequestParams.RdfXmlOnly;
+
+ await Assert.That(rdfXmlParams.AcceptHeader).IsEqualTo("application/rdf+xml");
+ await Assert.That(rdfXmlParams.ContentType).IsEqualTo("application/rdf+xml");
+ await Assert.That(rdfXmlParams.OslcCoreVersion).IsEqualTo("2.0");
+ }
+
+ [Test]
+ public async Task TurtleOnly_HasTurtleValues()
+ {
+ var turtleParams = OslcRequestParams.TurtleOnly;
+
+ await Assert.That(turtleParams.AcceptHeader).IsEqualTo("text/turtle");
+ await Assert.That(turtleParams.ContentType).IsEqualTo("text/turtle");
+ await Assert.That(turtleParams.OslcCoreVersion).IsEqualTo("2.0");
+ }
+
+ [Test]
+ public async Task Merge_WithNullOverride_ReturnsSelf()
+ {
+ var original = OslcRequestParams.Default;
+
+ var result = original.Merge(null);
+
+ await Assert.That(result).IsEqualTo(original);
+ }
+
+ [Test]
+ public async Task Merge_WithOverride_UsesOverrideValues()
+ {
+ var original = OslcRequestParams.Default;
+ var overrides = new OslcRequestParams
+ {
+ AcceptHeader = "custom/accept",
+ ContentType = null // Should keep original
+ };
+
+ var result = original.Merge(overrides);
+
+ await Assert.That(result.AcceptHeader).IsEqualTo("custom/accept");
+ await Assert.That(result.ContentType).IsEqualTo(original.ContentType);
+ await Assert.That(result.OslcCoreVersion).IsEqualTo(original.OslcCoreVersion);
+ }
+
+ [Test]
+ public async Task Merge_CombinesCustomHeaders()
+ {
+ var original = new OslcRequestParams
+ {
+ AcceptHeader = "original/accept",
+ CustomHeaders = new Dictionary
+ {
+ { "Header1", "Value1" },
+ { "Header2", "Value2" }
+ }
+ };
+ var overrides = new OslcRequestParams
+ {
+ CustomHeaders = new Dictionary
+ {
+ { "Header2", "OverriddenValue2" },
+ { "Header3", "Value3" }
+ }
+ };
+
+ var result = original.Merge(overrides);
+
+ await Assert.That(result.CustomHeaders).IsNotNull();
+ await Assert.That(result.CustomHeaders!.Count).IsEqualTo(3);
+ await Assert.That(result.CustomHeaders["Header1"]).IsEqualTo("Value1");
+ await Assert.That(result.CustomHeaders["Header2"]).IsEqualTo("OverriddenValue2");
+ await Assert.That(result.CustomHeaders["Header3"]).IsEqualTo("Value3");
+ }
+
+ [Test]
+ public async Task Builder_CreatesParamsCorrectly()
+ {
+ var params1 = OslcRequestParams.Builder()
+ .WithAccept("custom/accept")
+ .WithContentType("custom/content")
+ .WithOslcCoreVersion("3.0")
+ .WithHeader("X-Custom", "CustomValue")
+ .Build();
+
+ await Assert.That(params1.AcceptHeader).IsEqualTo("custom/accept");
+ await Assert.That(params1.ContentType).IsEqualTo("custom/content");
+ await Assert.That(params1.OslcCoreVersion).IsEqualTo("3.0");
+ await Assert.That(params1.CustomHeaders).IsNotNull();
+ await Assert.That(params1.CustomHeaders!["X-Custom"]).IsEqualTo("CustomValue");
+ }
+
+ [Test]
+ public async Task Builder_WithMultipleHeaders_AddsAll()
+ {
+ var params1 = OslcRequestParams.Builder()
+ .WithHeader("Header1", "Value1")
+ .WithHeader("Header2", "Value2")
+ .Build();
+
+ await Assert.That(params1.CustomHeaders).IsNotNull();
+ await Assert.That(params1.CustomHeaders!.Count).IsEqualTo(2);
+ await Assert.That(params1.CustomHeaders["Header1"]).IsEqualTo("Value1");
+ await Assert.That(params1.CustomHeaders["Header2"]).IsEqualTo("Value2");
+ }
+
+ [Test]
+ public async Task DefaultContentType_IsRdfXml()
+ {
+ await Assert.That(OslcRequestParams.DefaultContentType).IsEqualTo("application/rdf+xml");
+ }
+
+ [Test]
+ public async Task DefaultAcceptHeader_IncludesMultipleFormats()
+ {
+ var acceptHeader = OslcRequestParams.DefaultAcceptHeader;
+
+ await Assert.That(acceptHeader).Contains("text/turtle");
+ await Assert.That(acceptHeader).Contains("application/rdf+xml");
+ await Assert.That(acceptHeader).Contains("application/n-triples");
+ await Assert.That(acceptHeader).Contains("text/n3");
+ }
+}