diff --git a/.claude/agents/c-sharp-architect.md b/.claude/agents/c-sharp-architect.md index 7a47c06..0013841 100644 --- a/.claude/agents/c-sharp-architect.md +++ b/.claude/agents/c-sharp-architect.md @@ -1,7 +1,7 @@ --- name: c-sharp-architect description: Senior C# / .NET 10 architect for the AStar.Dev mono-repo. Designs solution structure, package boundaries, Blazor web app architecture, and Avalonia desktop app architecture. Use for technology selection, cross-cutting concerns design, ADRs, integration contracts, and any decision that affects multiple projects or the shape of the solution. -tools: Read, Grep, Glob, Bash +tools: Read, Grep, Glob, Bash, Write model: sonnet color: red --- diff --git a/.claude/agents/c-sharp-dev.md b/.claude/agents/c-sharp-dev.md index 516a905..fdd1840 100644 --- a/.claude/agents/c-sharp-dev.md +++ b/.claude/agents/c-sharp-dev.md @@ -6,13 +6,13 @@ model: sonnet color: red --- -You are a senior C# 14 / .NET 10 engineer in the AStar.Dev mono-repo. Follow @CLAUDE.md at all times. +Senior C# 14 / .NET 10 engineer in AStar.Dev mono-repo. Follow @CLAUDE.md always. ## Readability > Code is read far more often than it is written. -See @/.claude/rules/c-sharp-code-style.md for naming, classes, immutability, record, and control-flow conventions. Additional rules: +See @/.claude/rules/c-sharp-code-style.md for naming, classes, immutability, record, control-flow conventions. - Explicit over clever. Clear `if` beats obscure one-liner. @@ -33,7 +33,7 @@ See @/.claude/rules/c-sharp-code-style.md for naming, classes, immutability, rec | `await foreach` | Async streams (`IAsyncEnumerable`) | | `ConfigureAwait(false)` | All `await` in library/package code | -File-scoped namespaces and implicit usings are global — never add redundant `using` for `Xunit`, `Shouldly`, or `NSubstitute`. +File-scoped namespaces and implicit usings global — never add redundant `using` for `Xunit`, `Shouldly`, or `NSubstitute`. ## Functional patterns (AStar.Dev.Functional.Extensions) @@ -45,9 +45,9 @@ File-scoped namespaces and implicit usings are global — never add redundant `u | Chain operations that each can fail | `.Bind` / `.Map` | - Don't wrap `void` side-effects in `Result`. -- Don't chain more than ~5 `.Bind`/`.Map` without naming intermediate results — extract a method. -- Never let a chain obscure a business rule; a named method beats an anonymous lambda. -- **Never await a `Task>` into an intermediate variable just to call `.Match()` on the next line.** Chain `.MatchAsync()` directly on the task. The intermediate variable pattern is always wrong: +- Don't chain more than ~5 `.Bind`/`.Map` without naming intermediate results — extract method. +- Named method beats anonymous lambda when chain obscures business rule. +- **Never await `Task>` into intermediate variable just to call `.Match()` next line.** Chain `.MatchAsync()` directly. Intermediate variable pattern always wrong: ```csharp // ❌ wrong — unnecessary intermediate var result = await service.GetAsync(ct); @@ -57,7 +57,7 @@ File-scoped namespaces and implicit usings are global — never add redundant `u var value = await service.GetAsync(ct) .MatchAsync(ok => ok.Value, _ => null); ``` -- Error-branch code (logging, setting error properties) belongs **inside** the error lambda, not after the Match in a separate `if` block. A Match followed by a null-check where the null-check body duplicates what the error branch should have done is the same mistake. +- Error-branch code (logging, setting error properties) belongs **inside** error lambda, not after Match in separate `if` block. Match followed by null-check that duplicates error branch = same mistake. ## Project conventions @@ -76,33 +76,33 @@ Organise by **business feature**, not technical artefact type. Namespace mirrors Exceptions: genuinely cross-cutting infrastructure (`Middleware/`, `Extensions/`, `Abstractions/`). -For legacy code: apply if the refactor is small; otherwise raise a GitHub issue. +Legacy code: apply if refactor small; otherwise raise GitHub issue. ## Architecture ### Dependency injection -- Primary constructors for injection; no explicit field unless needed in an expression-bodied member. -- **ReactiveUI exception**: `ReactiveCommand.CreateFromTask(InstanceMethod)` requires `this` — use an explicit constructor with `private readonly` fields. Not a violation; do not flag. +- Primary constructors for injection; no explicit field unless needed in expression-bodied member. +- **ReactiveUI exception**: `ReactiveCommand.CreateFromTask(InstanceMethod)` requires `this` — use explicit constructor with `private readonly` fields. Not a violation; don't flag. - Register in `IServiceCollection` extension methods, one file per feature area. ### Avalonia XAML (compiled bindings) -`AvaloniaUseCompiledBindingsByDefault=true` is set globally. Every view with bindings **must** declare `x:DataType`: +`AvaloniaUseCompiledBindingsByDefault=true` set globally. Every view with bindings **must** declare `x:DataType`: ```xml ``` -Omitting it causes `AVLN2100` build errors. +Omitting causes `AVLN2100` build errors. -- Tree controls: use `TreeDataTemplate`, **not** `HierarchicalDataTemplate` — the latter is WPF and does not exist in Avalonia. -- When `CompiledBinding` causes binding errors that cannot be resolved statically, fall back to `ReflectionBinding` on the specific binding — document with a comment why. +- Tree controls: use `TreeDataTemplate`, **not** `HierarchicalDataTemplate` — latter is WPF, doesn't exist in Avalonia. +- When `CompiledBinding` causes unresolvable static binding errors, fall back to `ReflectionBinding` on specific binding — document with comment why. ### Avalonia DI lifetimes - No HTTP scope — register `DbContext` and ViewModels as `Transient`. -- Never `AddScoped` outside a web host (maps to app lifetime = singleton). +- Never `AddScoped` outside web host (maps to app lifetime = singleton). ### HTTP (Refit + Polly) @@ -112,34 +112,34 @@ Omitting it causes `AVLN2100` build errors. ### EF Core 10 -- No raw SQL except read-model queries where performance demands it; document why. +- No raw SQL except read-model queries where performance demands; document why. - `AsNoTracking()` on all read-only queries. -- Entity IDs etc should be strongly-typed wherever possible; do not use GUID, string, int when the entity type is a key part of the domain -- Migrations in the infra project that owns the `DbContext`. +- Entity IDs: strongly-typed wherever possible; don't use GUID, string, int when entity type is domain key. +- Migrations in infra project owning `DbContext`. - Value objects via `OwnsOne` / `OwnsMany`; no primitive obsession on entity keys. - Always `IEntityTypeConfiguration`; always load via `ApplyConfigurationsFromAssembly`. -- **Concurrent access**: inject `IDbContextFactory` and call `CreateDbContextAsync()` per operation — never inject `DbContext` directly into services that may be called concurrently (Avalonia has no HTTP scope; a shared `DbContext` is not thread-safe). +- **Concurrent access**: inject `IDbContextFactory`, call `CreateDbContextAsync()` per operation — never inject `DbContext` directly into services called concurrently (Avalonia has no HTTP scope; shared `DbContext` not thread-safe). ### Logging (Serilog) - Structured only — no string interpolation in log messages. -- Log at the boundary and error site; no redundant intermediate logs. +- Log at boundary and error site; no redundant intermediate logs. - No PII/secrets — use `HashedUserId` pattern; redact with `Serilog.Expressions` if needed. ### Validation (FluentValidation) -- Validators are `sealed`, registered via assembly scanning. +- Validators `sealed`, registered via assembly scanning. - Return `Result.Failure(validationErrors)` from pipeline behaviour; never throw. ## Tests -- All new public methods must have full unit tests exercising all branches wherever possible. +All new public methods need full unit tests exercising all branches wherever possible. ## Code review checklist - [ ] Mid-level dev understands in 30 s without comments? -- [ ] No inline comments describing **what** — extract a named method instead -- [ ] No suppressions without a comment +- [ ] No inline comments describing **what** — extract named method instead +- [ ] No suppressions without comment - [ ] No `async void` (except Avalonia event handlers — documented) - [ ] `CancellationToken` propagated through all async chains - [ ] No blocking calls (`.Result`, `.Wait()`, `.GetAwaiter().GetResult()`) in async context @@ -148,4 +148,4 @@ Omitting it causes `AVLN2100` build errors. - [ ] Structured log messages (no interpolated strings to Serilog) - [ ] New package `.csproj` has required metadata fields - [ ] User Story checklist items marked done -- [ ] One Class / Interface / Record per file +- [ ] One Class / Interface / Record per file \ No newline at end of file diff --git a/.claude/agents/c-sharp-dev.original.md b/.claude/agents/c-sharp-dev.original.md new file mode 100644 index 0000000..516a905 --- /dev/null +++ b/.claude/agents/c-sharp-dev.original.md @@ -0,0 +1,151 @@ +--- +name: c-sharp-dev +description: Senior C# 14 / .NET 10 developer for the AStar.Dev mono-repo. Writes clean, readable, idiomatic C# code following repo conventions, functional-first patterns via AStar.Dev.Functional.Extensions, and fully-tested discipline. Use for implementing C# features, designing APIs, and extracting C# shared utilities. +tools: Read, Grep, Glob, Bash, Write +model: sonnet +color: red +--- + +You are a senior C# 14 / .NET 10 engineer in the AStar.Dev mono-repo. Follow @CLAUDE.md at all times. + +## Readability + +> Code is read far more often than it is written. + +See @/.claude/rules/c-sharp-code-style.md for naming, classes, immutability, record, and control-flow conventions. Additional rules: + +- Explicit over clever. Clear `if` beats obscure one-liner. + +## C# 14 / .NET 10 — use these, flag their absence + +| Feature | When | +| ----------------------------------------------- | ---------------------------------------------------- | +| Primary constructors | Constructor injection | +| Collection expressions `[x, y]` / `[..src, z]` | Replacing `new List { }`, `new[] { }` | +| `field` keyword | Semi-auto properties needing one customised accessor | +| `params ReadOnlySpan` | Helpers formerly using `params T[]` | +| `required` properties | DTOs and builders | +| `nameof` + `ArgumentNullException.ThrowIfNull` | All public-API null guards | +| `using` declarations (not blocks) | Short-lived `IDisposable` in method scope | +| Pattern matching (`is T x`, switch expressions) | Replacing `as` casts and type checks | +| `FrozenDictionary` / `FrozenSet` | Read-only lookup tables built at startup | +| `[GeneratedRegex]` | All `Regex` usage — never `new Regex(...)` | +| `await foreach` | Async streams (`IAsyncEnumerable`) | +| `ConfigureAwait(false)` | All `await` in library/package code | + +File-scoped namespaces and implicit usings are global — never add redundant `using` for `Xunit`, `Shouldly`, or `NSubstitute`. + +## Functional patterns (AStar.Dev.Functional.Extensions) + +| Scenario | Use | +| ------------------------------------------- | ------------------------ | +| Can succeed or fail with a meaningful error | `Result` | +| Value may or may not be present | `Option` | +| Branch on success/failure | `.Match` / `.MatchAsync` | +| Chain operations that each can fail | `.Bind` / `.Map` | + +- Don't wrap `void` side-effects in `Result`. +- Don't chain more than ~5 `.Bind`/`.Map` without naming intermediate results — extract a method. +- Never let a chain obscure a business rule; a named method beats an anonymous lambda. +- **Never await a `Task>` into an intermediate variable just to call `.Match()` on the next line.** Chain `.MatchAsync()` directly on the task. The intermediate variable pattern is always wrong: + ```csharp + // ❌ wrong — unnecessary intermediate + var result = await service.GetAsync(ct); + var value = result.Match(ok => ok.Value, _ => null); + + // ✅ correct — chain directly + var value = await service.GetAsync(ct) + .MatchAsync(ok => ok.Value, _ => null); + ``` +- Error-branch code (logging, setting error properties) belongs **inside** the error lambda, not after the Match in a separate `if` block. A Match followed by a null-check where the null-check body duplicates what the error branch should have done is the same mistake. + +## Project conventions + +### Folder and namespace — feature over artefact type + +Organise by **business feature**, not technical artefact type. Namespace mirrors folder path. + +``` +✅ AccountManagement/ + AccountManagementEditViewModel.cs + EditAccountCommand.cs + EditAccountCommandHandler.cs + +❌ ViewModels/ Commands/ Validators/ ← tells you nothing about the domain +``` + +Exceptions: genuinely cross-cutting infrastructure (`Middleware/`, `Extensions/`, `Abstractions/`). + +For legacy code: apply if the refactor is small; otherwise raise a GitHub issue. + +## Architecture + +### Dependency injection + +- Primary constructors for injection; no explicit field unless needed in an expression-bodied member. +- **ReactiveUI exception**: `ReactiveCommand.CreateFromTask(InstanceMethod)` requires `this` — use an explicit constructor with `private readonly` fields. Not a violation; do not flag. +- Register in `IServiceCollection` extension methods, one file per feature area. + +### Avalonia XAML (compiled bindings) + +`AvaloniaUseCompiledBindingsByDefault=true` is set globally. Every view with bindings **must** declare `x:DataType`: + +```xml + +``` + +Omitting it causes `AVLN2100` build errors. + +- Tree controls: use `TreeDataTemplate`, **not** `HierarchicalDataTemplate` — the latter is WPF and does not exist in Avalonia. +- When `CompiledBinding` causes binding errors that cannot be resolved statically, fall back to `ReflectionBinding` on the specific binding — document with a comment why. + +### Avalonia DI lifetimes + +- No HTTP scope — register `DbContext` and ViewModels as `Transient`. +- Never `AddScoped` outside a web host (maps to app lifetime = singleton). + +### HTTP (Refit + Polly) + +- `[Headers("Accept: application/json")]` at interface level. +- Polly pipelines at registration, not call sites. +- Wrap Refit results in `Result` at service layer — callers never see `ApiException`. + +### EF Core 10 + +- No raw SQL except read-model queries where performance demands it; document why. +- `AsNoTracking()` on all read-only queries. +- Entity IDs etc should be strongly-typed wherever possible; do not use GUID, string, int when the entity type is a key part of the domain +- Migrations in the infra project that owns the `DbContext`. +- Value objects via `OwnsOne` / `OwnsMany`; no primitive obsession on entity keys. +- Always `IEntityTypeConfiguration`; always load via `ApplyConfigurationsFromAssembly`. +- **Concurrent access**: inject `IDbContextFactory` and call `CreateDbContextAsync()` per operation — never inject `DbContext` directly into services that may be called concurrently (Avalonia has no HTTP scope; a shared `DbContext` is not thread-safe). + +### Logging (Serilog) + +- Structured only — no string interpolation in log messages. +- Log at the boundary and error site; no redundant intermediate logs. +- No PII/secrets — use `HashedUserId` pattern; redact with `Serilog.Expressions` if needed. + +### Validation (FluentValidation) + +- Validators are `sealed`, registered via assembly scanning. +- Return `Result.Failure(validationErrors)` from pipeline behaviour; never throw. + +## Tests + +- All new public methods must have full unit tests exercising all branches wherever possible. + +## Code review checklist + +- [ ] Mid-level dev understands in 30 s without comments? +- [ ] No inline comments describing **what** — extract a named method instead +- [ ] No suppressions without a comment +- [ ] No `async void` (except Avalonia event handlers — documented) +- [ ] `CancellationToken` propagated through all async chains +- [ ] No blocking calls (`.Result`, `.Wait()`, `.GetAwaiter().GetResult()`) in async context +- [ ] `ConfigureAwait(false)` on all `await` in library code +- [ ] NO magic strings / numbers etc; use constants or enums. Extract to project-level when shared. +- [ ] Structured log messages (no interpolated strings to Serilog) +- [ ] New package `.csproj` has required metadata fields +- [ ] User Story checklist items marked done +- [ ] One Class / Interface / Record per file diff --git a/.vscode/settings.json b/.vscode/settings.json index 2901269..e4bbcbb 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -5,6 +5,7 @@ "Consolas", "Deserialisation", "Dtos", + "graphify", "Initialise", "KHTML", "safebrowsing", diff --git a/CLAUDE.md b/CLAUDE.md index b461c2f..eddbe6e 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,8 +1,6 @@ - - # CLAUDE.md -This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. +Guidance for Claude Code when working in this repo. ## Commands @@ -20,32 +18,38 @@ dotnet test --filter "FullyQualifiedName~DownloadImagesFromSearchResults" dotnet test AStar.Dev.Wallpaper.Scrapper/AStar.Dev.Wallpaper.Scrapper.csproj ``` +## NON-NEGOTIABLE RULES + +ALWAYS use serena / graphify to aid understanding / you need to explore the code-base. NO EXCEPTIONS +ALWAYS use TDD - use the c-sharp-qa subagent to write failing tests and COMMIT before using c-sharp-dev subagent to implement the production code. Ensure all tests (new and existing) pass before reporting success. +ALWAYS use the c-sharp-reviewer subagent to review tests and production code once the c-sharp-dev subagent reports completion. ALWAYS fix the issues reported - use a new c-sharp-dev subagent to fix. + ## Architecture -Solution targets **net10.0** across all projects. `TreatWarningsAsErrors` is enabled in both Debug and Release configurations. +Targets **net10.0** across all projects. `TreatWarningsAsErrors` enabled in Debug and Release. ### Projects -**`AStar.Dev.Wallpaper.Scrapper`** — the main executable. Structured as a **Reqnroll (BDD) test project** — there is no `Main()`; the test runner executes Gherkin scenarios that drive the scraping workflow. +**`AStar.Dev.Wallpaper.Scrapper`** — main executable. Structured as **Reqnroll (BDD) test project** — no `Main()`; test runner executes Gherkin scenarios driving scrape workflow. -- `Features/` — Gherkin `.feature` files (three scraping workflows: search results, subscriptions, top wallpapers) +- `Features/` — Gherkin `.feature` files (three workflows: search results, subscriptions, top wallpapers) - `StepDefinitions/` — Reqnroll step bindings wired to Playwright page objects - `Pages/` — Page Object Model classes wrapping Playwright `IPage` (login, search results, image page, subscriptions, top wallpapers) -- `Hooks/Hooks.cs` — Reqnroll lifecycle: spins up Playwright Chromium/Edge browser, builds Serilog logger, registers all DI instances into Reqnroll's `ObjectContainer` -- `Support/` — helpers: `ConfigurationFactory` (loads config), `ConfigurationSaver`, `DirectoryHelper`, `ImageRetrieverHelper`, `ImageSaveHelper`, `TagsFactory` +- `Hooks/Hooks.cs` — Reqnroll lifecycle: spins up Playwright Chromium/Edge, builds Serilog logger, registers DI into Reqnroll's `ObjectContainer` +- `Support/` — helpers: `ConfigurationFactory`, `ConfigurationSaver`, `DirectoryHelper`, `ImageRetrieverHelper`, `ImageSaveHelper`, `TagsFactory` - `Models/` — strongly-typed bindings for `appsettings.json` sections (`ScrapeConfiguration`, `SearchConfiguration`, `ScrapeDirectories`, `UserConfiguration`, etc.) - `DTOs/` — deserialization models for `tagsTextToIgnore.json` and `tagsToIgnoreCompletely.json` **`AStar.Dev.Guard.Clauses`** — NuGet package. Single `GuardAgainst.Null()` helper. -**`AStar.Dev.Infrastructure.FilesDb`** — NuGet package. EF Core (SQL Server) infrastructure for file metadata. `EnumerableExtensions` provides filtering/sorting/duplicate-detection over `FileDetail` collections. Note: marked with `[Refactor]` — may duplicate logic in the files API project. +**`AStar.Dev.Infrastructure.FilesDb`** — NuGet package. EF Core (SQL Server) infrastructure for file metadata. `EnumerableExtensions` provides filtering/sorting/duplicate-detection over `FileDetail` collections. Marked `[Refactor]` — may duplicate logic in files API project. -**`AStar.Dev.Utilities`** — NuGet package. Extension methods and helpers shared across AStar packages (`StringExtensions`, `LinqExtensions`, `EncryptionExtensions`, `EnumExtensions`, `PathOperationExtensions`, `RegexExtensions`, `ObjectExtensions`, `ApplicationPathsProvider`). +**`AStar.Dev.Utilities`** — NuGet package. Shared extension methods/helpers (`StringExtensions`, `LinqExtensions`, `EncryptionExtensions`, `EnumExtensions`, `PathOperationExtensions`, `RegexExtensions`, `ObjectExtensions`, `ApplicationPathsProvider`). ### Configuration -`ConfigurationFactory` merges two sources (in order, later overrides earlier): -1. `appsettings.json` in the project root — non-secret defaults +`ConfigurationFactory` merges two sources (later overrides earlier): +1. `appsettings.json` in project root — non-secret defaults 2. **User Secrets** (`UserSecretsId: c35e09dc-dc30-416a-95a6-ec1a5ba1b43f`) — SQL connection string, login credentials Sensitive fields (`connectionStrings.sqlServer`, `userConfiguration.password`) must live in User Secrets, not `appsettings.json`. @@ -54,12 +58,12 @@ Sensitive fields (`connectionStrings.sqlServer`, `userConfiguration.password`) m ### Logging -Serilog, configured via `appsettings.json`. Writes to Console and Seq (`http://localhost:5341`). The `Hooks` constructor also hard-codes a Seq sink at the same address. +Serilog via `appsettings.json`. Writes to Console and Seq (`http://localhost:5341`). `Hooks` constructor also hard-codes Seq sink at same address. ### Tag filtering -Two JSON files control which wallpapers are skipped: -- `tagsToIgnoreCompletely.json` — skip any image whose tags include these values -- `tagsTextToIgnore.json` — skip based on partial text match in tag names +Two JSON files control skipped wallpapers: +- `tagsToIgnoreCompletely.json` — skip images whose tags include these values +- `tagsTextToIgnore.json` — skip on partial text match in tag names -Both are loaded by `TagsFactory` and registered into Reqnroll's DI container in `Hooks`. +Both loaded by `TagsFactory`, registered into Reqnroll DI in `Hooks`. \ No newline at end of file diff --git a/CLAUDE.original.md b/CLAUDE.original.md new file mode 100644 index 0000000..b461c2f --- /dev/null +++ b/CLAUDE.original.md @@ -0,0 +1,65 @@ + + +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Commands + +```bash +# Build entire solution +dotnet build AStar.Dev.Web.Scrapper.slnx + +# Run all Reqnroll scenarios +dotnet test AStar.Dev.Web.Scrapper.slnx + +# Run a specific scenario by name +dotnet test --filter "FullyQualifiedName~DownloadImagesFromSearchResults" + +# Run a specific project's tests +dotnet test AStar.Dev.Wallpaper.Scrapper/AStar.Dev.Wallpaper.Scrapper.csproj +``` + +## Architecture + +Solution targets **net10.0** across all projects. `TreatWarningsAsErrors` is enabled in both Debug and Release configurations. + +### Projects + +**`AStar.Dev.Wallpaper.Scrapper`** — the main executable. Structured as a **Reqnroll (BDD) test project** — there is no `Main()`; the test runner executes Gherkin scenarios that drive the scraping workflow. + +- `Features/` — Gherkin `.feature` files (three scraping workflows: search results, subscriptions, top wallpapers) +- `StepDefinitions/` — Reqnroll step bindings wired to Playwright page objects +- `Pages/` — Page Object Model classes wrapping Playwright `IPage` (login, search results, image page, subscriptions, top wallpapers) +- `Hooks/Hooks.cs` — Reqnroll lifecycle: spins up Playwright Chromium/Edge browser, builds Serilog logger, registers all DI instances into Reqnroll's `ObjectContainer` +- `Support/` — helpers: `ConfigurationFactory` (loads config), `ConfigurationSaver`, `DirectoryHelper`, `ImageRetrieverHelper`, `ImageSaveHelper`, `TagsFactory` +- `Models/` — strongly-typed bindings for `appsettings.json` sections (`ScrapeConfiguration`, `SearchConfiguration`, `ScrapeDirectories`, `UserConfiguration`, etc.) +- `DTOs/` — deserialization models for `tagsTextToIgnore.json` and `tagsToIgnoreCompletely.json` + +**`AStar.Dev.Guard.Clauses`** — NuGet package. Single `GuardAgainst.Null()` helper. + +**`AStar.Dev.Infrastructure.FilesDb`** — NuGet package. EF Core (SQL Server) infrastructure for file metadata. `EnumerableExtensions` provides filtering/sorting/duplicate-detection over `FileDetail` collections. Note: marked with `[Refactor]` — may duplicate logic in the files API project. + +**`AStar.Dev.Utilities`** — NuGet package. Extension methods and helpers shared across AStar packages (`StringExtensions`, `LinqExtensions`, `EncryptionExtensions`, `EnumExtensions`, `PathOperationExtensions`, `RegexExtensions`, `ObjectExtensions`, `ApplicationPathsProvider`). + +### Configuration + +`ConfigurationFactory` merges two sources (in order, later overrides earlier): +1. `appsettings.json` in the project root — non-secret defaults +2. **User Secrets** (`UserSecretsId: c35e09dc-dc30-416a-95a6-ec1a5ba1b43f`) — SQL connection string, login credentials + +Sensitive fields (`connectionStrings.sqlServer`, `userConfiguration.password`) must live in User Secrets, not `appsettings.json`. + +`ConfigurationFactory` also normalises `SearchConfiguration.SearchString` and `Subscriptions` URLs (strips base URL prefix and trailing page number) before returning. + +### Logging + +Serilog, configured via `appsettings.json`. Writes to Console and Seq (`http://localhost:5341`). The `Hooks` constructor also hard-codes a Seq sink at the same address. + +### Tag filtering + +Two JSON files control which wallpapers are skipped: +- `tagsToIgnoreCompletely.json` — skip any image whose tags include these values +- `tagsTextToIgnore.json` — skip based on partial text match in tag names + +Both are loaded by `TagsFactory` and registered into Reqnroll's DI container in `Hooks`. diff --git a/Directory.Build.props b/Directory.Build.props new file mode 100644 index 0000000..cfd2bd7 --- /dev/null +++ b/Directory.Build.props @@ -0,0 +1,134 @@ + + + + + + + + net10.0 + + + true + + + + + + latest + + + enable + + + enable + + + true + + + latest-recommended + true + + + + + + Jason Barden + AStar Development + Copyright © $([System.DateTime]::Now.Year) AStar Development + + + 0.1.0 + 0.1.0.0 + 0.1.0.0 + + + true + snupkg + + + true + true + + + https://github.com/your-org/astar-dev-mono/ + git + + + + + + $(MSBuildThisFileDirectory)artifacts/bin/$(MSBuildProjectName) + $(MSBuildThisFileDirectory)artifacts/obj/$(MSBuildProjectName) + + + true + true + + + + + + false + false + + $(NoWarn);CA1707;CA1859 + + + diff --git a/src/AStar.Dev.FunctionalParadigm/AStar.Dev.FunctionalParadigm.csproj b/src/AStar.Dev.FunctionalParadigm/AStar.Dev.FunctionalParadigm.csproj index b760144..84fc2de 100644 --- a/src/AStar.Dev.FunctionalParadigm/AStar.Dev.FunctionalParadigm.csproj +++ b/src/AStar.Dev.FunctionalParadigm/AStar.Dev.FunctionalParadigm.csproj @@ -1,9 +1,12 @@ - + - net10.0 - enable - enable + + CA1716 diff --git a/src/AStar.Dev.FunctionalParadigm/Result.cs b/src/AStar.Dev.FunctionalParadigm/Result.cs index d4760e8..d250637 100644 --- a/src/AStar.Dev.FunctionalParadigm/Result.cs +++ b/src/AStar.Dev.FunctionalParadigm/Result.cs @@ -1,16 +1,17 @@ -namespace AStar.Dev.FunctionalParadigm; +namespace AStar.Dev.FunctionalParadigm; -public abstract record Result +/// +/// Factory methods for creating instances of . +/// +public static class Result { - public static Result Success(TResult value) => new Ok(value); + /// + /// Creates a success wrapping the specified value. + /// + public static Result Success(TResult value) => new Ok(value); - public static Result Failure(TError error) => new Fail(error); - - public static implicit operator Result(TResult value) => Success(value); - - public static implicit operator Result(TError error) => Failure(error); + /// + /// Creates a failure wrapping the specified error. + /// + public static Result Failure(TError error) => new Fail(error); } - -public record Ok(TResult Value) : Result; - -public record Fail(TError Error) : Result; diff --git a/src/AStar.Dev.FunctionalParadigm/Result{TResult,TError}.cs b/src/AStar.Dev.FunctionalParadigm/Result{TResult,TError}.cs new file mode 100644 index 0000000..7006ad5 --- /dev/null +++ b/src/AStar.Dev.FunctionalParadigm/Result{TResult,TError}.cs @@ -0,0 +1,31 @@ +namespace AStar.Dev.FunctionalParadigm; + +/// +/// Represents the outcome of an operation that may either succeed with a +/// or fail with a . +/// Use the factory class to construct instances. +/// +/// The type of the success value. +/// The type of the failure error. +public abstract record Result +{ + /// + /// Implicitly lifts a success value into a . + /// + public static implicit operator Result(TResult value) => new Ok(value); + + /// + /// Implicitly lifts an error into a . + /// + public static implicit operator Result(TError error) => new Fail(error); +} + +/// +/// Represents a successful carrying a value. +/// +public record Ok(TResult Value) : Result; + +/// +/// Represents a failed carrying an error. +/// +public record Fail(TError Error) : Result; diff --git a/src/AStar.Dev.Guard.Clauses/AStar.Dev.Guard.Clauses.csproj b/src/AStar.Dev.Guard.Clauses/AStar.Dev.Guard.Clauses.csproj index c645a60..6c07208 100644 --- a/src/AStar.Dev.Guard.Clauses/AStar.Dev.Guard.Clauses.csproj +++ b/src/AStar.Dev.Guard.Clauses/AStar.Dev.Guard.Clauses.csproj @@ -1,18 +1,12 @@ - + - net10.0 - enable - enable astar.ico true AStar Development, Jason Barden AStar Development, 2025 https://github.com/astar-development/ https://github.com/astar-development/ - git - True - snupkg Readme.md true LICENSE @@ -24,14 +18,4 @@ - - True - 1701;1702; - - - - True - 1701;1702; - - diff --git a/src/AStar.Dev.Guard.Clauses/AStar.Dev.Guard.Clauses.xml b/src/AStar.Dev.Guard.Clauses/AStar.Dev.Guard.Clauses.xml index 1ea8107..404d8ec 100644 --- a/src/AStar.Dev.Guard.Clauses/AStar.Dev.Guard.Clauses.xml +++ b/src/AStar.Dev.Guard.Clauses/AStar.Dev.Guard.Clauses.xml @@ -16,7 +16,7 @@ Specifies the generic object to check for null. - + The object to check for null. diff --git a/src/AStar.Dev.Guard.Clauses/GuardAgainst.cs b/src/AStar.Dev.Guard.Clauses/GuardAgainst.cs index 169d8ca..a8fb905 100644 --- a/src/AStar.Dev.Guard.Clauses/GuardAgainst.cs +++ b/src/AStar.Dev.Guard.Clauses/GuardAgainst.cs @@ -1,4 +1,4 @@ -namespace AStar.Dev.Guard.Clauses; +namespace AStar.Dev.Guard.Clauses; /// /// The root class. @@ -11,7 +11,7 @@ public static class GuardAgainst /// /// Specifies the generic object to check for null. /// - /// + /// /// The object to check for null. /// /// @@ -20,6 +20,6 @@ public static class GuardAgainst /// /// Thrown when the object is, in fact, null. /// - public static T Null(T @object) - => @object is null ? throw new ArgumentNullException(nameof(@object)) : @object; + public static T Null(T value) + => value is null ? throw new ArgumentNullException(nameof(value)) : value; } diff --git a/src/AStar.Dev.Infrastructure.FilesDb/AStar.Dev.Infrastructure.FilesDb.csproj b/src/AStar.Dev.Infrastructure.FilesDb/AStar.Dev.Infrastructure.FilesDb.csproj index c530f63..cbeb768 100644 --- a/src/AStar.Dev.Infrastructure.FilesDb/AStar.Dev.Infrastructure.FilesDb.csproj +++ b/src/AStar.Dev.Infrastructure.FilesDb/AStar.Dev.Infrastructure.FilesDb.csproj @@ -1,24 +1,30 @@ - + - net10.0 - enable - enable astar.ico true AStar Development, Jason Barden AStar Development, 2025 https://github.com/astar-development/ https://github.com/astar-development/ - git - True - snupkg $(AssemblyName).xml + + $(NoWarn);CA1861 - - - - + + + + + @@ -40,20 +46,10 @@ - - - PreserveNewest - - - - - True - 1701;1702; - - - - True - 1701;1702; - + + + PreserveNewest + + diff --git a/src/AStar.Dev.Utilities/AStar.Dev.Utilities.csproj b/src/AStar.Dev.Utilities/AStar.Dev.Utilities.csproj index 239e838..49296ee 100644 --- a/src/AStar.Dev.Utilities/AStar.Dev.Utilities.csproj +++ b/src/AStar.Dev.Utilities/AStar.Dev.Utilities.csproj @@ -6,15 +6,12 @@ Extension methods, helpers, and shared primitives consumed across all AStar Development packages and applications. - Intentionally lean — TFM, nullable, analysers, IsPackable, GenerateDocumentationFile, - and SourceLink are all inherited from packages/Directory.Build.props and the - repo root Directory.Build.props. Only package-specific metadata lives here. + TFM, nullable, implicit usings, TreatWarningsAsErrors, IsPackable, IncludeSymbols, + SymbolPackageFormat, and output paths are all inherited from the repo-root + Directory.Build.props. Only package-specific metadata lives here. --> - net10.0 - enable - enable true true utilities;extensions;helpers;astar;astardevelopment @@ -25,15 +22,8 @@ https://github.com/astar-development/astar-dev-mono/tree/main/packages/core $(AssemblyName).xml No functional changes. Just a new 'home' on GitHub. - git - + - + + + + @@ -29,9 +37,10 @@ + + - PreserveNewest diff --git a/src/AStar.Dev.Wallpaper.Scrapper/App.axaml.cs b/src/AStar.Dev.Wallpaper.Scrapper/App.axaml.cs index e0cd66e..38f07d3 100644 --- a/src/AStar.Dev.Wallpaper.Scrapper/App.axaml.cs +++ b/src/AStar.Dev.Wallpaper.Scrapper/App.axaml.cs @@ -18,6 +18,7 @@ using AStar.Dev.Wallpaper.Scrapper.Workflows; using Microsoft.Playwright; using Serilog.Core; +using System.Globalization; using Testably.Abstractions; using System.IO.Abstractions; @@ -54,8 +55,8 @@ public override void OnFrameworkInitializationCompleted() .ToAppModel()) .AddSingleton(sp => new LoggerConfiguration() .MinimumLevel.Debug() - .WriteTo.Console() - .WriteTo.Seq("http://localhost:5341") + .WriteTo.Console(formatProvider: CultureInfo.InvariantCulture) + .WriteTo.Seq("http://localhost:5341", formatProvider: CultureInfo.InvariantCulture) .Enrich.WithExceptionDetails() .Enrich.FromLogContext() .ReadFrom.Configuration(sp.GetRequiredService()) @@ -73,6 +74,7 @@ public override void OnFrameworkInitializationCompleted() .AddTransient() .AddTransient() .AddTransient() + .AddTransient() .AddTransient() .AddTransient() .AddTransient() diff --git a/src/AStar.Dev.Wallpaper.Scrapper/ApplicationMetadata.cs b/src/AStar.Dev.Wallpaper.Scrapper/ApplicationMetadata.cs index 0feca64..2bb1b1f 100644 --- a/src/AStar.Dev.Wallpaper.Scrapper/ApplicationMetadata.cs +++ b/src/AStar.Dev.Wallpaper.Scrapper/ApplicationMetadata.cs @@ -11,5 +11,6 @@ public static class ApplicationMetadata public static string ApplicationFolder => Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location)!.CombinePath("..", "..", ".."); - public static string FileClassificationsExportFilePath => Path.Combine(SpecialDirectories.MyDocuments, "Scrapper", "FileClassifications.json"); + public static string FileClassificationsExportFilePath => SpecialDirectories.MyDocuments.CombinePath("Scrapper", "FileClassifications.json"); + public static string ScrapedTagsExportFilePath => SpecialDirectories.MyDocuments.CombinePath("Scrapper", "ScrapedTags.json"); } diff --git a/src/AStar.Dev.Wallpaper.Scrapper/DTOs/ScrapedTagExtensions.cs b/src/AStar.Dev.Wallpaper.Scrapper/DTOs/ScrapedTagExtensions.cs new file mode 100644 index 0000000..4e4b1b4 --- /dev/null +++ b/src/AStar.Dev.Wallpaper.Scrapper/DTOs/ScrapedTagExtensions.cs @@ -0,0 +1,32 @@ +using ScrapedTagDto = AStar.Dev.Wallpaper.Scrapper.DTOs.ScrapedTag; +using ScrapedTagDomain = AStar.Dev.Infrastructure.FilesDb.Models.ScrapedTag; +using ScrapedTagDomainId = AStar.Dev.Infrastructure.FilesDb.Models.ScrapedTagId; + +namespace AStar.Dev.Wallpaper.Scrapper.DTOs; + +public static class ScrapedTagExtensions +{ + public static ScrapedTagDomain ToDomain(this ScrapedTagDto dto) + => new() + { + Id = new ScrapedTagDomainId(dto.Id.Value), + Value = dto.Value, + Category = dto.Category, + IncludeInSearch = dto.IncludeInSearch + }; + + public static ScrapedTagDto ToDto(this ScrapedTagDomain domain) + => new() + { + Id = new ScrapedTagId(domain.Id.Value), + Value = domain.Value, + Category = domain.Category, + IncludeInSearch = domain.IncludeInSearch + }; + + public static List ToDomain(this List dtos) + => [.. dtos.Select(dto => dto.ToDomain())]; + + public static List ToDtos(this List domains) + => [.. domains.Select(domain => domain.ToDto())]; +} diff --git a/src/AStar.Dev.Wallpaper.Scrapper/MainWindow.axaml b/src/AStar.Dev.Wallpaper.Scrapper/MainWindow.axaml index e5a0451..87f1dd2 100644 --- a/src/AStar.Dev.Wallpaper.Scrapper/MainWindow.axaml +++ b/src/AStar.Dev.Wallpaper.Scrapper/MainWindow.axaml @@ -10,6 +10,8 @@