This guide helps you migrate from older versions of Unsplasharp and adopt new features like comprehensive error handling, IHttpClientFactory integration, and modern async patterns.
- Version Compatibility
- Breaking Changes
- New Features Overview
- Migration Strategies
- Error Handling Migration
- IHttpClientFactory Migration
- Logging Integration
- Performance Improvements
- Best Practices Updates
| Unsplasharp Version | .NET Framework | .NET Core | .NET 5+ |
|---|---|---|---|
| 1.x | 4.6.1+ | 2.0+ | β |
| 2.x | 4.6.1+ | 2.0+ | 5.0+ |
| 3.x (Current) | 4.6.1+ | 2.0+ | 5.0+ |
- Backward Compatible: All existing methods continue to work
- New Methods: Exception-throwing variants added (e.g.,
GetPhotoAsync) - Enhanced Features: Logging, metrics, and error context added
- No Breaking Changes: Existing code will compile and run without modifications
Unsplasharp maintains full backward compatibility. Your existing code will continue to work exactly as before.
// This code from v1.x still works in v3.x
var client = new UnsplasharpClient("YOUR_APP_ID");
var photo = await client.GetRandomPhoto();
if (photo != null)
{
Console.WriteLine(photo.Urls.Regular);
}- New Exception Types: Specific exceptions for different error scenarios
- Rich Error Context: Detailed information about requests and responses
- Correlation IDs: For better debugging and monitoring
- Rate Limit Awareness: Automatic rate limit detection and handling
- Modern HTTP Management: Proper HttpClient lifecycle management
- Dependency Injection: Seamless integration with DI containers
- Connection Pooling: Better performance and resource utilization
- Configuration Centralization: Configure HTTP clients in one place
- Microsoft.Extensions.Logging: Integration with standard logging framework
- Detailed Insights: HTTP requests, retries, rate limits, and errors
- Correlation Tracking: Link related log entries with correlation IDs
- Performance Metrics: Request timing and success rates
- CancellationToken Support: All methods now support cancellation
- Better Exception Handling: Exception-throwing variants for better error handling
- Timeout Management: Configurable timeouts with proper cancellation
Migrate your codebase gradually by replacing method calls one at a time:
// Step 1: Keep existing code working
var client = new UnsplasharpClient("YOUR_APP_ID");
var photo = await client.GetRandomPhoto(); // Old method
// Step 2: Add logging (optional)
var logger = loggerFactory.CreateLogger<UnsplasharpClient>();
var clientWithLogging = new UnsplasharpClient("YOUR_APP_ID", logger: logger);
// Step 3: Replace with exception-throwing methods
try
{
var photo = await client.GetRandomPhotoAsync(); // New method
// Handle success
}
catch (UnsplasharpException ex)
{
// Handle errors
}
// Step 4: Add IHttpClientFactory (for new projects or major refactoring)
services.AddUnsplasharp("YOUR_APP_ID");For new projects, start with the modern approach:
// Program.cs
services.AddUnsplasharp(options =>
{
options.ApplicationId = configuration["Unsplash:ApplicationId"];
options.ConfigureHttpClient = client =>
{
client.Timeout = TimeSpan.FromSeconds(30);
};
});
// Service class
public class PhotoService
{
private readonly UnsplasharpClient _client;
private readonly ILogger<PhotoService> _logger;
public PhotoService(UnsplasharpClient client, ILogger<PhotoService> logger)
{
_client = client;
_logger = logger;
}
public async Task<Photo?> GetPhotoAsync(string photoId)
{
try
{
return await _client.GetPhotoAsync(photoId);
}
catch (UnsplasharpNotFoundException)
{
_logger.LogWarning("Photo {PhotoId} not found", photoId);
return null;
}
catch (UnsplasharpException ex)
{
_logger.LogError(ex, "Error getting photo {PhotoId}", photoId);
throw;
}
}
}// Old approach - null checking
var photo = await client.GetRandomPhoto();
if (photo == null)
{
Console.WriteLine("Failed to get photo");
return;
}
// Process photo
Console.WriteLine($"Photo by {photo.User.Name}");// New approach - exception handling
try
{
var photo = await client.GetRandomPhotoAsync();
Console.WriteLine($"Photo by {photo.User.Name}");
}
catch (UnsplasharpNotFoundException)
{
Console.WriteLine("Photo not found");
}
catch (UnsplasharpRateLimitException ex)
{
Console.WriteLine($"Rate limited. Reset at: {ex.RateLimitReset}");
// Implement backoff strategy
}
catch (UnsplasharpAuthenticationException)
{
Console.WriteLine("Invalid API key");
}
catch (UnsplasharpNetworkException ex) when (ex.IsRetryable)
{
Console.WriteLine("Network error - retrying...");
// Implement retry logic
}
catch (UnsplasharpException ex)
{
Console.WriteLine($"API error: {ex.Message}");
// Access rich error context
if (ex.Context != null)
{
Console.WriteLine($"Correlation ID: {ex.Context.CorrelationId}");
Console.WriteLine($"Request URL: {ex.RequestUrl}");
}
}public static class MigrationHelper
{
/// <summary>
/// Wraps old-style methods to provide exception-based error handling
/// </summary>
public static async Task<T> WrapWithExceptions<T>(Func<Task<T?>> oldMethod) where T : class
{
var result = await oldMethod();
if (result == null)
{
throw new InvalidOperationException("Operation returned null - this may indicate an API error");
}
return result;
}
}
// Usage
try
{
var photo = await MigrationHelper.WrapWithExceptions(() => client.GetRandomPhoto());
// Process photo
}
catch (Exception ex)
{
// Handle error
}// Old approach - basic client creation
var client = new UnsplasharpClient("YOUR_APP_ID");
// Or with logging
var logger = loggerFactory.CreateLogger<UnsplasharpClient>();
var client = new UnsplasharpClient("YOUR_APP_ID", logger: logger);// New approach - dependency injection setup
// In Program.cs or Startup.cs
services.AddUnsplasharp("YOUR_APP_ID");
// Or with configuration
services.AddUnsplasharp(options =>
{
options.ApplicationId = configuration["Unsplash:ApplicationId"];
options.Secret = configuration["Unsplash:Secret"];
options.ConfigureHttpClient = client =>
{
client.Timeout = TimeSpan.FromSeconds(60);
client.DefaultRequestHeaders.UserAgent.ParseAdd("MyApp/1.0");
};
});
// In your service
public class PhotoService
{
private readonly UnsplasharpClient _client;
public PhotoService(UnsplasharpClient client)
{
_client = client; // Injected with proper HttpClient management
}
}If you can't use the extension method:
// Manual setup
services.AddHttpClient();
services.AddScoped<UnsplasharpClient>(provider =>
{
var httpClientFactory = provider.GetRequiredService<IHttpClientFactory>();
var logger = provider.GetService<ILogger<UnsplasharpClient>>();
return new UnsplasharpClient(
applicationId: "YOUR_APP_ID",
logger: logger,
httpClientFactory: httpClientFactory
);
});var client = new UnsplasharpClient("YOUR_APP_ID");
var photo = await client.GetRandomPhoto();
// No visibility into what's happening// Setup logging
services.AddLogging(builder =>
{
builder.AddConsole();
builder.AddDebug();
builder.SetMinimumLevel(LogLevel.Information);
});
// Client with logging
var logger = serviceProvider.GetRequiredService<ILogger<UnsplasharpClient>>();
var client = new UnsplasharpClient("YOUR_APP_ID", logger: logger);
// Or with DI
services.AddUnsplasharp("YOUR_APP_ID"); // Automatically includes logging
// Now you get detailed logs:
// [Information] Making HTTP request to https://api.unsplash.com/photos/random
// [Debug] Rate limit: 4999/5000
// [Information] Request completed in 245msservices.AddLogging(builder =>
{
builder.AddConsole(options =>
{
options.IncludeScopes = true;
options.TimestampFormat = "yyyy-MM-dd HH:mm:ss ";
});
// Set specific log levels
builder.AddFilter("Unsplasharp", LogLevel.Information);
builder.AddFilter("System.Net.Http", LogLevel.Warning);
});The new version automatically uses connection pooling when IHttpClientFactory is available:
// Old: Each client instance creates its own HttpClient
var client1 = new UnsplasharpClient("APP_ID");
var client2 = new UnsplasharpClient("APP_ID"); // Creates another HttpClient
// New: Shared connection pool
services.AddUnsplasharp("APP_ID");
// All injected clients share the same optimized HttpClient poolBuilt-in retry policies with exponential backoff:
// Automatic retry for transient failures
try
{
var photo = await client.GetPhotoAsync("photo-id");
}
catch (UnsplasharpNetworkException ex) when (ex.IsRetryable)
{
// The client already attempted retries with exponential backoff
// This exception means all retries were exhausted
}Better rate limit handling:
// Automatic rate limit tracking
Console.WriteLine($"Rate limit: {client.RateLimitRemaining}/{client.MaxRateLimit}");
// Smart retry on rate limit exceeded
try
{
var photos = await client.SearchPhotosAsync("nature");
}
catch (UnsplasharpRateLimitException ex)
{
// Exception includes exact reset time
var waitTime = ex.RateLimitReset - DateTimeOffset.UtcNow;
await Task.Delay(waitTime);
// Retry the request
}All methods now support cancellation tokens:
// Before: No cancellation support
var photos = await client.SearchPhotos("nature");
// After: With cancellation support
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(30));
try
{
var photos = await client.SearchPhotosAsync("nature", cancellationToken: cts.Token);
}
catch (OperationCanceledException)
{
Console.WriteLine("Search timed out");
}// Before: Basic null checking
public async Task<List<Photo>> GetPhotosOldWay(string query)
{
var photos = await client.SearchPhotos(query);
return photos ?? new List<Photo>();
}
// After: Comprehensive error handling
public async Task<List<Photo>> GetPhotosNewWay(string query)
{
try
{
return await client.SearchPhotosAsync(query);
}
catch (UnsplasharpNotFoundException)
{
return new List<Photo>();
}
catch (UnsplasharpRateLimitException ex)
{
logger.LogWarning("Rate limited, waiting {Delay}ms", ex.TimeUntilReset?.TotalMilliseconds);
throw; // Let caller handle rate limiting
}
catch (UnsplasharpException ex)
{
logger.LogError(ex, "Search failed for query: {Query}", query);
throw;
}
}// Before: Manual instantiation
public class PhotoService
{
private readonly UnsplasharpClient _client;
public PhotoService()
{
_client = new UnsplasharpClient("YOUR_APP_ID");
}
}
// After: Dependency injection
public class PhotoService
{
private readonly UnsplasharpClient _client;
private readonly ILogger<PhotoService> _logger;
public PhotoService(UnsplasharpClient client, ILogger<PhotoService> logger)
{
_client = client;
_logger = logger;
}
}
// Registration
services.AddUnsplasharp("YOUR_APP_ID");
services.AddScoped<PhotoService>();// Enhanced caching with error handling
public class CachedPhotoService
{
private readonly UnsplasharpClient _client;
private readonly IMemoryCache _cache;
private readonly ILogger<CachedPhotoService> _logger;
public async Task<Photo?> GetPhotoAsync(string photoId)
{
var cacheKey = $"photo:{photoId}";
if (_cache.TryGetValue(cacheKey, out Photo cachedPhoto))
{
return cachedPhoto;
}
try
{
var photo = await _client.GetPhotoAsync(photoId);
_cache.Set(cacheKey, photo, TimeSpan.FromHours(1));
return photo;
}
catch (UnsplasharpNotFoundException)
{
// Cache negative results for shorter time
_cache.Set(cacheKey, (Photo?)null, TimeSpan.FromMinutes(5));
return null;
}
catch (UnsplasharpRateLimitException)
{
// Don't cache rate limit errors
throw;
}
catch (UnsplasharpException ex)
{
_logger.LogError(ex, "Failed to get photo {PhotoId}", photoId);
throw;
}
}
}Problem: Code that worked before now throws null reference exceptions.
Cause: You might be using new exception-throwing methods without proper error handling.
Solution:
// If you get NullReferenceException here:
var photo = await client.GetPhotoAsync("invalid-id");
Console.WriteLine(photo.Description); // NullReferenceException
// Change to:
try
{
var photo = await client.GetPhotoAsync("invalid-id");
Console.WriteLine(photo.Description);
}
catch (UnsplasharpNotFoundException)
{
Console.WriteLine("Photo not found");
}Problem: "Cannot access a disposed object" errors.
Cause: Manual HttpClient management conflicts with IHttpClientFactory.
Solution:
// Don't do this:
using var httpClient = new HttpClient();
var client = new UnsplasharpClient("APP_ID", httpClientFactory: someFactory);
// Do this instead:
services.AddUnsplasharp("APP_ID"); // Let DI handle lifecycleProblem: Application stops working when rate limits are hit.
Solution:
public async Task<T> ExecuteWithRetry<T>(Func<Task<T>> operation, int maxRetries = 3)
{
for (int attempt = 1; attempt <= maxRetries; attempt++)
{
try
{
return await operation();
}
catch (UnsplasharpRateLimitException ex) when (attempt < maxRetries)
{
var delay = ex.TimeUntilReset ?? TimeSpan.FromMinutes(1);
await Task.Delay(delay);
}
catch (UnsplasharpNetworkException ex) when (ex.IsRetryable && attempt < maxRetries)
{
var delay = TimeSpan.FromSeconds(Math.Pow(2, attempt));
await Task.Delay(delay);
}
}
throw new InvalidOperationException($"Operation failed after {maxRetries} attempts");
}
// Usage
var photo = await ExecuteWithRetry(() => client.GetPhotoAsync("photo-id"));Problem: API key not being recognized or configuration not loading.
Solution:
// Check configuration loading
var config = builder.Configuration.GetSection("Unsplash").Get<UnsplashConfiguration>();
if (string.IsNullOrEmpty(config?.ApplicationId))
{
throw new InvalidOperationException("Unsplash ApplicationId not configured");
}
// Validate at startup
services.AddUnsplasharp(options =>
{
options.ApplicationId = config.ApplicationId ??
throw new ArgumentNullException(nameof(config.ApplicationId));
});- Review current Unsplasharp usage in your codebase
- Identify error handling patterns
- Plan migration strategy (gradual vs. complete)
- Set up logging infrastructure
- Update NuGet package
- Add logging to existing clients
- Replace null checks with try-catch blocks
- Add CancellationToken support to async methods
- Test existing functionality
- Implement IHttpClientFactory integration
- Add comprehensive error handling
- Implement retry policies
- Add performance monitoring
- Update caching strategies
- Review and optimize HTTP client configuration
- Implement connection pooling
- Add metrics collection
- Performance testing
- Documentation updates
[Test]
public async Task GetPhoto_WithValidId_ReturnsPhoto()
{
// Arrange
var mockFactory = new Mock<IHttpClientFactory>();
var mockLogger = new Mock<ILogger<UnsplasharpClient>>();
var client = new UnsplasharpClient("test-app-id", logger: mockLogger.Object, httpClientFactory: mockFactory.Object);
// Act & Assert
try
{
var photo = await client.GetPhotoAsync("valid-photo-id");
Assert.IsNotNull(photo);
}
catch (UnsplasharpNotFoundException)
{
// Expected for invalid test ID
Assert.Pass("Exception handling working correctly");
}
}
[Test]
public async Task GetPhoto_WithInvalidId_ThrowsNotFoundException()
{
var client = new UnsplasharpClient("test-app-id");
await Assert.ThrowsAsync<UnsplasharpNotFoundException>(
() => client.GetPhotoAsync("invalid-photo-id"));
}[Test]
public async Task Integration_SearchPhotos_ReturnsResults()
{
var client = new UnsplasharpClient(TestConfiguration.ApplicationId);
var photos = await client.SearchPhotosAsync("nature", perPage: 5);
Assert.IsNotEmpty(photos);
Assert.All(photos, photo => Assert.IsNotNull(photo.Id));
}- Manual HttpClient management
- No connection pooling
- Basic error handling
- No retry logic
- No structured logging
- Optimized HttpClient with connection pooling
- Automatic retry with exponential backoff
- Comprehensive error handling with context
- Structured logging with correlation IDs
- Rate limit awareness
- Reduced memory usage: Better HttpClient lifecycle management
- Improved reliability: Automatic retries and better error handling
- Better observability: Structured logging and metrics
- Enhanced performance: Connection pooling and optimized HTTP settings
The migration to the latest Unsplasharp version provides significant improvements while maintaining full backward compatibility. Key benefits include:
β Zero Breaking Changes - Existing code continues to work β Enhanced Error Handling - Specific exceptions with rich context β Modern HTTP Management - IHttpClientFactory integration β Structured Logging - Better observability and debugging β Improved Performance - Connection pooling and retry policies β Better Testing - Easier to mock and test
Take your time with the migration and adopt new features gradually. The investment in proper error handling and logging will pay dividends in production reliability and maintainability.