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
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -398,6 +398,7 @@ While Probot-Sharp maintains API familiarity with the original Node.js Probot fr
| **Resilience** | Manual retry logic | Built-in circuit breakers, retry policies with exponential backoff, and timeout handling |
| **Testing** | Jest/Mocha | xUnit with strong mocking (NSubstitute), property-based testing support |
| **Dependency Injection** | Manual wiring | First-class DI container with lifetime scoping |
| **Webhook Deduplication** | Manual (app responsibility) | Automatic `UseIdempotency()` middleware with dual-layer strategy (database + distributed lock) |

**Key Architectural Benefits:**

Expand Down
2 changes: 2 additions & 0 deletions docs/AdapterConfiguration.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,8 @@ Following **Hexagonal Architecture** and **Cloud Design Patterns**, adapter conf

### Idempotency Adapters

> **Probot Comparison:** Unlike Probot (Node.js), which requires manual deduplication tracking, ProbotSharp provides automatic webhook deduplication via the idempotency adapter. This can be disabled by removing `app.UseIdempotency()` from your Program.cs if you need Probot-compatible behavior.

| Provider | Use Case | Dependencies | Data Loss on Restart |
|----------|----------|--------------|---------------------|
| **InMemory** | Development, testing, single instance | None | Yes |
Expand Down
4 changes: 4 additions & 0 deletions docs/Architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -1294,6 +1294,8 @@ Shared -> Domain -> Application -> Adapters (Inbound) -> Bootstraps

## Idempotency Strategy

**Architectural Difference from Probot (Node.js):** Probot does NOT automatically deduplicate webhooks - the application is responsible for tracking processed delivery IDs. ProbotSharp takes a more opinionated approach with built-in deduplication.

Probot-Sharp implements a dual-layer idempotency strategy to prevent duplicate webhook processing:

### Layer 1: Database-Level (IWebhookStoragePort)
Expand Down Expand Up @@ -1378,6 +1380,8 @@ DLQ items should trigger alerts for operational teams to investigate and resolve

**Context**: GitHub may deliver the same webhook multiple times. The application must prevent duplicate processing while supporting horizontal scaling across multiple instances.

**Note:** This differs from Probot (Node.js), which does not provide automatic deduplication and requires applications to implement their own tracking of processed delivery IDs (typically using Redis or a database).

**Decision**: Implement a dual-layer idempotency strategy:
1. Database-level: DeliveryId as primary key in IWebhookStoragePort
2. Distributed lock: IIdempotencyPort using Redis or in-memory cache
Expand Down
22 changes: 22 additions & 0 deletions docs/ConfigurationBestPractices.md
Original file line number Diff line number Diff line change
Expand Up @@ -235,6 +235,28 @@ builder.Configuration.AddSecretsManager(
});
```

## Middleware Configuration

### Idempotency Middleware

**Enabled by default in all production examples.** This prevents duplicate webhook processing.

```text
// Production (recommended):
app.UseIdempotency();

// Development/testing only (Probot-compatible):
// Comment out for Probot Node.js behavioral compatibility
```

**Trade-offs:**
- ✅ **Enabled:** Prevents accidental duplicate actions in production
- ❌ **Disabled:** Matches Probot (Node.js) behavior for integration testing

**See Also:**
- [Architecture docs - Idempotency Strategy](Architecture.md#idempotency-strategy)
- [Probot-to-ProbotSharp Guide - Section 7](Probot-to-ProbotSharp-Guide.md#7-webhook-deduplication-architectural-difference)

## Complete Example

Here's a production-ready `appsettings.json` following all best practices:
Expand Down
8 changes: 8 additions & 0 deletions docs/Operations.md
Original file line number Diff line number Diff line change
Expand Up @@ -925,6 +925,14 @@ curl http://localhost:8080/health
- Average processing time
- Retry attempts distribution

**Idempotency Metrics:**
- Idempotency hits (duplicate deliveries blocked)
- Idempotency misses (unique deliveries processed)
- Idempotency errors (lock acquisition failures)
- Duplicate delivery rate (hits / total deliveries)

> **Note:** High duplicate rates may indicate GitHub infrastructure issues, webhook delivery retries (normal for failures), or load balancer misconfiguration. Duplicates are expected when GitHub retries failed webhook deliveries (status codes 500, 503), during network timeouts, or after application restarts during request handling.

### Alert Thresholds

| Metric | Warning | Critical |
Expand Down
117 changes: 117 additions & 0 deletions docs/Probot-to-ProbotSharp-Guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -293,6 +293,123 @@ services.AddTransient<IGitHubOAuthPort, GitHubOAuthClient>();

---

### 7) Webhook Deduplication (Architectural Difference)

**This is a critical behavioral difference between Probot and ProbotSharp.**

#### Probot (Node.js) Behavior

```javascript
// Probot does NOT deduplicate webhooks automatically
export default (app) => {
app.on("push", async (context) => {
// This will be called for EVERY webhook delivery
// Even if GitHub sends the same delivery ID multiple times
app.log.info("Processing push", context.id);
});
};
```

- Does NOT automatically deduplicate webhooks by delivery ID
- All webhook deliveries are processed, including duplicates
- Application is responsible for implementing deduplication if needed
- Common pattern: Store processed delivery IDs in Redis/database

#### ProbotSharp (.NET) Behavior

```text
// ProbotSharp deduplicates by default (production safety)
var app = builder.Build();
app.UseProbotSharpMiddleware();
app.UseIdempotency(); // Prevents duplicate processing
```

- Automatic deduplication via `UseIdempotency()` middleware (enabled by default in all examples)
- Prevents duplicate processing for horizontal scaling and reliability
- Uses dual-layer strategy: database-level + distributed lock (Redis/in-memory)
- Configured via `Adapters.Idempotency` in appsettings.json

#### How to Disable (Probot-Compatible Behavior)

```text
var app = builder.Build();
app.UseProbotSharpMiddleware();
// Comment out or remove this line for Probot-compatible behavior:
// app.UseIdempotency();
```

#### When to Disable Idempotency

**Disable for:**
- ✅ Integration testing against Probot behavior
- ✅ Apps that implement custom deduplication logic
- ✅ Single-instance deployments where duplicates are acceptable
- ✅ Testing scenarios that verify duplicate handling

**Enable for (RECOMMENDED for production):**
- ✅ Production deployments
- ✅ Horizontal scaling (multiple instances)
- ✅ High-reliability requirements
- ✅ Preventing accidental duplicate actions

#### Configuration

**In-Memory (Development/Testing):**
```json
{
"ProbotSharp": {
"Adapters": {
"Idempotency": {
"Provider": "InMemory",
"Options": {
"ExpirationHours": "24"
}
}
}
}
}
```

**Redis (Production):**
```json
{
"ProbotSharp": {
"Adapters": {
"Idempotency": {
"Provider": "Redis",
"Options": {
"ConnectionString": "localhost:6379",
"ExpirationHours": "24"
}
}
}
}
}
```

**Database (Enterprise):**
```json
{
"ProbotSharp": {
"Adapters": {
"Idempotency": {
"Provider": "Database",
"Options": {
"ExpirationHours": "24"
}
}
}
}
}
```

**See Also:**
- [Architecture docs - Idempotency Strategy](Architecture.md#idempotency-strategy)
- [Architecture docs - ADR-002: Dual-Layer Idempotency](Architecture.md#adr-002-dual-layer-idempotency-strategy)
- [Adapter Configuration Guide](AdapterConfiguration.md)

---

Refer to `docs/Architecture.md`, `docs/Extensions.md`, and `docs/LocalDevelopment.md` for deeper dives and runnable examples.


Loading
Loading