Learnixo
Back to blog
Backend Systemsbeginner

14 Must-Know Libraries in .NET Every Developer Should Use

A practical guide to the 14 essential .NET libraries: EF Core, Swagger, Serilog, Hangfire, Polly, StackExchange.Redis, Refit, Mapster, System.Text.Json, NSubstitute, xUnit, BenchmarkDotNet, and more — with real code for each.

LearnixoJune 4, 202614 min read
.NETC#LibrariesEF CoreSerilogPollyHangfireRedisRefitxUnit
Share:𝕏

Why These Libraries Matter

Every professional .NET developer relies on a set of battle-tested libraries that solve common problems better than hand-rolling solutions. These 14 libraries — curated by Aram Tchekrekjian (Microsoft MVP, @AramT87) — cover the full stack: data access, logging, resilience, background jobs, testing, caching, and performance.

Master these and you'll be productive in any .NET codebase.


1. Entity Framework Core

What it does: Comprehensive ORM that supports LINQ, change tracking, migrations, raw SQL, and multiple database providers.

Bash
dotnet add package Microsoft.EntityFrameworkCore.SqlServer
dotnet add package Microsoft.EntityFrameworkCore.Tools
C#
// Define your model
public class Product
{
    public int     Id       { get; set; }
    public string  Name     { get; set; } = "";
    public decimal Price    { get; set; }
    public int     CategoryId { get; set; }
    public Category Category { get; set; } = null!;
}

// DbContext
public class AppDbContext : DbContext
{
    public AppDbContext(DbContextOptions<AppDbContext> opts) : base(opts) { }
    public DbSet<Product> Products => Set<Product>();
}

// Register
builder.Services.AddDbContext<AppDbContext>(opts =>
    opts.UseSqlServer(builder.Configuration.GetConnectionString("Default")));

// Query
var expensiveProducts = await db.Products
    .Include(p => p.Category)
    .Where(p => p.Price > 50)
    .OrderBy(p => p.Name)
    .ToListAsync();

// Migrations
// dotnet ef migrations add InitialCreate
// dotnet ef database update

Why use it: You write C# instead of SQL. Change tracking means you only UPDATE the fields you modify. LINQ queries are type-safe and composable.


2. Swashbuckle.Swagger (OpenAPI)

What it does: Generates and enables configuration of OpenAPI specifications for API discovery, interactive testing, and client code generation.

Bash
dotnet add package Swashbuckle.AspNetCore
C#
// Program.cs
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen(options =>
{
    options.SwaggerDoc("v1", new OpenApiInfo
    {
        Title       = "OrderFlow API",
        Version     = "v1",
        Description = "Order management REST API",
        Contact     = new OpenApiContact { Email = "api@orderflow.com" }
    });

    // Add JWT auth to Swagger UI
    options.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme
    {
        Type   = SecuritySchemeType.Http,
        Scheme = "bearer",
        BearerFormat = "JWT"
    });
    options.AddSecurityRequirement(new OpenApiSecurityRequirement
    {
        [new OpenApiSecurityScheme
        {
            Reference = new OpenApiReference { Type = ReferenceType.SecurityScheme, Id = "Bearer" }
        }] = Array.Empty<string>()
    });

    // Include XML comments from /// summaries
    var xmlFile = $"{Assembly.GetExecutingAssembly().GetName().Name}.xml";
    options.IncludeXmlComments(Path.Combine(AppContext.BaseDirectory, xmlFile));
});

app.UseSwagger();
app.UseSwaggerUI(c => c.SwaggerEndpoint("/swagger/v1/swagger.json", "OrderFlow API v1"));

// Document an endpoint
/// <summary>Get an order by ID</summary>
/// <param name="id">The order UUID</param>
/// <response code="200">Order found</response>
/// <response code="404">Order not found</response>
[HttpGet("{id:guid}")]
[ProducesResponseType(typeof(OrderDto), 200)]
[ProducesResponseType(404)]
public async Task<IActionResult> GetOrder(Guid id) { /* ... */ }

