Agent-facing guide for the EvolutionScraper repository. Read this before making changes.
EvolutionScraper is a .NET 8.0 Windows Service that automatically books gym classes at Evolution gym via browser automation (PuppeteerSharp / Chromium) on a Quartz.NET schedule.
Solution structure:
EvolutionScraper/ # Class library — core scraper logic
EvolutionScraper.Service/ # Windows Service executable — DI setup, job scheduling
# Restore dependencies
dotnet restore
# Build (debug)
dotnet build
# Build (release)
dotnet build -c Release
# Run the service locally
dotnet run --project EvolutionScraper.Service
# Publish as a self-contained Windows Service
dotnet publish EvolutionScraper.Service -c Release -r win-x64 -o ./publish
# Clean build artifacts
dotnet cleanThere are currently no test projects in this solution.
When adding tests, the expected conventions are:
# Run all tests
dotnet test
# Run a single test by fully-qualified name
dotnet test --filter "FullyQualifiedName=Namespace.ClassName.MethodName"
# Run tests matching a pattern
dotnet test --filter "DisplayName~BookClass"
# Run tests in a specific project
dotnet test EvolutionScraper.Tests/EvolutionScraper.Tests.csprojUse xUnit as the preferred test framework if tests are added; follow the Namespace.Tests naming pattern for test projects.
- Target C# 12 features on .NET 8.0.
- Both projects enable
<Nullable>enable</Nullable>and<ImplicitUsings>enable</ImplicitUsings>. - Treat all nullable warnings as errors in your head — do not suppress them without justification.
| Construct | Convention | Example |
|---|---|---|
| Classes, Methods, Properties | PascalCase | BookClassAsync, ClassName |
| Private / instance fields | _camelCase |
_options, _browser, _page |
| Local variables, parameters | camelCase | launchOptions, classToBook |
| Constants | PascalCase | StartImmediatlyTriggerName |
| Async methods | Suffix Async |
LoginAsync, RunBrowserAsync |
- Prefer
sealedclasses to prevent unintended inheritance:public sealed class EvolutionScraper(...) : IDisposable, IAsyncDisposable { } internal sealed class DateTimeConverter : JsonConverter<DateTime> { }
- Use
recordfor immutable option/configuration types:public record EvolutionScraperOptions(string ChromePath, string Username, string Password) { public EvolutionScraperOptions() : this(string.Empty, string.Empty, string.Empty) { } }
- Use primary constructors (C# 12) for DI-injected classes:
internal class ScrapeJob( ILogger<ScrapeJob> logger, EvolutionScraperOptions options, Dictionary<DayOfWeek, ClassBooking[]> bookings) : BaseJob(logger)
- All I/O-bound methods must be
async Taskorasync ValueTask; name them with theAsyncsuffix. - Always apply
ConfigureAwait(false)in library (EvolutionScraper) code:await RunBrowserAsync().ConfigureAwait(false); await LoginAsync().ConfigureAwait(false);
- Use
ValueTaskfor hot-path or lightweight async operations;Taskeverywhere else. - Never call
.Resultor.GetAwaiter().GetResult()except insideDispose()where async is unavailable.
- Nullable reference types are enabled — never disable them project-wide.
- Use
is null/is not nullpattern matching for null checks (not== null):if (_page is null) throw new InvalidOperationException("Browser is not initialized.");
- Use the null-coalescing default operator
?? []or?? string.Emptyfor safe fallbacks. - Prefer
!postfix only when you can guarantee non-null via logic that the compiler cannot see.
- Catch exceptions at job/boundary level; do not swallow them silently.
- Log before re-throwing. In scraper code, dump the current page HTML for diagnostics:
private async Task ThrowLoggingPageAsync(Exception ex) { if (_page is not null) { string content = await _page.GetContentAsync().ConfigureAwait(false); await File.WriteAllTextAsync($"page_dump_{DateTime.Now:yyyyMMddHHmmss}.html", content) .ConfigureAwait(false); } throw ex; }
BaseJob.Executewraps all job execution in try/catch/finally — always delegate job logic toExecuteImplAsync.
- Classes that own unmanaged or disposable resources implement both
IDisposableandIAsyncDisposable. IAsyncDisposableis preferred at call sites when available;IDisposableexists as a sync fallback.- Always check
IsClosed/ null before disposing browser/page objects.
- Rely on implicit global usings for common namespaces (
System,System.Collections.Generic, etc.). - Add explicit
usingstatements at the top of each file, grouped as:Microsoft.*/System.*(alphabetical)- Third-party packages (
PuppeteerSharp,Quartz,NLog, etc.) - Internal project namespaces
- No unused
usingdirectives.
- Prefer collection expressions
[item1, item2]overnew List<T> { }where possible (C# 12):Args = ["--disable-blink-features=AutomationControlled", "--disable-dev-shm-usage"]
- Chain LINQ in method syntax; avoid query syntax.
- Use
?? []as an empty-collection fallback afterDeserialize.
- Use string interpolation (
$"...") for runtime values. - Use verbatim strings (
@"...") for multi-line JavaScript or long selectors injected into the page. - Format DateTime in file names with
DateTime.Now:yyyyMMddHHmmss.
- Use NLog via the
Microsoft.Extensions.Logging.ILogger<T>abstraction; never reference NLog directly in business logic. - Log at appropriate levels:
LogInformationfor normal flow,LogWarningfor recoverable issues,LogErrorfor exceptions. - Include structured context (job name, class name, booking time) in log messages.
- Register options via the custom
AddSingletonOption<T>extension inOptionsDIExtensions.cs:services.AddSingletonOption<EvolutionScraperOptions>() .AddSingletonOption<Dictionary<DayOfWeek, ClassBooking[]>>("Bookings")
- All DI registrations live in
Program.cs— do not scatter registrations across the codebase. - Prefer constructor injection (via primary constructors); avoid service locator patterns.
- Application settings live in
EvolutionScraper.Service/appsettings.json. - Do not commit real credentials. Use .NET User Secrets (
dotnet user-secrets) for local development and a secure vault for production. - Quartz job schedules are configured in
appsettings.jsonunderQuartzConfig; supports both simple interval and cron expression triggers.
- The
EvolutionScraperproject is a pure class library — it has no entry point and no direct dependency on hosting abstractions. - The
EvolutionScraper.Serviceproject owns all hosting, scheduling, and DI wiring. - JavaScript executed in the browser lives in
EvolutionScraper/class_selector.jsand is loaded as an embedded resource — keep browser automation scripts in.jsfiles, not inline C# strings. - DTOs shared between layers go in
EvolutionScraper/DTOs.cs; keep them plain data types (classorstruct), no logic. - Extension methods go in
Extensions.cswithin the relevant project; keep them focused and stateless.