Chd.AutoUI 8.0.4

There is a newer version of this package available.
See the version list below for details.
dotnet add package Chd.AutoUI --version 8.0.4
                    
NuGet\Install-Package Chd.AutoUI -Version 8.0.4
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="Chd.AutoUI" Version="8.0.4" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="Chd.AutoUI" Version="8.0.4" />
                    
Directory.Packages.props
<PackageReference Include="Chd.AutoUI" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add Chd.AutoUI --version 8.0.4
                    
#r "nuget: Chd.AutoUI, 8.0.4"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package Chd.AutoUI@8.0.4
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=Chd.AutoUI&version=8.0.4
                    
Install as a Cake Addin
#tool nuget:?package=Chd.AutoUI&version=8.0.4
                    
Install as a Cake Tool

Chd.AutoUI – Attribute-Driven UI Metadata for .NET

Define your UI once in C#. Let the library handle the rest.

NuGet License: MIT


πŸ“ Table of Contents


About

Chd.AutoUI is a .NET library that generates UI metadata from C# attributes on your DTOs. You annotate your classes with [AutoCRUD], [GridColumn], and [FormField], and the library exposes that information as a JSON endpoint. Any frontend that can consume a REST API can use that metadata to render forms, grids, and validation rules without you writing them manually.

The workflow is: define once in C#, render anywhere in the frontend. When a field changes on the backend, the UI reflects it automatically on the next request β€” no JavaScript to update, no configuration files to maintain.


🎯 Features

  • Attribute-driven: [AutoCRUD], [GridColumn], [FormField]
  • 20+ field types: text, number, date, dropdown, multiselect, radio, file, tag input, autocomplete, rich text editor, image preview, tree select, color picker, date range picker, stepper
  • Role-based permissions via [CreatePermission], [UpdatePermission], [DeletePermission]
  • Auto-detection of foreign key relationships for dropdown fields (e.g. CategoryId β†’ categories endpoint)
  • Built-in JWT login endpoint β€” implement one interface, get a working login flow
  • Metadata and identity endpoints registered automatically in a single UseAutoUI<T> call
  • No code generation, no build steps β€” pure reflection at runtime
  • Works with any frontend framework (React, Vue, Angular, Blazor)

πŸ“¦ Installation

dotnet add package Chd.AutoUI

βš™οΈ How It Works

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚  Your C# DTO                                        β”‚
β”‚  [AutoCRUD] [GridColumn] [FormField] [Permissions]  β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                        β”‚  reflection at runtime
                        β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚  MetadataGenerator                                  β”‚
β”‚  ScanAssemblyForEntities() / GenerateMetadata()     β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                        β”‚  JSON
                        β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚  REST Endpoints (auto-registered)                   β”‚
β”‚  GET /api/metadata                                  β”‚
β”‚  GET /api/metadata/{entityName}                     β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                        β”‚  consumed by frontend
                        β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚  Frontend (React / Vue / Angular / Blazor)          β”‚
β”‚  DynamicGrid  DynamicForm  Permission-aware UI      β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

πŸš€ Quick Start

1. Annotate Your DTO

using Chd.AutoUI.Attributes;

[AutoCRUD(Title = "Products", Icon = "shopping-bag", Route = "/products", Description = "Manage your product catalog")]
[CreatePermission("Admin", "Manager")]
[UpdatePermission("Admin", "Editor")]
[DeletePermission("Admin")]
public class ProductDto
{
    // Read-only field shown in grid but not editable in form
    [GridColumn(Order = 1, Width = 80, Sortable = false)]
    [FormField(ReadOnly = true)]
    public int Id { get; set; }

    [GridColumn(Order = 2, Width = 200)]
    [FormField(Label = "Product Name", Type = FieldType.Text, Required = true, MaxLength = 100, Order = 1)]
    public string Name { get; set; } = string.Empty;

    [GridColumn(Order = 3, Width = 300)]
    [FormField(Label = "Description", Type = FieldType.TextArea, MaxLength = 500, Order = 2)]
    public string? Description { get; set; }