Why use it: Live interactive API docs at /swagger. Auto-generates client SDKs. No manual API documentation maintenance.


3. Serilog

What it does: Structured logging through different levels of verbosity with pluggable sinks (file, console, Seq, Elastic, Application Insights).

Bash
dotnet add package Serilog.AspNetCore
dotnet add package Serilog.Sinks.Console
dotnet add package Serilog.Sinks.File
dotnet add package Serilog.Sinks.Seq
C#
// Program.cs
Log.Logger = new LoggerConfiguration()
    .MinimumLevel.Information()
    .MinimumLevel.Override("Microsoft", LogEventLevel.Warning)
    .Enrich.FromLogContext()
    .Enrich.WithMachineName()
    .Enrich.WithThreadId()
    .WriteTo.Console(outputTemplate:
        "[{Timestamp:HH:mm:ss} {Level:u3}] {Message:lj} {Properties:j}{NewLine}{Exception}")
    .WriteTo.File("logs/app-.log",
        rollingInterval:  RollingInterval.Day,
        retainedFileCountLimit: 30)
    .WriteTo.Seq("http://localhost:5341")  // searchable log UI
    .CreateLogger();

builder.Host.UseSerilog();

// Usage — structured logging (not string interpolation!)
_logger.LogInformation("Order {OrderId} created for customer {CustomerId} — total {Total:C}",
    order.Id, order.CustomerId, order.Total);

// Enriching with context
using (_logger.BeginScope(new { OrderId = order.Id, UserId = userId }))
{
    _logger.LogInformation("Starting payment processing");
    await _paymentService.ChargeAsync(order.Total);
    _logger.LogInformation("Payment complete");
}

Why use it: Every log entry becomes a queryable object. Filter by OrderId, UserId, or any property in Seq. Structured logs scale; string logs don't.


4. Hangfire

What it does: Create different types of background jobs including fire-and-forget, delayed, recurring batches, and continuations — with a built-in dashboard.

Bash
dotnet add package Hangfire.AspNetCore
dotnet add package Hangfire.SqlServer
C#
// Program.cs
builder.Services.AddHangfire(config => config
    .SetDataCompatibilityLevel(CompatibilityLevel.Version_180)
    .UseSimpleAssemblyNameTypeSerializer()
    .UseRecommendedSerializerSettings()
    .UseSqlServerStorage(builder.Configuration.GetConnectionString("Hangfire")));

builder.Services.AddHangfireServer();
app.UseHangfireDashboard("/hangfire"); // dashboard at /hangfire

// 1. Fire-and-forget — runs once, ASAP
BackgroundJob.Enqueue<IEmailService>(email =>
    email.SendOrderConfirmationAsync(orderId, CancellationToken.None));

// 2. Delayed — runs after a delay
BackgroundJob.Schedule<IReportService>(
    report => report.GenerateMonthlyReportAsync(CancellationToken.None),
    TimeSpan.FromMinutes(30));

// 3. Recurring — runs on a schedule (cron)
RecurringJob.AddOrUpdate<IOrderCleanupService>(
    "cleanup-expired-orders",
    service => service.CleanupAsync(CancellationToken.None),
    Cron.Daily(3, 0)); // 3am every day

// 4. Continuation — runs after another job completes
var jobId = BackgroundJob.Enqueue<IOrderService>(s => s.ProcessAsync(orderId));
BackgroundJob.ContinueJobWith<INotificationService>(jobId,
    n => n.NotifyCustomerAsync(orderId));

// 5. Batch — multiple jobs as a unit
var batchId = BatchJob.StartNew(batch =>
{
    batch.Enqueue<IReportService>(s => s.GeneratePartAAsync());
    batch.Enqueue<IReportService>(s => s.GeneratePartBAsync());
});
BatchJob.ContinueWith(batchId, batch =>
    batch.Enqueue<IReportService>(s => s.MergeReportsAsync()));

