From e5709238219d7c87870bcabe066bc5924d8b1207 Mon Sep 17 00:00:00 2001 From: mkarim Date: Fri, 9 Oct 2026 16:03:01 +0300 Subject: [PATCH] docs: document Native AOT support and fix [PromptBindable] examples (#412) --- README.md | 13 +++++ docs/en/advanced.md | 76 +++++++++++++++++++++++++++ docs/en/prompt-types/bind.md | 60 +++++++++++++++++---- docs/ja/advanced.md | 76 +++++++++++++++++++++++++++ docs/ja/prompt-types/bind.md | 60 +++++++++++++++++---- samples/Sharprompt.Example/Program.cs | 6 ++- 6 files changed, 268 insertions(+), 23 deletions(-) diff --git a/README.md b/README.md index efd13d7..39cbe5c 100644 --- a/README.md +++ b/README.md @@ -118,8 +118,15 @@ Console.WriteLine($"You picked {string.Join(", ", value)}"); ### Bind (Model-based prompts) +Bind automatically generates a sequence of prompts from a model class. The model must be a `class` with a public parameterless constructor and accessible property setters, annotated with `[PromptBindable]`: + ```csharp +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using Sharprompt; + // Input model definition +[PromptBindable] public class MyFormModel { [Display(Name = "What's your name?")] @@ -137,8 +144,14 @@ public class MyFormModel [InlineItems("Seattle", "London", "Tokyo")] public string City { get; set; } + [Display(Name = "Select favorite fruit")] + [MemberItems(nameof(Fruits))] + public string Fruit { get; set; } + [Display(Name = "Are you ready?")] public bool? Ready { get; set; } + + public static IEnumerable Fruits => new[] { "Apple", "Banana", "Orange" }; } var result = Prompt.Bind(); diff --git a/docs/en/advanced.md b/docs/en/advanced.md index 21d2869..1d3308b 100644 --- a/docs/en/advanced.md +++ b/docs/en/advanced.md @@ -83,3 +83,79 @@ var city = Prompt.Select(new SelectOptions LoopingSelection = false }); ``` + +## Native AOT & Source Generators + +Sharprompt is configured with `true` and supports Native AOT compilation on .NET 8 and .NET 10. + +### How It Works + +Sharprompt uses Roslyn Incremental Source Generators to generate code at compile time, eliminating runtime reflection: + +1. **`[PromptBindable]` Generator**: Inspects classes annotated with `[PromptBindable]` and generates strongly typed model binding code registered via a C# `[ModuleInitializer]`. When invoking `Prompt.Bind()`, Sharprompt uses the precompiled binder with zero reflection. +2. **Enum Metadata Generator**: Inspects enum declarations in your project and generates metadata (values, `[Display]` names, and ordering) registered via a `[ModuleInitializer]`. + +### Project Configuration & Publishing + +To publish an application with Native AOT, add `true` to your project file: + +```xml + + + + Exe + net8.0 + true + + + + + + + +``` + +Publish your application using the `dotnet publish` command targeting your runtime: + +```sh +dotnet publish -c Release -r win-x64 +``` + +Replace `win-x64` with your target runtime identifier (such as `linux-x64` or `osx-arm64`). + +### Enum Handling & External Assemblies + +- **Project Enums**: Any enum declared within the consuming project is automatically analyzed by the source generator at compile time. Display names and custom ordering specified with `[Display]` are generated directly into code. +- **External Enums**: Enums from external assemblies (such as BCL enums like `DayOfWeek` or enums from referenced packages) are not analyzed by the source generator. At runtime, Sharprompt falls back to an AOT-safe reflection mechanism. To avoid reflection entirely or customize the display, register the enum manually at startup: + +```csharp +using Sharprompt; + +EnumMetadataRegistry.Register( + Enum.GetValues(), + day => day.ToString() +); +``` + +### Type Conversion & Custom Types + +Prompts rely on `TypeDescriptor` converters to parse user input. Built-in intrinsic converters (such as `string`, numeric types, `bool`, `char`, `Guid`, `DateTime`, and enums) are fully preserved under Native AOT. + +If using custom types annotated with `[TypeConverter]`, ensure your application preserves the converter type from trimming (for example, using `[DynamicallyAccessedMembers]` or trimming configuration). + +### Cancellation + +All prompts, including model binding, support cancellation via `CancellationToken`. Pass the token to `Prompt.Bind`: + +```csharp +using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(30)); + +try +{ + var form = Prompt.Bind(cts.Token); +} +catch (OperationCanceledException) +{ + Console.WriteLine("Operation timed out or was canceled."); +} +``` diff --git a/docs/en/prompt-types/bind.md b/docs/en/prompt-types/bind.md index 41e52e2..09b2758 100644 --- a/docs/en/prompt-types/bind.md +++ b/docs/en/prompt-types/bind.md @@ -1,33 +1,42 @@ # Bind (Model-based Prompts) -The `Bind` prompt automatically generates prompts based on a model class using data annotations. +The `Bind` prompt automatically generates a sequence of prompts based on a model class. ## Basic Usage -Define a model class with data annotations: +Define a model class with `[PromptBindable]` and data annotations: ```csharp +using System.Collections.Generic; using System.ComponentModel.DataAnnotations; +using Sharprompt; +[PromptBindable] public class MyFormModel { [Display(Name = "What's your name?")] [Required] - public string Name { get; set; } + public string Name { get; set; } = null!; [Display(Name = "Type new password")] [DataType(DataType.Password)] [Required] [MinLength(8)] - public string Password { get; set; } + public string Password { get; set; } = null!; [Display(Name = "Select your city")] [Required] [InlineItems("Seattle", "London", "Tokyo")] - public string City { get; set; } + public string City { get; set; } = null!; + + [Display(Name = "Select favorite fruit")] + [MemberItems(nameof(Fruits))] + public string Fruit { get; set; } = null!; [Display(Name = "Are you ready?")] public bool? Ready { get; set; } + + public static IEnumerable Fruits => new[] { "Apple", "Banana", "Orange" }; } ``` @@ -37,26 +46,55 @@ Then use `Prompt.Bind` to generate prompts for the model: var result = Prompt.Bind(); ``` +## Model Requirements + +To work with source-generated binding (including Native AOT): +- The model must be declared as a `class`. +- The class must have the `[PromptBindable]` attribute so the Roslyn source generator generates a binder at compile time. +- The class must have a public parameterless constructor when using `Prompt.Bind()`. Alternatively, pass an existing instance to `Prompt.Bind(model)`. +- Bound properties must be public instance properties with accessible setters (`public`, `internal`, or `protected internal`). Init-only or read-only properties are skipped. + ## How It Works -Sharprompt automatically determines the prompt type based on the property type and attributes: +Sharprompt automatically determines the prompt type based on property types and attributes: | Property Type / Attribute | Prompt Type | |--------------------------|-------------| | `string` | Input | | `string` with `[DataType(DataType.Password)]` | Password | -| `bool?` | Confirm | -| `string` with `[InlineItems(...)]` | Select | -| Enum type | Select | +| `bool` or `bool?` | Confirm | +| Single value with `[InlineItems(...)]` or `[MemberItems(...)]` | Select | +| `IEnumerable` with `[InlineItems(...)]` or `[MemberItems(...)]` | MultiSelect | +| Enum type | Select (or MultiSelect if collection) | +| `IEnumerable` (without items attribute) | List | ## Supported Attributes | Attribute | Description | |-----------|-------------| -| `[Display(Name = "...")]` | Sets the prompt message | +| `[PromptBindable]` | Marks the class for source-generated model binding | +| `[Display(Name = "...")]` | Sets prompt message (`Name`), placeholder (`Prompt`), and order (`Order`) | | `[Required]` | Makes the field required | | `[MinLength(n)]` | Sets minimum length validation | | `[MaxLength(n)]` | Sets maximum length validation | | `[DataType(DataType.Password)]` | Uses password prompt | -| `[InlineItems("a", "b", "c")]` | Provides items for select prompt | +| `[InlineItems("a", "b", "c")]` | Provides static inline items for selection | +| `[MemberItems(nameof(Member))]` | Provides items dynamically from a static property or method | | `[BindIgnore]` | Skips the property during binding | + +## Cancellation + +You can pass a `CancellationToken` to cancel model binding at any point: + +```csharp +using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(30)); + +try +{ + var result = Prompt.Bind(cts.Token); +} +catch (OperationCanceledException) +{ + Console.WriteLine("Prompt binding was canceled."); +} +``` diff --git a/docs/ja/advanced.md b/docs/ja/advanced.md index d7a6be0..412c8d5 100644 --- a/docs/ja/advanced.md +++ b/docs/ja/advanced.md @@ -83,3 +83,79 @@ var city = Prompt.Select(new SelectOptions LoopingSelection = false }); ``` + +## Native AOT とソースジェネレーター + +Sharprompt は `true` に対応しており、.NET 8 および .NET 10 で Native AOT コンパイルをサポートしています。 + +### 仕組み + +Sharprompt は Roslyn インクリメンタルソースジェネレーターを利用してコンパイル時にコードを生成し、実行時のリフレクションを排除しています: + +1. **`[PromptBindable]` ジェネレーター**: `[PromptBindable]` 属性が付与されたクラスを解析し、C# の `[ModuleInitializer]` 経由で登録される型安全なモデルバインダーコードを生成します。`Prompt.Bind()` の呼び出し時、リフレクションなしで事前コンパイルされたバインダーが直接実行されます。 +2. **Enum メタデータジェネレーター**: プロジェクト内の enum 宣言を解析し、値の一覧や `[Display]` 属性(表示名・並び順)を `[ModuleInitializer]` 経由で登録するコードを生成します。 + +### プロジェクト構成と発行 + +Native AOT でアプリケーションを発行するには、プロジェクトファイルに `true` を追加します: + +```xml + + + + Exe + net8.0 + true + + + + + + + +``` + +対象のランタイムを指定して `dotnet publish` コマンドを実行します: + +```sh +dotnet publish -c Release -r win-x64 +``` + +※ `win-x64` はターゲットのランタイム識別子(例: `linux-x64`、`osx-arm64`)に適宜置き換えてください。 + +### Enum の取り扱いと外部アセンブリ + +- **同一プロジェクト内の Enum**: 同一プロジェクト内で宣言された enum は、コンパイル時にソースジェネレーターによって自動検出されます。`[Display]` で指定された表示名や並び順もコードとして直接生成されます。 +- **外部アセンブリの Enum**: 外部アセンブリの enum(BCL の `DayOfWeek` や参照パッケージの enum など)はソースジェネレーターの解析対象外です。実行時、Sharprompt は Native AOT に配慮したリフレクションフォールバックを使用します。リフレクションを完全に排除したい場合や表示をカスタマイズしたい場合は、アプリケーション起動時に手動で登録できます: + +```csharp +using Sharprompt; + +EnumMetadataRegistry.Register( + Enum.GetValues(), + day => day.ToString() +); +``` + +### 型変換とカスタム型 + +プロンプト入力の型変換は `TypeDescriptor` コンバーターを利用しています。組み込みの標準コンバーター(`string`、数値型、`bool`、`char`、`Guid`、`DateTime`、enum、およびこれらの Nullable)は Native AOT 環境でも完全に保持されます。 + +`[TypeConverter]` 属性を持つ独自のカスタム型を使用する場合は、トリミングによってコンバーター型が削除されないよう、アプリケーション側で型を保持する設定(`[DynamicallyAccessedMembers]` やトリマー記述ファイルなど)を行ってください。 + +### キャンセル + +モデルバインディングを含むすべてのプロンプトで `CancellationToken` によるキャンセルをサポートしています。`Prompt.Bind` にトークンを渡します: + +```csharp +using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(30)); + +try +{ + var form = Prompt.Bind(cts.Token); +} +catch (OperationCanceledException) +{ + Console.WriteLine("操作がタイムアウトまたはキャンセルされました。"); +} +``` diff --git a/docs/ja/prompt-types/bind.md b/docs/ja/prompt-types/bind.md index b5a95c0..ac09dba 100644 --- a/docs/ja/prompt-types/bind.md +++ b/docs/ja/prompt-types/bind.md @@ -1,42 +1,59 @@ # Bind (モデルベースプロンプト) -`Bind` プロンプトはデータアノテーションを使用してモデルクラスに基づいたプロンプトを自動生成します。 +`Bind` プロンプトは、モデルクラスに基づいて一連のプロンプトを自動生成します。 ## 基本的な使い方 -データアノテーション付きのモデルクラスを定義します: +`[PromptBindable]` とデータアノテーションを付与したモデルクラスを定義します: ```csharp +using System.Collections.Generic; using System.ComponentModel.DataAnnotations; +using Sharprompt; +[PromptBindable] public class MyFormModel { [Display(Name = "What's your name?")] [Required] - public string Name { get; set; } + public string Name { get; set; } = null!; [Display(Name = "Type new password")] [DataType(DataType.Password)] [Required] [MinLength(8)] - public string Password { get; set; } + public string Password { get; set; } = null!; [Display(Name = "Select your city")] [Required] [InlineItems("Seattle", "London", "Tokyo")] - public string City { get; set; } + public string City { get; set; } = null!; + + [Display(Name = "Select favorite fruit")] + [MemberItems(nameof(Fruits))] + public string Fruit { get; set; } = null!; [Display(Name = "Are you ready?")] public bool? Ready { get; set; } + + public static IEnumerable Fruits => new[] { "Apple", "Banana", "Orange" }; } ``` -`Prompt.Bind` を使用してモデルのプロンプトを生成します: +`Prompt.Bind` を使用してモデルのプロンプトを実行します: ```csharp var result = Prompt.Bind(); ``` +## モデルの要件 + +ソースジェネレーターによるモデルバインディング(Native AOT を含む)を利用するには、以下の要件を満たす必要があります: +- `class` として宣言されている必要があります。 +- クラスに `[PromptBindable]` 属性が付与されている必要があります(コンパイル時に Roslyn ソースジェネレーターがバインダーを生成します)。 +- `Prompt.Bind()` を呼び出す場合は、パブリックなパラメーターなしコンストラクターが必要です(インスタンスを渡す `Prompt.Bind(model)` を使用することも可能です)。 +- バインド対象のプロパティは、アクセス可能なセッター(`public`、`internal`、または `protected internal`)を持つパブリックインスタンスプロパティである必要があります。読み取り専用や init-only プロパティはスキップされます。 + ## 仕組み Sharprompt はプロパティの型と属性に基づいてプロンプトタイプを自動的に決定します: @@ -45,18 +62,39 @@ Sharprompt はプロパティの型と属性に基づいてプロンプトタイ |--------------------------|-------------| | `string` | Input | | `string` + `[DataType(DataType.Password)]` | Password | -| `bool?` | Confirm | -| `string` + `[InlineItems(...)]` | Select | -| Enum 型 | Select | +| `bool` または `bool?` | Confirm | +| 単一値 + `[InlineItems(...)]` または `[MemberItems(...)]` | Select | +| `IEnumerable` + `[InlineItems(...)]` または `[MemberItems(...)]` | MultiSelect | +| Enum 型 | Select (コレクションの場合は MultiSelect) | +| `IEnumerable` (項目指定なし) | List | ## サポートされている属性 | 属性 | 説明 | |-----------|-------------| -| `[Display(Name = "...")]` | プロンプトメッセージを設定 | +| `[PromptBindable]` | ソース生成モデルバインディングの対象としてクラスを指定 | +| `[Display(Name = "...")]` | プロンプトメッセージを設定(`Prompt` でプレースホルダー、`Order` で表示順) | | `[Required]` | フィールドを必須にする | | `[MinLength(n)]` | 最小文字数のバリデーションを設定 | | `[MaxLength(n)]` | 最大文字数のバリデーションを設定 | | `[DataType(DataType.Password)]` | パスワードプロンプトを使用 | -| `[InlineItems("a", "b", "c")]` | Select プロンプトの項目を提供 | +| `[InlineItems("a", "b", "c")]` | インライン配列で選択項目を提供 | +| `[MemberItems(nameof(Member))]` | 静的プロパティまたはメソッドから動的に項目を提供 | | `[BindIgnore]` | バインディング時にプロパティをスキップ | + +## キャンセル + +`CancellationToken` を渡すことで、モデルバインディング全体をキャンセルできます: + +```csharp +using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(30)); + +try +{ + var result = Prompt.Bind(cts.Token); +} +catch (OperationCanceledException) +{ + Console.WriteLine("プロンプトのバインディングがキャンセルされました。"); +} +``` diff --git a/samples/Sharprompt.Example/Program.cs b/samples/Sharprompt.Example/Program.cs index f2b30e6..dc91cb2 100644 --- a/samples/Sharprompt.Example/Program.cs +++ b/samples/Sharprompt.Example/Program.cs @@ -103,6 +103,10 @@ private static void RunListSample() private static void RunBindSample() { var model = Prompt.Bind(); - Console.WriteLine($"Forms OK, {JsonSerializer.Serialize(model)}"); + Console.WriteLine($"Forms OK, {JsonSerializer.Serialize(model, SampleJsonSerializerContext.Default.MyFormModel)}"); } } + +[System.Text.Json.Serialization.JsonSerializable(typeof(MyFormModel))] +internal partial class SampleJsonSerializerContext : System.Text.Json.Serialization.JsonSerializerContext; +