using Castle.Core.Logging; using System.Net.Mime; using System.Web; namespace Pilz.Net.Api; public class ApiClient(string apiUrl) : IApiClient { protected readonly List clients = []; public event ApiRequestCancelEventHandler? OnApiRequestStart; public event ApiRequestEventHandler? OnApiRequestResponse; public event ApiRequestEventHandler? OnApiRequestCompleted; public HttpClient HttpClient { get; set; } = new(); public virtual string ApiUrl { get; } = apiUrl; public string? AuthKey { get; set; } public virtual IApiMessageSerializer Serializer { get; set; } = new DefaultApiMessageSerializer(); public ILogger Log { get; set; } = NullLogger.Instance; public T GetClient() where T : IApiSubClient { foreach (var c in clients) { if (c is T t) return t; } if (T.CreateClient(this) is T client) { clients.Add(client); return client; } throw new ApiException("Could not create an instance of this client."); } public virtual async Task SendRequest(string route, HttpMethod method, ApiMessage? message, ApiParameterCollection? @params, IApiMessageSerializer? serializer) { Log.InfoFormat("Send message to {0}", route); var context = new ApiRequestContext(route, method, message, @params, serializer, null); await Send(context); var response = new ApiResponse(context.HttpResponse); context.ApiResponse = response; return response; } public virtual async Task> SendRequest(string route, HttpMethod method, ApiMessage? message, ApiParameterCollection? @params, IApiMessageSerializer? serializer) where TResponse : ApiMessage { Log.InfoFormat("Send request to {0}", route); var context = new ApiRequestContext(route, method, message, @params, serializer, typeof(TResponse)); await Send(context); TResponse? result = null; if (context.HttpResponse != null && context.HttpResponse.IsSuccessStatusCode) { var mediaType = context.HttpResponse.Content.Headers.ContentType?.MediaType; if (!string.IsNullOrWhiteSpace(mediaType)) { if (mediaType == "application/json" && context.Serializer != null) result = context.Serializer.Deserialize(await context.HttpResponse.Content.ReadAsStringAsync(), typeof(TResponse)) as TResponse; else if (typeof(TResponse).IsAssignableTo(typeof(ApiRawByteMessage)) && mediaType == "application/octet-stream") result = (TResponse)(object)new ApiRawByteMessage(await context.HttpResponse.Content.ReadAsByteArrayAsync()); else if (typeof(TResponse).IsAssignableTo(typeof(ApiRawStreamMessage)) && mediaType == "application/octet-stream") result = (TResponse)(object)new ApiRawStreamMessage(await context.HttpResponse.Content.ReadAsStreamAsync()); } } var response = new ApiResponse(context.HttpResponse, result); context.ApiResponse = response; return response; } protected virtual async Task Send(ApiRequestContext context) { var url = ApiUrl + context.Route + BuildParameters(context.Params); HttpContent content; context.Serializer ??= Serializer; Log.DebugFormat("Api endpoint url is {0}", url); if (OnApiRequestStart != null) { var args = new ApiRequestCancelEventArgs(context); OnApiRequestStart.Invoke(this, args); if (args.Cancel) { context.Canceled = true; return; } } Log.Debug("Create content"); if (context.Message is ApiRawByteMessage messageRawBytes) { content = new ByteArrayContent(messageRawBytes.Data); content.Headers.ContentType = new("application/octet-stream"); content.Headers.ContentLength = messageRawBytes.Data.Length; } else if (context.Message is ApiRawStreamMessage messageRawStream) { content = new StreamContent(messageRawStream.Data); content.Headers.ContentType = new("application/octet-stream"); content.Headers.ContentLength = messageRawStream.Data.Length; } else if (context.Message is not null) content = new StringContent(context.Serializer.Serialize(context.Message)!, null, "application/json"); else content = new StringContent(string.Empty, null, "application/json"); Log.Debug("Build headers"); content.Headers.Add("API-AUTH-KEY", EncodeAuthKey()); Log.Debug("Sending request"); var httpmsg = new HttpRequestMessage(context.Method, url) { Content = content }; context.HttpResponse = await HttpClient.SendAsync(httpmsg); OnApiRequestResponse?.Invoke(this, new(context)); } protected virtual string BuildParameters(ApiParameterCollection? @params) { if (@params == null || @params.Count == 0) return string.Empty; return "?" + string.Join("&", @params.Select(kvp => { var key = kvp.Key; var value = HttpUtility.UrlEncode(ConvertParameter(kvp.Value)); return $"{key}={value}"; })); } protected virtual string? ConvertParameter(object? @param) { return (string?)Convert.ChangeType(@param, typeof(string)); } protected virtual string? EncodeAuthKey() { return AuthKey; } }