Skip to content

Commit ae18415

Browse files
committed
wip: RemoteDocumentDbContext
1 parent 9be7fd3 commit ae18415

7 files changed

Lines changed: 362 additions & 3 deletions

File tree

BLite.Server.slnx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
</Folder>
88
<Folder Name="/src/">
99
<Project Path="src/BLite.Client/BLite.Client.csproj" />
10+
<Project Path="src/BLite.Client.SourceGenerators/BLite.Client.SourceGenerators.csproj" />
1011
<Project Path="src/BLite.Proto/BLite.Proto.csproj" />
1112
<Project Path="src/BLite.Server/BLite.Server.csproj" />
1213
</Folder>
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
3+
<PropertyGroup>
4+
<TargetFramework>netstandard2.0</TargetFramework>
5+
<LangVersion>latest</LangVersion>
6+
<Nullable>enable</Nullable>
7+
<EnforceExtendedAnalyzerRules>true</EnforceExtendedAnalyzerRules>
8+
<IsRoslynComponent>true</IsRoslynComponent>
9+
<RootNamespace>BLite.Client.SourceGenerators</RootNamespace>
10+
<AssemblyName>BLite.Client.SourceGenerators</AssemblyName>
11+
12+
<Version>1.0.0</Version>
13+
<Authors>BLite Team</Authors>
14+
<Company>EntglDb</Company>
15+
<Copyright>Copyright © 2026 Luca Fabbri</Copyright>
16+
<Description>Source Generators for BLite.Client — generates remote DocumentDbContext constructors.</Description>
17+
<RepositoryUrl>https://github.com/EntglDb/BLite.Server</RepositoryUrl>
18+
<RepositoryType>git</RepositoryType>
19+
<PackageLicenseExpression>AGPL-3.0-only</PackageLicenseExpression>
20+
21+
<!-- Generator assemblies must not be included in the consuming project's output -->
22+
<IncludeBuildOutput>false</IncludeBuildOutput>
23+
<DevelopmentDependency>true</DevelopmentDependency>
24+
<NoPackageAnalysis>true</NoPackageAnalysis>
25+
</PropertyGroup>
26+
27+
<ItemGroup>
28+
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="5.0.0" PrivateAssets="all" />
29+
<PackageReference Include="Microsoft.CodeAnalysis.Analyzers" Version="3.11.0" PrivateAssets="all" />
30+
</ItemGroup>
31+
32+
</Project>
Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
// BLite.Client.SourceGenerators — RemoteContextGenerator
2+
// Copyright (C) 2026 Luca Fabbri — AGPL-3.0
3+
//
4+
// For each partial class inheriting from DocumentDbContext, generates:
5+
// - A _remoteClient backing field
6+
// - A public constructor accepting BLiteClient
7+
// - A CreateCollection<TId,T> override that delegates to BLiteClient.GetDocumentCollection
8+
9+
using System.Collections.Generic;
10+
using System.Linq;
11+
using System.Text;
12+
using Microsoft.CodeAnalysis;
13+
using Microsoft.CodeAnalysis.CSharp;
14+
using Microsoft.CodeAnalysis.CSharp.Syntax;
15+
16+
namespace BLite.Client.SourceGenerators;
17+
18+
[Generator]
19+
public sealed class RemoteContextGenerator : IIncrementalGenerator
20+
{
21+
public void Initialize(IncrementalGeneratorInitializationContext context)
22+
{
23+
var pipeline = context.SyntaxProvider
24+
.CreateSyntaxProvider(
25+
predicate: static (node, _) => IsPotentialDbContext(node),
26+
transform: static (ctx, _) => GetContextInfo(ctx))
27+
.Where(static info => info is not null)
28+
.Collect()
29+
// De-duplicate by full class name (multiple partial declarations map to the same type)
30+
.SelectMany(static (items, _) =>
31+
items.GroupBy(static i => i!.FullName).Select(static g => g.First()));
32+
33+
context.RegisterSourceOutput(pipeline, static (spc, info) =>
34+
{
35+
if (info is null) return;
36+
spc.AddSource($"{info.FullName}.RemoteContext.g.cs", Emit(info!));
37+
});
38+
}
39+
40+
// ── Syntax predicate ─────────────────────────────────────────────────────
41+
42+
private static bool IsPotentialDbContext(SyntaxNode node)
43+
{
44+
// Skip already-generated files
45+
if (node.SyntaxTree.FilePath.EndsWith(".g.cs")) return false;
46+
47+
return node is ClassDeclarationSyntax cls
48+
&& cls.BaseList is { Types.Count: > 0 }
49+
&& cls.Modifiers.Any(m => m.IsKind(SyntaxKind.PartialKeyword));
50+
}
51+
52+
// ── Semantic transform ───────────────────────────────────────────────────
53+
54+
private static ContextInfo? GetContextInfo(GeneratorSyntaxContext ctx)
55+
{
56+
var cls = (ClassDeclarationSyntax)ctx.Node;
57+
if (ctx.SemanticModel.GetDeclaredSymbol(cls) is not INamedTypeSymbol symbol)
58+
return null;
59+
60+
if (!InheritsFromDocumentDbContext(symbol))
61+
return null;
62+
63+
return new ContextInfo
64+
{
65+
Name = symbol.Name,
66+
Namespace = symbol.ContainingNamespace?.ToDisplayString() ?? "",
67+
FullName = symbol.ToDisplayString()
68+
};
69+
}
70+
71+
private static bool InheritsFromDocumentDbContext(INamedTypeSymbol symbol)
72+
{
73+
var current = symbol.BaseType;
74+
while (current is not null)
75+
{
76+
if (current.Name == "DocumentDbContext")
77+
return true;
78+
current = current.BaseType;
79+
}
80+
return false;
81+
}
82+
83+
// ── Code emission ─────────────────────────────────────────────────────────
84+
85+
private static string Emit(ContextInfo info)
86+
{
87+
var sb = new StringBuilder();
88+
sb.AppendLine("// <auto-generated/>");
89+
sb.AppendLine("#nullable enable");
90+
sb.AppendLine();
91+
sb.AppendLine($"namespace {info.Namespace}");
92+
sb.AppendLine("{");
93+
sb.AppendLine($" public partial class {info.Name}");
94+
sb.AppendLine(" {");
95+
96+
// Backing field — intentionally prefixed to minimise collision risk
97+
sb.AppendLine(" private global::BLite.Client.BLiteClient? _remoteClient;");
98+
sb.AppendLine();
99+
100+
// Constructor
101+
sb.AppendLine($" public {info.Name}(global::BLite.Client.BLiteClient client) : base()");
102+
sb.AppendLine(" {");
103+
sb.AppendLine(" _remoteClient = client;");
104+
// base() already called InitializeCollections() with _remoteClient == null (no-op path).
105+
// Call it again now that the client is available so all collection properties are set.
106+
sb.AppendLine(" InitializeCollections();");
107+
sb.AppendLine(" }");
108+
sb.AppendLine();
109+
110+
// CreateCollection override
111+
sb.AppendLine(" protected override global::BLite.Core.Collections.IDocumentCollection<TId, T>");
112+
sb.AppendLine(" CreateCollection<TId, T>(global::BLite.Core.Collections.IDocumentMapper<TId, T> mapper)");
113+
sb.AppendLine(" where T : class");
114+
sb.AppendLine(" {");
115+
// Guard: called too early from the base ctor before _remoteClient is set
116+
sb.AppendLine(" if (_remoteClient is null) return null!;");
117+
sb.AppendLine(" return _remoteClient.GetDocumentCollection<TId, T>(mapper);");
118+
sb.AppendLine(" }");
119+
120+
sb.AppendLine(" }");
121+
sb.AppendLine("}");
122+
123+
return sb.ToString();
124+
}
125+
126+
// ── Model ─────────────────────────────────────────────────────────────────
127+
128+
private sealed class ContextInfo
129+
{
130+
public string Name { get; set; } = "";
131+
public string Namespace { get; set; } = "";
132+
public string FullName { get; set; } = "";
133+
}
134+
}

