Skip to main content

Overview

The Intent.Application.Contracts.Clients module generates client-side service contracts for consuming your application services from other applications. This is particularly useful for microservices architectures, integration projects, and creating client SDKs.
Module: Intent.Application.Contracts.ClientsVersion: 5.1.6+Dependencies:
  • Intent.Common.CSharp
  • Intent.Modelers.Services
  • Intent.Modelers.Services.CQRS
  • Intent.Modelers.Types.ServiceProxies

Key Features

  • Client DTOs: Generate client-side DTO contracts
  • Service Proxies: Interface definitions for service clients
  • Paginated Results: Client-side pagination support
  • Type Safety: Strongly-typed contracts
  • Enum Contracts: Client-side enum definitions
  • Separation of Concerns: Keep client contracts separate from server implementation

Use Cases

Microservices

Service A consumes Service B’s contracts

Client SDKs

Distribute client libraries for your API

Integration Projects

Connect to external services

BFF Pattern

Backend-for-Frontend service integration

Installation

intent install Intent.Application.Contracts.Clients

Architecture

Typical Setup

YourSolution/
├── OrderService.Api/              # Service provider
│   ├── Application/
│   │   ├── Interfaces/           # Server contracts
│   │   │   └── IOrderService.cs
│   │   └── Orders/
│   │       └── OrderDto.cs
│   └── ...

├── Warehouse.Api/                 # Service consumer
│   ├── Application/
│   │   └── IntegrationServices/  # Client contracts
│   │       ├── Contracts/        # Client DTOs
│   │       │   ├── OrderDto.cs
│   │       │   └── OrderStatus.cs
│   │       └── IOrderServiceClient.cs
│   └── ...

└── Shared.Contracts/              # Alternative: Shared library
    └── Orders/
        ├── OrderDto.cs
        └── IOrderService.cs

Generated Components

Client DTO Contracts

// In OrderService.Api
namespace OrderService.Application.Orders
{
    public class OrderDto
    {
        public Guid Id { get; set; }
        public string OrderNumber { get; set; }
        public OrderStatus Status { get; set; }
        public decimal TotalAmount { get; set; }
        public List<OrderItemDto> Items { get; set; }
    }
}

Client Service Contract

Generated Client Interface
namespace Warehouse.Application.IntegrationServices
{
    public interface IOrderServiceClient
    {
        Task<OrderDto> GetOrderAsync(
            Guid id,
            CancellationToken cancellationToken = default);

        Task<PagedResult<OrderDto>> GetOrdersAsync(
            int pageNo,
            int pageSize,
            CancellationToken cancellationToken = default);

        Task<Guid> CreateOrderAsync(
            CreateOrderDto dto,
            CancellationToken cancellationToken = default);

        Task UpdateOrderStatusAsync(
            Guid id,
            OrderStatus status,
            CancellationToken cancellationToken = default);
    }
}

Client Enum Contracts

Generated Enum
namespace Warehouse.Application.IntegrationServices.Contracts
{
    public enum OrderStatus
    {
        Pending = 0,
        Confirmed = 1,
        Shipped = 2,
        Delivered = 3,
        Cancelled = 4
    }
}

Paginated Results

The module generates client-side pagination types:
Client PagedResult
namespace Warehouse.Application.IntegrationServices.Contracts
{
    public class PagedResult<T>
    {
        public int TotalCount { get; set; }
        public int PageCount { get; set; }
        public int PageSize { get; set; }
        public int PageNumber { get; set; }
        public IEnumerable<T> Data { get; set; }
    }
}

Configuration in Designer

Creating a Service Proxy

1

Add Service Proxy

In the Service Proxies Designer, add a new Service Proxy
2

Reference Source Service

Point to the source service in the external application
3

Select Operations

Choose which operations to expose in the client contract
4

Generate

Run Software Factory to generate client contracts

Implementation Examples

HTTP Client Implementation

Implement the client interface using HttpClient:
OrderServiceClient.cs
using System.Net.Http.Json;
using Warehouse.Application.IntegrationServices;
using Warehouse.Application.IntegrationServices.Contracts;

public class OrderServiceClient : IOrderServiceClient
{
    private readonly HttpClient _httpClient;
    private readonly ILogger<OrderServiceClient> _logger;

    public OrderServiceClient(
        HttpClient httpClient,
        ILogger<OrderServiceClient> logger)
    {
        _httpClient = httpClient;
        _logger = logger;
    }

    public async Task<OrderDto> GetOrderAsync(
        Guid id,
        CancellationToken cancellationToken)
    {
        var response = await _httpClient.GetAsync(
            $"api/orders/{id}",
            cancellationToken);

        response.EnsureSuccessStatusCode();

        return await response.Content.ReadFromJsonAsync<OrderDto>(
            cancellationToken: cancellationToken)
            ?? throw new InvalidOperationException("Failed to deserialize order");
    }

    public async Task<PagedResult<OrderDto>> GetOrdersAsync(
        int pageNo,
        int pageSize,
        CancellationToken cancellationToken)
    {
        var response = await _httpClient.GetAsync(
            $"api/orders?pageNo={pageNo}&pageSize={pageSize}",
            cancellationToken);

        response.EnsureSuccessStatusCode();

        return await response.Content.ReadFromJsonAsync<PagedResult<OrderDto>>(
            cancellationToken: cancellationToken)
            ?? throw new InvalidOperationException("Failed to deserialize orders");
    }

    public async Task<Guid> CreateOrderAsync(
        CreateOrderDto dto,
        CancellationToken cancellationToken)
    {
        var response = await _httpClient.PostAsJsonAsync(
            "api/orders",
            dto,
            cancellationToken);

        response.EnsureSuccessStatusCode();

        return await response.Content.ReadFromJsonAsync<Guid>(
            cancellationToken: cancellationToken);
    }

    public async Task UpdateOrderStatusAsync(
        Guid id,
        OrderStatus status,
        CancellationToken cancellationToken)
    {
        var response = await _httpClient.PutAsJsonAsync(
            $"api/orders/{id}/status",
            new { Status = status },
            cancellationToken);

        response.EnsureSuccessStatusCode();
    }
}

Dependency Injection Setup

Startup.cs / Program.cs
services.AddHttpClient<IOrderServiceClient, OrderServiceClient>(client =>
{
    client.BaseAddress = new Uri("https://orderservice.example.com");
    client.Timeout = TimeSpan.FromSeconds(30);
});

// With Polly for resilience
services.AddHttpClient<IOrderServiceClient, OrderServiceClient>(client =>
{
    client.BaseAddress = new Uri("https://orderservice.example.com");
})
.AddTransientHttpErrorPolicy(policy =>
    policy.WaitAndRetryAsync(3, retryAttempt =>
        TimeSpan.FromSeconds(Math.Pow(2, retryAttempt))))
.AddTransientHttpErrorPolicy(policy =>
    policy.CircuitBreakerAsync(5, TimeSpan.FromSeconds(30)));

Using the Client

WarehouseService.cs
public class WarehouseService : IWarehouseService
{
    private readonly IOrderServiceClient _orderServiceClient;
    private readonly IWarehouseRepository _repository;

    public WarehouseService(
        IOrderServiceClient orderServiceClient,
        IWarehouseRepository repository)
    {
        _orderServiceClient = orderServiceClient;
        _repository = repository;
    }

    public async Task ProcessShipmentAsync(
        Guid orderId,
        CancellationToken cancellationToken)
    {
        // Get order details from Order Service
        var order = await _orderServiceClient.GetOrderAsync(
            orderId,
            cancellationToken);

        // Process shipment
        var shipment = await _repository.CreateShipmentAsync(
            order,
            cancellationToken);

        // Update order status in Order Service
        await _orderServiceClient.UpdateOrderStatusAsync(
            orderId,
            OrderStatus.Shipped,
            cancellationToken);
    }
}

gRPC Client Support

For gRPC services, implement the client differently:
gRPC Client
using Grpc.Net.Client;
using OrderService.Grpc;

public class OrderServiceGrpcClient : IOrderServiceClient
{
    private readonly OrderService.OrderServiceClient _grpcClient;

    public OrderServiceGrpcClient(GrpcChannel channel)
    {
        _grpcClient = new OrderService.OrderServiceClient(channel);
    }

    public async Task<OrderDto> GetOrderAsync(
        Guid id,
        CancellationToken cancellationToken)
    {
        var request = new GetOrderRequest { Id = id.ToString() };
        var response = await _grpcClient.GetOrderAsync(request, cancellationToken: cancellationToken);

        return new OrderDto
        {
            Id = Guid.Parse(response.Id),
            OrderNumber = response.OrderNumber,
            Status = (OrderStatus)response.Status,
            TotalAmount = response.TotalAmount
        };
    }

    // ... other methods
}

Advanced Scenarios

Authentication & Authorization

Add authentication to client calls:
Authenticated Client
public class AuthenticatedOrderServiceClient : IOrderServiceClient
{
    private readonly HttpClient _httpClient;
    private readonly ITokenProvider _tokenProvider;

    public AuthenticatedOrderServiceClient(
        HttpClient httpClient,
        ITokenProvider tokenProvider)
    {
        _httpClient = httpClient;
        _tokenProvider = tokenProvider;
    }

    public async Task<OrderDto> GetOrderAsync(
        Guid id,
        CancellationToken cancellationToken)
    {
        var token = await _tokenProvider.GetAccessTokenAsync(cancellationToken);
        
        using var request = new HttpRequestMessage(HttpMethod.Get, $"api/orders/{id}");
        request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token);

        var response = await _httpClient.SendAsync(request, cancellationToken);
        response.EnsureSuccessStatusCode();

        return await response.Content.ReadFromJsonAsync<OrderDto>(
            cancellationToken: cancellationToken)
            ?? throw new InvalidOperationException();
    }
}

Circuit Breaker Pattern

