Created
October 30, 2025 18:15
-
-
Save mikedevita/d3a0afbcec8f176995a5e4b413281886 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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