Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/masastack/MASA.Framework/llms.txt

Use this file to discover all available pages before exploring further.

Caller is MASA Framework’s typed HTTP client abstraction. It wraps HttpClient or Dapr’s service-invocation API behind a consistent ICaller interface, giving your application services a single, testable way to call downstream microservices — with automatic JSON serialisation, configurable error handling, Bearer token forwarding, and support for named multi-client registrations.

Installation

Choose the transport that matches your infrastructure:
# Plain HttpClient transport
dotnet add package Masa.Contrib.Service.Caller.HttpClient

# Dapr service invocation transport
dotnet add package Masa.Contrib.Service.Caller.DaprClient

# Optional: automatic Bearer token forwarding for ASP.NET Core
dotnet add package Masa.Contrib.Service.Caller.Authentication.AspNetCore

The ICaller Interface

ICaller is the central abstraction. Once injected, all downstream calls go through it regardless of the underlying transport.

Standard HTTP Methods

// GET
Task<TResponse?> GetAsync<TResponse>(string methodName,
    CancellationToken cancellationToken = default);

Task<TResponse?> GetAsync<TRequest, TResponse>(string methodName, TRequest data,
    CancellationToken cancellationToken = default);

// POST
Task<TResponse?> PostAsync<TRequest, TResponse>(string methodName, TRequest data,
    CancellationToken cancellationToken = default);

Task PostAsync<TRequest>(string methodName, TRequest data,
    CancellationToken cancellationToken = default);

// PUT
Task<TResponse?> PutAsync<TRequest, TResponse>(string methodName, TRequest data,
    CancellationToken cancellationToken = default);

// DELETE
Task<TResponse?> DeleteAsync<TRequest, TResponse>(string methodName, TRequest data,
    CancellationToken cancellationToken = default);

// PATCH
Task<TResponse?> PatchAsync<TRequest, TResponse>(string methodName, TRequest data,
    CancellationToken cancellationToken = default);

Raw and gRPC Variants

// Send a pre-built HttpRequestMessage
Task<TResponse?> SendAsync<TResponse>(HttpRequestMessage request,
    CancellationToken cancellationToken = default);

// Send with explicit method, path, and body
Task<TResponse?> SendAsync<TResponse>(HttpMethod method, string methodName,
    HttpContent? content, bool autoThrowException = true,
    CancellationToken cancellationToken = default);

// gRPC (Dapr transport)
Task<TResponse?> SendGrpcAsync<TRequest, TResponse>(string methodName,
    TRequest request, CancellationToken cancellationToken = default);

// Response as primitive types
Task<string?> GetStringAsync(string methodName, ...);
Task<byte[]?> GetByteArrayAsync(string methodName, ...);
Task<Stream?> GetStreamAsync(string methodName, ...);
The autoThrowException parameter (defaults to true) causes ICaller to throw a UserFriendlyException or MasaException when the HTTP response status indicates failure. Set it to false to inspect the raw response yourself.

Defining a Typed Caller

Extend CallerBase and override UseCallerExtension() to bind your service-specific base address and transport. Methods on the class delegate to the protected Caller property:

HttpClient Caller

public class OrderCaller : CallerBase
{
    public override void UseCallerExtension()
        => CallerBuilder.UseHttpClient(client =>
        {
            client.BaseAddress = new Uri("http://order-service");
            client.Timeout = TimeSpan.FromSeconds(30);
        });

    public Task<List<Order>?> GetOrdersAsync()
        => Caller.GetAsync<List<Order>>("/api/v1/orders");

    public Task<Order?> GetOrderAsync(Guid id)
        => Caller.GetAsync<Order>($"/api/v1/orders/{id}");

    public Task<Order?> CreateOrderAsync(CreateOrderRequest request)
        => Caller.PostAsync<CreateOrderRequest, Order>("/api/v1/orders", request);

    public Task UpdateOrderAsync(Guid id, UpdateOrderRequest request)
        => Caller.PutAsync<UpdateOrderRequest>($"/api/v1/orders/{id}", request);

    public Task DeleteOrderAsync(Guid id)
        => Caller.DeleteAsync($"/api/v1/orders/{id}");
}

Dapr Caller