Resilient Client
services.AddHttpClient<IOrderServiceClient, OrderServiceClient>(client =>
{
    client.BaseAddress = new Uri("https://orderservice.example.com");
})
.AddPolicyHandler(Policy<HttpResponseMessage>
    .Handle<HttpRequestException>()
    .OrResult(msg => !msg.IsSuccessStatusCode)
    .CircuitBreakerAsync(
        handledEventsAllowedBeforeBreaking: 3,
        durationOfBreak: TimeSpan.FromSeconds(30),
        onBreak: (result, duration) =>
        {
            Log.Warning("Circuit breaker opened for {Duration}s", duration.TotalSeconds);
        },
        onReset: () =>
        {
            Log.Information("Circuit breaker reset");
        }));

Caching

Add caching to reduce service calls:
Cached Client
public class CachedOrderServiceClient : IOrderServiceClient
{
    private readonly IOrderServiceClient _inner;
    private readonly IMemoryCache _cache;

    public CachedOrderServiceClient(
        IOrderServiceClient inner,
        IMemoryCache cache)
    {
        _inner = inner;
        _cache = cache;
    }

    public async Task<OrderDto> GetOrderAsync(
        Guid id,
        CancellationToken cancellationToken)
    {
        var cacheKey = $"order_{id}";

        if (_cache.TryGetValue<OrderDto>(cacheKey, out var cachedOrder))
        {
            return cachedOrder!;
        }

        var order = await _inner.GetOrderAsync(id, cancellationToken);

        _cache.Set(cacheKey, order, TimeSpan.FromMinutes(5));

        return order;
    }

    // ... other methods
}

Testing

Mocking Client Contracts

Unit Test
public class WarehouseServiceTests
{
    private readonly Mock<IOrderServiceClient> _mockOrderClient;
    private readonly WarehouseService _service;

    public WarehouseServiceTests()
    {
        _mockOrderClient = new Mock<IOrderServiceClient>();
        _service = new WarehouseService(_mockOrderClient.Object, ...);
    }

    [Fact]
    public async Task ProcessShipment_ValidOrder_UpdatesStatus()
    {
        // Arrange
        var orderId = Guid.NewGuid();
        var order = new OrderDto
        {
            Id = orderId,
            OrderNumber = "ORD-001",
            Status = OrderStatus.Confirmed
        };

        _mockOrderClient
            .Setup(c => c.GetOrderAsync(orderId, It.IsAny<CancellationToken>()))
            .ReturnsAsync(order);

        _mockOrderClient
            .Setup(c => c.UpdateOrderStatusAsync(
                orderId,
                OrderStatus.Shipped,
                It.IsAny<CancellationToken>()))
            .Returns(Task.CompletedTask);

        // Act
        await _service.ProcessShipmentAsync(orderId, CancellationToken.None);

        // Assert
        _mockOrderClient.Verify(
            c => c.UpdateOrderStatusAsync(
                orderId,
                OrderStatus.Shipped,
                It.IsAny<CancellationToken>()),
            Times.Once);
    }
}

Best Practices

Create focused client interfaces that only expose the operations a particular client needs:
// ✅ Good - Focused client
public interface IOrderQueryClient
{
    Task<OrderDto> GetOrderAsync(...);
    Task<List<OrderDto>> GetOrdersAsync(...);
}

public interface IOrderCommandClient
{
    Task<Guid> CreateOrderAsync(...);
    Task UpdateOrderAsync(...);
}

// ❌ Bad - Exposing everything
public interface IOrderServiceClient
{
    // 20+ methods
}
Use versioned namespaces or separate packages for different API versions:
namespace Warehouse.Application.IntegrationServices.V1.Contracts
namespace Warehouse.Application.IntegrationServices.V2.Contracts
Always add retry logic, circuit breakers, and timeouts:
services.AddHttpClient<IOrderServiceClient, OrderServiceClient>()
    .AddTransientHttpErrorPolicy(p => p.WaitAndRetryAsync(3, _ => TimeSpan.FromSeconds(2)))
    .AddTransientHttpErrorPolicy(p => p.CircuitBreakerAsync(5, TimeSpan.FromSeconds(30)));
Always handle potential failures:
try
{
    var order = await _orderClient.GetOrderAsync(id, ct);
}
catch (HttpRequestException ex)
{
    _logger.LogError(ex, "Failed to retrieve order {OrderId}", id);
    // Return cached data, default, or rethrow
}

Default Output Location

Generated client contracts are placed in:
YourProject.Application/IntegrationServices/
├── Contracts/           # Client DTOs and enums
│   ├── OrderDto.cs
│   ├── OrderStatus.cs
│   └── PagedResult.cs
└── IOrderServiceClient.cs

Troubleshooting

Issue: Client contracts aren’t being created.Solution:
  1. Verify Intent.Application.Contracts.Clients is installed
  2. Check that Service Proxy is properly configured in the designer
  3. Ensure source service is correctly referenced
  4. Run Software Factory
Issue: Client DTOs don’t match server DTOs.Solution:
  1. Ensure both applications use the same source designer files
  2. Regenerate both server and client contracts
  3. Check for designer synchronization issues

External Resources

Build docs developers (and LLMs) love