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
38 changes: 38 additions & 0 deletions .squad/agents/fenster/history.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,3 +54,41 @@

**Conclusion**:
This is not a bug in Vogen's code generation. It's **incorrect documentation** that suggests an invalid protobuf-net pattern. The README should be corrected to remove or fix the protobuf example.

---

### 2026-05-15: Enhanced gRPC Scenario with Real Server Testing

**Task**: Enhance `samples\Vogen.Examples\SerializationAndConversion\Grpc\GrpcScenario.cs` to test real protobuf-net serialization over HTTP/2 gRPC.

**Implementation**:
- Used in-process ASP.NET Core hosting approach (standard pattern for .NET gRPC integration tests)
- Created real HTTP/2 server with `WebApplication` and Kestrel
- Used `protobuf-net.Grpc.AspNetCore` for server-side code-first gRPC hosting
- Used `Grpc.Net.Client` for client channel creation
- Dynamic port allocation via socket binding to avoid port conflicts

**Key Patterns Discovered**:
1. **In-process ASP.NET Core testing pattern** is preferred over Docker/Testcontainers for custom gRPC services (unlike MongoDb scenario which uses pre-built images)
2. **Required NuGet packages for code-first gRPC**:
- `protobuf-net.Grpc.AspNetCore` (v1.2.2) - for `AddCodeFirstGrpc()` and `MapGrpcService<T>()`
- `Grpc.Net.Client` (v2.70.0) - for `GrpcChannel.ForAddress()`
- `ProtoBuf.Grpc.Client` namespace - for `channel.CreateGrpcService<T>()` extension
- `ProtoBuf.Grpc.Server` namespace - for `AddCodeFirstGrpc()` extension
3. **Framework reference needed**: Added `<FrameworkReference Include="Microsoft.AspNetCore.App" />` to csproj since it uses `Microsoft.NET.Sdk` (not Web SDK)
4. **Port allocation**: Use socket binding to `IPAddress.Loopback:0` to get OS-assigned available port
5. **HTTP/2 enforcement**: Must configure Kestrel with `HttpProtocols.Http2` explicitly for gRPC
6. **Proper cleanup**: Server must be stopped with `StopAsync()` and disposed in finally block
7. **Error handling**: Wrap in try/catch similar to Mongo scenario pattern

**Files Modified**:
- `samples\Vogen.Examples\Vogen.Examples.csproj` - Added packages and framework reference
- `samples\Vogen.Examples\SerializationAndConversion\Grpc\GrpcScenario.cs` - Complete rewrite with server hosting

**Build Verification**: ✓ `dotnet build samples\Vogen.Examples\Vogen.Examples.csproj -c Release` succeeds

**Architecture Notes**:
- This pattern tests the FULL serialization stack: Vogen value objects → VogenSurrogate → protobuf wire format → HTTP/2 → deserialize back
- The existing `VogenSurrogate<TW, TP>` pattern (using `IVogen<TW, TP>` static abstracts) works perfectly with code-first gRPC
- No changes needed to value object definitions or surrogate class

32 changes: 32 additions & 0 deletions .squad/decisions.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,38 @@ public partial struct MyValueObject;

---

## 2026-05-15: In-Process ASP.NET Core Hosting for gRPC Scenario Testing

**By:** Fenster (Backend Dev)

**What:** Enhanced GrpcScenario.cs with in-process ASP.NET Core gRPC hosting for real serialization testing.

**Context:** The original GrpcScenario.cs called the gRPC service directly in-process, which didn't test protobuf-net serialization over the wire. Steve requested enhancement to test real serialization.

**Decision:** Use in-process ASP.NET Core hosting with WebApplication and Kestrel configured for HTTP/2 instead of Docker/Testcontainers.

**Implementation:**
- Host gRPC service using WebApplication with Kestrel for HTTP/2
- Use protobuf-net.Grpc.AspNetCore for code-first gRPC server
- Use Grpc.Net.Client for client channel creation
- Dynamic port allocation via socket binding
- Added `<FrameworkReference Include="Microsoft.AspNetCore.App" />` to Vogen.Examples.csproj

**Rationale:**
1. **Simpler than Docker:** No custom Dockerfile needed; standard .NET pattern
2. **Full stack validation:** Tests Vogen value objects → protobuf serialization → HTTP/2 transport → deserialization
3. **Fast execution:** No container startup overhead
4. **No external dependencies:** Runs anywhere .NET 8 runs

