Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
49 commits
Select commit Hold shift + click to select a range
5887baa
ci: add security scanning workflows
joshsmithxrm Dec 20, 2025
c17631e
feat: wire CLI commands to Migration library and fix CMT compatibility
joshsmithxrm Dec 20, 2025
f53ab8c
feat: add schema generation and improve dependency detection
joshsmithxrm Dec 20, 2025
4988972
fix: CMT import compatibility - add required schema elements
joshsmithxrm Dec 20, 2025
d4cf5e0
fix: CmtDataReader to parse CMT format correctly
joshsmithxrm Dec 20, 2025
66a0c01
fix: CMT format compatibility for data.xml and schema writers
joshsmithxrm Dec 21, 2025
4a89a62
feat: implement M2M relationship export/import with CMT format compat…
joshsmithxrm Dec 21, 2025
097f79b
feat: add attribute filtering, user mapping, and plugin disable/enable
joshsmithxrm Dec 21, 2025
a33506a
fix: M2M schema generation - use entity-relative direction
joshsmithxrm Dec 22, 2025
8af287b
ci: bump actions/setup-dotnet from 4 to 5
dependabot[bot] Dec 22, 2025
addfa1d
ci: bump actions/checkout from 4 to 6
dependabot[bot] Dec 22, 2025
91d4140
deps: Bump coverlet.collector from 6.0.0 to 6.0.4
dependabot[bot] Dec 22, 2025
c8a1370
deps: Bump FluentAssertions from 6.12.2 to 8.8.0
dependabot[bot] Dec 22, 2025
7a250f7
deps: Bump Microsoft.Extensions.DependencyInjection from 8.0.1 to 10.0.1
dependabot[bot] Dec 22, 2025
cb01de5
deps: Bump Microsoft.Extensions.DependencyInjection.Abstractions and …
dependabot[bot] Dec 22, 2025
e812f90
deps: Bump Microsoft.Extensions.DependencyInjection.Abstractions and …
dependabot[bot] Dec 22, 2025
41207cf
feat: warn when connection pool contains multiple organizations
joshsmithxrm Dec 22, 2025
8d74ee8
feat: add parallel batch processing, progress tracking, and bug fixes
joshsmithxrm Dec 22, 2025
9167d4a
docs: add UpsertMultiple alternate key pitfall documentation
joshsmithxrm Dec 22, 2025
325b5f9
feat(dataverse): add progress reporting to bulk operations
joshsmithxrm Dec 22, 2025
639a9a8
feat(dataverse): add debug logging for batch execution
joshsmithxrm Dec 22, 2025
48b676a
feat(dataverse): add RatePerSecond property for stable throughput dis…
joshsmithxrm Dec 22, 2025
0b7e7dd
fix(dataverse): fix connection pool leak and add retry logic
joshsmithxrm Dec 22, 2025
5bff49c
fix(dataverse): add spin-wait to prevent connection pool over-expansion
joshsmithxrm Dec 22, 2025
4c00e6e
fix(dataverse): fix connection pool leak caused by non-resetting disp…
joshsmithxrm Dec 22, 2025
65acffa
docs: update bulk operations benchmarks with validated results
joshsmithxrm Dec 23, 2025
2dc975c
docs: add Microsoft performance recommendations and references
joshsmithxrm Dec 23, 2025
58f6b4f
feat(dataverse): use RecommendedDegreesOfParallelism by default
joshsmithxrm Dec 23, 2025
e5ad6ce
feat(dataverse): warn when parallelism exceeds pool size
joshsmithxrm Dec 23, 2025
fbecbd1
docs: add specs for throttle detection and connection health management
joshsmithxrm Dec 23, 2025
5d56cbc
feat(dataverse): implement throttle detection and intelligent routing
joshsmithxrm Dec 23, 2025
59280c7
docs: update benchmarks with server-recommended parallelism results a…
joshsmithxrm Dec 23, 2025
16af483
feat(dataverse): implement connection health management and TVP retry
joshsmithxrm Dec 23, 2025
ca83200
refactor(dataverse): centralize error handling in batch execution wra…
joshsmithxrm Dec 23, 2025
3f29c86
feat(dataverse): add SQL deadlock retry to bulk operations
joshsmithxrm Dec 23, 2025
52d9040
docs: add high-throughput parallelism benchmark results
joshsmithxrm Dec 23, 2025
0a20974
fix: address CodeQL findings
joshsmithxrm Dec 23, 2025
8d7e187
fix: resolve vulnerable transitive dependency
joshsmithxrm Dec 23, 2025
a19233b
deps: bump actions/setup-dotnet from 4 to 5
joshsmithxrm Dec 23, 2025
aa139cf
ci: bump actions/checkout from 4 to 6
joshsmithxrm Dec 23, 2025
7e6478f
deps: bump coverlet.collector from 6.0.0 to 6.0.4
joshsmithxrm Dec 23, 2025
ee5f6d9
deps: bump FluentAssertions from 6.12.2 to 8.8.0
joshsmithxrm Dec 23, 2025
c92a21d
deps: bump Microsoft.Extensions.DependencyInjection from 8.0.1 to 10.0.1
joshsmithxrm Dec 23, 2025
3ccc93a
deps: bump Microsoft.Extensions.DependencyInjection.Abstractions and …
joshsmithxrm Dec 23, 2025
21a0c57
deps: bump Microsoft.Extensions.Options to 10.0.1
joshsmithxrm Dec 23, 2025
fd32687
feat: implement transparent throttle-aware connection routing
joshsmithxrm Dec 23, 2025
e5da03d
fix: remove unused lastException variable in batch execution
joshsmithxrm Dec 23, 2025
6945548
refactor: replace recursive pool retry with bounded loop
joshsmithxrm Dec 23, 2025
23a640f
docs: add design specs for future enhancements
joshsmithxrm Dec 23, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 31 additions & 0 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,22 @@ on:
branches: [main]

