Skip to content

Commit 70065e4

Browse files
authored
feat: support structural json mapping (#584)
Add support for using POCOs mapped to JSON columns. Fixes #581
1 parent d180478 commit 70065e4

File tree

19 files changed

+308
-21
lines changed

19 files changed

+308
-21
lines changed

Google.Cloud.EntityFrameworkCore.Spanner.IntegrationTests/Model/SpannerSampleDbContext.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -209,6 +209,10 @@ protected override void OnModelCreating(ModelBuilder modelBuilder)
209209
entity.Property(e => e.Code).HasMaxLength(10);
210210

211211
entity.Property(e => e.Name).HasMaxLength(100);
212+
entity.OwnsMany(e => e.Descriptions, builder =>
213+
{
214+
builder.ToJson();
215+
});
212216
});
213217

214218
modelBuilder.Entity<TicketSales>();
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
// Copyright 2025 Google Inc. All Rights Reserved.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
namespace Google.Cloud.EntityFrameworkCore.Spanner.IntegrationTests.Model;
16+
17+
public class VenueDescription
18+
{
19+
public string Category { get; set; }
20+
public string Description { get; set; }
21+
public long Capacity { get; set; }
22+
public bool Active { get; set; }
23+
}

Google.Cloud.EntityFrameworkCore.Spanner.IntegrationTests/Model/Venues.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ public Venues()
2929
public bool Active { get; set; }
3030
public long? Capacity { get; set; }
3131
public List<Nullable<double>> Ratings { get; set; }
32+
public List<VenueDescription> Descriptions { get; set; }
3233

3334
public virtual ICollection<Concerts> Concerts { get; set; }
3435
}

Google.Cloud.EntityFrameworkCore.Spanner.IntegrationTests/SampleDataModel.sql

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -47,11 +47,12 @@ CREATE TABLE Tracks (
4747
CREATE UNIQUE INDEX Idx_Tracks_AlbumId_Title ON Tracks (TrackId, Title);
4848

4949
CREATE TABLE Venues (
50-
Code STRING(10) NOT NULL,
51-
Name STRING(100),
52-
Active BOOL NOT NULL,
53-
Capacity INT64,
54-
Ratings ARRAY<FLOAT64>,
50+
Code STRING(10) NOT NULL,
51+
Name STRING(100),
52+
Active BOOL NOT NULL,
53+
Capacity INT64,
54+
Ratings ARRAY<FLOAT64>,
55+
Descriptions JSON,
5556
) PRIMARY KEY (Code);
5657

5758
CREATE TABLE Concerts (

Google.Cloud.EntityFrameworkCore.Spanner.IntegrationTests/TransactionTests.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -274,6 +274,10 @@ public async Task CanExecuteStaleRead()
274274
{
275275
Code = venueCode,
276276
Name = venueName,
277+
Descriptions = [
278+
new () {Active = true, Capacity = 1000, Category = "Concert Hall", Description = "Large concert hall"},
279+
new () {Active = false, Capacity = 1000, Category = "Hall", Description = "Large hall"},
280+
],
277281
});
278282
await db.SaveChangesAsync();
279283
await transaction.CommitAsync();

Google.Cloud.EntityFrameworkCore.Spanner.Samples/SampleModel/SampleDataModel.sql

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -48,11 +48,12 @@ CREATE TABLE Tracks (
4848
CREATE UNIQUE INDEX Idx_Tracks_AlbumId_Title ON Tracks (AlbumId, Title);
4949

5050
CREATE TABLE Venues (
51-
Code STRING(10) NOT NULL,
52-
Name STRING(100),
53-
Description JSON,
54-
Active BOOL NOT NULL,
55-
Version INT64 NOT NULL,
51+
Code STRING(10) NOT NULL,
52+
Name STRING(100),
53+
Description JSON,
54+
Active BOOL NOT NULL,
55+
Descriptions JSON,
56+
Version INT64 NOT NULL,
5657
) PRIMARY KEY (Code);
5758

5859
CREATE TABLE Concerts (

Google.Cloud.EntityFrameworkCore.Spanner.Samples/SampleModel/SpannerSampleDbContext.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,10 @@ protected override void OnModelCreating(ModelBuilder modelBuilder)
106106
{
107107
entity.HasKey(entity => new { entity.Code });
108108
entity.Property(e => e.Version).IsConcurrencyToken();
109+
entity.OwnsMany(e => e.Descriptions, builder =>
110+
{
111+
builder.ToJson();
112+
});
109113
});
110114

111115
modelBuilder.Entity<Concert>(entity =>

Google.Cloud.EntityFrameworkCore.Spanner.Samples/SampleModel/Venue.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ public class Venue : VersionedEntity
2424
public string Name { get; set; }
2525
public JsonDocument Description { get; set; }
2626
public bool Active { get; set; }
27+
public List<VenueDescription> Descriptions { get; set; }
2728

2829
public virtual ICollection<Concert> Concerts { get; set; } = new HashSet<Concert>();
2930
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
// Copyright 2025 Google Inc. All Rights Reserved.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
namespace Google.Cloud.EntityFrameworkCore.Spanner.Samples.SampleModel;
16+
17+
public class VenueDescription
18+
{
19+
public string Category { get; set; }
20+
public string Description { get; set; }
21+
public long Capacity { get; set; }
22+
public bool Active { get; set; }
23+
}
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
// Copyright 2025 Google Inc. All Rights Reserved.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
using Google.Cloud.EntityFrameworkCore.Spanner.Samples.SampleModel;
16+
using System;
17+
using System.Collections.Generic;
18+
using System.Text.Json;
19+
using System.Threading.Tasks;
20+
using Microsoft.EntityFrameworkCore;
21+
22+
/// <summary>
23+
/// Simple sample showing how to map a POCO to a JSON column.
24+
///
25+
/// Run from the command line with `dotnet run StructuralJsonSample`
26+
/// </summary>
27+
public static class StructuralJsonSample
28+
{
29+
public static async Task Run(string connectionString)
30+
{
31+
using var context = new SpannerSampleDbContext(connectionString);
32+
33+
// Create a new Venue with two VenueDescriptions.
34+
// VenueDescriptions are mapped to a single JSON column.
35+
await context.Venues.AddAsync(new Venue
36+
{
37+
Code = "CH",
38+
Name = "Concert Hall",
39+
Descriptions =
40+
[
41+
new() { Active = true, Category = "Concert Hall", Capacity = 1000, Description = "Large Concert Hall" },
42+
new() { Active = false, Category = "Hall", Capacity = 1000, Description = "Large Hall" }
43+
]
44+
});
45+
var count = await context.SaveChangesAsync();
46+
47+
// SaveChangesAsync returns the total number of rows that was inserted/updated/deleted.
48+
Console.WriteLine($"Added {count} venue.");
49+
50+
// Read back the Venue that we added and iterate over the VenueDescriptions that were serialized as JSON in the database.
51+
var venue = await context.Venues.FirstAsync();
52+
Console.WriteLine($"Venue has {venue.Descriptions.Count} descriptions.");
53+
foreach (var description in venue.Descriptions)
54+
{
55+
Console.WriteLine($"{JsonSerializer.Serialize(description)}");
56+
}
57+
}
58+
}

0 commit comments

Comments
 (0)