Skip to main content

Overview

The Intent.Application.Contracts module generates service interface contracts that define the public API of your application services. This promotes the Interface Segregation Principle and enables clean separation between interface definitions and implementations.
Module: Intent.Application.ContractsVersion: 5.1.2+Dependencies:
  • Intent.Application.Dtos
  • Intent.Common.CSharp
  • Intent.Modelers.Services

Key Features

  • Clean Architecture: Separate interface definitions from implementations
  • Service Contracts: Generate strongly-typed service interfaces
  • Async Support: Automatic Task<T> return types and CancellationToken parameters
  • DTO Integration: Seamless integration with DTO types
  • XML Documentation: Preserve comments from designer
  • Generic Operations: Support for generic type parameters

Installation

intent install Intent.Application.Contracts

Architecture Benefits

Service contracts enable:

Dependency Inversion

Depend on abstractions, not implementations

Testability

Easy mocking and unit testing

Modularity

Clear boundaries between modules

Flexibility

Swap implementations without changing consumers

Generated Service Contracts

Basic Service Interface

// ProductService (in Services Designer)
//   Operations:
//     - GetProduct(id: Guid): ProductDto
//     - GetAllProducts(): List<ProductDto>
//     - CreateProduct(dto: CreateProductDto): Guid
//     - UpdateProduct(id: Guid, dto: UpdateProductDto): void
//     - DeleteProduct(id: Guid): void

Async by Default

All service operations are generated as async by default:
  • Return types wrapped in Task<T>
  • CancellationToken parameter added automatically
  • Follows async best practices
The Async suffix is automatically added to method names following .NET async conventions.

XML Documentation

Comments from the Services Designer are preserved:
// Operation: GetProduct
// Description: Retrieves a product by its unique identifier
// Parameter id: The unique identifier of the product
// Returns: The product details if found, null otherwise

Service Implementation

Implement the generated interface:
ProductService.cs
using MyApp.Application.Interfaces;
using MyApp.Domain.Repositories;

public class ProductService : IProductService
{
    private readonly IProductRepository _repository;
    private readonly IMapper _mapper;

    public ProductService(
        IProductRepository repository,
        IMapper mapper)
    {
        _repository = repository;
        _mapper = mapper;
    }

    public async Task<ProductDto> GetProductAsync(
        Guid id,
        CancellationToken cancellationToken)
    {
        var product = await _repository.FindByIdAsync(id, cancellationToken)
            ?? throw new NotFoundException(nameof(Product), id);
        
        return _mapper.Map<ProductDto>(product);
    }

    public async Task<List<ProductDto>> GetAllProductsAsync(
        CancellationToken cancellationToken)
    {
        var products = await _repository.FindAllAsync(cancellationToken);
        return _mapper.Map<List<ProductDto>>(products);
    }

    public async Task<Guid> CreateProductAsync(
        CreateProductDto dto,
        CancellationToken cancellationToken)
    {
        var product = new Product
        {
            Name = dto.Name,
            Price = dto.Price,
            // ... map other properties
        };

        await _repository.AddAsync(product, cancellationToken);
        return product.Id;
    }

    public async Task UpdateProductAsync(
        Guid id,
        UpdateProductDto dto,
        CancellationToken cancellationToken)
    {
        var product = await _repository.FindByIdAsync(id, cancellationToken)
            ?? throw new NotFoundException(nameof(Product), id);

        product.Name = dto.Name;
        product.Price = dto.Price;
        // ... update other properties

        await _repository.UpdateAsync(product, cancellationToken);
    }

    public async Task DeleteProductAsync(
        Guid id,
        CancellationToken cancellationToken)
    {
        var product = await _repository.FindByIdAsync(id, cancellationToken)
            ?? throw new NotFoundException(nameof(Product), id);

        await _repository.RemoveAsync(product, cancellationToken);
    }
}

Dependency Injection

Register interfaces and implementations:
DI Configuration
services.AddScoped<IProductService, ProductService>();
services.AddScoped<IOrderService, OrderService>();
services.AddScoped<ICustomerService, CustomerService>();
Intent Architect modules automatically generate DI registration code. You typically don’t need to manually register services.

Advanced Features

Generic Operations

Service operations can have generic type parameters:
// Operation: GetById<T>(id: Guid): T

Complex Return Types

Supports complex return types including:
Complex Return Types
public interface IOrderService
{
    // Paginated results
    Task<PagedResult<OrderDto>> GetOrdersAsync(
        int pageNo,
        int pageSize,
        CancellationToken cancellationToken = default);

    // Collections
    Task<List<OrderDto>> GetOrdersByCustomerAsync(
        Guid customerId,
        CancellationToken cancellationToken = default);

    // Nullable results
    Task<OrderDto?> FindOrderAsync(
        string orderNumber,
        CancellationToken cancellationToken = default);

    // Void operations
    Task CancelOrderAsync(
        Guid id,
        CancellationToken cancellationToken = default);

    // Tuples
    Task<(int TotalOrders, decimal TotalRevenue)> GetOrderStatisticsAsync(
        CancellationToken cancellationToken = default);
}

Parameter Attributes

The module preserves parameter attributes from stereotypes:
Parameter Attributes
public interface IProductService
{
    Task<ProductDto> GetProductAsync(
        [FromRoute] Guid id,
        CancellationToken cancellationToken = default);

    Task<List<ProductDto>> SearchProductsAsync(
        [FromQuery] string searchTerm,
        [FromQuery] int? minPrice,
        [FromQuery] int? maxPrice,
        CancellationToken cancellationToken = default);

    Task<Guid> CreateProductAsync(
        [FromBody] CreateProductDto dto,
        CancellationToken cancellationToken = default);
}

Testing

Service contracts make unit testing straightforward:

Mocking with Moq