jobs:
# Dependency review for PRs - checks for vulnerabilities in dependency changes
dependency-review:
runs-on: ubuntu-latest
if: github.event_name == 'pull_request'

steps:
- name: Checkout
uses: actions/checkout@v6

- name: Dependency Review
uses: actions/dependency-review-action@v4
with:
fail-on-severity: high
# Allow existing vulnerabilities, only fail on new ones
deny-licenses: ''

build:
runs-on: windows-latest

Expand All @@ -23,6 +39,21 @@ jobs:
- name: Restore dependencies
run: dotnet restore

- name: Check for vulnerable packages
shell: pwsh
run: |
$output = dotnet list package --vulnerable --include-transitive 2>&1
$output | Write-Host

if ($output -match "has the following vulnerable packages") {
Write-Host ""
Write-Host "::warning::Vulnerable packages detected - review security advisories above"
# Note: Not failing the build to avoid blocking on transitive dependencies
# that require upstream fixes. Dependency-review-action will catch new vulns.
} else {
Write-Host "No known vulnerabilities found in packages"
}

- name: Build
run: dotnet build --configuration Release --no-restore

Expand Down
75 changes: 75 additions & 0 deletions .github/workflows/codeql.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
# =============================================================================
# Security: CodeQL Analysis
# =============================================================================
# Performs semantic code analysis to find security vulnerabilities and
# code quality issues in C# code.
#
# Triggers:
# - Push to main branch
# - Pull requests to main
# - Weekly schedule (Monday 9 AM UTC)
#
# For more info: https://docs.github.com/en/code-security/code-scanning
# =============================================================================

name: 'Security: CodeQL'

on:
push:
branches:
- main
paths:
- 'src/**/*.cs'
- '**/*.csproj'
- '*.sln'

pull_request:
branches:
- main
paths:
- 'src/**/*.cs'
- '**/*.csproj'
- '*.sln'

schedule:
# Weekly scan on Monday at 9 AM UTC
- cron: '0 9 * * 1'

