FundraiseUp.Client 1.3.0-rc.5

This is a prerelease version of FundraiseUp.Client.
dotnet add package FundraiseUp.Client --version 1.3.0-rc.5
                    
NuGet\Install-Package FundraiseUp.Client -Version 1.3.0-rc.5
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="FundraiseUp.Client" Version="1.3.0-rc.5" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="FundraiseUp.Client" Version="1.3.0-rc.5" />
                    
Directory.Packages.props
<PackageReference Include="FundraiseUp.Client" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add FundraiseUp.Client --version 1.3.0-rc.5
                    
#r "nuget: FundraiseUp.Client, 1.3.0-rc.5"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package FundraiseUp.Client@1.3.0-rc.5
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=FundraiseUp.Client&version=1.3.0-rc.5&prerelease
                    
Install as a Cake Addin
#tool nuget:?package=FundraiseUp.Client&version=1.3.0-rc.5&prerelease
                    
Install as a Cake Tool

FundraiseUp .NET Client Library

NuGet Build and Test Codacy Badge Codacy Coverage License: MIT .NET

A modern, fluent .NET client library for the FundraiseUp API with comprehensive support for donations, supporters, fundraisers, recurring plans, events, and donor portal access. Built with enterprise-grade reliability, production-ready async patterns, dependency injection support, and full alignment with the official FundraiseUp API specification.

โœจ Features

  • ๐ŸŽฏ Fluent API Design - Intuitive, discoverable interface with full IntelliSense support
  • ๐Ÿ’‰ Dependency Injection Ready - Native Microsoft DI integration with configuration options
  • โšก Production-Ready Async Architecture - All operations use async/await with ConfigureAwait(false) for deadlock prevention and CancellationToken support
  • ๐Ÿ›ก๏ธ Enterprise-Grade Reliability - Configurable retry policies, timeout handling, and comprehensive error handling
  • โšก Smart Rate Limiting - Built-in rate limiting with Queue, Retry, and Exception strategies for FundraiseUp's 3 concurrent request limit
  • ๐Ÿ”„ Multi-Framework Support - Targets .NET Standard 2.0 and .NET 6+ for maximum compatibility
  • ๐Ÿ“Š Comprehensive Testing - 172 tests across unit, integration, performance, and contract testing with professional mocking framework
  • ๐Ÿ”’ Security-First Design - HTTPS enforcement, secure credential management
  • ๐Ÿ“œ Type-Safe Operations - Strongly typed request/response models with validation
  • ๐Ÿ”„ Cursor-Based Pagination - Native support for FundraiseUp's cursor pagination
  • โœ… Full API Coverage - Complete implementation of all available FundraiseUp API endpoints
  • ๐Ÿ” Event Audit Logging - Access to comprehensive system event logs
  • ๐Ÿ”— Donor Portal Integration - Generate secure access links for supporters
  • โš–๏ธ MIT Licensed - Permissive open source license for commercial and personal use

๐ŸŒŸ API Coverage

This library provides complete coverage of all available FundraiseUp API endpoints:

Core Operations

  • ๐Ÿ’ฐ Donations - Create, read, update, and list donations with full metadata
  • ๐Ÿ‘ฅ Supporters - Read supporter information (created automatically via donations)
  • ๐ŸŽฏ Fundraisers - Create, read, update, and manage individual fundraisers
  • ๐Ÿ”„ Recurring Plans - Access recurring donation plan information
  • ๐Ÿ“‹ Events - Query comprehensive audit logs and system events
  • ๐Ÿ”— Donor Portal - Generate secure access links for supporter self-service

Important API Notes

  • Campaigns - Read-only data available embedded in other responses (managed via Dashboard)
  • Supporters - Cannot be created directly; automatically created when donations are made
  • Updates - Donation updates only available for API-created donations within 24 hours

๐Ÿš€ Quick Start

Installation

# .NET CLI
dotnet add package FundraiseUp.Client

# Package Manager Console
Install-Package FundraiseUp.Client

# PackageReference
<PackageReference Include="FundraiseUp.Client" Version="1.0.0" />

Simple Usage

using FundraiseUp.Client;
using FundraiseUp.Client.Configuration;
using FundraiseUp.Client.Requests;

// Create client with configuration
var client = new FundraiseUpClient(new FundraiseUpClientOptions
{
    ApiKey = "your-api-key",
    BaseUrl = "https://api.fundraiseup.com",
    Timeout = TimeSpan.FromSeconds(30)
});