When using Dapr, replace UseHttpClient with UseDaprClient. The app ID replaces the base address — Dapr’s sidecar handles service discovery and mTLS:
public class InventoryCaller : CallerBase
{
    public override void UseCallerExtension()
        => CallerBuilder.UseDaprClient("inventory-service");

    public Task<int?> GetStockAsync(string productId)
        => Caller.GetAsync<int>($"/api/v1/inventory/{productId}");

    public Task ReserveStockAsync(ReserveStockRequest request)
        => Caller.PostAsync("/api/v1/inventory/reserve", request);

    public Task<int?> GetStockGrpcAsync(string productId)
        => Caller.SendGrpcAsync<string, int>("GetStock", productId);
}

Registration

Register callers in Program.cs. The framework automatically discovers CallerBase subclasses or you can register them explicitly:
// Auto-discover all CallerBase subclasses in the assembly
builder.Services.AddCaller(Assembly.GetEntryAssembly()!);

// Or register explicitly
builder.Services.AddCaller(callerBuilder =>
{
    callerBuilder.AddCaller<OrderCaller>();
    callerBuilder.AddCaller<InventoryCaller>();
});

Named Callers via ICallerFactory

When you have multiple downstream services, use ICallerFactory to resolve a specific caller by name at runtime:
// Registration with a name
builder.Services.AddCaller("order", callerBuilder =>
    callerBuilder.UseHttpClient(client =>
        client.BaseAddress = new Uri("http://order-service")));

builder.Services.AddCaller("inventory", callerBuilder =>
    callerBuilder.UseHttpClient(client =>
        client.BaseAddress = new Uri("http://inventory-service")));

// Resolution at runtime
public class CheckoutService : ServiceBase
{
    private readonly ICallerFactory _callerFactory;

    public CheckoutService(ICallerFactory callerFactory)
        => _callerFactory = callerFactory;

    public async Task CheckoutAsync(Guid cartId)
    {
        var orderCaller = _callerFactory.Create("order");
        var inventoryCaller = _callerFactory.Create("inventory");

        var stock = await inventoryCaller.GetAsync<int>($"/api/v1/inventory/{cartId}");
        var order = await orderCaller.PostAsync<CheckoutRequest, Order>(
            "/api/v1/orders",
            new CheckoutRequest { CartId = cartId });
    }
}

Injecting a Typed Caller

Typed callers (subclasses of CallerBase) are registered in DI and can be injected directly:
public class CartService : ServiceBase
{
    private readonly OrderCaller _orderCaller;
    private readonly InventoryCaller _inventoryCaller;

    public CartService(OrderCaller orderCaller, InventoryCaller inventoryCaller)
    {
        _orderCaller = orderCaller;
        _inventoryCaller = inventoryCaller;
    }

    public async Task CheckoutAsync(Guid cartId)
    {
        var stock = await _inventoryCaller.GetStockAsync(cartId.ToString());
        if (stock <= 0)
            throw new InvalidOperationException("Out of stock");

        var order = await _orderCaller.CreateOrderAsync(
            new CreateOrderRequest { CartId = cartId });
    }
}

Bearer Token Forwarding

Install Masa.Contrib.Service.Caller.Authentication.AspNetCore to automatically forward the current request’s Bearer token to downstream services. Register it alongside your caller:
builder.Services.AddCaller(callerBuilder =>
{
    callerBuilder.AddCaller<OrderCaller>();
    callerBuilder.UseAuthentication(); // forwards Authorization header automatically
});
When the current HTTP context carries an Authorization: Bearer <token> header, every outgoing ICaller request will include the same header — no manual token propagation required.

Request and Response Customisation

Override RequestMessageFactory or ResponseMessageFactory on your CallerBase subclass to intercept and modify requests or responses globally for that caller:
public class SignedOrderCaller : CallerBase
{
    public override void UseCallerExtension()
        => CallerBuilder.UseHttpClient(client =>
            client.BaseAddress = new Uri("http://order-service"));

    protected override void ConfigureRequest(HttpRequestMessage request)
    {
        request.Headers.Add("X-Api-Key", "secret-key");
        request.Headers.Add("X-Request-Id", Guid.NewGuid().ToString());
    }
}

Build docs developers (and LLMs) love