    [GridColumn(Order = 4, Width = 150, Format = "currency")]
    [FormField(Label = "Price", Type = FieldType.Number, Required = true, Order = 3)]
    public decimal Price { get; set; }

    // Dropdown with remote data β€” RelatedEntity points to the API endpoint
    [GridColumn(Order = 5, Width = 150)]
    [FormField(Label = "Category", Type = FieldType.Dropdown, Order = 4, RelatedEntity = "categories")]
    public int? CategoryId { get; set; }

    // Auto-detected: property ends with "Id" so MetadataGenerator infers the endpoint automatically
    [GridColumn(Order = 6, Width = 150)]
    [FormField(Label = "Supplier", Type = FieldType.Dropdown, Order = 5)]
    public int? SupplierId { get; set; }

    // Radio with static options
    [GridColumn(Order = 7, Width = 120)]
    [FormField(Label = "Status", Type = FieldType.Radio, Required = true, Order = 6,
        Options = new[] { "Active", "Inactive", "Discontinued" })]
    public string Status { get; set; } = "Active";

    // Checkbox
    [GridColumn(Order = 8, Width = 100)]
    [FormField(Label = "Featured", Type = FieldType.Checkbox, Order = 7)]
    public bool IsFeatured { get; set; }

    // Tag input β€” stored as comma-separated string on the backend
    [GridColumn(Order = 9, Width = 200)]
    [FormField(Label = "Tags", Type = FieldType.TagInput, Order = 8)]
    public string? Tags { get; set; }

    // Date picker
    [GridColumn(Order = 10, Width = 130)]
    [FormField(Label = "Expiry Date", Type = FieldType.Date, Order = 9)]
    public DateTime? ExpiryDate { get; set; }
}

2. Implement IUserTokenProvider

UseAutoUI registers a POST /api/account/login endpoint that issues JWT tokens. You control the authentication logic by implementing IUserTokenProvider. The interface has a single async method β€” validate credentials, build the UserDTO, and return it. Return null to reject the login.

using Chd.AutoUI.Extensions;
using Chd.Security.DTOs;
using Chd.Security.Models;

public class AppUserTokenProvider : IUserTokenProvider
{
    private readonly AppDbContext _db;
    private readonly IPasswordHasher _hasher;

    public AppUserTokenProvider(AppDbContext db, IPasswordHasher hasher)
    {
        _db = db;
        _hasher = hasher;
    }

    public async Task<UserDTO?> GetUserTokenInfoAsync(UserModel model)
    {
        var user = await _db.Users
            .Include(u => u.Roles)
            .FirstOrDefaultAsync(u => u.UserName == model.UserName);

        if (user == null || !_hasher.Verify(model.Password, user.PasswordHash))
            return null;

        return new UserDTO
        {
            UserName = user.UserName,
            Roles = user.Roles.Select(r => r.Name).ToList(),
            ExpirationSecond = 3600
        };
    }
}

UserDTO properties:

Property Type Description
UserName string Becomes the JWT subject (sub claim)
Roles List<string> Each role maps to a role claim in the token
ExpirationSecond int? Token lifetime in seconds (default: 3600)

Because the provider is resolved from DI, you can inject DbContext, password hashers, external identity services, or anything else through the constructor.

3. Configure Program.cs

using Chd.AutoUI.EF.Services;
using Chd.AutoUI.Extensions;
using Serilog;

var builder = WebApplication.CreateBuilder(args);

// Logging (optional, but useful)
var logger = new LoggerConfiguration()
    .MinimumLevel.Debug()
    .WriteTo.Console()
    .CreateLogger();
builder.Logging.ClearProviders();
builder.Logging.AddSerilog(logger);

// Database
builder.Services.AddDbContext<AppDbContext>(options =>
    options.UseNpgsql(builder.Configuration.GetConnectionString("DefaultConnection")));

// Generic repository β€” one line covers all entities
builder.Services.AddScoped(typeof(IGenericRepository<>), typeof(GenericRepository<>));

