diff --git a/src/PinguApps.Appwrite.Client/ServiceCollectionExtensions.cs b/src/PinguApps.Appwrite.Client/ServiceCollectionExtensions.cs index d3b7bb89..38f2ceaf 100644 --- a/src/PinguApps.Appwrite.Client/ServiceCollectionExtensions.cs +++ b/src/PinguApps.Appwrite.Client/ServiceCollectionExtensions.cs @@ -5,7 +5,6 @@ using System.Text.Json; using System.Text.Json.Serialization; using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Http; using Microsoft.Extensions.Logging; using PinguApps.Appwrite.Client.Clients; using PinguApps.Appwrite.Client.Handlers; @@ -19,7 +18,7 @@ namespace PinguApps.Appwrite.Client; /// -/// Provides extenions to IServiceCollection, to enable adding the SDK to your DI container +/// Provides extensions to IServiceCollection, to enable adding the SDK to your DI container /// public static class ServiceCollectionExtensions { @@ -38,52 +37,22 @@ public static IServiceCollection AddAppwriteClient(this IServiceCollection servi var policyOptions = new ResiliencePolicyOptions(); configureResiliencePolicy?.Invoke(policyOptions); - var customRefitSettings = AddSerializationConfigToRefitSettings(refitSettings); - services.AddKeyedSingleton("Client", new Config(endpoint, projectId)); + + services.AddSingleton>(sp => + CreateResiliencePolicy(sp.GetRequiredService>(), policyOptions)); + services.AddTransient(); services.AddTransient(); - services.AddRefitClient(customRefitSettings) - .ConfigureHttpClient(x => ConfigureHttpClient(x, endpoint)) - .AddHttpMessageHandler() - .AddHttpMessageHandler(() => new PolicyHttpMessageHandler(GetRetryPolicy(services, policyOptions))) - .AddHttpMessageHandler(() => new PolicyHttpMessageHandler(GetCircuitBreakerPolicy(services, policyOptions))) - .AddHttpMessageHandler(); - - services.AddRefitClient(customRefitSettings) - .ConfigureHttpClient(x => ConfigureHttpClient(x, endpoint)) - .AddHttpMessageHandler() - .AddHttpMessageHandler(() => new PolicyHttpMessageHandler(GetRetryPolicy(services, policyOptions))) - .AddHttpMessageHandler(() => new PolicyHttpMessageHandler(GetCircuitBreakerPolicy(services, policyOptions))) - .AddHttpMessageHandler(); + var customRefitSettings = AddSerializationConfigToRefitSettings(refitSettings); - services.AddRefitClient(customRefitSettings) - .ConfigureHttpClient(x => ConfigureHttpClient(x, endpoint)) - .AddHttpMessageHandler() - .AddHttpMessageHandler(() => new PolicyHttpMessageHandler(GetRetryPolicy(services, policyOptions))) - .AddHttpMessageHandler(() => new PolicyHttpMessageHandler(GetCircuitBreakerPolicy(services, policyOptions))) - .AddHttpMessageHandler(); + RegisterRefitClient(services, customRefitSettings, endpoint, true); + RegisterRefitClient(services, customRefitSettings, endpoint, true); + RegisterRefitClient(services, customRefitSettings, endpoint, true); - services.AddSingleton(sp => - { - var api = sp.GetRequiredService(); - var config = sp.GetRequiredKeyedService("Client"); - return new ClientAccountClient(api, config); - }); - services.AddSingleton(sp => - { - var api = sp.GetRequiredService(); - var config = sp.GetRequiredKeyedService("Client"); - return new ClientTeamsClient(api, config); - }); - services.AddSingleton(sp => - { - var api = sp.GetRequiredService(); - return new ClientDatabasesClient(api); - }); - services.AddSingleton(); - services.AddSingleton(x => new Lazy(() => x.GetRequiredService())); + // Register business logic clients + RegisterBusinessClients(services, true); return services; } @@ -103,80 +72,121 @@ public static IServiceCollection AddAppwriteClientForServer(this IServiceCollect var policyOptions = new ResiliencePolicyOptions(); configureResiliencePolicy?.Invoke(policyOptions); - var customRefitSettings = AddSerializationConfigToRefitSettings(refitSettings); - services.AddKeyedSingleton("Client", new Config(endpoint, projectId)); + + services.AddSingleton>(sp => + CreateResiliencePolicy(sp.GetRequiredService>(), policyOptions)); + services.AddTransient(); - services.AddRefitClient(customRefitSettings) - .ConfigureHttpClient(x => ConfigureHttpClient(x, endpoint)) - .AddHttpMessageHandler() - .AddHttpMessageHandler(() => new PolicyHttpMessageHandler(GetRetryPolicy(services, policyOptions))) - .AddHttpMessageHandler(() => new PolicyHttpMessageHandler(GetCircuitBreakerPolicy(services, policyOptions))) - .ConfigurePrimaryHttpMessageHandler(ConfigurePrimaryHttpMessageHandler); + var customRefitSettings = AddSerializationConfigToRefitSettings(refitSettings); - services.AddRefitClient(customRefitSettings) - .ConfigureHttpClient(x => ConfigureHttpClient(x, endpoint)) - .AddHttpMessageHandler() - .AddHttpMessageHandler(() => new PolicyHttpMessageHandler(GetRetryPolicy(services, policyOptions))) - .AddHttpMessageHandler(() => new PolicyHttpMessageHandler(GetCircuitBreakerPolicy(services, policyOptions))) - .ConfigurePrimaryHttpMessageHandler(ConfigurePrimaryHttpMessageHandler); + // Register API clients + RegisterRefitClient(services, customRefitSettings, endpoint, false); + RegisterRefitClient(services, customRefitSettings, endpoint, false); + RegisterRefitClient(services, customRefitSettings, endpoint, false); + + // Register business logic clients + RegisterBusinessClients(services, false); - services.AddRefitClient(customRefitSettings) - .ConfigureHttpClient(x => ConfigureHttpClient(x, endpoint)) + return services; + } + + private static void RegisterRefitClient(IServiceCollection services, RefitSettings refitSettings, + string endpoint, bool includeSessionHandler) + where T : class + { + var builder = services.AddRefitClient(refitSettings) + .ConfigureHttpClient(client => + { + client.BaseAddress = new Uri(endpoint); + client.DefaultRequestHeaders.UserAgent.ParseAdd(BuildUserAgent()); + client.Timeout = TimeSpan.FromSeconds(30); + }) .AddHttpMessageHandler() - .AddHttpMessageHandler(() => new PolicyHttpMessageHandler(GetRetryPolicy(services, policyOptions))) - .AddHttpMessageHandler(() => new PolicyHttpMessageHandler(GetCircuitBreakerPolicy(services, policyOptions))) - .ConfigurePrimaryHttpMessageHandler(ConfigurePrimaryHttpMessageHandler); + .AddPolicyHandler((sp, _) => sp.GetRequiredService>()); - services.AddScoped(sp => + if (includeSessionHandler) + { + builder.AddHttpMessageHandler(); + } + else + { + builder.ConfigurePrimaryHttpMessageHandler(ConfigurePrimaryHttpMessageHandler); + } + } + + private static void RegisterBusinessClients(IServiceCollection services, bool asSingleton) + { + if (asSingleton) + { + RegisterSingletonClients(services); + } + else + { + RegisterScopedClients(services); + } + } + + private static void RegisterSingletonClients(IServiceCollection services) + { + services.AddSingleton(sp => { var api = sp.GetRequiredService(); var config = sp.GetRequiredKeyedService("Client"); return new ClientAccountClient(api, config); }); - services.AddScoped(sp => + + services.AddSingleton(sp => { var api = sp.GetRequiredService(); var config = sp.GetRequiredKeyedService("Client"); return new ClientTeamsClient(api, config); }); - services.AddScoped(sp => + + services.AddSingleton(sp => { var api = sp.GetRequiredService(); return new ClientDatabasesClient(api); }); - services.AddScoped(); - return services; + services.AddSingleton(); + services.AddSingleton(x => new Lazy(() => + x.GetRequiredService())); } - [ExcludeFromCodeCoverage] - private static void ConfigurePrimaryHttpMessageHandler(HttpMessageHandler messageHandler, IServiceProvider serviceProvider) + private static void RegisterScopedClients(IServiceCollection services) { - if (messageHandler is HttpClientHandler clientHandler) + services.AddScoped(sp => { - clientHandler.UseCookies = false; - } - } + var api = sp.GetRequiredService(); + var config = sp.GetRequiredKeyedService("Client"); + return new ClientAccountClient(api, config); + }); - private static void ConfigureHttpClient(HttpClient client, string endpoint) - { - client.BaseAddress = new Uri(endpoint); - client.DefaultRequestHeaders.UserAgent.ParseAdd(BuildUserAgent()); + services.AddScoped(sp => + { + var api = sp.GetRequiredService(); + var config = sp.GetRequiredKeyedService("Client"); + return new ClientTeamsClient(api, config); + }); + + services.AddScoped(sp => + { + var api = sp.GetRequiredService(); + return new ClientDatabasesClient(api); + }); + + services.AddScoped(); } - [ExcludeFromCodeCoverage] - private static IAsyncPolicy GetRetryPolicy(IServiceCollection services, ResiliencePolicyOptions options) + private static IAsyncPolicy CreateResiliencePolicy(ILogger logger, ResiliencePolicyOptions options) { if (options.DisableResilience) { return Policy.NoOpAsync(); } - var serviceProvider = services.BuildServiceProvider(); - var logger = serviceProvider.GetRequiredService>(); - return HttpPolicyExtensions .HandleTransientHttpError() .Or() @@ -186,45 +196,31 @@ private static IAsyncPolicy GetRetryPolicy(IServiceColle onRetry: (exception, timeSpan, retryCount, context) => { logger.LogWarning(exception.Exception, - "Retry {RetryCount} for {Service} after {Seconds} seconds due to: {Message}", + "Retry {RetryCount} after {Seconds} seconds due to: {Message}", retryCount, - typeof(T).Name, timeSpan.TotalSeconds, exception.Exception.Message); - }); + }) + .WrapAsync(HttpPolicyExtensions + .HandleTransientHttpError() + .CircuitBreakerAsync(options.CircuitBreakerThreshold, TimeSpan.FromSeconds(options.CircuitBreakerDurationSeconds), + (exception, duration) => + { + logger.LogError(exception.Exception, + "Circuit breaker opened for {Seconds} seconds due to: {Message}", + duration.TotalSeconds, + exception.Exception.Message); + }, + () => logger.LogInformation("Circuit breaker reset"))); } [ExcludeFromCodeCoverage] - private static IAsyncPolicy GetCircuitBreakerPolicy(IServiceCollection services, ResiliencePolicyOptions options) + private static void ConfigurePrimaryHttpMessageHandler(HttpMessageHandler messageHandler, IServiceProvider serviceProvider) { - if (options.DisableResilience) + if (messageHandler is HttpClientHandler clientHandler) { - return Policy.NoOpAsync(); + clientHandler.UseCookies = false; } - - var serviceProvider = services.BuildServiceProvider(); - var logger = serviceProvider.GetRequiredService>(); - - return HttpPolicyExtensions - .HandleTransientHttpError() - .CircuitBreakerAsync( - handledEventsAllowedBeforeBreaking: options.CircuitBreakerThreshold, - durationOfBreak: TimeSpan.FromSeconds(options.CircuitBreakerDurationSeconds), - onBreak: (exception, duration) => - { - logger.LogError( - exception.Exception, - "Circuit breaker for {Service} opened for {Seconds} seconds due to: {Message}", - typeof(T).Name, - duration.TotalSeconds, - exception.Exception.Message); - }, - onReset: () => - { - logger.LogInformation( - "Circuit breaker for {Service} reset", - typeof(T).Name); - }); } private static RefitSettings AddSerializationConfigToRefitSettings(RefitSettings? refitSettings) @@ -256,4 +252,7 @@ private static string BuildUserAgent() return $"PinguAppsAppwriteDotNetClientSdk/{Constants.Version} (.NET/{dotnetVersion}; {RuntimeInformation.OSDescription.Trim()})"; } + + // Small helper class to avoid logger type conflicts + internal class ResiliencePolicy { } } diff --git a/src/PinguApps.Appwrite.Server/ServiceCollectionExtensions.cs b/src/PinguApps.Appwrite.Server/ServiceCollectionExtensions.cs index 84bd9fe8..906482a2 100644 --- a/src/PinguApps.Appwrite.Server/ServiceCollectionExtensions.cs +++ b/src/PinguApps.Appwrite.Server/ServiceCollectionExtensions.cs @@ -5,7 +5,6 @@ using System.Text.Json; using System.Text.Json.Serialization; using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Http; using Microsoft.Extensions.Logging; using PinguApps.Appwrite.Server.Clients; using PinguApps.Appwrite.Server.Handlers; @@ -19,7 +18,7 @@ namespace PinguApps.Appwrite.Server; /// -/// Provides extenions to IServiceCollection, to enable adding the SDK to your DI container +/// Provides extensions to IServiceCollection, to enable adding the SDK to your DI container /// public static class ServiceCollectionExtensions { @@ -39,93 +38,78 @@ public static IServiceCollection AddAppwriteServer(this IServiceCollection servi var policyOptions = new ResiliencePolicyOptions(); configureResiliencePolicy?.Invoke(policyOptions); + services.AddSingleton>(sp => + CreateResiliencePolicy(sp.GetRequiredService>(), policyOptions)); + var customRefitSettings = AddSerializationConfigToRefitSettings(refitSettings); services.AddKeyedSingleton("Server", new Config(endpoint, projectId, apiKey)); - services.AddTransient(); + services.AddScoped(); - services.AddRefitClient(customRefitSettings) - .ConfigureHttpClient(x => ConfigureHttpClient(x, endpoint)) - .AddHttpMessageHandler() - .AddHttpMessageHandler(() => new PolicyHttpMessageHandler(GetRetryPolicy(services, policyOptions))) - .AddHttpMessageHandler(() => new PolicyHttpMessageHandler(GetCircuitBreakerPolicy(services, policyOptions))) - .ConfigurePrimaryHttpMessageHandler(ConfigurePrimaryHttpMessageHandler); + RegisterRefitClient(services, customRefitSettings, endpoint); + RegisterRefitClient(services, customRefitSettings, endpoint); + RegisterRefitClient(services, customRefitSettings, endpoint); + RegisterRefitClient(services, customRefitSettings, endpoint); - services.AddRefitClient(customRefitSettings) - .ConfigureHttpClient(x => ConfigureHttpClient(x, endpoint)) - .AddHttpMessageHandler() - .AddHttpMessageHandler(() => new PolicyHttpMessageHandler(GetRetryPolicy(services, policyOptions))) - .AddHttpMessageHandler(() => new PolicyHttpMessageHandler(GetCircuitBreakerPolicy(services, policyOptions))) - .ConfigurePrimaryHttpMessageHandler(ConfigurePrimaryHttpMessageHandler); + RegisterBusinessClients(services); - services.AddRefitClient(customRefitSettings) - .ConfigureHttpClient(x => ConfigureHttpClient(x, endpoint)) - .AddHttpMessageHandler() - .AddHttpMessageHandler(() => new PolicyHttpMessageHandler(GetRetryPolicy(services, policyOptions))) - .AddHttpMessageHandler(() => new PolicyHttpMessageHandler(GetCircuitBreakerPolicy(services, policyOptions))) - .ConfigurePrimaryHttpMessageHandler(ConfigurePrimaryHttpMessageHandler); + return services; + } - services.AddRefitClient(customRefitSettings) - .ConfigureHttpClient(x => ConfigureHttpClient(x, endpoint)) + private static void RegisterRefitClient(IServiceCollection services, RefitSettings refitSettings, string endpoint) + where T : class + { + services.AddRefitClient(refitSettings) + .ConfigureHttpClient((sp, client) => + { + client.BaseAddress = new Uri(endpoint); + client.Timeout = TimeSpan.FromSeconds(30); + client.DefaultRequestHeaders.UserAgent.ParseAdd(BuildUserAgent()); + }) + .ConfigurePrimaryHttpMessageHandler(ConfigurePrimaryHttpMessageHandler) .AddHttpMessageHandler() - .AddHttpMessageHandler(() => new PolicyHttpMessageHandler(GetRetryPolicy(services, policyOptions))) - .AddHttpMessageHandler(() => new PolicyHttpMessageHandler(GetCircuitBreakerPolicy(services, policyOptions))) - .ConfigurePrimaryHttpMessageHandler(ConfigurePrimaryHttpMessageHandler); + .AddPolicyHandler((sp, _) => sp.GetRequiredService>()); + } + private static void RegisterBusinessClients(IServiceCollection services) + { services.AddSingleton(sp => { var api = sp.GetRequiredService(); var config = sp.GetRequiredKeyedService("Server"); return new ServerAccountClient(api, config); }); + services.AddSingleton(sp => { var api = sp.GetRequiredService(); var config = sp.GetRequiredKeyedService("Server"); return new ServerUsersClient(api, config); }); + services.AddSingleton(sp => { var api = sp.GetRequiredService(); var config = sp.GetRequiredKeyedService("Server"); return new ServerTeamsClient(api, config); }); + services.AddSingleton(sp => { var api = sp.GetRequiredService(); return new ServerDatabasesClient(api); }); - services.AddSingleton(); - - return services; - } - - [ExcludeFromCodeCoverage] - private static void ConfigurePrimaryHttpMessageHandler(HttpMessageHandler messageHandler, IServiceProvider serviceProvider) - { - if (messageHandler is HttpClientHandler clientHandler) - { - clientHandler.UseCookies = false; - } - } - private static void ConfigureHttpClient(HttpClient client, string endpoint) - { - client.BaseAddress = new Uri(endpoint); - client.DefaultRequestHeaders.UserAgent.ParseAdd(BuildUserAgent()); + services.AddSingleton(); } - [ExcludeFromCodeCoverage] - private static IAsyncPolicy GetRetryPolicy(IServiceCollection services, ResiliencePolicyOptions options) + private static IAsyncPolicy CreateResiliencePolicy(ILogger logger, ResiliencePolicyOptions options) { if (options.DisableResilience) { return Policy.NoOpAsync(); } - var serviceProvider = services.BuildServiceProvider(); - var logger = serviceProvider.GetRequiredService>(); - return HttpPolicyExtensions .HandleTransientHttpError() .Or() @@ -135,45 +119,31 @@ private static IAsyncPolicy GetRetryPolicy(IServiceColle onRetry: (exception, timeSpan, retryCount, context) => { logger.LogWarning(exception.Exception, - "Retry {RetryCount} for {Service} after {Seconds} seconds due to: {Message}", + "Retry {RetryCount} after {Seconds} seconds due to: {Message}", retryCount, - typeof(T).Name, timeSpan.TotalSeconds, exception.Exception.Message); - }); + }) + .WrapAsync(HttpPolicyExtensions + .HandleTransientHttpError() + .CircuitBreakerAsync(options.CircuitBreakerThreshold, TimeSpan.FromSeconds(options.CircuitBreakerDurationSeconds), + (exception, duration) => + { + logger.LogError(exception.Exception, + "Circuit breaker opened for {Seconds} seconds due to: {Message}", + duration.TotalSeconds, + exception.Exception.Message); + }, + () => logger.LogInformation("Circuit breaker reset"))); } [ExcludeFromCodeCoverage] - private static IAsyncPolicy GetCircuitBreakerPolicy(IServiceCollection services, ResiliencePolicyOptions options) + private static void ConfigurePrimaryHttpMessageHandler(HttpMessageHandler messageHandler, IServiceProvider serviceProvider) { - if (options.DisableResilience) + if (messageHandler is HttpClientHandler clientHandler) { - return Policy.NoOpAsync(); + clientHandler.UseCookies = false; } - - var serviceProvider = services.BuildServiceProvider(); - var logger = serviceProvider.GetRequiredService>(); - - return HttpPolicyExtensions - .HandleTransientHttpError() - .CircuitBreakerAsync( - handledEventsAllowedBeforeBreaking: options.CircuitBreakerThreshold, - durationOfBreak: TimeSpan.FromSeconds(options.CircuitBreakerDurationSeconds), - onBreak: (exception, duration) => - { - logger.LogError( - exception.Exception, - "Circuit breaker for {Service} opened for {Seconds} seconds due to: {Message}", - typeof(T).Name, - duration.TotalSeconds, - exception.Exception.Message); - }, - onReset: () => - { - logger.LogInformation( - "Circuit breaker for {Service} reset", - typeof(T).Name); - }); } private static RefitSettings AddSerializationConfigToRefitSettings(RefitSettings? refitSettings) @@ -205,4 +175,7 @@ private static string BuildUserAgent() return $"PinguAppsAppwriteDotNetServerSdk/{Constants.Version} (.NET/{dotnetVersion}; {RuntimeInformation.OSDescription.Trim()})"; } + + // Small helper class to avoid logger type conflicts + internal class ResiliencePolicy { } } diff --git a/src/PinguApps.Appwrite.Shared/Constants.cs b/src/PinguApps.Appwrite.Shared/Constants.cs index 0587666f..e5a378df 100644 --- a/src/PinguApps.Appwrite.Shared/Constants.cs +++ b/src/PinguApps.Appwrite.Shared/Constants.cs @@ -1,5 +1,5 @@ namespace PinguApps.Appwrite.Shared; public static class Constants { - public const string Version = "2.0.4"; + public const string Version = "2.0.5"; }