Skip to content

Feat/performance and safety#6

Merged
diomonogatari merged 31 commits intomainfrom
feat/performance-and-safety
Feb 13, 2026
Merged

Feat/performance and safety#6
diomonogatari merged 31 commits intomainfrom
feat/performance-and-safety

Conversation

@diomonogatari
Copy link
Copy Markdown
Owner

  • Refactor JSON deserialization to eliminate reflection fallback, optimize performance by deserializing directly from streams, and migrate to FrozenDictionary for enum lookups.
  • Introduce dedicated request DTOs for write operations, implement IDisposable for resource management, and enhance input validation across public API methods.
  • Update method signatures to return IReadOnlyList for better performance and clarity.
  • Add tracing support with OpenTelemetry and improve architectural tests.
  • All changes maintain backward compatibility where possible and ensure comprehensive test coverage.

Remove DefaultJsonTypeInfoResolver from s_jsonOptions so deserialization
uses only the source-generated BitbucketJsonContext. This ensures missing
type registrations fail fast with NotSupportedException rather than
silently falling back to slower reflection-based serialization.

- Separate s_writeJsonOptions retains reflection fallback for outbound
  request bodies that use anonymous types (to be replaced with typed
  DTOs in a future change)
- Register missing collection types in BitbucketJsonContext:
  Dictionary<string, BuildStats>, IEnumerable<KeyedUrl>,
  IEnumerable<User>, IEnumerable<DefaultReviewerPullRequestCondition>,
  IEnumerable<RefRestriction>
- Fix direct JsonSerializer.Serialize calls in Branches and Ssh clients
  to use s_writeJsonOptions
- Add SourceGenCoverageTests verifying all model types and PagedResults<T>
  instantiations are registered

All 773 tests pass.
Replace string-based deserialization in ReadResponseContentAsync<T> with
stream-based JsonSerializer.DeserializeAsync<T>. This avoids allocating
an intermediate UTF-16 string for every API response, which is especially
beneficial for large paged responses (100+ items).

The contentHandler overload path still reads as string for non-JSON
responses. Error handling continues to read the body as string since
error bodies are small and need to be included in exception messages.

All 773 tests pass.
- Convert all 25 Dictionary<TEnum, string> fields to FrozenDictionary
  for optimal read-only lookup performance
- Add 14 reverse FrozenDictionary<string, TEnum> fields with
  StringComparer.OrdinalIgnoreCase for O(1) string-to-enum lookups
- Replace O(n) FirstOrDefault reverse scans with O(1) TryGetValue
- Add missing Global value to ScopeTypes enum (Bitbucket API returns
  GLOBAL for hooks configured at server level)
- Improve XML documentation across all helper methods
- Add IDisposable with ownership tracking via _ownsClient flag
- HttpClient constructor owns the FlurlClient wrapper and disposes it
- IFlurlClient constructor does not own, disposal is a no-op
- Basic/token auth constructors have no disposable resources
- Add ObjectDisposedException guard in GetBaseUrl() to cover all API methods
- Dispose is idempotent (safe to call multiple times)
- Add 6 unit tests covering all constructor variants and disposal semantics
- Add ExecuteAsync<TResult>, ExecuteAsync (bool), and
  ExecuteWithNoContentAsync methods that unify HTTP call + error handling
- New methods make it impossible to forget error handling in future code
- Add architectural test verifying all BitbucketClient HTTP calls have
  corresponding error handlers (HandleResponseAsync, HandleErrorsAsync,
  ExecuteAsync, or explicit StatusCode checks)
- Existing methods continue using HandleResponseAsync/HandleErrorsAsync;
  new methods should prefer ExecuteAsync for safety
BREAKING CHANGE: All buffered collection methods now return
Task<IReadOnlyList<T>> instead of Task<IEnumerable<T>>. This
accurately reflects that results are fully materialized and
enables direct Count/indexer access without LINQ.

- Change GetPagedResultsAsync<T> return type to IReadOnlyList<T>
- Update ~70 public method signatures across all API domains
- Methods not using GetPagedResultsAsync add .ToList() to convert
  IEnumerable<T> results to List<T> (implements IReadOnlyList<T>)