**Alternatives Considered:**
- Docker/Testcontainers — would require Dockerfile; overkill for this scenario
- Keep in-process call — doesn't test serialization
- External gRPC server — requires manual setup; not portable

**Impact:** Tests verify that `VogenSurrogate<TW, TP>` pattern works correctly with protobuf-net over real gRPC. Example is more realistic and demonstrates end-to-end integration.

---

## End of decisions

New decisions will be merged here by Scribe from `.squad/decisions/inbox/`.
17 changes: 17 additions & 0 deletions docs/site/Writerside/topics/reference/FAQ.md
Original file line number Diff line number Diff line change
Expand Up @@ -424,6 +424,8 @@ Yes, but the mechanism is different depending on the version of protobuf-net you

### protobuf-net v2 (pre-v3)

> ⚠️ **This pattern is broken in protobuf-net v3 and later.** If you are on v3+, skip to the next section.

Add a dependency to protobuf-net and annotate the value object with `[ProtoContract(Surrogate = typeof(string))]`: (NOTE: replace `string` with the correct primitive)

```c#
Expand Down Expand Up @@ -469,6 +471,21 @@ public partial class BoxId;
public partial class Age;
```

Any DTO or message class that contains these value objects must also be annotated with `[ProtoContract]` and `[ProtoMember]`
on each property — without these, protobuf-net cannot deserialize the message and will return `null`:

```c#
[ProtoContract]
public class Person
{
[ProtoMember(1)] public Name? Name { get; set; }
[ProtoMember(2)] public Age? Age { get; set; }
}
```

Note that properties should use `{ get; set; }` (not `init`) and be nullable, as protobuf-net constructs the object
via a parameterless constructor and then sets each property — Vogen prohibits assigning `default` to value object fields.

A full working example including schema generation and a gRPC service can be found in
[`samples/Vogen.Examples/SerializationAndConversion/Grpc/GrpcScenario.cs`](../../../../samples/Vogen.Examples/SerializationAndConversion/Grpc/GrpcScenario.cs).

Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,17 @@
using System;
using System.Net;
using System.Net.Sockets;
using System.Threading;
using System.Threading.Tasks;
using Grpc.Net.Client;
using JetBrains.Annotations;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using ProtoBuf;
using ProtoBuf.Grpc.Client;
using ProtoBuf.Grpc.Server;
using System.ServiceModel;

namespace Vogen.Examples.SerializationAndConversion.Grpc;
Expand All @@ -17,19 +26,81 @@
string schema = generator.GetSchema<IService>();
Console.WriteLine("Schema is: " + schema);

IService service = new Service();
var person = await service.GetDataAsync();

Console.WriteLine($"Received Person: name={person.Name}, age={person.Age}, boxId={person.BoxId}, temperature={person.Temperature}");
try
{
// Find an available port
int port = GetAvailablePort();
string serverUrl = $"http://127.0.0.1:{port}";

Console.WriteLine($"Starting gRPC server on {serverUrl}...");

// Build and start the ASP.NET Core gRPC server
var builder = WebApplication.CreateBuilder();

// Configure services for gRPC
builder.Services.AddCodeFirstGrpc();
builder.WebHost.UseUrls(serverUrl);
builder.WebHost.UseKestrel(options =>
{
options.Listen(IPAddress.Loopback, port, listenOptions =>
{
listenOptions.Protocols = Microsoft.AspNetCore.Server.Kestrel.Core.HttpProtocols.Http2;
});
});

var app = builder.Build();

// Map the gRPC service
app.MapGrpcService<Service>();

// Start the server in the background
await app.StartAsync();

Console.WriteLine("gRPC server started successfully.");

try
{
// Create a gRPC channel and client
using var channel = GrpcChannel.ForAddress(serverUrl);
var client = channel.CreateGrpcService<IService>();

Console.WriteLine("Calling gRPC service over HTTP/2...");

// Call the service through the real gRPC channel
var person = await client.GetDataAsync();

Console.WriteLine($"✓ Received Person over gRPC: name={person.Name}, age={person.Age}, boxId={person.BoxId}, temperature={person.Temperature}");
Console.WriteLine("✓ protobuf-net serialization over gRPC verified successfully!");
}
finally
{
// Shutdown the server
await app.StopAsync();
await app.DisposeAsync();
}
}
catch (Exception ex)
{
Console.WriteLine($"Error running gRPC scenario: {ex.Message}");
Console.WriteLine("This scenario requires HTTP/2 support.");
}
}

private static int GetAvailablePort()
{
using var socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
socket.Bind(new IPEndPoint(IPAddress.Loopback, 0));
return ((IPEndPoint)socket.LocalEndPoint!).Port;
}
}

[ProtoContract]
public class Person
{
public Name Name { get; init; }
public Age Age { get; init; }
public BoxId BoxId { get; init; }
public Temperature Temperature { get; init; }
[ProtoMember(1)] public Name? Name { get; set; }

Check failure on line 100 in samples/Vogen.Examples/SerializationAndConversion/Grpc/GrpcScenario.cs

View workflow job for this annotation

GitHub Actions / Analyze (csharp)

The annotation for nullable reference types should only be used in code within a '#nullable' annotations context.

Check failure on line 100 in samples/Vogen.Examples/SerializationAndConversion/Grpc/GrpcScenario.cs

View workflow job for this annotation

GitHub Actions / Analyze (csharp)

The annotation for nullable reference types should only be used in code within a '#nullable' annotations context.

Check failure on line 100 in samples/Vogen.Examples/SerializationAndConversion/Grpc/GrpcScenario.cs

View workflow job for this annotation

GitHub Actions / ubuntu-latest

The annotation for nullable reference types should only be used in code within a '#nullable' annotations context.

Check failure on line 100 in samples/Vogen.Examples/SerializationAndConversion/Grpc/GrpcScenario.cs

View workflow job for this annotation

GitHub Actions / ubuntu-latest

The annotation for nullable reference types should only be used in code within a '#nullable' annotations context.

Check failure on line 100 in samples/Vogen.Examples/SerializationAndConversion/Grpc/GrpcScenario.cs

View workflow job for this annotation

GitHub Actions / macos-latest

The annotation for nullable reference types should only be used in code within a '#nullable' annotations context.

Check failure on line 100 in samples/Vogen.Examples/SerializationAndConversion/Grpc/GrpcScenario.cs

View workflow job for this annotation

GitHub Actions / macos-latest

The annotation for nullable reference types should only be used in code within a '#nullable' annotations context.

Check failure on line 100 in samples/Vogen.Examples/SerializationAndConversion/Grpc/GrpcScenario.cs

View workflow job for this annotation

GitHub Actions / windows-latest

The annotation for nullable reference types should only be used in code within a '#nullable' annotations context.

Check failure on line 100 in samples/Vogen.Examples/SerializationAndConversion/Grpc/GrpcScenario.cs

View workflow job for this annotation

GitHub Actions / windows-latest

The annotation for nullable reference types should only be used in code within a '#nullable' annotations context.
[ProtoMember(2)] public Age? Age { get; set; }

Check failure on line 101 in samples/Vogen.Examples/SerializationAndConversion/Grpc/GrpcScenario.cs

View workflow job for this annotation

GitHub Actions / Analyze (csharp)

The annotation for nullable reference types should only be used in code within a '#nullable' annotations context.

Check failure on line 101 in samples/Vogen.Examples/SerializationAndConversion/Grpc/GrpcScenario.cs

View workflow job for this annotation

GitHub Actions / Analyze (csharp)

The annotation for nullable reference types should only be used in code within a '#nullable' annotations context.

Check failure on line 101 in samples/Vogen.Examples/SerializationAndConversion/Grpc/GrpcScenario.cs

View workflow job for this annotation

GitHub Actions / ubuntu-latest

The annotation for nullable reference types should only be used in code within a '#nullable' annotations context.

Check failure on line 101 in samples/Vogen.Examples/SerializationAndConversion/Grpc/GrpcScenario.cs

View workflow job for this annotation

GitHub Actions / ubuntu-latest

The annotation for nullable reference types should only be used in code within a '#nullable' annotations context.

Check failure on line 101 in samples/Vogen.Examples/SerializationAndConversion/Grpc/GrpcScenario.cs

View workflow job for this annotation

GitHub Actions / macos-latest

The annotation for nullable reference types should only be used in code within a '#nullable' annotations context.

Check failure on line 101 in samples/Vogen.Examples/SerializationAndConversion/Grpc/GrpcScenario.cs

View workflow job for this annotation

GitHub Actions / macos-latest

The annotation for nullable reference types should only be used in code within a '#nullable' annotations context.

Check failure on line 101 in samples/Vogen.Examples/SerializationAndConversion/Grpc/GrpcScenario.cs

View workflow job for this annotation

GitHub Actions / windows-latest

The annotation for nullable reference types should only be used in code within a '#nullable' annotations context.

Check failure on line 101 in samples/Vogen.Examples/SerializationAndConversion/Grpc/GrpcScenario.cs

View workflow job for this annotation

GitHub Actions / windows-latest

The annotation for nullable reference types should only be used in code within a '#nullable' annotations context.
[ProtoMember(3)] public BoxId? BoxId { get; set; }

Check failure on line 102 in samples/Vogen.Examples/SerializationAndConversion/Grpc/GrpcScenario.cs

View workflow job for this annotation

GitHub Actions / Analyze (csharp)

The annotation for nullable reference types should only be used in code within a '#nullable' annotations context.

Check failure on line 102 in samples/Vogen.Examples/SerializationAndConversion/Grpc/GrpcScenario.cs

View workflow job for this annotation

GitHub Actions / Analyze (csharp)

The annotation for nullable reference types should only be used in code within a '#nullable' annotations context.

Check failure on line 102 in samples/Vogen.Examples/SerializationAndConversion/Grpc/GrpcScenario.cs

View workflow job for this annotation

GitHub Actions / ubuntu-latest

The annotation for nullable reference types should only be used in code within a '#nullable' annotations context.

Check failure on line 102 in samples/Vogen.Examples/SerializationAndConversion/Grpc/GrpcScenario.cs

View workflow job for this annotation

GitHub Actions / ubuntu-latest

The annotation for nullable reference types should only be used in code within a '#nullable' annotations context.

Check failure on line 102 in samples/Vogen.Examples/SerializationAndConversion/Grpc/GrpcScenario.cs

View workflow job for this annotation

GitHub Actions / macos-latest

The annotation for nullable reference types should only be used in code within a '#nullable' annotations context.

Check failure on line 102 in samples/Vogen.Examples/SerializationAndConversion/Grpc/GrpcScenario.cs

View workflow job for this annotation

GitHub Actions / macos-latest

The annotation for nullable reference types should only be used in code within a '#nullable' annotations context.

Check failure on line 102 in samples/Vogen.Examples/SerializationAndConversion/Grpc/GrpcScenario.cs

View workflow job for this annotation

GitHub Actions / windows-latest

The annotation for nullable reference types should only be used in code within a '#nullable' annotations context.

Check failure on line 102 in samples/Vogen.Examples/SerializationAndConversion/Grpc/GrpcScenario.cs

View workflow job for this annotation

GitHub Actions / windows-latest

The annotation for nullable reference types should only be used in code within a '#nullable' annotations context.
[ProtoMember(4)] public Temperature? Temperature { get; set; }

Check failure on line 103 in samples/Vogen.Examples/SerializationAndConversion/Grpc/GrpcScenario.cs

View workflow job for this annotation

GitHub Actions / Analyze (csharp)

The annotation for nullable reference types should only be used in code within a '#nullable' annotations context.

Check failure on line 103 in samples/Vogen.Examples/SerializationAndConversion/Grpc/GrpcScenario.cs

View workflow job for this annotation

GitHub Actions / Analyze (csharp)

The annotation for nullable reference types should only be used in code within a '#nullable' annotations context.

Check failure on line 103 in samples/Vogen.Examples/SerializationAndConversion/Grpc/GrpcScenario.cs

View workflow job for this annotation

GitHub Actions / ubuntu-latest

The annotation for nullable reference types should only be used in code within a '#nullable' annotations context.

Check failure on line 103 in samples/Vogen.Examples/SerializationAndConversion/Grpc/GrpcScenario.cs

View workflow job for this annotation

GitHub Actions / ubuntu-latest

The annotation for nullable reference types should only be used in code within a '#nullable' annotations context.

Check failure on line 103 in samples/Vogen.Examples/SerializationAndConversion/Grpc/GrpcScenario.cs

View workflow job for this annotation

GitHub Actions / macos-latest

The annotation for nullable reference types should only be used in code within a '#nullable' annotations context.

Check failure on line 103 in samples/Vogen.Examples/SerializationAndConversion/Grpc/GrpcScenario.cs

View workflow job for this annotation

GitHub Actions / macos-latest

The annotation for nullable reference types should only be used in code within a '#nullable' annotations context.

Check failure on line 103 in samples/Vogen.Examples/SerializationAndConversion/Grpc/GrpcScenario.cs

View workflow job for this annotation

GitHub Actions / windows-latest

The annotation for nullable reference types should only be used in code within a '#nullable' annotations context.

Check failure on line 103 in samples/Vogen.Examples/SerializationAndConversion/Grpc/GrpcScenario.cs

View workflow job for this annotation

GitHub Actions / windows-latest

The annotation for nullable reference types should only be used in code within a '#nullable' annotations context.
}

[ServiceContract]
Expand Down Expand Up @@ -74,11 +145,10 @@
[ProtoContract]
public class VogenSurrogate<TW, TP> where TW: IVogen<TW, TP>
{
[ProtoMember(1)]
public TP Value { get; set; }
[ProtoMember(1)] public TP Value { get; set; } = default!;

public static implicit operator TW(VogenSurrogate<TW, TP> surrogate) => TW.From(surrogate.Value);
public static implicit operator VogenSurrogate<TW, TP>(TW value) => new() { Value = value.Value };
public static implicit operator VogenSurrogate<TW, TP>?(TW? value) => value is null ? null : new VogenSurrogate<TW, TP> { Value = value.Value };

Check failure on line 151 in samples/Vogen.Examples/SerializationAndConversion/Grpc/GrpcScenario.cs

View workflow job for this annotation

GitHub Actions / Analyze (csharp)

The annotation for nullable reference types should only be used in code within a '#nullable' annotations context.

Check failure on line 151 in samples/Vogen.Examples/SerializationAndConversion/Grpc/GrpcScenario.cs

View workflow job for this annotation

GitHub Actions / Analyze (csharp)

The annotation for nullable reference types should only be used in code within a '#nullable' annotations context.

Check failure on line 151 in samples/Vogen.Examples/SerializationAndConversion/Grpc/GrpcScenario.cs

View workflow job for this annotation

GitHub Actions / ubuntu-latest

The annotation for nullable reference types should only be used in code within a '#nullable' annotations context.

Check failure on line 151 in samples/Vogen.Examples/SerializationAndConversion/Grpc/GrpcScenario.cs

View workflow job for this annotation

GitHub Actions / ubuntu-latest

The annotation for nullable reference types should only be used in code within a '#nullable' annotations context.

Check failure on line 151 in samples/Vogen.Examples/SerializationAndConversion/Grpc/GrpcScenario.cs

View workflow job for this annotation

GitHub Actions / macos-latest

The annotation for nullable reference types should only be used in code within a '#nullable' annotations context.

Check failure on line 151 in samples/Vogen.Examples/SerializationAndConversion/Grpc/GrpcScenario.cs

View workflow job for this annotation

GitHub Actions / macos-latest

The annotation for nullable reference types should only be used in code within a '#nullable' annotations context.

Check failure on line 151 in samples/Vogen.Examples/SerializationAndConversion/Grpc/GrpcScenario.cs

View workflow job for this annotation

GitHub Actions / windows-latest

The annotation for nullable reference types should only be used in code within a '#nullable' annotations context.

Check failure on line 151 in samples/Vogen.Examples/SerializationAndConversion/Grpc/GrpcScenario.cs

View workflow job for this annotation

GitHub Actions / windows-latest

The annotation for nullable reference types should only be used in code within a '#nullable' annotations context.
}


Expand Down
6 changes: 6 additions & 0 deletions samples/Vogen.Examples/Vogen.Examples.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,10 @@
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="8.0.8" />
<PackageReference Include="protobuf-net" Version="3.2.56" />
<PackageReference Include="protobuf-net.Grpc" Version="1.2.2" />
<PackageReference Include="protobuf-net.Grpc.AspNetCore" Version="1.2.2" />
<PackageReference Include="protobuf-net.Grpc.Reflection" Version="1.2.2" />
<PackageReference Include="protobuf-net.Reflection" Version="3.2.52" />
<PackageReference Include="Grpc.Net.Client" Version="2.70.0" />
<PackageReference Include="SQLitePCLRaw.lib.e_sqlite3" Version="2.1.9" />
<PackageReference Include="Microsoft.EntityFrameworkCore.InMemory" Version="8.0.8" />

Expand All @@ -43,6 +45,10 @@
</PackageReference>
</ItemGroup>

<ItemGroup>
<FrameworkReference Include="Microsoft.AspNetCore.App" />
</ItemGroup>

<ItemGroup Condition=" '$(UseLocallyBuiltPackage)' != ''">
<PackageReference Include="Vogen" Version="999.9.*" />
</ItemGroup>
Expand Down
Loading