diff --git a/src/Marten/StoreOptions.Identity.cs b/src/Marten/StoreOptions.Identity.cs index 7108b5aba0..cf031b33ff 100644 --- a/src/Marten/StoreOptions.Identity.cs +++ b/src/Marten/StoreOptions.Identity.cs @@ -147,8 +147,14 @@ public ValueTypeInfo RegisterValueType(Type type) return valueType; } - var builder = type.GetMethods(BindingFlags.Static | BindingFlags.Public).FirstOrDefault(x => - x.GetParameters().Length == 1 && x.GetParameters()[0].ParameterType == valueProperty.PropertyType); + var candidateBuilders = type.GetMethods(BindingFlags.Static | BindingFlags.Public).Where(x => + { + var parameters = x.GetParameters(); + return parameters.Length == 1 && parameters[0].ParameterType == valueProperty.PropertyType; + }).ToArray(); + + var builder = candidateBuilders.FirstOrDefault(x => x.ReturnType == type) + ?? candidateBuilders.FirstOrDefault(); if (builder != null) { diff --git a/src/ValueTypeTests/Bugs/Bug_4288_value_type_with_nullable_factory.cs b/src/ValueTypeTests/Bugs/Bug_4288_value_type_with_nullable_factory.cs new file mode 100644 index 0000000000..373716c854 --- /dev/null +++ b/src/ValueTypeTests/Bugs/Bug_4288_value_type_with_nullable_factory.cs @@ -0,0 +1,66 @@ +using System; +using System.Linq; +using System.Threading.Tasks; +using Marten; +using Marten.Testing.Harness; +using Shouldly; +using Vogen; + +namespace ValueTypeTests.Bugs; + +public class Bug_4288_value_type_with_nullable_factory : BugIntegrationContext +{ + [Fact] + public void can_generate_ddl_for_index_over_value_type_with_nullable_sibling_factory() + { + // Adding a `static FromNullable(string?)` sibling to a Vogen value object used to make + // Marten select that method as the value type's builder, which then crashed in + // ValueTypeInfo.CreateWrapper because the return type is Nullable, not + // Bug4288Value. The crash surfaced when generating DDL for any computed index over + // the value type. + StoreOptions(opts => + { + opts.RegisterValueType(); + opts.Schema.For().Index(x => x.Value); + }); + + Should.NotThrow(() => theStore.Storage.ToDatabaseScript()); + } + + [Fact] + public async Task can_round_trip_and_query_value_type_with_nullable_sibling_factory() + { + StoreOptions(opts => + { + opts.RegisterValueType(); + }); + + var doc = new Bug4288Doc { Value = Bug4288Value.From("abc") }; + theSession.Store(doc); + await theSession.SaveChangesAsync(); + + var key = Bug4288Value.From("abc"); + var found = await theSession.Query() + .Where(x => x.Value == key) + .ToListAsync(); + + found.Count.ShouldBe(1); + found.Single().Id.ShouldBe(doc.Id); + } +} + +[ValueObject] +public readonly partial struct Bug4288Value +{ + private static Validation Validate(string value) + => string.IsNullOrWhiteSpace(value) ? Validation.Invalid("Cannot be empty.") : Validation.Ok; + + public static Bug4288Value? FromNullable(string? value) + => value is null ? null : From(value); +} + +public class Bug4288Doc +{ + public Guid Id { get; set; } + public Bug4288Value? Value { get; set; } +} diff --git a/src/ValueTypeTests/registration.cs b/src/ValueTypeTests/registration.cs index daed2b77ff..ac8ce8e2fc 100644 --- a/src/ValueTypeTests/registration.cs +++ b/src/ValueTypeTests/registration.cs @@ -26,6 +26,15 @@ public void register_happy_path_with_static_method() value.ValueProperty.Name.ShouldBe("Value"); } + [Fact] + public void picks_builder_whose_return_type_matches_value_type() + { + var options = new StoreOptions(); + var value = options.RegisterValueType(typeof(SpecialValueWithNullableSibling)); + value.Builder.Name.ShouldBe("From"); + value.Builder.ReturnType.ShouldBe(typeof(SpecialValueWithNullableSibling)); + } + [Theory] [InlineData(typeof(NotValidId))] [InlineData(typeof(DefinitelyNotValid))] @@ -53,6 +62,21 @@ private SpecialValue(string value) public static SpecialValue From(string value) => new SpecialValue(value); } +public readonly struct SpecialValueWithNullableSibling +{ + private SpecialValueWithNullableSibling(string value) + { + Value = value; + } + + public string Value { get; } + + public static SpecialValueWithNullableSibling? FromNullable(string? value) + => value is null ? null : From(value); + + public static SpecialValueWithNullableSibling From(string value) => new(value); +} + public class NotValidId(string Value); public class DefinitelyNotValid;