src/BLite.Client/BLite.Client.csproj

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,11 +28,15 @@
2828
<ItemGroup> <!-- BLite engine — direct project reference (local workspace) -->
2929
<!-- gRPC proto definitions + QueryDescriptor -->
3030
<ProjectReference Include="..\..\..\BLite.Server\src\BLite.Proto\BLite.Proto.csproj" />
31+
<!-- Remote context source generator -->
32+
<ProjectReference Include="..\BLite.Client.SourceGenerators\BLite.Client.SourceGenerators.csproj"
33+
OutputItemType="Analyzer"
34+
ReferenceOutputAssembly="false" />
3135
</ItemGroup>
3236

3337
<ItemGroup>
3438
<!-- gRPC client channel -->
35-
<PackageReference Include="BLite" Version="3.4.2" />
39+
<PackageReference Include="BLite" Version="3.5.1" />
3640
<PackageReference Include="Grpc.Net.Client" Version="2.76.0" />
3741
<!-- QueryDescriptor wire serialization (same options as server) -->
3842
<PackageReference Include="MessagePack" Version="3.1.*" />

src/BLite.Client/BLiteClient.cs

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,22 @@ public RemoteCollection<TId, T> GetCollection<TId, T>(
131131
mapper, _dynStub, _docStub, _keyMap, _headers);
132132
}
133133