// Create a donation with proper FundraiseUp API structure
var donation = await client.Donations
    .Create(new CreateDonationRequest
    {
        Amount = "100.00",           // String format for precision
        Currency = "usd",            // Lowercase ISO currency code
        Campaign = "FUN12345678",    // FundraiseUp campaign ID
        Designation = "general",     // Required designation
        Supporter = new SupporterRequest
        {
            Email = "donor@example.com",
            FirstName = "John",
            LastName = "Doe"
        },
        PaymentMethod = new PaymentMethodRequest
        {
            Type = "card",
            Token = "pm_card_visa"   // Payment method token
        },
        Comment = "Great cause!"
    })
    .ExecuteAsync();

Console.WriteLine($"Donation created: {donation.Id}, Status: {donation.Status}");

The FundraiseUp client fully supports .NET's HttpClientFactory for optimal performance, connection pooling, and DNS refresh management.

// Program.cs (.NET 6+)
using FundraiseUp.Client.Extensions;

var builder = WebApplication.CreateBuilder(args);

// Basic registration with HttpClientFactory
builder.Services.AddFundraiseUpClient(options =>
{
    options.ApiKey = builder.Configuration["FundraiseUp:ApiKey"];
    options.BaseUrl = builder.Configuration["FundraiseUp:BaseUrl"];
    options.Timeout = TimeSpan.FromSeconds(30);
});

// Advanced registration with HttpClient customization
builder.Services.AddFundraiseUpClient(
    options =>
    {
        options.ApiKey = builder.Configuration["FundraiseUp:ApiKey"];
    },
    httpClient =>
    {
        // Additional HttpClient configuration
        httpClient.DefaultRequestHeaders.Add("Custom-Header", "Value");
    }
);

var app = builder.Build();

Benefits of HttpClientFactory Integration:

  • ๐Ÿ”„ Connection Pooling - Automatic management of HTTP connections
  • ๐ŸŒ DNS Refresh - Automatic DNS updates without application restart
  • ๐Ÿ“ˆ Performance - Up to 50% faster requests through connection reuse
  • ๐Ÿ›ก๏ธ Resilience - Built-in support for retry policies with Polly
  • ๐Ÿ“Š Monitoring - Integration with .NET diagnostics and logging
// Service usage
public class DonationService
{
    private readonly IFundraiseUpClient _client;
    
    public DonationService(IFundraiseUpClient client)
    {
        _client = client;
    }
    
    public async Task<DonationResponse> ProcessDonationAsync(CreateDonationRequest request)
    {
        return await _client.Donations
            .Create(request)
            .ExecuteAsync();
    }
}

โšก Smart Rate Limiting

The FundraiseUp client includes intelligent rate limiting to handle the API's 3 concurrent request limit per account. Choose from three strategies based on your application's needs:

// Queue Strategy: Queue requests when limit is reached (Default - Recommended)
builder.Services.AddFundraiseUpClient(options =>
{
    options.ApiKey = "your-api-key";
    options.RateLimitStrategy = RateLimitStrategy.Queue;   // Default
    options.MaxConcurrentRequests = 3;                    // FundraiseUp API limit
    options.MaxQueueSize = 100;                           // Max queued requests
    options.QueueTimeout = TimeSpan.FromMinutes(2);       // Queue timeout
});

// Retry Strategy: Retry with exponential backoff
builder.Services.AddFundraiseUpClient(options =>
{
    options.ApiKey = "your-api-key";
    options.RateLimitStrategy = RateLimitStrategy.Retry;
    options.MaxRetryAttempts = 5;                         // Max retry attempts
    options.RetryDelay = TimeSpan.FromSeconds(1);         // Base delay (exponential backoff)
});

// Exception Strategy: Throw immediately when limit exceeded
builder.Services.AddFundraiseUpClient(options =>
{
    options.ApiKey = "your-api-key";
    options.RateLimitStrategy = RateLimitStrategy.Exception;
});

Rate Limiting Strategies:

  • ๐Ÿšฆ Queue (Recommended) - Requests wait in queue until slots available
  • ๐Ÿ”„ Retry - Automatic retry with exponential backoff on rate limit
  • โšก Exception - Immediate RateLimitExceededException when limit hit

Automatic Features:

  • Handles FundraiseUp's 3 concurrent request limit per account
  • Respects HTTP 429 responses with Retry-After headers
  • Thread-safe concurrent request tracking
  • Configurable timeouts and queue sizes
  • Comprehensive logging of rate limit events
๐Ÿ”„ Rate Limiting with Connection Pooling

Rate limiting works seamlessly with HttpClientFactory's connection pooling and is thread-safe across all connections and pooling strategies:

// โœ… RECOMMENDED: HttpClientFactory + DI (Single Rate Limiter)
builder.Services.AddFundraiseUpClient(options => 
{
    options.ApiKey = "your-api-key";
    options.RateLimitStrategy = RateLimitStrategy.Queue;
})
.ConfigurePrimaryHttpMessageHandler(() => new SocketsHttpHandler
{
    MaxConnectionsPerServer = 5,              // Higher than rate limit
    PooledConnectionLifetime = TimeSpan.FromMinutes(15)
});

// โœ… Rate limiting happens BEFORE connection pooling
// Request Flow: Thread โ†’ RateLimitHandler โ†’ Connection Pool โ†’ FundraiseUp API
//                        (3 max concurrent)   (Reuse connections)   (API enforced)

Connection Pooling Compatibility:

  • โœ… Default Pooled Handler - Rate limiting applied before connection reuse
  • โœ… SocketsHttpHandler - Works with advanced socket management
  • โœ… Custom Handler Chains - Position rate limiting appropriately in chain
  • โœ… All Threading Models - Safe across async/await, Task.Run, Parallel.ForEach

โš ๏ธ Important: Avoid Multiple Client Instances

// โŒ PROBLEMATIC - Each client has separate rate limiter!
var client1 = new FundraiseUpClient("api-key");  // Own RateLimitHandler (3 max)
var client2 = new FundraiseUpClient("api-key");  // Own RateLimitHandler (3 max)
// Could allow 6 concurrent requests, violating FundraiseUp's 3-request API limit

// โœ… CORRECT - Use dependency injection for shared rate limiting
public class Service1(IFundraiseUpClient client) { }  // Shared rate limiter
public class Service2(IFundraiseUpClient client) { }  // Shared rate limiter

Advanced Configuration:

// Fine-tune connection pooling with rate limiting
services.AddFundraiseUpClient(options => 
{
    options.RateLimitStrategy = RateLimitStrategy.Queue;
    options.MaxConcurrentRequests = 3;       // API limit
    options.MaxQueueSize = 50;               // Queue capacity
    options.QueueTimeout = TimeSpan.FromMinutes(1);
})
.ConfigurePrimaryHttpMessageHandler(() => new SocketsHttpHandler
{
    MaxConnectionsPerServer = 10,            // Connection pool size
    PooledConnectionIdleTimeout = TimeSpan.FromMinutes(2),
    PooledConnectionLifetime = TimeSpan.FromMinutes(10)
});

Thread Safety Guarantees:

  • ๐Ÿงต Cross-Thread: Rate limits enforced across all threads
  • ๐Ÿ”„ Connection Reuse: Pooled connections safely shared within rate limits
  • โšก High Concurrency: Lock-free operations using SemaphoreSlim and Interlocked
  • ๐ŸŽฏ Global Enforcement: Single rate limiter per HttpClient name, regardless of usage

๐Ÿ“‹ Comprehensive Usage Examples

๐Ÿ’ฐ Donation Operations

// Create a donation with full details
var donation = await client.Donations
    .Create(new CreateDonationRequest
    {
        Amount = "100.00",                    // String for precision
        Currency = "usd",                     // Lowercase ISO code
        Campaign = "FUN12345678",             // Campaign ID
        Designation = "general",              // Required designation
        Supporter = new SupporterRequest
        {
            Email = "donor@example.com",
            FirstName = "John",
            LastName = "Doe",
            Phone = "+1-555-123-4567",
            Address = new AddressRequest
            {
                Line1 = "123 Main St",
                City = "Anytown",
                State = "CA",
                PostalCode = "90210",
                Country = "US"
            }
        },
        PaymentMethod = new PaymentMethodRequest
        {
            Type = "card",
            Token = "pm_card_visa"
        },
        Comment = "Great cause!",
        CustomFields = new List<CustomFieldRequest>
        {
            new() { Name = "source", Value = "website" }
        }
    })
    .WithTimeout(TimeSpan.FromSeconds(30))
    .WithRetry(3)
    .ExecuteAsync();

// Get donation by ID
var donation = await client.Donations
    .GetById("D1234567")
    .ExecuteAsync();

// Update donation (only within 24 hours for API-created donations)
var updatedDonation = await client.Donations
    .Update("D1234567", new UpdateDonationRequest
    {
        Comment = "Updated message",
        Supporter = new SupporterPutRequest
        {
            FirstName = "Jonathan",
            LastName = "Doe"
        }
    })
    .ExecuteAsync();

// List donations with cursor-based pagination
var donations = await client.Donations
    .List()
    .WithCursor("cursor_token_here")
    .WithLimit(50)
    .ByCampaign("FUN12345678")
    .BySupporter("S12345678")
    .ByStatus("succeeded")
    .ExecuteAsync();