// Controllers, Swagger
builder.Services.AddControllers();
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();

// CORS
builder.Services.AddCors(options =>
{
    options.AddDefaultPolicy(policy =>
        policy.WithOrigins("http://localhost:3000")
              .AllowAnyMethod()
              .AllowAnyHeader());
});

// UseAutoUI<TProvider> does the following in one call:
//   - Registers AppUserTokenProvider as IUserTokenProvider in DI
//   - Configures JWT middleware (UseJwtTokenAuthorization internally)
//   - Calls UseAuthentication() and UseAuthorization()
//   - Registers GET /api/metadata, GET /api/metadata/{entityName}, GET /api/me, POST /api/account/login
var app = builder.UseAutoUI<AppUserTokenProvider>(typeof(ProductDto).Assembly);

if (app.Environment.IsDevelopment())
{
    app.UseSwagger();
    app.UseSwaggerUI();
}

app.UseHttpsRedirection();
app.MapControllers();
app.Run();

Note: Do not call app.UseAuthentication() or app.UseAuthorization() again after UseAutoUI β€” the method already calls them internally in the correct order.


πŸ” Permission Management

One of the more useful things about the library is that permissions are defined where your data model is, not scattered across controllers and React components.

Defining Permissions in C#

[AutoCRUD(Title = "Products", Route = "/products")]
[CreatePermission("Admin", "Manager")]   // who can create
[UpdatePermission("Admin", "Editor")]    // who can edit
[DeletePermission("Admin")]              // who can delete
// no ReadPermission = everyone can read (open)
public class ProductDto { ... }

Roles are strings β€” they match the role claims in your JWT. The IUserTokenProvider you implement sets those roles when issuing a token.

What the Metadata Looks Like

When the frontend calls GET /api/metadata/ProductDto, the permissions are part of the response:

{
  "entityName": "ProductDto",
  "title": "Products",
  "permissions": {
    "create": ["Admin", "Manager"],
    "read": [],
    "update": ["Admin", "Editor"],
    "delete": ["Admin"]
  },
  "properties": [...]
}

An empty read array means no restrictions β€” everyone can see the list.

Consuming Permissions in React

The chd-auto-ui-react components read the permissions field from the metadata and compare it against the current user's roles (fetched from GET /api/me). The Create button is hidden if the user lacks the create role. The Edit and Delete buttons follow the same logic.

// The DynamicGrid component does this internally:
const can = (action: 'create' | 'update' | 'delete') => {
  const allowed = metadata.permissions?.[action] ?? [];
  if (allowed.length === 0) return true;             // no restriction
  return currentUser?.roles?.some(r => allowed.includes(r)) ?? false;
};

// Create button only renders if can('create') is true
// Edit/Delete only render if can('update') / can('delete') is true

No extra configuration needed in React β€” the permission flow is automatic once the attributes are on the DTO.


🌐 Exposed Endpoints

These are registered automatically when you call UseAutoUI:

Method Path Auth required Description
GET /api/metadata No All [AutoCRUD] entity metadata in the assembly
GET /api/metadata/{entityName} No Metadata for a single entity by class name
GET /api/me Yes (JWT) Current user β€” { id, name, roles }
POST /api/account/login No Issues a JWT token via IUserTokenProvider

Login request:

POST /api/account/login
Content-Type: application/json

{ "UserName": "admin", "Password": "your-password" }

Response:

{ "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." }

πŸ“‹ Attribute Reference

AutoCRUD

Applied to a class. Marks the DTO for inclusion in the metadata scan and defines entity-level UI settings.

[AutoCRUD(
    Title = "Products",
    Icon = "shopping-bag",
    Route = "/products",
    Description = "Manage your product catalog"
)]
public class ProductDto { }
Property Type Description
Title string Display name shown in headers and navigation
Icon string Icon identifier β€” emoji or a CSS class name (depends on your icon library)
Route string Frontend route path, e.g. /products
Description string Short description displayed below the title

GridColumn

Applied to a property. Controls whether and how the field appears in a data table.

[GridColumn(
    Order = 3,
    Width = 150,
    Sortable = true,
    Filterable = true,
    Format = "currency",
    Hidden = false
)]
public decimal Price { get; set; }
Property Type Default Description
Order int 0 Column position left to right
Width int 100 Column width in pixels
Sortable bool true Allows column sorting
Filterable bool false Allows column filtering
Format string null "currency", "date", "percent", "number"
Hidden bool false Hides the column from the grid but still includes it in metadata

FormField

Applied to a property. Controls whether and how the field appears in create/edit forms.

[FormField(
    Label = "Product Name",
    Type = FieldType.Text,
    Required = true,
    MaxLength = 100,
    Placeholder = "Enter product name",
    Order = 1,
    ReadOnly = false
)]
public string Name { get; set; } = string.Empty;
Property Type Default Description
Label string property name Field label text
Type FieldType Text Input type (see field types below)
Required bool false Marks the field as required
MaxLength int null Maximum allowed characters
Placeholder string null Placeholder text
Order int 0 Field position in the form
ReadOnly bool false Renders the field as non-editable
ValidationPattern string null Regex pattern for client-side validation
RelatedEntity string null For Dropdown/MultiSelect β€” API endpoint to load options from
DisplayProperty string "Name" Property name to show as option label
ValueProperty string "Id" Property name to use as option value
Options string[] null For Radio/Select β€” static list of options
Accept string null For File β€” accepted MIME types, e.g. "image/*"
Multiple bool false For File β€” allow multiple file selection
AutocompleteEndpoint string null For Autocomplete β€” search endpoint URL
AutocompleteMinChars int 2 For Autocomplete β€” minimum chars before search triggers
TreeEntity string null For TreeSelect β€” endpoint returning hierarchical data
AllowCustomTags bool false For TagInput β€” allow user-defined tags not in suggestions
TagSuggestions string[] null For TagInput β€” preset tag suggestions
ColorFormat string "hex" For ColorPicker β€” "hex" or "rgb"
ImageMaxWidth int null For ImagePreview β€” max width in pixels
ImageMaxHeight int null For ImagePreview β€” max height in pixels
IsRange bool false For DateRangePicker β€” enables two-date selection
EditorConfig object null For RichTextEditor β€” configuration passed to the editor

Field Types

Type Description Key Options
Text Single-line text MaxLength, Placeholder, ValidationPattern
Number Numeric input β€”
Email Email address ValidationPattern
Password Password (masked) MaxLength
TextArea Multi-line text MaxLength, Placeholder
Date Date picker β€”
DateTime Date and time picker β€”
Checkbox Boolean toggle β€”
Radio Radio button group Options (required)
Dropdown Single-select dropdown RelatedEntity or Options, DisplayProperty, ValueProperty
MultiSelect Multi-select with chips RelatedEntity, DisplayProperty, ValueProperty
File File upload Accept, Multiple
TagInput Freeform tag entry AllowCustomTags, TagSuggestions
Autocomplete Search-as-you-type AutocompleteEndpoint, AutocompleteMinChars
RichTextEditor WYSIWYG HTML editor EditorConfig
ImagePreview Image upload + preview Accept, ImageMaxWidth, ImageMaxHeight
TreeSelect Hierarchical select TreeEntity
ColorPicker Color selector ColorFormat
DateRangePicker Two-date range IsRange = true
Stepper Multi-step form grouping β€”

Permission Attributes

Applied to a class. The metadata generator reads these and includes them in the permissions object of the JSON output.

[CreatePermission("Admin", "Manager")]
[UpdatePermission("Admin", "Editor")]
[DeletePermission("Admin")]
public class ProductDto { }

If neither custom permission attributes nor [Authorize] are present on the class, all actions are permitted by default.


πŸ“Š JSON Metadata Format

This is what GET /api/metadata/ProductDto returns:

{
  "entityName": "ProductDto",
  "title": "Products",
  "icon": "shopping-bag",
  "route": "/products",
  "description": "Manage your product catalog",
  "permissions": {
    "create": ["Admin", "Manager"],
    "read": [],
    "update": ["Admin", "Editor"],
    "delete": ["Admin"]
  },
  "properties": [
    {
      "name": "Id",
      "type": "number",
      "grid": { "order": 1, "width": 80, "sortable": false, "hidden": false },
      "form": { "readOnly": true, "order": 0 }
    },
    {
      "name": "Name",
      "type": "string",
      "grid": { "order": 2, "width": 200, "sortable": true },
      "form": {
        "label": "Product Name",
        "type": "text",
        "required": true,
        "maxLength": 100,
        "order": 1
      }
    },
    {
      "name": "CategoryId",
      "type": "number",
      "grid": { "order": 5, "width": 150 },
      "form": {
        "label": "Category",
        "type": "dropdown",
        "relatedEntity": "categories",
        "displayProperty": "Name",
        "valueProperty": "Id",
        "order": 4
      }
    }
  ]
}

πŸ”¬ Advanced Usage

Assembly Scanning

Scan an entire assembly to get metadata for all [AutoCRUD] types at once:

var generator = new MetadataGenerator();
var all = generator.ScanAssemblyForEntities(typeof(ProductDto).Assembly);
// returns List<EntityMetadata> for every class decorated with [AutoCRUD]

This is what the /api/metadata endpoint uses internally.

Auto-Detection of Foreign Keys

If a form field is a Dropdown or MultiSelect and the property name ends with Id, the library infers the related endpoint automatically:

// No RelatedEntity needed β€” "SupplierId" β†’ MetadataGenerator sets relatedEntity to "suppliers"
[FormField(Label = "Supplier", Type = FieldType.Dropdown, Order = 5)]
public int? SupplierId { get; set; }

You can still set RelatedEntity explicitly if your endpoint name does not follow the convention.

Obsolete Overload

The old UseAutoUI(assembly, Func<UserModel, UserDTO>) overload still works but is marked [Obsolete]. Migrate to UseAutoUI<TProvider>(assembly) when you get the chance β€” it integrates properly with DI and supports async validation.

// Old (still works, but obsolete)
var app = builder.UseAutoUI(assembly, UserRepository.GetUserTokenInfo);

// New (recommended)
var app = builder.UseAutoUI<UserRepository>(typeof(ProductDto).Assembly);

πŸ—ΊοΈ Roadmap

  • React integration (chd-auto-ui-react)
  • Role-based permission attributes
  • Built-in JWT authentication endpoint
  • Auto-detection of foreign key dropdown relationships
  • Vue.js support
  • Angular support
  • Blazor support
  • Improved localization and multi-language labels
  • Advanced grid features (grouping, virtual scroll, export)

Project Description
chd-auto-ui-react React components that render the metadata as forms, grids, and login
Chd.AutoUI.EF Generic EF Core repository β€” pairs naturally with the metadata setup
Chd.Pos.Api Full POS demo showing everything working end-to-end

πŸ“„ License

MIT

Product Compatible and additional computed target framework versions.
.NET net8.0 is compatible.  net8.0-android was computed.  net8.0-browser was computed.  net8.0-ios was computed.  net8.0-maccatalyst was computed.  net8.0-macos was computed.  net8.0-tvos was computed.  net8.0-windows was computed.  net9.0 was computed.  net9.0-android was computed.  net9.0-browser was computed.  net9.0-ios was computed.  net9.0-maccatalyst was computed.  net9.0-macos was computed.  net9.0-tvos was computed.  net9.0-windows was computed.  net10.0 was computed.  net10.0-android was computed.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-maccatalyst was computed.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
8.0.5 38 4/6/2026
8.0.4 40 4/5/2026
8.0.3 39 4/5/2026
8.0.2 170 3/28/2026
8.0.0 88 3/24/2026
1.0.0 108 3/24/2026