134+
/// <summary>
135+
/// Returns a typed remote collection that implements
136+
/// <see cref="IDocumentCollection{TId,T}"/>, compatible with
137+
/// <c>DocumentDbContext</c>-based contexts generated by
138+
/// <c>BLite.Client.SourceGenerators</c>.
139+
/// </summary>
140+
public IDocumentCollection<TId, T> GetDocumentCollection<TId, T>(
141+
IDocumentMapper<TId, T> mapper)
142+
where T : class
143+
{
144+
ThrowIfDisposed();
145+
ArgumentNullException.ThrowIfNull(mapper);
146+
return new RemoteDocumentCollection<TId, T>(
147+
new RemoteCollection<TId, T>(mapper, _dynStub, _docStub, _keyMap, _headers));
148+
}
149+
134150
// ── Collection management ─────────────────────────────────────────────────
135151

136152
/// <summary>
Lines changed: 172 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,172 @@
1+
// BLite.Client — RemoteDocumentCollection<TId, T>
2+
// Copyright (C) 2026 Luca Fabbri — AGPL-3.0
3+
//
4+
// IDocumentCollection<TId,T> wrapper around RemoteCollection<TId,T>.
5+
// Sync operations and local-engine-only features throw NotSupportedException.
6+
7+
using BLite.Bson;
8+
using BLite.Core.Collections;
9+
using BLite.Core.Indexing;
10+
using BLite.Core.Query;
11+
using System.Linq.Expressions;
12+
13+
namespace BLite.Client.Collections;
14+
15+
/// <summary>
16+
/// Remote implementation of <see cref="IDocumentCollection{TId,T}"/> that
17+
/// delegates typed CRUD operations to a <see cref="RemoteCollection{TId,T}"/>
18+
/// over gRPC.
19+
///
20+
/// <para>
21+
/// Sync operations and local-engine-only features (indexes, scans, ForcePrune)
22+
/// throw <see cref="NotSupportedException"/>. Use the <c>Async</c> overloads.
23+
/// </para>
24+
/// </summary>
25+
public sealed class RemoteDocumentCollection<TId, T> : IDocumentCollection<TId, T>
26+
where T : class
27+
{
28+
private readonly RemoteCollection<TId, T> _inner;
29+
30+
internal RemoteDocumentCollection(RemoteCollection<TId, T> inner) => _inner = inner;
31+
32+
// ── Metadata ──────────────────────────────────────────────────────────────
33+
34+
public SchemaVersion? CurrentSchemaVersion => null;
35+
36+
// ── Insert ────────────────────────────────────────────────────────────────
37+
38+
public TId Insert(T entity) =>
39+
throw new NotSupportedException("Use InsertAsync for remote collections.");
40+
41+
public Task<TId> InsertAsync(T entity, CancellationToken ct = default) =>
42+
_inner.InsertAsync(entity, null, ct);
43+
44+
public List<TId> InsertBulk(IEnumerable<T> entities) =>
45+
throw new NotSupportedException("Use InsertBulkAsync for remote collections.");
46+
47+
public async Task<List<TId>> InsertBulkAsync(IEnumerable<T> entities, CancellationToken ct = default) =>
48+
(await _inner.InsertBulkAsync(entities, null, ct)).ToList();
49+
50+
// ── Read ──────────────────────────────────────────────────────────────────
51+
52+
public T? FindById(TId id) =>
53+
throw new NotSupportedException("Use FindByIdAsync for remote collections.");
54+
55+
public async ValueTask<T?> FindByIdAsync(TId id, CancellationToken ct = default) =>
56+
await _inner.FindByIdAsync(id, ct);
57+
58+
public IAsyncEnumerable<T> FindAllAsync(CancellationToken ct = default) =>
59+
_inner.FindAllAsync(ct);
60+
61+
public IAsyncEnumerable<T> FindAsync(Func<T, bool> predicate, CancellationToken ct = default) =>
62+
_inner.FindAsync(predicate, ct);
63+
64+
public IBLiteQueryable<T> AsQueryable() => _inner.AsQueryable();
65+
66+
// ── Update ────────────────────────────────────────────────────────────────
67+
68+
public bool Update(T entity) =>
69+
throw new NotSupportedException("Use UpdateAsync for remote collections.");
70+
71+
public Task<bool> UpdateAsync(T entity, CancellationToken ct = default) =>
72+
_inner.UpdateAsync(entity, null, ct);
73+
74+
public int UpdateBulk(IEnumerable<T> entities) =>
75+
throw new NotSupportedException("Use UpdateBulkAsync for remote collections.");
76+
77+
public Task<int> UpdateBulkAsync(IEnumerable<T> entities, CancellationToken ct = default) =>
78+
_inner.UpdateBulkAsync(entities, null, ct);
79+
80+
// ── Delete ────────────────────────────────────────────────────────────────
81+
82+
public bool Delete(TId id) =>
83+
throw new NotSupportedException("Use DeleteAsync for remote collections.");
84+
85+
public Task<bool> DeleteAsync(TId id, CancellationToken ct = default) =>
86+
_inner.DeleteAsync(id, null, ct);
87+
88+
public int DeleteBulk(IEnumerable<TId> ids) =>
89+
throw new NotSupportedException("Use DeleteBulkAsync for remote collections.");
90+
91+
public Task<int> DeleteBulkAsync(IEnumerable<TId> ids, CancellationToken ct = default) =>
92+
_inner.DeleteBulkAsync(ids, null, ct);
93+
94+
// ── Index management (not supported on remote) ────────────────────────────
95+
96+
public CollectionSecondaryIndex<TId, T> CreateIndex<TKey>(
97+
Expression<Func<T, TKey>> keySelector, string? name = null, bool unique = false) =>
98+
throw new NotSupportedException("Index operations are not supported on remote collections.");
99+
100+
public Task<CollectionSecondaryIndex<TId, T>> CreateIndexAsync<TKey>(
101+
Expression<Func<T, TKey>> keySelector, string? name = null, bool unique = false,
102+
CancellationToken ct = default) =>
103+
throw new NotSupportedException("Index operations are not supported on remote collections.");
104+
105+
public CollectionSecondaryIndex<TId, T> CreateVectorIndex<TKey>(
106+
Expression<Func<T, TKey>> keySelector, int dimensions,
107+
VectorMetric metric = VectorMetric.Cosine, string? name = null) =>
108+
throw new NotSupportedException("Index operations are not supported on remote collections.");
109+
110+
public Task<CollectionSecondaryIndex<TId, T>> CreateVectorIndexAsync<TKey>(
111+
Expression<Func<T, TKey>> keySelector, int dimensions,
112+
VectorMetric metric = VectorMetric.Cosine, string? name = null,
113+
CancellationToken ct = default) =>
114+
throw new NotSupportedException("Index operations are not supported on remote collections.");
115+
116+
public CollectionSecondaryIndex<TId, T> EnsureIndex<TKey>(
117+
Expression<Func<T, TKey>> keySelector, string? name = null, bool unique = false) =>
118+
throw new NotSupportedException("Index operations are not supported on remote collections.");
119+
120+
public Task<CollectionSecondaryIndex<TId, T>> EnsureIndexAsync<TKey>(
121+
Expression<Func<T, TKey>> keySelector, string? name = null, bool unique = false,
122+
CancellationToken ct = default) =>
123+
throw new NotSupportedException("Index operations are not supported on remote collections.");
124+
125+
public bool DropIndex(string name) =>
126+
throw new NotSupportedException("Index operations are not supported on remote collections.");
127+
128+
public Task<bool> DropIndexAsync(string name, CancellationToken ct = default) =>
129+
throw new NotSupportedException("Index operations are not supported on remote collections.");
130+
131+
public IEnumerable<CollectionIndexInfo> GetIndexes() =>
132+
throw new NotSupportedException("Index operations are not supported on remote collections.");
133+
134+
public CollectionSecondaryIndex<TId, T>? GetIndex(string name) =>
135+
throw new NotSupportedException("Index operations are not supported on remote collections.");
136+
137+
public IEnumerable<T> QueryIndex(string indexName, object? minKey, object? maxKey, bool ascending = true) =>
138+
throw new NotSupportedException("Index operations are not supported on remote collections.");
139+
140+
public IAsyncEnumerable<T> QueryIndexAsync(
141+
string indexName, object? minKey, object? maxKey, bool ascending = true,
142+
CancellationToken ct = default) =>
143+
throw new NotSupportedException("Index operations are not supported on remote collections.");
144+
145+
// ── Scan (not supported on remote) ───────────────────────────────────────
146+
147+
public IEnumerable<T> Scan(BsonReaderPredicate predicate) =>
148+
throw new NotSupportedException("Scan is not supported on remote collections.");
149+
150+
public IAsyncEnumerable<T> ScanAsync(BsonReaderPredicate predicate, CancellationToken ct = default) =>
151+
throw new NotSupportedException("Scan is not supported on remote collections.");
152+
153+
public IEnumerable<TResult> Scan<TResult>(BsonReaderProjector<TResult> projector) =>
154+
throw new NotSupportedException("Scan is not supported on remote collections.");
155+
156+
public IAsyncEnumerable<TResult> ScanAsync<TResult>(
157+
BsonReaderProjector<TResult> projector, CancellationToken ct = default) =>
158+
throw new NotSupportedException("Scan is not supported on remote collections.");
159+
160+
public IEnumerable<T> ParallelScan(BsonReaderPredicate predicate, int degreeOfParallelism = -1) =>
161+
throw new NotSupportedException("Scan is not supported on remote collections.");
162+
163+
public IAsyncEnumerable<T> ParallelScanAsync(
164+
BsonReaderPredicate predicate, int degreeOfParallelism = -1,
165+
CancellationToken ct = default) =>
166+
throw new NotSupportedException("Scan is not supported on remote collections.");
167+
168+
// ── TimeSeries (not supported on remote) ─────────────────────────────────
169+
170+
public void ForcePrune() =>
171+
throw new NotSupportedException("ForcePrune is not supported on remote collections.");
172+
}

0 commit comments

Comments
 (0)