Skip to content

Showcase Demo

David Kallesen edited this page Dec 22, 2025 · 1 revision

🎪 Showcase Demo

The Showcase sample is a comprehensive demonstration of all features provided by the Atc.Rest.Api.SourceGenerator. It includes a full-featured API, a Blazor WebAssembly client, and Aspire orchestration.

🚀 Quick Start

1️⃣ Option 1: Run with .NET Aspire (Recommended)

.NET Aspire orchestrates all projects together:

cd sample/Showcase

# Run the Aspire AppHost
dotnet run --project Showcase.Aspire/Showcase.Aspire.csproj

Open the Aspire Dashboard at https://localhost:17037 to see:

  • 🖥️ Showcase.Api - The REST API
  • 🌐 Showcase.BlazorApp - Blazor WebAssembly UI
  • 💻 Showcase.ClientApp - Console client (optional)

2️⃣ Option 2: Run API Only

cd sample/Showcase

# Start the API
dotnet run --project Showcase.Api/Showcase.Api.csproj

Then access:

  • 📖 Swagger UI: https://localhost:7100/swagger
  • Scalar API Docs: https://localhost:7100/scalar
  • 📄 OpenAPI Spec: https://localhost:7100/openapi/v1.json

3️⃣ Option 3: Run API + Blazor App Separately

# Terminal 1: Start the API
dotnet run --project sample/Showcase/Showcase.Api/Showcase.Api.csproj

# Terminal 2: Start the Blazor app
dotnet run --project sample/Showcase/Showcase.BlazorApp/Showcase.BlazorApp.csproj

📋 API Endpoints Overview

The Showcase API demonstrates a variety of patterns:

💼 Accounts

Method Endpoint Description Features
📋 GET /accounts List accounts Basic CRUD
📄 GET /accounts/paginated List with pagination PaginatedResult<T>
🌊 GET /accounts/stream Stream accounts IAsyncEnumerable<T>
🔍 GET /accounts/{id} Get by ID Single item
➕ POST /accounts Create account Request body
✏️ PUT /accounts/{id} Update account Route + body
🗑️ DELETE /accounts/{id} Delete account 204 No Content

✅ Tasks

Method Endpoint Description
📋 GET /tasks List all tasks
🔍 GET /tasks/{id} Get task by ID
➕ POST /tasks Create task
✏️ PUT /tasks/{id} Update task
🗑️ DELETE /tasks/{id} Delete task

⚠️ Type Conflict Resolution: The Task model conflicts with System.Threading.Tasks.Task. The generator automatically uses fully qualified names.

👥 Users

Method Endpoint Description Features
📋 GET /users List users Query filters
🔍 GET /users/{id} Get user Nested Address object
➕ POST /users Create user UserRole enum
✏️ PUT /users/{id} Update user Profile image (base64)
🗑️ DELETE /users/{id} Delete user Confirmation

🔍 Query Parameters on GET /users:

  • search - Search by name
  • country - Filter by country
  • role - Filter by UserRole enum
  • isActive - Filter by status

📁 Files

Method Endpoint Description Features
⬆️ POST /files/upload Single file application/octet-stream
⬆️ POST /files/upload-multiple Multiple files multipart/form-data
⬆️ POST /files/upload-with-metadata File + metadata Mixed form data
⬇️ GET /files/{id} Download file BinaryEndpointResponse

💡 Binary Downloads: The GET /files/{id} endpoint returns BinaryEndpointResponse directly from the Atc.Rest.Client library.


🌐 Blazor WebAssembly Features

The Blazor app demonstrates a complete client-side implementation:

🎨 UI Framework

  • 🎭 MudBlazor v8 - Material Design components
  • 🌙 Light/Dark Mode - Toggle in app bar
  • 📱 Responsive Layout - Works on mobile and desktop

🧭 Navigation

The nav menu is organized by API path segment:

  • 💼 Accounts
  • ✅ Tasks
  • 👥 Users
  • 📁 Files

🧩 Component Patterns

📋 List Pages:

+-----------------------------------------+
| Accounts                      [+ New]   |
+-----------------------------------------+
| 🔍 Search...                            |
+-----------------------------------------+
| ID    | Name        | Actions           |
| abc-1 | Account One | [👁️][✏️][🗑️]    |
| def-2 | Account Two | [👁️][✏️][🗑️]    |
+-----------------------------------------+

📄 Detail Pages:

+--------------------------------------+
| ← Back                               |
+--------------------------------------+
| 👤 User Details                      |
|                                      |
| Name: John Doe                       |
| Role: Admin                          |
| Status: ✅ Active                    |
|                                      |
| 📍 Address:                          |
|   123 Main St                        |
|   New York, USA 10001                |
|                                      |
|              [✏️ Edit] [🗑️ Delete]  |
+--------------------------------------+

⚠️ Confirmation Dialogs

All delete operations use confirmation dialogs:

var confirmed = await DialogService.ShowAsync<ConfirmDialog>(
    "Delete User",
    new DialogParameters
    {
        ["Message"] = $"Are you sure you want to delete {user.Name}?",
        ["ConfirmText"] = "Delete",
        ["ConfirmColor"] = Color.Error
    });

if (confirmed)
{
    await UsersClient.DeleteUserByIdAsync(userId);
}

🔢 Enum Handling

The Users section demonstrates enum dropdowns:

<MudSelect T="UserRole?" @bind-Value="user.Role" Label="Role">
    @foreach (var role in Enum.GetValues<UserRole>())
    {
        <MudSelectItem Value="@((UserRole?)role)">@role</MudSelectItem>
    }
</MudSelect>

🌊 IAsyncEnumerable Streaming

The Accounts section shows real-time streaming:

await foreach (var account in AccountsClient.ListAccountsAsAsyncEnumerableAsync())
{
    accounts.Add(account);
    StateHasChanged();  // Update UI as each item arrives
}

✅ FluentValidation Integration

The Showcase sample demonstrates FluentValidation integration with the ValidationFilter<T> from Atc.Rest.MinimalApi.

📝 Validator Examples

public sealed partial class CreateUserRequestValidator : AbstractValidator<CreateUserRequest>
{
    public CreateUserRequestValidator()
    {
        RuleFor(x => x.FirstName)
            .Matches(EnsureFirstCharacterUpperCase())
            .WithMessage(x => $"{nameof(x.FirstName)} must start with an uppercase letter.");

        RuleFor(x => x.LastName)
            .Matches(EnsureFirstCharacterUpperCase())
            .NotEqual(x => x.FirstName)
            .WithMessage("FirstName and LastName must not be the same.");

        RuleFor(x => x.Address)
            .NotNull()
            .WithMessage("Address is required.");

        When(x => x.Address is not null, () =>
        {
            RuleFor(x => x.Address.City)
                .Matches(EnsureFirstCharacterUpperCase())
                .WithMessage("Address.City must start with an uppercase letter.");
        });
    }

    [GeneratedRegex("^[A-Z]", RegexOptions.ExplicitCapture, matchTimeoutMilliseconds: 1000)]
    private static partial Regex EnsureFirstCharacterUpperCase();
}

💉 DI Registration

// Program.cs
builder.Services.AddApiValidatorsFromDomain();

❌ ValidationProblemDetails Response

{
  "type": "https://tools.ietf.org/html/rfc9110#section-15.5.1",
  "title": "One or more validation errors occurred.",
  "status": 400,
  "errors": {
    "Request.FirstName": ["FirstName must start with an uppercase letter."],
    "Request.Address.City": ["Address.City must start with an uppercase letter."]
  }
}

🔍 Code Exploration Guide

📂 Where to Find Generated Code

sample/Showcase/
├── 📁 Showcase.Api.Contracts/
│   └── 📁 obj/Generated/                    # ⚙️ Generated server code
│       └── Atc.Rest.Api.SourceGenerator/
│           └── ApiServerGenerator/
│               ├── Showcase.Accounts.Models.g.cs
│               ├── Showcase.Accounts.Parameters.g.cs
│               ├── Showcase.Accounts.Results.g.cs
│               ├── Showcase.Accounts.Handlers.g.cs
│               ├── Showcase.Accounts.Endpoints.g.cs
│               ├── Showcase.Users.Models.g.cs
│               ├── Showcase.Users.Enums.g.cs
│               └── ... (other path segments)