workflow_dispatch:

jobs:
analyze:
name: Analyze C#
runs-on: ubuntu-latest

permissions:
security-events: write
packages: read
actions: read
contents: read

steps:
- name: Checkout repository
uses: actions/checkout@v6

- name: Initialize CodeQL
uses: github/codeql-action/init@v4
with:
languages: csharp
# Use security-and-quality for comprehensive analysis
queries: security-and-quality

- name: Setup .NET SDK
uses: actions/setup-dotnet@v5
with:
dotnet-version: '8.x'

- name: Build solution
run: |
dotnet restore
dotnet build --no-restore --configuration Release

- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v4
with:
category: "/language:csharp"
4 changes: 2 additions & 2 deletions .github/workflows/publish-nuget.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,10 @@ jobs:
runs-on: windows-latest

steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6

- name: Setup .NET
uses: actions/setup-dotnet@v4
uses: actions/setup-dotnet@v5
with:
dotnet-version: |
8.0.x
Expand Down
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,10 +32,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Connection selection strategies: RoundRobin, LeastConnections, ThrottleAware
- Throttle tracking with automatic routing away from throttled connections
- Bulk operation wrappers: CreateMultiple, UpdateMultiple, UpsertMultiple, DeleteMultiple
- `IProgress<ProgressSnapshot>` support for real-time progress reporting during bulk operations
- DI integration via `AddDataverseConnectionPool()` extension method
- Affinity cookie disabled by default for improved throughput
- Targets: `net8.0`, `net10.0`

### Documentation

- Added UpsertMultiple pitfalls section to `BULK_OPERATIONS_PATTERNS.md` - documents the duplicate key error when setting alternate key columns in both `KeyAttributes` and `Attributes`

### Changed

- Updated publish workflow to support multiple packages and extract version from git tag
Expand Down
87 changes: 80 additions & 7 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# CLAUDE.md - ppds-sdk

**NuGet packages for Power Platform plugin development.**
**NuGet packages for Power Platform development: plugin attributes, Dataverse connectivity, and migration tooling.**

**Part of the PPDS Ecosystem** - See `C:\VS\ppds\CLAUDE.md` for cross-project context.

Expand All @@ -15,6 +15,10 @@
| Skip XML documentation on public APIs | Consumers need IntelliSense documentation |
| Multi-target without testing all frameworks | Dataverse has specific .NET requirements |
| Commit with failing tests | All tests must pass before merge |
| Create new ServiceClient per request | 42,000x slower than Clone/pool pattern; wastes ~446ms per instance |
| Guess parallelism values | Use `RecommendedDegreesOfParallelism` from server; guessing degrades performance |
| Enable affinity cookie for bulk operations | Routes all requests to single backend node; 10x throughput loss |
| Store pooled clients in fields | Causes connection leaks; get per operation, dispose immediately |

---

Expand All @@ -28,6 +32,10 @@
| Run `dotnet test` before PR | Ensures no regressions |
| Update `CHANGELOG.md` with changes | Release notes for consumers |
| Follow SemVer versioning | Clear compatibility expectations |
| Use connection pool for multi-request scenarios | Reuses connections, applies performance settings automatically |
| Dispose pooled clients with `await using` | Returns connections to pool; prevents leaks |
| Use bulk APIs (`CreateMultiple`, `UpdateMultiple`, `UpsertMultiple`) | 5x faster than `ExecuteMultiple` (~10M vs ~2M records/hour) |
| Reference Microsoft Learn docs in ADRs | Authoritative source for Dataverse best practices |

---

