Documentation

how-to/modify-component.md

How to Modify Components

This guide shows you how to add features, modify APIs, and extend existing Dynaplex components.

Adding New Methods to an API

When you need to add functionality to an existing component:

Step 1: Add to the Interface

Update the interface in the .Abstractions project:

// In Acsis.Dynaplex.Engines.MyFeature.Abstractions/IMyFeatureApi.cs
public interface IMyFeatureApi
{
    // Existing methods...
    Task<FeatureItemModel?> GetByIdAsync(int id);

    // NEW: Add your new method
    /// <summary>
    /// Searches for feature items by name
    /// </summary>
    /// <param name="searchTerm">The search term</param>
    /// <returns>Matching feature items</returns>
    Task<List<FeatureItemModel>> SearchByNameAsync(string searchTerm);
}

Step 2: Implement in the Service

Add the implementation in the main project:

// In Acsis.Dynaplex.Engines.MyFeature/MyFeatureApi.cs
public class MyFeatureApi : IMyFeatureApi
{
    // Existing implementation...

    // NEW: Implement the new method
    public async Task<List<FeatureItemModel>> SearchByNameAsync(string searchTerm)
    {
        _logger.LogInformation("Searching for items with term: {SearchTerm}", searchTerm);

        // Your implementation here
        var results = await _repository.SearchAsync(searchTerm);
        return results;
    }
}

Step 3: Add Endpoint (if needed)

Update Program.cs to expose the new functionality:

// In Program.cs
app.MapGet("/api/features/search", async (
    [FromQuery] string q,
    IMyFeatureApi api) =>
{
    var results = await api.SearchByNameAsync(q);
    return Results.Ok(results);
});

Step 4: Build and Test

dotnet build engines/my-feature/
dotnet test engines/my-feature/test/

Adding New Models

Models represent data structures used by your API.

Step 1: Create the Model in .Abstractions

// In Acsis.Dynaplex.Engines.MyFeature.Abstractions/Models/FeatureItemSummary.cs
namespace Acsis.Dynaplex.Engines.MyFeature.Abstractions.Models;

/// <summary>
/// Lightweight summary of a feature item
/// </summary>
public class FeatureItemSummary
{
    public int Id { get; set; }

    [Required]
    public string Name { get; set; } = string.Empty;

    public int ItemCount { get; set; }
}

Step 2: Use in Your API

public interface IMyFeatureApi
{
    /// <summary>
    /// Gets a summary of all feature items
    /// </summary>
    Task<List<FeatureItemSummary>> GetSummariesAsync();
}

Best Practices for Models

Use data annotations for validation
Document with XML comments
Make classes sealed unless inheritance is needed
Use nullable reference types appropriately
Consider backwards compatibility when modifying existing models

Adding Dependencies

External NuGet Packages

Add to the appropriate .csproj file:

<!-- In Acsis.Dynaplex.Engines.MyFeature.csproj -->
<ItemGroup>
    <PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
</ItemGroup>

Referencing Other Components

⚠️ IMPORTANT: Only reference .Abstractions projects of other components.

<!-- CORRECT: Reference the .Abstractions project -->
<ItemGroup>
    <ProjectReference Include="$(EventsAbstractions)" />
</ItemGroup>
<!-- WRONG: Never reference implementation projects -->
<ItemGroup>
    <ProjectReference Include="$(Events)" /> <!-- ❌ This will fail -->
</ItemGroup>

At runtime, services communicate via HTTP using auto-generated API clients.

Using Another Component's API

public class MyFeatureApi : IMyFeatureApi
{
    private readonly IEventsApiClient _events;

    public MyFeatureApi(IEventsApiClient events)
    {
        _events = events;
    }

    public async Task DoSomething()
    {
        // Call another component via HTTP client
        await _events.PublishEventAsync(new Event { /* ... */ });
    }
}

Testing Components

Unit Test Structure

test/Acsis.Dynaplex.Engines.MyFeature.Tests/
├── Acsis.Dynaplex.Engines.MyFeature.Tests.csproj
├── MyFeatureApiTests.cs
├── Models/
│   └── FeatureItemModelTests.cs
├── TestHelpers/
│   └── MyFeatureTestHelpers.cs
└── TestData/
    └── SampleFeatureItems.json

Example Unit Test

using Acsis.Dynaplex.Engines.MyFeature.Abstractions;
using Acsis.Dynaplex.Engines.MyFeature.Abstractions.Models;
using Microsoft.Extensions.Logging.Abstractions;
using Xunit;

namespace Acsis.Dynaplex.Engines.MyFeature.Tests;

public class MyFeatureApiTests
{
    private readonly IMyFeatureApi _api;

    public MyFeatureApiTests()
    {
        var logger = NullLogger<MyFeatureApi>.Instance;
        _api = new MyFeatureApi(logger);
    }

    [Fact]
    public async Task GetByIdAsync_WithValidId_ReturnsFeatureItem()
    {
        // Arrange
        const int id = 123;

        // Act
        var result = await _api.GetByIdAsync(id);

        // Assert
        Assert.NotNull(result);
        Assert.Equal(id, result.Id);
        Assert.NotEmpty(result.Name);
    }

    [Fact]
    public async Task CreateAsync_WithValidRequest_ReturnsCreatedItem()
    {
        // Arrange
        var request = new CreateFeatureItemRequest
        {
            Name = "Test Feature",
            Description = "Test Description"
        };

        // Act
        var result = await _api.CreateAsync(request);

        // Assert
        Assert.NotNull(result);
        Assert.Equal(request.Name, result.Name);
        Assert.Equal(request.Description, result.Description);
        Assert.True(result.Id > 0);
    }

    [Fact]
    public async Task CreateAsync_WithNullRequest_ThrowsArgumentNullException()
    {
        // Act & Assert
        await Assert.ThrowsAsync<ArgumentNullException>(() =>
            _api.CreateAsync(null!));
    }
}

Integration Tests

Test service-to-service communication:

public class MyFeatureIntegrationTests : IClassFixture<WebApplicationFactory<Program>>
{
    private readonly WebApplicationFactory<Program> _factory;

    public MyFeatureIntegrationTests(WebApplicationFactory<Program> factory)
    {
        _factory = factory;
    }

    [Fact]
    public async Task GetFeature_ReturnsExpectedResponse()
    {
        // Arrange
        var client = _factory.CreateClient();

        // Act
        var response = await client.GetAsync("/api/features/1");

        // Assert
        response.EnsureSuccessStatusCode();
        var content = await response.Content.ReadAsStringAsync();
        Assert.Contains("Feature Item", content);
    }
}

Common Modification Scenarios

Adding Configuration

Step 1: Create options class in .Abstractions:

// Acsis.Dynaplex.Engines.MyFeature.Abstractions/Configuration/MyFeatureOptions.cs
public class MyFeatureOptions
{
    public string ApiKey { get; set; } = string.Empty;
    public int MaxRetries { get; set; } = 3;
    public bool EnableCaching { get; set; } = true;
}

Step 2: Configure in appsettings.json:

{
  "MyFeature": {
    "ApiKey": "your-api-key",
    "MaxRetries": 5,
    "EnableCaching": true
  }
}

Step 3: Use in implementation:

public class MyFeatureApi : IMyFeatureApi
{
    private readonly MyFeatureOptions _options;

    public MyFeatureApi(IOptions<MyFeatureOptions> options)
    {
        _options = options.Value;
    }
}

Step 4: Register in Program.cs:

builder.Services.Configure<MyFeatureOptions>(
    builder.Configuration.GetSection("MyFeature"));

Adding Background Services

// In Acsis.Dynaplex.Engines.MyFeature/Services/MyBackgroundService.cs
public class MyBackgroundService : BackgroundService
{
    private readonly ILogger<MyBackgroundService> _logger;

    public MyBackgroundService(ILogger<MyBackgroundService> logger)
    {
        _logger = logger;
    }

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        while (!stoppingToken.IsCancellationRequested)
        {
            _logger.LogInformation("Background service running at: {Time}", DateTimeOffset.Now);

            // Do work here

            await Task.Delay(TimeSpan.FromMinutes(5), stoppingToken);
        }
    }
}

Register in Program.cs:

builder.Services.AddHostedService<MyBackgroundService>();

Next Steps

Checklist

Before committing your changes:

  • Interface changes are in .Abstractions project
  • Implementation is complete and tested
  • Unit tests added for new functionality
  • XML documentation comments added
  • Configuration properly registered
  • Solution builds without warnings
  • Existing tests still pass
  • OpenAPI documentation updated (if endpoints changed)