From 2d42b5c7d5948785ab22a1402b770e365b2b0041 Mon Sep 17 00:00:00 2001 From: "Jeremy D. Miller" Date: Thu, 17 Oct 2024 13:55:54 -0500 Subject: [PATCH] You can use non-nullable value type members for document identifiers. Closes GH-3481 --- docs/documents/identity.md | 5 +- .../Schema/Identity/ValueTypeIdGeneration.cs | 52 +++- .../guid_based_document_operations.cs | 275 +++++++++++++++++- .../int_based_document_operations.cs | 256 ++++++++++++++++ .../long_based_document_operations.cs | 263 +++++++++++++++++ .../string_id_document_operations.cs | 233 +++++++++++++++ 6 files changed, 1071 insertions(+), 13 deletions(-) diff --git a/docs/documents/identity.md b/docs/documents/identity.md index 1cd9b331b8..21c961bb47 100644 --- a/docs/documents/identity.md +++ b/docs/documents/identity.md @@ -399,11 +399,14 @@ public async Task update_a_document_smoke_test() snippet source | anchor +::: tip +Marten 7.31.0 "fixed" it so that you don't have to use `Nullable` for the identity member of strong typed identifiers. +::: + As you might infer -- or not -- there's a couple rules and internal behavior: * The identity selection is done just the same as the primitive types, Marten is either looking for an `id`/`Id` member, or a member decorated with `[Identity]` -* If Marten is going to assign the identity, you will need to use `Nullable` for the identity member of the document * There is a new `IQuerySession.LoadAsync(object id)` overload that was specifically built for strong typed identifiers * For `Guid`-wrapped values, Marten is assigning missing identity values based on its sequential `Guid` support * For `int` or `long`-wrapped values, Marten is using its HiLo support to define the wrapped values diff --git a/src/Marten/Schema/Identity/ValueTypeIdGeneration.cs b/src/Marten/Schema/Identity/ValueTypeIdGeneration.cs index f56ca6451a..522afbcd23 100644 --- a/src/Marten/Schema/Identity/ValueTypeIdGeneration.cs +++ b/src/Marten/Schema/Identity/ValueTypeIdGeneration.cs @@ -66,54 +66,65 @@ public void GenerateCode(GeneratedMethod method, DocumentMapping mapping) method.Frames.Code($"return {{0}}.{mapping.CodeGen.AccessId};", document); } + private string innerValueAccessor(DocumentMapping mapping) + { + return mapping.IdMember.GetRawMemberType().IsNullable() ? $"{mapping.IdMember.Name}.Value" : mapping.IdMember.Name; + } + private void generateStringWrapper(GeneratedMethod method, DocumentMapping mapping, Use document) { - method.Frames.Code($"return {{0}}.{mapping.IdMember.Name}.Value;", document); + method.Frames.Code($"return {{0}}.{innerValueAccessor(mapping)};", document); } private void generateLongWrapper(GeneratedMethod method, DocumentMapping mapping, Use document) { + var isDefault = mapping.IdMember.GetRawMemberType().IsNullable() ? $"{mapping.IdMember.Name} == null" : $"{mapping.IdMember.Name}.Value == default"; + var database = Use.Type(); if (Ctor != null) { method.Frames.Code( - $"if ({{0}}.{mapping.IdMember.Name} == null) _setter({{0}}, new {OuterType.FullNameInCode()}({{1}}.Sequences.SequenceFor({{2}}).NextLong()));", + $"if ({{0}}.{isDefault}) _setter({{0}}, new {OuterType.FullNameInCode()}({{1}}.Sequences.SequenceFor({{2}}).NextLong()));", document, database, mapping.DocumentType); } else { method.Frames.Code( - $"if ({{0}}.{mapping.IdMember.Name} == null) _setter({{0}}, {OuterType.FullNameInCode()}.{Builder.Name}({{1}}.Sequences.SequenceFor({{2}}).NextLong()));", + $"if ({{0}}.{isDefault}) _setter({{0}}, {OuterType.FullNameInCode()}.{Builder.Name}({{1}}.Sequences.SequenceFor({{2}}).NextLong()));", document, database, mapping.DocumentType); } } private void generateIntWrapper(GeneratedMethod method, DocumentMapping mapping, Use document) { + var isDefault = mapping.IdMember.GetRawMemberType().IsNullable() ? $"{mapping.IdMember.Name} == null" : $"{mapping.IdMember.Name}.Value == default"; + var database = Use.Type(); if (Ctor != null) { method.Frames.Code( - $"if ({{0}}.{mapping.IdMember.Name} == null) _setter({{0}}, new {OuterType.FullNameInCode()}({{1}}.Sequences.SequenceFor({{2}}).NextInt()));", + $"if ({{0}}.{isDefault}) _setter({{0}}, new {OuterType.FullNameInCode()}({{1}}.Sequences.SequenceFor({{2}}).NextInt()));", document, database, mapping.DocumentType); } else { method.Frames.Code( - $"if ({{0}}.{mapping.IdMember.Name} == null) _setter({{0}}, {OuterType.FullNameInCode()}.{Builder.Name}({{1}}.Sequences.SequenceFor({{2}}).NextInt()));", + $"if ({{0}}.{isDefault}) _setter({{0}}, {OuterType.FullNameInCode()}.{Builder.Name}({{1}}.Sequences.SequenceFor({{2}}).NextInt()));", document, database, mapping.DocumentType); } } private void generateGuidWrapper(GeneratedMethod method, DocumentMapping mapping, Use document) { + var isDefault = mapping.IdMember.GetRawMemberType().IsNullable() ? $"{mapping.IdMember.Name} == null" : $"{mapping.IdMember.Name}.Value == default"; + var newGuid = $"{typeof(CombGuidIdGeneration).FullNameInCode()}.NewGuid()"; var create = Ctor == null ? $"{OuterType.FullNameInCode()}.{Builder.Name}({newGuid})" : $"new {OuterType.FullNameInCode()}({newGuid})"; method.Frames.Code( - $"if ({{0}}.{mapping.IdMember.Name} == null) _setter({{0}}, {create});", + $"if ({{0}}.{isDefault}) _setter({{0}}, {create});", document); } @@ -122,7 +133,6 @@ public ISelectClause BuildSelectClause(string tableName) return _selector.CloneToOtherTable(tableName); } - public static bool IsCandidate(Type idType, out ValueTypeIdGeneration? idGeneration) { if (idType.IsGenericType && idType.IsNullable()) @@ -231,15 +241,35 @@ public Func BuildInnerValueSource() public void WriteBulkWriterCode(GeneratedMethod load, DocumentMapping mapping) { var dbType = PostgresqlProvider.Instance.ToParameterType(SimpleType); - load.Frames.Code($"writer.Write(document.{mapping.IdMember.Name}.Value.{ValueProperty.Name}, {{0}});", dbType); + + if (mapping.IdMember.GetRawMemberType().IsNullable()) + { + load.Frames.Code($"writer.Write(document.{mapping.IdMember.Name}.Value.{ValueProperty.Name}, {{0}});", dbType); + } + else + { + load.Frames.Code($"writer.Write(document.{mapping.IdMember.Name}.{ValueProperty.Name}, {{0}});", dbType); + } } public void WriteBulkWriterCodeAsync(GeneratedMethod load, DocumentMapping mapping) { var dbType = PostgresqlProvider.Instance.ToParameterType(SimpleType); - load.Frames.Code( - $"await writer.WriteAsync(document.{mapping.IdMember.Name}.Value.{ValueProperty.Name}, {{0}}, {{1}});", - dbType, Use.Type()); + + if (mapping.IdMember.GetRawMemberType().IsNullable()) + { + load.Frames.Code( + $"await writer.WriteAsync(document.{mapping.IdMember.Name}.Value.{ValueProperty.Name}, {{0}}, {{1}});", + dbType, Use.Type()); + } + else + { + load.Frames.Code( + $"await writer.WriteAsync(document.{mapping.IdMember.Name}.{ValueProperty.Name}, {{0}}, {{1}});", + dbType, Use.Type()); + } + + } } diff --git a/src/ValueTypeTests/StrongTypedId/guid_based_document_operations.cs b/src/ValueTypeTests/StrongTypedId/guid_based_document_operations.cs index 02ceab4e57..8494def661 100644 --- a/src/ValueTypeTests/StrongTypedId/guid_based_document_operations.cs +++ b/src/ValueTypeTests/StrongTypedId/guid_based_document_operations.cs @@ -274,9 +274,10 @@ public void bulk_writing_sync() theStore.BulkInsertDocuments(invoices); } - } + + [StronglyTypedId(Template.Guid)] public partial struct Invoice2Id; @@ -290,3 +291,275 @@ public class Invoice2 public Invoice2Id? Id { get; set; } public string Name { get; set; } } + +public class Invoice3 +{ + // Marten will use this for the identifier + // of the Invoice document + public Invoice2Id Id { get; set; } + public string Name { get; set; } +} + +public class guid_id_document_operations_with_non_nullable_identifier : IDisposable, IAsyncDisposable +{ + private readonly DocumentStore theStore; + + public guid_id_document_operations_with_non_nullable_identifier() + { + theStore = DocumentStore.For(opts => + { + opts.Connection(ConnectionSource.ConnectionString); + opts.DatabaseSchemaName = "strong_typed"; + + opts.ApplicationAssembly = GetType().Assembly; + opts.GeneratedCodeMode = TypeLoadMode.Auto; + opts.GeneratedCodeOutputPath = + AppContext.BaseDirectory.ParentDirectory().ParentDirectory().ParentDirectory().AppendPath("Internal", "Generated"); + }); + + theSession = theStore.LightweightSession(); + } + + public void Dispose() + { + theStore?.Dispose(); + theSession?.Dispose(); + } + + private IDocumentSession theSession; + + public async ValueTask DisposeAsync() + { + if (theStore != null) + { + await theStore.DisposeAsync(); + } + } + + [Fact] + public void store_document_will_assign_the_identity() + { + var invoice = new Invoice3(); + theSession.Store(invoice); + + // Marten sees that there is no existing identity, + // so it assigns a new identity + invoice.Id.ShouldNotBeNull(); + invoice.Id.Value.ShouldNotBe(Guid.Empty); + } + + [Fact] + public async Task store_a_document_smoke_test() + { + var invoice = new Invoice3(); + theSession.Store(invoice); + + await theSession.SaveChangesAsync(); + + (await theSession.Query().AnyAsync()).ShouldBeTrue(); + } + + [Fact] + public async Task insert_a_document_smoke_test() + { + var invoice = new Invoice3(); + theSession.Insert(invoice); + + await theSession.SaveChangesAsync(); + + (await theSession.Query().AnyAsync()).ShouldBeTrue(); + } + + [Fact] + public async Task update_a_document_smoke_test() + { + var invoice = new Invoice3(); + theSession.Insert(invoice); + await theSession.SaveChangesAsync(); + + invoice.Name = "updated"; + await theSession.SaveChangesAsync(); + + var loaded = await theSession.LoadAsync(invoice.Id); + loaded.Name.ShouldBeNull("updated"); + } + + [Fact] + public async Task use_within_identity_map() + { + var invoice = new Invoice3(); + theSession.Insert(invoice); + await theSession.SaveChangesAsync(); + + await using var session = theStore.IdentitySession(); + var loaded1 = await session.LoadAsync(invoice.Id); + var loaded2 = await session.LoadAsync(invoice.Id); + + loaded1.ShouldBeSameAs(loaded2); + } + + [Fact] + public async Task usage_within_dirty_checking() + { + var invoice = new Invoice3(); + theSession.Insert(invoice); + await theSession.SaveChangesAsync(); + + await using var session = theStore.DirtyTrackedSession(); + var loaded1 = await session.LoadAsync(invoice.Id); + loaded1.Name = "something else"; + + await session.SaveChangesAsync(); + + var loaded2 = await theSession.LoadAsync(invoice.Id); + loaded2.Name.ShouldBe(loaded1.Name); + } + + [Fact] + public async Task load_document() + { + var invoice = new Invoice3{Name = Guid.NewGuid().ToString()}; + theSession.Store(invoice); + + await theSession.SaveChangesAsync(); + + (await theSession.LoadAsync(invoice.Id)) + .Name.ShouldBe(invoice.Name); + } + + [Fact] + public async Task load_many_through_linq() + { + var invoice1 = new Invoice3{Name = Guid.NewGuid().ToString()}; + var Invoice3 = new Invoice3{Name = Guid.NewGuid().ToString()}; + var invoice3 = new Invoice3{Name = Guid.NewGuid().ToString()}; + theSession.Store(invoice1, Invoice3, invoice3); + + await theSession.SaveChangesAsync(); + + var results = await theSession + .Query() + .Where(x => x.Id.IsOneOf(invoice1.Id, Invoice3.Id, invoice3.Id)) + .ToListAsync(); + + results.Count.ShouldBe(3); + } + + [Fact] + public async Task delete_by_id() + { + var invoice = new Invoice3{Name = Guid.NewGuid().ToString()}; + theSession.Store(invoice); + + await theSession.SaveChangesAsync(); + + theSession.Delete(invoice.Id); + await theSession.SaveChangesAsync(); + + (await theSession.LoadAsync(invoice.Id)) + .ShouldBeNull(); + } + + [Fact] + public async Task delete_by_document() + { + var invoice = new Invoice3{Name = Guid.NewGuid().ToString()}; + theSession.Store(invoice); + + await theSession.SaveChangesAsync(); + + theSession.Delete(invoice); + await theSession.SaveChangesAsync(); + + (await theSession.LoadAsync(invoice.Id)) + .ShouldBeNull(); + } + + + [Theory] + [InlineData(1)] + [InlineData(1L)] + [InlineData("something")] + public async Task throw_id_mismatch_when_wrong(object id) + { + await Should.ThrowAsync(async () => await theSession.LoadAsync(id)); + } + + [Fact] + public async Task can_not_use_just_guid_as_id() + { + await Should.ThrowAsync(async () => await theSession.LoadAsync(Guid.NewGuid())); + } + + [Fact] + public async Task can_not_use_another_guid_based_strong_typed_id_as_id() + { + await Should.ThrowAsync(async () => await theSession.LoadAsync(new WrongId(Guid.NewGuid()))); + } + + [Fact] + public async Task use_in_LINQ_where_clause() + { + var invoice = new Invoice3{Name = Guid.NewGuid().ToString()}; + theSession.Store(invoice); + + await theSession.SaveChangesAsync(); + + var loaded = await theSession.Query().FirstOrDefaultAsync(x => x.Id == invoice.Id); + + loaded + .Name.ShouldBe(invoice.Name); + } + + [Fact] + public async Task use_in_LINQ_order_clause() + { + var invoice = new Invoice3{Name = Guid.NewGuid().ToString()}; + theSession.Store(invoice); + + await theSession.SaveChangesAsync(); + + var loaded = await theSession.Query().OrderBy(x => x.Id).Take(3).ToListAsync(); + } + + [Fact] + public async Task use_in_LINQ_select_clause() + { + var invoice = new Invoice3{Name = Guid.NewGuid().ToString()}; + theSession.Store(invoice); + + await theSession.SaveChangesAsync(); + + var loaded = await theSession.Query().Select(x => x.Id).Take(3).ToListAsync(); + + } + + [Fact] + public async Task bulk_writing_async() + { + Invoice3[] invoices = [ + new Invoice3{Name = Guid.NewGuid().ToString()}, + new Invoice3{Name = Guid.NewGuid().ToString()}, + new Invoice3{Name = Guid.NewGuid().ToString()}, + new Invoice3{Name = Guid.NewGuid().ToString()}, + new Invoice3{Name = Guid.NewGuid().ToString()} + ]; + + await theStore.BulkInsertDocumentsAsync(invoices); + } + + [Fact] + public void bulk_writing_sync() + { + Invoice3[] invoices = [ + new Invoice3{Name = Guid.NewGuid().ToString()}, + new Invoice3{Name = Guid.NewGuid().ToString()}, + new Invoice3{Name = Guid.NewGuid().ToString()}, + new Invoice3{Name = Guid.NewGuid().ToString()}, + new Invoice3{Name = Guid.NewGuid().ToString()} + ]; + + theStore.BulkInsertDocuments(invoices); + } + +} diff --git a/src/ValueTypeTests/StrongTypedId/int_based_document_operations.cs b/src/ValueTypeTests/StrongTypedId/int_based_document_operations.cs index 440204aa65..67ec0d79f6 100644 --- a/src/ValueTypeTests/StrongTypedId/int_based_document_operations.cs +++ b/src/ValueTypeTests/StrongTypedId/int_based_document_operations.cs @@ -272,3 +272,259 @@ public class Order2 #endregion +public class Order3 +{ + public Order2Id Id { get; set; } + public string Name { get; set; } +} + +public class int_based_document_operations_with_non_nullable_id : IAsyncLifetime +{ + private readonly DocumentStore theStore; + + public int_based_document_operations_with_non_nullable_id() + { + theStore = DocumentStore.For(opts => + { + opts.Connection(ConnectionSource.ConnectionString); + opts.DatabaseSchemaName = "strong_typed"; + }); + + theSession = theStore.LightweightSession(); + } + + public async Task InitializeAsync() + { + await theStore.Advanced.Clean.DeleteDocumentsByTypeAsync(typeof(Order3)); + } + + public async Task DisposeAsync() + { + await theStore.DisposeAsync(); + theSession?.Dispose(); + } + + private IDocumentSession theSession; + + [Fact] + public void store_document_will_assign_the_identity() + { + var order = new Order3(); + theSession.Store(order); + + order.Id.ShouldNotBeNull(); + ShouldBeTestExtensions.ShouldNotBe(order.Id.Value, 0); + } + + [Fact] + public async Task store_a_document_smoke_test() + { + var order = new Order3(); + theSession.Store(order); + + await theSession.SaveChangesAsync(); + + (await theSession.Query().AnyAsync()).ShouldBeTrue(); + } + + [Fact] + public async Task bulk_writing_async() + { + Order3[] invoices = [ + new Order3{Name = Guid.NewGuid().ToString()}, + new Order3{Name = Guid.NewGuid().ToString()}, + new Order3{Name = Guid.NewGuid().ToString()}, + new Order3{Name = Guid.NewGuid().ToString()}, + new Order3{Name = Guid.NewGuid().ToString()} + ]; + + await theStore.BulkInsertDocumentsAsync(invoices); + } + + [Fact] + public void bulk_writing_sync() + { + Order3[] invoices = [ + new Order3{Name = Guid.NewGuid().ToString()}, + new Order3{Name = Guid.NewGuid().ToString()}, + new Order3{Name = Guid.NewGuid().ToString()}, + new Order3{Name = Guid.NewGuid().ToString()}, + new Order3{Name = Guid.NewGuid().ToString()} + ]; + + theStore.BulkInsertDocuments(invoices); + } + + [Fact] + public async Task insert_a_document_smoke_test() + { + var order = new Order3(); + theSession.Insert(order); + + await theSession.SaveChangesAsync(); + + (await theSession.Query().AnyAsync()).ShouldBeTrue(); + } + + [Fact] + public async Task update_a_document_smoke_test() + { + var order = new Order3(); + theSession.Insert(order); + await theSession.SaveChangesAsync(); + + order.Name = "updated"; + await theSession.SaveChangesAsync(); + + var loaded = await theSession.LoadAsync(order.Id); + loaded.Name.ShouldBeNull("updated"); + } + + [Fact] + public async Task use_within_identity_map() + { + var order = new Order3(); + theSession.Insert(order); + await theSession.SaveChangesAsync(); + + await using var session = theStore.IdentitySession(); + var loaded1 = await session.LoadAsync(order.Id); + var loaded2 = await session.LoadAsync(order.Id); + + loaded1.ShouldBeSameAs(loaded2); + } + + [Fact] + public async Task usage_within_dirty_checking() + { + var order = new Order3(); + theSession.Insert(order); + await theSession.SaveChangesAsync(); + + await using var session = theStore.DirtyTrackedSession(); + var loaded1 = await session.LoadAsync(order.Id); + loaded1.Name = "something else"; + + await session.SaveChangesAsync(); + + var loaded2 = await theSession.LoadAsync(order.Id); + loaded2.Name.ShouldBe(loaded1.Name); + } + + [Fact] + public async Task load_document() + { + var order = new Order3{Name = Guid.NewGuid().ToString()}; + theSession.Store(order); + + await theSession.SaveChangesAsync(); + + (await theSession.LoadAsync(order.Id)) + .Name.ShouldBe(order.Name); + } + + [Fact] + public async Task load_many() + { + var order1 = new Order3{Name = Guid.NewGuid().ToString()}; + var Order3 = new Order3{Name = Guid.NewGuid().ToString()}; + var order3 = new Order3{Name = Guid.NewGuid().ToString()}; + theSession.Store(order1, Order3, order3); + + await theSession.SaveChangesAsync(); + + var results = await theSession.Query().Where(x => x.Id.IsOneOf(order1.Id, Order3.Id, order3.Id)).ToListAsync(); + results.Count.ShouldBe(3); + } + + [Fact] + public async Task delete_by_id() + { + var order = new Order3{Name = Guid.NewGuid().ToString()}; + theSession.Store(order); + + await theSession.SaveChangesAsync(); + + theSession.Delete(order.Id); + await theSession.SaveChangesAsync(); + + (await theSession.LoadAsync(order.Id)) + .ShouldBeNull(); + } + + [Fact] + public async Task delete_by_document() + { + var order = new Order3{Name = Guid.NewGuid().ToString()}; + theSession.Store(order); + + await theSession.SaveChangesAsync(); + + theSession.Delete(order); + await theSession.SaveChangesAsync(); + + (await theSession.LoadAsync(order.Id)) + .ShouldBeNull(); + } + + + [Theory] + [InlineData(1)] + [InlineData(1L)] + [InlineData("something")] + public async Task throw_id_mismatch_when_wrong(object id) + { + await Should.ThrowAsync(async () => await theSession.LoadAsync(id)); + } + + [Fact] + public async Task can_not_use_just_guid_as_id() + { + await Should.ThrowAsync(async () => await theSession.LoadAsync(Guid.NewGuid())); + } + + [Fact] + public async Task can_not_use_another_guid_based_strong_typed_id_as_id() + { + await Should.ThrowAsync(async () => await theSession.LoadAsync(new WrongId(Guid.NewGuid()))); + } + + [Fact] + public async Task use_in_LINQ_where_clause() + { + var order = new Order3{Name = Guid.NewGuid().ToString()}; + theSession.Store(order); + + await theSession.SaveChangesAsync(); + + var loaded = await theSession.Query().FirstOrDefaultAsync(x => x.Id == order.Id); + + loaded + .Name.ShouldBe(order.Name); + } + + [Fact] + public async Task use_in_LINQ_order_clause() + { + var order = new Order3{Name = Guid.NewGuid().ToString()}; + theSession.Store(order); + + await theSession.SaveChangesAsync(); + + var loaded = await theSession.Query().OrderBy(x => x.Id).Take(3).ToListAsync(); + } + + [Fact] + public async Task use_in_LINQ_select_clause() + { + var order = new Order3{Name = Guid.NewGuid().ToString()}; + theSession.Store(order); + + await theSession.SaveChangesAsync(); + + var loaded = await theSession.Query().Select(x => x.Id).Take(3).ToListAsync(); + + } +} + + diff --git a/src/ValueTypeTests/StrongTypedId/long_based_document_operations.cs b/src/ValueTypeTests/StrongTypedId/long_based_document_operations.cs index 148ed5f4e0..dcaefa0005 100644 --- a/src/ValueTypeTests/StrongTypedId/long_based_document_operations.cs +++ b/src/ValueTypeTests/StrongTypedId/long_based_document_operations.cs @@ -275,3 +275,266 @@ public class Issue2 public string Name { get; set; } } +public class Issue3 +{ + public Issue2Id Id { get; set; } + public string Name { get; set; } +} + +public class long_based_document_operations_with_non_nullable_id : IAsyncLifetime +{ + private readonly DocumentStore theStore; + + public long_based_document_operations_with_non_nullable_id() + { + theStore = DocumentStore.For(opts => + { + opts.Connection(ConnectionSource.ConnectionString); + opts.DatabaseSchemaName = "strong_typed"; + }); + + theSession = theStore.LightweightSession(); + } + + public async Task InitializeAsync() + { + await theStore.Advanced.Clean.DeleteDocumentsByTypeAsync(typeof(Issue3)); + } + + public async Task DisposeAsync() + { + await theStore.DisposeAsync(); + theSession?.Dispose(); + } + + private IDocumentSession theSession; + + [Fact] + public void store_document_will_assign_the_identity() + { + var issue = new Issue3(); + + theSession.Store(issue); + + issue.Id.ShouldNotBeNull(); + issue.Id.Value.ShouldNotBe(0); + } + + [Fact] + public async Task store_a_document_smoke_test() + { + var issue = new Issue3(); + theSession.Store(issue); + + await theSession.SaveChangesAsync(); + + (await theSession.Query().AnyAsync()).ShouldBeTrue(); + } + + [Fact] + public async Task insert_a_document_smoke_test() + { + var issue = new Issue3(); + theSession.Insert(issue); + + await theSession.SaveChangesAsync(); + + (await theSession.Query().AnyAsync()).ShouldBeTrue(); + } + + [Fact] + public async Task update_a_document_smoke_test() + { + var issue = new Issue3(); + theSession.Insert(issue); + await theSession.SaveChangesAsync(); + + issue.Name = "updated"; + await theSession.SaveChangesAsync(); + + var loaded = await theSession.LoadAsync(issue.Id); + loaded.Name.ShouldBeNull("updated"); + } + + [Fact] + public async Task use_within_identity_map() + { + var issue = new Issue3(); + theSession.Insert(issue); + await theSession.SaveChangesAsync(); + + await using var session = theStore.IdentitySession(); + var loaded1 = await session.LoadAsync(issue.Id); + var loaded2 = await session.LoadAsync(issue.Id); + + loaded1.ShouldBeSameAs(loaded2); + } + + [Fact] + public async Task usage_within_dirty_checking() + { + var issue = new Issue3(); + theSession.Insert(issue); + await theSession.SaveChangesAsync(); + + await using var session = theStore.DirtyTrackedSession(); + var loaded1 = await session.LoadAsync(issue.Id); + loaded1.Name = "something else"; + + await session.SaveChangesAsync(); + + var loaded2 = await theSession.LoadAsync(issue.Id); + loaded2.Name.ShouldBe(loaded1.Name); + } + + [Fact] + public async Task load_document() + { + var issue = new Issue3{Name = Guid.NewGuid().ToString()}; + theSession.Store(issue); + + await theSession.SaveChangesAsync(); + + (await theSession.LoadAsync(issue.Id)) + .Name.ShouldBe(issue.Name); + } + + #region sample_strong_typed_identifier_and_is_one_of + + [Fact] + public async Task load_many() + { + var issue1 = new Issue3{Name = Guid.NewGuid().ToString()}; + var Issue3 = new Issue3{Name = Guid.NewGuid().ToString()}; + var issue3 = new Issue3{Name = Guid.NewGuid().ToString()}; + theSession.Store(issue1, Issue3, issue3); + + await theSession.SaveChangesAsync(); + + var results = await theSession.Query() + .Where(x => x.Id.IsOneOf(issue1.Id, Issue3.Id, issue3.Id)) + .ToListAsync(); + + results.Count.ShouldBe(3); + } + + #endregion + + [Fact] + public async Task delete_by_id() + { + var issue = new Issue3{Name = Guid.NewGuid().ToString()}; + theSession.Store(issue); + + await theSession.SaveChangesAsync(); + + theSession.Delete(issue.Id); + await theSession.SaveChangesAsync(); + + (await theSession.LoadAsync(issue.Id)) + .ShouldBeNull(); + } + + [Fact] + public async Task delete_by_document() + { + var issue = new Issue3{Name = Guid.NewGuid().ToString()}; + theSession.Store(issue); + + await theSession.SaveChangesAsync(); + + theSession.Delete(issue); + await theSession.SaveChangesAsync(); + + (await theSession.LoadAsync(issue.Id)) + .ShouldBeNull(); + } + + + [Theory] + [InlineData(1)] + [InlineData(1L)] + [InlineData("something")] + public async Task throw_id_mismatch_when_wrong(object id) + { + await Should.ThrowAsync(async () => await theSession.LoadAsync(id)); + } + + [Fact] + public async Task can_not_use_just_guid_as_id() + { + await Should.ThrowAsync(async () => await theSession.LoadAsync(Guid.NewGuid())); + } + + [Fact] + public async Task can_not_use_another_guid_based_strong_typed_id_as_id() + { + await Should.ThrowAsync(async () => await theSession.LoadAsync(new WrongId(Guid.NewGuid()))); + } + + [Fact] + public async Task bulk_writing_async() + { + Issue3[] invoices = [ + new Issue3{Name = Guid.NewGuid().ToString()}, + new Issue3{Name = Guid.NewGuid().ToString()}, + new Issue3{Name = Guid.NewGuid().ToString()}, + new Issue3{Name = Guid.NewGuid().ToString()}, + new Issue3{Name = Guid.NewGuid().ToString()} + ]; + + await theStore.BulkInsertDocumentsAsync(invoices); + } + + [Fact] + public void bulk_writing_sync() + { + Issue3[] invoices = [ + new Issue3{Name = Guid.NewGuid().ToString()}, + new Issue3{Name = Guid.NewGuid().ToString()}, + new Issue3{Name = Guid.NewGuid().ToString()}, + new Issue3{Name = Guid.NewGuid().ToString()}, + new Issue3{Name = Guid.NewGuid().ToString()} + ]; + + theStore.BulkInsertDocuments(invoices); + } + + [Fact] + public async Task use_in_LINQ_where_clause() + { + var issue = new Issue3{Name = Guid.NewGuid().ToString()}; + theSession.Store(issue); + + await theSession.SaveChangesAsync(); + + var loaded = await theSession.Query().FirstOrDefaultAsync(x => x.Id == issue.Id); + + loaded + .Name.ShouldBe(issue.Name); + } + + [Fact] + public async Task use_in_LINQ_issue_clause() + { + var issue = new Issue3{Name = Guid.NewGuid().ToString()}; + theSession.Store(issue); + + await theSession.SaveChangesAsync(); + + var loaded = await theSession.Query().OrderBy(x => x.Id).Take(3).ToListAsync(); + } + + [Fact] + public async Task use_in_LINQ_select_clause() + { + var issue = new Issue3{Name = Guid.NewGuid().ToString()}; + theSession.Store(issue); + + await theSession.SaveChangesAsync(); + + var loaded = await theSession.Query().Select(x => x.Id).Take(3).ToListAsync(); + + } +} + diff --git a/src/ValueTypeTests/StrongTypedId/string_id_document_operations.cs b/src/ValueTypeTests/StrongTypedId/string_id_document_operations.cs index f3b2af1dd8..f1a8c1415a 100644 --- a/src/ValueTypeTests/StrongTypedId/string_id_document_operations.cs +++ b/src/ValueTypeTests/StrongTypedId/string_id_document_operations.cs @@ -251,3 +251,236 @@ public class Team2 public string Name { get; set; } } +public class Team3 +{ + // Marten will use this for the identifier + // of the Team document + public Team2Id Id { get; set; } + public string Name { get; set; } +} + +public class string_id_document_operations_with_non_nullable_id : IDisposable, IAsyncDisposable +{ + private readonly DocumentStore theStore; + + public string_id_document_operations_with_non_nullable_id() + { + theStore = DocumentStore.For(opts => + { + opts.Connection(ConnectionSource.ConnectionString); + opts.DatabaseSchemaName = "strong_typed"; + + opts.ApplicationAssembly = GetType().Assembly; + opts.GeneratedCodeMode = TypeLoadMode.Auto; + opts.GeneratedCodeOutputPath = + AppContext.BaseDirectory.ParentDirectory().ParentDirectory().ParentDirectory().AppendPath("Internal", "Generated"); + }); + + theSession = theStore.LightweightSession(); + } + + public void Dispose() + { + theStore?.Dispose(); + theSession?.Dispose(); + } + + private IDocumentSession theSession; + + public async ValueTask DisposeAsync() + { + if (theStore != null) + { + await theStore.DisposeAsync(); + } + } + + [Fact] + public async Task store_a_document_smoke_test() + { + var team = new Team3{Id = new Team2Id(Guid.NewGuid().ToString())}; + theSession.Store(team); + + await theSession.SaveChangesAsync(); + + (await theSession.Query().AnyAsync()).ShouldBeTrue(); + } + + [Fact] + public async Task insert_a_document_smoke_test() + { + var team = new Team3{Id = new Team2Id(Guid.NewGuid().ToString())}; + theSession.Insert(team); + + await theSession.SaveChangesAsync(); + + (await theSession.Query().AnyAsync()).ShouldBeTrue(); + } + + [Fact] + public async Task update_a_document_smoke_test() + { + var team = new Team3{Id = new Team2Id(Guid.NewGuid().ToString())}; + theSession.Insert(team); + await theSession.SaveChangesAsync(); + + team.Name = "updated"; + await theSession.SaveChangesAsync(); + + var loaded = await theSession.LoadAsync(team.Id); + loaded.Name.ShouldBeNull("updated"); + } + + [Fact] + public async Task use_within_identity_map() + { + var team = new Team3{Id = new Team2Id(Guid.NewGuid().ToString())}; + theSession.Insert(team); + await theSession.SaveChangesAsync(); + + await using var session = theStore.IdentitySession(); + var loaded1 = await session.LoadAsync(team.Id); + var loaded2 = await session.LoadAsync(team.Id); + + loaded1.ShouldBeSameAs(loaded2); + } + + [Fact] + public async Task usage_within_dirty_checking() + { + var team = new Team3{Id = new Team2Id(Guid.NewGuid().ToString())}; + theSession.Insert(team); + await theSession.SaveChangesAsync(); + + await using var session = theStore.DirtyTrackedSession(); + var loaded1 = await session.LoadAsync(team.Id); + loaded1.Name = "something else"; + + await session.SaveChangesAsync(); + + var loaded2 = await theSession.LoadAsync(team.Id); + loaded2.Name.ShouldBe(loaded1.Name); + } + + [Fact] + public async Task load_document() + { + var team = new Team3{Name = Guid.NewGuid().ToString(), Id = new Team2Id(Guid.NewGuid().ToString())}; + theSession.Store(team); + + await theSession.SaveChangesAsync(); + + (await theSession.LoadAsync(team.Id)) + .Name.ShouldBe(team.Name); + } + + [Fact] + public async Task load_many() + { + var team1 = new Team3{Name = Guid.NewGuid().ToString(), Id = new Team2Id(Guid.NewGuid().ToString())}; + var Team3 = new Team3{Name = Guid.NewGuid().ToString(), Id = new Team2Id(Guid.NewGuid().ToString())}; + var team3 = new Team3{Name = Guid.NewGuid().ToString(), Id = new Team2Id(Guid.NewGuid().ToString())}; + theSession.Store(team1, Team3, team3); + + await theSession.SaveChangesAsync(); + + var results = await theSession + .Query() + .Where(x => x.Id.IsOneOf(team1.Id, Team3.Id, team3.Id)) + .ToListAsync(); + + results.Count.ShouldBe(3); + } + + [Fact] + public async Task delete_by_id() + { + var team = new Team3{Name = Guid.NewGuid().ToString(), Id = new Team2Id(Guid.NewGuid().ToString())}; + theSession.Store(team); + + await theSession.SaveChangesAsync(); + + theSession.Delete(team.Id); + await theSession.SaveChangesAsync(); + + (await theSession.LoadAsync(team.Id)) + .ShouldBeNull(); + } + + [Fact] + public async Task delete_by_document() + { + var team = new Team3{Name = Guid.NewGuid().ToString(), Id = new Team2Id(Guid.NewGuid().ToString())}; + theSession.Store(team); + + await theSession.SaveChangesAsync(); + + theSession.Delete(team); + await theSession.SaveChangesAsync(); + + (await theSession.LoadAsync(team.Id)) + .ShouldBeNull(); + } + + + [Theory] + [InlineData(1)] + [InlineData(1L)] + [InlineData("something")] + public async Task throw_id_mismatch_when_wrong(object id) + { + await Should.ThrowAsync(async () => await theSession.LoadAsync(id)); + } + + [Fact] + public async Task can_not_use_just_string_as_id() + { + await Should.ThrowAsync(async () => await theSession.LoadAsync("something")); + } + + [Fact] + public async Task can_not_use_another_string_based_strong_typed_id_as_id() + { + await Should.ThrowAsync(async () => await theSession.LoadAsync(new WrongStringId(Guid.NewGuid().ToString()))); + } + + [Fact] + public async Task use_in_LINQ_where_clause() + { + var team = new Team3{Name = Guid.NewGuid().ToString(), Id = new Team2Id(Guid.NewGuid().ToString())}; + theSession.Store(team); + + await theSession.SaveChangesAsync(); + + var loaded = await theSession.Query().FirstOrDefaultAsync(x => x.Id == team.Id); + + loaded + .Name.ShouldBe(team.Name); + } + + [Fact] + public async Task use_in_LINQ_order_clause() + { + var team = new Team3{Name = Guid.NewGuid().ToString(), Id = new Team2Id(Guid.NewGuid().ToString())}; + theSession.Store(team); + + await theSession.SaveChangesAsync(); + + var loaded = await theSession.Query().OrderBy(x => x.Id).Take(3).ToListAsync(); + } + + [Fact] + public async Task use_in_LINQ_select_clause() + { + var team = new Team3{Name = Guid.NewGuid().ToString(), Id = new Team2Id(Guid.NewGuid().ToString())}; + theSession.Store(team); + + await theSession.SaveChangesAsync(); + + var loaded = await theSession.Query().Select(x => x.Id).Take(3).ToListAsync(); + + } + +} + +