Skip to content

Commit 304219c

Browse files
authored
Tweaks for by-convention mapping of gRPC model (#24553)
Fixes #23703 At some point we started eagerly throwing when attempting to build a setter delegate. This should be lazy because we don't always need a setter. Fixes #23901 Detects "propertyName_" as a backing field.
1 parent 34fa402 commit 304219c

File tree

9 files changed

+307
-2
lines changed

9 files changed

+307
-2
lines changed

src/EFCore/Metadata/Conventions/BackingFieldConvention.cs

+2
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ namespace Microsoft.EntityFrameworkCore.Metadata.Conventions
2121
/// * _[property name]
2222
/// * m_[camel-cased property name]
2323
/// * m_[property name]
24+
/// * [property name]_
2425
/// </para>
2526
/// <para>
2627
/// The field type must be of a type that's assignable to or from the property type.
@@ -181,6 +182,7 @@ private void DiscoverField(IConventionPropertyBaseBuilder conventionPropertyBase
181182
match = TryMatch(sortedFields, "_", "", propertyName, propertyBase, match, entityClrType, propertyName);
182183
match = TryMatch(sortedFields, "m_", camelPrefix, camelizedSuffix, propertyBase, match, entityClrType, propertyName);
183184
match = TryMatch(sortedFields, "m_", "", propertyName, propertyBase, match, entityClrType, propertyName);
185+
match = TryMatch(sortedFields, "", camelPrefix + camelizedSuffix, "_", propertyBase, match, entityClrType, propertyName);
184186
}
185187

186188
return match;

src/EFCore/Metadata/Internal/ClrCollectionAccessorFactory.cs

+2-2
Original file line numberDiff line numberDiff line change
@@ -125,8 +125,8 @@ private static IClrCollectionAccessor CreateGeneric<TEntity, TCollection, TEleme
125125
var valueParameter = Expression.Parameter(typeof(TCollection), "collection");
126126

127127
var memberInfoForRead = navigation.GetMemberInfo(forMaterialization: false, forSet: false);
128-
var memberInfoForWrite = navigation.GetMemberInfo(forMaterialization: false, forSet: true);
129-
var memberInfoForMaterialization = navigation.GetMemberInfo(forMaterialization: true, forSet: true);
128+
navigation.TryGetMemberInfo(forConstruction: false, forSet: true, out var memberInfoForWrite, out _);
129+
navigation.TryGetMemberInfo(forConstruction: true, forSet: true, out var memberInfoForMaterialization, out _);
130130

131131
var memberAccessForRead = (Expression)Expression.MakeMemberAccess(entityParameter, memberInfoForRead);
132132
if (memberAccessForRead.Type != typeof(TCollection))
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3+
4+
using Microsoft.EntityFrameworkCore.TestUtilities;
5+
6+
namespace Microsoft.EntityFrameworkCore
7+
{
8+
public class GrpcInMemoryTest : GrpcTestBase<GrpcInMemoryTest.GrpcInMemoryFixture>
9+
{
10+
public GrpcInMemoryTest(GrpcInMemoryFixture fixture)
11+
: base(fixture)
12+
{
13+
}
14+
15+
public class GrpcInMemoryFixture : GrpcFixtureBase
16+
{
17+
protected override ITestStoreFactory TestStoreFactory
18+
=> InMemoryTestStoreFactory.Instance;
19+
}
20+
}
21+
}

test/EFCore.AspNet.Specification.Tests/EFCore.AspNet.Specification.Tests.csproj

+5
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,11 @@
1919
<ProjectReference Include="..\EFCore.Specification.Tests\EFCore.Specification.Tests.csproj" />
2020
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="5.0.1" />
2121
<PackageReference Include="IdentityServer4.EntityFramework" Version="4.1.1" />
22+
<PackageReference Include="Grpc.AspNetCore" Version="2.35.0" />
23+
</ItemGroup>
24+
25+
<ItemGroup>
26+
<Protobuf Include="ProtoTest.proto" />
2227
</ItemGroup>
2328

2429
</Project>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,166 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3+
4+
using System;
5+
using System.Collections.Generic;
6+
using System.Linq;
7+
using Google.Protobuf.WellKnownTypes;
8+
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
9+
using Microsoft.EntityFrameworkCore.TestUtilities;
10+
using ProtoTest;
11+
using Xunit;
12+
13+
namespace Microsoft.EntityFrameworkCore
14+
{
15+
public abstract class GrpcTestBase<TFixture> : IClassFixture<TFixture>
16+
where TFixture : GrpcTestBase<TFixture>.GrpcFixtureBase
17+
{
18+
protected GrpcTestBase(TFixture fixture)
19+
=> Fixture = fixture;
20+
21+
protected TFixture Fixture { get; }
22+
23+
protected List<EntityTypeMapping> ExpectedMappings
24+
=> new()
25+
{
26+
new()
27+
{
28+
Name = "PostTag",
29+
TableName = "PostTag",
30+
PrimaryKey =
31+
"Key: PostTag (Dictionary<string, object>).PostsInTagDataPostId, PostTag (Dictionary<string, object>).TagsInPostDataTagId PK",
32+
Properties =
33+
{
34+
"Property: PostTag (Dictionary<string, object>).PostsInTagDataPostId (no field, int) Indexer Required PK FK AfterSave:Throw",
35+
"Property: PostTag (Dictionary<string, object>).TagsInPostDataTagId (no field, int) Indexer Required PK FK Index AfterSave:Throw",
36+
},
37+
Indexes = { "{'TagsInPostDataTagId'} ", },
38+
FKs =
39+
{
40+
"ForeignKey: PostTag (Dictionary<string, object>) {'PostsInTagDataPostId'} -> Post {'PostId'} Cascade",
41+
"ForeignKey: PostTag (Dictionary<string, object>) {'TagsInPostDataTagId'} -> Tag {'TagId'} Cascade",
42+
},
43+
},
44+
new()
45+
{
46+
Name = "ProtoTest.Author",
47+
TableName = "Author",
48+
PrimaryKey = "Key: Author.AuthorId PK",
49+
Properties =
50+
{
51+
"Property: Author.AuthorId (authorId_, int) Required PK AfterSave:Throw ValueGenerated.OnAdd",
52+
"Property: Author.DateCreated (dateCreated_, Timestamp)",
53+
"Property: Author.Name (name_, string)",
54+
},
55+
},
56+
new()
57+
{
58+
Name = "ProtoTest.Post",
59+
TableName = "Post",
60+
PrimaryKey = "Key: Post.PostId PK",
61+
Properties =
62+
{
63+
"Property: Post.PostId (postId_, int) Required PK AfterSave:Throw ValueGenerated.OnAdd",
64+
"Property: Post.AuthorId (authorId_, int) Required FK Index",
65+
"Property: Post.DateCreated (dateCreated_, Timestamp)",
66+
"Property: Post.PostStat (postStat_, PostStatus) Required",
67+
"Property: Post.Title (title_, string)",
68+
},
69+
Indexes = { "{'AuthorId'} ", },
70+
FKs = { "ForeignKey: Post {'AuthorId'} -> Author {'AuthorId'} ToPrincipal: PostAuthor Cascade", },
71+
Navigations = { "Navigation: Post.PostAuthor (postAuthor_, Author) ToPrincipal Author", },
72+
SkipNavigations =
73+
{
74+
"SkipNavigation: Post.TagsInPostData (tagsInPostData_, RepeatedField<Tag>) CollectionTag Inverse: PostsInTagData",
75+
},
76+
},
77+
new()
78+
{
79+
Name = "ProtoTest.Tag",
80+
TableName = "Tag",
81+
PrimaryKey = "Key: Tag.TagId PK",
82+
Properties =
83+
{
84+
"Property: Tag.TagId (tagId_, int) Required PK AfterSave:Throw ValueGenerated.OnAdd",
85+
"Property: Tag.Name (name_, string)",
86+
},
87+
SkipNavigations =
88+
{
89+
"SkipNavigation: Tag.PostsInTagData (postsInTagData_, RepeatedField<Post>) CollectionPost Inverse: TagsInPostData",
90+
},
91+
},
92+
};
93+
94+
[ConditionalFact]
95+
public void Can_build_Grpc_model()
96+
{
97+
using var context = Fixture.CreateContext();
98+
99+
var entityTypeMappings = context.Model.GetEntityTypes().Select(e => new EntityTypeMapping(e)).ToList();
100+
EntityTypeMapping.AssertEqual(ExpectedMappings, entityTypeMappings);
101+
}
102+
103+
[ConditionalFact]
104+
public void Can_query_Grpc_model()
105+
{
106+
using var context = Fixture.CreateContext();
107+
108+
var post = context.Set<Post>().Include(e => e.PostAuthor).Include(e => e.TagsInPostData).Single();
109+
110+
Assert.Equal("Arthur's post", post.Title);
111+
Assert.Equal(new DateTime(2021, 9, 3, 12, 10, 0, DateTimeKind.Utc), post.DateCreated.ToDateTime());
112+
Assert.Equal(PostStatus.Published, post.PostStat);
113+
Assert.Equal("Arthur", post.PostAuthor.Name);
114+
Assert.Equal(new DateTime(1973, 9, 3, 12, 10, 0, DateTimeKind.Utc), post.PostAuthor.DateCreated.ToDateTime());
115+
116+
Assert.Equal(2, post.TagsInPostData.Count);
117+
Assert.Contains("Puppies", post.TagsInPostData.Select(e => e.Name).ToList());
118+
Assert.Contains("Kittens", post.TagsInPostData.Select(e => e.Name).ToList());
119+
Assert.Same(post, post.TagsInPostData.First().PostsInTagData.First());
120+
Assert.Same(post, post.TagsInPostData.Skip(1).First().PostsInTagData.First());
121+
}
122+
123+
public class GrpcContext : PoolableDbContext
124+
{
125+
public GrpcContext(DbContextOptions options)
126+
: base(options)
127+
{
128+
}
129+
130+
protected override void OnModelCreating(ModelBuilder modelBuilder)
131+
{
132+
var timeStampConverter = new ValueConverter<Timestamp, DateTime>(
133+
v => v.ToDateTime(),
134+
v => new DateTime(v.Ticks, DateTimeKind.Utc).ToTimestamp());
135+
136+
modelBuilder.Entity<Author>().Property(e => e.DateCreated).HasConversion(timeStampConverter);
137+
modelBuilder.Entity<Post>().Property(e => e.DateCreated).HasConversion(timeStampConverter);
138+
modelBuilder.Entity<Tag>();
139+
}
140+
}
141+
142+
public abstract class GrpcFixtureBase : SharedStoreFixtureBase<GrpcContext>
143+
{
144+
protected override string StoreName { get; } = "GrpcTest";
145+
146+
protected override void Seed(GrpcContext context)
147+
{
148+
var post = new Post
149+
{
150+
DateCreated = Timestamp.FromDateTime(new DateTime(2021, 9, 3, 12, 10, 0, DateTimeKind.Utc)),
151+
Title = "Arthur's post",
152+
PostAuthor = new Author
153+
{
154+
DateCreated = Timestamp.FromDateTime(new DateTime(1973, 9, 3, 12, 10, 0, DateTimeKind.Utc)), Name = "Arthur"
155+
},
156+
PostStat = PostStatus.Published,
157+
TagsInPostData = { new Tag { Name = "Kittens" }, new Tag { Name = "Puppies" } }
158+
};
159+
160+
context.Add(post);
161+
162+
context.SaveChanges();
163+
}
164+
}
165+
}
166+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
syntax = "proto3";
2+
option csharp_namespace = "ProtoTest";
3+
import "google/protobuf/empty.proto";
4+
import "google/protobuf/timestamp.proto";
5+
package Test;
6+
7+
service ProtoTest{
8+
rpc GetPosts(google.protobuf.Empty) returns (Posts);
9+
rpc GetPost(GetPostQuery) returns (Post);
10+
rpc GetAuthors(google.protobuf.Empty) returns (Authors);
11+
rpc GetAuthor(GetAuthorQuery) returns (Author);
12+
}
13+
14+
message Author {
15+
int32 author_id = 1;
16+
string name = 2;
17+
google.protobuf.Timestamp date_created = 3;
18+
}
19+
message Authors {
20+
repeated Author authors_data = 1;
21+
}
22+
23+
message Post {
24+
int32 post_id = 1;
25+
int32 author_id = 2;
26+
string title = 3;
27+
google.protobuf.Timestamp date_created = 4;
28+
PostStatus post_stat = 5;
29+
Author post_author = 6;
30+
repeated Tag tags_in_post_data = 7;
31+
}
32+
message Posts {
33+
repeated Post posts_data = 1;
34+
}
35+
36+
message Tag {
37+
int32 tag_id = 1;
38+
string name = 2;
39+
repeated Post posts_in_tag_data = 3;
40+
}
41+
message Tags {
42+
repeated Tag tags_data = 1;
43+
}
44+
45+
enum PostStatus {
46+
POST_STATUS_HIDDEN = 0;
47+
POST_STATUS_PUBLISHED = 1;
48+
POST_STATUS_DELETED = 2;
49+
}
50+
51+
message GetPostQuery {
52+
int32 id = 1;
53+
}
54+
55+
message GetAuthorQuery {
56+
int32 id = 1;
57+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3+
4+
using Microsoft.EntityFrameworkCore.TestUtilities;
5+
6+
namespace Microsoft.EntityFrameworkCore
7+
{
8+
public class GrpcSqlServerTest : GrpcTestBase<GrpcSqlServerTest.GrpcSqlServerFixture>
9+
{
10+
public GrpcSqlServerTest(GrpcSqlServerFixture fixture)
11+
: base(fixture)
12+
{
13+
}
14+
15+
public class GrpcSqlServerFixture : GrpcFixtureBase
16+
{
17+
protected override ITestStoreFactory TestStoreFactory
18+
=> SqlServerTestStoreFactory.Instance;
19+
}
20+
}
21+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3+
4+
using Microsoft.EntityFrameworkCore.TestUtilities;
5+
6+
namespace Microsoft.EntityFrameworkCore
7+
{
8+
public class GrpcSqliteTest : GrpcTestBase<GrpcSqliteTest.GrpcSqliteFixture>
9+
{
10+
public GrpcSqliteTest(GrpcSqliteFixture fixture)
11+
: base(fixture)
12+
{
13+
}
14+
15+
public class GrpcSqliteFixture : GrpcFixtureBase
16+
{
17+
protected override ITestStoreFactory TestStoreFactory
18+
=> SqliteTestStoreFactory.Instance;
19+
}
20+
}
21+
}

test/EFCore.Tests/Metadata/Conventions/BackingFieldConventionTest.cs

+12
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,10 @@ public void Camel_case_matching_field_is_not_used_if_type_is_not_compatible()
4646
public void Underscore_camel_case_matching_field_is_used_as_next_preference()
4747
=> FieldMatchTest<TheDarkSideOfTheMoon>("Time", "_time");
4848

49+
[ConditionalFact]
50+
public void Underscore_suffix_camel_case_matching_field_is_used_as_next_preference()
51+
=> FieldMatchTest<TheDarkSideOfTheMoon>("Time", "_time");
52+
4953
[ConditionalFact]
5054
public void Underscore_camel_case_matching_field_is_not_used_if_type_is_not_compatible()
5155
=> FieldMatchTest<TheDarkSideOfTheMoon>("TheGreatGigInTheSky", "_TheGreatGigInTheSky");
@@ -330,6 +334,14 @@ public int Time
330334
set { _time = value; }
331335
}
332336

337+
private int? time2_;
338+
339+
public int Time2
340+
{
341+
get { return (int)_time; }
342+
set { _time = value; }
343+
}
344+
333345
private readonly string _theGreatGigInTheSky;
334346
private int? _TheGreatGigInTheSky;
335347

0 commit comments

Comments
 (0)