Documentation

how-to/create-component.md

How to Create a Component

This guide provides detailed step-by-step instructions for creating a complete Dynaplex component with all four projects, proper configuration, and integration with the Aspire AppHost.

Prerequisites

Complete Component Creation

Step 1: Create the Directory Structure

# From repository root
COMPONENT_NAME="my-feature"  # Change this to your component name
mkdir -p engines/${COMPONENT_NAME}/resources
mkdir -p engines/${COMPONENT_NAME}/src/Acsis.Dynaplex.Engines.MyFeature.Abstractions/Models
mkdir -p engines/${COMPONENT_NAME}/src/Acsis.Dynaplex.Engines.MyFeature.Abstractions/Configuration
mkdir -p engines/${COMPONENT_NAME}/src/Acsis.Dynaplex.Engines.MyFeature/Services
mkdir -p engines/${COMPONENT_NAME}/src/Acsis.Dynaplex.Engines.MyFeature/Data
mkdir -p engines/${COMPONENT_NAME}/test/Acsis.Dynaplex.Engines.MyFeature.Tests

Step 2: Create the .Abstractions Project

File: engines/my-feature/src/Acsis.Dynaplex.Engines.MyFeature.Abstractions/Acsis.Dynaplex.Engines.MyFeature.Abstractions.csproj

<Project Sdk="Microsoft.NET.Sdk">

	<PropertyGroup>
		<TargetFramework>net9.0</TargetFramework>
		<LangVersion>latest</LangVersion>
		<ImplicitUsings>enable</ImplicitUsings>
		<Nullable>enable</Nullable>
	</PropertyGroup>

	<!-- Add common packages that define contracts -->
	<ItemGroup>
		<PackageReference Include="System.ComponentModel.Annotations" Version="5.0.0" />
		<PackageReference Include="FluentValidation" Version="12.0.0" />
	</ItemGroup>

	<!-- Reference other component abstractions if needed -->
	<ItemGroup>
		<ProjectReference Include="$(CoreDataAbstractions)" />
	</ItemGroup>

</Project>

Step 3: Define the Main Interface

File: engines/my-feature/src/Acsis.Dynaplex.Engines.MyFeature.Abstractions/IMyFeatureApi.cs

using Acsis.Dynaplex.Engines.MyFeature.Abstractions.Models;

namespace Acsis.Dynaplex.Engines.MyFeature.Abstractions;

/// <summary>
/// API for managing my feature functionality
/// </summary>
public interface IMyFeatureApi
{
    /// <summary>
    /// Gets a feature item by ID
    /// </summary>
    /// <param name="id">The feature item ID</param>
    /// <returns>The feature item or null if not found</returns>
    Task<FeatureItemModel?> GetByIdAsync(int id);

    /// <summary>
    /// Creates a new feature item
    /// </summary>
    /// <param name="request">The creation request</param>
    /// <returns>The created feature item</returns>
    Task<FeatureItemModel> CreateAsync(CreateFeatureItemRequest request);

    /// <summary>
    /// Updates an existing feature item
    /// </summary>
    /// <param name="id">The feature item ID</param>
    /// <param name="request">The update request</param>
    /// <returns>The updated feature item</returns>
    Task<FeatureItemModel> UpdateAsync(int id, UpdateFeatureItemRequest request);

    /// <summary>
    /// Deletes a feature item
    /// </summary>
    /// <param name="id">The feature item ID</param>
    Task DeleteAsync(int id);
}

Step 4: Create Models

File: engines/my-feature/src/Acsis.Dynaplex.Engines.MyFeature.Abstractions/Models/FeatureItemModel.cs

using System.ComponentModel.DataAnnotations;

namespace Acsis.Dynaplex.Engines.MyFeature.Abstractions.Models;

/// <summary>
/// Represents a feature item
/// </summary>
public sealed class FeatureItemModel
{
    /// <summary>
    /// The unique identifier
    /// </summary>
    public int Id { get; set; }

    /// <summary>
    /// The feature item name
    /// </summary>
    [Required]
    [StringLength(100)]
    public string Name { get; set; } = string.Empty;

    /// <summary>
    /// The feature item description
    /// </summary>
    [StringLength(500)]
    public string? Description { get; set; }

    /// <summary>
    /// When the item was created
    /// </summary>
    public DateTime CreatedAt { get; set; }

    /// <summary>
    /// When the item was last modified
    /// </summary>
    public DateTime ModifiedAt { get; set; }
}

/// <summary>
/// Request to create a new feature item
/// </summary>
public sealed class CreateFeatureItemRequest
{
    /// <summary>
    /// The feature item name
    /// </summary>
    [Required]
    [StringLength(100)]
    public string Name { get; set; } = string.Empty;

    /// <summary>
    /// The feature item description
    /// </summary>
    [StringLength(500)]
    public string? Description { get; set; }
}

/// <summary>
/// Request to update an existing feature item
/// </summary>
public sealed class UpdateFeatureItemRequest
{
    /// <summary>
    /// The feature item name
    /// </summary>
    [Required]
    [StringLength(100)]
    public string Name { get; set; } = string.Empty;

    /// <summary>
    /// The feature item description
    /// </summary>
    [StringLength(500)]
    public string? Description { get; set; }
}

Step 5: Create the Implementation Project

File: engines/my-feature/src/Acsis.Dynaplex.Engines.MyFeature/Acsis.Dynaplex.Engines.MyFeature.csproj

<Project Sdk="Microsoft.NET.Sdk.Web">

	<PropertyGroup>
		<TargetFramework>net9.0</TargetFramework>
		<LangVersion>latest</LangVersion>
		<ImplicitUsings>enable</ImplicitUsings>
		<Nullable>enable</Nullable>
	</PropertyGroup>

	<!-- Implementation-specific packages -->
	<ItemGroup>
		<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="9.0.0" />
		<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="9.0.0" />
		<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="9.0.0" />
	</ItemGroup>

	<!-- Always reference your own abstractions -->
	<ItemGroup>
		<ProjectReference Include="$(MyFeatureAbstractions)" />
	</ItemGroup>

	<!-- Reference analyzers for architectural enforcement -->
	<ItemGroup>
		<ProjectReference Include="$(Analyzers)" OutputItemType="Analyzer" ReferenceOutputAssembly="false"/>
	</ItemGroup>

</Project>

Step 6: Implement the API

File: engines/my-feature/src/Acsis.Dynaplex.Engines.MyFeature/MyFeatureApi.cs

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

namespace Acsis.Dynaplex.Engines.MyFeature;

/// <summary>
/// Implementation of the My Feature API
/// </summary>
public sealed class MyFeatureApi : IMyFeatureApi
{
    private readonly ILogger<MyFeatureApi> _logger;

    public MyFeatureApi(ILogger<MyFeatureApi> logger)
    {
        _logger = logger ?? throw new ArgumentNullException(nameof(logger));
    }

    public async Task<FeatureItemModel?> GetByIdAsync(int id)
    {
        _logger.LogInformation("Getting feature item with ID {Id}", id);

        // TODO: Implement data access
        await Task.Delay(1); // Remove this

        return new FeatureItemModel
        {
            Id = id,
            Name = $"Feature Item {id}",
            Description = "Sample description",
            CreatedAt = DateTime.UtcNow.AddDays(-1),
            ModifiedAt = DateTime.UtcNow
        };
    }

    public async Task<FeatureItemModel> CreateAsync(CreateFeatureItemRequest request)
    {
        if (request == null) throw new ArgumentNullException(nameof(request));

        _logger.LogInformation("Creating new feature item: {Name}", request.Name);

        // TODO: Implement data access
        await Task.Delay(1); // Remove this

        return new FeatureItemModel
        {
            Id = Random.Shared.Next(1000, 9999),
            Name = request.Name,
            Description = request.Description,
            CreatedAt = DateTime.UtcNow,
            ModifiedAt = DateTime.UtcNow
        };
    }

    public async Task<FeatureItemModel> UpdateAsync(int id, UpdateFeatureItemRequest request)
    {
        if (request == null) throw new ArgumentNullException(nameof(request));

        _logger.LogInformation("Updating feature item {Id}: {Name}", id, request.Name);

        // TODO: Implement data access
        await Task.Delay(1); // Remove this

        return new FeatureItemModel
        {
            Id = id,
            Name = request.Name,
            Description = request.Description,
            CreatedAt = DateTime.UtcNow.AddDays(-1),
            ModifiedAt = DateTime.UtcNow
        };
    }

    public async Task DeleteAsync(int id)
    {
        _logger.LogInformation("Deleting feature item with ID {Id}", id);

        // TODO: Implement data access
        await Task.Delay(1); // Remove this
    }
}

Step 7: Create the ASP.NET Core Service

File: engines/my-feature/src/Acsis.Dynaplex.Engines.MyFeature/Program.cs

using Acsis.Dynaplex.Engines.MyFeature;
using Acsis.Dynaplex.Engines.MyFeature.Abstractions;

var builder = WebApplication.CreateBuilder(args);

// Add service defaults (includes OpenTelemetry, health checks, CORS, etc.)
builder.AddServiceDefaults();

// Add authentication (optional, if your component needs auth)
// builder.AddAcsisAuthentication();

// Register component services
builder.Services.AddScoped<IMyFeatureApi, MyFeatureApi>();

var app = builder.Build();

// Map component endpoints using MapAcsisEndpoints
// This automatically adds CORS, authentication, authorization, OpenAPI, and Scalar
app.MapAcsisEndpoints(
    MyFeatureEndpoints.MapEndpoints  // Create this in a separate file
);

app.Run();

Step 8: Register in Directory.Build.props

Add to Directory.Build.props:

<PropertyGroup>
    <!-- ... existing properties ... -->
    <MyFeatureAbstractions>$(EnginesRoot)my-feature/src/Acsis.Dynaplex.Engines.MyFeature.Abstractions/Acsis.Dynaplex.Engines.MyFeature.Abstractions.csproj</MyFeatureAbstractions>
</PropertyGroup>

Step 9: Add to Development Solution

Add to acsis-core.slnx:

<Folder Name="/engines/my-feature/">
    <Project Path="engines/my-feature/src/Acsis.Dynaplex.Engines.MyFeature/Acsis.Dynaplex.Engines.MyFeature.csproj" />
    <Project Path="engines/my-feature/src/Acsis.Dynaplex.Engines.MyFeature.Abstractions/Acsis.Dynaplex.Engines.MyFeature.Abstractions.csproj" />
</Folder>

Step 10: Register with Aspire AppHost

In your project's AppHost (e.g., projects/bbu-rfid/src/Acsis.Dynaplex.Projects.BbuRfid/Program.cs):

var builder = DistributedApplication.CreateBuilder(args);

// Add your component as a service
var myFeature = builder.AddProject<Acsis_Components_MyFeature>("my-feature")
    .WithHttpsEndpoint(port: 44443);

builder.Build().Run();

Step 11: Build and Test

# Build the abstractions
dotnet build engines/my-feature/src/Acsis.Dynaplex.Engines.MyFeature.Abstractions/

# Build the component service
dotnet build engines/my-feature/src/Acsis.Dynaplex.Engines.MyFeature/

# Build the full solution
dotnet build acsis-core.slnx

# Run via Aspire AppHost
dotnet run --project projects/bbu-rfid/src/Acsis.Dynaplex.Projects.BbuRfid/

# Your service will be available at:
# https://localhost:44443/swagger

Verification

Your component is successfully created when:

✅ All projects build without errors
✅ Component appears in Aspire Dashboard
✅ Swagger UI is accessible at the configured port
✅ Health check endpoint responds
✅ API endpoints are functional

Next Steps

Troubleshooting

Build fails with missing reference

  • Verify Directory.Build.props has your component property
  • Check project references use the property: $(MyFeatureAbstractions)

Component doesn't appear in Aspire Dashboard

  • Verify AppHost registration
  • Check for build errors
  • Ensure correct project name in AddProject<T>

Port conflict

  • Change the port in WithHttpsEndpoint(port: XXXXX)
  • Ensure port isn't used by another service

See troubleshooting guide for more help.