Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
17 changes: 17 additions & 0 deletions .editorconfig
Original file line number Diff line number Diff line change
Expand Up @@ -68,3 +68,20 @@ csharp_new_line_before_open_brace = all
csharp_new_line_before_else = true
csharp_new_line_before_catch = true
csharp_new_line_before_finally = true

# Generated code - relax rules that are noisy for auto-generated files
[**/Generated/*.cs]
# Disable naming warnings (generated code may not follow conventions)
dotnet_diagnostic.IDE1006.severity = none

# Disable documentation warnings (generated code has its own docs)
dotnet_diagnostic.CS1591.severity = none

# Disable nullable warnings (generated code handles nullability its own way)
dotnet_diagnostic.CS8618.severity = none

# Don't enforce file-scoped namespaces on generated code
csharp_style_namespace_declarations = block_scoped:none

# Don't warn about partial class declarations
dotnet_diagnostic.CA1052.severity = none
80 changes: 79 additions & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@
| 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 |
| Use magic strings for generated entities | Use `EntityLogicalName` and `Fields.*` constants; see [Generated Entities](#-generated-entities) |
| Use late-bound `Entity` for generated entity types | Use early-bound classes (`PluginAssembly`, `SystemUser`, etc.); compile-time safety |

---

Expand All @@ -32,13 +34,15 @@
| XML documentation for public APIs | IntelliSense support for consumers |
| Multi-target appropriately | PPDS.Plugins: 4.6.2 only; libraries: 8.0, 9.0, 10.0 |
| Run `dotnet test` before PR | Ensures no regressions |
| Update `CHANGELOG.md` with changes | Release notes for consumers |
| Update `CHANGELOG.md` for user-facing changes only | Skip internal refactoring, tooling, generated code |
| 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 |
| Scale throughput by adding Application Users | Each user has independent API quota; DOP × connections = total parallelism |
| Use early-bound classes for generated entities | Type safety, IntelliSense, refactoring support |
| Ask user before using late-bound for ambiguous cases | If unsure whether dynamic entity handling is needed, ask first |

---

Expand Down Expand Up @@ -66,6 +70,7 @@ ppds-sdk/
│ ├── PPDS.Dataverse/
│ │ ├── BulkOperations/ # CreateMultiple, UpdateMultiple, UpsertMultiple
│ │ ├── Client/ # DataverseClient, IDataverseClient
│ │ ├── Generated/ # Early-bound entity classes (DO NOT manually edit)
│ │ ├── Pooling/ # Connection pool, strategies
│ │ ├── Resilience/ # Throttle tracking, retry logic
│ │ └── PPDS.Dataverse.csproj
Expand Down Expand Up @@ -94,6 +99,79 @@ ppds-sdk/

---

## 🏗️ Generated Entities

Early-bound entity classes in `src/PPDS.Dataverse/Generated/` provide compile-time type safety.

### Available Entities

| Entity Class | Logical Name | Used For |
|--------------|--------------|----------|
| `PluginAssembly` | `pluginassembly` | Plugin registration |
| `PluginPackage` | `pluginpackage` | NuGet plugin packages |
| `PluginType` | `plugintype` | Plugin type registration |
| `SdkMessage` | `sdkmessage` | Message lookups |
| `SdkMessageFilter` | `sdkmessagefilter` | Entity/message filtering |
| `SdkMessageProcessingStep` | `sdkmessageprocessingstep` | Step registration |
| `SdkMessageProcessingStepImage` | `sdkmessageprocessingstepimage` | Pre/post images |
| `Solution` | `solution` | Solution operations |
| `SolutionComponent` | `solutioncomponent` | Solution components |
| `AsyncOperation` | `asyncoperation` | System jobs / async operations |
| `ImportJob` | `importjob` | Solution import jobs |
| `SystemUser` | `systemuser` | User mapping |
| `Publisher` | `publisher` | Solution publishers |

### Usage Patterns

```csharp
// ✅ Correct - Early-bound with constants
var query = new QueryExpression(PluginAssembly.EntityLogicalName)
{
ColumnSet = new ColumnSet(
PluginAssembly.Fields.Name,
PluginAssembly.Fields.Version)
};

var assembly = new PluginAssembly
{
Name = "MyPlugin",
IsolationMode = pluginassembly_isolationmode.Sandbox
};

// ❌ Wrong - Magic strings
var query = new QueryExpression("pluginassembly")
{
ColumnSet = new ColumnSet("name", "version")
};
```

### When Late-Bound Is Acceptable

Late-bound `new Entity(logicalName)` is correct **only** when:
- Entity type is determined at runtime (migration import/export)
- Building generic tooling for arbitrary entities
- Entity doesn't have a generated class

```csharp
// ✅ Correct - Dynamic entity from schema (migration scenario)
var entity = new Entity(record.LogicalName);

// ❌ Wrong - Known entity type, should use early-bound
var entity = new Entity("pluginassembly");
```

### Regenerating Entities

If Dataverse schema changes or new entities are needed:

```powershell
.\scripts\Generate-EarlyBoundModels.ps1 -Force
```

Requires `pac auth` to be configured. See script for entity list.

---

## 🛠️ Common Commands

```powershell
Expand Down
167 changes: 167 additions & 0 deletions scripts/Generate-EarlyBoundModels.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
<#
.SYNOPSIS
Generates early-bound entity classes from Dataverse metadata using pac modelbuilder.

.DESCRIPTION
Uses the Power Platform CLI (pac) to generate strongly-typed entity classes
for system/development entities used by PPDS. Generated classes provide
compile-time type safety and IntelliSense instead of magic strings.

Prerequisites:
1. Install Power Platform CLI: https://learn.microsoft.com/en-us/power-platform/developer/cli/introduction
2. Authenticate: pac auth create --deviceCode

.EXAMPLE
.\scripts\Generate-EarlyBoundModels.ps1

.EXAMPLE
# Regenerate after schema changes
.\scripts\Generate-EarlyBoundModels.ps1 -Force

.NOTES
- Generated files are checked into source control
- No build-time Dataverse connection needed after generation
- Re-run this script only when adding entities or after Dataverse schema changes
#>

[CmdletBinding()]
param(
[Parameter()]
[switch]$Force,

[Parameter()]
[string]$OutputDirectory = (Join-Path $PSScriptRoot '../src/PPDS.Dataverse/Generated')
)

$ErrorActionPreference = 'Stop'

# Entities used by PPDS CLI and Migration
$Entities = @(
# Plugin registration
'pluginassembly'
'pluginpackage'
'plugintype'
'sdkmessage'
'sdkmessagefilter'
'sdkmessageprocessingstep'
'sdkmessageprocessingstepimage'
# Solution/ALM
'solution'
'solutioncomponent'
'asyncoperation'
'importjob'
# User management
'systemuser'
'publisher'
)

Write-Host "PPDS Early-Bound Model Generator" -ForegroundColor Cyan
Write-Host "=================================" -ForegroundColor Cyan
Write-Host ""

# Check for pac CLI
$pacPath = Get-Command pac -ErrorAction SilentlyContinue
if (-not $pacPath) {
Write-Error @"
Power Platform CLI (pac) not found in PATH.

Install pac CLI:
1. Via .NET tool: dotnet tool install --global Microsoft.PowerApps.CLI.Tool
2. Via standalone: https://learn.microsoft.com/en-us/power-platform/developer/cli/introduction

After installation, restart your terminal and try again.
"@
exit 1
}

Write-Host "Found pac CLI: $($pacPath.Source)" -ForegroundColor Green

# Check for pac auth
Write-Host "Checking authentication..." -ForegroundColor Gray
$authOutput = pac auth list 2>&1
if ($LASTEXITCODE -ne 0 -or $authOutput -match 'No profiles') {
Write-Error @"
No pac authentication profile found.

Create an auth profile:
pac auth create --deviceCode

This opens a browser for interactive login. The profile is stored locally
and persists across sessions.
"@
exit 1
}

# Show active profile
$activeProfile = $authOutput | Select-String '\*' | Select-Object -First 1
if ($activeProfile) {
Write-Host "Active profile: $($activeProfile.ToString().Trim())" -ForegroundColor Green
}

# Check output directory
$OutputDirectory = [System.IO.Path]::GetFullPath($OutputDirectory)
if (Test-Path $OutputDirectory) {
if (-not $Force) {
$existingFiles = Get-ChildItem $OutputDirectory -Filter "*.cs" -ErrorAction SilentlyContinue
if ($existingFiles.Count -gt 0) {
Write-Host ""
Write-Host "Output directory already contains $($existingFiles.Count) file(s):" -ForegroundColor Yellow
Write-Host " $OutputDirectory" -ForegroundColor Yellow
Write-Host ""
Write-Host "Use -Force to regenerate, or delete the directory manually." -ForegroundColor Yellow
exit 0
}
}
else {
Write-Host "Cleaning existing output directory..." -ForegroundColor Gray
Remove-Item $OutputDirectory -Recurse -Force
}
}

# Create output directory
New-Item -ItemType Directory -Path $OutputDirectory -Force | Out-Null
Write-Host "Output directory: $OutputDirectory" -ForegroundColor Gray

# Build entity filter
$entityFilter = $Entities -join ';'
Write-Host ""
Write-Host "Generating classes for $($Entities.Count) entities:" -ForegroundColor Cyan
$Entities | ForEach-Object { Write-Host " - $_" -ForegroundColor Gray }
Write-Host ""

# Run pac modelbuilder
# Note: Most options are switches (present = enabled, absent = disabled)
# Only include switches we want enabled
$pacArgs = @(
'modelbuilder', 'build'
'--outdirectory', $OutputDirectory
'--namespace', 'PPDS.Dataverse.Generated'
'--entitynamesfilter', $entityFilter
'--emitfieldsclasses' # Generate Field constants class
'--language', 'CSharp'
)

Write-Host "Running: pac $($pacArgs -join ' ')" -ForegroundColor Gray
Write-Host ""

& pac @pacArgs

if ($LASTEXITCODE -ne 0) {
Write-Error "pac modelbuilder failed with exit code $LASTEXITCODE"
exit $LASTEXITCODE
}

# Count generated files
$generatedFiles = Get-ChildItem $OutputDirectory -Filter "*.cs" -Recurse
Write-Host ""
Write-Host "Successfully generated $($generatedFiles.Count) file(s):" -ForegroundColor Green
$generatedFiles | ForEach-Object {
Write-Host " - $($_.Name)" -ForegroundColor Gray
}

Write-Host ""
Write-Host "Next steps:" -ForegroundColor Cyan
Write-Host " 1. Review generated files in: $OutputDirectory" -ForegroundColor Gray
Write-Host " 2. Build solution: dotnet build" -ForegroundColor Gray
Write-Host " 3. Update code to use early-bound classes" -ForegroundColor Gray
Write-Host ""
4 changes: 4 additions & 0 deletions src/PPDS.Cli/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Changed

- **PluginRegistrationService refactored to use early-bound entities** - Replaced all magic string attribute access with strongly-typed `PPDS.Dataverse.Generated` classes (`PluginAssembly`, `PluginPackage`, `PluginType`, `SdkMessageProcessingStep`, `SdkMessageProcessingStepImage`, `SdkMessage`, `SdkMessageFilter`, `SystemUser`). Provides compile-time type safety and IntelliSense for all Dataverse entity operations. ([#56](https://github.com/joshsmithxrm/ppds-sdk/issues/56))

### Fixed

- **Environment resolution for service principals** - `ppds env select` now works with full URLs for service principals by trying direct Dataverse connection first, before falling back to Global Discovery (which requires user auth). ([#89](https://github.com/joshsmithxrm/ppds-sdk/issues/89))
Expand Down
Loading
Loading