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
- Dynaplex repository cloned
- .NET 9 SDK installed
- Familiarity with component anatomy
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
- Modify your component - Add features and functionality
- Add database support - Persist data with EF Core
- Write tests - Test your component
- Learn patterns - Best practices
Troubleshooting
Build fails with missing reference
- Verify
Directory.Build.propshas 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.