Why use it: Persistent background jobs (survive server restarts), built-in retry with exponential backoff, visual dashboard showing job history.


5. Polly

What it does: Make your app resilient to transient faults with strategies including retry, timeout, circuit breaker, and bulkhead.

Bash
dotnet add package Microsoft.Extensions.Http.Resilience
C#
// Standard resilience handler for HttpClient
builder.Services.AddHttpClient<IProductService, ProductService>()
    .AddStandardResilienceHandler(options =>
    {
        options.Retry.MaxRetryAttempts = 3;
        options.Retry.Delay            = TimeSpan.FromMilliseconds(200);
        options.Retry.BackoffType      = DelayBackoffType.Exponential;
        options.CircuitBreaker.BreakDuration    = TimeSpan.FromSeconds(30);
        options.CircuitBreaker.FailureRatio     = 0.5;
        options.TotalRequestTimeout.Timeout     = TimeSpan.FromSeconds(10);
    });

// Custom resilience pipeline
var pipeline = new ResiliencePipelineBuilder()
    .AddRetry(new RetryStrategyOptions
    {
        MaxRetryAttempts = 3,
        Delay            = TimeSpan.FromSeconds(1),
        BackoffType      = DelayBackoffType.Exponential,
        OnRetry          = args =>
        {
            _logger.LogWarning("Retry {Attempt} after {Delay}ms",
                args.AttemptNumber, args.RetryDelay.TotalMilliseconds);
            return ValueTask.CompletedTask;
        }
    })
    .AddCircuitBreaker(new CircuitBreakerStrategyOptions
    {
        FailureRatio     = 0.5,
        SamplingDuration = TimeSpan.FromSeconds(60),
        BreakDuration    = TimeSpan.FromSeconds(30),
        OnOpened = args =>
        {
            _logger.LogError("Circuit breaker opened!");
            return ValueTask.CompletedTask;
        }
    })
    .AddTimeout(TimeSpan.FromSeconds(5))
    .Build();

await pipeline.ExecuteAsync(async ct => await CallExternalServiceAsync(ct));

Why use it: Without Polly, one slow downstream service takes your entire app down. With Polly, transient failures are handled automatically and cascading failures are stopped at the circuit breaker.


6. StackExchange.Redis

What it does: Leverage the distributed in-memory caching, streaming, and messaging of Redis — supports sync and async operations.

Bash
dotnet add package StackExchange.Redis
dotnet add package Microsoft.Extensions.Caching.StackExchangeRedis
C#
// Register
builder.Services.AddStackExchangeRedisCache(options =>
    options.Configuration = builder.Configuration.GetConnectionString("Redis"));

// IDistributedCache — simple string/byte cache
public class ProductCacheService
{
    private readonly IDistributedCache _cache;

    public async Task<Product?> GetProductAsync(int id, CancellationToken ct)
    {
        var key   = $"product:{id}";
        var bytes = await _cache.GetAsync(key, ct);

        if (bytes is not null)
            return JsonSerializer.Deserialize<Product>(bytes);

        var product = await _db.Products.FindAsync(id, ct);
        if (product is null) return null;

        await _cache.SetAsync(key,
            JsonSerializer.SerializeToUtf8Bytes(product),
            new DistributedCacheEntryOptions
            {
                AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(10)
            }, ct);

        return product;
    }

    public async Task InvalidateAsync(int id, CancellationToken ct)
        => await _cache.RemoveAsync($"product:{id}", ct);
}

// IConnectionMultiplexer — direct Redis access (pub/sub, transactions, Lua)
builder.Services.AddSingleton<IConnectionMultiplexer>(
    ConnectionMultiplexer.Connect(builder.Configuration.GetConnectionString("Redis")!));

