diff --git a/readme.md b/readme.md index 323da60..e07bb54 100644 --- a/readme.md +++ b/readme.md @@ -101,7 +101,11 @@ connection.UseStructId(); connection.Open(); ``` -The supported types are `Guid`, `int`, `long` and `string` for now. +The value types `Guid`, `int`, `long` and `string` have built-in support, as well as +any other types that implement `IParsable` and `IFormattable` (by persisting them +as strings). This means that you can, for example, use [Ulid](https://github.com/Cysharp/Ulid) +out of the box without any further configuration or customization (since it implements +both interfaces). ## Customization via Templates diff --git a/src/StructId.Analyzer/AnalysisExtensions.cs b/src/StructId.Analyzer/AnalysisExtensions.cs index 2432bb3..cc8aee1 100644 --- a/src/StructId.Analyzer/AnalysisExtensions.cs +++ b/src/StructId.Analyzer/AnalysisExtensions.cs @@ -25,6 +25,10 @@ public static class AnalysisExtensions public static string ToFullName(this ISymbol symbol) => symbol.ToDisplayString(FullNameNullable); + public static CSharpParseOptions GetParseOptions(this Compilation compilation) + => (CSharpParseOptions?)compilation.SyntaxTrees.FirstOrDefault()?.Options ?? + CSharpParseOptions.Default.WithLanguageVersion(LanguageVersion.Latest); + /// /// Checks whether the type inherits or implements the /// type, even if it's a generic type. @@ -152,6 +156,13 @@ public static string ToFileName(this ITypeSymbol type) public static bool IsStructId(this ITypeSymbol type) => type.AllInterfaces.Any(x => x.Name == "IStructId"); + public static bool IsValueTemplate(this AttributeData attribute) + => attribute.AttributeClass?.Name == "TValue" || + attribute.AttributeClass?.Name == "TValueAttribute"; + + public static bool IsValueTemplate(this AttributeSyntax attribute) + => attribute.Name.ToString() == "TValue" || attribute.Name.ToString() == "TValueAttribute"; + public static bool IsStructIdTemplate(this AttributeData attribute) => attribute.AttributeClass?.Name == "TStructId" || attribute.AttributeClass?.Name == "TStructIdAttribute"; diff --git a/src/StructId.Analyzer/BaseGenerator.cs b/src/StructId.Analyzer/BaseGenerator.cs index 72ceb36..fb93028 100644 --- a/src/StructId.Analyzer/BaseGenerator.cs +++ b/src/StructId.Analyzer/BaseGenerator.cs @@ -1,6 +1,7 @@ using System.Linq; using System.Text; using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.Text; namespace StructId; @@ -72,8 +73,8 @@ public virtual void Initialize(IncrementalGeneratorInitializationContext context void GenerateCode(SourceProductionContext context, TemplateArgs args) => AddFromTemplate( context, args, $"{args.TSelf.ToFileName()}.cs", args.TId.Equals(args.KnownTypes.String, SymbolEqualityComparer.Default) ? - (stringSyntax ??= CodeTemplate.Parse(stringTemplate)) : - (typedSyntax ??= CodeTemplate.Parse(typeTemplate))); + (stringSyntax ??= CodeTemplate.Parse(stringTemplate, args.KnownTypes.Compilation.GetParseOptions())) : + (typedSyntax ??= CodeTemplate.Parse(typeTemplate, args.KnownTypes.Compilation.GetParseOptions()))); protected static void AddFromTemplate(SourceProductionContext context, TemplateArgs args, string hintName, SyntaxNode template) { diff --git a/src/StructId.Analyzer/CodeTemplate.cs b/src/StructId.Analyzer/CodeTemplate.cs index ef61a25..217ad71 100644 --- a/src/StructId.Analyzer/CodeTemplate.cs +++ b/src/StructId.Analyzer/CodeTemplate.cs @@ -10,10 +10,10 @@ namespace StructId; public static class CodeTemplate { - public static SyntaxNode Parse(string template) + public static SyntaxNode Parse(string template, CSharpParseOptions? parseOptions = default) { var tree = CSharpSyntaxTree.ParseText(template, - CSharpParseOptions.Default.WithLanguageVersion(LanguageVersion.Latest)); + parseOptions ?? CSharpParseOptions.Default.WithLanguageVersion(LanguageVersion.Latest)); return tree.GetRoot(); } @@ -30,6 +30,17 @@ public static string Apply(string template, string structIdType, string valueTyp applied.ToFullString(); } + public static string Apply(string template, string valueType, bool normalizeWhitespace = false) + { + var applied = ApplyImpl(Parse(template), valueType); + + return normalizeWhitespace ? + applied.NormalizeWhitespace().ToFullString().Trim() : + applied.ToFullString().Trim(); + } + + public static SyntaxNode ApplyValue(this SyntaxNode node, INamedTypeSymbol valueType) => ApplyImpl(node, valueType.ToFullName()); + public static SyntaxNode Apply(this SyntaxNode node, INamedTypeSymbol structId) { var root = node.SyntaxTree.GetCompilationUnitRoot(); @@ -49,6 +60,17 @@ public static SyntaxNode Apply(this SyntaxNode node, INamedTypeSymbol structId) return ApplyImpl(root, structId.Name, tid, targetNamespace, corens); } + static SyntaxNode ApplyImpl(this SyntaxNode node, string valueType) + { + var root = node.SyntaxTree.GetCompilationUnitRoot(); + if (root == null) + return node; + + node = new ValueRewriter(valueType).Visit(root)!; + + return node; + } + static SyntaxNode ApplyImpl(this SyntaxNode node, string structIdType, string valueType, string? targetNamespace = default, string coreNamespace = "StructId") { var root = node.SyntaxTree.GetCompilationUnitRoot(); @@ -95,6 +117,84 @@ static SyntaxNode ApplyImpl(this SyntaxNode node, string structIdType, string va return node; } + class ValueRewriter(string tvalue) : CSharpSyntaxRewriter + { + public override SyntaxNode? VisitRecordDeclaration(RecordDeclarationSyntax node) + { + if (IsFileLocal(node)) + return null; + + return node; + } + + public override SyntaxNode? VisitStructDeclaration(StructDeclarationSyntax node) + { + if (IsFileLocal(node)) + return null; + + return base.VisitStructDeclaration(node); + } + + public override SyntaxNode? VisitClassDeclaration(ClassDeclarationSyntax node) + { + if (IsFileLocal(node)) + return null; + + return base.VisitClassDeclaration(node); + } + + public override SyntaxNode? VisitAttribute(AttributeSyntax node) + { + if (node.IsValueTemplate()) + return null; + + return base.VisitAttribute(node); + } + + public override SyntaxNode? VisitAttributeList(AttributeListSyntax node) + { + node = (AttributeListSyntax)base.VisitAttributeList(node)!; + if (node.Attributes.Count == 0) + return null; + + return base.VisitAttributeList(node); + } + + public override SyntaxNode? VisitIdentifierName(IdentifierNameSyntax node) + { + if (node.Identifier.Text == "TValue") + return IdentifierName(tvalue) + .WithLeadingTrivia(node.Identifier.LeadingTrivia) + .WithTrailingTrivia(node.Identifier.TrailingTrivia); + + if (node.Identifier.Text.StartsWith("TValue_")) + return IdentifierName(node.Identifier.Text.Replace("TValue_", tvalue.Replace('.', '_') + "_")) + .WithLeadingTrivia(node.Identifier.LeadingTrivia) + .WithTrailingTrivia(node.Identifier.TrailingTrivia); + + return base.VisitIdentifierName(node); + } + + public override SyntaxToken VisitToken(SyntaxToken token) + { + if (token.IsKind(SyntaxKind.IdentifierToken) && token.Text == "TValue") + return Identifier(tvalue) + .WithLeadingTrivia(token.LeadingTrivia) + .WithTrailingTrivia(token.TrailingTrivia); + + if (token.IsKind(SyntaxKind.IdentifierToken) && token.Text.StartsWith("TValue_")) + return Identifier(token.Text.Replace("TValue_", tvalue.Replace('.', '_') + "_")) + .WithLeadingTrivia(token.LeadingTrivia) + .WithTrailingTrivia(token.TrailingTrivia); + + return base.VisitToken(token); + } + + bool IsFileLocal(TypeDeclarationSyntax node) => + node.Modifiers.Any(x => x.IsKind(SyntaxKind.FileKeyword)) && + !node.AttributeLists.Any(list => list.Attributes.Any(a => a.IsValueTemplate())); + } + class TemplateRewriter(string tself, string tid) : CSharpSyntaxRewriter { public override SyntaxNode? VisitRecordDeclaration(RecordDeclarationSyntax node) @@ -183,7 +283,7 @@ class TemplateRewriter(string tself, string tid) : CSharpSyntaxRewriter return IdentifierName(tself) .WithLeadingTrivia(node.Identifier.LeadingTrivia) .WithTrailingTrivia(node.Identifier.TrailingTrivia); - else if (node.Identifier.Text == "TId") + else if (node.Identifier.Text == "TId" || node.Identifier.Text == "TValue") return IdentifierName(tid) .WithLeadingTrivia(node.Identifier.LeadingTrivia) .WithTrailingTrivia(node.Identifier.TrailingTrivia); @@ -198,7 +298,7 @@ public override SyntaxToken VisitToken(SyntaxToken token) return Identifier(tself) .WithLeadingTrivia(token.LeadingTrivia) .WithTrailingTrivia(token.TrailingTrivia); - else if (token.IsKind(SyntaxKind.IdentifierToken) && token.Text == "TId") + else if (token.IsKind(SyntaxKind.IdentifierToken) && (token.Text == "TId" || token.Text == "TValue")) return Identifier(tid) .WithLeadingTrivia(token.LeadingTrivia) .WithTrailingTrivia(token.TrailingTrivia); diff --git a/src/StructId.Analyzer/DapperExtensions.sbn b/src/StructId.Analyzer/DapperExtensions.sbn index f727ebc..799d8ff 100644 --- a/src/StructId.Analyzer/DapperExtensions.sbn +++ b/src/StructId.Analyzer/DapperExtensions.sbn @@ -22,20 +22,24 @@ public static partial class DapperExtensions { {{~ for id in Ids ~}} if (!SqlMapper.HasTypeHandler(typeof({{ id.TSelf }}))) - SqlMapper.AddTypeHandler(new DapperTypeHandler{{ id.TId }}<{{ id.TSelf }}>()); + SqlMapper.AddTypeHandler(new DapperTypeHandler{{ id.TValue }}<{{ id.TSelf }}>()); {{~ end ~}} {{~ for id in CustomIds ~}} if (!SqlMapper.HasTypeHandler(typeof({{ id.TSelf }}))) - SqlMapper.AddTypeHandler(new DapperTypeHandler<{{ id.TSelf }}, {{ id.TId }}, {{ id.THandler }}>()); + SqlMapper.AddTypeHandler(new DapperTypeHandler<{{ id.TSelf }}, {{ id.TValue }}, {{ id.THandler }}>()); {{~ end ~}} - {{~ for handler in CustomHandlers ~}} - if (!SqlMapper.HasTypeHandler(typeof({{ handler }}))) - SqlMapper.AddTypeHandler(new {{ handler }}()); + {{~ for handler in CustomValues ~}} + if (!SqlMapper.HasTypeHandler(typeof({{ handler.TValue }}))) + SqlMapper.AddTypeHandler(new {{ handler.THandler }}()); {{~ end ~}} + {{~ for handler in TemplatizedValueHandlers ~}} + if (!SqlMapper.HasTypeHandler(typeof({{ handler.TValue }}))) + SqlMapper.AddTypeHandler(new {{ handler.THandler }}()); + {{~ end ~}} return connection; } @@ -159,4 +163,8 @@ public static partial class DapperExtensions }; } } -} \ No newline at end of file +} + +{{~ for handler in TemplatizedValueHandlers ~}} +{{ handler.Code }} +{{~ end ~}} \ No newline at end of file diff --git a/src/StructId.Analyzer/DapperGenerator.cs b/src/StructId.Analyzer/DapperGenerator.cs index 46b6855..ea38fd6 100644 --- a/src/StructId.Analyzer/DapperGenerator.cs +++ b/src/StructId.Analyzer/DapperGenerator.cs @@ -1,7 +1,9 @@ using System.Collections.Generic; using System.Collections.Immutable; using System.Linq; +using System.Reflection.Metadata.Ecma335; using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp.Syntax; using Scriban; namespace StructId; @@ -14,7 +16,7 @@ public class DapperGenerator() : BaseGenerator( protected override IncrementalValuesProvider OnInitialize(IncrementalGeneratorInitializationContext context, IncrementalValuesProvider source) { - var supported = source.Where(x => x.TId.ToFullName() switch + bool IsBuiltIn(string type) => type switch { "System.String" => true, "System.Guid" => true, @@ -24,62 +26,130 @@ protected override IncrementalValuesProvider OnInitialize(Incremen "int" => true, "long" => true, _ => false - }); + }; - var handlers = context.CompilationProvider + var builtInHandled = source.Where(x => IsBuiltIn(x.TId.ToFullName())); + + var customHandlers = context.CompilationProvider .SelectMany((x, _) => x.Assembly.GetAllTypes().OfType()) .Combine(context.CompilationProvider.Select((x, _) => x.GetTypeByMetadataName("Dapper.SqlMapper+TypeHandler`1"))) - .Where(x => x.Left != null && x.Right != null && x.Left.Is(x.Right)) + .Where(x => x.Left != null && x.Right != null && + x.Left.Is(x.Right) && + // Don't emit as plain handlers if they are id templates + !x.Left.GetAttributes().Any(a => a.IsValueTemplate())) .Select((x, _) => x.Left) .Collect(); - var custom = source - .Combine(handlers) + var templatizedValues = context.SelectTemplatizedValues() + .Where(x => !IsBuiltIn(x.TValue.ToFullName())) + .Combine(context.CompilationProvider.Select((x, _) => x.GetTypeByMetadataName("Dapper.SqlMapper+TypeHandler`1"))) + .Where(x => x.Left.Template.TTemplate.Is(x.Right)) + .Select((x, _) => x.Left); + + var customHandled = source + .Combine(customHandlers.Combine(templatizedValues.Collect())) .Select((x, _) => { - (TemplateArgs args, ImmutableArray handlers) = x; + (TemplateArgs args, (ImmutableArray handlers, ImmutableArray templatized)) = x; var handlerType = args.ReferenceType.Construct(args.TId); var handler = handlers.FirstOrDefault(x => x.Is(handlerType, false)); + if (handler == null) + { + var templated = templatized.Where(x => x.TValue.Equals(args.TId, SymbolEqualityComparer.Default)) + .FirstOrDefault(); + // Consider templatized handlers that will be emitted as custom handlers too for registration. + if (templated != null) + { + var compilation = args.KnownTypes.Compilation.AddSyntaxTrees(templated.Syntax.SyntaxTree); + handler = compilation.Assembly.GetAllTypes().FirstOrDefault(x => x.Name == templated.TypeName); + } + } + return args with { ReferenceType = handler! }; }) .Where(x => x.ReferenceType != null); - context.RegisterSourceOutput(supported.Collect().Combine(custom.Collect()), GenerateHandlers); + context.RegisterSourceOutput(builtInHandled.Collect().Combine(customHandled.Collect()).Combine(templatizedValues.Collect()), GenerateHandlers); // Turn off codegen in the base template. return source.Where(x => false); } - void GenerateHandlers(SourceProductionContext context, (ImmutableArray ids, ImmutableArray custom) source) + void GenerateHandlers(SourceProductionContext context, ((ImmutableArray builtInHandled, ImmutableArray customHandled), ImmutableArray templatizedValues) source) { - var (ids, custom) = source; - if (ids.Length == 0 && custom.Length == 0) + var ((builtInHandled, customHandled), templatizedValues) = source; + if (builtInHandled.Length == 0 && customHandled.Length == 0 && templatizedValues.Length == 0) return; - var known = ids.Concat(custom).First().KnownTypes; - var customHandlers = custom.Select(x => x.ReferenceType.ToFullName()).Distinct().ToArray(); + var structIdNamespace = builtInHandled.Concat(customHandled).Select(x => x.KnownTypes.StructIdNamespace).FirstOrDefault() + ?? "StructId"; + + var templatizedHandlers = new HashSet(templatizedValues + .Select(x => x.TypeName)); + + var customValueHandlers = customHandled + .GroupBy(x => x.ReferenceType.ToFullName()) + // Avoid registering twice the same templatized value handlers since they are + // already added at the end of the scriban rendering. + .Where(x => !templatizedHandlers.Contains(x.Key)) + .Select(x => new ValueHandlerModel(x.First().TId.ToFullName(), x.Key)) + .ToArray(); var model = new SelectorModel( - known.StructIdNamespace, - ids.Select(x => new StructIdModel(x.TSelf.ToFullName(), x.TId.Name)), - custom.Select(x => new StructIdCustomModel(x.TSelf.ToFullName(), x.TId.Name, x.ReferenceType.ToFullName())), - customHandlers); + structIdNamespace, + // Built-in use the Name of the value type since it's used as a suffix for well-known provided implementations. + builtInHandled.Select(x => new StructIdModel(x.TSelf.ToFullName(), x.TId.Name)), + customHandled.Select(x => new StructIdCustomModel(x.TSelf.ToFullName(), x.TId.ToFullName(), x.ReferenceType.ToFullName())), + customValueHandlers, + templatizedValues.Select(x => new ValueHandlerModelCode(x))); var output = template.Render(model, member => member.Name); - context.AddSource($"DapperExtensions.cs", output); + context.AddSource($"DapperExtensions.cs", output.Trim()); } - public static string Render(string @namespace, string tself, string tid) - => template.Render(new SelectorModel(@namespace, [new(tself, tid)], [], []), member => member.Name); + public static string Render(string @namespace, string tself, string tvalue) + => template.Render(new SelectorModel(@namespace, [new(tself, tvalue)], [], [], []), member => member.Name).Trim(); - public static string RenderCustom(string @namespace, string tself, string tid, string thandler) - => template.Render(new SelectorModel(@namespace, [], [new(tself, tid, thandler)], [thandler]), member => member.Name); + public static string RenderCustom(string @namespace, string tself, string tvalue, string thandler) + => template.Render(new SelectorModel(@namespace, [], [new(tself, tvalue, thandler)], [new(tvalue, thandler)], []), member => member.Name).Trim(); - record StructIdModel(string TSelf, string TId); + public static string RenderTemplatized(string @namespace, string tself, string tvalue, string thandler, string handlerCode) + => template.Render(new SelectorModel(@namespace, [], [new(tself, tvalue, thandler)], [], [new(tvalue, thandler, handlerCode)]), member => member.Name).Trim(); - record StructIdCustomModel(string TSelf, string TId, string THandler); + record StructIdModel(string TSelf, string TValue); + + record StructIdCustomModel(string TSelf, string TValue, string THandler); + + record ValueHandlerModel(string TValue, string THandler); + + class ValueHandlerModelCode + { + public ValueHandlerModelCode(TValueTemplate template) + { + var declaration = template.Template.Syntax.ApplyValue(template.TValue) + .DescendantNodes() + .OfType() + .First(); + + TValue = template.TValue.ToFullName(); + THandler = declaration.Identifier.Text; + Code = declaration.ToFullString(); + } + + public ValueHandlerModelCode(string tvalue, string thandler, string code) + => (TValue, THandler, Code) = (tvalue, thandler, code); + + public string TValue { get; } + public string THandler { get; } + public string Code { get; } + } - record SelectorModel(string Namespace, IEnumerable Ids, IEnumerable CustomIds, IEnumerable CustomHandlers); + record SelectorModel( + string Namespace, + IEnumerable Ids, + IEnumerable CustomIds, + IEnumerable CustomValues, + IEnumerable TemplatizedValueHandlers); } \ No newline at end of file diff --git a/src/StructId.Analyzer/EntityFrameworkGenerator.cs b/src/StructId.Analyzer/EntityFrameworkGenerator.cs index 3c7b5f6..ee61ac7 100644 --- a/src/StructId.Analyzer/EntityFrameworkGenerator.cs +++ b/src/StructId.Analyzer/EntityFrameworkGenerator.cs @@ -17,18 +17,36 @@ public class EntityFrameworkGenerator() : BaseGenerator( protected override IncrementalValuesProvider OnInitialize(IncrementalGeneratorInitializationContext context, IncrementalValuesProvider source) { - context.RegisterSourceOutput(source.Collect(), GenerateValueSelector); + var converters = context.CompilationProvider + .SelectMany((x, _) => x.Assembly.GetAllTypes().OfType()) + .Combine(context.CompilationProvider.Select((x, _) => x.GetTypeByMetadataName("Microsoft.EntityFrameworkCore.Storage.ValueConversion.ValueConverter`2"))) + .Where(x => x.Left != null && x.Right != null && + x.Left.Is(x.Right) && + !x.Left.IsUnboundGenericType && + x.Left.BaseType?.TypeArguments.Length == 2 && + // Don't emit as plain converters if they are id templates + !x.Left.GetAttributes().Any(a => a.IsValueTemplate())) + .Select((x, _) => x.Left) + .Collect(); + + context.RegisterSourceOutput(source.Collect().Combine(converters), GenerateValueSelector); return base.OnInitialize(context, source); } - void GenerateValueSelector(SourceProductionContext context, ImmutableArray args) + void GenerateValueSelector(SourceProductionContext context, (ImmutableArray, ImmutableArray) args) { - if (args.Length == 0) + (var ids, var converters) = args; + + if (ids.Length == 0) return; - var model = new SelectorModel(args.Select(x => new StructIdModel(x.TSelf.ToFullName(), x.TId.ToFullName()))); + var model = new SelectorModel( + ids.Select(x => new StructIdModel(x.TSelf.ToFullName(), x.TId.ToFullName())), + converters.Select(x => new ConverterModel(x.BaseType!.TypeArguments[0].ToFullName(), x.BaseType!.TypeArguments[1].ToFullName(), x.ToFullName()))); + var output = template.Render(model, member => member.Name); + context.AddSource($"ValueConverterSelector.cs", output); } @@ -59,5 +77,7 @@ record StructIdModel(string TSelf, string TIdType) }; } - record SelectorModel(IEnumerable Ids); + record ConverterModel(string TModel, string TProvider, string TConverter); + + record SelectorModel(IEnumerable Ids, IEnumerable Converters); } \ No newline at end of file diff --git a/src/StructId.Analyzer/EntityFrameworkSelector.sbn b/src/StructId.Analyzer/EntityFrameworkSelector.sbn index a5414d1..c9a02a1 100644 --- a/src/StructId.Analyzer/EntityFrameworkSelector.sbn +++ b/src/StructId.Analyzer/EntityFrameworkSelector.sbn @@ -46,6 +46,13 @@ public static class StructIdDbContextOptionsBuilderExtensions info => new {{ id.TSelf }}.EntityFrameworkValueConverter(info.MappingHints))); {{~ end ~}} + {{~ for converter in Converters ~}} + if (modelClrType == typeof({{ converter.TModel }})) + yield return converters.GetOrAdd((modelClrType, providerClrType), key => new ValueConverterInfo( + key.ModelClrType, key.ProviderClrType ?? typeof({{ converter.TProvider }}), + info => new {{ converter.TConverter }}(info.MappingHints))); + + {{~ end ~}} } static Type? Unwrap(Type? type) diff --git a/src/StructId.Analyzer/NoStringSyntaxWalker.cs b/src/StructId.Analyzer/NoStringSyntaxWalker.cs new file mode 100644 index 0000000..cc835c7 --- /dev/null +++ b/src/StructId.Analyzer/NoStringSyntaxWalker.cs @@ -0,0 +1,28 @@ +using System.Linq; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; + +namespace StructId; + +class NoStringSyntaxWalker : CSharpSyntaxWalker +{ + bool nostring; + + public bool Accept(SyntaxNode node) + { + Visit(node); + return nostring; + } + + // visit primary constructor and check if there's a trivia with "/*!string*/" + public override void VisitRecordDeclaration(RecordDeclarationSyntax node) + { + if (node.AttributeLists.Any(list => list.Attributes.Any(a => a.IsStructIdTemplate())) && + node.ParameterList is { } parameters && + parameters.OpenParenToken.GetAllTrivia().Any(x => x.ToString().Contains("!string"))) + { + nostring = true; + } + } +} diff --git a/src/StructId.Analyzer/TValueTemplateExtensions.cs b/src/StructId.Analyzer/TValueTemplateExtensions.cs new file mode 100644 index 0000000..03ab94d --- /dev/null +++ b/src/StructId.Analyzer/TValueTemplateExtensions.cs @@ -0,0 +1,123 @@ +using System.Linq; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; + +namespace StructId; + +/// +/// Represents a template for the value type of struct ids. +/// +/// The type of value the struct id holds, such as Guid or string. +/// The template to apply to it. +record TValueTemplate(INamedTypeSymbol TValue, TValueTemplateInfo Template) +{ + SyntaxNode? applied; + + public SyntaxNode Syntax => (applied ??= Template.Syntax.ApplyValue(TValue)); + + public TypeDeclarationSyntax Declaration => Syntax + .DescendantNodes() + .OfType() + .First(); + + public string TypeName => Declaration.Identifier.Text; + + public string Render() => Declaration.ToFullString(); +} + +record TValueTemplateInfo(INamedTypeSymbol TTemplate, KnownTypes KnownTypes) +{ + public SyntaxNode Syntax { get; } = TTemplate.DeclaringSyntaxReferences[0].GetSyntax().SyntaxTree.GetRoot(); + + public bool NoString { get; } = new NoStringSyntaxWalker().Accept( + TTemplate.DeclaringSyntaxReferences[0].GetSyntax().SyntaxTree.GetRoot()); + + public INamedTypeSymbol TValue => Syntax.DescendantNodes() + .OfType() + .Select(x => KnownTypes.Compilation.GetSemanticModel(Syntax.SyntaxTree).GetDeclaredSymbol(x)) + .FirstOrDefault(x => x != null && x.Name == "TValue") ?? TTemplate; + + /// + /// Checks the value type against the template's TValue for compatibility + /// + public bool AppliesTo(INamedTypeSymbol valueType) + { + if (NoString && valueType.Equals(KnownTypes.String, SymbolEqualityComparer.Default)) + return false; + + if (valueType.Equals(TValue, SymbolEqualityComparer.Default)) + return true; + + if (valueType.Is(TValue)) + return true; + + // If the template had a generic attribute, we'd be looking at an intermediate + // type (typically TValue or TId) being used to define multiple constraints on + // the struct id's value type, such as implementing multiple interfaces. In + // this case, the tid would never equal or inherit from the template's TId, + // but we want instead to check for base type compatibility plus all interfaces. + return TValue.IsFileLocal && + // TId is a derived class of the template's TId base type (i.e. object or ValueType) + valueType.Is(TValue.BaseType) && + // All template provided TId interfaces must be implemented by the struct id's TId + TValue.AllInterfaces.All(iface => + valueType.AllInterfaces.Any(tface => tface.Is(iface))); + } +} + +static class TValueTemplateExtensions +{ + public static IncrementalValuesProvider SelectTemplatizedValues(this IncrementalGeneratorInitializationContext context) + { + var structIdNamespace = context.AnalyzerConfigOptionsProvider.GetStructIdNamespace(); + + var known = context.CompilationProvider + .Combine(structIdNamespace) + .Select((x, _) => new KnownTypes(x.Left, x.Right)); + + var templates = context.CompilationProvider + .SelectMany((x, _) => x.GetAllTypes(includeReferenced: true).OfType()) + .Where(x => + // Ensure template is a file-local type + x.IsFileLocal && + // We can only work with templates where we have the actual syntax tree. + x.DeclaringSyntaxReferences.Any( + // And we can locate the TStructIdAttribute type that should be applied to it. + r => r.GetSyntax() is TypeDeclarationSyntax declaration && x.GetAttributes().Any( + a => a.IsValueTemplate()))) + .Combine(known) + .Select((x, cancellation) => new TValueTemplateInfo(x.Left, x.Right)) + .Collect(); + + var values = context.CompilationProvider + .SelectMany((x, _) => x.Assembly.GetAllTypes().OfType()) + .Where(x => x.IsRecord && x.IsValueType && x.IsPartial()) + .Combine(known) + .Where(x => x.Left.Is(x.Right.IStructIdT)) + .Combine(templates) + .SelectMany((x, _) => + { + var ((id, known), templates) = x; + // Locate the IStructId interface implemented by the id + var structId = id.AllInterfaces.First(i => i.Is(known.IStructIdT)); + var tvalue = (INamedTypeSymbol)structId.TypeArguments[0]; + return templates + .Where(template => template.AppliesTo(tvalue)) + .Select(template => new TValueTemplate(tvalue, template)); + }); + + return values; + } + + //void GenerateCode(SourceProductionContext context, TIdTemplate source) + //{ + // var templateFile = Path.GetFileNameWithoutExtension(source.Template.Syntax.SyntaxTree.FilePath); + // var hintName = $"{source.TId.ToFileName()}/{templateFile}.cs"; + + // var applied = source.Template.Syntax.Apply(source.TId); + // var output = applied.ToFullString(); + + // context.AddSource(hintName, SourceText.From(output, Encoding.UTF8)); + //} +} diff --git a/src/StructId.Analyzer/TemplatedGenerator.cs b/src/StructId.Analyzer/TemplatedGenerator.cs index 817e313..aa020c1 100644 --- a/src/StructId.Analyzer/TemplatedGenerator.cs +++ b/src/StructId.Analyzer/TemplatedGenerator.cs @@ -28,7 +28,7 @@ record Template(INamedTypeSymbol TSelf, INamedTypeSymbol TId, AttributeData Attr public SyntaxNode Syntax { get; } = TSelf.DeclaringSyntaxReferences[0].GetSyntax().SyntaxTree.GetRoot(); - public bool NoString { get; } = new NoStringWalker().Accept( + public bool NoString { get; } = new NoStringSyntaxWalker().Accept( TSelf.DeclaringSyntaxReferences[0].GetSyntax().SyntaxTree.GetRoot()); /// @@ -45,11 +45,11 @@ public bool AppliesTo(INamedTypeSymbol valueType) if (valueType.Is(TId)) return true; - // If the template had a generic attribute, we'd be looking at an intermediate - // type (typically TValue or TId) being used to define multiple constraints on - // the struct id's value type, such as implementing multiple interfaces. In - // this case, the tid would never equal or inherit from the template's TId, - // but we want instead to check for base type compatibility plus all interfaces. + // The underlying TId may be an intermediate type (typically TValue or TId) + // being used to define multiple constraints on the struct id's value type, + // such as implementing multiple interfaces. In this case, the tid would never equal + // or inherit from the template's TId, but we want instead to check for base + // type compatibility plus all interfaces. return IsLocalTId && // TId is a derived class of the template's TId base type (i.e. object or ValueType) valueType.Is(TId.BaseType) && @@ -57,28 +57,6 @@ public bool AppliesTo(INamedTypeSymbol valueType) TId.AllInterfaces.All(iface => valueType.AllInterfaces.Any(tface => tface.Is(iface))); } - - class NoStringWalker : CSharpSyntaxWalker - { - bool nostring; - - public bool Accept(SyntaxNode node) - { - Visit(node); - return nostring; - } - - // visit primary constructor and check if there's a trivia with "/*!string*/" - public override void VisitRecordDeclaration(RecordDeclarationSyntax node) - { - if (node.AttributeLists.Any(list => list.Attributes.Any(a => a.IsStructIdTemplate())) && - node.ParameterList is { } parameters && - parameters.OpenParenToken.GetAllTrivia().Any(x => x.ToString().Contains("!string"))) - { - nostring = true; - } - } - } } public void Initialize(IncrementalGeneratorInitializationContext context) diff --git a/src/StructId.FunctionalTests/DapperTests.cs b/src/StructId.FunctionalTests/DapperTests.cs deleted file mode 100644 index b69088e..0000000 --- a/src/StructId.FunctionalTests/DapperTests.cs +++ /dev/null @@ -1,90 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Data; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -using Dapper; -using Microsoft.Data.Sqlite; - -namespace StructId.Functional; - -// showcases Ulid integration -public readonly partial record struct UlidId : IStructId; - -public record UlidProduct(UlidId Id, string Name); - -public class StringUlidHandler : Dapper.SqlMapper.TypeHandler -{ - public override Ulid Parse(object value) - { - return Ulid.Parse((string)value); - } - - public override void SetValue(IDbDataParameter parameter, Ulid value) - { - parameter.DbType = DbType.StringFixedLength; - parameter.Size = 26; - parameter.Value = value.ToString(); - } -} - -// showcases alternative serialization -//public class BinaryUlidHandler : TypeHandler -//{ -// public override Ulid Parse(object value) -// { -// return new Ulid((byte[])value); -// } - -// public override void SetValue(IDbDataParameter parameter, Ulid value) -// { -// parameter.DbType = DbType.Binary; -// parameter.Size = 16; -// parameter.Value = value.ToByteArray(); -// } -//} - -public class DapperTests -{ - [Fact] - public void Dapper() - { - using var connection = new SqliteConnection("Data Source=dapper.db") - .UseStructId(); - - connection.Open(); - - // Seed data - var productId = Guid.NewGuid(); - var product = new Product(new ProductId(productId), "Product"); - - connection.Execute("INSERT INTO Products (Id, Name) VALUES (@Id, @Name)", new Product(ProductId.New(), "Product1")); - connection.Execute("INSERT INTO Products (Id, Name) VALUES (@Id, @Name)", product); - connection.Execute("INSERT INTO Products (Id, Name) VALUES (@Id, @Name)", new Product(ProductId.New(), "Product2")); - - var product2 = connection.QueryFirst("SELECT * FROM Products WHERE Id = @Id", new { Id = productId }); - Assert.Equal(product, product2); - } - - [Fact] - public void DapperUlid() - { - using var connection = new SqliteConnection("Data Source=dapper.db") - .UseStructId(); - - connection.Open(); - - // Seed data - var productId = Ulid.NewUlid(); - var product = new UlidProduct(new UlidId(productId), "Product"); - - connection.Execute("INSERT INTO Products (Id, Name) VALUES (@Id, @Name)", new UlidProduct(UlidId.New(), "Product1")); - connection.Execute("INSERT INTO Products (Id, Name) VALUES (@Id, @Name)", product); - connection.Execute("INSERT INTO Products (Id, Name) VALUES (@Id, @Name)", new UlidProduct(UlidId.New(), "Product2")); - - var product2 = connection.QueryFirst("SELECT * FROM Products WHERE Id = @Id", new { Id = productId }); - Assert.Equal(product, product2); - } - -} diff --git a/src/StructId.FunctionalTests/StructId.FunctionalTests.csproj b/src/StructId.FunctionalTests/StructId.FunctionalTests.csproj index e7a207a..b5945af 100644 --- a/src/StructId.FunctionalTests/StructId.FunctionalTests.csproj +++ b/src/StructId.FunctionalTests/StructId.FunctionalTests.csproj @@ -42,8 +42,7 @@ - - + diff --git a/src/StructId.FunctionalTests/UlidEntityFramework.cs b/src/StructId.FunctionalTests/UlidEntityFramework.cs new file mode 100644 index 0000000..7e94b3b --- /dev/null +++ b/src/StructId.FunctionalTests/UlidEntityFramework.cs @@ -0,0 +1,25 @@ +// +#nullable enable + +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using StructId.Functional; + +[TStructId] +file partial record struct TSelf(Ulid Value) : INewable +{ + /// + /// Provides value conversion for Entity Framework Core + /// + public partial class EntityFrameworkUlidValueConverter : ValueConverter + { + public EntityFrameworkUlidValueConverter() : this(null) { } + + public EntityFrameworkUlidValueConverter(ConverterMappingHints? mappingHints = null) + : base(id => id.Value.ToString(), value => TSelf.New(Ulid.Parse(value)), mappingHints) { } + } +} + +file partial record struct TSelf +{ + public static TSelf New(Ulid value) => throw new System.NotImplementedException(); +} \ No newline at end of file diff --git a/src/StructId.FunctionalTests/UlidTests.cs b/src/StructId.FunctionalTests/UlidTests.cs new file mode 100644 index 0000000..42c923c --- /dev/null +++ b/src/StructId.FunctionalTests/UlidTests.cs @@ -0,0 +1,163 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Data; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Dapper; +using Microsoft.Data.Sqlite; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using static StructId.Functional.FunctionalTests; + +namespace StructId.Functional; + +// showcases Ulid integration +public readonly partial record struct UlidId : IStructId; + +public record UlidProduct(UlidId Id, string Name); + +// showcases a custom Dapper type handler trumps the built-in support for +// types that provide IParsable and IFormattable. +public class StringUlidHandler : Dapper.SqlMapper.TypeHandler +{ + // To ensure in tests that this is used over the built-in templatized support + // due to Ulid implementing IParsable and IFormattable. + public static bool Used { get; private set; } + + public override Ulid Parse(object value) + { + Used = true; + return Ulid.Parse((string)value, null); + } + + public override void SetValue(IDbDataParameter parameter, Ulid value) + { + Used = true; + parameter.DbType = DbType.StringFixedLength; + parameter.Size = 26; + parameter.Value = value.ToString(null, null); + } +} + +public partial class UlidToStringConverter : ValueConverter +{ + public UlidToStringConverter() : this(null) { } + + public UlidToStringConverter(ConverterMappingHints? mappingHints = null) + : base(id => id.ToString(), value => Ulid.Parse(value), mappingHints) { } +} + +// showcases alternative serialization +//public class BinaryUlidHandler : TypeHandler +//{ +// public override Ulid Parse(object value) +// { +// return new Ulid((byte[])value); +// } + +// public override void SetValue(IDbDataParameter parameter, Ulid value) +// { +// parameter.DbType = DbType.Binary; +// parameter.Size = 16; +// parameter.Value = value.ToByteArray(); +// } +//} + +public class UlidTests +{ + [Fact] + public void Dapper() + { + using var connection = new SqliteConnection("Data Source=dapper.db") + .UseStructId(); + + connection.Open(); + + // Seed data + var productId = Guid.NewGuid(); + var product = new Product(new ProductId(productId), "Product"); + + connection.Execute("INSERT INTO Products (Id, Name) VALUES (@Id, @Name)", new Product(ProductId.New(), "Product1")); + connection.Execute("INSERT INTO Products (Id, Name) VALUES (@Id, @Name)", product); + connection.Execute("INSERT INTO Products (Id, Name) VALUES (@Id, @Name)", new Product(ProductId.New(), "Product2")); + + var product2 = connection.QueryFirst("SELECT * FROM Products WHERE Id = @Id", new { Id = productId }); + Assert.Equal(product, product2); + } + + [Fact] + public void DapperUlid() + { + using var connection = new SqliteConnection("Data Source=dapper.db") + .UseStructId(); + + connection.Open(); + + // Seed data + var productId = Ulid.NewUlid(); + var product = new UlidProduct(new UlidId(productId), "Product"); + + connection.Execute("INSERT INTO Products (Id, Name) VALUES (@Id, @Name)", new UlidProduct(UlidId.New(), "Product1")); + connection.Execute("INSERT INTO Products (Id, Name) VALUES (@Id, @Name)", product); + connection.Execute("INSERT INTO Products (Id, Name) VALUES (@Id, @Name)", new UlidProduct(UlidId.New(), "Product2")); + + var product2 = connection.QueryFirst("SELECT * FROM Products WHERE Id = @Id", new { Id = productId }); + Assert.Equal(product, product2); + + Assert.True(StringUlidHandler.Used); + } + + [Fact] + public void EntityFramework() + { + var options = new DbContextOptionsBuilder() + .UseStructId() + .UseSqlite("Data Source=ef.db") + // Uncomment to see full SQL being run + // .EnableSensitiveDataLogging() + // .UseLoggerFactory(new LoggerFactory(output)) + .Options; + + using var context = new UlidContext(options); + + var id = UlidId.New(); + var product = new UlidProduct(new UlidId(id), "Product"); + + // Seed data + context.Products.Add(new UlidProduct(UlidId.New(), "Product1")); + context.Products.Add(product); + context.Products.Add(new UlidProduct(UlidId.New(), "Product2")); + + context.SaveChanges(); + + var product2 = context.Products.Where(x => x.Id == id).FirstOrDefault(); + Assert.Equal(product, product2); + + Ulid guid = id; + + var dict = new ConcurrentDictionary( + [ + new("foo", 1), + new("bar", 2), + ]); + + var product3 = context.Products.FirstOrDefault(x => guid == x.Id); + Assert.Equal(product, product3); + } + + public class UlidContext : DbContext + { + public UlidContext(DbContextOptions options) : base(options) { } + public DbSet Products { get; set; } = null!; + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + modelBuilder.Entity().Property(x => x.Id) + //.HasConversion(new UlidToStringConverter()) + .HasConversion(new UlidId.EntityFrameworkUlidValueConverter()); + //.HasConversion(new UlidId.EntityFrameworkValueConverter()); + } + } +} diff --git a/src/StructId.FunctionalTests/ulid-ef.db b/src/StructId.FunctionalTests/ulid-ef.db new file mode 100644 index 0000000..e69de29 diff --git a/src/StructId.Tests/CodeTemplateTests.cs b/src/StructId.Tests/CodeTemplateTests.cs index e317660..53d8dc0 100644 --- a/src/StructId.Tests/CodeTemplateTests.cs +++ b/src/StructId.Tests/CodeTemplateTests.cs @@ -163,7 +163,48 @@ partial record struct ItemId } """, applied); + } + + [Fact] + public void AppliesValueTemplate() + { + var template = + """ + using System; + + [TValue] + file class TValue_TypeHandler : Dapper.SqlMapper.TypeHandler + { + public override TValue Parse(object value) => TValue.Parse((string)value, null); + + public override void SetValue(IDbDataParameter parameter, TValue value) + { + parameter.DbType = DbType.String; + parameter.Value = value.ToString(null, null); + } + } + + file partial struct TValue : IParsable, IFormattable + { + } + """; - output.WriteLine(applied); + var applied = CodeTemplate.Apply(template, "System.Ulid"); + + Assert.Equal( + """ + using System; + file class System_Ulid_TypeHandler : Dapper.SqlMapper.TypeHandler + { + public override System.Ulid Parse(object value) => System.Ulid.Parse((string)value, null); + + public override void SetValue(IDbDataParameter parameter, System.Ulid value) + { + parameter.DbType = DbType.String; + parameter.Value = value.ToString(null, null); + } + } + """, + applied); } } diff --git a/src/StructId.Tests/DapperGeneratorTests.cs b/src/StructId.Tests/DapperGeneratorTests.cs index 9998997..b4d601a 100644 --- a/src/StructId.Tests/DapperGeneratorTests.cs +++ b/src/StructId.Tests/DapperGeneratorTests.cs @@ -45,7 +45,7 @@ public async Task GenerateHandler() [Fact] public async Task GenerateCustomHandler() { - output.WriteLine(DapperGenerator.RenderCustom("StructId", "UserId", "Ulid", "StringUlidHandler")); + var code = DapperGenerator.RenderCustom("StructId", "UserId", "System.Ulid", "StringUlidHandler"); var test = new StructIdGeneratorTest("UserId", "System.Ulid") { @@ -90,9 +90,80 @@ public override void SetValue(IDbDataParameter parameter, Ulid value) }, GeneratedSources = { - (typeof(DapperGenerator), "DapperExtensions.cs", - DapperGenerator.RenderCustom("StructId", "UserId", "Ulid", "StringUlidHandler"), - Encoding.UTF8) + (typeof(DapperGenerator), "DapperExtensions.cs", code, Encoding.UTF8) + }, + }, + }.WithAnalyzerDefaults(); + + await test.RunAsync(); + } + + [Fact] + public async Task GenerateTempletizedHandler() + { + var code = DapperGenerator.RenderTemplatized("StructId", "UserId", "System.Ulid", "System_Ulid_TypeHandler", + """ + file class System_Ulid_TypeHandler : Dapper.SqlMapper.TypeHandler + { + public override System.Ulid Parse(object value) => System.Ulid.Parse((string)value, null); + + public override void SetValue(IDbDataParameter parameter, System.Ulid value) + { + parameter.DbType = DbType.String; + parameter.Value = value.ToString(null, null); + } + } + """); + + var test = new StructIdGeneratorTest("UserId", "System.Ulid") + { + SolutionTransforms = + { + (solution, projectId) => solution + .GetProject(projectId)? + .AddMetadataReference(MetadataReference.CreateFromFile(typeof(Dapper.DbString).Assembly.Location)) + .AddMetadataReference(MetadataReference.CreateFromFile(typeof(Ulid).Assembly.Location)) + .Solution ?? solution + }, + TestState = + { + Sources = + { + """ + using System; + using StructId; + + public readonly partial record struct UserId(Ulid Value): IStructId; + """, + """ + using System; + using System.Data; + using System.Diagnostics.CodeAnalysis; + using StructId; + + [TValue] + file class TValue_TypeHandler : Dapper.SqlMapper.TypeHandler + { + public override TValue Parse(object value) => TValue.Parse((string)value, null); + + public override void SetValue(IDbDataParameter parameter, TValue value) + { + parameter.DbType = DbType.String; + parameter.Value = value.ToString(null, null); + } + } + + file partial struct TValue : IParsable, IFormattable + { + public static TValue Parse(string s, IFormatProvider? provider) => throw new NotImplementedException(); + public static bool TryParse([NotNullWhen(true)] string? s, IFormatProvider? provider, [MaybeNullWhen(false)] out TValue result) => throw new NotImplementedException(); + public string ToString(string? format, IFormatProvider? formatProvider) => throw new NotImplementedException(); + } + """ + }, + GeneratedSources = + { + (typeof(DapperGenerator), "DapperExtensions.cs", code, Encoding.UTF8) }, }, }.WithAnalyzerDefaults(); diff --git a/src/StructId.Tests/StructIdExtensions.cs b/src/StructId.Tests/StructIdExtensions.cs index 46427fe..2ee8211 100644 --- a/src/StructId.Tests/StructIdExtensions.cs +++ b/src/StructId.Tests/StructIdExtensions.cs @@ -24,6 +24,7 @@ public static TTest WithCodeFixDefaults(this TTest test) where TTest : Co AddSourceIfNotExists(test.FixedState.Sources, "INewable.cs", ThisAssembly.Resources.StructId.INewable.Text); AddSourceIfNotExists(test.FixedState.Sources, "INewableT.cs", ThisAssembly.Resources.StructId.INewableT.Text); AddSourceIfNotExists(test.FixedState.Sources, "TStructIdAttribute.cs", ThisAssembly.Resources.StructId.TStructIdAttribute.Text); + AddSourceIfNotExists(test.FixedState.Sources, "TValueAttribute.cs", ThisAssembly.Resources.StructId.TValueAttribute.Text); return test; } @@ -33,7 +34,7 @@ public static TTest WithAnalyzerDefaults(this TTest test) where TTest : A test.SolutionTransforms.Add((solution, projectId) => { var project = solution.GetProject(projectId)!; - var parseOptions = ((CSharpParseOptions)project.ParseOptions!).WithLanguageVersion(LanguageVersion.CSharp12); + var parseOptions = ((CSharpParseOptions)project.ParseOptions!).WithLanguageVersion(LanguageVersion.Latest); return project.WithParseOptions(parseOptions).Solution; }); @@ -43,6 +44,7 @@ public static TTest WithAnalyzerDefaults(this TTest test) where TTest : A AddSourceIfNotExists(test.TestState.Sources, "INewable.cs", ThisAssembly.Resources.StructId.INewable.Text); AddSourceIfNotExists(test.TestState.Sources, "INewableT.cs", ThisAssembly.Resources.StructId.INewableT.Text); AddSourceIfNotExists(test.TestState.Sources, "TStructIdAttribute.cs", ThisAssembly.Resources.StructId.TStructIdAttribute.Text); + AddSourceIfNotExists(test.TestState.Sources, "TValueAttribute.cs", ThisAssembly.Resources.StructId.TValueAttribute.Text); return test; } diff --git a/src/StructId/StructId.csproj b/src/StructId/StructId.csproj index 3087f25..204bb54 100644 --- a/src/StructId/StructId.csproj +++ b/src/StructId/StructId.csproj @@ -7,7 +7,9 @@ + + diff --git a/src/StructId/TValueAttribute.cs b/src/StructId/TValueAttribute.cs new file mode 100644 index 0000000..c19c40b --- /dev/null +++ b/src/StructId/TValueAttribute.cs @@ -0,0 +1,11 @@ +using System; + +namespace StructId; + +/// +/// Attribute for marking a template type for the underlying value of struct id. +/// +[AttributeUsage(AttributeTargets.Struct | AttributeTargets.Class)] +public class TValueAttribute : Attribute +{ +} \ No newline at end of file diff --git a/src/StructId/Templates/DapperTypeHandler.cs b/src/StructId/Templates/DapperTypeHandler.cs new file mode 100644 index 0000000..4473fe0 --- /dev/null +++ b/src/StructId/Templates/DapperTypeHandler.cs @@ -0,0 +1,23 @@ +using System; +using System.Data; +using System.Diagnostics.CodeAnalysis; +using StructId; + +[TValue] +file class TId_TypeHandler : Dapper.SqlMapper.TypeHandler +{ + public override TId Parse(object value) => TId.Parse((string)value, null); + + public override void SetValue(IDbDataParameter parameter, TId value) + { + parameter.DbType = DbType.String; + parameter.Value = value.ToString(null, null); + } +} + +file partial struct TId : IParsable, IFormattable +{ + public static TId Parse(string s, IFormatProvider? provider) => throw new NotImplementedException(); + public static bool TryParse([NotNullWhen(true)] string? s, IFormatProvider? provider, [MaybeNullWhen(false)] out TId result) => throw new NotImplementedException(); + public string ToString(string? format, IFormatProvider? formatProvider) => throw new NotImplementedException(); +} \ No newline at end of file diff --git a/src/StructId/Templates/EntityFrameworkValueConverter.cs b/src/StructId/Templates/EntityFrameworkValueConverter.cs new file mode 100644 index 0000000..dbbbcc6 --- /dev/null +++ b/src/StructId/Templates/EntityFrameworkValueConverter.cs @@ -0,0 +1,23 @@ +// +#nullable enable + +using System; +using System.Diagnostics.CodeAnalysis; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using StructId; + +[TValue] +file class TId_ValueConverter : ValueConverter +{ + public TId_ValueConverter() : this(null) { } + + public TId_ValueConverter(ConverterMappingHints? mappingHints = null) + : base(id => id.ToString(null, null), value => TId.Parse(value, null), mappingHints) { } +} + +file partial struct TId : IParsable, IFormattable +{ + public static TId Parse(string s, IFormatProvider? provider) => throw new NotImplementedException(); + public static bool TryParse([NotNullWhen(true)] string? s, IFormatProvider? provider, [MaybeNullWhen(false)] out TId result) => throw new NotImplementedException(); + public string ToString(string? format, IFormatProvider? formatProvider) => throw new NotImplementedException(); +} \ No newline at end of file