Skip to content

Instantly share code, notes, and snippets.

@mikedevita
Created October 30, 2025 18:15
Show Gist options
  • Select an option

  • Save mikedevita/d3a0afbcec8f176995a5e4b413281886 to your computer and use it in GitHub Desktop.

Select an option

Save mikedevita/d3a0afbcec8f176995a5e4b413281886 to your computer and use it in GitHub Desktop.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Simple.OData.Client;
using SapCi.OData.Models;
namespace SapCi.OData
{
/// <summary>
/// Read-only OData client wrapper for SAP Cloud Integration (CPI) using Simple.OData.Client.
/// Example:
/// var client = new SapCpiODataClient(new SapCpiODataClientOptions {
/// BaseUrl = new Uri("https://your-cpi-host/api/v1"),
/// Username = "user",
/// Password = "pass"
/// });
/// var artifacts = await client.GetIntegrationDesigntimeArtifactsAsync(q => q.Top(10));
/// var logs = await client.GetMessageProcessingLogsForArtifactAsync("iflow-id", q => q.Top(50));
/// </summary>
public sealed class SapCpiODataClient : ISapCpiODataClient
{
private readonly ODataClient _client;
private readonly SapCpiODataClientOptions _options;
public SapCpiODataClient(SapCpiODataClientOptions options)
{
if (options == null) throw new ArgumentNullException(nameof(options));
if (options.BaseUrl == null) throw new ArgumentNullException(nameof(options.BaseUrl));
if (string.IsNullOrEmpty(options.Username)) throw new ArgumentNullException(nameof(options.Username));
if (string.IsNullOrEmpty(options.Password)) throw new ArgumentNullException(nameof(options.Password));
_options = options;
var settings = new ODataClientSettings(options.BaseUrl)
{
IgnoreUnmappedProperties = true,
PayloadFormat = ODataPayloadFormat.Json,
BeforeRequest = request =>
{
// Basic auth header
var auth = Convert.ToBase64String(Encoding.UTF8.GetBytes(options.Username + ":" + options.Password));
request.Headers["Authorization"] = "Basic " + auth;
// Additional headers
if (options.DefaultRequestHeaders != null)
{
foreach (var kv in options.DefaultRequestHeaders)
{
request.Headers[kv.Key] = kv.Value;
}
}
},
OnTrace = msg => options.Trace?.Invoke(msg)
};
// Configure HTTP handler / proxy if provided
var handler = options.HttpMessageHandlerFactory?.Invoke() ?? BuildDefaultHandler(options.Proxy);
if (options.Timeout.HasValue)
{
settings.HttpClient = new HttpClient(handler)
{
Timeout = options.Timeout.Value
};
}
else
{
settings.HttpClient = new HttpClient(handler);
}
_client = new ODataClient(settings);
}
public Task<IEnumerable<IntegrationDesigntimeArtifact>> GetIntegrationDesigntimeArtifactsAsync(Func<IBoundClient<IntegrationDesigntimeArtifact>, IBoundClient<IntegrationDesigntimeArtifact>> builder = null)
{
return QueryAsync("IntegrationDesigntimeArtifacts", builder);
}
public Task<IEnumerable<MessageProcessingLog>> GetMessageProcessingLogsAsync(Func<IBoundClient<MessageProcessingLog>, IBoundClient<MessageProcessingLog>> builder = null)
{
return QueryAsync("MessageProcessingLogs", builder);
}
public Task<IEnumerable<MessageProcessingLog>> GetMessageProcessingLogsForArtifactAsync(string artifactId, Func<IBoundClient<MessageProcessingLog>, IBoundClient<MessageProcessingLog>> builder = null)
{
if (string.IsNullOrEmpty(artifactId)) throw new ArgumentNullException(nameof(artifactId));
return QueryAsync("MessageProcessingLogs", q =>
{
// Try common field names used in CPI tenants
var filtered = q.Filter(l => l.IntegrationArtifactId == artifactId);
if (builder != null)
{
filtered = builder(filtered);
}
return filtered;
});
}
public async Task<IEnumerable<T>> QueryAsync<T>(string resource, Func<IBoundClient<T>, IBoundClient<T>> builder = null)
{
if (string.IsNullOrEmpty(resource)) throw new ArgumentNullException(nameof(resource));
var query = _client.For<T>(resource);
if (builder != null)
{
query = builder(query);
}
return await ExecuteWithRetry(async () => await query.FindEntriesAsync().ConfigureAwait(false)).ConfigureAwait(false);
}
public async Task<T> GetByKeyAsync<T>(string resource, object key)
{
if (string.IsNullOrEmpty(resource)) throw new ArgumentNullException(nameof(resource));
if (key == null) throw new ArgumentNullException(nameof(key));
var query = _client.For<T>(resource).Key(key);
return await ExecuteWithRetry(async () => await query.FindEntryAsync().ConfigureAwait(false)).ConfigureAwait(false);
}
public async Task<IEnumerable<IDictionary<string, object>>> QueryRawAsync(string resource, Func<IBoundClient<IDictionary<string, object>>, IFluentClient> builder = null)
{
if (string.IsNullOrEmpty(resource)) throw new ArgumentNullException(nameof(resource));
IFluentClient fluent = _client.For(resource);
if (builder != null)
{
fluent = builder(_client.For<IDictionary<string, object>>(resource));
}
return await ExecuteWithRetry(async () => await fluent.FindEntriesAsync().ConfigureAwait(false)).ConfigureAwait(false);
}
private static HttpMessageHandler BuildDefaultHandler(IWebProxy proxy)
{
var handler = new HttpClientHandler
{
UseCookies = false,
Proxy = proxy,
UseProxy = proxy != null,
AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate
};
return handler;
}
private async Task<T> ExecuteWithRetry<T>(Func<Task<T>> action)
{
int attempts = 0;
var maxAttempts = Math.Max(1, _options.RetryCount);
var baseDelay = _options.RetryBaseDelay < TimeSpan.Zero ? TimeSpan.FromMilliseconds(300) : _options.RetryBaseDelay;
while (true)
{
try
{
return await action().ConfigureAwait(false);
}
catch (HttpRequestException) when (attempts < maxAttempts - 1)
{
attempts++;
await Task.Delay(ComputeDelay(baseDelay, attempts)).ConfigureAwait(false);
}
catch (WebException) when (attempts < maxAttempts - 1)
{
attempts++;
await Task.Delay(ComputeDelay(baseDelay, attempts)).ConfigureAwait(false);
}
}
}
private static TimeSpan ComputeDelay(TimeSpan baseDelay, int attempt)
{
var factor = (int)Math.Pow(2, attempt - 1);
var jitterMs = new Random().Next(0, 100);
return TimeSpan.FromMilliseconds(baseDelay.TotalMilliseconds * factor + jitterMs);
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment