diff --git a/.nuke/build.schema.json b/.nuke/build.schema.json index d1eb44aa8..de20c17b2 100644 --- a/.nuke/build.schema.json +++ b/.nuke/build.schema.json @@ -18,6 +18,12 @@ "type": "boolean", "description": "Indicates to continue a previously failed build attempt" }, + "Filter": { + "type": "string" + }, + "Framework": { + "type": "string" + }, "GithubToken": { "type": "string" }, diff --git a/build/Build.cs b/build/Build.cs index 39bb2c919..fa309a7b9 100644 --- a/build/Build.cs +++ b/build/Build.cs @@ -48,6 +48,8 @@ class Build : NukeBuild [Parameter] readonly string GithubToken; [Parameter] readonly string NuGetToken; [Parameter] readonly AbsolutePath PackagesDirectory = RootDirectory / "packages"; + [Parameter] readonly string Filter; + [Parameter] readonly string Framework; const string NugetOrgUrl = "https://api.nuget.org/v3/index.json"; bool IsTag => GitHubActions.Instance?.GitHubRef?.StartsWith("refs/tags/") ?? false; @@ -88,6 +90,8 @@ class Build : NukeBuild DotNetTest(s => s .SetProjectFile(Solution) .SetConfiguration(Configuration) + .When(!string.IsNullOrEmpty(Filter), x => x.SetFilter(Filter)) + .When(!string.IsNullOrEmpty(Framework), x => x.SetFramework(Framework)) .EnableNoBuild() .EnableNoRestore()); }); diff --git a/src/StronglyTypedIds.Attributes/StronglyTypedIdConvertersAttribute.cs b/src/StronglyTypedIds.Attributes/StronglyTypedIdConvertersAttribute.cs new file mode 100644 index 000000000..05de31caa --- /dev/null +++ b/src/StronglyTypedIds.Attributes/StronglyTypedIdConvertersAttribute.cs @@ -0,0 +1,38 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by the StronglyTypedId source generator +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +#pragma warning disable 1591 // publicly visible type or member must be documented + +#nullable enable + +namespace StronglyTypedIds +{ + /// + /// Place on partial structs to generate converters for a strongly-typed ID, . + /// + [global::System.AttributeUsage(global::System.AttributeTargets.Struct, Inherited = false, AllowMultiple = false)] + [global::System.Diagnostics.Conditional("STRONGLY_TYPED_ID_USAGES")] + public sealed class StronglyTypedIdConvertersAttribute : global::System.Attribute + where T: struct + { + /// + /// Generate converters for a strongly typed ID + /// + /// The names of the template to use to generate the converters. + /// Templates must be added to the project using the format NAME.typedid, + /// where NAME is the name of the template passed in . + /// If no templates are provided, the default value is used, as specified by + /// + /// The StronglyTyped ID implementation for which these converters are associated + /// + public StronglyTypedIdConvertersAttribute(params string[] templateNames) + { + } + } +} \ No newline at end of file diff --git a/src/StronglyTypedIds.Attributes/StronglyTypedIdConvertersDefaultsAttribute.cs b/src/StronglyTypedIds.Attributes/StronglyTypedIdConvertersDefaultsAttribute.cs new file mode 100644 index 000000000..f75b9ab4e --- /dev/null +++ b/src/StronglyTypedIds.Attributes/StronglyTypedIdConvertersDefaultsAttribute.cs @@ -0,0 +1,49 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by the StronglyTypedId source generator +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +#pragma warning disable 1591 // publicly visible type or member must be documented + +#nullable enable + +namespace StronglyTypedIds +{ + /// + /// Used to control the default templates to use with instances + /// + [global::System.AttributeUsage(global::System.AttributeTargets.Assembly, Inherited = false, AllowMultiple = false)] + [global::System.Diagnostics.Conditional("STRONGLY_TYPED_ID_USAGES")] + public sealed class StronglyTypedIdConvertersDefaultsAttribute : global::System.Attribute + { + /// + /// Set the default template to use for strongly typed ID converter types + /// + /// The name of the template to use to generate the converter type. + /// Templates must be added to the project using the format NAME.typedid, + /// where NAME is the name of the template passed in . + /// + public StronglyTypedIdConvertersDefaultsAttribute(string templateName) + { + } + + /// + /// Set the default templates to use for strongly typed ID converter types + /// + /// The name of the template to use to generate the converter type. + /// Templates must be added to the project using the format NAME.typedid, + /// where NAME is the name of the template passed in . + /// + /// The names of additional custom templates to use to generate the converter type. + /// Templates must be added to the project using the format NAME.typedid, + /// where NAME is the name of the template passed in . + /// + public StronglyTypedIdConvertersDefaultsAttribute(string templateName, params string[] templateNames) + { + } + } +} \ No newline at end of file diff --git a/src/StronglyTypedIds.Templates/guid-dapper.typedid b/src/StronglyTypedIds.Templates/guid-dapper.typedid index 2453e2c83..36ab5c4fa 100644 --- a/src/StronglyTypedIds.Templates/guid-dapper.typedid +++ b/src/StronglyTypedIds.Templates/guid-dapper.typedid @@ -1,4 +1,4 @@ - partial struct PLACEHOLDERID + partial struct TARGETTYPE { public partial class DapperTypeHandler : global::Dapper.SqlMapper.TypeHandler { diff --git a/src/StronglyTypedIds.Templates/guid-efcore.typedid b/src/StronglyTypedIds.Templates/guid-efcore.typedid index f6a59d0a5..055614151 100644 --- a/src/StronglyTypedIds.Templates/guid-efcore.typedid +++ b/src/StronglyTypedIds.Templates/guid-efcore.typedid @@ -1,4 +1,4 @@ - partial struct PLACEHOLDERID + partial struct TARGETTYPE { public partial class EfCoreValueConverter : global::Microsoft.EntityFrameworkCore.Storage.ValueConversion.ValueConverter { diff --git a/src/StronglyTypedIds.Templates/int-dapper.typedid b/src/StronglyTypedIds.Templates/int-dapper.typedid index 8a10bf16f..23c03aad2 100644 --- a/src/StronglyTypedIds.Templates/int-dapper.typedid +++ b/src/StronglyTypedIds.Templates/int-dapper.typedid @@ -1,4 +1,4 @@ - partial struct PLACEHOLDERID + partial struct TARGETTYPE { public partial class DapperTypeHandler : global::Dapper.SqlMapper.TypeHandler { diff --git a/src/StronglyTypedIds.Templates/int-efcore.typedid b/src/StronglyTypedIds.Templates/int-efcore.typedid index c1f8fb3c6..85c5b4bae 100644 --- a/src/StronglyTypedIds.Templates/int-efcore.typedid +++ b/src/StronglyTypedIds.Templates/int-efcore.typedid @@ -1,4 +1,4 @@ - partial struct PLACEHOLDERID + partial struct TARGETTYPE { public partial class EfCoreValueConverter : global::Microsoft.EntityFrameworkCore.Storage.ValueConversion.ValueConverter { diff --git a/src/StronglyTypedIds.Templates/long-dapper.typedid b/src/StronglyTypedIds.Templates/long-dapper.typedid index 9466a251e..9a003d5a5 100644 --- a/src/StronglyTypedIds.Templates/long-dapper.typedid +++ b/src/StronglyTypedIds.Templates/long-dapper.typedid @@ -1,4 +1,4 @@ - partial struct PLACEHOLDERID + partial struct TARGETTYPE { public partial class DapperTypeHandler : global::Dapper.SqlMapper.TypeHandler { diff --git a/src/StronglyTypedIds.Templates/long-efcore.typedid b/src/StronglyTypedIds.Templates/long-efcore.typedid index ef88f8b7d..4836d4226 100644 --- a/src/StronglyTypedIds.Templates/long-efcore.typedid +++ b/src/StronglyTypedIds.Templates/long-efcore.typedid @@ -1,4 +1,4 @@ - partial struct PLACEHOLDERID + partial struct TARGETTYPE { public partial class EfCoreValueConverter : global::Microsoft.EntityFrameworkCore.Storage.ValueConversion.ValueConverter { diff --git a/src/StronglyTypedIds.Templates/string-dapper.typedid b/src/StronglyTypedIds.Templates/string-dapper.typedid index 4af050343..7071db1ad 100644 --- a/src/StronglyTypedIds.Templates/string-dapper.typedid +++ b/src/StronglyTypedIds.Templates/string-dapper.typedid @@ -1,4 +1,4 @@ - partial struct PLACEHOLDERID + partial struct TARGETTYPE { public partial class DapperTypeHandler : global::Dapper.SqlMapper.TypeHandler { diff --git a/src/StronglyTypedIds.Templates/string-efcore.typedid b/src/StronglyTypedIds.Templates/string-efcore.typedid index 6fd5fc403..a1f03d2af 100644 --- a/src/StronglyTypedIds.Templates/string-efcore.typedid +++ b/src/StronglyTypedIds.Templates/string-efcore.typedid @@ -1,4 +1,4 @@ - partial struct PLACEHOLDERID + partial struct TARGETTYPE { public partial class EfCoreValueConverter : global::Microsoft.EntityFrameworkCore.Storage.ValueConversion.ValueConverter { diff --git a/src/StronglyTypedIds/Diagnostics/MissingDefaultsDiagnostic.cs b/src/StronglyTypedIds/Diagnostics/MissingDefaultsDiagnostic.cs new file mode 100644 index 000000000..d7843580b --- /dev/null +++ b/src/StronglyTypedIds/Diagnostics/MissingDefaultsDiagnostic.cs @@ -0,0 +1,16 @@ +using Microsoft.CodeAnalysis; + +namespace StronglyTypedIds.Diagnostics; + +internal static class MissingDefaultsDiagnostic +{ + internal const string Id = "STRONGID005"; + internal const string Message = "You must specify the default template to use for converters using [StronglyTypedIdConvertersDefaults]"; + internal const string Title = "Missing [StronglyTypedIdConvertersDefaults] attribute"; + + public static DiagnosticInfo CreateInfo(Location location) => + new(new DiagnosticDescriptor( + Id, Title, Message, category: Constants.Usage, defaultSeverity: DiagnosticSeverity.Warning, + isEnabledByDefault: true), + location); +} \ No newline at end of file diff --git a/src/StronglyTypedIds/EmbeddedSources.cs b/src/StronglyTypedIds/EmbeddedSources.cs index e54f812ab..7eca7a334 100644 --- a/src/StronglyTypedIds/EmbeddedSources.cs +++ b/src/StronglyTypedIds/EmbeddedSources.cs @@ -10,6 +10,8 @@ internal static partial class EmbeddedSources private static readonly Assembly ThisAssembly = typeof(EmbeddedSources).Assembly; internal static readonly string StronglyTypedIdAttributeSource = LoadAttributeTemplateForEmitting("StronglyTypedIdAttribute"); internal static readonly string StronglyTypedIdDefaultsAttributeSource = LoadAttributeTemplateForEmitting("StronglyTypedIdDefaultsAttribute"); + internal static readonly string StronglyTypedIdConvertersAttributeSource = LoadAttributeTemplateForEmitting("StronglyTypedIdConvertersAttribute"); + internal static readonly string StronglyTypedIdConvertersDefaultsAttributeSource = LoadAttributeTemplateForEmitting("StronglyTypedIdConvertersDefaultsAttribute"); internal static readonly string TemplateSource = LoadAttributeTemplateForEmitting("Template"); internal static readonly string AutoGeneratedHeader = LoadEmbeddedResource("StronglyTypedIds.Templates.AutoGeneratedHeader.cs"); diff --git a/src/StronglyTypedIds/Parser.cs b/src/StronglyTypedIds/Parser.cs index 2f4162f89..1eb6342dc 100644 --- a/src/StronglyTypedIds/Parser.cs +++ b/src/StronglyTypedIds/Parser.cs @@ -12,8 +12,10 @@ internal static class Parser { public const string StronglyTypedIdAttribute = "StronglyTypedIds.StronglyTypedIdAttribute"; public const string StronglyTypedIdDefaultsAttribute = "StronglyTypedIds.StronglyTypedIdDefaultsAttribute"; + public const string StronglyTypedIdConvertersAttribute = "StronglyTypedIds.StronglyTypedIdConvertersAttribute`1"; + public const string StronglyTypedIdConvertersDefaultsAttribute = "StronglyTypedIds.StronglyTypedIdConvertersDefaultsAttribute"; - public static Result<(StructToGenerate info, bool valid)> GetStructSemanticTarget(GeneratorAttributeSyntaxContext ctx, CancellationToken ct) + public static Result<(StructToGenerate info, bool valid)> GetIdSemanticTarget(GeneratorAttributeSyntaxContext ctx, CancellationToken ct) { var structSymbol = ctx.TargetSymbol as INamedTypeSymbol; if (structSymbol is null) @@ -88,7 +90,7 @@ internal static class Parser return new Result<(StructToGenerate, bool)>((toGenerate, true), errors); } - public static Result<(Defaults defaults, bool valid)> GetDefaults( + public static Result<(Defaults defaults, bool valid)> GetIdDefaults( GeneratorAttributeSyntaxContext ctx, CancellationToken ct) { var assemblyAttributes = ctx.TargetSymbol.GetAttributes(); @@ -147,6 +149,147 @@ internal static class Parser } var defaults = new Defaults(template, templateNames, attributeLocation!, hasMultiple); + return new Result<(Defaults, bool)>((defaults, true), errors); + } + + public static Result<(ConverterToGenerate info, bool valid)> GetConvertersSemanticTarget(GeneratorAttributeSyntaxContext ctx, CancellationToken ct) + { + var structSymbol = ctx.TargetSymbol as INamedTypeSymbol; + if (structSymbol is null) + { + return Result.Fail(); + } + + var structSyntax = (StructDeclarationSyntax)ctx.TargetNode; + + var hasMisconfiguredInput = false; + List? diagnostics = null; + string[]? templateNames = null; + LocationInfo? attributeLocation = null; + string? idName = null; + + foreach (AttributeData attribute in structSymbol.GetAttributes()) + { + if (!((attribute.AttributeClass?.Name == "StronglyTypedIdConvertersAttribute" || + attribute.AttributeClass?.Name == "StronglyTypedIdConverters") && + attribute.AttributeClass.IsGenericType && + attribute.AttributeClass.TypeArguments.Length == 1)) + { + // wrong attribute + continue; + } + + // Can never have template + (var result, (_, templateNames)) = GetConstructorValues(attribute); + hasMisconfiguredInput |= result; + + if (attribute.ApplicationSyntaxReference?.GetSyntax() is { } s) + { + attributeLocation = LocationInfo.CreateFrom(s); + } + + var typeParameter = attribute.AttributeClass.TypeArguments[0]; + idName = typeParameter.ToString(); + } + + var hasPartialModifier = false; + foreach (var modifier in structSyntax.Modifiers) + { + if (modifier.IsKind(SyntaxKind.PartialKeyword)) + { + hasPartialModifier = true; + break; + } + } + + if (!hasPartialModifier) + { + diagnostics ??= new(); + diagnostics.Add(NotPartialDiagnostic.CreateInfo(structSyntax)); + } + + var errors = diagnostics is null + ? EquatableArray.Empty + : new EquatableArray(diagnostics.ToArray()); + + if (hasMisconfiguredInput || idName is null) + { + return new Result<(ConverterToGenerate, bool)>((default, false), errors); + } + + string nameSpace = GetNameSpace(structSyntax); + ParentClass? parentClass = GetParentClasses(structSyntax); + var name = structSymbol.Name; + + var toGenerate = new ConverterToGenerate( + name: name, + nameSpace: nameSpace, + idName: idName, + templateNames: templateNames, + templateLocation: attributeLocation!, + parent: parentClass); + + return new Result<(ConverterToGenerate, bool)>((toGenerate, true), errors); + } + + public static Result<(Defaults defaults, bool valid)> GetConverterDefaults( + GeneratorAttributeSyntaxContext ctx, CancellationToken ct) + { + var assemblyAttributes = ctx.TargetSymbol.GetAttributes(); + if (assemblyAttributes.IsDefaultOrEmpty) + { + return Result.Fail(); + } + + // We only return the first config that we find + string[]? templateNames = null; + LocationInfo? attributeLocation = null; + List? diagnostics = null; + bool hasMisconfiguredInput = false; + bool hasMultiple = false; + + // if we have multiple attributes we still check them, so that we can add extra diagnostics if necessary + // the "first" one found won't be flagged as a duplicate though. + foreach (AttributeData attribute in assemblyAttributes) + { + if (!((attribute.AttributeClass?.Name == "StronglyTypedIdConvertersDefaultsAttribute" || + attribute.AttributeClass?.Name == "StronglyTypedIdConvertersDefaults") && + attribute.AttributeClass.ToDisplayString() == StronglyTypedIdConvertersDefaultsAttribute)) + { + // wrong attribute + continue; + } + + var syntax = attribute.ApplicationSyntaxReference?.GetSyntax(); + if (templateNames is not null || hasMisconfiguredInput) + { + hasMultiple = true; + if (syntax is not null) + { + diagnostics ??= new(); + diagnostics.Add(MultipleAssemblyAttributeDiagnostic.CreateInfo(syntax)); + } + } + + (var result, (_, templateNames)) = GetConstructorValues(attribute); + hasMisconfiguredInput |= result; + + if (syntax is not null) + { + attributeLocation = LocationInfo.CreateFrom(syntax); + } + } + + var errors = diagnostics is null + ? EquatableArray.Empty + : new EquatableArray(diagnostics.ToArray()); + + if (hasMisconfiguredInput) + { + return new Result<(Defaults, bool)>((default, false), errors); + } + + var defaults = new Defaults(template: null, templateNames, attributeLocation!, hasMultiple); return new Result<(Defaults, bool)>((defaults, true), errors); } diff --git a/src/StronglyTypedIds/SourceGenerationHelper.cs b/src/StronglyTypedIds/SourceGenerationHelper.cs index 5bebbee85..bb47c13d1 100644 --- a/src/StronglyTypedIds/SourceGenerationHelper.cs +++ b/src/StronglyTypedIds/SourceGenerationHelper.cs @@ -6,7 +6,8 @@ namespace StronglyTypedIds internal static class SourceGenerationHelper { public static string CreateId( - string idNamespace, + string targetNamespace, + string targetName, string idName, ParentClass? parentClass, string template, @@ -14,12 +15,12 @@ public static string CreateId( bool addGeneratedCodeAttribute, StringBuilder? sb) { - if (string.IsNullOrEmpty(idName)) + if (string.IsNullOrEmpty(targetName)) { - throw new ArgumentException("Value cannot be null or empty.", nameof(idName)); + throw new ArgumentException("Value cannot be null or empty.", nameof(targetName)); } - var hasNamespace = !string.IsNullOrEmpty(idNamespace); + var hasNamespace = !string.IsNullOrEmpty(targetNamespace); var parentsCount = 0; @@ -38,7 +39,7 @@ public static string CreateId( { sb .Append("namespace ") - .Append(idNamespace) + .Append(targetNamespace) .AppendLine(@" {"); } @@ -85,6 +86,7 @@ public static string CreateId( sb.AppendLine(template); + sb.Replace("TARGETTYPE", targetName); sb.Replace("PLACEHOLDERID", idName); for (int i = 0; i < parentsCount; i++) diff --git a/src/StronglyTypedIds/StronglyTypedIdGenerator.cs b/src/StronglyTypedIds/StronglyTypedIdGenerator.cs index 2801dc58c..a71a0c0a3 100644 --- a/src/StronglyTypedIds/StronglyTypedIdGenerator.cs +++ b/src/StronglyTypedIds/StronglyTypedIdGenerator.cs @@ -26,9 +26,12 @@ public void Initialize(IncrementalGeneratorInitializationContext context) { i.AddSource("StronglyTypedIdAttribute.g.cs", EmbeddedSources.StronglyTypedIdAttributeSource); i.AddSource("StronglyTypedIdDefaultsAttribute.g.cs", EmbeddedSources.StronglyTypedIdDefaultsAttributeSource); + i.AddSource("StronglyTypedIdConvertersAttribute.g.cs", EmbeddedSources.StronglyTypedIdConvertersAttributeSource); + i.AddSource("StronglyTypedIdConvertersDefaultsAttribute.g.cs", EmbeddedSources.StronglyTypedIdConvertersDefaultsAttributeSource); i.AddSource("Template.g.cs", EmbeddedSources.TemplateSource); }); + // Templates IncrementalValuesProvider<(string Path, string Name, string? Content)> allTemplates = context.AdditionalTextsProvider .Where(template => Path.GetExtension(template.Path).Equals(TemplateSuffix, StringComparison.OrdinalIgnoreCase)) .Select((template, ct) => ( @@ -39,48 +42,95 @@ public void Initialize(IncrementalGeneratorInitializationContext context) var templates = allTemplates .Where(template => !string.IsNullOrWhiteSpace(template.Name) && template.Content is not null) .Collect(); - - IncrementalValuesProvider> structAndDiagnostics = context.SyntaxProvider - .ForAttributeWithMetadataName( - Parser.StronglyTypedIdAttribute, - predicate: (node, _) => node is StructDeclarationSyntax, - transform: Parser.GetStructSemanticTarget) - .Where(static m => m is not null); - IncrementalValuesProvider> defaultsAndDiagnostics = context.SyntaxProvider + // ID defaults + IncrementalValuesProvider> idDefaultsAndDiagnostics = context.SyntaxProvider .ForAttributeWithMetadataName( Parser.StronglyTypedIdDefaultsAttribute, predicate: (node, _) => node is CompilationUnitSyntax, - transform: Parser.GetDefaults) + transform: Parser.GetIdDefaults) .Where(static m => m is not null); - context.RegisterSourceOutput( - structAndDiagnostics.SelectMany((x, _) => x.Errors), - static (context, info) => context.ReportDiagnostic(info)); + IncrementalValueProvider<(EquatableArray<(string Name, string Content)> Content, bool isValid, DiagnosticInfo? Diagnostic)> idDefaultTemplateContent = idDefaultsAndDiagnostics + .Where(static x => x.Value.valid) + .Select((result, _) => result.Value.defaults) + .Collect() + .Combine(templates) + .Select(ProcessIdDefaults); context.RegisterSourceOutput( - defaultsAndDiagnostics.SelectMany((x, _) => x.Errors), + idDefaultsAndDiagnostics.SelectMany((x, _) => x.Errors), static (context, info) => context.ReportDiagnostic(info)); - IncrementalValuesProvider structs = structAndDiagnostics + // IDs + IncrementalValuesProvider> idsAndDiagnostics = context.SyntaxProvider + .ForAttributeWithMetadataName( + Parser.StronglyTypedIdAttribute, + predicate: (node, _) => node is StructDeclarationSyntax, + transform: Parser.GetIdSemanticTarget) + .Where(static m => m is not null); + + IncrementalValuesProvider ids = idsAndDiagnostics .Where(static x => x.Value.valid) .Select((result, _) => result.Value.info); - IncrementalValueProvider<(EquatableArray<(string Name, string Content)> Content, bool isValid, DiagnosticInfo? Diagnostic)> defaultTemplateContent = defaultsAndDiagnostics + context.RegisterSourceOutput( + idsAndDiagnostics.SelectMany((x, _) => x.Errors), + static (context, info) => context.ReportDiagnostic(info)); + + // Converter defaults + IncrementalValuesProvider> converterDefaultsAndDiagnostics = context.SyntaxProvider + .ForAttributeWithMetadataName( + Parser.StronglyTypedIdConvertersDefaultsAttribute, + predicate: (node, _) => node is CompilationUnitSyntax, + transform: Parser.GetConverterDefaults) + .Where(static m => m is not null); + + IncrementalValueProvider<(EquatableArray<(string Name, string Content)> Content, bool isValid, DiagnosticInfo? Diagnostic)> converterDefaultTemplateContent = converterDefaultsAndDiagnostics .Where(static x => x.Value.valid) .Select((result, _) => result.Value.defaults) .Collect() .Combine(templates) - .Select(ProcessDefaults); + .Select(ProcessConverterDefaults); + + context.RegisterSourceOutput( + converterDefaultsAndDiagnostics.SelectMany((x, _) => x.Errors), + static (context, info) => context.ReportDiagnostic(info)); + + // Converters + IncrementalValuesProvider> convertersAndDiagnostics = context.SyntaxProvider + .ForAttributeWithMetadataName( + Parser.StronglyTypedIdConvertersAttribute, + predicate: (node, _) => node is StructDeclarationSyntax, + transform: Parser.GetConvertersSemanticTarget) + .Where(static m => m is not null); + + IncrementalValuesProvider converters = convertersAndDiagnostics + .Where(static x => x.Value.valid) + .Select((result, _) => result.Value.info); + + context.RegisterSourceOutput( + convertersAndDiagnostics.SelectMany((x, _) => x.Errors), + static (context, info) => context.ReportDiagnostic(info)); - var structsWithDefaultsAndTemplates = structs + // Combined + var idsWithDefaultsAndTemplates = ids .Combine(templates) - .Combine(defaultTemplateContent); + .Combine(idDefaultTemplateContent); - context.RegisterSourceOutput(structsWithDefaultsAndTemplates, - static (spc, source) => Execute(source.Left.Left, source.Left.Right, source.Right, spc)); + var convertersWithDefaultsAndTemplates = converters + .Combine(templates) + .Combine(converterDefaultTemplateContent); + + // Output + context.RegisterSourceOutput(idsWithDefaultsAndTemplates, + static (spc, source) => GenerateIds(source.Left.Left, source.Left.Right, source.Right, spc)); + + context.RegisterSourceOutput(convertersWithDefaultsAndTemplates, + static (spc, source) => GenerateConverters(source.Left.Left, source.Left.Right, source.Right, spc)); } - private static void Execute( + + private static void GenerateIds( StructToGenerate idToGenerate, ImmutableArray<(string Path, string Name, string? Content)> templates, (EquatableArray<(string Name, string Content)>, bool IsValid, DiagnosticInfo? Diagnostic) defaults, @@ -92,7 +142,7 @@ private static void Execute( context.ReportDiagnostic(diagnostic); } - if (!TryGetTemplateContent(idToGenerate, templates, defaults, in context, out var templateContents)) + if (!TryGetTemplateContent(idToGenerate.Template, idToGenerate.TemplateNames, idToGenerate.TemplateLocation, templates, defaults, in context, out var templateContents)) { return; } @@ -104,6 +154,7 @@ private static void Execute( var result = SourceGenerationHelper.CreateId( idToGenerate.NameSpace, idToGenerate.Name, + idToGenerate.Name, // same type idToGenerate.Parent, content, addDefaultAttributes: string.IsNullOrEmpty(name), @@ -123,23 +174,99 @@ private static void Execute( } } + private static void GenerateConverters( + ConverterToGenerate converterToGenerate, + ImmutableArray<(string Path, string Name, string? Content)> templates, + (EquatableArray<(string Name, string Content)> Templates, bool IsValid, DiagnosticInfo? Diagnostic) defaults, + SourceProductionContext context) + { + if (defaults.Diagnostic is { } diagnostic) + { + // report error with the default template + context.ReportDiagnostic(diagnostic); + } + + if (converterToGenerate.TemplateNames.Count == 0 + && defaults is { IsValid: false, Templates.Count: 0 }) + { + // not allowed this, so add a diagnostic + if (converterToGenerate.TemplateLocation is { } l) + { + var location = Location.Create(l.FilePath, l.TextSpan, l.LineSpan); + context.ReportDiagnostic(MissingDefaultsDiagnostic.CreateInfo(location)); + } - private static (EquatableArray<(string Name, string Content)>, bool, DiagnosticInfo?) ProcessDefaults((ImmutableArray Left, ImmutableArray<(string Path, string Name, string? Content)> Right) all, CancellationToken _) + return; + } + + if (!TryGetTemplateContent(selectedTemplate: null, converterToGenerate.TemplateNames, converterToGenerate.TemplateLocation, templates, defaults, in context, out var templateContents)) + { + return; + } + + var addGeneratedCodeAttribute = true; + + var sb = new StringBuilder(); + foreach (var (name, content) in templateContents.Distinct()) + { + var result = SourceGenerationHelper.CreateId( + converterToGenerate.NameSpace, + converterToGenerate.Name, + converterToGenerate.IdName, + converterToGenerate.Parent, + content, + addDefaultAttributes: string.IsNullOrEmpty(name), + addGeneratedCodeAttribute: addGeneratedCodeAttribute, + sb); + + addGeneratedCodeAttribute = false; // We can only add it once, so just add to the first rendering + + var fileName = SourceGenerationHelper.CreateSourceName( + sb, + converterToGenerate.NameSpace, + converterToGenerate.Parent, + converterToGenerate.Name, + name); + + context.AddSource(fileName, SourceText.From(result, Encoding.UTF8)); + } + } + + + private static (EquatableArray<(string Name, string Content)>, bool, DiagnosticInfo?) ProcessIdDefaults( + (ImmutableArray Selected, ImmutableArray<(string Path, string Name, string? Content)> Templates) all, + CancellationToken _) + => ProcessDefaults(all, allowEmptyDefaults: true); + + private static (EquatableArray<(string Name, string Content)>, bool, DiagnosticInfo?) ProcessConverterDefaults( + (ImmutableArray Selected, ImmutableArray<(string Path, string Name, string? Content)> Templates) all, + CancellationToken _) + => ProcessDefaults(all, allowEmptyDefaults: false); + + private static (EquatableArray<(string Name, string Content)>, bool, DiagnosticInfo?) ProcessDefaults( + (ImmutableArray Selected, ImmutableArray<(string Path, string Name, string? Content)> Templates) all, + bool allowEmptyDefaults) { - if (all.Left.IsDefaultOrEmpty) + if (all.Selected.IsDefaultOrEmpty) { // no default attributes, valid, but no content - return (EquatableArray<(string Name, string Content)>.Empty, true, null); + if (allowEmptyDefaults) + { + return (EquatableArray<(string Name, string Content)>.Empty, true, null); + } + + // Not allowed empty + return (EquatableArray<(string Name, string Content)>.Empty, false, null); } // technically we can never have more than one `Defaults` here // but check for it just in case - if (all.Left is {IsDefaultOrEmpty: false, Length: > 1}) + if (all.Selected is {IsDefaultOrEmpty: false, Length: > 1}) { return (EquatableArray<(string Name, string Content)>.Empty, false, null); } - var defaults = all.Left[0]; + var defaults = all.Selected[0]; if (defaults.HasMultiple) { // not valid @@ -168,7 +295,7 @@ private static (EquatableArray<(string Name, string Content)>, bool, DiagnosticI } // We have already checked for null/empty template name and flagged it as an error - if (!GetContent(templateNames, defaults.TemplateLocation!, builtInTemplate.HasValue, in all.Right, out var contents, out var diagnostic)) + if (!GetContent(templateNames, defaults.TemplateLocation!, builtInTemplate.HasValue, in all.Templates, out var contents, out var diagnostic)) { return (EquatableArray<(string Name, string Content)>.Empty, false, diagnostic); } @@ -183,26 +310,28 @@ private static (EquatableArray<(string Name, string Content)>, bool, DiagnosticI } private static bool TryGetTemplateContent( - in StructToGenerate idToGenerate, + Template? selectedTemplate, + EquatableArray selectedTemplateNames, + LocationInfo? attributeLocation, in ImmutableArray<(string Path, string Name, string? Content)> templates, (EquatableArray<(string Name, string Content)> Contents, bool IsValid, DiagnosticInfo? Diagnostics) defaults, in SourceProductionContext context, [NotNullWhen(true)] out (string Name, string Content)[]? templateContents) { (string, string)? builtIn = null; - if (idToGenerate.Template is { } templateId) + if (selectedTemplate is { } templateId) { // built-in template specified var content = EmbeddedSources.GetTemplate(templateId); builtIn = (string.Empty, content); } - if (idToGenerate.TemplateNames.GetArray() is {Length: > 0} templateNames) + if (selectedTemplateNames.GetArray() is {Length: > 0} templateNames) { // custom template specified if (GetContent( templateNames, - idToGenerate.TemplateLocation, + attributeLocation, builtIn.HasValue, in templates, out templateContents, diff --git a/src/StronglyTypedIds/StructToGenerate.cs b/src/StronglyTypedIds/StructToGenerate.cs index 2f2a666c7..0c3fe15e2 100644 --- a/src/StronglyTypedIds/StructToGenerate.cs +++ b/src/StronglyTypedIds/StructToGenerate.cs @@ -24,6 +24,26 @@ public StructToGenerate(string name, string nameSpace, Template? template, strin public LocationInfo? TemplateLocation { get; } } +internal readonly record struct ConverterToGenerate +{ + public ConverterToGenerate(string name, string nameSpace, string idName, string[]? templateNames, ParentClass? parent, LocationInfo templateLocation) + { + Name = name; + NameSpace = nameSpace; + IdName = idName; + TemplateNames = templateNames is null ? EquatableArray.Empty : new EquatableArray(templateNames); + Parent = parent; + TemplateLocation = templateLocation; + } + + public string Name { get; } + public string NameSpace { get; } + public string IdName { get; } + public EquatableArray TemplateNames { get; } + public ParentClass? Parent { get; } + public LocationInfo? TemplateLocation { get; } +} + internal sealed record Result(TValue Value, EquatableArray Errors) where TValue : IEquatable? { diff --git a/test/StronglyTypedIds.IntegrationTests.ExternalIds/Converters.cs b/test/StronglyTypedIds.IntegrationTests.ExternalIds/Converters.cs new file mode 100644 index 000000000..48b6494d1 --- /dev/null +++ b/test/StronglyTypedIds.IntegrationTests.ExternalIds/Converters.cs @@ -0,0 +1,15 @@ +using StronglyTypedIds.IntegrationTests.Types; + +namespace StronglyTypedIds.IntegrationTests; + +[StronglyTypedIdConverters("guid-dapper", "guid-efcore")] +internal partial struct Guid1Converters { } + +[StronglyTypedIdConverters("int-dapper", "int-efcore")] +internal partial struct IntConverters { } + +[StronglyTypedIdConverters("long-dapper", "long-efcore")] +internal partial struct LongConverters { } + +[StronglyTypedIdConverters("string-dapper", "string-efcore")] +internal partial struct StringConverters { } \ No newline at end of file diff --git a/test/StronglyTypedIds.IntegrationTests.ExternalIds/StronglyTypedIds.IntegrationTests.ExternalIds.csproj b/test/StronglyTypedIds.IntegrationTests.ExternalIds/StronglyTypedIds.IntegrationTests.ExternalIds.csproj index 833c1281f..2e422fd2e 100644 --- a/test/StronglyTypedIds.IntegrationTests.ExternalIds/StronglyTypedIds.IntegrationTests.ExternalIds.csproj +++ b/test/StronglyTypedIds.IntegrationTests.ExternalIds/StronglyTypedIds.IntegrationTests.ExternalIds.csproj @@ -6,6 +6,15 @@ false true + + + + + + + + + diff --git a/test/StronglyTypedIds.IntegrationTests/DapperTypeHandlers.cs b/test/StronglyTypedIds.IntegrationTests/DapperTypeHandlers.cs index 517239aea..478e1b150 100644 --- a/test/StronglyTypedIds.IntegrationTests/DapperTypeHandlers.cs +++ b/test/StronglyTypedIds.IntegrationTests/DapperTypeHandlers.cs @@ -19,6 +19,10 @@ public static void AddHandlers() SqlMapper.AddTypeHandler(new ConvertersStringId2.DapperTypeHandler()); SqlMapper.AddTypeHandler(new NullableStringId.DapperTypeHandler()); SqlMapper.AddTypeHandler(new NewIdId1.DapperTypeHandler()); + SqlMapper.AddTypeHandler(new Guid1Converters.DapperTypeHandler()); + SqlMapper.AddTypeHandler(new IntConverters.DapperTypeHandler()); + SqlMapper.AddTypeHandler(new LongConverters.DapperTypeHandler()); + SqlMapper.AddTypeHandler(new StringConverters.DapperTypeHandler()); } } } \ No newline at end of file diff --git a/test/StronglyTypedIds.IntegrationTests/Enums.cs b/test/StronglyTypedIds.IntegrationTests/Enums.cs index 66e88e574..d71808b14 100644 --- a/test/StronglyTypedIds.IntegrationTests/Enums.cs +++ b/test/StronglyTypedIds.IntegrationTests/Enums.cs @@ -72,3 +72,14 @@ internal readonly partial struct VeryNestedId } } +[StronglyTypedIdConverters("guid-dapper", "guid-efcore")] +internal partial struct Guid1Converters { } + +[StronglyTypedIdConverters("int-dapper", "int-efcore")] +internal partial struct IntConverters { } + +[StronglyTypedIdConverters("long-dapper", "long-efcore")] +internal partial struct LongConverters { } + +[StronglyTypedIdConverters("string-dapper", "string-efcore")] +internal partial struct StringConverters { } \ No newline at end of file diff --git a/test/StronglyTypedIds.IntegrationTests/GuidIdTests.cs b/test/StronglyTypedIds.IntegrationTests/GuidIdTests.cs index 6cdca8eca..f5fd41532 100644 --- a/test/StronglyTypedIds.IntegrationTests/GuidIdTests.cs +++ b/test/StronglyTypedIds.IntegrationTests/GuidIdTests.cs @@ -374,20 +374,36 @@ public void WhenEfCoreValueConverterUsesValueConverter() } [Fact] - public async Task WhenDapperValueConverterUsesValueConverter() + public Task WhenDapperValueConverterUsesValueConverter_Id() + => WhenDapperValueConverterUsesValueConverter(g => new ConvertersGuidId(g)); + + [Fact] + public Task WhenDapperValueConverterUsesValueConverter_Converter() + => WhenDapperValueConverterUsesValueConverter(g => new GuidId1(g)); + + private async Task WhenDapperValueConverterUsesValueConverter(Func newFunc) { using var connection = new SqliteConnection("DataSource=:memory:"); await connection.OpenAsync(); - var results = await connection.QueryAsync("SELECT '5640dad4-862a-4738-9e3c-c76dc227eb66'"); + var results = await connection.QueryAsync("SELECT '5640dad4-862a-4738-9e3c-c76dc227eb66'"); var value = Assert.Single(results); - Assert.Equal(value, new ConvertersGuidId(Guid.Parse("5640dad4-862a-4738-9e3c-c76dc227eb66"))); + Assert.Equal(value, newFunc(Guid.Parse("5640dad4-862a-4738-9e3c-c76dc227eb66"))); } #if NET6_0_OR_GREATER [Fact] - public void WhenConventionBasedEfCoreValueConverterUsesValueConverter() + public void WhenConventionBasedEfCoreValueConverterUsesValueConverter_Id() + => WhenConventionBasedEfCoreValueConverterUsesValueConverter(x => x.Entities, + new TestEntity { Id = ConvertersGuidId.New() }); + + [Fact] + public void WhenConventionBasedEfCoreValueConverterUsesValueConverter_Converter() + => WhenConventionBasedEfCoreValueConverterUsesValueConverter(c => c.Entities3, + new TestEntity3 { Id = GuidId1.New() }); + + private void WhenConventionBasedEfCoreValueConverterUsesValueConverter(Func> dbsetFunc, T entity) where T : class { var connection = new SqliteConnection("DataSource=:memory:"); connection.Open(); @@ -399,13 +415,12 @@ public void WhenConventionBasedEfCoreValueConverterUsesValueConverter() using (var context = new ConventionsDbContext(options)) { context.Database.EnsureCreated(); - context.Entities.Add( - new TestEntity { Id = ConvertersGuidId.New() }); + dbsetFunc(context).Add(entity); context.SaveChanges(); } using (var context = new ConventionsDbContext(options)) { - var all = context.Entities.ToList(); + var all = dbsetFunc(context).ToList(); Assert.Single(all); } } @@ -519,6 +534,7 @@ internal class ConventionsDbContext : DbContext { public DbSet Entities { get; set; } public DbSet Entities2 { get; set; } + public DbSet Entities3 { get; set; } public ConventionsDbContext(DbContextOptions options) : base(options) { @@ -532,6 +548,9 @@ protected override void ConfigureConventions(ModelConfigurationBuilder configura configurationBuilder .Properties() .HaveConversion(); + configurationBuilder + .Properties() + .HaveConversion(); } protected override void OnModelCreating(ModelBuilder modelBuilder) @@ -550,6 +569,13 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) .Property(x => x.Id) .ValueGeneratedNever(); }); + modelBuilder + .Entity(builder => + { + builder + .Property(x => x.Id) + .ValueGeneratedNever(); + }); } } #endif @@ -605,6 +631,11 @@ internal class TestEntity2 public ConvertersGuidId2 Id { get; set; } } + internal class TestEntity3 + { + public GuidId1 Id { get; set; } + } + internal class EntityWithNullableId2 { public ConvertersGuidId2? Id { get; set; } diff --git a/test/StronglyTypedIds.IntegrationTests/IntIdTests.cs b/test/StronglyTypedIds.IntegrationTests/IntIdTests.cs index dd73bae77..af687fce0 100644 --- a/test/StronglyTypedIds.IntegrationTests/IntIdTests.cs +++ b/test/StronglyTypedIds.IntegrationTests/IntIdTests.cs @@ -273,15 +273,22 @@ public void WhenEfCoreValueConverterUsesValueConverter() } [Fact] - public async Task WhenDapperValueConverterUsesValueConverter() + public Task WhenDapperValueConverterUsesValueConverter_Id() + => WhenDapperValueConverterUsesValueConverter(g => new ConvertersIntId(g)); + + [Fact] + public Task WhenDapperValueConverterUsesValueConverter_Converter() + => WhenDapperValueConverterUsesValueConverter(g => new IntId(g)); + + private async Task WhenDapperValueConverterUsesValueConverter(Func newFunc) { using var connection = new SqliteConnection("DataSource=:memory:"); await connection.OpenAsync(); - var results = await connection.QueryAsync("SELECT 123"); + var results = await connection.QueryAsync("SELECT 123"); var value = Assert.Single(results); - Assert.Equal(new ConvertersIntId(123), value); + Assert.Equal(newFunc(123), value); } [Fact(Skip = "Requires localdb to be available")] @@ -321,7 +328,16 @@ public void TypeConverter_CanConvertToAndFrom(object value) #if NET6_0_OR_GREATER [Fact] - public void WhenConventionBasedEfCoreValueConverterUsesValueConverter() + public void WhenConventionBasedEfCoreValueConverterUsesValueConverter_Id() + => WhenConventionBasedEfCoreValueConverterUsesValueConverter(x => x.Entities, + new TestEntity { Id = new ConvertersIntId(123) }); + + [Fact] + public void WhenConventionBasedEfCoreValueConverterUsesValueConverter_Converter() + => WhenConventionBasedEfCoreValueConverterUsesValueConverter(c => c.Entities3, + new TestEntity3 { Id = new IntId(123) }); + + private void WhenConventionBasedEfCoreValueConverterUsesValueConverter(Func> dbsetFunc, T entity) where T : class { var connection = new SqliteConnection("DataSource=:memory:"); connection.Open(); @@ -333,14 +349,13 @@ public void WhenConventionBasedEfCoreValueConverterUsesValueConverter() using (var context = new ConventionsDbContext(options)) { context.Database.EnsureCreated(); - context.Entities.Add( - new TestEntity {Id = new ConvertersIntId(123)}); + dbsetFunc(context).Add(entity); context.SaveChanges(); } using (var context = new ConventionsDbContext(options)) { - var all = context.Entities.ToList(); + var all = dbsetFunc(context).ToList(); Assert.Single(all); } } @@ -499,6 +514,7 @@ internal class ConventionsDbContext : DbContext { public DbSet Entities { get; set; } public DbSet Entities2 { get; set; } + public DbSet Entities3 { get; set; } public ConventionsDbContext(DbContextOptions options) : base(options) { @@ -512,6 +528,9 @@ protected override void ConfigureConventions(ModelConfigurationBuilder configura configurationBuilder .Properties() .HaveConversion(); + configurationBuilder + .Properties() + .HaveConversion(); } protected override void OnModelCreating(ModelBuilder modelBuilder) @@ -530,6 +549,13 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) .Property(x => x.Id) .ValueGeneratedNever(); }); + modelBuilder + .Entity(builder => + { + builder + .Property(x => x.Id) + .ValueGeneratedNever(); + }); } } #endif @@ -579,6 +605,11 @@ internal class TestEntity2 public ConvertersIntId2 Id { get; set; } } + internal class TestEntity3 + { + public IntId Id { get; set; } + } + internal class EntityWithNullableId2 { public ConvertersIntId2? Id { get; set; } diff --git a/test/StronglyTypedIds.IntegrationTests/LongIdTests.cs b/test/StronglyTypedIds.IntegrationTests/LongIdTests.cs index 45805b944..9f8370f44 100644 --- a/test/StronglyTypedIds.IntegrationTests/LongIdTests.cs +++ b/test/StronglyTypedIds.IntegrationTests/LongIdTests.cs @@ -285,15 +285,22 @@ public void WhenEfCoreValueConverterUsesValueConverter() } [Fact] - public async Task WhenDapperValueConverterUsesValueConverter() + public Task WhenDapperValueConverterUsesValueConverter_Id() + => WhenDapperValueConverterUsesValueConverter(g => new ConvertersLongId(g)); + + [Fact] + public Task WhenDapperValueConverterUsesValueConverter_Converter() + => WhenDapperValueConverterUsesValueConverter(g => new LongId(g)); + + private async Task WhenDapperValueConverterUsesValueConverter(Func newFunc) { using var connection = new SqliteConnection("DataSource=:memory:"); await connection.OpenAsync(); - var results = await connection.QueryAsync("SELECT 123"); + var results = await connection.QueryAsync("SELECT 123"); var value = Assert.Single(results); - Assert.Equal(value, new ConvertersLongId(123)); + Assert.Equal(value, newFunc(123)); } [Fact(Skip = "Requires localdb to be available")] @@ -319,7 +326,16 @@ public void WhenDapperValueConverterAndDecimalUsesValueConverter() #if NET6_0_OR_GREATER [Fact] - public void WhenConventionBasedEfCoreValueConverterUsesValueConverter() + public void WhenConventionBasedEfCoreValueConverterUsesValueConverter_Id() + => WhenConventionBasedEfCoreValueConverterUsesValueConverter(x => x.Entities, + new TestEntity { Id = new ConvertersLongId(123) }); + + [Fact] + public void WhenConventionBasedEfCoreValueConverterUsesValueConverter_Converter() + => WhenConventionBasedEfCoreValueConverterUsesValueConverter(c => c.Entities3, + new TestEntity3 { Id = new LongId(123) }); + + private void WhenConventionBasedEfCoreValueConverterUsesValueConverter(Func> dbsetFunc, T entity) where T : class { var connection = new SqliteConnection("DataSource=:memory:"); connection.Open(); @@ -331,13 +347,12 @@ public void WhenConventionBasedEfCoreValueConverterUsesValueConverter() using (var context = new ConventionsDbContext(options)) { context.Database.EnsureCreated(); - context.Entities.Add( - new TestEntity { Id = new ConvertersLongId(123) }); + dbsetFunc(context).Add(entity); context.SaveChanges(); } using (var context = new ConventionsDbContext(options)) { - var all = context.Entities.ToList(); + var all = dbsetFunc(context).ToList(); Assert.Single(all); } } @@ -481,6 +496,7 @@ internal class ConventionsDbContext : DbContext { public DbSet Entities { get; set; } public DbSet Entities2 { get; set; } + public DbSet Entities3 { get; set; } public ConventionsDbContext(DbContextOptions options) : base(options) { @@ -494,6 +510,9 @@ protected override void ConfigureConventions(ModelConfigurationBuilder configura configurationBuilder .Properties() .HaveConversion(); + configurationBuilder + .Properties() + .HaveConversion(); } protected override void OnModelCreating(ModelBuilder modelBuilder) @@ -512,6 +531,13 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) .Property(x => x.Id) .ValueGeneratedNever(); }); + modelBuilder + .Entity(builder => + { + builder + .Property(x => x.Id) + .ValueGeneratedNever(); + }); } } #endif @@ -561,6 +587,11 @@ internal class TestEntity2 public ConvertersLongId2 Id { get; set; } } + internal class TestEntity3 + { + public LongId Id { get; set; } + } + internal class EntityWithNullableId2 { public ConvertersLongId2? Id { get; set; } diff --git a/test/StronglyTypedIds.IntegrationTests/StringIdTests.cs b/test/StronglyTypedIds.IntegrationTests/StringIdTests.cs index 4a2e34945..d7642e761 100644 --- a/test/StronglyTypedIds.IntegrationTests/StringIdTests.cs +++ b/test/StronglyTypedIds.IntegrationTests/StringIdTests.cs @@ -295,21 +295,39 @@ public void WhenEfCoreValueConverterUsesValueConverter() } } + + + [Fact] + public Task WhenDapperValueConverterUsesValueConverter_Id() + => WhenDapperValueConverterUsesValueConverter(g => new ConvertersStringId(g)); + [Fact] - public async Task WhenDapperValueConverterUsesValueConverter() + public Task WhenDapperValueConverterUsesValueConverter_Converter() + => WhenDapperValueConverterUsesValueConverter(g => new StringId(g)); + + private async Task WhenDapperValueConverterUsesValueConverter(Func newFunc) { using var connection = new SqliteConnection("DataSource=:memory:"); await connection.OpenAsync(); - var results = await connection.QueryAsync("SELECT 'this is a value'"); + var results = await connection.QueryAsync("SELECT 'this is a value'"); var value = Assert.Single(results); - Assert.Equal(value, new ConvertersStringId("this is a value")); + Assert.Equal(value, newFunc("this is a value")); } #if NET6_0_OR_GREATER [Fact] - public void WhenConventionBasedEfCoreValueConverterUsesValueConverter() + public void WhenConventionBasedEfCoreValueConverterUsesValueConverter_Id() + => WhenConventionBasedEfCoreValueConverterUsesValueConverter(x => x.Entities, + new TestEntity { Id = Guid.NewGuid(), Name = new ConvertersStringId("some name") }); + + [Fact] + public void WhenConventionBasedEfCoreValueConverterUsesValueConverter_Converter() + => WhenConventionBasedEfCoreValueConverterUsesValueConverter(c => c.Entities3, + new TestEntity3 { Id = Guid.NewGuid(), Name = new StringId("some name") }); + + private void WhenConventionBasedEfCoreValueConverterUsesValueConverter(Func> dbsetFunc, T entity) where T : class { var connection = new SqliteConnection("DataSource=:memory:"); connection.Open(); @@ -318,20 +336,17 @@ public void WhenConventionBasedEfCoreValueConverterUsesValueConverter() .UseSqlite(connection) .Options; - var original = new TestEntity { Id = Guid.NewGuid(), Name = new ConvertersStringId("some name") }; using (var context = new ConventionsDbContext(options)) { context.Database.EnsureCreated(); - context.Entities.Add(original); + dbsetFunc(context).Add(entity); context.SaveChanges(); } using (var context = new ConventionsDbContext(options)) { - var all = context.Entities.ToList(); + var all = dbsetFunc(context).ToList(); var retrieved = Assert.Single(all); - Assert.Equal(original.Id, retrieved.Id); - Assert.Equal(original.Name, retrieved.Name); } } #endif @@ -470,6 +485,7 @@ internal class ConventionsDbContext : DbContext { public DbSet Entities { get; set; } public DbSet Entities2 { get; set; } + public DbSet Entities3 { get; set; } public ConventionsDbContext(DbContextOptions options) : base(options) { @@ -483,6 +499,9 @@ protected override void ConfigureConventions(ModelConfigurationBuilder configura configurationBuilder .Properties() .HaveConversion(); + configurationBuilder + .Properties() + .HaveConversion(); } protected override void OnModelCreating(ModelBuilder modelBuilder) @@ -501,6 +520,13 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) .Property(x => x.Id) .ValueGeneratedNever(); }); + modelBuilder + .Entity(builder => + { + builder + .Property(x => x.Id) + .ValueGeneratedNever(); + }); } } #endif @@ -552,6 +578,12 @@ internal class TestEntity2 public ConvertersStringId2 Name { get; set; } } + internal class TestEntity3 + { + public Guid Id { get; set; } + public StringId Name { get; set; } + } + internal class EntityWithNullableId2 { public ConvertersStringId2? Id { get; set; } diff --git a/test/StronglyTypedIds.Tests/EmbeddedResourceTests.cs b/test/StronglyTypedIds.Tests/EmbeddedResourceTests.cs index fd4aef693..3a993efd0 100644 --- a/test/StronglyTypedIds.Tests/EmbeddedResourceTests.cs +++ b/test/StronglyTypedIds.Tests/EmbeddedResourceTests.cs @@ -11,6 +11,8 @@ public class EmbeddedResourceTests { "StronglyTypedIdAttribute", "StronglyTypedIdDefaultsAttribute", + "StronglyTypedIdConvertersAttribute", + "StronglyTypedIdConvertersDefaultsAttribute", "Template", }; diff --git a/test/StronglyTypedIds.Tests/Snapshots/EmbeddedResourceTests.EmittedResourceIsSameAsCompiledResource_resource=StronglyTypedIdConvertersAttribute.verified.txt b/test/StronglyTypedIds.Tests/Snapshots/EmbeddedResourceTests.EmittedResourceIsSameAsCompiledResource_resource=StronglyTypedIdConvertersAttribute.verified.txt new file mode 100644 index 000000000..a0aa2596d --- /dev/null +++ b/test/StronglyTypedIds.Tests/Snapshots/EmbeddedResourceTests.EmittedResourceIsSameAsCompiledResource_resource=StronglyTypedIdConvertersAttribute.verified.txt @@ -0,0 +1,41 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by the StronglyTypedId source generator +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +#pragma warning disable 1591 // publicly visible type or member must be documented + +#nullable enable + +#if STRONGLY_TYPED_ID_EMBED_ATTRIBUTES + +namespace StronglyTypedIds +{ + /// + /// Place on partial structs to generate converters for a strongly-typed ID, . + /// + [global::System.AttributeUsage(global::System.AttributeTargets.Struct, Inherited = false, AllowMultiple = false)] + [global::System.Diagnostics.Conditional("STRONGLY_TYPED_ID_USAGES")] + internal sealed class StronglyTypedIdConvertersAttribute : global::System.Attribute + where T: struct + { + /// + /// Generate converters for a strongly typed ID + /// + /// The names of the template to use to generate the converters. + /// Templates must be added to the project using the format NAME.typedid, + /// where NAME is the name of the template passed in . + /// If no templates are provided, the default value is used, as specified by + /// + /// The StronglyTyped ID implementation for which these converters are associated + /// + public StronglyTypedIdConvertersAttribute(params string[] templateNames) + { + } + } +} +#endif \ No newline at end of file diff --git a/test/StronglyTypedIds.Tests/Snapshots/EmbeddedResourceTests.EmittedResourceIsSameAsCompiledResource_resource=StronglyTypedIdConvertersDefaultsAttribute.verified.txt b/test/StronglyTypedIds.Tests/Snapshots/EmbeddedResourceTests.EmittedResourceIsSameAsCompiledResource_resource=StronglyTypedIdConvertersDefaultsAttribute.verified.txt new file mode 100644 index 000000000..2f3ea4bcd --- /dev/null +++ b/test/StronglyTypedIds.Tests/Snapshots/EmbeddedResourceTests.EmittedResourceIsSameAsCompiledResource_resource=StronglyTypedIdConvertersDefaultsAttribute.verified.txt @@ -0,0 +1,52 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by the StronglyTypedId source generator +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +#pragma warning disable 1591 // publicly visible type or member must be documented + +#nullable enable + +#if STRONGLY_TYPED_ID_EMBED_ATTRIBUTES + +namespace StronglyTypedIds +{ + /// + /// Used to control the default templates to use with instances + /// + [global::System.AttributeUsage(global::System.AttributeTargets.Assembly, Inherited = false, AllowMultiple = false)] + [global::System.Diagnostics.Conditional("STRONGLY_TYPED_ID_USAGES")] + internal sealed class StronglyTypedIdConvertersDefaultsAttribute : global::System.Attribute + { + /// + /// Set the default template to use for strongly typed ID converter types + /// + /// The name of the template to use to generate the converter type. + /// Templates must be added to the project using the format NAME.typedid, + /// where NAME is the name of the template passed in . + /// + public StronglyTypedIdConvertersDefaultsAttribute(string templateName) + { + } + + /// + /// Set the default templates to use for strongly typed ID converter types + /// + /// The name of the template to use to generate the converter type. + /// Templates must be added to the project using the format NAME.typedid, + /// where NAME is the name of the template passed in . + /// + /// The names of additional custom templates to use to generate the converter type. + /// Templates must be added to the project using the format NAME.typedid, + /// where NAME is the name of the template passed in . + /// + public StronglyTypedIdConvertersDefaultsAttribute(string templateName, params string[] templateNames) + { + } + } +} +#endif \ No newline at end of file diff --git a/test/StronglyTypedIds.Tests/Snapshots/StronglyTypedIdConverterTests.CanGenerateDefaultConverterIdInDifferentNamespace.verified.txt b/test/StronglyTypedIds.Tests/Snapshots/StronglyTypedIdConverterTests.CanGenerateDefaultConverterIdInDifferentNamespace.verified.txt new file mode 100644 index 000000000..e2bc0e43d --- /dev/null +++ b/test/StronglyTypedIds.Tests/Snapshots/StronglyTypedIdConverterTests.CanGenerateDefaultConverterIdInDifferentNamespace.verified.txt @@ -0,0 +1,271 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by the StronglyTypedId source generator +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +#pragma warning disable 1591 // publicly visible type or member must be documented + +#nullable enable +namespace SomeNamespace1 +{ + [global::System.ComponentModel.TypeConverter(typeof(MyIdTypeConverter))] + [global::System.Text.Json.Serialization.JsonConverter(typeof(MyIdSystemTextJsonConverter))] + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("StronglyTypedId", "1.0.0-beta6")] + partial struct MyId : +#if NET6_0_OR_GREATER + global::System.ISpanFormattable, +#endif +#if NET7_0_OR_GREATER + global::System.IParsable, global::System.ISpanParsable, +#endif +#if NET8_0_OR_GREATER + global::System.IUtf8SpanFormattable, +#endif + global::System.IComparable, global::System.IEquatable, global::System.IFormattable + { + public global::System.Guid Value { get; } + + public MyId(global::System.Guid value) + { + Value = value; + } + + public static MyId New() => new MyId(global::System.Guid.NewGuid()); + public static readonly MyId Empty = new MyId(global::System.Guid.Empty); + + /// + public bool Equals(MyId other) => this.Value.Equals(other.Value); + public override bool Equals(object? obj) + { + if (ReferenceEquals(null, obj)) return false; + return obj is MyId other && Equals(other); + } + + public override int GetHashCode() => Value.GetHashCode(); + + public override string ToString() => Value.ToString(); + + public static bool operator ==(MyId a, MyId b) => a.Equals(b); + public static bool operator !=(MyId a, MyId b) => !(a == b); + public static bool operator > (MyId a, MyId b) => a.CompareTo(b) > 0; + public static bool operator < (MyId a, MyId b) => a.CompareTo(b) < 0; + public static bool operator >= (MyId a, MyId b) => a.CompareTo(b) >= 0; + public static bool operator <= (MyId a, MyId b) => a.CompareTo(b) <= 0; + + /// + public int CompareTo(MyId other) => Value.CompareTo(other.Value); + + public partial class MyIdTypeConverter : global::System.ComponentModel.TypeConverter + { + public override bool CanConvertFrom(global::System.ComponentModel.ITypeDescriptorContext? context, global::System.Type sourceType) + { + return sourceType == typeof(global::System.Guid) || sourceType == typeof(string) || base.CanConvertFrom(context, sourceType); + } + + public override object? ConvertFrom(global::System.ComponentModel.ITypeDescriptorContext? context, global::System.Globalization.CultureInfo? culture, object value) + { + return value switch + { + global::System.Guid guidValue => new MyId(guidValue), + string stringValue when !string.IsNullOrEmpty(stringValue) && global::System.Guid.TryParse(stringValue, out var result) => new MyId(result), + _ => base.ConvertFrom(context, culture, value), + }; + } + + public override bool CanConvertTo(global::System.ComponentModel.ITypeDescriptorContext? context, global::System.Type? sourceType) + { + return sourceType == typeof(global::System.Guid) || sourceType == typeof(string) || base.CanConvertTo(context, sourceType); + } + + public override object? ConvertTo(global::System.ComponentModel.ITypeDescriptorContext? context, global::System.Globalization.CultureInfo? culture, object? value, global::System.Type destinationType) + { + if (value is MyId idValue) + { + if (destinationType == typeof(global::System.Guid)) + { + return idValue.Value; + } + + if (destinationType == typeof(string)) + { + return idValue.Value.ToString(); + } + } + + return base.ConvertTo(context, culture, value, destinationType); + } + } + + public partial class MyIdSystemTextJsonConverter : global::System.Text.Json.Serialization.JsonConverter + { + public override bool CanConvert(global::System.Type typeToConvert) + => typeToConvert == typeof(global::System.Guid) || typeToConvert == typeof(string) || base.CanConvert(typeToConvert); + + public override MyId Read(ref global::System.Text.Json.Utf8JsonReader reader, global::System.Type typeToConvert, global::System.Text.Json.JsonSerializerOptions options) + => new (reader.GetGuid()); + + public override void Write(global::System.Text.Json.Utf8JsonWriter writer, MyId value, global::System.Text.Json.JsonSerializerOptions options) + => writer.WriteStringValue(value.Value); + +#if NET6_0_OR_GREATER + public override MyId ReadAsPropertyName(ref global::System.Text.Json.Utf8JsonReader reader, global::System.Type typeToConvert, global::System.Text.Json.JsonSerializerOptions options) + => new(global::System.Guid.Parse(reader.GetString() ?? throw new global::System.FormatException("The string for the MyId property was null"))); + + public override void WriteAsPropertyName(global::System.Text.Json.Utf8JsonWriter writer, MyId value, global::System.Text.Json.JsonSerializerOptions options) + => writer.WritePropertyName(value.Value.ToString()); +#endif + } + + public static MyId Parse(string input) + => new(global::System.Guid.Parse(input)); + +#if NET7_0_OR_GREATER + /// + public static MyId Parse(string input, global::System.IFormatProvider? provider) + => new(global::System.Guid.Parse(input, provider)); + + /// + public static bool TryParse( + [global::System.Diagnostics.CodeAnalysis.NotNullWhen(true)] string? input, + global::System.IFormatProvider? provider, + out MyId result) + { + if (input is null) + { + result = default; + return false; + } + + if (global::System.Guid.TryParse(input, provider, out var guid)) + { + result = new(guid); + return true; + } + else + { + result = default; + return false; + } + } +#endif + + /// + public string ToString( +#if NET7_0_OR_GREATER + [global::System.Diagnostics.CodeAnalysis.StringSyntax(global::System.Diagnostics.CodeAnalysis.StringSyntaxAttribute.GuidFormat)] +#endif + string? format, + global::System.IFormatProvider? formatProvider) + => Value.ToString(format, formatProvider); + +#if NETCOREAPP2_1_OR_GREATER + public static MyId Parse(global::System.ReadOnlySpan input) + => new(global::System.Guid.Parse(input)); +#endif + +#if NET6_0_OR_GREATER +#if NET7_0_OR_GREATER + /// +#endif + public static MyId Parse(global::System.ReadOnlySpan input, global::System.IFormatProvider? provider) +#if NET7_0_OR_GREATER + => new(global::System.Guid.Parse(input, provider)); +#else + => new(global::System.Guid.Parse(input)); +#endif + +#if NET7_0_OR_GREATER + /// +#endif + public static bool TryParse(global::System.ReadOnlySpan input, global::System.IFormatProvider? provider, out MyId result) + { +#if NET7_0_OR_GREATER + if (global::System.Guid.TryParse(input, provider, out var guid)) +#else + if (global::System.Guid.TryParse(input, out var guid)) +#endif + { + result = new(guid); + return true; + } + else + { + result = default; + return false; + } + } + + /// + public bool TryFormat( + global::System.Span destination, + out int charsWritten, +#if NET7_0_OR_GREATER + [global::System.Diagnostics.CodeAnalysis.StringSyntax(global::System.Diagnostics.CodeAnalysis.StringSyntaxAttribute.GuidFormat)] +#endif + global::System.ReadOnlySpan format, + global::System.IFormatProvider? provider) + => Value.TryFormat(destination, out charsWritten, format); + + /// + public bool TryFormat( + global::System.Span destination, + out int charsWritten, +#if NET7_0_OR_GREATER + [global::System.Diagnostics.CodeAnalysis.StringSyntax(global::System.Diagnostics.CodeAnalysis.StringSyntaxAttribute.GuidFormat)] +#endif + global::System.ReadOnlySpan format = default) + => Value.TryFormat(destination, out charsWritten, format); +#endif +#if NET8_0_OR_GREATER + /// + public bool TryFormat( + global::System.Span utf8Destination, + out int bytesWritten, + [global::System.Diagnostics.CodeAnalysis.StringSyntax(global::System.Diagnostics.CodeAnalysis.StringSyntaxAttribute.GuidFormat)] + global::System.ReadOnlySpan format, + global::System.IFormatProvider? provider) + => Value.TryFormat(utf8Destination, out bytesWritten, format); +#endif + } +} + +//------------------------------------------------------------------------------ +// +// This code was generated by the StronglyTypedId source generator +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +#pragma warning disable 1591 // publicly visible type or member must be documented + +#nullable enable +namespace SomeNamespace2 +{ + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("StronglyTypedId", "1.0.0-beta6")] + partial struct MyIdConverters + { + public partial class DapperTypeHandler : global::Dapper.SqlMapper.TypeHandler + { + public override void SetValue(global::System.Data.IDbDataParameter parameter, SomeNamespace1.MyId value) + { + parameter.Value = value.Value; + } + + public override SomeNamespace1.MyId Parse(object value) + { + return value switch + { + global::System.Guid guidValue => new SomeNamespace1.MyId(guidValue), + string stringValue when !string.IsNullOrEmpty(stringValue) && global::System.Guid.TryParse(stringValue, out var result) => new SomeNamespace1.MyId(result), + _ => throw new global::System.InvalidCastException($"Unable to cast object of type {value.GetType()} to SomeNamespace1.MyId"), + }; + } + } + } +} diff --git a/test/StronglyTypedIds.Tests/Snapshots/StronglyTypedIdConverterTests.CanGenerateDefaultConverterIdInFileScopedNamespace.verified.txt b/test/StronglyTypedIds.Tests/Snapshots/StronglyTypedIdConverterTests.CanGenerateDefaultConverterIdInFileScopedNamespace.verified.txt new file mode 100644 index 000000000..2cfd883a2 --- /dev/null +++ b/test/StronglyTypedIds.Tests/Snapshots/StronglyTypedIdConverterTests.CanGenerateDefaultConverterIdInFileScopedNamespace.verified.txt @@ -0,0 +1,271 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by the StronglyTypedId source generator +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +#pragma warning disable 1591 // publicly visible type or member must be documented + +#nullable enable +namespace SomeNamespace +{ + [global::System.ComponentModel.TypeConverter(typeof(MyIdTypeConverter))] + [global::System.Text.Json.Serialization.JsonConverter(typeof(MyIdSystemTextJsonConverter))] + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("StronglyTypedId", "1.0.0-beta6")] + partial struct MyId : +#if NET6_0_OR_GREATER + global::System.ISpanFormattable, +#endif +#if NET7_0_OR_GREATER + global::System.IParsable, global::System.ISpanParsable, +#endif +#if NET8_0_OR_GREATER + global::System.IUtf8SpanFormattable, +#endif + global::System.IComparable, global::System.IEquatable, global::System.IFormattable + { + public global::System.Guid Value { get; } + + public MyId(global::System.Guid value) + { + Value = value; + } + + public static MyId New() => new MyId(global::System.Guid.NewGuid()); + public static readonly MyId Empty = new MyId(global::System.Guid.Empty); + + /// + public bool Equals(MyId other) => this.Value.Equals(other.Value); + public override bool Equals(object? obj) + { + if (ReferenceEquals(null, obj)) return false; + return obj is MyId other && Equals(other); + } + + public override int GetHashCode() => Value.GetHashCode(); + + public override string ToString() => Value.ToString(); + + public static bool operator ==(MyId a, MyId b) => a.Equals(b); + public static bool operator !=(MyId a, MyId b) => !(a == b); + public static bool operator > (MyId a, MyId b) => a.CompareTo(b) > 0; + public static bool operator < (MyId a, MyId b) => a.CompareTo(b) < 0; + public static bool operator >= (MyId a, MyId b) => a.CompareTo(b) >= 0; + public static bool operator <= (MyId a, MyId b) => a.CompareTo(b) <= 0; + + /// + public int CompareTo(MyId other) => Value.CompareTo(other.Value); + + public partial class MyIdTypeConverter : global::System.ComponentModel.TypeConverter + { + public override bool CanConvertFrom(global::System.ComponentModel.ITypeDescriptorContext? context, global::System.Type sourceType) + { + return sourceType == typeof(global::System.Guid) || sourceType == typeof(string) || base.CanConvertFrom(context, sourceType); + } + + public override object? ConvertFrom(global::System.ComponentModel.ITypeDescriptorContext? context, global::System.Globalization.CultureInfo? culture, object value) + { + return value switch + { + global::System.Guid guidValue => new MyId(guidValue), + string stringValue when !string.IsNullOrEmpty(stringValue) && global::System.Guid.TryParse(stringValue, out var result) => new MyId(result), + _ => base.ConvertFrom(context, culture, value), + }; + } + + public override bool CanConvertTo(global::System.ComponentModel.ITypeDescriptorContext? context, global::System.Type? sourceType) + { + return sourceType == typeof(global::System.Guid) || sourceType == typeof(string) || base.CanConvertTo(context, sourceType); + } + + public override object? ConvertTo(global::System.ComponentModel.ITypeDescriptorContext? context, global::System.Globalization.CultureInfo? culture, object? value, global::System.Type destinationType) + { + if (value is MyId idValue) + { + if (destinationType == typeof(global::System.Guid)) + { + return idValue.Value; + } + + if (destinationType == typeof(string)) + { + return idValue.Value.ToString(); + } + } + + return base.ConvertTo(context, culture, value, destinationType); + } + } + + public partial class MyIdSystemTextJsonConverter : global::System.Text.Json.Serialization.JsonConverter + { + public override bool CanConvert(global::System.Type typeToConvert) + => typeToConvert == typeof(global::System.Guid) || typeToConvert == typeof(string) || base.CanConvert(typeToConvert); + + public override MyId Read(ref global::System.Text.Json.Utf8JsonReader reader, global::System.Type typeToConvert, global::System.Text.Json.JsonSerializerOptions options) + => new (reader.GetGuid()); + + public override void Write(global::System.Text.Json.Utf8JsonWriter writer, MyId value, global::System.Text.Json.JsonSerializerOptions options) + => writer.WriteStringValue(value.Value); + +#if NET6_0_OR_GREATER + public override MyId ReadAsPropertyName(ref global::System.Text.Json.Utf8JsonReader reader, global::System.Type typeToConvert, global::System.Text.Json.JsonSerializerOptions options) + => new(global::System.Guid.Parse(reader.GetString() ?? throw new global::System.FormatException("The string for the MyId property was null"))); + + public override void WriteAsPropertyName(global::System.Text.Json.Utf8JsonWriter writer, MyId value, global::System.Text.Json.JsonSerializerOptions options) + => writer.WritePropertyName(value.Value.ToString()); +#endif + } + + public static MyId Parse(string input) + => new(global::System.Guid.Parse(input)); + +#if NET7_0_OR_GREATER + /// + public static MyId Parse(string input, global::System.IFormatProvider? provider) + => new(global::System.Guid.Parse(input, provider)); + + /// + public static bool TryParse( + [global::System.Diagnostics.CodeAnalysis.NotNullWhen(true)] string? input, + global::System.IFormatProvider? provider, + out MyId result) + { + if (input is null) + { + result = default; + return false; + } + + if (global::System.Guid.TryParse(input, provider, out var guid)) + { + result = new(guid); + return true; + } + else + { + result = default; + return false; + } + } +#endif + + /// + public string ToString( +#if NET7_0_OR_GREATER + [global::System.Diagnostics.CodeAnalysis.StringSyntax(global::System.Diagnostics.CodeAnalysis.StringSyntaxAttribute.GuidFormat)] +#endif + string? format, + global::System.IFormatProvider? formatProvider) + => Value.ToString(format, formatProvider); + +#if NETCOREAPP2_1_OR_GREATER + public static MyId Parse(global::System.ReadOnlySpan input) + => new(global::System.Guid.Parse(input)); +#endif + +#if NET6_0_OR_GREATER +#if NET7_0_OR_GREATER + /// +#endif + public static MyId Parse(global::System.ReadOnlySpan input, global::System.IFormatProvider? provider) +#if NET7_0_OR_GREATER + => new(global::System.Guid.Parse(input, provider)); +#else + => new(global::System.Guid.Parse(input)); +#endif + +#if NET7_0_OR_GREATER + /// +#endif + public static bool TryParse(global::System.ReadOnlySpan input, global::System.IFormatProvider? provider, out MyId result) + { +#if NET7_0_OR_GREATER + if (global::System.Guid.TryParse(input, provider, out var guid)) +#else + if (global::System.Guid.TryParse(input, out var guid)) +#endif + { + result = new(guid); + return true; + } + else + { + result = default; + return false; + } + } + + /// + public bool TryFormat( + global::System.Span destination, + out int charsWritten, +#if NET7_0_OR_GREATER + [global::System.Diagnostics.CodeAnalysis.StringSyntax(global::System.Diagnostics.CodeAnalysis.StringSyntaxAttribute.GuidFormat)] +#endif + global::System.ReadOnlySpan format, + global::System.IFormatProvider? provider) + => Value.TryFormat(destination, out charsWritten, format); + + /// + public bool TryFormat( + global::System.Span destination, + out int charsWritten, +#if NET7_0_OR_GREATER + [global::System.Diagnostics.CodeAnalysis.StringSyntax(global::System.Diagnostics.CodeAnalysis.StringSyntaxAttribute.GuidFormat)] +#endif + global::System.ReadOnlySpan format = default) + => Value.TryFormat(destination, out charsWritten, format); +#endif +#if NET8_0_OR_GREATER + /// + public bool TryFormat( + global::System.Span utf8Destination, + out int bytesWritten, + [global::System.Diagnostics.CodeAnalysis.StringSyntax(global::System.Diagnostics.CodeAnalysis.StringSyntaxAttribute.GuidFormat)] + global::System.ReadOnlySpan format, + global::System.IFormatProvider? provider) + => Value.TryFormat(utf8Destination, out bytesWritten, format); +#endif + } +} + +//------------------------------------------------------------------------------ +// +// This code was generated by the StronglyTypedId source generator +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +#pragma warning disable 1591 // publicly visible type or member must be documented + +#nullable enable +namespace SomeNamespace +{ + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("StronglyTypedId", "1.0.0-beta6")] + partial struct MyIdConverters + { + public partial class DapperTypeHandler : global::Dapper.SqlMapper.TypeHandler + { + public override void SetValue(global::System.Data.IDbDataParameter parameter, SomeNamespace.MyId value) + { + parameter.Value = value.Value; + } + + public override SomeNamespace.MyId Parse(object value) + { + return value switch + { + global::System.Guid guidValue => new SomeNamespace.MyId(guidValue), + string stringValue when !string.IsNullOrEmpty(stringValue) && global::System.Guid.TryParse(stringValue, out var result) => new SomeNamespace.MyId(result), + _ => throw new global::System.InvalidCastException($"Unable to cast object of type {value.GetType()} to SomeNamespace.MyId"), + }; + } + } + } +} diff --git a/test/StronglyTypedIds.Tests/Snapshots/StronglyTypedIdConverterTests.CanGenerateDefaultConverterIdInGlobalNamespace.verified.txt b/test/StronglyTypedIds.Tests/Snapshots/StronglyTypedIdConverterTests.CanGenerateDefaultConverterIdInGlobalNamespace.verified.txt new file mode 100644 index 000000000..fa0bd9ba1 --- /dev/null +++ b/test/StronglyTypedIds.Tests/Snapshots/StronglyTypedIdConverterTests.CanGenerateDefaultConverterIdInGlobalNamespace.verified.txt @@ -0,0 +1,265 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by the StronglyTypedId source generator +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +#pragma warning disable 1591 // publicly visible type or member must be documented + +#nullable enable + [global::System.ComponentModel.TypeConverter(typeof(MyIdTypeConverter))] + [global::System.Text.Json.Serialization.JsonConverter(typeof(MyIdSystemTextJsonConverter))] + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("StronglyTypedId", "1.0.0-beta6")] + partial struct MyId : +#if NET6_0_OR_GREATER + global::System.ISpanFormattable, +#endif +#if NET7_0_OR_GREATER + global::System.IParsable, global::System.ISpanParsable, +#endif +#if NET8_0_OR_GREATER + global::System.IUtf8SpanFormattable, +#endif + global::System.IComparable, global::System.IEquatable, global::System.IFormattable + { + public global::System.Guid Value { get; } + + public MyId(global::System.Guid value) + { + Value = value; + } + + public static MyId New() => new MyId(global::System.Guid.NewGuid()); + public static readonly MyId Empty = new MyId(global::System.Guid.Empty); + + /// + public bool Equals(MyId other) => this.Value.Equals(other.Value); + public override bool Equals(object? obj) + { + if (ReferenceEquals(null, obj)) return false; + return obj is MyId other && Equals(other); + } + + public override int GetHashCode() => Value.GetHashCode(); + + public override string ToString() => Value.ToString(); + + public static bool operator ==(MyId a, MyId b) => a.Equals(b); + public static bool operator !=(MyId a, MyId b) => !(a == b); + public static bool operator > (MyId a, MyId b) => a.CompareTo(b) > 0; + public static bool operator < (MyId a, MyId b) => a.CompareTo(b) < 0; + public static bool operator >= (MyId a, MyId b) => a.CompareTo(b) >= 0; + public static bool operator <= (MyId a, MyId b) => a.CompareTo(b) <= 0; + + /// + public int CompareTo(MyId other) => Value.CompareTo(other.Value); + + public partial class MyIdTypeConverter : global::System.ComponentModel.TypeConverter + { + public override bool CanConvertFrom(global::System.ComponentModel.ITypeDescriptorContext? context, global::System.Type sourceType) + { + return sourceType == typeof(global::System.Guid) || sourceType == typeof(string) || base.CanConvertFrom(context, sourceType); + } + + public override object? ConvertFrom(global::System.ComponentModel.ITypeDescriptorContext? context, global::System.Globalization.CultureInfo? culture, object value) + { + return value switch + { + global::System.Guid guidValue => new MyId(guidValue), + string stringValue when !string.IsNullOrEmpty(stringValue) && global::System.Guid.TryParse(stringValue, out var result) => new MyId(result), + _ => base.ConvertFrom(context, culture, value), + }; + } + + public override bool CanConvertTo(global::System.ComponentModel.ITypeDescriptorContext? context, global::System.Type? sourceType) + { + return sourceType == typeof(global::System.Guid) || sourceType == typeof(string) || base.CanConvertTo(context, sourceType); + } + + public override object? ConvertTo(global::System.ComponentModel.ITypeDescriptorContext? context, global::System.Globalization.CultureInfo? culture, object? value, global::System.Type destinationType) + { + if (value is MyId idValue) + { + if (destinationType == typeof(global::System.Guid)) + { + return idValue.Value; + } + + if (destinationType == typeof(string)) + { + return idValue.Value.ToString(); + } + } + + return base.ConvertTo(context, culture, value, destinationType); + } + } + + public partial class MyIdSystemTextJsonConverter : global::System.Text.Json.Serialization.JsonConverter + { + public override bool CanConvert(global::System.Type typeToConvert) + => typeToConvert == typeof(global::System.Guid) || typeToConvert == typeof(string) || base.CanConvert(typeToConvert); + + public override MyId Read(ref global::System.Text.Json.Utf8JsonReader reader, global::System.Type typeToConvert, global::System.Text.Json.JsonSerializerOptions options) + => new (reader.GetGuid()); + + public override void Write(global::System.Text.Json.Utf8JsonWriter writer, MyId value, global::System.Text.Json.JsonSerializerOptions options) + => writer.WriteStringValue(value.Value); + +#if NET6_0_OR_GREATER + public override MyId ReadAsPropertyName(ref global::System.Text.Json.Utf8JsonReader reader, global::System.Type typeToConvert, global::System.Text.Json.JsonSerializerOptions options) + => new(global::System.Guid.Parse(reader.GetString() ?? throw new global::System.FormatException("The string for the MyId property was null"))); + + public override void WriteAsPropertyName(global::System.Text.Json.Utf8JsonWriter writer, MyId value, global::System.Text.Json.JsonSerializerOptions options) + => writer.WritePropertyName(value.Value.ToString()); +#endif + } + + public static MyId Parse(string input) + => new(global::System.Guid.Parse(input)); + +#if NET7_0_OR_GREATER + /// + public static MyId Parse(string input, global::System.IFormatProvider? provider) + => new(global::System.Guid.Parse(input, provider)); + + /// + public static bool TryParse( + [global::System.Diagnostics.CodeAnalysis.NotNullWhen(true)] string? input, + global::System.IFormatProvider? provider, + out MyId result) + { + if (input is null) + { + result = default; + return false; + } + + if (global::System.Guid.TryParse(input, provider, out var guid)) + { + result = new(guid); + return true; + } + else + { + result = default; + return false; + } + } +#endif + + /// + public string ToString( +#if NET7_0_OR_GREATER + [global::System.Diagnostics.CodeAnalysis.StringSyntax(global::System.Diagnostics.CodeAnalysis.StringSyntaxAttribute.GuidFormat)] +#endif + string? format, + global::System.IFormatProvider? formatProvider) + => Value.ToString(format, formatProvider); + +#if NETCOREAPP2_1_OR_GREATER + public static MyId Parse(global::System.ReadOnlySpan input) + => new(global::System.Guid.Parse(input)); +#endif + +#if NET6_0_OR_GREATER +#if NET7_0_OR_GREATER + /// +#endif + public static MyId Parse(global::System.ReadOnlySpan input, global::System.IFormatProvider? provider) +#if NET7_0_OR_GREATER + => new(global::System.Guid.Parse(input, provider)); +#else + => new(global::System.Guid.Parse(input)); +#endif + +#if NET7_0_OR_GREATER + /// +#endif + public static bool TryParse(global::System.ReadOnlySpan input, global::System.IFormatProvider? provider, out MyId result) + { +#if NET7_0_OR_GREATER + if (global::System.Guid.TryParse(input, provider, out var guid)) +#else + if (global::System.Guid.TryParse(input, out var guid)) +#endif + { + result = new(guid); + return true; + } + else + { + result = default; + return false; + } + } + + /// + public bool TryFormat( + global::System.Span destination, + out int charsWritten, +#if NET7_0_OR_GREATER + [global::System.Diagnostics.CodeAnalysis.StringSyntax(global::System.Diagnostics.CodeAnalysis.StringSyntaxAttribute.GuidFormat)] +#endif + global::System.ReadOnlySpan format, + global::System.IFormatProvider? provider) + => Value.TryFormat(destination, out charsWritten, format); + + /// + public bool TryFormat( + global::System.Span destination, + out int charsWritten, +#if NET7_0_OR_GREATER + [global::System.Diagnostics.CodeAnalysis.StringSyntax(global::System.Diagnostics.CodeAnalysis.StringSyntaxAttribute.GuidFormat)] +#endif + global::System.ReadOnlySpan format = default) + => Value.TryFormat(destination, out charsWritten, format); +#endif +#if NET8_0_OR_GREATER + /// + public bool TryFormat( + global::System.Span utf8Destination, + out int bytesWritten, + [global::System.Diagnostics.CodeAnalysis.StringSyntax(global::System.Diagnostics.CodeAnalysis.StringSyntaxAttribute.GuidFormat)] + global::System.ReadOnlySpan format, + global::System.IFormatProvider? provider) + => Value.TryFormat(utf8Destination, out bytesWritten, format); +#endif + } + +//------------------------------------------------------------------------------ +// +// This code was generated by the StronglyTypedId source generator +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +#pragma warning disable 1591 // publicly visible type or member must be documented + +#nullable enable + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("StronglyTypedId", "1.0.0-beta6")] + partial struct MyIdConverters + { + public partial class DapperTypeHandler : global::Dapper.SqlMapper.TypeHandler + { + public override void SetValue(global::System.Data.IDbDataParameter parameter, MyId value) + { + parameter.Value = value.Value; + } + + public override MyId Parse(object value) + { + return value switch + { + global::System.Guid guidValue => new MyId(guidValue), + string stringValue when !string.IsNullOrEmpty(stringValue) && global::System.Guid.TryParse(stringValue, out var result) => new MyId(result), + _ => throw new global::System.InvalidCastException($"Unable to cast object of type {value.GetType()} to MyId"), + }; + } + } + } diff --git a/test/StronglyTypedIds.Tests/Snapshots/StronglyTypedIdConverterTests.CanGenerateDefaultConverterIdInNamespace.verified.txt b/test/StronglyTypedIds.Tests/Snapshots/StronglyTypedIdConverterTests.CanGenerateDefaultConverterIdInNamespace.verified.txt new file mode 100644 index 000000000..2cfd883a2 --- /dev/null +++ b/test/StronglyTypedIds.Tests/Snapshots/StronglyTypedIdConverterTests.CanGenerateDefaultConverterIdInNamespace.verified.txt @@ -0,0 +1,271 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by the StronglyTypedId source generator +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +#pragma warning disable 1591 // publicly visible type or member must be documented + +#nullable enable +namespace SomeNamespace +{ + [global::System.ComponentModel.TypeConverter(typeof(MyIdTypeConverter))] + [global::System.Text.Json.Serialization.JsonConverter(typeof(MyIdSystemTextJsonConverter))] + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("StronglyTypedId", "1.0.0-beta6")] + partial struct MyId : +#if NET6_0_OR_GREATER + global::System.ISpanFormattable, +#endif +#if NET7_0_OR_GREATER + global::System.IParsable, global::System.ISpanParsable, +#endif +#if NET8_0_OR_GREATER + global::System.IUtf8SpanFormattable, +#endif + global::System.IComparable, global::System.IEquatable, global::System.IFormattable + { + public global::System.Guid Value { get; } + + public MyId(global::System.Guid value) + { + Value = value; + } + + public static MyId New() => new MyId(global::System.Guid.NewGuid()); + public static readonly MyId Empty = new MyId(global::System.Guid.Empty); + + /// + public bool Equals(MyId other) => this.Value.Equals(other.Value); + public override bool Equals(object? obj) + { + if (ReferenceEquals(null, obj)) return false; + return obj is MyId other && Equals(other); + } + + public override int GetHashCode() => Value.GetHashCode(); + + public override string ToString() => Value.ToString(); + + public static bool operator ==(MyId a, MyId b) => a.Equals(b); + public static bool operator !=(MyId a, MyId b) => !(a == b); + public static bool operator > (MyId a, MyId b) => a.CompareTo(b) > 0; + public static bool operator < (MyId a, MyId b) => a.CompareTo(b) < 0; + public static bool operator >= (MyId a, MyId b) => a.CompareTo(b) >= 0; + public static bool operator <= (MyId a, MyId b) => a.CompareTo(b) <= 0; + + /// + public int CompareTo(MyId other) => Value.CompareTo(other.Value); + + public partial class MyIdTypeConverter : global::System.ComponentModel.TypeConverter + { + public override bool CanConvertFrom(global::System.ComponentModel.ITypeDescriptorContext? context, global::System.Type sourceType) + { + return sourceType == typeof(global::System.Guid) || sourceType == typeof(string) || base.CanConvertFrom(context, sourceType); + } + + public override object? ConvertFrom(global::System.ComponentModel.ITypeDescriptorContext? context, global::System.Globalization.CultureInfo? culture, object value) + { + return value switch + { + global::System.Guid guidValue => new MyId(guidValue), + string stringValue when !string.IsNullOrEmpty(stringValue) && global::System.Guid.TryParse(stringValue, out var result) => new MyId(result), + _ => base.ConvertFrom(context, culture, value), + }; + } + + public override bool CanConvertTo(global::System.ComponentModel.ITypeDescriptorContext? context, global::System.Type? sourceType) + { + return sourceType == typeof(global::System.Guid) || sourceType == typeof(string) || base.CanConvertTo(context, sourceType); + } + + public override object? ConvertTo(global::System.ComponentModel.ITypeDescriptorContext? context, global::System.Globalization.CultureInfo? culture, object? value, global::System.Type destinationType) + { + if (value is MyId idValue) + { + if (destinationType == typeof(global::System.Guid)) + { + return idValue.Value; + } + + if (destinationType == typeof(string)) + { + return idValue.Value.ToString(); + } + } + + return base.ConvertTo(context, culture, value, destinationType); + } + } + + public partial class MyIdSystemTextJsonConverter : global::System.Text.Json.Serialization.JsonConverter + { + public override bool CanConvert(global::System.Type typeToConvert) + => typeToConvert == typeof(global::System.Guid) || typeToConvert == typeof(string) || base.CanConvert(typeToConvert); + + public override MyId Read(ref global::System.Text.Json.Utf8JsonReader reader, global::System.Type typeToConvert, global::System.Text.Json.JsonSerializerOptions options) + => new (reader.GetGuid()); + + public override void Write(global::System.Text.Json.Utf8JsonWriter writer, MyId value, global::System.Text.Json.JsonSerializerOptions options) + => writer.WriteStringValue(value.Value); + +#if NET6_0_OR_GREATER + public override MyId ReadAsPropertyName(ref global::System.Text.Json.Utf8JsonReader reader, global::System.Type typeToConvert, global::System.Text.Json.JsonSerializerOptions options) + => new(global::System.Guid.Parse(reader.GetString() ?? throw new global::System.FormatException("The string for the MyId property was null"))); + + public override void WriteAsPropertyName(global::System.Text.Json.Utf8JsonWriter writer, MyId value, global::System.Text.Json.JsonSerializerOptions options) + => writer.WritePropertyName(value.Value.ToString()); +#endif + } + + public static MyId Parse(string input) + => new(global::System.Guid.Parse(input)); + +#if NET7_0_OR_GREATER + /// + public static MyId Parse(string input, global::System.IFormatProvider? provider) + => new(global::System.Guid.Parse(input, provider)); + + /// + public static bool TryParse( + [global::System.Diagnostics.CodeAnalysis.NotNullWhen(true)] string? input, + global::System.IFormatProvider? provider, + out MyId result) + { + if (input is null) + { + result = default; + return false; + } + + if (global::System.Guid.TryParse(input, provider, out var guid)) + { + result = new(guid); + return true; + } + else + { + result = default; + return false; + } + } +#endif + + /// + public string ToString( +#if NET7_0_OR_GREATER + [global::System.Diagnostics.CodeAnalysis.StringSyntax(global::System.Diagnostics.CodeAnalysis.StringSyntaxAttribute.GuidFormat)] +#endif + string? format, + global::System.IFormatProvider? formatProvider) + => Value.ToString(format, formatProvider); + +#if NETCOREAPP2_1_OR_GREATER + public static MyId Parse(global::System.ReadOnlySpan input) + => new(global::System.Guid.Parse(input)); +#endif + +#if NET6_0_OR_GREATER +#if NET7_0_OR_GREATER + /// +#endif + public static MyId Parse(global::System.ReadOnlySpan input, global::System.IFormatProvider? provider) +#if NET7_0_OR_GREATER + => new(global::System.Guid.Parse(input, provider)); +#else + => new(global::System.Guid.Parse(input)); +#endif + +#if NET7_0_OR_GREATER + /// +#endif + public static bool TryParse(global::System.ReadOnlySpan input, global::System.IFormatProvider? provider, out MyId result) + { +#if NET7_0_OR_GREATER + if (global::System.Guid.TryParse(input, provider, out var guid)) +#else + if (global::System.Guid.TryParse(input, out var guid)) +#endif + { + result = new(guid); + return true; + } + else + { + result = default; + return false; + } + } + + /// + public bool TryFormat( + global::System.Span destination, + out int charsWritten, +#if NET7_0_OR_GREATER + [global::System.Diagnostics.CodeAnalysis.StringSyntax(global::System.Diagnostics.CodeAnalysis.StringSyntaxAttribute.GuidFormat)] +#endif + global::System.ReadOnlySpan format, + global::System.IFormatProvider? provider) + => Value.TryFormat(destination, out charsWritten, format); + + /// + public bool TryFormat( + global::System.Span destination, + out int charsWritten, +#if NET7_0_OR_GREATER + [global::System.Diagnostics.CodeAnalysis.StringSyntax(global::System.Diagnostics.CodeAnalysis.StringSyntaxAttribute.GuidFormat)] +#endif + global::System.ReadOnlySpan format = default) + => Value.TryFormat(destination, out charsWritten, format); +#endif +#if NET8_0_OR_GREATER + /// + public bool TryFormat( + global::System.Span utf8Destination, + out int bytesWritten, + [global::System.Diagnostics.CodeAnalysis.StringSyntax(global::System.Diagnostics.CodeAnalysis.StringSyntaxAttribute.GuidFormat)] + global::System.ReadOnlySpan format, + global::System.IFormatProvider? provider) + => Value.TryFormat(utf8Destination, out bytesWritten, format); +#endif + } +} + +//------------------------------------------------------------------------------ +// +// This code was generated by the StronglyTypedId source generator +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +#pragma warning disable 1591 // publicly visible type or member must be documented + +#nullable enable +namespace SomeNamespace +{ + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("StronglyTypedId", "1.0.0-beta6")] + partial struct MyIdConverters + { + public partial class DapperTypeHandler : global::Dapper.SqlMapper.TypeHandler + { + public override void SetValue(global::System.Data.IDbDataParameter parameter, SomeNamespace.MyId value) + { + parameter.Value = value.Value; + } + + public override SomeNamespace.MyId Parse(object value) + { + return value switch + { + global::System.Guid guidValue => new SomeNamespace.MyId(guidValue), + string stringValue when !string.IsNullOrEmpty(stringValue) && global::System.Guid.TryParse(stringValue, out var result) => new SomeNamespace.MyId(result), + _ => throw new global::System.InvalidCastException($"Unable to cast object of type {value.GetType()} to SomeNamespace.MyId"), + }; + } + } + } +} diff --git a/test/StronglyTypedIds.Tests/Snapshots/StronglyTypedIdConverterTests.CanGenerateMultipleConvertersWithSameName.verified.txt b/test/StronglyTypedIds.Tests/Snapshots/StronglyTypedIdConverterTests.CanGenerateMultipleConvertersWithSameName.verified.txt new file mode 100644 index 000000000..a960e3c57 --- /dev/null +++ b/test/StronglyTypedIds.Tests/Snapshots/StronglyTypedIdConverterTests.CanGenerateMultipleConvertersWithSameName.verified.txt @@ -0,0 +1,314 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by the StronglyTypedId source generator +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +#pragma warning disable 1591 // publicly visible type or member must be documented + +#nullable enable + public partial class ParentClass + { + [global::System.ComponentModel.TypeConverter(typeof(MyIdTypeConverter))] + [global::System.Text.Json.Serialization.JsonConverter(typeof(MyIdSystemTextJsonConverter))] + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("StronglyTypedId", "1.0.0-beta6")] + partial struct MyId : +#if NET6_0_OR_GREATER + global::System.ISpanFormattable, +#endif +#if NET7_0_OR_GREATER + global::System.IParsable, global::System.ISpanParsable, +#endif +#if NET8_0_OR_GREATER + global::System.IUtf8SpanFormattable, +#endif + global::System.IComparable, global::System.IEquatable, global::System.IFormattable + { + public global::System.Guid Value { get; } + + public MyId(global::System.Guid value) + { + Value = value; + } + + public static MyId New() => new MyId(global::System.Guid.NewGuid()); + public static readonly MyId Empty = new MyId(global::System.Guid.Empty); + + /// + public bool Equals(MyId other) => this.Value.Equals(other.Value); + public override bool Equals(object? obj) + { + if (ReferenceEquals(null, obj)) return false; + return obj is MyId other && Equals(other); + } + + public override int GetHashCode() => Value.GetHashCode(); + + public override string ToString() => Value.ToString(); + + public static bool operator ==(MyId a, MyId b) => a.Equals(b); + public static bool operator !=(MyId a, MyId b) => !(a == b); + public static bool operator > (MyId a, MyId b) => a.CompareTo(b) > 0; + public static bool operator < (MyId a, MyId b) => a.CompareTo(b) < 0; + public static bool operator >= (MyId a, MyId b) => a.CompareTo(b) >= 0; + public static bool operator <= (MyId a, MyId b) => a.CompareTo(b) <= 0; + + /// + public int CompareTo(MyId other) => Value.CompareTo(other.Value); + + public partial class MyIdTypeConverter : global::System.ComponentModel.TypeConverter + { + public override bool CanConvertFrom(global::System.ComponentModel.ITypeDescriptorContext? context, global::System.Type sourceType) + { + return sourceType == typeof(global::System.Guid) || sourceType == typeof(string) || base.CanConvertFrom(context, sourceType); + } + + public override object? ConvertFrom(global::System.ComponentModel.ITypeDescriptorContext? context, global::System.Globalization.CultureInfo? culture, object value) + { + return value switch + { + global::System.Guid guidValue => new MyId(guidValue), + string stringValue when !string.IsNullOrEmpty(stringValue) && global::System.Guid.TryParse(stringValue, out var result) => new MyId(result), + _ => base.ConvertFrom(context, culture, value), + }; + } + + public override bool CanConvertTo(global::System.ComponentModel.ITypeDescriptorContext? context, global::System.Type? sourceType) + { + return sourceType == typeof(global::System.Guid) || sourceType == typeof(string) || base.CanConvertTo(context, sourceType); + } + + public override object? ConvertTo(global::System.ComponentModel.ITypeDescriptorContext? context, global::System.Globalization.CultureInfo? culture, object? value, global::System.Type destinationType) + { + if (value is MyId idValue) + { + if (destinationType == typeof(global::System.Guid)) + { + return idValue.Value; + } + + if (destinationType == typeof(string)) + { + return idValue.Value.ToString(); + } + } + + return base.ConvertTo(context, culture, value, destinationType); + } + } + + public partial class MyIdSystemTextJsonConverter : global::System.Text.Json.Serialization.JsonConverter + { + public override bool CanConvert(global::System.Type typeToConvert) + => typeToConvert == typeof(global::System.Guid) || typeToConvert == typeof(string) || base.CanConvert(typeToConvert); + + public override MyId Read(ref global::System.Text.Json.Utf8JsonReader reader, global::System.Type typeToConvert, global::System.Text.Json.JsonSerializerOptions options) + => new (reader.GetGuid()); + + public override void Write(global::System.Text.Json.Utf8JsonWriter writer, MyId value, global::System.Text.Json.JsonSerializerOptions options) + => writer.WriteStringValue(value.Value); + +#if NET6_0_OR_GREATER + public override MyId ReadAsPropertyName(ref global::System.Text.Json.Utf8JsonReader reader, global::System.Type typeToConvert, global::System.Text.Json.JsonSerializerOptions options) + => new(global::System.Guid.Parse(reader.GetString() ?? throw new global::System.FormatException("The string for the MyId property was null"))); + + public override void WriteAsPropertyName(global::System.Text.Json.Utf8JsonWriter writer, MyId value, global::System.Text.Json.JsonSerializerOptions options) + => writer.WritePropertyName(value.Value.ToString()); +#endif + } + + public static MyId Parse(string input) + => new(global::System.Guid.Parse(input)); + +#if NET7_0_OR_GREATER + /// + public static MyId Parse(string input, global::System.IFormatProvider? provider) + => new(global::System.Guid.Parse(input, provider)); + + /// + public static bool TryParse( + [global::System.Diagnostics.CodeAnalysis.NotNullWhen(true)] string? input, + global::System.IFormatProvider? provider, + out MyId result) + { + if (input is null) + { + result = default; + return false; + } + + if (global::System.Guid.TryParse(input, provider, out var guid)) + { + result = new(guid); + return true; + } + else + { + result = default; + return false; + } + } +#endif + + /// + public string ToString( +#if NET7_0_OR_GREATER + [global::System.Diagnostics.CodeAnalysis.StringSyntax(global::System.Diagnostics.CodeAnalysis.StringSyntaxAttribute.GuidFormat)] +#endif + string? format, + global::System.IFormatProvider? formatProvider) + => Value.ToString(format, formatProvider); + +#if NETCOREAPP2_1_OR_GREATER + public static MyId Parse(global::System.ReadOnlySpan input) + => new(global::System.Guid.Parse(input)); +#endif + +#if NET6_0_OR_GREATER +#if NET7_0_OR_GREATER + /// +#endif + public static MyId Parse(global::System.ReadOnlySpan input, global::System.IFormatProvider? provider) +#if NET7_0_OR_GREATER + => new(global::System.Guid.Parse(input, provider)); +#else + => new(global::System.Guid.Parse(input)); +#endif + +#if NET7_0_OR_GREATER + /// +#endif + public static bool TryParse(global::System.ReadOnlySpan input, global::System.IFormatProvider? provider, out MyId result) + { +#if NET7_0_OR_GREATER + if (global::System.Guid.TryParse(input, provider, out var guid)) +#else + if (global::System.Guid.TryParse(input, out var guid)) +#endif + { + result = new(guid); + return true; + } + else + { + result = default; + return false; + } + } + + /// + public bool TryFormat( + global::System.Span destination, + out int charsWritten, +#if NET7_0_OR_GREATER + [global::System.Diagnostics.CodeAnalysis.StringSyntax(global::System.Diagnostics.CodeAnalysis.StringSyntaxAttribute.GuidFormat)] +#endif + global::System.ReadOnlySpan format, + global::System.IFormatProvider? provider) + => Value.TryFormat(destination, out charsWritten, format); + + /// + public bool TryFormat( + global::System.Span destination, + out int charsWritten, +#if NET7_0_OR_GREATER + [global::System.Diagnostics.CodeAnalysis.StringSyntax(global::System.Diagnostics.CodeAnalysis.StringSyntaxAttribute.GuidFormat)] +#endif + global::System.ReadOnlySpan format = default) + => Value.TryFormat(destination, out charsWritten, format); +#endif +#if NET8_0_OR_GREATER + /// + public bool TryFormat( + global::System.Span utf8Destination, + out int bytesWritten, + [global::System.Diagnostics.CodeAnalysis.StringSyntax(global::System.Diagnostics.CodeAnalysis.StringSyntaxAttribute.GuidFormat)] + global::System.ReadOnlySpan format, + global::System.IFormatProvider? provider) + => Value.TryFormat(utf8Destination, out bytesWritten, format); +#endif + } + } + +//------------------------------------------------------------------------------ +// +// This code was generated by the StronglyTypedId source generator +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +#pragma warning disable 1591 // publicly visible type or member must be documented + +#nullable enable +namespace MyContracts.V1 +{ + public partial class ConverterClass + { + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("StronglyTypedId", "1.0.0-beta6")] + partial struct MyIdConverters + { + public partial class DapperTypeHandler : global::Dapper.SqlMapper.TypeHandler + { + public override void SetValue(global::System.Data.IDbDataParameter parameter, MyId value) + { + parameter.Value = value.Value; + } + + public override MyId Parse(object value) + { + return value switch + { + global::System.Guid guidValue => new MyId(guidValue), + string stringValue when !string.IsNullOrEmpty(stringValue) && global::System.Guid.TryParse(stringValue, out var result) => new MyId(result), + _ => throw new global::System.InvalidCastException($"Unable to cast object of type {value.GetType()} to MyId"), + }; + } + } + } + } +} + +//------------------------------------------------------------------------------ +// +// This code was generated by the StronglyTypedId source generator +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +#pragma warning disable 1591 // publicly visible type or member must be documented + +#nullable enable +namespace MyContracts.V2 +{ + public partial class ConverterClass + { + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("StronglyTypedId", "1.0.0-beta6")] + partial struct MyIdConverters + { + public partial class DapperTypeHandler : global::Dapper.SqlMapper.TypeHandler + { + public override void SetValue(global::System.Data.IDbDataParameter parameter, MyId value) + { + parameter.Value = value.Value; + } + + public override MyId Parse(object value) + { + return value switch + { + global::System.Guid guidValue => new MyId(guidValue), + string stringValue when !string.IsNullOrEmpty(stringValue) && global::System.Guid.TryParse(stringValue, out var result) => new MyId(result), + _ => throw new global::System.InvalidCastException($"Unable to cast object of type {value.GetType()} to MyId"), + }; + } + } + } + } +} diff --git a/test/StronglyTypedIds.Tests/Snapshots/StronglyTypedIdConverterTests.CanGenerateNestedIdInFileScopeNamespace.verified.txt b/test/StronglyTypedIds.Tests/Snapshots/StronglyTypedIdConverterTests.CanGenerateNestedIdInFileScopeNamespace.verified.txt new file mode 100644 index 000000000..c3bda2a74 --- /dev/null +++ b/test/StronglyTypedIds.Tests/Snapshots/StronglyTypedIdConverterTests.CanGenerateNestedIdInFileScopeNamespace.verified.txt @@ -0,0 +1,277 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by the StronglyTypedId source generator +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +#pragma warning disable 1591 // publicly visible type or member must be documented + +#nullable enable +namespace SomeNamespace +{ + public partial class ParentClass + { + [global::System.ComponentModel.TypeConverter(typeof(MyIdTypeConverter))] + [global::System.Text.Json.Serialization.JsonConverter(typeof(MyIdSystemTextJsonConverter))] + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("StronglyTypedId", "1.0.0-beta6")] + partial struct MyId : +#if NET6_0_OR_GREATER + global::System.ISpanFormattable, +#endif +#if NET7_0_OR_GREATER + global::System.IParsable, global::System.ISpanParsable, +#endif +#if NET8_0_OR_GREATER + global::System.IUtf8SpanFormattable, +#endif + global::System.IComparable, global::System.IEquatable, global::System.IFormattable + { + public global::System.Guid Value { get; } + + public MyId(global::System.Guid value) + { + Value = value; + } + + public static MyId New() => new MyId(global::System.Guid.NewGuid()); + public static readonly MyId Empty = new MyId(global::System.Guid.Empty); + + /// + public bool Equals(MyId other) => this.Value.Equals(other.Value); + public override bool Equals(object? obj) + { + if (ReferenceEquals(null, obj)) return false; + return obj is MyId other && Equals(other); + } + + public override int GetHashCode() => Value.GetHashCode(); + + public override string ToString() => Value.ToString(); + + public static bool operator ==(MyId a, MyId b) => a.Equals(b); + public static bool operator !=(MyId a, MyId b) => !(a == b); + public static bool operator > (MyId a, MyId b) => a.CompareTo(b) > 0; + public static bool operator < (MyId a, MyId b) => a.CompareTo(b) < 0; + public static bool operator >= (MyId a, MyId b) => a.CompareTo(b) >= 0; + public static bool operator <= (MyId a, MyId b) => a.CompareTo(b) <= 0; + + /// + public int CompareTo(MyId other) => Value.CompareTo(other.Value); + + public partial class MyIdTypeConverter : global::System.ComponentModel.TypeConverter + { + public override bool CanConvertFrom(global::System.ComponentModel.ITypeDescriptorContext? context, global::System.Type sourceType) + { + return sourceType == typeof(global::System.Guid) || sourceType == typeof(string) || base.CanConvertFrom(context, sourceType); + } + + public override object? ConvertFrom(global::System.ComponentModel.ITypeDescriptorContext? context, global::System.Globalization.CultureInfo? culture, object value) + { + return value switch + { + global::System.Guid guidValue => new MyId(guidValue), + string stringValue when !string.IsNullOrEmpty(stringValue) && global::System.Guid.TryParse(stringValue, out var result) => new MyId(result), + _ => base.ConvertFrom(context, culture, value), + }; + } + + public override bool CanConvertTo(global::System.ComponentModel.ITypeDescriptorContext? context, global::System.Type? sourceType) + { + return sourceType == typeof(global::System.Guid) || sourceType == typeof(string) || base.CanConvertTo(context, sourceType); + } + + public override object? ConvertTo(global::System.ComponentModel.ITypeDescriptorContext? context, global::System.Globalization.CultureInfo? culture, object? value, global::System.Type destinationType) + { + if (value is MyId idValue) + { + if (destinationType == typeof(global::System.Guid)) + { + return idValue.Value; + } + + if (destinationType == typeof(string)) + { + return idValue.Value.ToString(); + } + } + + return base.ConvertTo(context, culture, value, destinationType); + } + } + + public partial class MyIdSystemTextJsonConverter : global::System.Text.Json.Serialization.JsonConverter + { + public override bool CanConvert(global::System.Type typeToConvert) + => typeToConvert == typeof(global::System.Guid) || typeToConvert == typeof(string) || base.CanConvert(typeToConvert); + + public override MyId Read(ref global::System.Text.Json.Utf8JsonReader reader, global::System.Type typeToConvert, global::System.Text.Json.JsonSerializerOptions options) + => new (reader.GetGuid()); + + public override void Write(global::System.Text.Json.Utf8JsonWriter writer, MyId value, global::System.Text.Json.JsonSerializerOptions options) + => writer.WriteStringValue(value.Value); + +#if NET6_0_OR_GREATER + public override MyId ReadAsPropertyName(ref global::System.Text.Json.Utf8JsonReader reader, global::System.Type typeToConvert, global::System.Text.Json.JsonSerializerOptions options) + => new(global::System.Guid.Parse(reader.GetString() ?? throw new global::System.FormatException("The string for the MyId property was null"))); + + public override void WriteAsPropertyName(global::System.Text.Json.Utf8JsonWriter writer, MyId value, global::System.Text.Json.JsonSerializerOptions options) + => writer.WritePropertyName(value.Value.ToString()); +#endif + } + + public static MyId Parse(string input) + => new(global::System.Guid.Parse(input)); + +#if NET7_0_OR_GREATER + /// + public static MyId Parse(string input, global::System.IFormatProvider? provider) + => new(global::System.Guid.Parse(input, provider)); + + /// + public static bool TryParse( + [global::System.Diagnostics.CodeAnalysis.NotNullWhen(true)] string? input, + global::System.IFormatProvider? provider, + out MyId result) + { + if (input is null) + { + result = default; + return false; + } + + if (global::System.Guid.TryParse(input, provider, out var guid)) + { + result = new(guid); + return true; + } + else + { + result = default; + return false; + } + } +#endif + + /// + public string ToString( +#if NET7_0_OR_GREATER + [global::System.Diagnostics.CodeAnalysis.StringSyntax(global::System.Diagnostics.CodeAnalysis.StringSyntaxAttribute.GuidFormat)] +#endif + string? format, + global::System.IFormatProvider? formatProvider) + => Value.ToString(format, formatProvider); + +#if NETCOREAPP2_1_OR_GREATER + public static MyId Parse(global::System.ReadOnlySpan input) + => new(global::System.Guid.Parse(input)); +#endif + +#if NET6_0_OR_GREATER +#if NET7_0_OR_GREATER + /// +#endif + public static MyId Parse(global::System.ReadOnlySpan input, global::System.IFormatProvider? provider) +#if NET7_0_OR_GREATER + => new(global::System.Guid.Parse(input, provider)); +#else + => new(global::System.Guid.Parse(input)); +#endif + +#if NET7_0_OR_GREATER + /// +#endif + public static bool TryParse(global::System.ReadOnlySpan input, global::System.IFormatProvider? provider, out MyId result) + { +#if NET7_0_OR_GREATER + if (global::System.Guid.TryParse(input, provider, out var guid)) +#else + if (global::System.Guid.TryParse(input, out var guid)) +#endif + { + result = new(guid); + return true; + } + else + { + result = default; + return false; + } + } + + /// + public bool TryFormat( + global::System.Span destination, + out int charsWritten, +#if NET7_0_OR_GREATER + [global::System.Diagnostics.CodeAnalysis.StringSyntax(global::System.Diagnostics.CodeAnalysis.StringSyntaxAttribute.GuidFormat)] +#endif + global::System.ReadOnlySpan format, + global::System.IFormatProvider? provider) + => Value.TryFormat(destination, out charsWritten, format); + + /// + public bool TryFormat( + global::System.Span destination, + out int charsWritten, +#if NET7_0_OR_GREATER + [global::System.Diagnostics.CodeAnalysis.StringSyntax(global::System.Diagnostics.CodeAnalysis.StringSyntaxAttribute.GuidFormat)] +#endif + global::System.ReadOnlySpan format = default) + => Value.TryFormat(destination, out charsWritten, format); +#endif +#if NET8_0_OR_GREATER + /// + public bool TryFormat( + global::System.Span utf8Destination, + out int bytesWritten, + [global::System.Diagnostics.CodeAnalysis.StringSyntax(global::System.Diagnostics.CodeAnalysis.StringSyntaxAttribute.GuidFormat)] + global::System.ReadOnlySpan format, + global::System.IFormatProvider? provider) + => Value.TryFormat(utf8Destination, out bytesWritten, format); +#endif + } + } +} + +//------------------------------------------------------------------------------ +// +// This code was generated by the StronglyTypedId source generator +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +#pragma warning disable 1591 // publicly visible type or member must be documented + +#nullable enable +namespace SomeNamespace +{ + public partial class ConverterClass + { + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("StronglyTypedId", "1.0.0-beta6")] + partial struct MyIdConverters + { + public partial class DapperTypeHandler : global::Dapper.SqlMapper.TypeHandler + { + public override void SetValue(global::System.Data.IDbDataParameter parameter, MyId value) + { + parameter.Value = value.Value; + } + + public override MyId Parse(object value) + { + return value switch + { + global::System.Guid guidValue => new MyId(guidValue), + string stringValue when !string.IsNullOrEmpty(stringValue) && global::System.Guid.TryParse(stringValue, out var result) => new MyId(result), + _ => throw new global::System.InvalidCastException($"Unable to cast object of type {value.GetType()} to MyId"), + }; + } + } + } + } +} diff --git a/test/StronglyTypedIds.Tests/Snapshots/StronglyTypedIdConverterTests.CanGenerateNonDefaultConverterIdInNamespace.verified.txt b/test/StronglyTypedIds.Tests/Snapshots/StronglyTypedIdConverterTests.CanGenerateNonDefaultConverterIdInNamespace.verified.txt new file mode 100644 index 000000000..6972e8a1f --- /dev/null +++ b/test/StronglyTypedIds.Tests/Snapshots/StronglyTypedIdConverterTests.CanGenerateNonDefaultConverterIdInNamespace.verified.txt @@ -0,0 +1,286 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by the StronglyTypedId source generator +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +#pragma warning disable 1591 // publicly visible type or member must be documented + +#nullable enable +namespace SomeNamespace +{ + [global::System.ComponentModel.TypeConverter(typeof(MyIdTypeConverter))] + [global::System.Text.Json.Serialization.JsonConverter(typeof(MyIdSystemTextJsonConverter))] + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("StronglyTypedId", "1.0.0-beta6")] + partial struct MyId : +#if NET6_0_OR_GREATER + global::System.ISpanFormattable, +#endif +#if NET7_0_OR_GREATER + global::System.IParsable, global::System.ISpanParsable, +#endif +#if NET8_0_OR_GREATER + global::System.IUtf8SpanParsable, global::System.IUtf8SpanFormattable, +#endif + global::System.IComparable, global::System.IEquatable, global::System.IFormattable + { + public int Value { get; } + + public MyId(int value) + { + Value = value; + } + + public static readonly MyId Empty = new MyId(0); + + /// + public bool Equals(MyId other) => this.Value.Equals(other.Value); + public override bool Equals(object? obj) + { + if (ReferenceEquals(null, obj)) return false; + return obj is MyId other && Equals(other); + } + + public override int GetHashCode() => Value.GetHashCode(); + + public override string ToString() => Value.ToString(global::System.Globalization.CultureInfo.InvariantCulture); + + public static bool operator ==(MyId a, MyId b) => a.Equals(b); + public static bool operator !=(MyId a, MyId b) => !(a == b); + public static bool operator > (MyId a, MyId b) => a.CompareTo(b) > 0; + public static bool operator < (MyId a, MyId b) => a.CompareTo(b) < 0; + public static bool operator >= (MyId a, MyId b) => a.CompareTo(b) >= 0; + public static bool operator <= (MyId a, MyId b) => a.CompareTo(b) <= 0; + + /// + public int CompareTo(MyId other) => Value.CompareTo(other.Value); + + public partial class MyIdTypeConverter : global::System.ComponentModel.TypeConverter + { + public override bool CanConvertFrom(global::System.ComponentModel.ITypeDescriptorContext? context, global::System.Type sourceType) + { + return sourceType == typeof(int) || sourceType == typeof(string) || base.CanConvertFrom(context, sourceType); + } + + public override object? ConvertFrom(global::System.ComponentModel.ITypeDescriptorContext? context, global::System.Globalization.CultureInfo? culture, object value) + { + return value switch + { + int intValue => new MyId(intValue), + string stringValue when !string.IsNullOrEmpty(stringValue) && int.TryParse(stringValue, out var result) => new MyId(result), + _ => base.ConvertFrom(context, culture, value), + }; + } + + public override bool CanConvertTo(global::System.ComponentModel.ITypeDescriptorContext? context, global::System.Type? sourceType) + { + return sourceType == typeof(int) || sourceType == typeof(string) || base.CanConvertTo(context, sourceType); + } + + public override object? ConvertTo(global::System.ComponentModel.ITypeDescriptorContext? context, global::System.Globalization.CultureInfo? culture, object? value, global::System.Type destinationType) + { + if (value is MyId idValue) + { + if (destinationType == typeof(int)) + { + return idValue.Value; + } + + if (destinationType == typeof(string)) + { + return idValue.Value.ToString(global::System.Globalization.CultureInfo.InvariantCulture); + } + } + + return base.ConvertTo(context, culture, value, destinationType); + } + } + + public partial class MyIdSystemTextJsonConverter : global::System.Text.Json.Serialization.JsonConverter + { + public override bool CanConvert(global::System.Type typeToConvert) + => typeToConvert == typeof(int) || typeToConvert == typeof(string) || base.CanConvert(typeToConvert); + + public override MyId Read(ref global::System.Text.Json.Utf8JsonReader reader, global::System.Type typeToConvert, global::System.Text.Json.JsonSerializerOptions options) + => new (reader.GetInt32()); + + public override void Write(global::System.Text.Json.Utf8JsonWriter writer, MyId value, global::System.Text.Json.JsonSerializerOptions options) + => writer.WriteNumberValue(value.Value); + +#if NET6_0_OR_GREATER + public override MyId ReadAsPropertyName(ref global::System.Text.Json.Utf8JsonReader reader, global::System.Type typeToConvert, global::System.Text.Json.JsonSerializerOptions options) + => new(int.Parse(reader.GetString() ?? throw new global::System.FormatException("The string for the MyId property was null"))); + + public override void WriteAsPropertyName(global::System.Text.Json.Utf8JsonWriter writer, MyId value, global::System.Text.Json.JsonSerializerOptions options) + => writer.WritePropertyName(value.Value.ToString(global::System.Globalization.CultureInfo.InvariantCulture)); +#endif + } + + public static MyId Parse(string input) + => new(int.Parse(input)); + +#if NET7_0_OR_GREATER + /// + public static MyId Parse(string input, global::System.IFormatProvider? provider) + => new(int.Parse(input, provider)); + + /// + public static bool TryParse( + [global::System.Diagnostics.CodeAnalysis.NotNullWhen(true)] string? input, + global::System.IFormatProvider? provider, + out MyId result) + { + if (input is null) + { + result = default; + return false; + } + + if (int.TryParse(input, provider, out var value)) + { + result = new(value); + return true; + } + + result = default; + return false; + } +#endif + + /// + public string ToString( +#if NET7_0_OR_GREATER + [global::System.Diagnostics.CodeAnalysis.StringSyntax(global::System.Diagnostics.CodeAnalysis.StringSyntaxAttribute.NumericFormat)] +#endif + string? format, + global::System.IFormatProvider? formatProvider) + => Value.ToString(format, formatProvider); + +#if NETCOREAPP2_1_OR_GREATER + public static MyId Parse(global::System.ReadOnlySpan input) + => new(int.Parse(input)); +#endif + +#if NET6_0_OR_GREATER +#if NET7_0_OR_GREATER + /// +#endif + public static MyId Parse(global::System.ReadOnlySpan input, global::System.IFormatProvider? provider) +#if NET7_0_OR_GREATER + => new(int.Parse(input, provider)); +#else + => new(int.Parse(input)); +#endif + +#if NET7_0_OR_GREATER + /// +#endif + public static bool TryParse(global::System.ReadOnlySpan input, global::System.IFormatProvider? provider, out MyId result) + { +#if NET7_0_OR_GREATER + if (int.TryParse(input, provider, out var value)) +#else + if (int.TryParse(input, out var value)) +#endif + { + result = new(value); + return true; + } + + result = default; + return false; + } + + /// + public bool TryFormat( + global::System.Span destination, + out int charsWritten, +#if NET7_0_OR_GREATER + [global::System.Diagnostics.CodeAnalysis.StringSyntax(global::System.Diagnostics.CodeAnalysis.StringSyntaxAttribute.NumericFormat)] +#endif + global::System.ReadOnlySpan format, + global::System.IFormatProvider? provider) + => Value.TryFormat(destination, out charsWritten, format); + + /// + public bool TryFormat( + global::System.Span destination, + out int charsWritten, +#if NET7_0_OR_GREATER + [global::System.Diagnostics.CodeAnalysis.StringSyntax(global::System.Diagnostics.CodeAnalysis.StringSyntaxAttribute.NumericFormat)] +#endif + global::System.ReadOnlySpan format = default) + => Value.TryFormat(destination, out charsWritten, format); +#endif +#if NET8_0_OR_GREATER + /// + public bool TryFormat( + global::System.Span utf8Destination, + out int bytesWritten, + [global::System.Diagnostics.CodeAnalysis.StringSyntax(global::System.Diagnostics.CodeAnalysis.StringSyntaxAttribute.NumericFormat)] + global::System.ReadOnlySpan format = default, + global::System.IFormatProvider? provider = null) + => Value.TryFormat(utf8Destination, out bytesWritten, format, provider); + + /// + public static MyId Parse(global::System.ReadOnlySpan utf8Text, global::System.IFormatProvider? provider) + => new(int.Parse(utf8Text, provider)); + + /// + public static bool TryParse(global::System.ReadOnlySpan utf8Text, global::System.IFormatProvider? provider, out MyId result) + { + if (int.TryParse(utf8Text, provider, out var intResult)) + { + result = new MyId(intResult); + return true; + } + + result = default; + return false; + } +#endif + } +} + +//------------------------------------------------------------------------------ +// +// This code was generated by the StronglyTypedId source generator +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +#pragma warning disable 1591 // publicly visible type or member must be documented + +#nullable enable +namespace SomeNamespace +{ + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("StronglyTypedId", "1.0.0-beta6")] + partial struct MyIdConverters + { + public partial class DapperTypeHandler : global::Dapper.SqlMapper.TypeHandler + { + public override void SetValue(global::System.Data.IDbDataParameter parameter, SomeNamespace.MyId value) + { + parameter.Value = value.Value; + } + + public override SomeNamespace.MyId Parse(object value) + { + return value switch + { + int intValue => new SomeNamespace.MyId(intValue), + short shortValue => new SomeNamespace.MyId(shortValue), + long longValue and < int.MaxValue and > int.MinValue => new SomeNamespace.MyId((int)longValue), + decimal decimalValue and < int.MaxValue and > int.MinValue => new SomeNamespace.MyId((int)decimalValue), + string stringValue when !string.IsNullOrEmpty(stringValue) && int.TryParse(stringValue, out var result) => new SomeNamespace.MyId(result), + _ => throw new global::System.InvalidCastException($"Unable to cast object of type {value.GetType()} to SomeNamespace.MyId"), + }; + } + } + } +} diff --git a/test/StronglyTypedIds.Tests/Snapshots/StronglyTypedIdConverterTests.DefaultConverterIdInGlobalNamespaceWithoutDefaultsDoesntGenerateConverters.verified.txt b/test/StronglyTypedIds.Tests/Snapshots/StronglyTypedIdConverterTests.DefaultConverterIdInGlobalNamespaceWithoutDefaultsDoesntGenerateConverters.verified.txt new file mode 100644 index 000000000..77006d100 --- /dev/null +++ b/test/StronglyTypedIds.Tests/Snapshots/StronglyTypedIdConverterTests.DefaultConverterIdInGlobalNamespaceWithoutDefaultsDoesntGenerateConverters.verified.txt @@ -0,0 +1,231 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by the StronglyTypedId source generator +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +#pragma warning disable 1591 // publicly visible type or member must be documented + +#nullable enable + [global::System.ComponentModel.TypeConverter(typeof(MyIdTypeConverter))] + [global::System.Text.Json.Serialization.JsonConverter(typeof(MyIdSystemTextJsonConverter))] + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("StronglyTypedId", "1.0.0-beta6")] + partial struct MyId : +#if NET6_0_OR_GREATER + global::System.ISpanFormattable, +#endif +#if NET7_0_OR_GREATER + global::System.IParsable, global::System.ISpanParsable, +#endif +#if NET8_0_OR_GREATER + global::System.IUtf8SpanFormattable, +#endif + global::System.IComparable, global::System.IEquatable, global::System.IFormattable + { + public global::System.Guid Value { get; } + + public MyId(global::System.Guid value) + { + Value = value; + } + + public static MyId New() => new MyId(global::System.Guid.NewGuid()); + public static readonly MyId Empty = new MyId(global::System.Guid.Empty); + + /// + public bool Equals(MyId other) => this.Value.Equals(other.Value); + public override bool Equals(object? obj) + { + if (ReferenceEquals(null, obj)) return false; + return obj is MyId other && Equals(other); + } + + public override int GetHashCode() => Value.GetHashCode(); + + public override string ToString() => Value.ToString(); + + public static bool operator ==(MyId a, MyId b) => a.Equals(b); + public static bool operator !=(MyId a, MyId b) => !(a == b); + public static bool operator > (MyId a, MyId b) => a.CompareTo(b) > 0; + public static bool operator < (MyId a, MyId b) => a.CompareTo(b) < 0; + public static bool operator >= (MyId a, MyId b) => a.CompareTo(b) >= 0; + public static bool operator <= (MyId a, MyId b) => a.CompareTo(b) <= 0; + + /// + public int CompareTo(MyId other) => Value.CompareTo(other.Value); + + public partial class MyIdTypeConverter : global::System.ComponentModel.TypeConverter + { + public override bool CanConvertFrom(global::System.ComponentModel.ITypeDescriptorContext? context, global::System.Type sourceType) + { + return sourceType == typeof(global::System.Guid) || sourceType == typeof(string) || base.CanConvertFrom(context, sourceType); + } + + public override object? ConvertFrom(global::System.ComponentModel.ITypeDescriptorContext? context, global::System.Globalization.CultureInfo? culture, object value) + { + return value switch + { + global::System.Guid guidValue => new MyId(guidValue), + string stringValue when !string.IsNullOrEmpty(stringValue) && global::System.Guid.TryParse(stringValue, out var result) => new MyId(result), + _ => base.ConvertFrom(context, culture, value), + }; + } + + public override bool CanConvertTo(global::System.ComponentModel.ITypeDescriptorContext? context, global::System.Type? sourceType) + { + return sourceType == typeof(global::System.Guid) || sourceType == typeof(string) || base.CanConvertTo(context, sourceType); + } + + public override object? ConvertTo(global::System.ComponentModel.ITypeDescriptorContext? context, global::System.Globalization.CultureInfo? culture, object? value, global::System.Type destinationType) + { + if (value is MyId idValue) + { + if (destinationType == typeof(global::System.Guid)) + { + return idValue.Value; + } + + if (destinationType == typeof(string)) + { + return idValue.Value.ToString(); + } + } + + return base.ConvertTo(context, culture, value, destinationType); + } + } + + public partial class MyIdSystemTextJsonConverter : global::System.Text.Json.Serialization.JsonConverter + { + public override bool CanConvert(global::System.Type typeToConvert) + => typeToConvert == typeof(global::System.Guid) || typeToConvert == typeof(string) || base.CanConvert(typeToConvert); + + public override MyId Read(ref global::System.Text.Json.Utf8JsonReader reader, global::System.Type typeToConvert, global::System.Text.Json.JsonSerializerOptions options) + => new (reader.GetGuid()); + + public override void Write(global::System.Text.Json.Utf8JsonWriter writer, MyId value, global::System.Text.Json.JsonSerializerOptions options) + => writer.WriteStringValue(value.Value); + +#if NET6_0_OR_GREATER + public override MyId ReadAsPropertyName(ref global::System.Text.Json.Utf8JsonReader reader, global::System.Type typeToConvert, global::System.Text.Json.JsonSerializerOptions options) + => new(global::System.Guid.Parse(reader.GetString() ?? throw new global::System.FormatException("The string for the MyId property was null"))); + + public override void WriteAsPropertyName(global::System.Text.Json.Utf8JsonWriter writer, MyId value, global::System.Text.Json.JsonSerializerOptions options) + => writer.WritePropertyName(value.Value.ToString()); +#endif + } + + public static MyId Parse(string input) + => new(global::System.Guid.Parse(input)); + +#if NET7_0_OR_GREATER + /// + public static MyId Parse(string input, global::System.IFormatProvider? provider) + => new(global::System.Guid.Parse(input, provider)); + + /// + public static bool TryParse( + [global::System.Diagnostics.CodeAnalysis.NotNullWhen(true)] string? input, + global::System.IFormatProvider? provider, + out MyId result) + { + if (input is null) + { + result = default; + return false; + } + + if (global::System.Guid.TryParse(input, provider, out var guid)) + { + result = new(guid); + return true; + } + else + { + result = default; + return false; + } + } +#endif + + /// + public string ToString( +#if NET7_0_OR_GREATER + [global::System.Diagnostics.CodeAnalysis.StringSyntax(global::System.Diagnostics.CodeAnalysis.StringSyntaxAttribute.GuidFormat)] +#endif + string? format, + global::System.IFormatProvider? formatProvider) + => Value.ToString(format, formatProvider); + +#if NETCOREAPP2_1_OR_GREATER + public static MyId Parse(global::System.ReadOnlySpan input) + => new(global::System.Guid.Parse(input)); +#endif + +#if NET6_0_OR_GREATER +#if NET7_0_OR_GREATER + /// +#endif + public static MyId Parse(global::System.ReadOnlySpan input, global::System.IFormatProvider? provider) +#if NET7_0_OR_GREATER + => new(global::System.Guid.Parse(input, provider)); +#else + => new(global::System.Guid.Parse(input)); +#endif + +#if NET7_0_OR_GREATER + /// +#endif + public static bool TryParse(global::System.ReadOnlySpan input, global::System.IFormatProvider? provider, out MyId result) + { +#if NET7_0_OR_GREATER + if (global::System.Guid.TryParse(input, provider, out var guid)) +#else + if (global::System.Guid.TryParse(input, out var guid)) +#endif + { + result = new(guid); + return true; + } + else + { + result = default; + return false; + } + } + + /// + public bool TryFormat( + global::System.Span destination, + out int charsWritten, +#if NET7_0_OR_GREATER + [global::System.Diagnostics.CodeAnalysis.StringSyntax(global::System.Diagnostics.CodeAnalysis.StringSyntaxAttribute.GuidFormat)] +#endif + global::System.ReadOnlySpan format, + global::System.IFormatProvider? provider) + => Value.TryFormat(destination, out charsWritten, format); + + /// + public bool TryFormat( + global::System.Span destination, + out int charsWritten, +#if NET7_0_OR_GREATER + [global::System.Diagnostics.CodeAnalysis.StringSyntax(global::System.Diagnostics.CodeAnalysis.StringSyntaxAttribute.GuidFormat)] +#endif + global::System.ReadOnlySpan format = default) + => Value.TryFormat(destination, out charsWritten, format); +#endif +#if NET8_0_OR_GREATER + /// + public bool TryFormat( + global::System.Span utf8Destination, + out int bytesWritten, + [global::System.Diagnostics.CodeAnalysis.StringSyntax(global::System.Diagnostics.CodeAnalysis.StringSyntaxAttribute.GuidFormat)] + global::System.ReadOnlySpan format, + global::System.IFormatProvider? provider) + => Value.TryFormat(utf8Destination, out bytesWritten, format); +#endif + } diff --git a/test/StronglyTypedIds.Tests/StronglyTypedIdConverterTests.cs b/test/StronglyTypedIds.Tests/StronglyTypedIdConverterTests.cs new file mode 100644 index 000000000..6b389763b --- /dev/null +++ b/test/StronglyTypedIds.Tests/StronglyTypedIdConverterTests.cs @@ -0,0 +1,288 @@ +using System.Threading.Tasks; +using StronglyTypedIds.Diagnostics; +using VerifyXunit; +using Xunit; +using Xunit.Abstractions; + +namespace StronglyTypedIds.Tests; + +[UsesVerify] +public class StronglyTypedIdConverterTests +{ + readonly ITestOutputHelper _output; + + public StronglyTypedIdConverterTests(ITestOutputHelper output) + { + _output = output; + } + + [Fact] + public Task DefaultConverterIdInGlobalNamespaceWithoutDefaultsDoesntGenerateConverters() + { + const string input = + """ + using StronglyTypedIds; + + [StronglyTypedId] + public partial struct MyId {} + + [StronglyTypedIdConverters] + public partial struct MyIdConverters {} + """; + var (diagnostics, output) = TestHelpers.GetGeneratedOutput(input, includeAttributes: false); + + Assert.Contains(diagnostics, diagnostic => diagnostic.Id == MissingDefaultsDiagnostic.Id); + + return Verifier.Verify(output) + .UseDirectory("Snapshots"); + } + + [Theory] + [InlineData("", true)] + [InlineData("(\"guid-dapper\")", true)] + [InlineData("(\"guid-dapper\")", false)] + public Task CanGenerateDefaultConverterIdInGlobalNamespace(string template, bool includeDefaults) + { + var attribute = includeDefaults + ? "[assembly:StronglyTypedIdConvertersDefaults(\"guid-dapper\")]" + : string.Empty; + + var input = + $$""" + using StronglyTypedIds; + {{attribute}} + + [StronglyTypedId] + public partial struct MyId {} + + [StronglyTypedIdConverters{{template}}] + public partial struct MyIdConverters {} + """; + var (diagnostics, output) = TestHelpers.GetGeneratedOutput(input, includeAttributes: false); + + Assert.Empty(diagnostics); + + return Verifier.Verify(output) + .DisableRequireUniquePrefix() + .UseDirectory("Snapshots"); + } + + [Theory] + [InlineData("", true)] + [InlineData("(\"guid-dapper\")", true)] + [InlineData("(\"guid-dapper\")", false)] + public Task CanGenerateDefaultConverterIdInNamespace(string template, bool includeDefaults) + { + var attribute = includeDefaults + ? "[assembly:StronglyTypedIdConvertersDefaults(\"guid-dapper\")]" + : string.Empty; + + var input = + $$""" + using StronglyTypedIds; + {{attribute}} + + namespace SomeNamespace + { + [StronglyTypedId] + public partial struct MyId {} + + [StronglyTypedIdConverters{{template}}] + public partial struct MyIdConverters {} + } + """; + var (diagnostics, output) = TestHelpers.GetGeneratedOutput(input, includeAttributes: false); + + Assert.Empty(diagnostics); + + return Verifier.Verify(output) + .DisableRequireUniquePrefix() + .UseDirectory("Snapshots"); + } + + [Theory] + [InlineData("", true)] + [InlineData("(\"int-dapper\")", true)] + [InlineData("(\"int-dapper\")", false)] + public Task CanGenerateNonDefaultConverterIdInNamespace(string template, bool includeDefaults) + { + var attribute = includeDefaults + ? "[assembly:StronglyTypedIdConvertersDefaults(\"int-dapper\")]" + : string.Empty; + + var input = + $$""" + using StronglyTypedIds; + {{attribute}} + + namespace SomeNamespace + { + [StronglyTypedId(Template.Int)] + public partial struct MyId {} + + [StronglyTypedIdConverters{{template}}] + public partial struct MyIdConverters {} + } + """; + var (diagnostics, output) = TestHelpers.GetGeneratedOutput(input, includeAttributes: false); + + Assert.Empty(diagnostics); + + return Verifier.Verify(output) + .DisableRequireUniquePrefix() + .UseDirectory("Snapshots"); + } + + [Theory] + [InlineData("", true)] + [InlineData("(\"guid-dapper\")", true)] + [InlineData("(\"guid-dapper\")", false)] + public Task CanGenerateDefaultConverterIdInFileScopedNamespace(string template, bool includeDefaults) + { + var attribute = includeDefaults + ? "[assembly:StronglyTypedIdConvertersDefaults(\"guid-dapper\")]" + : string.Empty; + + var input = + $$""" + using StronglyTypedIds; + {{attribute}} + + namespace SomeNamespace; + + [StronglyTypedId] + public partial struct MyId {} + + [StronglyTypedIdConverters{{template}}] + public partial struct MyIdConverters {} + """; + var (diagnostics, output) = TestHelpers.GetGeneratedOutput(input, includeAttributes: false); + + Assert.Empty(diagnostics); + + return Verifier.Verify(output) + .DisableRequireUniquePrefix() + .UseDirectory("Snapshots"); + } + + [Theory] + [InlineData("", true)] + [InlineData("(\"guid-dapper\")", true)] + [InlineData("(\"guid-dapper\")", false)] + public Task CanGenerateDefaultConverterIdInDifferentNamespace(string template, bool includeDefaults) + { + var attribute = includeDefaults + ? "[assembly:StronglyTypedIdConvertersDefaults(\"guid-dapper\")]" + : string.Empty; + + var input = + $$""" + using StronglyTypedIds; + {{attribute}} + + namespace SomeNamespace1 + { + [StronglyTypedId] + public partial struct MyId {} + } + namespace SomeNamespace2 + { + using SomeNamespace1; + + [StronglyTypedIdConverters{{template}}] + public partial struct MyIdConverters {} + } + """; + var (diagnostics, output) = TestHelpers.GetGeneratedOutput(input, includeAttributes: false); + + Assert.Empty(diagnostics); + + return Verifier.Verify(output) + .DisableRequireUniquePrefix() + .UseDirectory("Snapshots"); + } + + [Theory] + [InlineData("", true)] + [InlineData("(\"guid-dapper\")", true)] + [InlineData("(\"guid-dapper\")", false)] + public Task CanGenerateNestedIdInFileScopeNamespace(string template, bool includeDefaults) + { + var attribute = includeDefaults + ? "[assembly:StronglyTypedIdConvertersDefaults(\"guid-dapper\")]" + : string.Empty; + + var input = $$""" + using StronglyTypedIds; + {{attribute}} + + namespace SomeNamespace; + + public class ParentClass + { + [StronglyTypedId] + public partial struct MyId {} + } + + public class ConverterClass + { + [StronglyTypedIdConverters{{template}}] + public partial struct MyIdConverters {} + } + """; + var (diagnostics, output) = TestHelpers.GetGeneratedOutput(input, includeAttributes: false); + + Assert.Empty(diagnostics); + + return Verifier.Verify(output) + .DisableRequireUniquePrefix() + .UseDirectory("Snapshots"); + } + + [Theory] + [InlineData("", true)] + [InlineData("(\"guid-dapper\")", true)] + [InlineData("(\"guid-dapper\")", false)] + public Task CanGenerateMultipleConvertersWithSameName(string template, bool includeDefaults) + { + var attribute = includeDefaults + ? "[assembly:StronglyTypedIdConvertersDefaults(\"guid-dapper\")]" + : string.Empty; + + var input = $$""" + using StronglyTypedIds; + {{attribute}} + + public class ParentClass + { + [StronglyTypedId] + public partial struct MyId {} + } + + namespace MyContracts.V1 + { + public class ConverterClass + { + [StronglyTypedIdConverters{{template}}] + public partial struct MyIdConverters {} + } + } + + namespace MyContracts.V2 + { + public class ConverterClass + { + [StronglyTypedIdConverters{{template}}] + public partial struct MyIdConverters {} + } + } + """; + var (diagnostics, output) = TestHelpers.GetGeneratedOutput(input, includeAttributes: false); + + Assert.Empty(diagnostics); + + return Verifier.Verify(output) + .DisableRequireUniquePrefix() + .UseDirectory("Snapshots"); + } +} \ No newline at end of file diff --git a/test/StronglyTypedIds.Tests/StronglyTypedIds.Tests.csproj b/test/StronglyTypedIds.Tests/StronglyTypedIds.Tests.csproj index 4ef5fe171..1f9b872a6 100644 --- a/test/StronglyTypedIds.Tests/StronglyTypedIds.Tests.csproj +++ b/test/StronglyTypedIds.Tests/StronglyTypedIds.Tests.csproj @@ -31,6 +31,24 @@ + + + + + + + + + + + + + + + + + +