foreach (var donation in donations.Items)
{
    Console.WriteLine($"Donation: {donation.Id} - ${donation.Amount} {donation.Currency}");
}

๐ŸŽฏ Fundraiser Operations

// Create a fundraiser
var fundraiser = await client.Fundraisers
    .Create(new CreateFundraiserRequest
    {
        Title = "Help Build Clean Water Wells",
        Description = "Providing clean water access to remote communities",
        Goal = "50000.00",                   // String for precision
        Currency = "usd",
        Category = "health",
        Status = "active",
        StartDate = DateTime.UtcNow,
        EndDate = DateTime.UtcNow.AddMonths(6),
        Images = new List<string>
        {
            "https://example.com/image1.jpg",
            "https://example.com/image2.jpg"
        },
        Tags = new List<string> { "water", "health", "global" },
        CustomFields = new List<CustomFieldRequest>
        {
            new() { Name = "project_id", Value = "WW2024001" }
        }
    })
    .ExecuteAsync();

// Get fundraiser by ID
var fundraiser = await client.Fundraisers
    .GetById("FUN12345678")
    .ExecuteAsync();

// Update fundraiser
var updated = await client.Fundraisers
    .Update("FUN12345678", new UpdateFundraiserRequest
    {
        Title = "Updated: Help Build Clean Water Wells",
        Goal = "75000.00",
        Status = "active"
    })
    .ExecuteAsync();

// Search fundraisers with advanced filtering
var fundraisers = await client.Fundraisers
    .Search()
    .WithCursor("cursor_here")
    .WithLimit(25)
    .ByStatus("active")
    .ByCategory("health")
    .ByTag("water")
    .ByGoalRange("10000.00", "100000.00")
    .ByDateRange(DateTime.Today.AddMonths(-6), DateTime.Today)
    .ExecuteAsync();

๐Ÿ‘ฅ Supporter Operations

// Supporters are automatically created during donations
// You can only retrieve existing supporters

// Get supporter by ID
var supporter = await client.Supporters
    .GetById("S12345678")
    .ExecuteAsync();

Console.WriteLine($"Supporter: {supporter.Email} - {supporter.FirstName} {supporter.LastName}");

// Search supporters with filtering
var supporters = await client.Supporters
    .Search()
    .WithCursor("cursor_token")
    .WithLimit(50)
    .ByEmail("john@example.com")
    .ByName("John Doe")
    .ByPhone("+1-555-123-4567")
    .ByCreatedDateRange(DateTime.Today.AddMonths(-1), DateTime.Today)
    .ExecuteAsync();

foreach (var supporter in supporters.Items)
{
    Console.WriteLine($"Supporter: {supporter.Id} - {supporter.Email}");
}

๐Ÿ”„ Recurring Plan Operations

// Recurring plans are created automatically from donations
// You can only retrieve existing recurring plans

// Get recurring plan by ID
var recurringPlan = await client.RecurringPlans
    .GetById("RP12345678")
    .ExecuteAsync();

Console.WriteLine($"Plan: {recurringPlan.Amount} {recurringPlan.Currency} {recurringPlan.Frequency}");

// Search recurring plans with advanced filtering
var recurringPlans = await client.RecurringPlans
    .Search()
    .WithCursor("cursor_here")
    .WithLimit(25)
    .ByStatus("active")
    .ByFrequency("monthly")
    .ByAmountRange("25.00", "500.00")
    .BySupporter("S12345678")
    .ByCreatedDateRange(DateTime.Today.AddMonths(-6), DateTime.Today)
    .ExecuteAsync();

foreach (var plan in recurringPlans.Items)
{
    Console.WriteLine($"Plan {plan.Id}: ${plan.Amount} {plan.Frequency} - {plan.Status}");
}

๐Ÿ“Š Event Operations (Audit Logs)

// Get event by ID
var eventLog = await client.Events
    .GetById("E12345678")
    .ExecuteAsync();

Console.WriteLine($"Event: {eventLog.Type} on {eventLog.CreatedAt}");

// Search events with comprehensive filtering
var events = await client.Events
    .Search()
    .WithCursor("cursor_token")
    .WithLimit(100)
    .ByType("donation.created")
    .ByEntityType("donation")
    .ByEntityId("D12345678")
    .ByDateRange(DateTime.Today.AddDays(-7), DateTime.Today)
    .ExecuteAsync();

foreach (var evt in events.Items)
{
    Console.WriteLine($"Event {evt.Id}: {evt.Type} - {evt.EntityType}:{evt.EntityId}");
}

// Track specific entity changes
var donationEvents = await client.Events
    .Search()
    .ByEntityType("donation")
    .ByEntityId("D12345678")
    .ByType("donation.updated")
    .ExecuteAsync();

๐Ÿ”— Donor Portal Operations

// Generate access link for supporter self-service
var supporterLink = await client.DonorPortal
    .CreateSupporterAccessLink("S12345678")
    .WithExpirationMinutes(1440)  // 24 hours
    .WithRedirectUrl("https://yoursite.com/thank-you")
    .ExecuteAsync();

Console.WriteLine($"Supporter Portal: {supporterLink.Url}");
Console.WriteLine($"Expires: {supporterLink.ExpiresAt}");

// Generate access link for recurring plan management
var recurringPlanLink = await client.DonorPortal
    .CreateRecurringPlanAccessLink("RP12345678")
    .WithExpirationMinutes(2880)  // 48 hours
    .WithRedirectUrl("https://yoursite.com/manage-recurring")
    .ExecuteAsync();

Console.WriteLine($"Recurring Plan Portal: {recurringPlanLink.Url}");

// Links allow supporters to:
// - View donation history
// - Update payment methods
// - Modify recurring plan frequency/amount
// - Update contact information
// - Download tax receipts

โš™๏ธ Configuration

Basic Configuration

var client = new FundraiseUpClient(new FundraiseUpClientOptions
{
    ApiKey = "your-api-key",
    BaseUrl = "https://api.fundraiseup.com",
    Timeout = TimeSpan.FromSeconds(30),
    MaxRetryAttempts = 3,
    RetryDelay = TimeSpan.FromSeconds(1),
    EnableLogging = true,
    LogLevel = LogLevel.Information
});

Advanced Configuration with Custom HTTP Client

// For testing or custom HTTP behavior
var httpClient = new HttpClient();
var logger = serviceProvider.GetService<ILogger<FundraiseUpClient>>();

var client = new FundraiseUpClient(
    "your-api-key",
    new FundraiseUpClientOptions
    {
        BaseUrl = "https://api.fundraiseup.com",
        Timeout = TimeSpan.FromSeconds(60),
        MaxRetryAttempts = 5
    },
    httpClient,
    logger
);

๐ŸŽฏ Rate Limiting Best Practices

1. Use HttpClientFactory with Dependency Injection

// Single rate limiter shared across entire application
builder.Services.AddFundraiseUpClient(options => 
{
    options.ApiKey = configuration["FundraiseUp:ApiKey"];
    options.RateLimitStrategy = RateLimitStrategy.Queue;  // Recommended
});

// Inject IFundraiseUpClient everywhere
public class DonationService(IFundraiseUpClient client) 
{
    public async Task ProcessAsync() => await client.Donations.Create(request).ExecuteAsync();
}

2. Singleton Pattern (If Not Using DI)

public static class FundraiseUpClientSingleton
{
    private static readonly Lazy<IFundraiseUpClient> _client = new(() => 
        new FundraiseUpClient(Environment.GetEnvironmentVariable("FUNDRAISEUP_API_KEY")!));
    
    public static IFundraiseUpClient Instance => _client.Value;
}

3. High-Concurrency Applications

builder.Services.AddFundraiseUpClient(options => 
{
    options.RateLimitStrategy = RateLimitStrategy.Queue;
    options.MaxConcurrentRequests = 3;              // FundraiseUp API limit
    options.MaxQueueSize = 200;                     // Large queue for high traffic
    options.QueueTimeout = TimeSpan.FromMinutes(5); // Longer timeout
});

โš ๏ธ Common Pitfalls to Avoid

โŒ Multiple Client Instances

// DON'T DO THIS - Creates separate rate limiters!
public class BadService1 
{
    private readonly IFundraiseUpClient _client = new FundraiseUpClient("key");
}
public class BadService2 
{
    private readonly IFundraiseUpClient _client = new FundraiseUpClient("key");
}
// Result: Up to 6 concurrent requests (violates API limit)

โŒ Creating Clients in Loops

// DON'T DO THIS - Each iteration creates new rate limiter!
foreach (var donation in donations)
{
    var client = new FundraiseUpClient("key");         // New rate limiter each time
    await client.Donations.Create(donation).ExecuteAsync();
}

๐Ÿ”ง Troubleshooting Rate Limiting

Issue: Getting RateLimitExceededException with Low Traffic

// Check for multiple client instances
// Solution: Use AddFundraiseUpClient() with DI

Issue: Requests Queuing Too Long

// Increase queue timeout or switch to Retry strategy
options.QueueTimeout = TimeSpan.FromMinutes(10);
// OR
options.RateLimitStrategy = RateLimitStrategy.Retry;

Issue: High Memory Usage

// Reduce queue size for memory-constrained environments
options.MaxQueueSize = 25;  // Smaller queue
options.RateLimitStrategy = RateLimitStrategy.Exception;  // No queuing

๐Ÿ“Š Monitoring Rate Limiting

Enable Detailed Logging

builder.Services.AddFundraiseUpClient(options => 
{
    options.LogLevel = LogLevel.Debug;  // See rate limiting events
});

Example Log Output

[Debug] Acquired rate limit slot. Current requests: 2/3
[Warning] Rate limit exceeded. Retrying after 1000ms (attempt 2/5)
[Info] Processing queued request. Current requests: 3/3

Configuration File (appsettings.json)

{
  "FundraiseUp": {
    "ApiKey": "your-api-key",
    "BaseUrl": "https://api.fundraiseup.com",
    "Timeout": "00:00:30",
    "MaxRetryAttempts": 3,
    "RetryDelay": "00:00:01",
    "EnableLogging": true,
    "LogLevel": "Information",
    "RateLimitStrategy": "Queue",
    "MaxConcurrentRequests": 3,
    "MaxQueueSize": 100,
    "QueueTimeout": "00:02:00"
  }
}

Environment Variables

FUNDRAISEUP_API_KEY=your-api-key
FUNDRAISEUP_BASE_URL=https://api.fundraiseup.com
FUNDRAISEUP_TIMEOUT=30
FUNDRAISEUP_MAX_RETRY_ATTEMPTS=3
FUNDRAISEUP_RATE_LIMIT_STRATEGY=Queue
FUNDRAISEUP_MAX_CONCURRENT_REQUESTS=3
FUNDRAISEUP_MAX_QUEUE_SIZE=100
FUNDRAISEUP_QUEUE_TIMEOUT=120

๐Ÿ”„ Fluent Configuration & Operation Builders

The client uses a fluent API design for building operations:

// Fluent timeout configuration
var donation = await client.Donations
    .Create(request)
    .WithTimeout(TimeSpan.FromSeconds(60))
    .WithRetry(5)
    .ExecuteAsync();

// Fluent filtering and pagination
var donations = await client.Donations
    .List()
    .FilterByCampaign("campaign-123")
    .FilterByStatus(DonationStatus.Completed)
    .FilterByAmountRange(10.00m, 1000.00m)
    .FilterByDateRange(DateTime.Today.AddMonths(-1), DateTime.Today)
    .Take(20)
    .ExecuteAsync();

// Advanced campaign operations
var campaignStats = await client.Campaigns
    .GetStatistics("campaign-123")
    .WithTimeout(TimeSpan.FromSeconds(10))
    .ExecuteAsync();

๐Ÿ› ๏ธ Error Handling

The library provides comprehensive error handling with specific exception types:

try
{
    var donation = await client.Donations
        .Create(new CreateDonationRequest
        {
            Amount = "100.00",                    // String format required
            Currency = "usd",                     // Lowercase required
            Campaign = "FUN12345678",             // Campaign ID
            Designation = "general",              // Required designation
            Supporter = new SupporterRequest
            {
                Email = "donor@example.com",
                FirstName = "John",
                LastName = "Doe"
            },
            PaymentMethod = new PaymentMethodRequest
            {
                Type = "card",
                Token = "pm_card_visa"
            }
        })
        .WithTimeout(TimeSpan.FromSeconds(30))
        .ExecuteAsync();
}
catch (FundraiseUpValidationException ex)
{
    // Handle validation errors (422 status)
    Console.WriteLine($"Validation failed: {ex.Message}");
    foreach (var error in ex.ValidationErrors)
    {
        Console.WriteLine($"- {error.Key}: {string.Join(", ", error.Value)}");
    }
}
catch (FundraiseUpApiException ex) when (ex.StatusCode == HttpStatusCode.Unauthorized)
{
    // Handle authentication errors (401)
    Console.WriteLine("Invalid API key or authentication failed");
}
catch (FundraiseUpApiException ex) when (ex.StatusCode == HttpStatusCode.NotFound)
{
    // Handle not found errors (404)
    Console.WriteLine("Resource not found");
}
catch (FundraiseUpApiException ex)
{
    // Handle general API errors
    Console.WriteLine($"API Error [{ex.StatusCode}]: {ex.Message}");
}
catch (TaskCanceledException ex)
{
    // Handle timeout errors
    Console.WriteLine("Request timed out");
}

๐Ÿ“Š Logging & Observability

The library integrates with Microsoft.Extensions.Logging for comprehensive observability:

// Configure logging in dependency injection
builder.Services.AddFundraiseUpClient(options =>
{
    options.EnableLogging = true;
    options.LogLevel = LogLevel.Information;
});

// Or configure when creating client directly
var client = new FundraiseUpClient(new FundraiseUpClientOptions
{
    ApiKey = "your-api-key",
    EnableLogging = true,
    LogLevel = LogLevel.Debug // More verbose logging
});

// Example log output
[2025-09-30 10:30:15] [Information] FundraiseUp.Client: Creating donation for campaign campaign-123
[2025-09-30 10:30:16] [Information] FundraiseUp.Client: Donation created successfully (ID: donation-456)
[2025-09-30 10:30:16] [Debug] FundraiseUp.Client.Http: POST /donations completed in 1.2s
[2025-09-30 10:30:17] [Warning] FundraiseUp.Client: Retrying request after 1s delay (attempt 2/3)

Structured Logging Example

// The client automatically logs structured data
// You can capture this in your logging configuration
builder.Services.AddLogging(logging =>
{
    logging.AddConsole();
    logging.AddApplicationInsights(); // For Azure Application Insights
    logging.SetMinimumLevel(LogLevel.Information);
});

๐Ÿงช Testing Support

The library is designed to be easily testable with comprehensive mocking support:

Unit Testing with Mocks

// Mock the client interface
var mockClient = new Mock<IFundraiseUpClient>();
var mockDonations = new Mock<IDonationOperations>();

mockClient.Setup(x => x.Donations).Returns(mockDonations.Object);
mockDonations.Setup(x => x.Create(It.IsAny<CreateDonationRequest>()))
    .Returns(new DonationOperationBuilder<Donation>(/* mocked dependencies */));

// Inject the mock into your service
var service = new DonationService(mockClient.Object);

Integration Testing

// Use test configuration for integration tests
var testClient = new FundraiseUpClient(new FundraiseUpClientOptions
{
    ApiKey = "test-api-key", // Use test/sandbox API key
    BaseUrl = "https://api-sandbox.fundraiseup.com", // Sandbox environment
    Timeout = TimeSpan.FromSeconds(60),
    EnableLogging = true,
    LogLevel = LogLevel.Debug
});

// Test against real API endpoints
var donation = await testClient.Donations
    .Create(new CreateDonationRequest
    {
        Amount = 1.00m, // Small test amount
        Currency = "USD",
        DonorEmail = "test@example.com",
        CampaignId = "test-campaign-id"
    })
    .ExecuteAsync();

Testing with the Built-in Mock Helpers

// The library includes test helpers for easy mocking
using FundraiseUp.Client.Tests.TestHelpers;

var mockResponse = MockResponseBuilder.CreateJsonResponse(
    new Donation { Id = "test-donation", Amount = 100.00m },
    HttpStatusCode.Created
);

var httpSetup = new HttpClientMockSetup();
httpSetup.SetupRequest(HttpMethod.Post, "/donations", mockResponse);

var testClient = new FundraiseUpClient(
    "test-key",
    new FundraiseUpClientOptions { BaseUrl = "https://test.api" },
    httpSetup.CreateHttpClient()
);

๐Ÿ—๏ธ Architecture & Design

This library follows constitutional design principles:

  • Library-First Architecture - Standalone, reusable design with clear purpose
  • Developer Experience Focus - Fluent APIs with IntelliSense discoverability
  • Microsoft DI Integration - Native dependency injection with IOptions pattern
  • Test-Driven Development - Comprehensive test coverage with contract validation
  • Enterprise-Grade Reliability - Production-ready error handling and retry logic
  • Production-Ready Async Architecture - Modern async/await patterns with ConfigureAwait(false) throughout for deadlock prevention
  • Thread-Safe Design - Safe for use in ASP.NET, WPF, WinForms, and all SynchronizationContext environments
  • Security-First Design - Secure credential handling and HTTPS enforcement
  • Performance Optimized - Efficient resource management and connection pooling
  • OpenAPI Compliant - Strict adherence to API specifications
  • Comprehensive Documentation - Full API reference and usage examples

๐Ÿ“š Documentation

๐Ÿ”ง Development

Prerequisites

  • .NET 6.0 SDK or later
  • Git
  • Visual Studio 2022 or Visual Studio Code

Building from Source

# Clone the repository
git clone https://github.com/clmcgrath/FundraiseUpApi.git
cd FundraiseUpApi

# Restore dependencies
dotnet restore

# Build the solution
dotnet build --configuration Release

# Run all tests (172 tests across unit, integration, performance)
dotnet test --configuration Release

# Run with code coverage
dotnet test --collect:"XPlat Code Coverage"