Unit Test
public class ProductControllerTests
{
    private readonly Mock<IProductService> _mockProductService;
    private readonly ProductController _controller;

    public ProductControllerTests()
    {
        _mockProductService = new Mock<IProductService>();
        _controller = new ProductController(_mockProductService.Object);
    }

    [Fact]
    public async Task GetProduct_WithValidId_ReturnsProduct()
    {
        // Arrange
        var productId = Guid.NewGuid();
        var expectedProduct = new ProductDto
        {
            Id = productId,
            Name = "Test Product",
            Price = 29.99m
        };

        _mockProductService
            .Setup(s => s.GetProductAsync(productId, It.IsAny<CancellationToken>()))
            .ReturnsAsync(expectedProduct);

        // Act
        var result = await _controller.GetProduct(productId, CancellationToken.None);

        // Assert
        var okResult = Assert.IsType<OkObjectResult>(result);
        var product = Assert.IsType<ProductDto>(okResult.Value);
        Assert.Equal(expectedProduct.Id, product.Id);
        Assert.Equal(expectedProduct.Name, product.Name);
    }

    [Fact]
    public async Task CreateProduct_WithValidDto_ReturnsCreatedProduct()
    {
        // Arrange
        var createDto = new CreateProductDto
        {
            Name = "New Product",
            Price = 39.99m
        };
        var newProductId = Guid.NewGuid();

        _mockProductService
            .Setup(s => s.CreateProductAsync(createDto, It.IsAny<CancellationToken>()))
            .ReturnsAsync(newProductId);

        // Act
        var result = await _controller.CreateProduct(createDto, CancellationToken.None);

        // Assert
        var createdResult = Assert.IsType<CreatedAtActionResult>(result);
        Assert.Equal(newProductId, createdResult.Value);
    }
}

Integration Testing

Test real implementations:
Integration Test
public class ProductServiceIntegrationTests : IClassFixture<WebApplicationFactory<Program>>
{
    private readonly WebApplicationFactory<Program> _factory;
    private readonly IServiceScope _scope;
    private readonly IProductService _productService;

    public ProductServiceIntegrationTests(WebApplicationFactory<Program> factory)
    {
        _factory = factory;
        _scope = _factory.Services.CreateScope();
        _productService = _scope.ServiceProvider.GetRequiredService<IProductService>();
    }

    [Fact]
    public async Task CreateAndRetrieveProduct_Success()
    {
        // Arrange
        var createDto = new CreateProductDto
        {
            Name = "Integration Test Product",
            Price = 49.99m
        };

        // Act - Create
        var productId = await _productService.CreateProductAsync(
            createDto,
            CancellationToken.None);

        // Act - Retrieve
        var retrievedProduct = await _productService.GetProductAsync(
            productId,
            CancellationToken.None);

        // Assert
        Assert.NotNull(retrievedProduct);
        Assert.Equal(createDto.Name, retrievedProduct.Name);
        Assert.Equal(createDto.Price, retrievedProduct.Price);
    }
}

Naming Conventions

The module automatically applies naming conventions:
Service NameGenerated Interface
ProductServiceIProductService
OrderServiceIOrderService
ProductRestControllerIProductService (removes “RestController”)
OrderControllerIOrderService (removes “Controller”)
The “Service” suffix is always used in the interface name regardless of the original service name.

Default Output Location

Generated contracts are placed in:
YourProject.Application/Interfaces/
You can customize this location in the template configuration.

Best Practices

Each service interface should represent a cohesive set of operations related to a single bounded context or aggregate.
// ✅ Good - Cohesive
public interface IProductService
{
    Task<ProductDto> GetProductAsync(...);
    Task<Guid> CreateProductAsync(...);
    Task UpdateProductAsync(...);
    Task DeleteProductAsync(...);
}

// ❌ Bad - Too broad
public interface IEverythingService
{
    Task<ProductDto> GetProductAsync(...);
    Task<OrderDto> GetOrderAsync(...);
    Task<CustomerDto> GetCustomerAsync(...);
    // ...
}
Avoid exposing domain entities in service interfaces. Use DTOs for all inputs and outputs.
// ✅ Good
Task<ProductDto> GetProductAsync(Guid id, ...);
Task<Guid> CreateProductAsync(CreateProductDto dto, ...);

// ❌ Bad
Task<Product> GetProductAsync(Guid id, ...);
Task<Guid> CreateProductAsync(Product product, ...);
All async operations should accept a CancellationToken for proper cancellation support.
// ✅ Good
Task<ProductDto> GetProductAsync(
    Guid id,
    CancellationToken cancellationToken = default);

// ❌ Bad
Task<ProductDto> GetProductAsync(Guid id);
The module adds CancellationToken parameters automatically.
Add XML documentation in the Services Designer for all operations and parameters. This documentation flows through to generated code and API documentation.

Integration with Other Modules

Service Implementations

Generate service implementations

MediatR

CQRS pattern with MediatR

ASP.NET Core Controllers

Expose services via REST APIs

Contracts Clients

Generate client proxies

Troubleshooting

Issue: Service interface not created after running Software Factory.Solution:
  1. Verify the module is installed
  2. Check that the service has operations defined
  3. Ensure the service is in a Services Designer package
  4. Run Software Factory again
Issue: Generated method signatures don’t match expectations.Solution:
  1. Check operation return types in designer
  2. Verify parameter types are correctly mapped
  3. Ensure DTOs are properly generated
  4. Review any applied stereotypes
Issue: XML documentation not appearing in generated code.Solution:
  1. Add comments in the Services Designer
  2. Save the designer file
  3. Regenerate with Software Factory
  4. Check that XML documentation generation is enabled in project settings

Release Notes

For detailed version history, see the module changelog.

Build docs developers (and LLMs) love