Expand All @@ -47,13 +55,25 @@
```
ppds-sdk/
├── src/
│ └── PPDS.Plugins/
│ ├── Attributes/ # PluginStepAttribute, PluginImageAttribute
│ ├── Enums/ # PluginStage, PluginMode, PluginImageType
│ ├── PPDS.Plugins.csproj
│ └── PPDS.Plugins.snk # Strong name key (DO NOT regenerate)
│ ├── PPDS.Plugins/
│ │ ├── Attributes/ # PluginStepAttribute, PluginImageAttribute
│ │ ├── Enums/ # PluginStage, PluginMode, PluginImageType
│ │ ├── PPDS.Plugins.csproj
│ │ └── PPDS.Plugins.snk # Strong name key (DO NOT regenerate)
│ ├── PPDS.Dataverse/
│ │ ├── BulkOperations/ # CreateMultiple, UpdateMultiple, UpsertMultiple
│ │ ├── Client/ # DataverseClient, IDataverseClient
│ │ ├── Pooling/ # Connection pool, strategies
│ │ ├── Resilience/ # Throttle tracking, retry logic
│ │ └── PPDS.Dataverse.csproj
│ ├── PPDS.Migration/ # Migration engine library
│ └── PPDS.Migration.Cli/ # CLI tool (ppds-migrate)
├── tests/
│ └── PPDS.Plugins.Tests/
│ ├── PPDS.Plugins.Tests/
│ └── PPDS.Dataverse.Tests/
├── docs/
│ ├── adr/ # Architecture Decision Records
│ └── architecture/ # Pattern documentation
├── .github/workflows/
│ ├── build.yml # CI build
│ ├── test.yml # CI tests
Expand Down Expand Up @@ -200,11 +220,64 @@ namespace PPDS.Plugins.Enums; // Enums
|------|---------|
| `PPDS.Plugins.csproj` | Project config, version, NuGet metadata |
| `PPDS.Plugins.snk` | Strong name key (DO NOT regenerate) |
| `PPDS.Dataverse.csproj` | Dataverse client library |
| `CHANGELOG.md` | Release notes |
| `.editorconfig` | Code style settings |

---

## ⚡ Dataverse Performance (PPDS.Dataverse)

### Microsoft's Required Settings for Maximum Throughput

The connection pool automatically applies these settings. If bypassing the pool, you MUST apply them manually:

```csharp
ThreadPool.SetMinThreads(100, 100); // Default is 4
ServicePointManager.DefaultConnectionLimit = 65000; // Default is 2
ServicePointManager.Expect100Continue = false;
ServicePointManager.UseNagleAlgorithm = false;
```

### Service Protection Limits (Per User, Per 5-Minute Window)

| Limit | Value |
|-------|-------|
| Requests | 6,000 |
| Execution time | 20 minutes |
| Concurrent requests | 52 (check `x-ms-dop-hint` header) |

### Throughput Benchmarks (Microsoft Reference)

| Approach | Throughput |
|----------|------------|
| Single requests | ~50K records/hour |
| ExecuteMultiple | ~2M records/hour |
| CreateMultiple/UpdateMultiple | ~10M records/hour |
| Elastic tables | ~120M writes/hour |

### Key Documentation

- [Optimize performance for bulk operations](https://learn.microsoft.com/en-us/power-apps/developer/data-platform/optimize-performance-create-update)
- [Send parallel requests](https://learn.microsoft.com/en-us/power-apps/developer/data-platform/send-parallel-requests)
- [Service protection API limits](https://learn.microsoft.com/en-us/power-apps/developer/data-platform/api-limits)
- [Use bulk operation messages](https://learn.microsoft.com/en-us/power-apps/developer/data-platform/bulk-operations)

### Throttle Recovery (Known Limitation)

The pool handles service protection errors transparently (waits and retries). However, it currently resumes at **full parallelism** after recovery, which can cause re-throttling with extended `Retry-After` durations.

**Microsoft recommends** gradual ramp-up after throttle recovery. This is planned for a future enhancement (see ADR-0004).

**Workaround**: Use lower `MaxParallelBatches` to reduce throttle frequency:

```csharp
var options = new BulkOperationOptions { MaxParallelBatches = 10 };
await executor.UpsertMultipleAsync(entities, options);
```

---

## 🧪 Testing Requirements

- **Target 80% code coverage**
Expand Down
Loading
Loading