diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json new file mode 100644 index 0000000..5488879 --- /dev/null +++ b/.claude-plugin/marketplace.json @@ -0,0 +1,21 @@ +{ + "name": "arbiter", + "owner": { + "name": "LoreSoft", + "url": "https://github.com/loresoft" + }, + "metadata": { + "description": "Claude Code plugins for the Arbiter .NET libraries — mediation, CQRS, mapping, Blazor dispatcher, communication, and more.", + "version": "1.0.0" + }, + "plugins": [ + { + "name": "arbiter-skills", + "source": "./plugins/arbiter-skills", + "description": "Skills that teach Claude how to install, register, and use the Arbiter family of .NET libraries (Mediation, CommandQuery + EF/Mongo, Mapping, Endpoints/Mvc, Dispatcher, Communication, Services, OpenTelemetry, Messaging.ServiceBus).", + "version": "1.0.0", + "category": "dotnet", + "tags": ["dotnet", "cqrs", "mediator", "blazor", "arbiter"] + } + ] +} diff --git a/plugins/arbiter-skills/.claude-plugin/plugin.json b/plugins/arbiter-skills/.claude-plugin/plugin.json new file mode 100644 index 0000000..7256cb8 --- /dev/null +++ b/plugins/arbiter-skills/.claude-plugin/plugin.json @@ -0,0 +1,13 @@ +{ + "name": "arbiter-skills", + "version": "1.0.0", + "description": "Skills that teach Claude how to install, register, and use the Arbiter family of .NET libraries (Mediation, CommandQuery + EF/Mongo, Mapping, Endpoints/Mvc, Dispatcher, Communication, Services, OpenTelemetry, Messaging.ServiceBus).", + "author": { + "name": "LoreSoft", + "url": "https://github.com/loresoft" + }, + "homepage": "https://github.com/loresoft/Arbiter", + "repository": "https://github.com/loresoft/Arbiter", + "license": "MIT", + "keywords": ["dotnet", "cqrs", "mediator", "blazor", "arbiter"] +} diff --git a/plugins/arbiter-skills/README.md b/plugins/arbiter-skills/README.md new file mode 100644 index 0000000..abeacf5 --- /dev/null +++ b/plugins/arbiter-skills/README.md @@ -0,0 +1,37 @@ +# Arbiter Skills + +A Claude Code plugin that teaches Claude how to use the [Arbiter](https://github.com/loresoft/Arbiter) family of .NET libraries. + +When installed, Claude auto-loads the relevant skill based on what you're asking about — e.g. asking *"how do I register an EF Core handler for my Product entity?"* loads `arbiter-commandquery-ef` and Claude responds with the canonical `AddEntityQueries` / `AddEntityCommands` pattern. + +## Install + +```bash +# Add this repo as a marketplace +/plugin marketplace add loresoft/arbiter + +# Install the skills bundle +/plugin install arbiter-skills@arbiter +``` + +## Skills included + +| Skill | When it loads | +| --- | --- | +| `arbiter-overview` | Routing skill — explains the Arbiter package landscape and points to the right specialist skill | +| `arbiter-mediation` | `IMediator`, `IRequest`, `IRequestHandler`, `INotification`, `IPipelineBehavior`, `AddMediator` | +| `arbiter-commandquery` | `EntityQuery`, `EntityFilter`, `FilterOperators`, `EntityPagedQuery`, `EntityIdentifierQuery`, behaviors | +| `arbiter-commandquery-ef` | Entity Framework Core handler registration (`AddEntityQueries`, `AddEntityCommands`) | +| `arbiter-commandquery-mongo` | MongoDB handler registration | +| `arbiter-endpoints` | Minimal API integration (`AddEndpointRoutes`, `MapEndpointRoutes`, `EntityCommandEndpointBase`) | +| `arbiter-mvc` | MVC controller integration | +| `arbiter-mapping` | Source-generated mapper (`[GenerateMapper]`, `MapperProfile`, `ProjectTo`) | +| `arbiter-dispatcher` | Blazor `IDispatcher`, WASM/Server/Auto setup, `ModelStateEditor` | +| `arbiter-communication` | Email/SMS templates + Azure / Graph / Twilio / SendGrid / SMTP providers | +| `arbiter-services` | CSV, AES encryption, cache tagging, token service, URL builder | +| `arbiter-opentelemetry` | Tracing/metrics with `MediatorTelemetry`, server + monitor packages | +| `arbiter-messaging-servicebus` | Azure Service Bus integration | + +## Authoring conventions + +Each skill is cheat-sheet style: a `description` frontmatter line that drives auto-loading, then a tight body with **When to use → Install → Register → Canonical pattern → Variations → Reference**. Keep skills under ~200 lines; link to `docs/guide/*.md` in the Arbiter repo for depth. diff --git a/plugins/arbiter-skills/skills/arbiter-commandquery-ef/SKILL.md b/plugins/arbiter-skills/skills/arbiter-commandquery-ef/SKILL.md new file mode 100644 index 0000000..2064c2a --- /dev/null +++ b/plugins/arbiter-skills/skills/arbiter-commandquery-ef/SKILL.md @@ -0,0 +1,97 @@ +--- +name: arbiter-commandquery-ef +description: Use when wiring Arbiter.CommandQuery with Entity Framework Core — registering AddEntityQueries / AddEntityCommands / AddQueryPipeline for an entity, defining DbContext-backed handlers, or implementing audited / soft-delete / tenant entities for EF Core. +--- + +# Arbiter.CommandQuery.EntityFramework + +EF Core implementations of the generic `Arbiter.CommandQuery` handlers. Provides ready-made handlers for `EntityIdentifierQuery`, `EntityPagedQuery`, and the create/update/patch/delete commands against a `DbContext`. + +## Install + +```bash +dotnet add package Arbiter.CommandQuery.EntityFramework +``` + +Pairs with `Arbiter.CommandQuery` (base) and `Arbiter.Mapping` (read/update model mapping). + +## Register + +```csharp +using Arbiter.CommandQuery; +using Arbiter.CommandQuery.EntityFramework; + +// EF Core DbContext +services.AddDbContext(o => o.UseSqlServer(connectionString)); + +// CQRS + mapping +services.AddCommandQuery(); +services.AddSingleton(); +services.AddSingleton, ProductToReadModelMapper>(); +services.AddSingleton, ProductCreateModelToProductMapper>(); +services.AddSingleton, ProductUpdateModelToProductMapper>(); + +// Per-entity registration +services.AddEntityQueries (); +services.AddEntityCommands(); +``` + +`AddEntityQueries` registers identifier, identifiers, and paged-query handlers. +`AddEntityCommands` registers create, update, patch, and delete handlers. + +## Entity requirements + +```csharp +public class Product : IHaveIdentifier, + ITrackCreated, ITrackUpdated, // audit fields + ITrackDeleted // soft delete +{ + public int Id { get; set; } + public string Name { get; set; } = ""; + public decimal Price { get; set; } + + public DateTimeOffset Created { get; set; } + public string? CreatedBy { get; set; } + public DateTimeOffset Updated { get; set; } + public string? UpdatedBy { get; set; } + + public bool IsDeleted { get; set; } +} +``` + +Implementing `IHaveIdentifier` is required. The other marker interfaces opt the entity into audit and soft-delete behaviors automatically — paged queries filter out soft-deleted rows; commands stamp audit fields from the `ClaimsPrincipal`. + +## Finer-grained registration + +Use these if you don't want every CRUD operation, or you want a custom handler for one: + +```csharp +// Queries +services.AddEntityIdentifierQuery (); +services.AddEntityIdentifiersQuery(); +services.AddEntityPagedQuery (); + +// Commands (DbContext-backed) +services.AddEntityCreateCommand(); +services.AddEntityUpdateCommand(); +services.AddEntityPatchCommand (); +services.AddEntityDeleteCommand(); + +// Commands (custom handler) +services.AddEntityCreateCommand(); +``` + +`AddQueryPipeline()` registers EF-specific pipeline pieces (call once; included automatically by the helpers above). + +## Key-based lookups (for string/natural keys) + +```csharp +// Entity implements IHaveKey + IHaveIdentifier +services.AddEntityKeyQuery(); +``` + +## Reference + +- EF handler guide: https://github.com/loresoft/Arbiter/blob/main/docs/guide/handlers/entityFramework.md +- Behaviors: https://github.com/loresoft/Arbiter/tree/main/docs/guide/behaviors +- Sample: https://github.com/loresoft/Arbiter/tree/main/samples/EntityFramework diff --git a/plugins/arbiter-skills/skills/arbiter-commandquery-mongo/SKILL.md b/plugins/arbiter-skills/skills/arbiter-commandquery-mongo/SKILL.md new file mode 100644 index 0000000..1d25f78 --- /dev/null +++ b/plugins/arbiter-skills/skills/arbiter-commandquery-mongo/SKILL.md @@ -0,0 +1,87 @@ +--- +name: arbiter-commandquery-mongo +description: Use when wiring Arbiter.CommandQuery with MongoDB — AddMongoRepository plus AddEntityQueries / AddEntityCommands for an IMongoEntityRepository. Trigger on IMongoRepository, IMongoEntityRepository, MongoDB document handlers. +--- + +# Arbiter.CommandQuery.MongoDB + +MongoDB implementations of the generic `Arbiter.CommandQuery` handlers. Uses `IMongoRepository` (typically resolved as `IMongoEntityRepository`) as the data gateway. + +## Install + +```bash +dotnet add package Arbiter.CommandQuery.MongoDB +``` + +Pairs with `Arbiter.CommandQuery` (base) and `Arbiter.Mapping`. + +## Register + +```csharp +using Arbiter.CommandQuery; +using Arbiter.CommandQuery.MongoDB; + +// MongoDB repository services (connection-string name) +services.AddMongoRepository("Tracker"); + +// CQRS + mapping +services.AddCommandQuery(); +services.AddSingleton(); +services.AddSingleton, ProductToReadModelMapper>(); +services.AddSingleton, ProductCreateModelToProductMapper>(); +services.AddSingleton, ProductUpdateModelToProductMapper>(); + +// Per-entity registration (TRepository is the concrete repo interface) +services.AddEntityQueries , Product, string, ProductReadModel>(); +services.AddEntityCommands, Product, string, ProductReadModel, ProductCreateModel, ProductUpdateModel>(); +``` + +## Entity requirements + +```csharp +public class Product : IHaveIdentifier, + ITrackCreated, ITrackUpdated +{ + public string Id { get; set; } = ""; + public string Name { get; set; } = ""; + + public DateTimeOffset Created { get; set; } + public string? CreatedBy { get; set; } + public DateTimeOffset Updated { get; set; } + public string? UpdatedBy { get; set; } +} +``` + +Mongo entities typically use `string` keys. Marker interfaces (`ITrackCreated`, `ITrackUpdated`, `ITrackDeleted`, `IHaveTenant`) opt into the standard behaviors just like the EF flavor — see `arbiter-commandquery`. + +## Finer-grained registration + +```csharp +services.AddEntityIdentifierQuery (); +services.AddEntityPagedQuery (); + +services.AddEntityCreateCommand, Product, string, ProductReadModel, ProductCreateModel>(); +services.AddEntityUpdateCommand, Product, string, ProductReadModel, ProductUpdateModel>(); +services.AddEntityPatchCommand , Product, string, ProductReadModel>(); +services.AddEntityDeleteCommand, Product, string, ProductReadModel>(); +``` + +Key-based (natural-key) lookup: + +```csharp +services.AddEntityKeyQuery, Product, string, ProductReadModel>(); +``` + +## Sending commands + +Same as the EF flavor — the handlers conform to the shared `Arbiter.CommandQuery` request types, so callers don't change: + +```csharp +var product = await mediator.Send( + new EntityIdentifierQuery(principal, id), ct); +``` + +## Reference + +- Mongo handler guide: https://github.com/loresoft/Arbiter/blob/main/docs/guide/handlers/mongo.md +- Sample: https://github.com/loresoft/Arbiter/tree/main/samples/MongoDB diff --git a/plugins/arbiter-skills/skills/arbiter-commandquery/SKILL.md b/plugins/arbiter-skills/skills/arbiter-commandquery/SKILL.md new file mode 100644 index 0000000..ca69e25 --- /dev/null +++ b/plugins/arbiter-skills/skills/arbiter-commandquery/SKILL.md @@ -0,0 +1,124 @@ +--- +name: arbiter-commandquery +description: Use when working with Arbiter's CQRS base layer — EntityQuery, EntityFilter, FilterOperators, SortDirections, EntityPagedQuery, EntityIdentifierQuery, EntityCreateCommand / EntityUpdateCommand / EntityPatchCommand / EntityDeleteCommand, or pipeline behaviors like validation, hybrid cache, audit, tenant, soft-delete. Trigger on AddCommandQuery, AddCommandValidation, AddEntityHybridCache. +--- + +# Arbiter.CommandQuery + +CQRS layer on top of `Arbiter.Mediation`: pre-built generic commands/queries for entity CRUD, plus filter/sort/page modeling and opt-in behaviors. + +## Install + +```bash +dotnet add package Arbiter.CommandQuery +``` + +A data provider package supplies the handlers — pick one: +- `Arbiter.CommandQuery.EntityFramework` → see `arbiter-commandquery-ef` +- `Arbiter.CommandQuery.MongoDB` → see `arbiter-commandquery-mongo` + +## Register + +```csharp +using Arbiter.CommandQuery; + +services.AddCommandQuery(); // core CQRS pipeline + mediator +services.AddCommandValidation(); // optional: FluentValidation pipeline behavior +services.AddEntityHybridCache(); // optional: HybridCache-backed query caching +services.AddMessagePackOptions(); // optional: configure MessagePack for dispatcher +``` + +## Built-in commands and queries + +| Type | Purpose | +| --- | --- | +| `EntityIdentifierQuery` | Get one by id | +| `EntityIdentifiersQuery` | Get many by ids | +| `EntityPagedQuery` | Filter + sort + (optional) page | +| `EntityCreateCommand` | Insert | +| `EntityUpdateCommand` | Update; pass `upsert: true` for upsert | +| `EntityPatchCommand` | JSON Patch partial update | +| `EntityDeleteCommand` | Delete | + +All commands/queries take a `ClaimsPrincipal` as the first ctor arg. + +## Canonical pattern — paged query with filter + sort + +```csharp +using Arbiter.CommandQuery.Queries; + +var entityQuery = new EntityQuery +{ + Filter = new EntityFilter + { + Logic = FilterLogic.And, + Filters = new List + { + new() { Name = "Category", Operator = FilterOperators.Equal, Value = "Electronics" }, + new() { Name = "Price", Operator = FilterOperators.GreaterThan, Value = 100m }, + } + }, + Sort = new List + { + new() { Name = "Name", Direction = SortDirections.Ascending } + }, + Page = 1, + PageSize = 20, // omit Page/PageSize to return all matches +}; + +var query = new EntityPagedQuery(principal, entityQuery); +var result = await mediator.Send(query, cancellationToken); // EntityPagedResult +``` + +## Variations + +```csharp +// Get by id +var product = await mediator.Send( + new EntityIdentifierQuery(principal, id), ct); + +// Create +var created = await mediator.Send( + new EntityCreateCommand(principal, createModel), ct); + +// Update (upsert with last bool) +var updated = await mediator.Send( + new EntityUpdateCommand(principal, id, model, upsert: true), ct); + +// Delete +var deleted = await mediator.Send( + new EntityDeleteCommand(principal, id), ct); +``` + +## Filter operators (enum, v2.0+) + +`FilterOperators`: `Equal`, `NotEqual`, `LessThan`, `LessThanOrEqual`, `GreaterThan`, `GreaterThanOrEqual`, `Contains`, `StartsWith`, `EndsWith`, `In`, `NotIn`, `IsNull`, `IsNotNull`. +`FilterLogic`: `And`, `Or`. +`SortDirections`: `Ascending`, `Descending`. + +## Behaviors (opt-in via marker interfaces on the entity / model) + +| Behavior | Triggered by | +| --- | --- | +| Validation | `services.AddCommandValidation()` + a `FluentValidation.IValidator` | +| Hybrid cache | `services.AddEntityHybridCache()` + `[Cacheable]` on the query/read-model | +| Audit fields | Entity implements `ITrackCreated` / `ITrackUpdated` | +| Soft delete | Entity implements `ITrackDeleted` — paged queries auto-filter | +| Tenant isolation | Entity implements `IHaveTenant` | + +Add per-entity behavior registrations only if you need extra pipeline stages beyond what `AddEntityQueries` / `AddEntityCommands` already wires: + +```csharp +services.AddEntityQueryBehaviors(); +services.AddEntityCreateBehaviors(); +services.AddEntityUpdateBehaviors(); +services.AddEntityPatchBehaviors(); +services.AddEntityDeleteBehaviors(); +``` + +## Reference + +- Guide: https://github.com/loresoft/Arbiter/blob/main/docs/guide/commandQuery.md +- Queries: https://github.com/loresoft/Arbiter/tree/main/docs/guide/queries +- Commands: https://github.com/loresoft/Arbiter/tree/main/docs/guide/commands +- Behaviors: https://github.com/loresoft/Arbiter/tree/main/docs/guide/behaviors diff --git a/plugins/arbiter-skills/skills/arbiter-communication/SKILL.md b/plugins/arbiter-skills/skills/arbiter-communication/SKILL.md new file mode 100644 index 0000000..7ef192e --- /dev/null +++ b/plugins/arbiter-skills/skills/arbiter-communication/SKILL.md @@ -0,0 +1,114 @@ +--- +name: arbiter-communication +description: Use when sending templated email or SMS via Arbiter.Communication — AddEmailServices / AddSmsServices, IEmailDeliveryService / ISmsDeliveryService, template resolvers, or wiring a delivery provider like Azure Communication Services, Microsoft Graph, SendGrid, Twilio, or SMTP. +--- + +# Arbiter.Communication + +Templated email + SMS abstraction with pluggable delivery providers. Templates resolve from embedded resources (or any custom `ITemplateResolver`), then are rendered with model substitution and handed off to the configured delivery service. + +## Install + +```bash +dotnet add package Arbiter.Communication # core (templates + abstractions) +``` + +Then one or more providers: + +```bash +dotnet add package Arbiter.Communication.Azure # Azure Communication Services (email + SMS) +dotnet add package Arbiter.Communication.Graph # Microsoft Graph (email) +dotnet add package Arbiter.Communication.Twilio # SendGrid (email) + Twilio (SMS) +``` + +SMTP delivery is built into the core package. + +## Register — email + +```csharp +using Arbiter.Communication; +using Arbiter.Communication.Azure; +using Arbiter.Communication.Twilio; +using Arbiter.Communication.Graph; + +services.AddTemplateServices(); +services.AddTemplateResourceResolver( + resourceNameFormat: "MyApp.Templates.{0}.html"); + +services.AddEmailServices(options => +{ + options.FromAddress = "noreply@example.com"; + options.FromName = "Example App"; +}); + +// Pick ONE delivery provider: +services.AddSmtpEmailDeliver(); // built-in SMTP +services.AddAzureEmailDeliver("AzureCommunicationConnectionString"); // Azure Communication +services.AddGraphEmailDeliver(); // Microsoft Graph +services.AddSendGridEmailDeliver("SendGrid:ApiKey"); // SendGrid +``` + +## Register — SMS + +```csharp +services.AddSmsServices(options => +{ + options.FromNumber = "+15555550100"; +}); + +// Pick ONE provider: +services.AddAzureSmsDeliver("AzureCommunicationConnectionString"); +services.AddTwilioSmsDeliver(accountSID: "AC…", authenticationToken: "…"); +``` + +## Sending + +```csharp +public class WelcomeEmailSender +{ + private readonly IEmailTemplateService _email; + public WelcomeEmailSender(IEmailTemplateService email) => _email = email; + + public Task SendAsync(User user, CancellationToken ct) => + _email.SendAsync( + templateName: "Welcome", + model: new { user.FirstName, user.Email }, + to: user.Email, + cancellationToken: ct); +} +``` + +```csharp +public class OtpSender +{ + private readonly ISmsTemplateService _sms; + public OtpSender(ISmsTemplateService sms) => _sms = sms; + + public Task SendAsync(string phone, string code, CancellationToken ct) => + _sms.SendAsync(templateName: "Otp", model: new { Code = code }, to: phone, cancellationToken: ct); +} +``` + +## Template resolution + +Templates are arbitrary strings (HTML for email, text for SMS) located by an `ITemplateResolver`. The built-in `EmbeddedResourceTemplateResolver` is wired via: + +```csharp +services.AddTemplateResourceResolver("MyApp.Templates.{0}.html"); +// {0} is the template name; mark the file as in the .csproj +``` + +Multiple resolvers can be registered; lower `priority` wins. Implement `ITemplateResolver` for filesystem, DB, or remote sources. + +## Notes + +- All `AddXxxDeliver` overloads accept an `Action` / `Action` to override the from address/number per provider. +- For testing, register `services.AddEmailDelivery()` (or your own fake). + +## Reference + +- Overview: https://github.com/loresoft/Arbiter/blob/main/docs/guide/communication/overview.md +- Templates: https://github.com/loresoft/Arbiter/blob/main/docs/guide/communication/templates.md +- Azure: https://github.com/loresoft/Arbiter/blob/main/docs/guide/communication/azure.md +- Graph: https://github.com/loresoft/Arbiter/blob/main/docs/guide/communication/graph.md +- Twilio/SendGrid: https://github.com/loresoft/Arbiter/blob/main/docs/guide/communication/twilio.md diff --git a/plugins/arbiter-skills/skills/arbiter-dispatcher/SKILL.md b/plugins/arbiter-skills/skills/arbiter-dispatcher/SKILL.md new file mode 100644 index 0000000..7c79031 --- /dev/null +++ b/plugins/arbiter-skills/skills/arbiter-dispatcher/SKILL.md @@ -0,0 +1,130 @@ +--- +name: arbiter-dispatcher +description: Use when wiring the Blazor IDispatcher abstraction from Arbiter.Dispatcher — MessagePackDispatcher / JsonDispatcher for WebAssembly, ServerDispatcher for Server Interactive, AddDispatcherService + MapDispatcherService on the host, or component state helpers ModelStateManager / ModelStateLoader / ModelStateEditor. Trigger on Blazor Auto render mode setup. +--- + +# Arbiter.Dispatcher + +Unified `IDispatcher` abstraction so Blazor components depend on a single interface regardless of render mode. The right transport is chosen at startup: + +| Render mode | Implementation | Transport | +| --- | --- | --- | +| WebAssembly | `MessagePackDispatcher` / `JsonDispatcher` | HTTP POST → `/api/dispatcher/send` | +| Server Interactive | `ServerDispatcher` | Direct in-process `IMediator` | + +## Install + +```bash +# Server (host) project +dotnet add package Arbiter.Dispatcher.Server + +# Client (WASM and/or shared component library) +dotnet add package Arbiter.Dispatcher.Client +``` + +## Register + +**Server host project** + +```csharp +using Arbiter.Dispatcher; +using Arbiter.Dispatcher.Server; + +builder.Services.AddCommandQuery(); +builder.Services.AddServerDispatcher(); // in-process IDispatcher for Server Interactive +builder.Services.AddDispatcherService(); // dispatcher HTTP endpoint for WASM clients + +var app = builder.Build(); +app.MapDispatcherService().RequireAuthorization(); +``` + +**WebAssembly client project** + +```csharp +using Arbiter.Dispatcher; + +// MessagePack is recommended (faster, smaller payloads) +builder.Services.AddMessagePackDispatcher((sp, client) => +{ + client.BaseAddress = new Uri(builder.HostEnvironment.BaseAddress); +}); + +// Or JSON if MessagePack interop is a problem: +// builder.Services.AddJsonDispatcher((sp, client) => { ... }); +``` + +For **Blazor Auto** render mode, register both — each render environment resolves the right `IDispatcher`: + +```csharp +// Host project +builder.Services.AddServerDispatcher(); +builder.Services.AddDispatcherService(); + +// WASM client project +builder.Services.AddMessagePackDispatcher((sp, client) => +{ + client.BaseAddress = new Uri(builder.HostEnvironment.BaseAddress); +}); +``` + +## Sending commands and queries + +```csharp +// Low-level +var user = await dispatcher.Send, UserReadModel>( + new EntityIdentifierQuery(principal, id), ct); + +// High-level — IDispatcherDataService wraps the common CRUD shapes +var user = await dataService.Get (id); +var page = await dataService.Page (entityQuery); +var saved = await dataService.Save (id, updateModel); +``` + +## Component state — load / edit / save / delete + +```razor +@inject ModelStateEditor Store +@implements IDisposable + +@code { + [Parameter] public int Id { get; set; } + [Parameter] public bool IsCreate { get; set; } + + protected override async Task OnInitializedAsync() + { + Store.OnStateChanged += HandleStateChanged; + if (IsCreate) Store.New(); + else await Store.Load(Id); + } + + private void HandleStateChanged(object? s, EventArgs e) => InvokeAsync(StateHasChanged); + + public void Dispose() => Store.OnStateChanged -= HandleStateChanged; +} + + +``` + +Lighter-weight helpers if you don't need the full edit lifecycle: + +- `ModelStateManager` — single-model holder with change notifications. +- `ModelStateLoader` — load-only flow. +- `ModelStateEditor` — full load/edit/save/delete. + +Register them once per closed type: + +```csharp +services.AddScoped>(); +``` + +## Reference + +- Overview: https://github.com/loresoft/Arbiter/blob/main/docs/guide/dispatcher/overview.md +- Server: https://github.com/loresoft/Arbiter/blob/main/docs/guide/dispatcher/server.md +- Client: https://github.com/loresoft/Arbiter/blob/main/docs/guide/dispatcher/client.md +- State: https://github.com/loresoft/Arbiter/blob/main/docs/guide/dispatcher/state.md diff --git a/plugins/arbiter-skills/skills/arbiter-endpoints/SKILL.md b/plugins/arbiter-skills/skills/arbiter-endpoints/SKILL.md new file mode 100644 index 0000000..bc25a19 --- /dev/null +++ b/plugins/arbiter-skills/skills/arbiter-endpoints/SKILL.md @@ -0,0 +1,86 @@ +--- +name: arbiter-endpoints +description: Use when exposing Arbiter commands and queries as REST via Minimal APIs — AddEndpointRoutes, MapEndpointRoutes, EntityCommandEndpointBase, EntityQueryEndpointBase, IEndpointRoute. Trigger on Minimal API endpoint registration for CRUD entities. +--- + +# Arbiter.CommandQuery.Endpoints + +Minimal API surface that exposes the generic `Arbiter.CommandQuery` commands and queries as REST endpoints automatically. Each entity gets `GET /{prefix}/{name}/{id}`, `GET /{prefix}/{name}` (paged), `POST`, `PUT`, `PATCH`, `DELETE` with no per-route boilerplate. + +## Install + +```bash +dotnet add package Arbiter.CommandQuery.Endpoints +``` + +## Register + +```csharp +using Arbiter.CommandQuery.Endpoints; + +var builder = WebApplication.CreateBuilder(args); + +builder.Services.AddCommandQuery(); +builder.Services.AddEndpointRoutes(); + +// One IEndpointRoute per entity (see below) +builder.Services.AddSingleton(); + +var app = builder.Build(); + +// Mounts every IEndpointRoute under the prefix (default "/api") +app.MapEndpointRoutes(); + +app.Run(); +``` + +`MapEndpointRoutes(prefix = "/api", serviceKey = null)` returns an `IEndpointConventionBuilder`, so chain `.RequireAuthorization()`, `.WithOpenApi()`, etc. + +## Canonical pattern — one endpoint class per entity + +```csharp +public class ProductEndpoint + : EntityCommandEndpointBase +{ + public ProductEndpoint(ILoggerFactory loggerFactory) + : base(loggerFactory, entityName: "Product") { } +} +``` + +That single class registers all of: + +``` +GET /api/Product/{id} +GET /api/Product +POST /api/Product +PUT /api/Product/{id} +PATCH /api/Product/{id} +DELETE /api/Product/{id} +``` + +If you only need queries, derive from `EntityQueryEndpointBase` instead. + +## Variations + +```csharp +// Read-only entity (queries only) +public class CategoryEndpoint : EntityQueryEndpointBase +{ + public CategoryEndpoint(ILoggerFactory l) : base(l, "Category") { } +} + +// Custom routes — override MapEndpoints to add or replace any of the above +protected override void MapEndpoints(IEndpointRouteBuilder app) +{ + base.MapEndpoints(app); + app.MapPost($"/{EntityName}/import", ImportProducts).WithName("ImportProducts"); +} + +// Auth applied via the convention builder returned by MapEndpointRoutes +app.MapEndpointRoutes().RequireAuthorization(); +``` + +## Reference + +- Minimal API guide: https://github.com/loresoft/Arbiter/blob/main/docs/guide/endpoints/minimal.md +- Sample: https://github.com/loresoft/Arbiter/tree/main/samples/EntityFramework diff --git a/plugins/arbiter-skills/skills/arbiter-mapping/SKILL.md b/plugins/arbiter-skills/skills/arbiter-mapping/SKILL.md new file mode 100644 index 0000000..918afcd --- /dev/null +++ b/plugins/arbiter-skills/skills/arbiter-mapping/SKILL.md @@ -0,0 +1,103 @@ +--- +name: arbiter-mapping +description: Use when defining or registering Arbiter.Mapping source-generated mappers — [GenerateMapper] attribute, partial class deriving from MapperProfile, ConfigureMapping with MappingBuilder, IMapper / IMapper, ProjectTo for IQueryable, ServiceProviderMapper registration. +--- + +# Arbiter.Mapping + +Roslyn incremental source generator that emits object-to-object mapping code at build time. Zero reflection, AOT-friendly, supports records, init-only properties, primary constructors, and `IQueryable` projection. + +## Install + +```bash +dotnet add package Arbiter.Mapping +``` + +The generator (`Arbiter.Mapping.Generators`) is referenced transitively. Mapping classes must be `partial` so the generator can emit the implementation. + +## Canonical pattern — one mapper per direction + +```csharp +using Arbiter.Mapping; + +[GenerateMapper] +public partial class UserToUserDtoMapper : MapperProfile +{ + protected override void ConfigureMapping(MappingBuilder mapping) + { + mapping.Property(d => d.FullName) .From(s => s.FirstName + " " + s.LastName); + mapping.Property(d => d.Age) .From(s => DateTime.Now.Year - s.BirthDate.Year); + mapping.Property(d => d.DepartmentName) .From(s => s.Department!.Name); + mapping.Property(d => d.AddressCount) .From(s => s.Addresses.Count()); + // Properties with matching names + compatible types are mapped automatically. + } +} +``` + +If no overrides are needed, leave `ConfigureMapping` empty (or omit it) — the generator still emits the auto-property mapping. + +## Register + +Register each closed mapper plus the generic dispatcher: + +```csharp +services.AddSingleton, UserToUserDtoMapper>(); +services.AddSingleton(); +``` + +`ServiceProviderMapper` implements the non-generic `IMapper` and looks up the right `IMapper` on demand. + +## Use + +```csharp +public class UserService +{ + private readonly IMapper _mapper; + public UserService(IMapper mapper) => _mapper = mapper; + + public UserDto ToDto(User user) => _mapper.Map(user); +} + +// Or via the generic IMapper: +UserDto dto = mapper.Map(user); +``` + +## IQueryable projection + +```csharp +IQueryable users = db.Users.Where(u => u.IsActive); + +// Translates property selection into the underlying query (EF Core, etc.) +IQueryable dtos = users.ProjectTo(mapper); + +var list = await dtos.ToListAsync(ct); +``` + +## Mapping into records / init-only / primary ctors + +Supported out of the box — name the parameter or `init` property to match the source property, or add a `mapping.Property(...).From(...)` override. + +```csharp +public record UserDto(int Id, string FullName, int Age); + +[GenerateMapper] +public partial class UserToUserDtoMapper : MapperProfile +{ + protected override void ConfigureMapping(MappingBuilder m) + { + m.Property(d => d.FullName).From(s => s.FirstName + " " + s.LastName); + m.Property(d => d.Age).From(s => DateTime.Now.Year - s.BirthDate.Year); + } +} +``` + +## Notes + +- Mappers must be `partial` and derive from `MapperProfile`. +- One mapper per direction — define a second `partial class DtoToUserMapper : MapperProfile` if you need the reverse. +- Register one mapper per direction; `ServiceProviderMapper` is the single non-generic entry point. + +## Reference + +- Mapping guide: https://github.com/loresoft/Arbiter/tree/main/docs/guide/mapping +- Code generation: https://github.com/loresoft/Arbiter/blob/main/docs/guide/codeGeneration.md diff --git a/plugins/arbiter-skills/skills/arbiter-mediation/SKILL.md b/plugins/arbiter-skills/skills/arbiter-mediation/SKILL.md new file mode 100644 index 0000000..7663aee --- /dev/null +++ b/plugins/arbiter-skills/skills/arbiter-mediation/SKILL.md @@ -0,0 +1,113 @@ +--- +name: arbiter-mediation +description: Use when wiring or extending Arbiter.Mediation — defining IRequest / IRequestHandler, INotification / INotificationHandler, IPipelineBehavior, or calling services.AddMediator(). Also use when the user asks about Arbiter's mediator pattern, request/response, notifications/events, or pipeline middleware. +--- + +# Arbiter.Mediation + +Lightweight mediator: requests with typed responses, notifications, and pipeline behaviors. No reflection at the hot path, AOT-friendly. + +## Install + +```bash +dotnet add package Arbiter.Mediation +``` + +## Register + +```csharp +using Arbiter.Mediation; + +// Singleton mediator by default; pass ServiceLifetime to change +services.AddMediator(); + +// Handlers and behaviors are registered separately +services.AddTransient, PingHandler>(); +services.AddTransient(typeof(IPipelineBehavior<,>), typeof(LoggingBehavior<,>)); +``` + +`AddMediator(ServiceLifetime serviceLifetime = ServiceLifetime.Singleton)`. + +## Canonical pattern — request/response + +```csharp +public class Ping : IRequest +{ + public string? Message { get; set; } +} + +public class Pong +{ + public string? Message { get; set; } +} + +public class PingHandler : IRequestHandler +{ + public async ValueTask Handle(Ping request, CancellationToken cancellationToken = default) + { + await Task.Delay(10, cancellationToken); + return new Pong { Message = $"{request.Message} Pong" }; + } +} + +// Send +var pong = await mediator.Send(new Ping { Message = "Hello" }, cancellationToken); +``` + +## Notifications (events) + +```csharp +public class UserCreated : INotification +{ + public int UserId { get; init; } +} + +public class SendWelcomeEmail : INotificationHandler +{ + public ValueTask Handle(UserCreated notification, CancellationToken cancellationToken) + { + // ... + return ValueTask.CompletedTask; + } +} + +services.AddTransient, SendWelcomeEmail>(); + +await mediator.Publish(new UserCreated { UserId = 42 }, cancellationToken); +``` + +## Pipeline behavior (middleware) + +```csharp +public class LoggingBehavior : IPipelineBehavior + where TRequest : IRequest +{ + private readonly ILogger> _logger; + public LoggingBehavior(ILogger> logger) => _logger = logger; + + public async ValueTask Handle( + TRequest request, + RequestHandlerDelegate next, + CancellationToken cancellationToken = default) + { + _logger.LogInformation("Handling {Request}", typeof(TRequest).Name); + var response = await next(cancellationToken); + _logger.LogInformation("Handled {Request}", typeof(TRequest).Name); + return response; + } +} + +services.AddTransient(typeof(IPipelineBehavior<,>), typeof(LoggingBehavior<,>)); +``` + +## Notes + +- Handler return type is `ValueTask` (nullable). `Send` returns `TResponse?`. +- Behaviors run in registration order, wrapping the next handler. +- For CQRS abstractions on top of this (entity CRUD, paging, filtering), see `arbiter-commandquery`. +- For OpenTelemetry, see `arbiter-opentelemetry` — sources are `MediatorTelemetry.SourceName` / `MediatorTelemetry.MeterName`. + +## Reference + +- Guide: https://github.com/loresoft/Arbiter/blob/main/docs/guide/mediation.md +- Source: `src/Arbiter.Mediation/` diff --git a/plugins/arbiter-skills/skills/arbiter-messaging-servicebus/SKILL.md b/plugins/arbiter-skills/skills/arbiter-messaging-servicebus/SKILL.md new file mode 100644 index 0000000..d15733d --- /dev/null +++ b/plugins/arbiter-skills/skills/arbiter-messaging-servicebus/SKILL.md @@ -0,0 +1,76 @@ +--- +name: arbiter-messaging-servicebus +description: Use when integrating Arbiter with Azure Service Bus — registering AddServiceBus with a connection string and ServiceBusOptions, publishing INotifications to a topic/queue, or consuming Service Bus messages as Arbiter requests/notifications. +--- + +# Arbiter.Messaging.ServiceBus + +Bridges Arbiter's mediator pattern to Azure Service Bus topics/queues. Lets you publish notifications across processes and dispatch incoming messages through the local `IMediator`. + +## Install + +```bash +dotnet add package Arbiter.Messaging.ServiceBus +``` + +Requires `Azure.Messaging.ServiceBus` (transitive). + +## Register + +```csharp +using Arbiter.Messaging.ServiceBus; + +services.AddServiceBus( + serviceName: null, // optional logical key + nameOrConnectionString: "ServiceBus", // config key OR raw connection string + configure: opts => + { + // Map notification/request types to topics or queues + opts.MapTopic("user-events"); + opts.MapQueue("orders"); + }); +``` + +`nameOrConnectionString` is resolved first as a `ConnectionStrings:` key, then as a literal connection string. + +## Publish + +```csharp +public class UserCreated : INotification +{ + public int UserId { get; init; } +} + +// In your handler / service — same Publish API as in-process notifications +await mediator.Publish(new UserCreated { UserId = 42 }, ct); +// Goes to the topic configured by opts.MapTopic(...) +``` + +## Consume + +Incoming Service Bus messages are deserialized to the mapped type and dispatched through `IMediator`, so you write a normal handler: + +```csharp +public class HandleUserCreated : INotificationHandler +{ + public ValueTask Handle(UserCreated notification, CancellationToken ct) + { + // ... + return ValueTask.CompletedTask; + } +} + +services.AddTransient, HandleUserCreated>(); +``` + +The hosted Service Bus listener starts automatically with the host; one subscription per mapped topic/queue. + +## Notes + +- The default subscription name comes from `ServiceBusOptions` — override per type if you run multiple consumers. +- For request/response over Service Bus, map a `IRequest` to a queue; the listener will reply on the session/reply-to. +- Combine with `arbiter-opentelemetry` to get end-to-end traces from publisher → consumer. + +## Reference + +- Source: https://github.com/loresoft/Arbiter/tree/main/src/Arbiter.Messaging.ServiceBus diff --git a/plugins/arbiter-skills/skills/arbiter-mvc/SKILL.md b/plugins/arbiter-skills/skills/arbiter-mvc/SKILL.md new file mode 100644 index 0000000..be79eed --- /dev/null +++ b/plugins/arbiter-skills/skills/arbiter-mvc/SKILL.md @@ -0,0 +1,81 @@ +--- +name: arbiter-mvc +description: Use when exposing Arbiter commands and queries via MVC controllers — deriving from EntityCommandControllerBase or EntityQueryControllerBase rather than Minimal API endpoints. Trigger on MVC + CQRS controllers for an entity. +--- + +# Arbiter.CommandQuery.Mvc + +ASP.NET Core MVC controller base classes that wrap the generic `Arbiter.CommandQuery` operations. Pick this over `arbiter-endpoints` if your app uses controllers, filters, model binding, or content negotiation features specific to MVC. + +## Install + +```bash +dotnet add package Arbiter.CommandQuery.Mvc +``` + +## Register + +```csharp +using Arbiter.CommandQuery; + +builder.Services.AddCommandQuery(); +builder.Services.AddControllers(); // standard MVC + +var app = builder.Build(); +app.MapControllers(); +``` + +There's no special `AddXxx` for the MVC package — controllers register through `AddControllers()` like any other. + +## Canonical pattern — one controller per entity + +```csharp +[Route("api/[controller]")] +public class ProductController + : EntityCommandControllerBase +{ + public ProductController(IMediator mediator) : base(mediator) { } +} +``` + +That single class exposes: + +``` +GET /api/Product/{id} +GET /api/Product (paged, with query string filter/sort) +POST /api/Product +PUT /api/Product/{id} +PATCH /api/Product/{id} +DELETE /api/Product/{id} +``` + +## Variations + +```csharp +// Read-only +[Route("api/[controller]")] +public class CategoryController : EntityQueryControllerBase +{ + public CategoryController(IMediator mediator) : base(mediator) { } +} + +// Add custom action alongside the inherited CRUD +[HttpPost("import")] +public async Task Import([FromBody] ImportRequest request, CancellationToken ct) +{ + var result = await Mediator.Send(new ImportProducts(User, request), ct); + return Ok(result); +} +``` + +Standard MVC features (`[Authorize]`, action filters, model binders) compose normally because each method on the base is a regular controller action. + +## When to choose MVC over Endpoints + +- Existing MVC-centric app with shared filters/conventions. +- Need full content negotiation (XML, etc.). +- Otherwise prefer `arbiter-endpoints` for lower overhead and minimal-API ergonomics. + +## Reference + +- Controller guide: https://github.com/loresoft/Arbiter/blob/main/docs/guide/endpoints/controller.md diff --git a/plugins/arbiter-skills/skills/arbiter-opentelemetry/SKILL.md b/plugins/arbiter-skills/skills/arbiter-opentelemetry/SKILL.md new file mode 100644 index 0000000..e3d32cd --- /dev/null +++ b/plugins/arbiter-skills/skills/arbiter-opentelemetry/SKILL.md @@ -0,0 +1,85 @@ +--- +name: arbiter-opentelemetry +description: Use when wiring OpenTelemetry tracing/metrics for Arbiter — adding the mediator ActivitySource and Meter (MediatorTelemetry.SourceName / MediatorTelemetry.MeterName), calling Arbiter.OpenTelemetry.Server.AddOpenTelemetry on a HostApplicationBuilder, or exposing log queries via Arbiter.OpenTelemetry.Monitor.AddLogQuery. +--- + +# Arbiter OpenTelemetry + +Three packages, layered: + +| Package | Purpose | +| --- | --- | +| `Arbiter.OpenTelemetry` | Shared models + the `MediatorTelemetry` ActivitySource/Meter names | +| `Arbiter.OpenTelemetry.Server` | One-call `AddOpenTelemetry` extension for ASP.NET Core hosts, pre-wires the mediator source | +| `Arbiter.OpenTelemetry.Monitor`| `AddLogQuery` — exposes telemetry log queries via the CQRS pipeline | + +## Manual wiring (any host) + +```bash +dotnet add package Arbiter.Mediation +dotnet add package OpenTelemetry.Extensions.Hosting +``` + +```csharp +using Arbiter.Mediation; +using OpenTelemetry.Metrics; +using OpenTelemetry.Trace; + +services.AddOpenTelemetry() + .WithTracing(t => t + .AddSource(MediatorTelemetry.SourceName) + .AddAspNetCoreInstrumentation() + .AddOtlpExporter()) + .WithMetrics(m => m + .AddMeter(MediatorTelemetry.MeterName) + .AddAspNetCoreInstrumentation() + .AddOtlpExporter()); +``` + +`MediatorTelemetry.SourceName` and `MediatorTelemetry.MeterName` are the only names you need — every Arbiter mediator activity/metric is emitted on them. + +## One-call wiring (ASP.NET Core) + +```bash +dotnet add package Arbiter.OpenTelemetry.Server +``` + +```csharp +using Arbiter.OpenTelemetry.Server; + +var builder = WebApplication.CreateBuilder(args); + +builder.AddOpenTelemetry( + configureOptions: opts => opts.ServiceName = "MyService", + configureMetrics: metrics => metrics.AddRuntimeInstrumentation(), + configureTracing: tracing => tracing.AddHttpClientInstrumentation(), + configureOpenTelemetry: ot => ot.UseAzureMonitor() // optional +); +``` + +The extension auto-adds the mediator source/meter, ASP.NET Core instrumentation, and an exporter (OTLP by default; honor `OTEL_*` env vars). + +## Log query API + +```bash +dotnet add package Arbiter.OpenTelemetry.Monitor +``` + +```csharp +using Arbiter.OpenTelemetry.Monitor; + +services.AddCommandQuery(); +services.AddLogQuery(); // registers handlers for the log-query requests +``` + +Then send the log queries through `IMediator` / `IDispatcher` just like any other Arbiter query — useful for surfacing observability data inside an admin UI. + +## Notes + +- Don't add `MediatorTelemetry.SourceName` twice — the server extension already wires it. +- If you only need metrics or only traces, pass `null` to the unused configurator delegate. + +## Reference + +- Source: https://github.com/loresoft/Arbiter/tree/main/src/Arbiter.OpenTelemetry +- Server extension: https://github.com/loresoft/Arbiter/blob/main/src/Arbiter.OpenTelemetry.Server/HostApplicationBuilderExtensions.cs diff --git a/plugins/arbiter-skills/skills/arbiter-overview/SKILL.md b/plugins/arbiter-skills/skills/arbiter-overview/SKILL.md new file mode 100644 index 0000000..cff6678 --- /dev/null +++ b/plugins/arbiter-skills/skills/arbiter-overview/SKILL.md @@ -0,0 +1,53 @@ +--- +name: arbiter-overview +description: Use when a user mentions the Arbiter .NET library family or is choosing which Arbiter package to install. Routes to the specialist skill for the area in question — Mediation, CommandQuery, Mapping, Dispatcher, Communication, Services, OpenTelemetry, or Messaging.ServiceBus. +--- + +# Arbiter — package landscape + +Arbiter (https://github.com/loresoft/Arbiter) is a family of small .NET libraries built around the Mediator pattern and CQRS. Pick packages à la carte. + +## Package map + +| Need | Package | Specialist skill | +| --- | --- | --- | +| In-process mediator (`IRequest`, `INotification`, pipeline behaviors) | `Arbiter.Mediation` | `arbiter-mediation` | +| CQRS commands/queries, filtering, paging, behaviors | `Arbiter.CommandQuery` | `arbiter-commandquery` | +| EF Core handlers for the base commands/queries | `Arbiter.CommandQuery.EntityFramework` | `arbiter-commandquery-ef` | +| MongoDB handlers for the base commands/queries | `Arbiter.CommandQuery.MongoDB` | `arbiter-commandquery-mongo` | +| Minimal API endpoints exposing commands/queries as REST | `Arbiter.CommandQuery.Endpoints` | `arbiter-endpoints` | +| MVC controllers exposing commands/queries | `Arbiter.CommandQuery.Mvc` | `arbiter-mvc` | +| Source-generated object mapping | `Arbiter.Mapping` (+ `.Generators`) | `arbiter-mapping` | +| Blazor dispatcher (WASM/Server/Auto), `ModelStateEditor` | `Arbiter.Dispatcher.Client` + `Arbiter.Dispatcher.Server` | `arbiter-dispatcher` | +| Email + SMS templates and delivery | `Arbiter.Communication` + `.Azure` / `.Graph` / `.Twilio` | `arbiter-communication` | +| CSV, encryption, caching, tokens, URL builder | `Arbiter.Services` | `arbiter-services` | +| OpenTelemetry tracing/metrics for Arbiter | `Arbiter.OpenTelemetry` + `.Server` / `.Monitor` | `arbiter-opentelemetry` | +| Azure Service Bus integration | `Arbiter.Messaging.ServiceBus` | `arbiter-messaging-servicebus` | + +## Typical layered setup + +A common Arbiter app stacks several packages: + +```text +Arbiter.Mediation ← always required +Arbiter.CommandQuery ← if you want CQRS commands/queries +Arbiter.Mapping ← for read-model / DTO mapping +Arbiter.CommandQuery.EntityFramework ← pick one data provider +Arbiter.CommandQuery.Endpoints ← pick one web surface +``` + +## Conventions to remember + +- Handlers return `ValueTask`. +- `EntityQuery` is the unified query shape (paged or non-paged); use `FilterOperators` / `SortDirections` enums (v2.0+). +- Mappers are source-generated via `[GenerateMapper]` + `MapperProfile`; register each closed mapper plus `ServiceProviderMapper`. +- Entity types implement `IHaveIdentifier`. Tracked/audited/soft-delete/tenant behaviors are opt-in via marker interfaces. + +## When in doubt + +Ask Claude to load the specialist skill — e.g. *"use arbiter-commandquery-ef"* — or just describe the concrete task (*"register CRUD handlers for my Product entity with EF Core"*) and the matching skill will trigger automatically. + +## Reference + +- README: https://github.com/loresoft/Arbiter/blob/main/README.md +- Full docs: https://loresoft.github.io/Arbiter/ diff --git a/plugins/arbiter-skills/skills/arbiter-services/SKILL.md b/plugins/arbiter-skills/skills/arbiter-services/SKILL.md new file mode 100644 index 0000000..54e2ade --- /dev/null +++ b/plugins/arbiter-skills/skills/arbiter-services/SKILL.md @@ -0,0 +1,94 @@ +--- +name: arbiter-services +description: Use when the user needs Arbiter.Services utilities — CSV reading/writing (CsvReader / CsvWriter), AES or XOR encryption, cache key building (CacheTagger), continuation tokens, numeric encoding, glob matching, URL building (UrlBuilder, QueryStringEncoder), or secure token generation (TokenService). +--- + +# Arbiter.Services + +Grab-bag of small zero-dependency utilities used across Arbiter. There are no DI extension methods — instantiate the types directly or register them yourself as needed. + +## Install + +```bash +dotnet add package Arbiter.Services +``` + +## What's in the box + +| Type | Purpose | +| --- | --- | +| `CsvReader` / `CsvWriter` | Streaming CSV parse/write, RFC-4180-compatible, optional headers | +| `AesEncryption` | Symmetric AES encrypt/decrypt with key + IV | +| `XorEncryption` | Lightweight XOR obfuscation (not for sensitive data) | +| `CacheTagger` | Build consistent cache keys + tags from arbitrary values | +| `ContinuationToken` | Opaque token encoder for cursor-paged queries | +| `NumericEncoder` | Base-N encoding for short numeric IDs | +| `GlobMatcher` | Wildcard (`*`, `?`) path/string matching | +| `TokenService` | Cryptographically secure random tokens | +| `UrlBuilder` / `QueryStringEncoder` | Fluent URL construction + query string encoding | +| `ObjectPool` + extensions | Lightweight per-thread object pooling | +| `ValueStringBuilder`, `TypeBuffer` | Stack-allocated builders for hot paths | + +## CSV + +```csharp +using Arbiter.Services; +using var reader = new CsvReader(File.OpenText("orders.csv")); +foreach (var row in reader.ReadAll()) // IEnumerable +{ + var id = row[0]; + var name = row[1]; +} + +using var writer = new CsvWriter(File.CreateText("out.csv")); +writer.WriteHeader("Id", "Name"); +writer.WriteRow("1", "Widget"); +``` + +## AES encryption + +```csharp +var aes = new AesEncryption(key: keyBytes, iv: ivBytes); +string cipher = aes.Encrypt("secret"); +string plain = aes.Decrypt(cipher); +``` + +## URL builder + +```csharp +string url = new UrlBuilder("https://api.example.com/users") + .AppendPath("42", "orders") + .AddQuery("status", "open") + .AddQuery("page", 2) + .ToString(); +// → https://api.example.com/users/42/orders?status=open&page=2 +``` + +## Cache key + tags + +```csharp +var key = CacheTagger.Build("product", productId, version); +var tag = CacheTagger.Tag("product", productId); +``` + +## Secure token + +```csharp +string token = TokenService.Create(byteLength: 32); // URL-safe base64 +``` + +## Continuation token (cursor paging) + +```csharp +string token = ContinuationToken.Encode(new { LastId = 1234, Sort = "Name" }); +var cursor = ContinuationToken.Decode(token); +``` + +## Notes + +- Most types are `sealed` / stack-friendly; allocate per use. +- Symmetric encryption helpers are convenience wrappers — for production secrets prefer Azure Key Vault / Data Protection. + +## Reference + +- Source: https://github.com/loresoft/Arbiter/tree/main/src/Arbiter.Services