- Streaming methods (IAsyncEnumerable<T>) are unaffected
- Consumers using explicit IEnumerable<T> types will get compile
  errors; switch to var or IReadOnlyList<T>
- Add ArgumentException.ThrowIfNullOrWhiteSpace() guards on ~130
  public methods for URL-path parameters
- Validate projectKey, repositorySlug, commitId, hookKey,
  userSlug, scmId, loggerName and other path segment parameters
- Cover methods that bypass guarded URL-builder helpers
- Add InputValidationTests with parameterized theories
- Prevents malformed URLs and confusing server-side errors
- Extract s_jsonOptions and s_writeJsonOptions initialization into
  CreateReadOptions() and CreateWriteOptions() factory methods
- Call MakeReadOnly() after configuration to prevent accidental
  mutation from any thread
- Add JsonSerializerOptions_AreExplicitlyFrozen architectural test
- Update CHANGELOG.md
- Introduce EnumMap<TEnum> as single source of truth for all 25
  enum-to-string mappings (BitbucketEnumMaps static registry)
- Replace 13 individual converter subclasses with unified
  BitbucketEnumConverterFactory using generic JsonEnumConverter<TEnum>
- Slim BitbucketHelpers.cs from ~1100 to ~490 lines; methods now
  delegate to EnumMap instances
- Add public ToApiString() extension methods on all enum types
- Update CHANGELOG.md
- Count await vs ConfigureAwait(false) calls per source file
- Catches suppressions via #pragma or files excluded from analyzer
- Update CHANGELOG.md
Convert { get; set; } to { get; init; } on 106 response model classes.
32 request body model files (98 properties) retain mutable setters.
Refactored BranchMetadata getter to use object initializer.

Breaking change: consumers that assign model properties after
construction must move to object-initializer syntax.

All 799 tests pass. Zero warnings.
Create purpose-built request DTOs for all write API methods, replacing
reused response models and anonymous types. DTOs use required/init-only
properties and expose only API-relevant fields.

New types in Models/Core/Projects/Requests/:
  CreateProjectRequest, UpdateProjectRequest, CreateRepositoryRequest,
  ForkRepositoryRequest, CreatePullRequestRequest,
  UpdatePullRequestRequest, CreateBranchRequest, CreateTaskRequest,
  UpdateTaskRequest, CreateWebHookRequest, UpdateWebHookRequest,
  MergePullRequestRequest

New type in Models/Builds/Requests/:
  AssociateBuildStatusRequest

13 method signatures updated. All tests updated to use new DTOs.
Breaking change: method parameter types changed.

All 799 tests pass. Zero warnings.
Introduce PullRequestQueryBuilder, CommitQueryBuilder,
BranchQueryBuilder, and ProjectQueryBuilder with fluent API methods.

Entry points: client.PullRequests(), client.Commits(),
client.Branches(), client.Projects(). Each builder supports
GetAsync() for buffered results and StreamAsync() for
IAsyncEnumerable streaming. Builders delegate to existing flat methods.

Existing flat methods are unchanged (additive, non-breaking).

12 new tests covering all builders. 811 total tests pass.
Zero warnings.
Internal zero-allocation reader extracts pagination metadata
(isLastPage, nextPageStart, start, limit, size) directly from
UTF-8 bytes using Utf8JsonReader, skipping full deserialization.

New files:
  Common/PagedResultsReader.cs, Common/PagedMetadata.cs
  PagedResultsReaderTests.cs (5 tests)
  PagedResultsReaderBenchmarks.cs (6 benchmarks)

Added InternalsVisibleTo for test and benchmark projects.
816 tests pass. Zero warnings.
Add RetryAfter, RateLimit, RateLimitRemaining, and RateLimitReset
properties to BitbucketRateLimitException, parsed from standard HTTP
rate-limit response headers (Retry-After, X-RateLimit-Limit,
X-RateLimit-Remaining, X-RateLimit-Reset).

