DEV Community

Custodia-Admin
Custodia-Admin

Posted on • Originally published at pagebolt.dev

How to Take a Screenshot with C# and .NET

How to Take a Screenshot with C# and .NET

You're building a .NET application that needs screenshots. You could self-host Puppeteer, but you'd be managing:

  • Browser processes on your servers
  • Memory and CPU overhead
  • Crash recovery and logging
  • Scaling across load-balanced instances

Use an API instead.

One HTTP request. Binary PNG response. Works with .NET 6+, ASP.NET Core, Azure Functions, and Console apps.

The Basic Pattern: HttpClient Binary Download

curl -X POST https://api.pagebolt.dev/v1/screenshot \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"url":"https://example.com","format":"png","fullPage":true}' \
  > screenshot.png
Enter fullscreen mode Exit fullscreen mode

In C#, use HttpClient for type-safe requests:

C# Implementation: HttpClient Binary Response

using System;
using System.Net.Http;
using System.Text.Json;
using System.Threading.Tasks;

public class PageBoltClient
{
    private readonly string _apiKey;
    private readonly HttpClient _httpClient;
    private const string BaseUrl = "https://api.pagebolt.dev/v1";

    public PageBoltClient(string apiKey)
    {
        _apiKey = apiKey;
        _httpClient = new HttpClient();
    }

    public async Task TakeScreenshotAsync(string url, string outputPath)
    {
        var payload = new
        {
            url = url,
            format = "png",
            width = 1280,
            height = 720,
            fullPage = true,
            blockBanners = true
        };

        var content = new StringContent(
            JsonSerializer.Serialize(payload),
            System.Text.Encoding.UTF8,
            "application/json"
        );

        var request = new HttpRequestMessage(HttpMethod.Post, $"{BaseUrl}/screenshot")
        {
            Content = content,
            Headers = { { "Authorization", $"Bearer {_apiKey}" } }
        };

        var response = await _httpClient.SendAsync(request);

        if (!response.IsSuccessStatusCode)
        {
            throw new Exception($"API error: {response.StatusCode}");
        }

        // Response is binary PNG — read as bytes
        var imageBytes = await response.Content.ReadAsByteArrayAsync();

        // Write directly to file
        await File.WriteAllBytesAsync(outputPath, imageBytes);
        Console.WriteLine($"Screenshot saved to {outputPath}");
    }

    public static async Task Main(string[] args)
    {
        var client = new PageBoltClient(Environment.GetEnvironmentVariable("PAGEBOLT_API_KEY"));
        await client.TakeScreenshotAsync("https://example.com", "screenshot.png");
    }
}
Enter fullscreen mode Exit fullscreen mode

Key points:

  • ReadAsByteArrayAsync() handles binary PNG data
  • File.WriteAllBytesAsync() writes bytes directly to disk
  • No JSON decoding — response is raw PNG bytes

Production Pattern: Reusable Service

using System;
using System.Net.Http;
using System.Text.Json.Serialization;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;

public class ScreenshotOptions
{
    [JsonPropertyName("url")]
    public string Url { get; set; }

    [JsonPropertyName("format")]
    public string Format { get; set; } = "png";

    [JsonPropertyName("width")]
    public int Width { get; set; } = 1280;

    [JsonPropertyName("height")]
    public int Height { get; set; } = 720;

    [JsonPropertyName("fullPage")]
    public bool FullPage { get; set; } = true;

    [JsonPropertyName("blockBanners")]
    public bool BlockBanners { get; set; } = true;

    [JsonPropertyName("blockAds")]
    public bool BlockAds { get; set; } = false;

    [JsonPropertyName("darkMode")]
    public bool DarkMode { get; set; } = false;
}

public interface IScreenshotService
{
    Task<byte[]> TakeScreenshotAsync(ScreenshotOptions options);
    Task SaveScreenshotAsync(ScreenshotOptions options, string filePath);
}

public class PageBoltScreenshotService : IScreenshotService
{
    private readonly HttpClient _httpClient;
    private readonly string _apiKey;
    private readonly ILogger<PageBoltScreenshotService> _logger;
    private const string BaseUrl = "https://api.pagebolt.dev/v1";

    public PageBoltScreenshotService(
        HttpClient httpClient,
        ILogger<PageBoltScreenshotService> logger,
        string apiKey)
    {
        _httpClient = httpClient;
        _logger = logger;
        _apiKey = apiKey;
    }

    public async Task<byte[]> TakeScreenshotAsync(ScreenshotOptions options)
    {
        if (string.IsNullOrEmpty(options.Url))
            throw new ArgumentException("URL is required");

        var content = new StringContent(
            System.Text.Json.JsonSerializer.Serialize(options),
            System.Text.Encoding.UTF8,
            "application/json"
        );

        var request = new HttpRequestMessage(HttpMethod.Post, $"{BaseUrl}/screenshot")
        {
            Content = content,
            Headers = { { "Authorization", $"Bearer {_apiKey}" } }
        };

        var response = await _httpClient.SendAsync(request);

        if (!response.IsSuccessStatusCode)
        {
            _logger.LogError($"Screenshot API error: {response.StatusCode}");
            throw new Exception($"API error: {response.StatusCode}");
        }

        return await response.Content.ReadAsByteArrayAsync();
    }

    public async Task SaveScreenshotAsync(ScreenshotOptions options, string filePath)
    {
        var imageBytes = await TakeScreenshotAsync(options);
        await File.WriteAllBytesAsync(filePath, imageBytes);
        _logger.LogInformation($"Screenshot saved to {filePath}");
    }
}
Enter fullscreen mode Exit fullscreen mode

ASP.NET Core Integration

Register in dependency injection:

