Merged
Conversation
Constructing an EsiClient on Blazor WebAssembly threw
"One or more errors occurred. (Operation is not supported on this platform.)"
before any request was made.
The default handler set HttpClientHandler.AutomaticDecompression
unconditionally. On the browser runtime that property is annotated
[UnsupportedOSPlatform("browser")] and the underlying BrowserHttpHandler
throws PlatformNotSupportedException from both the getter and the setter,
because the fetch API performs content decoding itself and exposes no way
to configure it.
Guard the assignment with HttpClientHandler.SupportsAutomaticDecompression,
which the browser handler defines as a compile-time constant false and which
returns true on every other supported platform. The property is a plain
bool getter that never throws, and it is present in every target framework
this project builds for (netstandard2.0, net462 through net48, netcoreapp3.1,
net6.0 and net7.0), so no additional conditional compilation is needed.
Behaviour is unchanged everywhere decompression is supported: the existing
#if NET split between DecompressionMethods.All and GZip|Deflate is preserved.
On Blazor WebAssembly the client now constructs successfully and the browser
handles gzip/deflate/brotli transparently.
The handler construction moves into a small private factory so that a
handler is still only allocated when no HttpClient is supplied by the caller.
Fixes #77
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Mirrors the versioning/CI system from the dev-prompt project, adapted for a
NuGet library.
Branch model:
- dev is the long-lived integration branch; everything targets it.
- master is the release branch. A dev->master merge cuts exactly one release.
Versioning (scripts/compute-version.sh):
- CalVer YYYY.(MM*100+DD).BUILD, e.g. 2026.909.1.
- Computed at release time from today's date + existing git tags (build =
highest same-day tag + 1, else 1). Nothing is hand-incremented.
- Tags are un-prefixed to match this repo's existing history; the version is
passed to `dotnet pack -p:Version=` so no file is mutated or committed.
- csproj <Version> is now a 0.0.0-dev placeholder.
Workflows:
- ci.yml PR gate on master + dev; aggregating `check` job for branch
protection. Replaces build-check.yml.
- ci-dev.yml per-commit build on push to dev.
- release.yml push to master (or manual dispatch) -> compute version, pack,
push to NuGet, GitHub Release (creates the tag), Discord.
Skips on [skip release] / doc- and CI-only changes. A manual
draft run packs without pushing to NuGet. Replaces deploy.yml.
- .github/actions/ci-dotnet shared restore + all-TFM Release build.
dependabot.yml: grouped weekly nuget + github-actions updates targeting dev;
semver-majors ignored (taken by hand).
Removed GetBuildVersion.psm1 (dead Azure-DevOps-era snippet) and the SAK
source-control junk PropertyGroup. .gitattributes keeps *.sh / *.yml LF-only
for the Linux runners.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Clears the Node 20 deprecation warning on the runners. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
ci: CalVer release automation + dev/master branch flow
Bumps the actions group with 5 updates: | Package | From | To | | --- | --- | --- | | [actions/checkout](https://github.com/actions/checkout) | `5` | `7` | | [actions/setup-dotnet](https://github.com/actions/setup-dotnet) | `5` | `6` | | [actions/upload-artifact](https://github.com/actions/upload-artifact) | `4` | `7` | | [softprops/action-gh-release](https://github.com/softprops/action-gh-release) | `2` | `3` | | [Ilshidur/action-discord](https://github.com/ilshidur/action-discord) | `0.3.2` | `0.4.0` | Updates `actions/checkout` from 5 to 7 - [Release notes](https://github.com/actions/checkout/releases) - [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md) - [Commits](actions/checkout@v5...v7) Updates `actions/setup-dotnet` from 5 to 6 - [Release notes](https://github.com/actions/setup-dotnet/releases) - [Commits](actions/setup-dotnet@v5...v6) Updates `actions/upload-artifact` from 4 to 7 - [Release notes](https://github.com/actions/upload-artifact/releases) - [Commits](actions/upload-artifact@v4...v7) Updates `softprops/action-gh-release` from 2 to 3 - [Release notes](https://github.com/softprops/action-gh-release/releases) - [Changelog](https://github.com/softprops/action-gh-release/blob/master/CHANGELOG.md) - [Commits](softprops/action-gh-release@v2...v3) Updates `Ilshidur/action-discord` from 0.3.2 to 0.4.0 - [Release notes](https://github.com/ilshidur/action-discord/releases) - [Commits](Ilshidur/action-discord@0.3.2...0.4.0) --- updated-dependencies: - dependency-name: actions/checkout dependency-version: '7' dependency-type: direct:production update-type: version-update:semver-major dependency-group: actions - dependency-name: actions/setup-dotnet dependency-version: '6' dependency-type: direct:production update-type: version-update:semver-major dependency-group: actions - dependency-name: actions/upload-artifact dependency-version: '7' dependency-type: direct:production update-type: version-update:semver-major dependency-group: actions - dependency-name: softprops/action-gh-release dependency-version: '3' dependency-type: direct:production update-type: version-update:semver-major dependency-group: actions - dependency-name: Ilshidur/action-discord dependency-version: 0.4.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: actions ... Signed-off-by: dependabot[bot] <support@github.com>
Drop the stored NUGET_API_KEY. The release job now requests a GitHub OIDC token (id-token: write) and exchanges it via NuGet/login@v1 for a ~1-hour API key, gated by a trusted-publishing policy on nuget.org bound to seraphx2/ESI.NET -> release.yml. nuget.org account: robmburke. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…ompression Only set AutomaticDecompression when the platform supports it
- TFMs: drop the EOL targets (netcoreapp3.1, net6.0, net7.0) and the five explicit .NET Framework entries -> netstandard2.0;net8.0. netstandard2.0 keeps .NET Framework 4.6.1+ consumers working; the library has no framework-specific #if, so those targets built byte-identical assemblies. - Microsoft.IdentityModel.Tokens / System.IdentityModel.Tokens.Jwt: 6.14.1 -> 8.22.0 - Microsoft.Extensions.*: 2.0.0 -> 8.0.x (LTS floor; consumers float up) - Newtonsoft.Json: 13.0.2 -> 13.0.4 - Drop the explicit System.Net.Http 4.3.4 reference (a known-advisory package); it is in-box on both targets now. - Microsoft.CSharp / System.Collections.Immutable: netstandard2.0-only now, in-box in the net8.0 shared framework. - OpportunitiesLogic: rename the `opportunities` using-alias to `Opportunities` (fixes CS8981, lower-cased type name). Local: dotnet build -c Release -> both TFMs, 0 errors, 0 warnings. dotnet pack -> clean nupkg, correct per-TFM dependency groups, no System.Net.Http. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Proves the Microsoft.IdentityModel 6.x -> 8.x bump is behaviour-neutral for the access-token validation path, offline (no EVE credentials, no network). - _SSOLogic: extract SsoLogic.ValidateAccessToken(token, ssoUrl, jwksJson) from Verify() as an internal seam. Same TokenValidationParameters, same raw claim reads, same field projection. Verify() keeps the JWKS fetch + affiliation lookup and is unchanged on the success path. Also: the JWKS fetch used `.GetAsync(url).Result.Content` — a blocking .Result inside an async method (sync-context deadlock risk) — now fully awaited; and the `jwtksUrl` typo is fixed. - ESI.NET.Tests: new xUnit project (net8.0), added to the solution. 7 tests — valid token projects claims, claim types are read raw (not remapped, the likeliest 6->8 silent break), wrong issuer / wrong signing key / expired beyond skew are rejected, expired within the 2s skew still validates, and a pinned snapshot of the real login.eveonline.com/oauth/jwks parses under IdentityModel 8.22.0 with Keys.First() being the RS256 signing key. - InternalsVisibleTo ESI.NET.Tests. - CI: ci-dotnet composite runs `dotnet test`; release.yml gates on it before pack. Local: restore -> build --no-restore -> test --no-build => 7/7 passed, 0 warnings. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…ecords
/dogma/dynamic/items/{type_id}/{item_id}/ was typed EsiResponse<Effect> and
returned an all-empty object; DynamicItem itself was a non-public class.
- Dogma.Attribute is now the { attribute_id, value } pair as it appears on an
item; Dogma.Effect is now { effect_id, is_default }. Both are what
/universe/types/{id}/ and the dynamic-items endpoint actually return inline.
- New Dogma.AttributeInfo / Dogma.EffectInfo are the full definitions from
/dogma/attributes/{id}/ and /dogma/effects/{id}/ (flat, no inheritance).
Modifier moves to EffectInfo.cs.
- DynamicItem is public; DogmaAttributes/DogmaEffects are List<Attribute> /
List<Effect>.
- Universe.Type drops its private duplicate Attribute/Effect classes and binds
to the Dogma types (via a `using Dogma =` alias so bare `Attribute` can't
collide with System.Attribute). Its value field goes float -> double.
- DogmaLogic: Attribute() -> EsiResponse<AttributeInfo>,
Effect() -> EsiResponse<EffectInfo>, DynamicItem() -> EsiResponse<DynamicItem>.
- ESI.NET.Tests/DogmaModelTests: 7 checks (the two *Info maps, DynamicItem,
Universe.Type, and a theory locking each endpoint's payload type). The JSON
is spec-shaped but illustrative; pinning real ESI response bodies as fixtures
is a follow-up.
Shapes verified against esi.evetech.net/meta/openapi.json.
Local: build 0 warnings, 14/14 tests.
BREAKING: DogmaLogic.Attribute()/Effect() return types; the meaning of
Dogma.Attribute/Effect; removal of Universe.Attribute/Effect.
Co-Authored-By: lsawin <mesa.lemur@gmail.com>
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
EsiResponse<T> was constructed synchronously and read the body twice with .Result inside the constructor (deadlock risk under a sync context, blocks a pool thread). - New internal static EsiResponse<T>.CreateAsync(response, path, ct): reads the body once, awaited (with the CancellationToken overload on net8; netstandard2.0 has no token overload for ReadAsStringAsync). The constructor is now private, takes the already-read body, and does only the synchronous header parsing. response.Dispose() moves to the factory's finally. - _noContentMessage was a readonly *instance* field, so the ~25-entry ImmutableDictionary was rebuilt on every response -> now static. - _noContentMessage[path] raw indexer -> TryGetValue(...) ? msg : "No Content". A 204 from an endpoint not in the table used to throw KeyNotFoundException into the catch and leave Message null. - Call sites (EsiRequest.Execute, SsoLogic.Verify) updated; both already async. - ESI.NET.Tests/EsiResponseTests: 10 in-memory cases (json object/array, non-json body, known/unknown 204, 304, error string, header parse, captured deserialization failure, response disposal). No caller-facing change beyond removing the public constructor. The ct parameter on CreateAsync is dormant until EsiCallOptions threads it. Local: build 0 warnings both TFMs, 24/24 tests. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…tic ETag
The client held the authorized character (SetCharacterData re-instantiated ~22
Logic objects) and the If-None-Match ETag in a *static* field shared across the
whole process. Both are now per-call state on a new EsiCallOptions.
EsiCallOptions { Character, CancellationToken, IfNoneMatch, Page }
- EsiRequest.Execute: `string token` param -> `EsiCallOptions options` (required,
positioned after `endpoint`). Threads CancellationToken into SendAsync and the
response read, Page -> `&page=`, IfNoneMatch -> header (tolerating quotes).
The static EsiRequest.ETag field is deleted.
- Every Logic class ctor is now (HttpClient, EsiConfig); the _data /
character_id / corporation_id / alliance_id fields are gone.
- Every endpoint method takes EsiCallOptions:
* required (no default) on authenticated endpoints -> forgetting the
character is now a compile error, not a runtime ArgumentException
* `= null` on public endpoints (unchanged call sites)
- `int page = 1` parameters folded into EsiCallOptions.Page and removed from 13
methods.
- EsiClient.SetCharacterData and SetIfNoneMatchHeader removed (class + IEsiClient).
- Stripped the now-unused `using ESI.NET.Models.SSO;` from 24 Logic files.
Tests: ESI.NET.Tests/EsiRequestTests - 8 cases via a capturing HttpMessageHandler
(url/datasource, path replacements, auth-guard throw, bearer token, &page=,
If-None-Match quoting, cancellation propagation, null-options tolerance).
BREAKING:
- Auth is per call: `client.Clones.List(new() { Character = data })` instead of
`client.SetCharacterData(data); client.Clones.List();`
- ETag: `options.IfNoneMatch` instead of `client.SetIfNoneMatchHeader(...)`
- Pagination: `new() { Page = 2 }` instead of a `page` argument.
Local: build 0 warnings both TFMs, 32/32 tests.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…andlers - AddEsi now registers IEsiClient as a typed HttpClient (AddHttpClient<IEsiClient, EsiClient>) and returns IHttpClientBuilder so callers can chain .AddStandardResilienceHandler() after referencing Microsoft.Extensions.Http.Resilience. Polly stays out of core. New AddEsi(Action<EsiConfig>) overload. - Http/EsiHeadersHandler: sets X-User-Agent (from EsiConfig.UserAgent) and Accept: application/json per request, skipping either if already present. - Http/EsiErrorLimitHandler + EsiErrorLimitState: reads X-Esi-Error-Limit-Remain/-Reset; once the budget is spent (or a 420 is returned) it blocks further sends on that client until the window resets, and throws EsiErrorLimitException on 420. - EsiClient ctor: a supplied HttpClient (DI pipeline or caller) is used as-is; headers/handler are only configured on a client the ctor creates itself. CreateDefaultHandler is now internal. - csproj: + Microsoft.Extensions.Http 8.0.1 (netstandard2.0-compatible). - ESI.NET.Tests/EsiHandlerTests: 7 cases (header add / no-duplicate / missing-UA throw; error-limit pass-through / 420 throw / blocks next send; AddEsi end-to-end through a stubbed primary handler). BREAKING: - AddEsi returns IHttpClientBuilder, not IServiceCollection; IEsiClient lifetime is now the typed-client default, not AddScoped. - A caller-supplied bare HttpClient no longer gets X-User-Agent / Accept added automatically. - The manual Accept-Encoding gzip/deflate request headers are gone; decompression is the primary handler's AutomaticDecompression. Local: build 0 warnings both TFMs, 39/39 tests. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…tions-c0f53b236f ci: bump the actions group with 5 updates
- SsoLogic.Verify: reuse the injected HttpClient instead of `new HttpClient()` per call; dispose the JWKS response; on validation failure throw InvalidOperationException instead of returning a blank AuthorizedCharacterData; affiliation lookup is now explicitly best-effort. XML docs note the CharacterOwnerHash re-login check. - CHANGELOG.md: full breaking-change list since 2023.12.12 with a before/after migration table (EsiCallOptions, SetCharacterData/SetIfNoneMatchHeader removal, AddEsi -> IHttpClientBuilder, Dogma Attribute/AttributeInfo split, TFM + dependency changes). - README.md: rewrote the DI / AddEsi / per-call-options / SSO sections for the new API; fixed the dead Azure DevOps build badge and swagger link. - ci-dotnet composite action: setup-dotnet@v5 -> v6 (consistency with #81). - Test: Verify throws InvalidOperationException on a bad token. Local: build 0 warnings both TFMs, 40/40 tests. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…nRefreshed Set OnTokenRefreshed and an authenticated call whose Character access token is within a minute of expiry is refreshed with its refresh token before the request goes out; Character is updated in place (Token / RefreshToken / ExpiresOn) and the callback is invoked with it so the caller can persist the rotated refresh token. No callback -> unchanged behaviour, no SSO calls made implicitly. - SsoLogic: internal static SsoHost(DataSource), RequestTokenAsync (HTTP Basic when SecretKey is set, else client_id in the body for a PKCE client), and RefreshAccessTokenAsync (mutates the character; ExpiresOn = now + expires_in). - EsiRequest.Execute: RefreshIfNeededAsync runs before the bearer token is attached, so the request (and the DI pipeline handlers) use the fresh token. - Refresh reuses the call's HttpClient. - Tests: expired -> refreshed + callback + new bearer on the real call; still-valid -> untouched; no callback -> untouched. - CHANGELOG / README updated. Local: build 0 warnings both TFMs, 43/43 tests. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…siTokenRefreshSink Refresh is now a pipeline concern, not something EsiRequest.Execute does inline, so a persist hook can be registered once at DI time instead of on every call. - EsiTokenRefreshHandler (DelegatingHandler): reads the character + optional per-call callback off the request, and if the access token is within a minute of expiry, exchanges the refresh token (routed through the rest of the pipeline via base.SendAsync), updates the character in place, swaps the bearer header, then invokes the per-call callback and — when a DI scope is available — a registered IEsiTokenRefreshSink (resolved in a fresh scope, so a scoped DbContext is fine). - IEsiTokenRefreshSink: register one (services.AddScoped<IEsiTokenRefreshSink, T>()) and it covers every authenticated call. EsiCallOptions.OnTokenRefreshed stays as the per-call / non-DI hook; both fire. - EsiRequest.Execute: stashes the character/callback on HttpRequestMessage (request.Options on net8, request.Properties on netstandard2.0) via EsiRequestState; no longer refreshes itself. - SsoLogic.RequestTokenAsync / RefreshAccessTokenAsync take a send delegate instead of an HttpClient. - AddEsi wires EsiTokenRefreshHandler outermost; non-DI EsiClient wraps its self-made handler with it (per-call callback only, no sink). - csproj: <LangVersion>latest</LangVersion>. - Tests: 5 cases incl. AddEsi + one sink registration -> every authenticated call refreshes and the sink fires. CHANGELOG / README updated (sink-first). Local: build 0 warnings both TFMs, 45/45 tests. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
A local, run-once console that walks the EVE SSO authorization-code flow in the browser, catches the redirect on http://localhost:8080/callback with an HttpListener, exchanges the code, calls Verify() to confirm the character and exercise the JWKS path, and prints a long-lived refresh token for the live auth probe / pre-release check. Optionally writes the token straight to a GitHub Actions secret via `gh secret set` (value over stdin, never argv). The repo has several GitHub remotes, so --repo is auto-resolved from `git remote get-url origin`. Not packed, not shipped. Added to the solution under a tools/ folder so it keeps building against the library API.
Compares the live ESI OpenAPI 3.1 document against the endpoint set the wrapper actually implements. The route, HTTP method and security live inside each Logic method body, so they are read from a Roslyn syntax walk of every Execute<T>(...) call; the response model T comes from reflecting the built ESI.NET assembly and is joined on (class, method). No hand-maintained manifest. Tier 1 diff: - orphaned (implemented, absent from spec) -> error (latent 404) - missing (in spec, not implemented) -> warning (error under --strict) - parameter-name drift -> warning Current run: 195/197 covered; 2 missing (GET /meta/changelog, /meta/compatibility-dates); 12 orphaned - the esi-bookmarks and esi-opportunities removals plus chat_channels, GET /characters/names and the public /search branch. Not packed. Added to the solution under tools/.
Flattens each covered endpoint's model (reflection) and its resolved 200
schema into a shared Node tree and walks them together.
- schema-shape / schema-type -> error
- schema-int-width, missing/extra prop,
enum drift -> warning (error under --strict)
- enum-unmodelled, date-as-string,
oneOf/anyOf/dictionary -> info
First run over 175 endpoints: 6 errors (2 array/object shape mismatches -
CorporationLogic.Standings, UniverseLogic.AsteroidBelt; 4 number-typed-as
int/string), plus warnings: 492 ids typed int32 vs the spec's int64, 7 enum
typos ("loan ", "cleamup", "vulnerable "), 12 unbound (often required)
properties, 89 stale properties.
Bullet and emphasis style, blank lines around headings and fenced blocks. Drops one parenthetical from the Discord line.
Runs tools/SpecCheck against the live ESI OpenAPI document. Schedule + workflow_dispatch only - not a PR/push gate; a failed run emails the maintainer as the nudge to address drift. workflow_dispatch takes a `strict` input to fail on warnings too. Currently red: 12 orphaned endpoints + 6 schema shape/type mismatches.
tests/ESI.NET.IntegrationTests - NOT in ESI.NET.sln, so the normal `dotnet test ESI.NET.sln` path never runs it. Runs only from .github/workflows/integration.yml (weekly + workflow_dispatch, the manual pre-release check). - PublicSmokeTests (collection "live"): ~25 unauthenticated GETs across ~12 tags, asserting 200 + bound Data + no exception. LiveFixture resolves entity ids by name and retries on transport errors / 5xx. - AuthProbeTests (collection "live-auth", [SkippableFact]): refresh-token exchange -> Verify (JWKS) -> a bearer call -> transparent refresh against live SSO. Skips unless ESI_CLIENT_ID + ESI_SECRET_KEY + ESI_REFRESH_TOKEN are set. First run flagged two real wrapper bugs (backlog): EsiResponse's JSON sniff doesn't trim, so endpoints whose body has a trailing newline return null Data; Bloodline.ship_type_id is non-nullable but ESI returns null.
Groups it with tests/ESI.NET.IntegrationTests, mirroring the tools/ folder. ProjectReference and the solution entry updated; InternalsVisibleTo is by assembly name so it is unaffected. Build + 45 tests green from the new path.
Replaces the plain one-line webhook with the embed format carried over from the old Azure DevOps pipeline (title links to the release, "Psianna Archeia" author), built with jq + curl so nothing needs hand-escaping. Still last step, still skips drafts, still test-webhook for dispatch / real-webhook for a dev->master release.
…alars
The 200/201 branch keyed on body.StartsWith("{")/EndsWith("}"), so any
endpoint whose body ends in a newline (several do) fell through to
Message and left Data null - a 200 with no exception and no data. It also
never bound bare-scalar bodies (a wallet balance, a CSPA cost).
Now: trim, then deserialize when the first non-space char can start a JSON
value ({ [ " - digit t f n). A genuinely non-JSON 200 body still goes to
Message with no exception; the outer catch still captures real
deserialization failures.
+3 tests (trailing newline, surrounding whitespace, bare decimal). 48/48.
Fixes 3 of the 4 integration smoke failures.
workflow_dispatch now defaults to a prerelease: packs <version>-<label>.<run#> (label input, default beta), pushes it to nuget.org + GitHub Packages, and stops - no tag, no GitHub Release, no Discord. Consumers opt in with `dotnet add package ESI.NET --prerelease`; same feed, no repo switch. Set prerelease=false on the dispatch to cut a full release by hand. push to master is unchanged (full release) and now also mirrors the package to GitHub Packages. permissions gains packages: write.
README is now first-time-implementer only: Install -> Setup (DI, with a non-DI fallback) -> Making a request -> Authenticated (SSO) as numbered steps, incl. a desktop/GUI note pointing at tools/MintToken -> Token refresh -> Resilience. No before/after, no "removed", no history. Fixes stale bits (Universe.Names takes List<int> not List<long>; DataSource lists Singularity; trimmed the duplicated user-agent prose and the EsiConfig sample). MIGRATION.md (new) holds every breaking change and the before/after tables, linked from the top of the README and from the changelog. CHANGELOG: new "Tooling & tests" block (test suites, the weekly SpecCheck spec-drift tool, CI/release automation, MintToken); the EsiResponse trim fix under Fixed; the Migration table replaced by a pointer to MIGRATION.md.
- assets/icon.svg + a GDI+ render script -> assets/icon.png (128x128), packed as PackageIcon - README.md packed as PackageReadmeFile (renders on nuget.org) - PackageTags, Copyright (matches LICENSE.txt), PackageReleaseNotes dotnet pack is warning-clean; the .nupkg carries icon, readme, license, and a <repository> commit entry.
New _esi.MilitaryCampaigns accessor. Campaign and objective listings are public; the character's own objective progress needs esi.activity.char:read. - MilitaryCampaigns.All() -> MilitaryCampaignList - MilitaryCampaigns.Get(campaign_id) -> MilitaryCampaign - MilitaryCampaigns.Objectives(campaign_id, ...) -> MilitaryObjectiveList - MilitaryCampaigns.Objective(campaign_id, obj_id) -> MilitaryObjective - MilitaryCampaigns.CharacterObjectives(...) -> CharacterMilitaryObjectiveList - MilitaryCampaigns.CharacterObjective(objective_id) -> CharacterMilitaryObjective Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…endpoints) New _esi.Structures accessor for the 2026 structure types. esi-structures.read_corporation.v1 / esi-structures.read_character.v1; /skyhooks/raidable is public. - Structures.Skyhooks() / Skyhook(id) -> SkyhookList / Skyhook - Structures.SovereigntyHubs() / SovereigntyHub(id) -> SovereigntyHubList / SovereigntyHub - Structures.MercenaryDens() / MercenaryDen(id) -> MercenaryDenList / MercenaryDen - Structures.RaidableSkyhooks() -> RaidableSkyhookList Sovereignty-hub workforce_transport (import/export/transit oneOf) kept loose. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
New _esi.Cosmetics accessor. Global Paragon listings and a design lookup
are public; the rest need esi.cosmetic.char:read.
- Cosmetics.ParagonListings(...) -> ParagonListingPage
- Cosmetics.Paragon{Alliance,Character,Corporation}Listings(id, ...)
- Cosmetics.MyParagonListings(...)
- Cosmetics.Skinr(skinr_id) -> SkinrDesign
- Cosmetics.MyLicenses() -> SkinrLicenses
- Cosmetics.MyComponents() -> SkinrComponents
price / target / runs / slot-configuration oneOf fields kept loose.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…ndpoints) Coverage is now 233/233 - every endpoint in the 2026-08-18 snapshot. Character (esi-access.read_lists.v1 / esi-activities.read_character.v1): - Character.AccessLists() / AccessList(id) - Character.MercenaryTacticalOperations() / MercenaryTacticalOperation(id) New _esi.Meta accessor (all public): - Meta.Changelog() / CompatibilityDates() / Name() / Status() SpecCheck allowlist is now empty. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
- MIGRATION.md sections 12-13: compatibility-date versioning, the Route / Sovereignty / Character / Corporation changes, and the new endpoint groups with their scopes - CHANGELOG: compatibility-date block + "every endpoint since 2020 is wrapped" - README: DataSource note (X-Tenant), a line on the pinned compatibility date - Integration smoke tests: fix Route_between_two_hubs and Sovereignty_systems for the new return shapes; add Meta / MilitaryCampaigns / FreelanceJobs / raidable-skyhooks round-trips Coverage 233/233. SpecCheck allowlist empty. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
SpecCheck now reads /meta/compatibility-dates, takes the newest, and checks the wrapper against that snapshot - not against the date the wrapper pins (EsiVersion.CompatibilityDate). When CCP publishes a new date the run goes red with the diff, which is the cue for a catch-up release: wrap/fix the changes, bump EsiVersion.CompatibilityDate, green again. When the two dates match the wrapper is current (printed on every run). Falls back to the pinned date if /meta is unreachable, and leaves an explicit --spec <file> alone. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Live ESI returns ship_type_id: null for bloodlines with no starter ship, even though the spec marks it required and non-null. Surfaced by the integration smoke test. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The spec almost never declares nullability (0 nullable:true in 2026-08-18), so a field ESI actually returns as null - like Bloodline.ship_type_id - throws for every caller and SchemaCheck can't see it. --probe fetches every public GET (path params from a fixture set) and deserializes the real body into the wrapper's model; a JsonException is a failure. Current state: 75 endpoints, 0 failures (2 skipped - military-campaigns needs a live campaign uuid). Also: FromClr now tracks nullability, and CompareScalar flags the rare case where the spec *does* declare null against a non-nullable model (schema-nullable, Error). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
When ESI_CLIENT_ID / ESI_SECRET_KEY / ESI_REFRESH_TOKEN are set, --probe exchanges the refresh token, resolves character/corp/alliance from the access token, and probes every authenticated GET. Sub-ids (contract_id, mail_id, planet_id, project_id, killmail, ...) are pulled from the matching list endpoint; anything unresolvable or non-200 is skipped and reported. New workflow_dispatch-only probe.yml runs it with the SSO secrets. Not scheduled: it rotates ESI_REFRESH_TOKEN, which would break the scheduled integration workflow. Public probe unchanged: 75 ok, 0 failures. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Fetches the full scope list from the spec's securitySchemes so the probe token can reach the whole authenticated surface. Anything else is still a literal space/comma list. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Directory.Build.props: AnalysisLevel=latest-all + EnforceCodeStyleInBuild, applied to every project via the SDK's built-in Microsoft.CodeAnalysis.NetAnalyzers. No external service, no token, nothing to go stale - the free replacement for SonarCloud (whose Automatic Analysis doesn't support C# anyway, which is most likely why it's been failing). Warnings only, build still green (2225 across the repo on a clean rebuild - see follow-up discussion on what's real vs. DTO-shape noise). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The one CA2007 category worth acting on from the analyzer sweep: 233 'await Execute<T>(...)' call sites across every *Logic.cs class, plus 4 awaits in SsoLogic.GetToken/RevokeToken that predated the refresh-handler work and never got the same treatment. This is a library, not an app - a caller running under a sync context (classic ASP.NET, some WPF/desktop code) that blocks on one of these calls (.Result / .Wait()) can deadlock without it. Every await in the request/response layer now opts out of capturing the caller's context. Left alone (deliberately): the CA2007/CA1305/etc. still open in tools/ and tests/ - console processes have no sync context, so the same risk doesn't apply there. Build: 0 errors. CA2007 in ESI.NET/ (the shipped library): 233 -> 0. Full unit suite: 48/48 passing, unchanged (ConfigureAwait doesn't alter observable behavior in a single-threaded test host). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Symbols: IncludeSymbols/SymbolPackageFormat=snupkg + Microsoft.SourceLink.GitHub so a consumer can step into ESI.NET's actual source while debugging. release.yml's GitHub Packages push gets --no-symbols - that registry has no symbol server (confirmed against GitHub's own NuGet registry docs, which never mention .snupkg), and dotnet nuget push auto-pushes a matching snupkg by default. nuget.org's push is untouched; it supports symbol packages natively. License: PackageLicenseExpression=MIT (SPDX id, nuget.org's own recommendation for a standard license) replaces the packed LICENSE.txt/PackageLicenseFile. The repository's LICENSE.txt file is untouched - this only changes what goes into the .nupkg. Verified locally with a throwaway Determining projects to restore... Restored D:\git\ESI.NET\tools\SpecCheck\SpecCheck.csproj (in 290 ms). Restored D:\git\ESI.NET\tools\MintToken\MintToken.csproj (in 290 ms). Restored D:\git\ESI.NET\ESI.NET\ESI.NET.csproj (in 290 ms). Restored D:\git\ESI.NET\tests\ESI.NET.Tests\ESI.NET.Tests.csproj (in 290 ms). Successfully created package 'D:\git\ESI.NET\ESI.NET\bin\Release\ESI.NET.9.9.9-local.nupkg'. Successfully created package 'D:\git\ESI.NET\ESI.NET\bin\Release\ESI.NET.9.9.9-local.snupkg'. (no push anywhere): nuspec shows <license type="expression">MIT</license> and licenseUrl -> licenses.nuget.org/MIT, LICENSE.txt is no longer packed, and the .snupkg's PDB embeds a real Source Link URL to this repo/commit. Build clean, 48/48 unit tests pass. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
_noContentMessage mapped ~19 specific METHOD|path pairs to friendly strings
('Fleet invitation sent', 'Mail deleted', ...) for 204 responses. It never
affected correctness - the body is never read for a 204 regardless - it was
purely cosmetic, and it was already stale: none of the 39 endpoints added
earlier in this modernization pass were ever added to it.
EsiResponse<T>.Message on a 204 is now always "No Content", same pattern
already used for 304 ("Not Modified"). StatusCode + Endpoint already tell a
consumer everything the dictionary was decorating; this needs no maintenance
and can't go stale.
Consolidated the two tests this touched into one. Build clean, 47/47 unit
tests passing.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
CA1305 findings that were real, not cosmetic - int.Parse/DateTime.Parse default to CurrentCulture: - Expires / LastModified: dropped the manual GetValues().First() + DateTime.Parse(headerString) entirely. HttpContentHeaders already exposes these as pre-parsed DateTimeOffset? (RFC 1123-aware, correct regardless of host locale) - read those instead. A raw DateTime.Parse on an HTTP date can misparse or throw under a non-default culture; this removes that class of bug rather than just widening the parse call. - Pages / ErrorLimitRemain / ErrorLimitReset: int.Parse(..., CultureInfo.InvariantCulture). Lower real-world risk (plain ASCII digit headers) but the same principle. Build clean, 47/47 unit tests passing (Headers_are_parsed_onto_the_response already covered Expires/LastModified/Pages/ErrorLimit*, unchanged). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…sal, documented CA1031 suppression A closer pass over EsiRequest/EsiResponse/Extensions, prompted by the earlier analyzer sweep rather than a full formal code review: - Extensions.ToEsiValue(): replaced hand-rolled reflection over [EnumMember] (plus a dead Enum.Parse(...).GetType() call that did nothing) with resolution through the same StringEnumConverter every ESI.NET enum already declares - it can no longer drift from what real JSON serialization of the same enum would produce. Flags enums still decompose and rejoin with a bare comma (verified against actual Newtonsoft output: its own flags serialization inserts ", " with a space, which is not what ESI's comma-separated query params expect). - DataSource and GrantType get the StringEnumConverter they were missing (SearchCategory already needed it for the above and now has it too) - without it, a direct JSON serialization of either would have emitted the underlying int, not the ESI string. Confirmed no model exposes any of the three as a serialized property, so this only changes ToEsiValue's own behavior, not accidentally something else. - EsiRequest.Execute<T>: using-var request - CA2000, low severity in practice (HttpRequestMessage holds no real OS resource) but free to fix since request isn't touched after both awaits complete. - EsiResponse's constructor catch(Exception): documented CA1031 suppression instead of narrowing it. This is the boundary between untrusted ESI bytes and a typed object - the whole point is that an unanticipated parsing failure lands on Exception/Message instead of throwing out of Execute<T>. Enumerating specific exception types would defeat that: the next ESI quirk nobody's hit yet would throw straight through instead of being caught. New ExtensionsTests.cs (6 tests): single-value resolution, the two previously-unconverted enums, one flag, two flags (asserts no space), three flags in a different combination order. Build clean (0 warnings), 53/53 unit tests passing. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…L segments
222 long.ToString() calls across Logic/*.cs building {placeholder} URL path
segments (e.g. alliance_id.ToString()) now pass CultureInfo.InvariantCulture
explicitly. Real-world risk was always low - a bare integer ToString() only
diverges from ASCII digits under an unusual locale configuration - but it's
a free fix with zero downside, so fixed rather than suppressed (unlike
CA1707/CA1724 elsewhere, where the analyzer's suggestion would itself be the
regression).
The one instance in this sweep that was NOT just cosmetic - a genuine
long.Parse(string) on a JWT claim in _SSOLogic.cs - is fixed in the next
commit alongside the other _SSOLogic.cs changes.
Build clean, 53/53 unit tests passing (pre-existing suite; _SSOLogic.cs's
own test additions land in the next commit).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…le noise CA5394 - GenerateChallengeCode() (the PKCE code_verifier, RFC 7636) used System.Random, which is seeded and predictable. PKCE's entire protection against authorization-code interception depends on the verifier being unpredictable. Replaced with RandomNumberGenerator, with reject-and-retry (not a plain modulo) so every character is exactly equally likely rather than slightly biased toward the first few. CA5404 - ValidateAccessToken() had ValidateAudience = false. Checked CCP's own published JWT validation docs (docs.esi.evetech.net/docs/sso/validating_eve_jwt.html): validating the aud claim is one of four steps they document as required, alongside signature/issuer/expiry (all three of which this code already does). A real EVE SSO JWT's aud is [clientId, "EVE Online"], and skipping this check means a well-formed, correctly-signed token issued for a *different* registered application would be accepted here - client confusion / cross-app token replay. Fixed with a custom AudienceValidator requiring both values; ValidateAccessToken now takes clientId and Verify() passes _config.ClientId through. Also fixed in this file: the one genuine CA1305 in the earlier sweep - long.Parse on the JWT sub claim now takes CultureInfo.InvariantCulture, same treatment as the header-parsing fix in EsiResponse.cs. Verification, because guessing wrong here breaks every login: - SsoTokenValidationTests.cs: DefaultClaims() now includes a correct aud claim (every existing test exercises the happy path against it), plus two new tests - a token audienced for a different client_id is rejected, and a token missing the "EVE Online" audience is rejected. - Live: nothing in CI previously exercised SsoLogic.Verify()/ValidateAccessToken against a real token - the probe's token exchange talks to the SSO endpoint directly and never called into SsoLogic. Added exactly that call to Probe.cs's ProbeAuthAsync (calls Verify() on the real access token, exactly as a consumer would) so this is checked against reality, not just a synthetic fixture built from reading CCP's docs. Dispatching probe.yml to confirm before considering this done. One residual unknown, noted rather than guessed at: CCP's docs describe Tranquility's JWT shape. I could not verify whether Serenity (login.evepc.163.com) uses the identical aud format - the same SSO software almost certainly backs both, but this is unverified for that path specifically. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…y covered AuthProbeTests.cs's AuthFixture.InitializeAsync() already exchanges the refresh token and calls Client.SSO.Verify(Token) against a real token, on integration.yml's weekly schedule - I should have checked for that before adding a second, redundant live-verification path in Probe.cs. No harm done (it reused the access token already in scope, no extra token exchange), but no reason to keep two things doing the same job. Dispatching integration.yml to confirm the audience-validation fix against reality through the path that already existed for it. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Same encoding-loss bug as the ConfigureAwait sweep, caught then and reproduced here because the CA1305 script used the same utf-8-sig-read/ utf-8-write pattern - missed re-checking for it this time before committing. No functional effect (BOM doesn't affect compilation), just inconsistent with the rest of the codebase's file encoding. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…ring ESI)
Every method parameter that copied ESI's own snake_case field name
(alliance_id, character_id, max_war_id, ...) is renamed to camelCase
(allianceId, characterId, maxWarId). These were hand-typed against ESI's docs
while coding ~230 endpoints by hand - understandable at the time, not
idiomatic C#, and now worth fixing for real rather than suppressing (unlike
CA1724/CA1002/CA1819 elsewhere, where the analyzer's own suggestion would be
the regression).
Done with a small Roslyn-based tool (not regex) specifically because a
plain text rename is unsafe here: every one of these methods also has a
same-named STRING LITERAL - {"alliance_id", alliance_id.ToString()} - the
quoted key must stay snake_case (it matches the {alliance_id} placeholder
in the endpoint's URL template and, for POST bodies like Routes.Map, the
literal JSON field name ESI expects), while only the bare identifier should
change. Roslyn's syntax tree makes this trivial to get right by
construction: IdentifierNameSyntax nodes are never string-literal tokens,
so a search scoped to "real identifier references inside this one method"
can't touch the quoted key next to it. 283 identifier renames + 106
<param name="..."> doc-comment fixes across 25 files, each one verified by
inspecting a sample diff (plain params, array params, Dictionary<string,object>
POST bodies) before trusting the full run - confirmed the quoted keys never
moved.
EsiClient.cs handled by hand instead: its params were _config/_client
specifically because the fields were named config/client (no underscore) -
the reverse of the _client/_config-as-fields convention every Logic class
already uses. Fixed properly: fields renamed to _config/_client, params to
config/client, matching the rest of the codebase instead of just moving the
naming inconsistency around.
Breaking change, documented in CHANGELOG.md and MIGRATION.md §14: only
affects callers using named arguments; positional calls (the common case)
need no change.
Build clean, 0 warnings for CA1707, 55/55 unit tests passing.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
CA2227 (settable collection property) / CA1002 (List<T> in public API) / CA1819 (array-returning property) assume a hand-authored public API surface where a mutable collection is a design smell - a caller reaching in and mutating what should be encapsulated state. Every type under Models/ is a plain JSON-deserialization target instead: Newtonsoft needs a settable property to populate it, there's no encapsulation to violate, and switching to Collection<T>/ReadOnlyCollection<T> would be pure ceremony around a bag of fields with zero behavioral benefit. Scoped via Models/.editorconfig - confirmed CA2227/CA1819 were 100% confined to Models/, so this is a full, correct fix for those two. CA1002 also has 8 real hits on Logic/*.cs METHOD PARAMETERS (List<T> instead of IEnumerable<T> as an input type) - a different, still-valid concern this justification doesn't cover, so those are deliberately left alone (deferred to the long-tail pass). CA1724 (type name collides with its own namespace - Alliance, Corporation, Market, Wallet, ...): every one of these mirrors ESI's own terminology, and renaming any of them is a breaking public API change for a cosmetic rule. Same Models/.editorconfig covers the 9 Models/ cases; Extensions.cs (the one hit outside Models/, colliding with the unrelated Microsoft.Extensions.* family) gets a documented [SuppressMessage] on the class instead, same pattern as EsiResponse's CA1031 suppression. Build clean, 584 warnings remaining (was 1014). 55/55 unit tests passing. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…red params The 15 of 138 CA1062 hits that were NOT the deliberately-optional EsiCallOptions parameter (that one's handled separately - see next commit): config on EsiClient/EsiHeadersHandler/EsiTokenRefreshHandler/SsoLogic's constructors, request on both DelegatingHandler.SendAsync overrides, SsoLogic.Verify's token, Extensions.ToEsiValue's e, AssetsLogic's four itemIds parameters, KillmailsLogic.Information's killmailHash, and UniverseLogic's anyIds/names. Every one of these is a required input a method can't do anything sensible without - unlike EsiCallOptions, null here really is a caller mistake, and CA1062's suggested fix (throw ArgumentNullException naming the parameter) is the correct, idiomatic improvement over whatever NullReferenceException would happen downstream. Most visible behavior change: SsoLogic.Verify(null) now throws ArgumentNullException instead of being silently caught inside Verify's own try/catch and re-wrapped as "SSO access-token verification failed" - misleading for what's actually a caller bug, not a token/network problem. New regression test locks this in. Expression-bodied methods (AssetsLogic, KillmailsLogic, UniverseLogic) use the `x ?? throw new ArgumentNullException(nameof(x))` inline pattern rather than converting to block bodies. UniverseLogic.cs specifically can't `using System;` (collides with its own ESI.NET.Models.Universe.Type), so those two throws are fully qualified System.ArgumentNullException instead. Build clean, 56/56 unit tests passing (was 55 - one new regression test). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…tions param The remaining 123 of 138 CA1062 hits, all the same case: the trailing EsiCallOptions options = null parameter every endpoint method has. Unlike the 15 fixed in the previous commit, null here isn't a caller mistake - it's the correct, most common value (most calls need no special options at all), and the null case is already handled centrally in EsiRequest.Execute<T>. Throwing ArgumentNullException, which is CA1062's literal suggested fix, would turn the library's single most common call shape into a guaranteed crash - a regression, not an improvement. Added via the same kind of small Roslyn-based script as the CA1707 rename (123 identical [SuppressMessage] insertions across 24 files) rather than by hand. One wrinkle worth noting: CA1062's diagnostic column for these points at the *use site* (options: options, the argument passed into Execute<T>) rather than the parameter declaration - different from CA1707/CA1305, whose columns pointed at the declaration/call being flagged. Confirmed empirically before trusting the run, same as always. Build clean, CA1062 fully eliminated (138 -> 0). 56/56 unit tests passing. Warning count for the whole session's cleanup: 1292 -> 318. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…ons.cs Moves all 125 attribute-based suppressions - the 123 CA1062 "options" ones, plus CA1031 on EsiResponse's constructor and CA1724 on Extensions - out of the source files and into one GlobalSuppressions.cs using assembly-level [assembly: SuppressMessage(..., Scope = "member"/"type", Target = "~...")]. No reason to have 24 Logic files each carrying an identical 2-line attribute+justification block when the reasoning is exactly the same every time; the source files go back to reading exactly as they did before tonight's CA1062 pass, and every suppression + its justification now lives in one place that's easy to scan or audit as a whole. Target strings are real compiler-verified DocumentationCommentIds (via MSBuildWorkspace + ISymbol.GetDocumentationCommentId()), not hand-typed - SuppressMessage's Target format is strict and a wrong string just silently fails to suppress anything, so this was resolved through the actual semantic model rather than guessed from source text. Worth recording since it cost real time tonight: the first attempt at removing the inline attributes used Roslyn's AttributeListSyntax.FullSpan, which pulled in each method's XML doc comment as leading trivia (nothing else was claiming it) - deleted every /// <summary> in the process. Caught before committing, reverted clean, redone with a plain, exact-text regex match instead (validated against every file's expected count before writing anything, not after) - doc comments and blank lines confirmed intact this time. Build clean, 318 warnings (no change from before this move - purely a reorganization). 56/56 unit tests passing. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Clears every warning left in the library after tonight's CA1305/CA1707/
CA1062 passes - 16 rules, verified via a clean rm -rf bin/obj rebuild and
deduped by stripping the trailing [project::TFM] tag (the first pass had
each warning appearing up to 4x from stacked incremental builds).
Real fixes:
- EsiErrorLimitException gains the 3 standard exception constructors
(parameterless, (message), (message, innerException)) - CA1032.
- 5 undisposed HttpRequestMessage/StringContent objects in _SSOLogic.cs
and EsiClient's manually-wired handler are now `using` - CA2000.
- RequestTokenAsync forwards cancellationToken to ReadAsStringAsync on
net8.0 instead of dropping it, matching EsiResponse.cs's existing
#if NET pattern - CA2016.
- PKCE hashing uses SHA256.HashData on net8.0 instead of
SHA256.Create().ComputeHash(...) - CA1850. GenerateChallengeCode is
static - CA1822. RevokeToken/Verify call PostAsync/GetAsync with a Uri
instead of a raw string - CA2234.
- New internal Guard.NotNull() replaces 8 duplicated
`if (x == null) throw new ArgumentNullException(nameof(x))` guards
(from tonight's own CA1062 pass), resolving to
ArgumentNullException.ThrowIfNull on net8.0 - CA1510.
- AssetsLogic's 4 itemIds params, UniverseLogic.Names/.IDs, and
CreateAuthenticationUrl's scope widened List<T> -> IReadOnlyList<T> -
CA1002. Non-breaking: a caller passing List<T> is unaffected.
- EsiClient implements IDisposable, disposing _client only when it
created it itself (a new _ownsClient flag) - never a caller- or
AddEsi-IHttpClientFactory-supplied one, which isn't this instance's
to dispose - CA1001.
- RoutesFlag renamed RoutePreference - CA1711. Breaking, but it wasn't
actually a [Flags] enum and mirrored no ESI concept name, so there was
no reason to keep one colliding with the reserved Flag suffix.
Documented in GlobalSuppressions.cs rather than fixed (15 more hits,
7 rules): Event/Module/Structure(x2)/Dogma.Attribute keep names that
mirror ESI's own concepts rather than dodge a reserved-keyword/-suffix
collision (CA1716/CA1711, same reasoning as Extensions's CA1724);
EsiConfig.EsiUrl/.CallbackUrl, Corporation.Url, and
CreateAuthenticationUrl's return type stay string rather than
System.Uri - config/DTO surface manipulated as strings throughout,
not worth the blast radius for a parser that can throw where a lenient
string never would (CA1056/CA1055); four .Replace/.IndexOf calls skip
an explicit StringComparison/.Contains(char) because those overloads
don't exist on netstandard2.0 - verified by actually compiling against
it, not assumed (CA1307/CA2249); Verify's best-effort affiliation-lookup
catch{} joins EsiResponse<T>'s constructor under the existing
"must not invalidate an otherwise-good token" CA1031 justification.
Caught along the way: widening those 4 AssetsLogic parameter types
changed their compiler-computed signature, which silently orphaned
their existing CA1062 suppression Targets in GlobalSuppressions.cs (a
stale Target fails to suppress with no error - it isn't line-based, it's
a DocumentationCommentId keyed to the exact parameter-type list). Caught
by a rebuild showing them reappear as "new" warnings; fixed by updating
the 4 Target strings and re-verifying via another rebuild that they
actually suppress again.
Build clean on both TFMs, 0 warnings in the library, 56/56 tests
passing. MIGRATION.md / CHANGELOG.md updated for the RoutesFlag rename
and the rest of this pass.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
A beta.3 reviewer's app is net10.0 and it resolved the netstandard2.0 asset instead of net8.0 - net8.0 should already be forward-compatible with a net10.0 consumer via NuGet's resolution, but there's no reason to depend on that working as expected when an explicit net10.0 build removes the ambiguity entirely and lets net10.0 consumers get whatever newer-BCL code paths (ArgumentNullException.ThrowIfNull, SHA256.HashData, ...) are already conditionally compiled in via #if NET. net8.0 stays alongside it - it's supported through November 2026, and dropping it now would just be another breaking move with no upside. net11.0 (STS, ships alongside .NET's usual November release) won't get the same treatment when it lands: the pattern here is floor (netstandard2.0, permanent) + whichever LTS release(s) are currently in support, not "every version" - non-LTS releases are already served correctly by the netstandard2.0 fallback and don't stay in production long enough to be worth a dedicated build. net8.0 drops the same way net7.0 already did, once it actually reaches EOL. - ESI.NET.csproj: TargetFrameworks netstandard2.0;net8.0 -> netstandard2.0;net8.0;net10.0. - ESI.NET.Tests.csproj: TargetFramework net8.0 -> TargetFrameworks net8.0;net10.0, so CI actually runs the suite on both runtimes instead of only compiling for one of them. - .github/actions/ci-dotnet, .github/workflows/release.yml: setup-dotnet bumped 8.0.x -> 10.0.x. Caught before it shipped: an 8.0.x SDK doesn't understand a net10.0 TargetFramework at all (unlike the reverse - a newer SDK builds older TFMs fine), so this would have broken every PR check and the release pipeline the moment the csproj change landed. integration.yml/probe.yml stay on 8.0.x; they only ever build single-TFM net8.0 projects and never touch the multi-targeted library directly. Verified locally: clean build and 0 analyzer warnings on all three TFMs, 56/56 tests passing on both net8.0 and net10.0, `dotnet pack` produces all three lib/ folders in the .nupkg, and the exact ci-dotnet restore/build/test sequence run by hand. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
First release since
2023.12.12. Full details are in CHANGELOG.md and the breaking-change migration steps are in MIGRATION.md — every consumer needs code changes, all mechanical.Highlights:
netstandard2.0;net8.0;net10.0(was the oldnet462...net7.0sprawl)X-Compatibility-Date/X-Tenant), current through2026-08-18EsiCallOptions(character, cancellation, ETag, paging) replacing the old mutable-state settersNetAnalyzersrule set enabled repo-wide; 0 warnings in the libraryVerified before this PR: 56/56 unit tests on both
net8.0andnet10.0, 23/23 live integration tests against the real ESI/SSO API.🤖 Generated with Claude Code