public class OrderEventPublisher
{
    private readonly IConnectionMultiplexer _redis;

    public async Task PublishOrderCreatedAsync(Guid orderId)
    {
        var db = _redis.GetDatabase();
        await db.PublishAsync("order-events",
            JsonSerializer.Serialize(new { type = "OrderCreated", orderId }));
    }
}

Why use it: Redis is 100–1000x faster than SQL for cached reads. Essential for high-traffic APIs to reduce database load and latency.


7. Refit

What it does: Build rich HTTP interfaces leveraging attributes to call and manage RESTful APIs — delegating handlers and intercept requests/responses.

Bash
dotnet add package Refit
dotnet add package Refit.HttpClientFactory
C#
// Define the API contract as an interface — Refit generates the implementation
public interface IProductsApi
{
    [Get("/api/products")]
    Task<List<ProductDto>> GetAllAsync(CancellationToken ct = default);

    [Get("/api/products/{id}")]
    Task<ProductDto> GetByIdAsync(int id, CancellationToken ct = default);

    [Post("/api/products")]
    Task<ProductDto> CreateAsync([Body] CreateProductRequest request, CancellationToken ct = default);

    [Put("/api/products/{id}")]
    Task<ProductDto> UpdateAsync(int id, [Body] UpdateProductRequest request, CancellationToken ct = default);

    [Delete("/api/products/{id}")]
    Task DeleteAsync(int id, CancellationToken ct = default);

    // Query parameters
    [Get("/api/products")]
    Task<PagedResult<ProductDto>> SearchAsync([Query] string? name, [Query] int page = 1, CancellationToken ct = default);

    // Headers
    [Get("/api/products")]
    Task<List<ProductDto>> GetWithHeaderAsync([Header("X-Tenant-Id")] string tenantId, CancellationToken ct = default);
}

// Register
builder.Services.AddRefitClient<IProductsApi>()
    .ConfigureHttpClient(c => c.BaseAddress = new Uri("https://api.products.com"))
    .AddStandardResilienceHandler(); // Polly retry built in

// Use — no manual HttpClient code
public class OrderService
{
    private readonly IProductsApi _productsApi;

    public async Task<bool> ValidateProductsAsync(IEnumerable<Guid> productIds, CancellationToken ct)
    {
        var products = await _productsApi.GetAllAsync(ct);
        return productIds.All(id => products.Any(p => p.Id == id));
    }
}

Why use it: Zero boilerplate HTTP client code. Change the URL or add a header in the interface, not in every call site. Pairs perfectly with Polly for resilience.


8. Mapster

What it does: Performant mapping library that supports FluentAPI, Nested Mappings, and Immutable Collections.

Bash
dotnet add package Mapster
dotnet add package MapsterMapper
C#
// Simple mapping — works with zero configuration for matching property names
var productDto = product.Adapt<ProductDto>();
var products   = productList.Adapt<List<ProductDto>>();

// Configuration — register once at startup
TypeAdapterConfig<Product, ProductDto>
    .NewConfig()
    .Map(dest => dest.CategoryName, src => src.Category.Name)
    .Map(dest => dest.FormattedPrice, src => $"{src.Price:C}")
    .Ignore(dest => dest.InternalNotes);

// Inject IMapper via DI
builder.Services.AddMapster();

public class ProductService
{
    private readonly IMapper _mapper;

    public ProductDto GetProduct(Product product)
        => _mapper.Map<ProductDto>(product);

    public List<ProductDto> GetProducts(IEnumerable<Product> products)
        => _mapper.Map<List<ProductDto>>(products);
}

// Projection — push mapping to SQL query (no materialise then map)
var dtos = await db.Products
    .Where(p => p.IsActive)
    .ProjectToType<ProductDto>()  // SELECT only mapped columns
    .ToListAsync();

Why use it: Mapster is typically 2–5x faster than AutoMapper. Projection to SQL means you only fetch the columns your DTO needs.