// Program.cs
builder.Services.AddHttpClient<IScreenshotService, PageBoltScreenshotService>()
    .ConfigureHttpClient(client =>
    {
        client.Timeout = TimeSpan.FromSeconds(30);
    });

builder.Services.AddScoped(provider =>
    new PageBoltScreenshotService(
        provider.GetRequiredService<HttpClient>(),
        provider.GetRequiredService<ILogger<PageBoltScreenshotService>>(),
        Environment.GetEnvironmentVariable("PAGEBOLT_API_KEY")
    )
);

var app = builder.Build();

// Controller
[ApiController]
[Route("api/[controller]")]
public class ScreenshotsController : ControllerBase
{
    private readonly IScreenshotService _screenshotService;

    public ScreenshotsController(IScreenshotService screenshotService)
    {
        _screenshotService = screenshotService;
    }

    [HttpPost]
    public async Task<IActionResult> TakeScreenshot([FromQuery] string url)
    {
        try
        {
            var options = new ScreenshotOptions { Url = url, FullPage = true };
            var imageBytes = await _screenshotService.TakeScreenshotAsync(options);

            return File(imageBytes, "image/png");
        }
        catch (Exception ex)
        {
            return StatusCode(500, ex.Message);
        }
    }
}

app.MapControllers();
app.Run();
Enter fullscreen mode Exit fullscreen mode

Test it:

curl -X POST "http://localhost:5000/api/screenshots?url=https://example.com" \
  > screenshot.png
Enter fullscreen mode Exit fullscreen mode

Real Use Case: Background Job Service

Generate screenshots in background tasks using Hangfire:

using Hangfire;

public class ReportService
{
    private readonly IScreenshotService _screenshotService;
    private readonly IBackgroundJobClient _jobClient;

    public ReportService(
        IScreenshotService screenshotService,
        IBackgroundJobClient jobClient)
    {
        _screenshotService = screenshotService;
        _jobClient = jobClient;
    }

    public void GenerateReportScreenshot(long reportId, string reportUrl)
    {
        // Queue background job
        _jobClient.Enqueue(() => GenerateScreenshotJob(reportId, reportUrl));
    }

    [AutomaticRetry(Attempts = 3)]
    public async Task GenerateScreenshotJob(long reportId, string reportUrl)
    {
        var outputPath = Path.Combine("reports", $"report-{reportId}.png");

        try
        {
            var options = new ScreenshotOptions { Url = reportUrl, FullPage = true };
            await _screenshotService.SaveScreenshotAsync(options, outputPath);

            // Update database
            // await _reportRepository.UpdateScreenshotPathAsync(reportId, outputPath);
        }
        catch (Exception ex)
        {
            throw; // Hangfire will retry
        }
    }
}

// In Startup/Program.cs
app.UseHangfireDashboard();
app.UseHangfireServer();
Enter fullscreen mode Exit fullscreen mode

NuGet Installation

No additional packages needed for .NET 6+. HttpClient and System.Text.Json are built-in.

For older .NET Framework projects:

<ItemGroup>
    <PackageReference Include="System.Net.Http" Version="4.3.4" />
    <PackageReference Include="System.Text.Json" Version="6.0.0" />
</ItemGroup>
Enter fullscreen mode Exit fullscreen mode

Error Handling and Retry Logic

public class PageBoltScreenshotService : IScreenshotService
{
    public async Task<byte[]> TakeScreenshotWithRetryAsync(
        ScreenshotOptions options,
        int maxRetries = 3)
    {
        for (int attempt = 1; attempt <= maxRetries; attempt++)
        {
            try
            {
                return await TakeScreenshotAsync(options);
            }
            catch (HttpRequestException ex) when (attempt < maxRetries)
            {
                // Exponential backoff
                var delayMs = 1000 * (int)Math.Pow(2, attempt - 1);
                _logger.LogWarning($"Retry {attempt}/{maxRetries} after {delayMs}ms");
                await Task.Delay(delayMs);
            }
        }

        throw new Exception("Max retries exceeded");
    }
}
Enter fullscreen mode Exit fullscreen mode

Testing: Mock the Service

using Moq;
using Xunit;

public class ScreenshotServiceTests
{
    [Fact]
    public async Task TakeScreenshot_ReturnsBytes()
    {
        // Arrange
        var mockHttpClientFactory = new Mock<IHttpClientFactory>();
        var mockLogger = new Mock<ILogger<PageBoltScreenshotService>>();
        var service = new PageBoltScreenshotService(
            new HttpClient(),
            mockLogger.Object,
            "test-key"
        );

        var options = new ScreenshotOptions { Url = "https://example.com" };

        // Act & Assert
        // Use a test HTTP handler or mock HttpClient
        var result = await service.TakeScreenshotAsync(options);
        Assert.NotNull(result);
        Assert.NotEmpty(result);
    }
}
Enter fullscreen mode Exit fullscreen mode

Pricing

Plan Requests/Month Cost Best For
Free 100 $0 Learning, low-volume projects
Starter 5,000 $29 Small teams, moderate use
Growth 25,000 $79 Production apps, frequent calls
Scale 100,000 $199 High-volume automation

Summary

  • ✅ Idiomatic C# with HttpClient and async/await
  • ✅ Binary response handling with ReadAsByteArrayAsync()
  • ✅ File writing with File.WriteAllBytesAsync()
  • ✅ ASP.NET Core dependency injection
  • ✅ Background job integration (Hangfire)
  • ✅ Error handling and retry logic
  • ✅ No browser management
  • ✅ Works in web apps, Windows services, Azure Functions

Get started free: pagebolt.dev — 100 requests/month, no credit card required.

Top comments (0)