Skip to content

Commit a100be0

Browse files
lilithclaude
andcommitted
Add simpler IBlobSource interface design
If routing handles all path parsing, provider becomes minimal: ```csharp public interface IBlobSource { string Name { get; } ValueTask<BlobResult> FetchAsync( IReadOnlyDictionary<string, string> params, CancellationToken ct); } ``` Provider just receives final params: - S3: { bucket, key } - Azure: { container, blob } - Filesystem: { path } Provider does NOT need to know: - Original request path - Route that matched - Template evaluation - URI construction Routing layer handles all the complexity. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 5ad7858 commit a100be0

1 file changed

Lines changed: 150 additions & 10 deletions

File tree

CLAUDE.md

Lines changed: 150 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -553,20 +553,160 @@ public class ProviderConfig
553553
3. Migrate providers one by one
554554
4. Remove old interfaces
555555

556-
### Open Questions
556+
### Even Simpler: Routing Does All Path Work
557557

558-
1. **Caching integration** - Current `ICacheableBlobPromise` has cache key computation. Where does this go?
559-
- Option A: `BlobFetchResult` includes cache metadata
560-
- Option B: Separate `ICacheKeyProvider` interface
561-
- Option C: Provider returns `IBlobWrapper` which has cache info
558+
If routing handles path parsing completely, provider just gets final params:
562559

563-
2. **Pre-signed URLs** - Current `ISupportsPreSignedUrls`. Keep as optional interface?
560+
```csharp
561+
// Minimal provider interface
562+
public interface IBlobSource
563+
{
564+
string Name { get; }
565+
566+
ValueTask<BlobResult> FetchAsync(
567+
IReadOnlyDictionary<string, string> params,
568+
CancellationToken ct = default);
569+
}
570+
571+
// Result is simple
572+
public readonly struct BlobResult
573+
{
574+
public bool Found { get; init; }
575+
public IBlobWrapper? Blob { get; init; }
576+
public int? ErrorCode { get; init; }
577+
public string? ErrorMessage { get; init; }
578+
579+
public static BlobResult Ok(IBlobWrapper blob) => new() { Found = true, Blob = blob };
580+
public static BlobResult NotFound() => new() { Found = false };
581+
public static BlobResult Error(int code, string msg) => new() { ErrorCode = code, ErrorMessage = msg };
582+
}
583+
```
584+
585+
**What each provider receives:**
586+
587+
| Provider | Params |
588+
|----------|--------|
589+
| S3 | `bucket`, `key` |
590+
| Azure | `container`, `blob` |
591+
| Filesystem | `path` |
592+
| HTTP | `url` or `path` |
593+
594+
**What provider does NOT need to know:**
595+
- Original request path ❌
596+
- Route that matched ❌
597+
- Query string parsing ❌
598+
- URI construction ❌
599+
- Template evaluation ❌
600+
601+
**Example S3 implementation:**
602+
```csharp
603+
public class S3BlobSource : IBlobSource
604+
{
605+
private readonly IAmazonS3 _client;
606+
private readonly S3Options _options;
607+
608+
public string Name { get; }
609+
610+
public async ValueTask<BlobResult> FetchAsync(
611+
IReadOnlyDictionary<string, string> params,
612+
CancellationToken ct)
613+
{
614+
var bucket = params["bucket"];
615+
var key = params["key"];
616+
617+
try
618+
{
619+
var response = await _client.GetObjectAsync(bucket, key, ct);
620+
return BlobResult.Ok(new S3BlobWrapper(response));
621+
}
622+
catch (AmazonS3Exception ex) when (ex.StatusCode == HttpStatusCode.NotFound)
623+
{
624+
return BlobResult.NotFound();
625+
}
626+
}
627+
}
628+
```
629+
630+
**Routing layer does the heavy lifting:**
631+
```csharp
632+
public class RoutingEngine
633+
{
634+
public async ValueTask<BlobResult?> RouteAsync(IRequestSnapshot request, CancellationToken ct)
635+
{
636+
foreach (var route in _routes)
637+
{
638+
// 1. Match request path
639+
if (!route.Matcher.TryMatch(request.Path, out var captures))
640+
continue;
641+
642+
// 2. Check header conditions
643+
if (!route.HeaderConditions.All(c => c.Matches(request)))
644+
continue;
645+
646+
// 3. Evaluate template to get provider path
647+
var templateOutput = route.Template.Evaluate(captures);
648+
649+
// 4. Run provider's path parsers to extract params
650+
var params = route.Provider.PathParsers
651+
.Select(p => p.TryParse(templateOutput))
652+
.FirstOrDefault(r => r != null);
653+
654+
if (params == null)
655+
continue; // No parser matched
656+
657+
// 5. Merge with any static flags [bucket=default]
658+
params = MergeWithFlags(params, route.Flags);
659+
660+
// 6. Call provider with just the params
661+
var provider = _providers[route.ProviderName];
662+
return await provider.FetchAsync(params, ct);
663+
}
664+
return null; // No route matched
665+
}
666+
}
667+
```
668+
669+
### Remaining Questions
670+
671+
1. **Caching** - Cache key needs to include provider name + params. Routing layer can compute this.
672+
673+
2. **Pre-signed URLs** - Optional interface on provider:
674+
```csharp
675+
public interface ISupportsPreSignedUrls : IBlobSource
676+
{
677+
string? GeneratePreSignedUrl(IReadOnlyDictionary<string, string> params, TimeSpan expiry);
678+
}
679+
```
680+
681+
3. **Latency zones** - Provider declares its zone, routing layer tracks:
682+
```csharp
683+
public interface IBlobSource
684+
{
685+
string Name { get; }
686+
LatencyTrackingZone LatencyZone { get; } // e.g., "s3:us-east-1:my-bucket"
687+
ValueTask<BlobResult> FetchAsync(...);
688+
}
689+
```
690+
691+
4. **Request context** - If provider needs headers (conditional GET):
692+
```csharp
693+
// Option A: Pass minimal context
694+
ValueTask<BlobResult> FetchAsync(
695+
IReadOnlyDictionary<string, string> params,
696+
BlobFetchHints? hints, // If-None-Match, Accept-Encoding, etc.
697+
CancellationToken ct);
698+
699+
// Option B: Let routing layer handle conditional logic
700+
// Provider always fetches, routing layer returns 304 if appropriate
701+
```
564702

565-
3. **Latency zones** - Current `LatencyTrackingZone`. Include in result or separate?
703+
### Interface Evolution Summary
566704

567-
4. **Hot-reload** - Provider instances are long-lived. How to update config?
568-
- Option A: Recreate provider on config change
569-
- Option B: Provider observes `IOptionsMonitor` internally
705+
| Version | Interface | Params | Responsibility |
706+
|---------|-----------|--------|----------------|
707+
| Legacy | `IBlobProvider` | `virtualPath` | Provider parses path |
708+
| Current | `IRoutedBlobProvider` | 6 params | Provider + routing mixed |
709+
| Proposed | `IBlobSource` | `params` dict | Routing parses, provider fetches |
570710

571711
## Existing API Alignment
572712

0 commit comments

Comments
 (0)