9. System.Text.Json

What it does: .NET's built-in library for blazing fast JSON processing, serialisation, and deserialisation — no external dependency needed.

C#
// Basic serialisation
var product = new Product { Id = 1, Name = "Widget", Price = 9.99m };
string json = JsonSerializer.Serialize(product);
var restored = JsonSerializer.Deserialize<Product>(json);

// Options
var options = new JsonSerializerOptions
{
    WriteIndented          = true,
    PropertyNamingPolicy   = JsonNamingPolicy.CamelCase,
    DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
    Converters             = { new JsonStringEnumConverter() }
};

// Attributes
public class Product
{
    [JsonPropertyName("product_id")]    // custom JSON property name
    public int Id { get; set; }

    [JsonIgnore]                        // never serialise
    public string InternalCode { get; set; } = "";

    [JsonInclude]                       // include private field
    private string _secret = "";
}

// Source generation — for AOT and better performance
[JsonSerializable(typeof(Product))]
[JsonSerializable(typeof(List<Product>))]
[JsonSourceGenerationOptions(
    PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase,
    WriteIndented = true)]
internal partial class AppJsonContext : JsonSerializerContext { }

// Use source-generated context
string json = JsonSerializer.Serialize(product, AppJsonContext.Default.Product);

// Stream serialisation — no intermediate string allocation
await using var stream = File.OpenWrite("products.json");
await JsonSerializer.SerializeAsync(stream, products, options);

// Partial read — JsonDocument for partial access without full deserialisation
using var doc = JsonDocument.Parse(json);
string name = doc.RootElement.GetProperty("name").GetString()!;

Why use it: Zero external dependency. Source generation eliminates reflection — required for Native AOT. Best performance for high-throughput APIs.


10. NSubstitute

What it does: Easily build mocks and substitutes to allow writing flexible Arrange-Act-Assert unit tests with ease.

Bash
dotnet add package NSubstitute
C#
// Create a substitute (mock)
var orderRepo = Substitute.For<IOrderRepository>();
var emailSvc  = Substitute.For<IEmailService>();

// Arrange — set up return values
orderRepo.GetByIdAsync(Arg.Any<Guid>(), Arg.Any<CancellationToken>())
    .Returns(new Order { Id = Guid.NewGuid(), Status = OrderStatus.Pending });

// Arrange — throw an exception
emailSvc.SendConfirmationAsync(Arg.Any<string>(), Arg.Any<Guid>(), Arg.Any<CancellationToken>())
    .Returns(Task.FromException(new SmtpException("SMTP unavailable")));

// Act
var service = new OrderService(orderRepo, emailSvc);
await service.SubmitOrderAsync(orderId, CancellationToken.None);

// Assert — verify calls
await orderRepo.Received(1).UpdateAsync(
    Arg.Is<Order>(o => o.Status == OrderStatus.Submitted),
    Arg.Any<CancellationToken>());

await emailSvc.Received(1).SendConfirmationAsync(
    Arg.Any<string>(),
    Arg.Is<Guid>(id => id == orderId),
    Arg.Any<CancellationToken>());

// Assert — verify NOT called
emailSvc.DidNotReceive().SendAsync(Arg.Any<string>(), Arg.Any<string>());

// Capture arguments
Order? capturedOrder = null;
await orderRepo.UpdateAsync(
    Arg.Do<Order>(o => capturedOrder = o),
    Arg.Any<CancellationToken>());
Assert.Equal(OrderStatus.Submitted, capturedOrder?.Status);

Why use it: Cleaner syntax than Moq. Intuitive Received() / DidNotReceive() assertions. Works great with xUnit.


11. xUnit.net

What it does: Supports TDD approach with tests designed for Facts and Theories — including ranges of input data.