🖥️ Where to Find Handler Implementations

sample/Showcase/
├── 📁 Showcase.Api.Domain/
│   └── 📁 ApiHandlers/
│       ├── 📁 Accounts/
│       │   ├── ListAccountsHandler.cs
│       │   ├── GetAccountByIdHandler.cs
│       │   └── CreateAccountHandler.cs
│       ├── 📁 Tasks/
│       │   └── ... (task handlers)
│       ├── 📁 Users/
│       │   └── ... (user handlers)
│       └── 📁 Files/
│           └── ... (file handlers)

🌐 Where to Find Blazor Pages

sample/Showcase/
├── 📁 Showcase.BlazorApp/
│   ├── 📁 Pages/
│   │   ├── 📁 Accounts/
│   │   │   ├── AccountsPage.razor      # 📋 List
│   │   │   ├── AccountDetails.razor    # 📄 Detail view
│   │   │   └── AccountForm.razor       # ✏️ Create/Edit
│   │   ├── 📁 Tasks/
│   │   │   └── ...
│   │   ├── 📁 Users/
│   │   │   └── ...
│   │   └── 📁 Files/
│   │       └── FilesPage.razor         # ⬆️ Upload demos
│   ├── 📁 Services/
│   │   └── GatewayService.cs           # 🔌 API client wrapper
│   └── 📁 Components/
│       └── ConfirmDialog.razor         # ⚠️ Reusable dialog

⚙️ Where to Find Configuration

sample/Showcase/
├── 📄 Showcase.yaml                        # 📝 OpenAPI specification
├── 📁 Showcase.Api.Contracts/
│   └── .atc-rest-api-server-contracts      # 🖥️ Server generator config
├── 📁 Showcase.Api.Domain/
│   └── .atc-rest-api-server-handlers       # 🛠️ Handler scaffold config
├── 📁 Showcase.BlazorApp/
│   ├── .atc-rest-api-client-contracts      # 📱 Client generator config
│   └── wwwroot/appsettings.json            # 🔗 API base URL
└── 📁 Showcase.Aspire/
    └── Program.cs                          # ☁️ Orchestration config

🔑 Key Implementation Patterns

🔒 Type-Safe Result Pattern

The generated Result classes ensure handlers can only return responses defined in the OpenAPI spec:

// GetAccountByIdResult has ONLY these factory methods:
// - Ok(Account) for 200
// - NotFound() for 404
// (because those are the only responses in the OpenAPI spec)

public class GetAccountByIdHandler : IGetAccountByIdHandler
{
    public async Task<GetAccountByIdResult> ExecuteAsync(
        GetAccountByIdParameters parameters,
        CancellationToken ct)
    {
        var account = await repository.GetByIdAsync(parameters.Id, ct);

        // ✅ Clean syntax with implicit operator
        if (account is not null)
            return account;  // Implicitly converts to Ok(account)

        // ✅ Explicit factory method
        return GetAccountByIdResult.NotFound();

        // ❌ COMPILE ERROR - Method doesn't exist!
        // return GetAccountByIdResult.InternalServerError();
    }
}

✨ Benefits:

  • 🛡️ Compile-time enforcement of OpenAPI contract
  • 📝 Clean code with implicit operators (return account;)
  • 💡 IDE support - IntelliSense shows only valid responses
  • 🔧 Refactoring safety - Change OpenAPI, get compile errors

🔌 GatewayService Pattern

The Blazor app uses a GatewayService that wraps all generated endpoints:

// Using [EndpointRegistration] attribute
[EndpointRegistration("Showcase")]
public sealed partial class GatewayService
{
    // 🔄 Fields and constructor auto-generated!
    // All 22 endpoint dependencies injected automatically
}

// Partial class methods organized by path segment
public partial class GatewayService
{
    public Task<Account[]?> ListAccountsAsync(CancellationToken ct = default)
        => listAccountsEndpoint.ExecuteAsync(ct);
}

🔗 Related Documentation

Clone this wiki locally