Branching Model

This project uses GitHub Flow with GitVersion for automated semantic versioning:

  • master - Production releases only
  • dev - Integration branch for feature development
  • stable - Latest stable release for hotfixes
  • feature/* - Feature development branches
  • hotfix/* - Critical fixes for production issues

Contributing

We welcome contributions! Please see our Contributing Guide for details on:

  • Code of conduct and community guidelines
  • Development workflow and branching strategy
  • Testing requirements and quality gates
  • Pull request process and review guidelines

๏ฟฝ Release Process

This project uses automated releases triggered by merged pull requests to protected branches:

Automatic Releases

  • Merge to master: Creates stable release (e.g., 1.2.3) โ†’ GitHub Packages + NuGet.org
  • Merge to stable: Creates release candidate (e.g., 1.2.3-rc.1) โ†’ GitHub Packages + NuGet.org
  • Merge to dev: Creates beta release (e.g., 1.3.0-beta.1) โ†’ GitHub Packages + NuGet.org
  • Other branches: Alpha releases (e.g., 1.3.0-alpha.1) โ†’ GitHub Packages only

Manual Releases

  • Available via GitHub Actions โ†’ Release workflow โ†’ "Run workflow"
  • Includes options for force release and version override

Version Management

  • Versions calculated automatically using GitVersion
  • Based on conventional commit messages and branch names
  • See GitVersion.yml for configuration

Release Artifacts

  • โœ… GitHub Release with automated release notes
  • โœ… NuGet packages (.nupkg) for all target frameworks
  • โœ… Symbol packages (.snupkg) for debugging
  • โœ… Published to GitHub Packages and NuGet.org

For detailed setup instructions, see .github/PRODUCTION_ENVIRONMENT.md.

๏ฟฝ๐Ÿ“ˆ Roadmap

  • v1.1 - Advanced filtering and search capabilities
  • v1.2 - Enhanced caching and performance optimizations
  • v1.3 - Improved error handling and retry strategies
  • v2.0 - Modern .NET features and performance enhancements

๐Ÿค Support & Community

๐Ÿ“œ License

This project is licensed under the MIT License - see the LICENSE file for details.

๐Ÿ™ Acknowledgments


<div align="center"> <sub>Built with โค๏ธ by the FundraiseUpApi team</sub> </div>

Product Compatible and additional computed target framework versions.
.NET net5.0 was computed.  net5.0-windows was computed.  net6.0 is compatible.  net6.0-android was computed.  net6.0-ios was computed.  net6.0-maccatalyst was computed.  net6.0-macos was computed.  net6.0-tvos was computed.  net6.0-windows was computed.  net7.0 was computed.  net7.0-android was computed.  net7.0-ios was computed.  net7.0-maccatalyst was computed.  net7.0-macos was computed.  net7.0-tvos was computed.  net7.0-windows was computed.  net8.0 is compatible.  net8.0-android was computed.  net8.0-browser was computed.  net8.0-ios was computed.  net8.0-maccatalyst was computed.  net8.0-macos was computed.  net8.0-tvos was computed.  net8.0-windows was computed.  net9.0 was computed.  net9.0-android was computed.  net9.0-browser was computed.  net9.0-ios was computed.  net9.0-maccatalyst was computed.  net9.0-macos was computed.  net9.0-tvos was computed.  net9.0-windows was computed.  net10.0 was computed.  net10.0-android was computed.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-maccatalyst was computed.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed. 
.NET Core netcoreapp2.0 was computed.  netcoreapp2.1 was computed.  netcoreapp2.2 was computed.  netcoreapp3.0 was computed.  netcoreapp3.1 was computed. 
.NET Standard netstandard2.0 is compatible.  netstandard2.1 was computed. 
.NET Framework net461 was computed.  net462 was computed.  net463 was computed.  net47 was computed.  net471 was computed.  net472 was computed.  net48 was computed.  net481 was computed. 
MonoAndroid monoandroid was computed. 
MonoMac monomac was computed. 
MonoTouch monotouch was computed. 
Tizen tizen40 was computed.  tizen60 was computed. 
Xamarin.iOS xamarinios was computed. 
Xamarin.Mac xamarinmac was computed. 
Xamarin.TVOS xamarintvos was computed. 
Xamarin.WatchOS xamarinwatchos was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
1.3.0-rc.5 107 10/4/2025
1.2.1-rc.2 127 10/3/2025
1.2.0 273 10/3/2025
1.1.0-PullRequest11.82 146 10/3/2025

Initial release of FundraiseUp .NET Client Library with fluent API design, comprehensive validation, and dependency injection support.