Bash
dotnet add package xunit
dotnet add package xunit.runner.visualstudio
dotnet add package Microsoft.NET.Test.Sdk
C#
public class OrderTests
{
    // Fact — single test case
    [Fact]
    public void Submit_PendingOrder_ChangesStatusToSubmitted()
    {
        var order = Order.Create(Guid.NewGuid(),
            new[] { new OrderLine(Guid.NewGuid(), 1, 10m) });

        order.Submit();

        Assert.Equal(OrderStatus.Submitted, order.Status);
    }

    [Fact]
    public void Submit_EmptyOrder_ThrowsDomainException()
    {
        var order = Order.Create(Guid.NewGuid(), Enumerable.Empty<OrderLine>());

        var ex = Assert.Throws<DomainException>(() => order.Submit());
        Assert.Contains("empty", ex.Message, StringComparison.OrdinalIgnoreCase);
    }

    // Theory — multiple test cases with [InlineData]
    [Theory]
    [InlineData(0,    false)]  // zero — invalid
    [InlineData(-1,   false)]  // negative — invalid
    [InlineData(1,    true)]   // one — valid
    [InlineData(100,  true)]   // hundred — valid
    [InlineData(1001, false)]  // over limit — invalid
    public void AddLine_QuantityValidation(int quantity, bool expectedValid)
    {
        var order = Order.Create(Guid.NewGuid(), Enumerable.Empty<OrderLine>());

        if (expectedValid)
        {
            order.AddLine(Guid.NewGuid(), quantity, 10m); // should not throw
            Assert.Single(order.Lines);
        }
        else
        {
            Assert.Throws<DomainException>(() =>
                order.AddLine(Guid.NewGuid(), quantity, 10m));
        }
    }

    // Theory with [MemberData] — complex objects as test data
    public static IEnumerable<object[]> InvalidOrderData =>
    [
        [Guid.Empty, "Customer ID cannot be empty"],
        [Guid.NewGuid(), "Order must have at least one line"]
    ];

    [Theory]
    [MemberData(nameof(InvalidOrderData))]
    public void Create_InvalidData_ThrowsDomainException(Guid customerId, string expectedMessage)
    {
        var ex = Assert.Throws<DomainException>(() =>
            Order.Create(customerId, Enumerable.Empty<OrderLine>()));
        Assert.Contains(expectedMessage, ex.Message);
    }
}

Why use it: Parallel test execution by default. [Theory] + [InlineData] eliminates duplicated test methods. Clean, no base class noise.


12. BenchmarkDotNet

What it does: Easily measure the performance of your functions by turning them into benchmarks using rich attributes.

Bash
dotnet add package BenchmarkDotNet
C#
[MemoryDiagnoser]           // shows memory allocations
[SimpleJob(RuntimeMoniker.Net90)]
public class SerializationBenchmarks
{
    private Product _product = null!;
    private string  _json    = null!;

    [GlobalSetup]
    public void Setup()
    {
        _product = new Product { Id = 1, Name = "Widget", Price = 9.99m };
        _json    = JsonSerializer.Serialize(_product);
    }

    [Benchmark(Baseline = true)]
    public string SystemTextJson_Serialise()
        => JsonSerializer.Serialize(_product);

    [Benchmark]
    public string NewtonsoftJson_Serialise()
        => Newtonsoft.Json.JsonConvert.SerializeObject(_product);

    [Benchmark]
    public Product? SystemTextJson_Deserialise()
        => JsonSerializer.Deserialize<Product>(_json);

    [Params(100, 1000, 10000)]  // test with different input sizes
    public int N;

    [Benchmark]
    public List<int> LinqSelect()
        => Enumerable.Range(0, N).Select(x => x * 2).ToList();

    [Benchmark]
    public List<int> ForLoopSelect()
    {
        var result = new List<int>(N);
        for (int i = 0; i < N; i++) result.Add(i * 2);
        return result;
    }
}

// Run
// dotnet run -c Release
BenchmarkRunner.Run<SerializationBenchmarks>();

Sample output:

| Method                     |      Mean |  Allocated |
|--------------------------- |----------:|------------|
| SystemTextJson_Serialise   | 145.3 ns  |      256 B |
| NewtonsoftJson_Serialise   | 847.2 ns  |      816 B |
| SystemTextJson_Deserialise | 198.4 ns  |      312 B |

Why use it: Never guess about performance. Benchmark first, optimise based on data. BenchmarkDotNet handles JIT warmup, statistical significance, and GC pressure measurement.


Quick Reference Table

| # | Library | Purpose | NuGet Package | |---|---|---|---| | 1 | Entity Framework Core | ORM — LINQ queries, migrations, change tracking | Microsoft.EntityFrameworkCore | | 2 | Swashbuckle.Swagger | OpenAPI docs + Swagger UI | Swashbuckle.AspNetCore | | 3 | Serilog | Structured logging with multiple sinks | Serilog.AspNetCore | | 4 | Hangfire | Background jobs — fire-and-forget, recurring, batches | Hangfire.AspNetCore | | 5 | Polly | Resilience — retry, circuit breaker, timeout, bulkhead | Microsoft.Extensions.Http.Resilience | | 6 | StackExchange.Redis | Distributed caching, pub/sub, streams | StackExchange.Redis | | 7 | Refit | Type-safe HTTP clients from interfaces | Refit.HttpClientFactory | | 8 | Mapster | Fast object-to-object mapping with projections | Mapster | | 9 | System.Text.Json | Built-in JSON serialisation, AOT-compatible | Built-in | | 10 | NSubstitute | Mocking framework for unit tests | NSubstitute | | 11 | xUnit.net | TDD test framework — Facts, Theories, InlineData | xunit | | 12 | BenchmarkDotNet | Performance benchmarking with statistical accuracy | BenchmarkDotNet |


Which Libraries Go Together

Web API project:

EF Core + Serilog + Polly + Refit + Mapster + Swashbuckle + StackExchange.Redis

Background worker:

Hangfire + Serilog + Polly + EF Core

Test project:

xUnit + NSubstitute + BenchmarkDotNet (separate perf project)

Interview Questions

Q: What is the difference between Polly retry and circuit breaker? Retry re-attempts a failed operation immediately or after a delay — good for transient blips. Circuit breaker tracks the failure rate over a time window; if it exceeds a threshold, it "opens" and stops all calls for a break period — preventing cascade failures when a downstream service is genuinely down. Use both together: retry for transient faults, circuit breaker for sustained failures.

Q: Why use Refit instead of HttpClient directly? Refit generates the HttpClient implementation from an interface. You express the API contract once (URL, method, parameters, headers) and get a strongly-typed client. No manual GetAsync, PostAsync, response reading, or error handling per call. Swap the base URL in one place. Mock the interface in tests.

Q: What is the difference between IMemoryCache and StackExchange.Redis? IMemoryCache is in-process — fast (nanoseconds), but lost on restart and not shared across multiple instances. StackExchange.Redis is external — millisecond latency, survives restarts, shared across all instances. Use IMemoryCache for single-instance or dev; use Redis for production multi-instance APIs.

Q: What is a BenchmarkDotNet MemoryDiagnoser? An attribute that measures memory allocations per benchmark operation — reports bytes allocated and GC collection counts. Essential for identifying high-allocation hot paths where Span<T>, ArrayPool, or struct changes could reduce GC pressure.

Q: When would you use NSubstitute over Moq? Personal preference mostly — both are excellent. NSubstitute has cleaner fluent syntax (Received() vs Verify()), doesn't require lambda setups for void methods, and is slightly easier for beginners. Moq has wider adoption and more Stack Overflow answers. Either is a solid choice.

Enjoyed this article?

Explore the Backend Systems learning path for more.

Found this helpful?

Share:𝕏

Leave a comment

Have a question, correction, or just found this helpful? Leave a note below.