-
Notifications
You must be signed in to change notification settings - Fork 0
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.
.NET Aspire orchestrates all projects together:
cd sample/Showcase
# Run the Aspire AppHost
dotnet run --project Showcase.Aspire/Showcase.Aspire.csprojOpen the Aspire Dashboard at https://localhost:17037 to see:
- 🖥️ Showcase.Api - The REST API
- 🌐 Showcase.BlazorApp - Blazor WebAssembly UI
- 💻 Showcase.ClientApp - Console client (optional)
cd sample/Showcase
# Start the API
dotnet run --project Showcase.Api/Showcase.Api.csprojThen access:
- 📖 Swagger UI:
https://localhost:7100/swagger - ⚡ Scalar API Docs:
https://localhost:7100/scalar - 📄 OpenAPI Spec:
https://localhost:7100/openapi/v1.json
# 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.csprojThe Showcase API demonstrates a variety of patterns:
| 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 |
| 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: TheTaskmodel conflicts withSystem.Threading.Tasks.Task. The generator automatically uses fully qualified names.
| 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 byUserRoleenum -
isActive- Filter by status
| 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 returnsBinaryEndpointResponsedirectly from theAtc.Rest.Clientlibrary.
The Blazor app demonstrates a complete client-side implementation:
- 🎭 MudBlazor v8 - Material Design components
- 🌙 Light/Dark Mode - Toggle in app bar
- 📱 Responsive Layout - Works on mobile and desktop
The nav menu is organized by API path segment:
- 💼 Accounts
- ✅ Tasks
- 👥 Users
- 📁 Files
📋 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] |
+--------------------------------------+
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);
}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>The Accounts section shows real-time streaming:
await foreach (var account in AccountsClient.ListAccountsAsAsyncEnumerableAsync())
{
accounts.Add(account);
StateHasChanged(); // Update UI as each item arrives
}The Showcase sample demonstrates FluentValidation integration with the ValidationFilter<T> from Atc.Rest.MinimalApi.
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();
}// Program.cs
builder.Services.AddApiValidatorsFromDomain();{
"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."]
}
}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)
sample/Showcase/
├── 📁 Showcase.Api.Domain/
│ └── 📁 ApiHandlers/
│ ├── 📁 Accounts/
│ │ ├── ListAccountsHandler.cs
│ │ ├── GetAccountByIdHandler.cs
│ │ └── CreateAccountHandler.cs
│ ├── 📁 Tasks/
│ │ └── ... (task handlers)
│ ├── 📁 Users/
│ │ └── ... (user handlers)
│ └── 📁 Files/
│ └── ... (file handlers)
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
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
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
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);
}- Getting Started - Set up your own project
- Working with OpenAPI - YAML patterns used here
- Working with Validations - DataAnnotations and FluentValidation
- Roadmap - Upcoming features