- New Create overload on BitbucketApiException accepts HttpResponseHeaders
- Graceful null on missing/unparseable headers
- 7 new tests (5 unit + 2 mock) — 823 total pass
- Updated CHANGELOG.md
- Version 0.2.0 -> 1.0.0
- Add PackageIcon with 128x128 placeholder (assets/icon.png)
- Expand PackageTags: rest-api, api-client, atlassian, sdk, dotnet
- Conditional icon inclusion (Condition=Exists)
- Verified dotnet pack produces BitbucketServer.Net.1.0.0.nupkg
- Updated CHANGELOG.md
Replace deprecated Polly v7 AddTransientHttpErrorPolicy with Polly v8
AddStandardResilienceHandler and custom AddResilienceHandler examples.

- Standard resilience handler with retry, circuit breaker, and timeout
- Custom resilience pipeline for fine-grained control over retried status codes
- Requires Microsoft.Extensions.Http.Resilience NuGet package
- Define IBitbucketClient with all ~230 public method signatures
- Update BitbucketClient to implement IBitbucketClient
- Update query builders to accept IBitbucketClient instead of concrete type
- Add NSubstitute 5.3.0 for mock-based testing
- Add InterfaceTests verifying parity, obsolete attributes, and mockability
- Update README DI examples to register IBitbucketClient
The parity and mockability tests were meta-assertions about
the interface shape, not meaningful behavioral tests.
Add distributed tracing instrumentation using System.Diagnostics.Activity,
following OTel stable HTTP client semantic conventions. No dependency on
OpenTelemetry SDK — pure DiagnosticSource (best practice for libraries).

- Create BitbucketClient.Tracing.cs partial class with ActivitySource
- Hook BeforeCall/AfterCall/OnError via Flurl 4 event handlers on GetBaseUrl()
- Tag spans with http.request.method, url.full, server.address, server.port,
  http.response.status_code, error.type, and custom bitbucket.project_key /
  bitbucket.repository_slug attributes
- Set ActivityStatusCode.Error for 4xx/5xx responses
- Record exception events with type and message on transport errors
- Store Activity ref on HttpRequestMessage.Options (AsyncLocal does not
  propagate back from Flurl's async RaiseEventAsync)
- Near-zero overhead when no listeners are subscribed
- Add TracingMockTests with ActivityListener-based assertions
Delete BitbucketEnumExtensions (38 dead enum wrappers) and
TypeExtensions (dead IsNullableType helper), along with their tests.
Delete 15 StringTo* methods (keep only StringToLogLevel which has
callers), StringToBool, and 4 *ToString methods that had zero call
sites. Remove corresponding tests.
Delete 3 private static overloads (ExecuteAsync<TResult>,
ExecuteAsync, ExecuteWithNoContentAsync) with zero call sites.
The stream-based Execute*StreamAsync methods remain in use.
Replace thin extension wrappers with direct BCL calls to
DateTimeOffset.FromUnixTimeMilliseconds in the JSON converters.
Delete UnixDateTimeExtensions.cs and its tests.
Introduce two convenience wrappers that absorb the repeated
GET + deserialize lambda used by all paginated endpoints.
The underlying GetPagedResultsAsync/StreamAsync loop methods
remain unchanged.
Replace inline GET + deserialize lambdas in all 64 list and
18 stream paginated methods with calls to GetPagedAsync<T>
and GetPagedStreamAsync<T>. Remove async from method signatures
where the only await was the paged call.
Split the monolithic IBitbucketClient (287 methods) into 12 focused
sub-interfaces co-located with their implementations:

- IProjectOperations, IRepositoryOperations, IPullRequestOperations
- ICommitOperations, IBranchOperations, IAdminOperations
- ISshOperations, IRefRestrictionOperations, IBuildOperations
- IGitOperations, ISearchOperations, IBitbucketMiscOperations

IBitbucketClient becomes a composition root inheriting all sub-interfaces.
Non-breaking change: all existing code using IBitbucketClient continues
to work. Consumers can now depend on narrower interfaces (ISP).
@diomonogatari diomonogatari merged commit 67ed7a9 into main Feb 13, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant