From 37845cc1c76d1f0e40b77119f74a51827a3631ee Mon Sep 17 00:00:00 2001 From: Tomas Fabian Date: Fri, 31 May 2024 16:33:24 +0200 Subject: [PATCH 01/17] [ksqlDB.RestApi.Client]: added HasColumnName to Fluent API for customizing column names --- .../FluentAPI/Builders/FieldTypeBuilder.cs | 13 +++++ .../Extensions/TypeExtensions.cs | 27 +++++++++- .../KSql/Query/Context/KSqlDBContext.cs | 2 + .../KSql/Query/Visitors/ConstantVisitor.cs | 2 +- .../KSql/Query/Visitors/KSqlJoinsVisitor.cs | 4 +- .../KSql/Query/Visitors/KSqlVisitor.cs | 31 +++++------ .../Extensions/MemberInfoExtensions.cs | 4 +- .../KSql/RestApi/Generators/TypeGenerator.cs | 4 +- .../KSql/RestApi/Json/JsonTypeInfoResolver.cs | 28 ++++++++++ .../KSql/RestApi/KSqlDbQueryStreamProvider.cs | 53 ++++++++++++++++++- .../KSql/RestApi/Parsers/IdentifierUtil.cs | 34 ++++++++++-- .../KSql/RestApi/Statements/CreateEntity.cs | 2 +- .../KSql/RestApi/Statements/CreateInsert.cs | 8 +-- .../RestApi/Statements/KSqlTypeTranslator.cs | 2 +- .../Metadata/EntityMetadata.cs | 18 +++---- .../Metadata/FieldMetadata.cs | 1 + 16 files changed, 190 insertions(+), 43 deletions(-) create mode 100644 ksqlDb.RestApi.Client/KSql/RestApi/Json/JsonTypeInfoResolver.cs diff --git a/ksqlDb.RestApi.Client/FluentAPI/Builders/FieldTypeBuilder.cs b/ksqlDb.RestApi.Client/FluentAPI/Builders/FieldTypeBuilder.cs index 941651e0..c2ff7cbc 100644 --- a/ksqlDb.RestApi.Client/FluentAPI/Builders/FieldTypeBuilder.cs +++ b/ksqlDb.RestApi.Client/FluentAPI/Builders/FieldTypeBuilder.cs @@ -19,11 +19,24 @@ public interface IFieldTypeBuilder /// /// The field type builder for chaining additional configuration. public IFieldTypeBuilder WithHeaders(); + + /// + /// Configures the column name that the property will be mapped to in the record schema. + /// + /// The name of the column in the record schema. + /// The same instance so that multiple calls can be chained. + IFieldTypeBuilder HasColumnName(string columnName); } internal class FieldTypeBuilder(FieldMetadata fieldMetadata) : IFieldTypeBuilder { + public IFieldTypeBuilder HasColumnName(string columnName) + { + fieldMetadata.ColumnName = columnName; + return this; + } + public IFieldTypeBuilder Ignore() { fieldMetadata.Ignore = true; diff --git a/ksqlDb.RestApi.Client/Infrastructure/Extensions/TypeExtensions.cs b/ksqlDb.RestApi.Client/Infrastructure/Extensions/TypeExtensions.cs index 39a6d4a8..77a955b5 100644 --- a/ksqlDb.RestApi.Client/Infrastructure/Extensions/TypeExtensions.cs +++ b/ksqlDb.RestApi.Client/Infrastructure/Extensions/TypeExtensions.cs @@ -1,10 +1,13 @@ using System.Collections; +using System.Linq.Expressions; using System.Reflection; using System.Runtime.CompilerServices; using System.Text.Json.Serialization; +using ksqlDb.RestApi.Client.FluentAPI.Builders; using ksqlDB.RestApi.Client.KSql.Linq; using ksqlDB.RestApi.Client.KSql.Query; using ksqlDB.RestApi.Client.KSql.RestApi.Statements.Annotations; +using ksqlDb.RestApi.Client.Metadata; namespace ksqlDB.RestApi.Client.Infrastructure.Extensions; @@ -101,9 +104,29 @@ internal static string ExtractTypeName(this Type type) return attribute; } - - internal static string GetMemberName(this MemberInfo memberInfo) + + internal static string GetMemberName(this MemberExpression memberExpression, ModelBuilder? modelBuilder) + { + var entityMetadata = modelBuilder?.GetEntities().FirstOrDefault(c => c.Type == memberExpression.Expression?.Type); + + return memberExpression.Member.GetMemberName(entityMetadata); + } + + internal static string GetMemberName(this MemberInfo memberInfo, ModelBuilder? modelBuilder) { + var entityMetadata = modelBuilder?.GetEntities().FirstOrDefault(c => c.Type == memberInfo.DeclaringType); + + return memberInfo.GetMemberName(entityMetadata); + } + + internal static string GetMemberName(this MemberInfo memberInfo, EntityMetadata? entityMetadata) + { + var fieldMetadata = + entityMetadata?.FieldsMetadata.FirstOrDefault(c => c.MemberInfo.Name == memberInfo.Name); + + if (fieldMetadata != null && !string.IsNullOrEmpty(fieldMetadata.ColumnName)) + return fieldMetadata.ColumnName; + var jsonPropertyNameAttribute = memberInfo.GetCustomAttribute(); var memberName = jsonPropertyNameAttribute?.Name ?? memberInfo.Name; diff --git a/ksqlDb.RestApi.Client/KSql/Query/Context/KSqlDBContext.cs b/ksqlDb.RestApi.Client/KSql/Query/Context/KSqlDBContext.cs index bce8a85d..01d6c8e2 100644 --- a/ksqlDb.RestApi.Client/KSql/Query/Context/KSqlDBContext.cs +++ b/ksqlDb.RestApi.Client/KSql/Query/Context/KSqlDBContext.cs @@ -11,6 +11,7 @@ using ksqlDB.RestApi.Client.KSql.RestApi.Statements.Inserts; using ksqlDB.RestApi.Client.KSql.RestApi.Statements.Properties; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.Extensions.Logging; namespace ksqlDB.RestApi.Client.KSql.Query.Context; @@ -72,6 +73,7 @@ protected override void OnConfigureServices(IServiceCollection serviceCollection { base.OnConfigureServices(serviceCollection, contextOptions); + serviceCollection.TryAddSingleton(modelBuilder); serviceCollection.RegisterEndpointDependencies(contextOptions); } diff --git a/ksqlDb.RestApi.Client/KSql/Query/Visitors/ConstantVisitor.cs b/ksqlDb.RestApi.Client/KSql/Query/Visitors/ConstantVisitor.cs index c97ca03c..19162142 100644 --- a/ksqlDb.RestApi.Client/KSql/Query/Visitors/ConstantVisitor.cs +++ b/ksqlDb.RestApi.Client/KSql/Query/Visitors/ConstantVisitor.cs @@ -51,7 +51,7 @@ protected override Expression VisitConstant(ConstantExpression constantExpressio } else if (value != null && type != null && (type.IsClass || type.IsStruct() || type.IsDictionary())) { - var ksqlValue = new CreateKSqlValue(QueryMetadata.ModelBuilder).ExtractValue(value, null, null, type, str => IdentifierUtil.Format(str, QueryMetadata.IdentifierEscaping)); + var ksqlValue = new CreateKSqlValue(QueryMetadata.ModelBuilder).ExtractValue(value, null, null, type, memberInfo => IdentifierUtil.Format(memberInfo, QueryMetadata.IdentifierEscaping, QueryMetadata.ModelBuilder)); StringBuilder.Append(ksqlValue); } diff --git a/ksqlDb.RestApi.Client/KSql/Query/Visitors/KSqlJoinsVisitor.cs b/ksqlDb.RestApi.Client/KSql/Query/Visitors/KSqlJoinsVisitor.cs index 6ff41d57..7051be10 100644 --- a/ksqlDb.RestApi.Client/KSql/Query/Visitors/KSqlJoinsVisitor.cs +++ b/ksqlDb.RestApi.Client/KSql/Query/Visitors/KSqlJoinsVisitor.cs @@ -191,7 +191,7 @@ protected override Expression VisitMember(MemberExpression memberExpression) if (memberExpression.Expression?.NodeType == ExpressionType.Parameter) { - var memberName = IdentifierUtil.Format(memberExpression.Member, QueryMetadata.IdentifierEscaping); + var memberName = IdentifierUtil.Format(memberExpression, QueryMetadata.IdentifierEscaping, QueryMetadata.ModelBuilder); Append(memberName); @@ -200,7 +200,7 @@ protected override Expression VisitMember(MemberExpression memberExpression) if (QueryMetadata.Joins != null && memberExpression.Expression?.NodeType == ExpressionType.MemberAccess) { - Append(memberExpression.Member.Format(QueryMetadata.IdentifierEscaping)); + Append(memberExpression.Member.Format(QueryMetadata.IdentifierEscaping, QueryMetadata.ModelBuilder)); } else base.VisitMember(memberExpression); diff --git a/ksqlDb.RestApi.Client/KSql/Query/Visitors/KSqlVisitor.cs b/ksqlDb.RestApi.Client/KSql/Query/Visitors/KSqlVisitor.cs index 644ccbaf..2a698b70 100644 --- a/ksqlDb.RestApi.Client/KSql/Query/Visitors/KSqlVisitor.cs +++ b/ksqlDb.RestApi.Client/KSql/Query/Visitors/KSqlVisitor.cs @@ -162,7 +162,7 @@ protected override Expression VisitMemberInit(MemberInitExpression node) else Append(ColumnsSeparator); - var memberName = memberBinding.Member.Format(QueryMetadata.IdentifierEscaping); + var memberName = memberBinding.Member.Format(QueryMetadata.IdentifierEscaping, QueryMetadata.ModelBuilder); Append($"{memberName} := "); @@ -346,7 +346,7 @@ private protected void PrintColumnWithAlias(MemberInfo memberInfo, Expression ex { Visit(expression); Append(" AS "); - Append(memberInfo.Format(QueryMetadata.IdentifierEscaping)); + Append(memberInfo.Format(QueryMetadata.IdentifierEscaping, QueryMetadata.ModelBuilder)); } protected virtual void ProcessVisitNewMember(MemberInfo memberInfo, Expression expression) @@ -355,14 +355,14 @@ protected virtual void ProcessVisitNewMember(MemberInfo memberInfo, Expression e { Visit(expression); - Append(" " + memberInfo.Format(QueryMetadata.IdentifierEscaping)); + Append(" " + memberInfo.Format(QueryMetadata.IdentifierEscaping, QueryMetadata.ModelBuilder)); return; } if (expression is MemberExpression { Expression: MemberExpression { Expression: not null } me1 } && me1.Expression.Type.IsKsqlGrouping()) { - Append(memberInfo.Format(QueryMetadata.IdentifierEscaping)); + Append(memberInfo.Format(QueryMetadata.IdentifierEscaping, QueryMetadata.ModelBuilder)); return; } @@ -379,14 +379,14 @@ protected virtual void ProcessVisitNewMember(MemberInfo memberInfo, Expression e break; case MemberExpression me2 when me2.Member.GetCustomAttribute() != null || me2.Member.GetCustomAttribute() != null: - QueryMetadata.EntityMetadata.Add(me2.Member); - Append(me2.Member.Format(QueryMetadata.IdentifierEscaping)); + QueryMetadata.EntityMetadata.Add(me2); + Append(me2.Member.Format(QueryMetadata.IdentifierEscaping, QueryMetadata.ModelBuilder)); break; case MemberExpression { Expression.NodeType: ExpressionType.Constant }: Visit(expression); break; default: - Append(memberInfo.Format(QueryMetadata.IdentifierEscaping)); + Append(memberInfo.Format(QueryMetadata.IdentifierEscaping, QueryMetadata.ModelBuilder)); break; } } @@ -401,7 +401,7 @@ protected override Expression VisitMember(MemberExpression memberExpression) { var foundFromItem = QueryMetadata.TrySetAlias(memberExpression, (_, alias) => string.IsNullOrEmpty(alias)); - var memberName = memberExpression.Member.Format(QueryMetadata.IdentifierEscaping); + var memberName = memberExpression.Member.Format(QueryMetadata.IdentifierEscaping, QueryMetadata.ModelBuilder); var alias = IdentifierUtil.Format(((ParameterExpression)memberExpression.Expression).Name!, QueryMetadata.IdentifierEscaping); @@ -417,7 +417,7 @@ protected override Expression VisitMember(MemberExpression memberExpression) if (fromItem != null && memberExpression.Expression?.NodeType == ExpressionType.MemberAccess) { - string alias = ((MemberExpression)memberExpression.Expression).Member.Format(QueryMetadata.IdentifierEscaping); + string alias = ((MemberExpression)memberExpression.Expression).Member.Format(QueryMetadata.IdentifierEscaping, QueryMetadata.ModelBuilder); fromItem.Alias = alias; @@ -425,7 +425,7 @@ protected override Expression VisitMember(MemberExpression memberExpression) Append("."); - var memberName = memberExpression.Member.Format(QueryMetadata.IdentifierEscaping); + var memberName = memberExpression.Member.Format(QueryMetadata.IdentifierEscaping, QueryMetadata.ModelBuilder); Append(memberName); return memberExpression; } @@ -439,8 +439,8 @@ protected override Expression VisitMember(MemberExpression memberExpression) return memberExpression; } - - var memberName2 = memberExpression.Member.GetMemberName(); + + var memberName2 = memberExpression.GetMemberName(QueryMetadata.ModelBuilder); switch (memberExpression.Expression.NodeType) { @@ -510,8 +510,9 @@ private void AppendVisitMemberParameter(MemberExpression memberExpression) if (type != fromItem?.Type) { - var memberInfo = QueryMetadata.EntityMetadata.TryGetMemberInfo(memberExpression.Member.Name) ?? memberExpression.Member; - Append(memberInfo.Format(QueryMetadata.IdentifierEscaping)); + memberExpression = QueryMetadata.EntityMetadata.TryGetMemberExpression(memberExpression.Member.Name) ?? memberExpression; + + Append(IdentifierUtil.Format(memberExpression, QueryMetadata.IdentifierEscaping, QueryMetadata.ModelBuilder)); } } @@ -538,7 +539,7 @@ protected void Destructure(MemberExpression memberExpression) if (fromItem == null) Append("->"); - var memberName = memberExpression.Member.Format(QueryMetadata.IdentifierEscaping); + var memberName = memberExpression.Member.Format(QueryMetadata.IdentifierEscaping, QueryMetadata.ModelBuilder); Append(memberName); } diff --git a/ksqlDb.RestApi.Client/KSql/RestApi/Extensions/MemberInfoExtensions.cs b/ksqlDb.RestApi.Client/KSql/RestApi/Extensions/MemberInfoExtensions.cs index b71b4eb5..eaf68e63 100644 --- a/ksqlDb.RestApi.Client/KSql/RestApi/Extensions/MemberInfoExtensions.cs +++ b/ksqlDb.RestApi.Client/KSql/RestApi/Extensions/MemberInfoExtensions.cs @@ -1,4 +1,5 @@ using System.Reflection; +using ksqlDb.RestApi.Client.FluentAPI.Builders; using ksqlDB.RestApi.Client.KSql.RestApi.Enums; using ksqlDb.RestApi.Client.KSql.RestApi.Parsers; @@ -11,7 +12,8 @@ internal static class MemberInfoExtensions /// /// /// + /// /// the memberInfo.Name modified based on the provided format - public static string Format(this MemberInfo memberInfo, IdentifierEscaping escaping) => IdentifierUtil.Format(memberInfo, escaping); + public static string Format(this MemberInfo memberInfo, IdentifierEscaping escaping, ModelBuilder modelBuilder) => IdentifierUtil.Format(memberInfo, escaping, modelBuilder); } } diff --git a/ksqlDb.RestApi.Client/KSql/RestApi/Generators/TypeGenerator.cs b/ksqlDb.RestApi.Client/KSql/RestApi/Generators/TypeGenerator.cs index 77d01615..f4363f5c 100644 --- a/ksqlDb.RestApi.Client/KSql/RestApi/Generators/TypeGenerator.cs +++ b/ksqlDb.RestApi.Client/KSql/RestApi/Generators/TypeGenerator.cs @@ -1,5 +1,6 @@ using System.Text; using ksqlDb.RestApi.Client.FluentAPI.Builders; +using ksqlDB.RestApi.Client.Infrastructure.Extensions; using ksqlDB.RestApi.Client.KSql.RestApi.Enums; using ksqlDb.RestApi.Client.KSql.RestApi.Parsers; using ksqlDB.RestApi.Client.KSql.RestApi.Statements; @@ -35,7 +36,8 @@ private void PrintProperties(StringBuilder stringBuilder, IdentifierEscaping var ksqlType = typeTranslator.Translate(type, escaping); - var columnDefinition = $"{EscapeName(memberInfo.Name, escaping)} {ksqlType}{typeTranslator.ExploreAttributes(typeof(T), memberInfo, type)}"; + var memberName = memberInfo.GetMemberName(modelBuilder); + var columnDefinition = $"{EscapeName(memberName, escaping)} {ksqlType}{typeTranslator.ExploreAttributes(typeof(T), memberInfo, type)}"; ksqlProperties.Add(columnDefinition); } diff --git a/ksqlDb.RestApi.Client/KSql/RestApi/Json/JsonTypeInfoResolver.cs b/ksqlDb.RestApi.Client/KSql/RestApi/Json/JsonTypeInfoResolver.cs new file mode 100644 index 00000000..18ad4766 --- /dev/null +++ b/ksqlDb.RestApi.Client/KSql/RestApi/Json/JsonTypeInfoResolver.cs @@ -0,0 +1,28 @@ +using System.Text.Json; +using System.Text.Json.Serialization.Metadata; + +namespace ksqlDb.RestApi.Client.KSql.RestApi.Json +{ + internal class JsonTypeInfoResolver(IJsonTypeInfoResolver typeInfoResolver) : IJsonTypeInfoResolver + { + private readonly IJsonTypeInfoResolver typeInfoResolver = typeInfoResolver ?? throw new ArgumentNullException(nameof(typeInfoResolver)); + + public IList> Modifiers => modifiers ??= new List>(); + private IList>? modifiers; + + public virtual JsonTypeInfo? GetTypeInfo(Type type, JsonSerializerOptions options) + { + var typeInfo = typeInfoResolver.GetTypeInfo(type, options); + + if (modifiers != null) + { + foreach (Action modifier in modifiers) + { + modifier(typeInfo); + } + } + + return typeInfo; + } + } +} diff --git a/ksqlDb.RestApi.Client/KSql/RestApi/KSqlDbQueryStreamProvider.cs b/ksqlDb.RestApi.Client/KSql/RestApi/KSqlDbQueryStreamProvider.cs index 2d995e40..67eba0a0 100644 --- a/ksqlDb.RestApi.Client/KSql/RestApi/KSqlDbQueryStreamProvider.cs +++ b/ksqlDb.RestApi.Client/KSql/RestApi/KSqlDbQueryStreamProvider.cs @@ -1,20 +1,26 @@ #if !NETSTANDARD using System.Net; using System.Text.Json; +using System.Text.Json.Serialization.Metadata; +using ksqlDb.RestApi.Client.FluentAPI.Builders; using ksqlDb.RestApi.Client.KSql.Query.Context.Options; using ksqlDB.RestApi.Client.KSql.RestApi.Exceptions; using ksqlDB.RestApi.Client.KSql.RestApi.Responses; using Microsoft.Extensions.Logging; using IHttpClientFactory = ksqlDB.RestApi.Client.KSql.RestApi.Http.IHttpClientFactory; +using JsonTypeInfoResolver = ksqlDb.RestApi.Client.KSql.RestApi.Json.JsonTypeInfoResolver; #nullable disable namespace ksqlDB.RestApi.Client.KSql.RestApi { internal class KSqlDbQueryStreamProvider : KSqlDbProvider { - public KSqlDbQueryStreamProvider(IHttpClientFactory httpClientFactory, KSqlDbProviderOptions options, ILogger logger = null) + private readonly ModelBuilder modelBuilder; + + public KSqlDbQueryStreamProvider(IHttpClientFactory httpClientFactory, ModelBuilder modelBuilder, KSqlDbProviderOptions options, ILogger logger = null) : base(httpClientFactory, options, logger) { + this.modelBuilder = modelBuilder ?? throw new ArgumentNullException(nameof(modelBuilder)); #if NETCOREAPP3_1 AppContext.SetSwitch("System.Net.Http.SocketsHttpHandler.Http2UnencryptedSupport", true); #endif @@ -48,6 +54,51 @@ protected override RowValue OnLineRead(string rawJson) return default; } + protected override JsonSerializerOptions OnCreateJsonSerializerOptions() + { + var jsonSerializerOptions = base.OnCreateJsonSerializerOptions(); + + if (jsonSerializerOptions.TypeInfoResolver == null) + { + var defaultJsonTypeInfoResolver = new DefaultJsonTypeInfoResolver(); + var resolver = new JsonTypeInfoResolver(defaultJsonTypeInfoResolver) + { + Modifiers = { JsonPropertyNameModifier } + }; + jsonSerializerOptions.TypeInfoResolver = resolver; + } + else if(jsonSerializerOptions.TypeInfoResolver is not JsonTypeInfoResolver) + { + var resolver = new JsonTypeInfoResolver(jsonSerializerOptions.TypeInfoResolver) + { + Modifiers = { JsonPropertyNameModifier } + }; + + jsonSerializerOptions.TypeInfoResolver = resolver; + } + + return jsonSerializerOptions; + } + + internal void JsonPropertyNameModifier(JsonTypeInfo jsonTypeInfo) + { + JsonPropertyNameModifier(jsonTypeInfo, modelBuilder); + } + + internal static void JsonPropertyNameModifier(JsonTypeInfo jsonTypeInfo, ModelBuilder modelBuilder) + { + var entityMetadata = modelBuilder.GetEntities().FirstOrDefault(c => c.Type == jsonTypeInfo.Type); + + foreach (var typeInfoProperty in jsonTypeInfo.Properties) + { + var fieldMetadata = + entityMetadata?.FieldsMetadata.FirstOrDefault(c => c.MemberInfo.Name == typeInfoProperty.Name); + + if (fieldMetadata != null && !string.IsNullOrEmpty(fieldMetadata.ColumnName)) + typeInfoProperty.Name = fieldMetadata.ColumnName; + } + } + private static void OnError(string rawJson) { var errorResponse = JsonSerializer.Deserialize(rawJson); diff --git a/ksqlDb.RestApi.Client/KSql/RestApi/Parsers/IdentifierUtil.cs b/ksqlDb.RestApi.Client/KSql/RestApi/Parsers/IdentifierUtil.cs index aab28a2a..5744919f 100644 --- a/ksqlDb.RestApi.Client/KSql/RestApi/Parsers/IdentifierUtil.cs +++ b/ksqlDb.RestApi.Client/KSql/RestApi/Parsers/IdentifierUtil.cs @@ -1,5 +1,7 @@ +using System.Linq.Expressions; using System.Reflection; using Antlr4.Runtime; +using ksqlDb.RestApi.Client.FluentAPI.Builders; using ksqlDB.RestApi.Client.Infrastructure.Extensions; using ksqlDB.RestApi.Client.KSql.RestApi.Enums; using ksqlDB.RestApi.Client.KSql.RestApi.Statements.Annotations; @@ -49,22 +51,44 @@ Keywords when IsValid(identifier) && SystemColumns.IsValid(identifier) => identi }; } + /// + /// Format the identifier, except when it is a PseudoColumn. + /// + /// the memberExpression with the identifier + /// the format + /// the model builder + /// the identifier modified based on the provided format + public static string Format(MemberExpression memberExpression, IdentifierEscaping escaping, ModelBuilder? modelBuilder = null) + { + return escaping switch + { + Never => memberExpression.GetMemberName(modelBuilder), + Keywords when memberExpression.Member.GetCustomAttribute() != null => memberExpression.Member.Name, + Keywords when IsValid(memberExpression.GetMemberName(modelBuilder)) && SystemColumns.IsValid(memberExpression.GetMemberName(modelBuilder)) => memberExpression.GetMemberName(modelBuilder), + Keywords => string.Concat("`", memberExpression.GetMemberName(modelBuilder), "`"), + Always when memberExpression.Member.GetCustomAttribute() != null => memberExpression.Member.Name, + Always => string.Concat("`", memberExpression.GetMemberName(modelBuilder), "`"), + _ => throw new ArgumentOutOfRangeException(nameof(escaping), escaping, "Non-exhaustive match.") + }; + } + /// /// Format the identifier, except when it is a PseudoColumn. /// /// the memberInfo with the identifier /// the format + /// the model builder /// the identifier modified based on the provided format - public static string Format(MemberInfo memberInfo, IdentifierEscaping escaping) + public static string Format(MemberInfo memberInfo, IdentifierEscaping escaping, ModelBuilder? modelBuilder = null) { return escaping switch { - Never => memberInfo.GetMemberName(), + Never => memberInfo.GetMemberName(modelBuilder), Keywords when memberInfo.GetCustomAttribute() != null => memberInfo.Name, - Keywords when IsValid(memberInfo.GetMemberName()) && SystemColumns.IsValid(memberInfo.GetMemberName()) => memberInfo.GetMemberName(), - Keywords => string.Concat("`", memberInfo.GetMemberName(), "`"), + Keywords when IsValid(memberInfo.GetMemberName(modelBuilder)) && SystemColumns.IsValid(memberInfo.GetMemberName(modelBuilder)) => memberInfo.GetMemberName(modelBuilder), + Keywords => string.Concat("`", memberInfo.GetMemberName(modelBuilder), "`"), Always when memberInfo.GetCustomAttribute() != null => memberInfo.Name, - Always => string.Concat("`", memberInfo.GetMemberName(), "`"), + Always => string.Concat("`", memberInfo.GetMemberName(modelBuilder), "`"), _ => throw new ArgumentOutOfRangeException(nameof(escaping), escaping, "Non-exhaustive match.") }; } diff --git a/ksqlDb.RestApi.Client/KSql/RestApi/Statements/CreateEntity.cs b/ksqlDb.RestApi.Client/KSql/RestApi/Statements/CreateEntity.cs index 89444539..14fba8dc 100644 --- a/ksqlDb.RestApi.Client/KSql/RestApi/Statements/CreateEntity.cs +++ b/ksqlDb.RestApi.Client/KSql/RestApi/Statements/CreateEntity.cs @@ -49,7 +49,7 @@ private void PrintProperties(StatementContext statementContext, EntityCreatio var ksqlType = typeTranslator.Translate(type, metadata.IdentifierEscaping); - var columnName = IdentifierUtil.Format(memberInfo, metadata.IdentifierEscaping); + var columnName = IdentifierUtil.Format(memberInfo, metadata.IdentifierEscaping, modelBuilder); string columnDefinition = $"\t{columnName} {ksqlType}{typeTranslator.ExploreAttributes(typeof(T), memberInfo, type)}"; columnDefinition += TryAttachKey(statementContext.KSqlEntityType, memberInfo); diff --git a/ksqlDb.RestApi.Client/KSql/RestApi/Statements/CreateInsert.cs b/ksqlDb.RestApi.Client/KSql/RestApi/Statements/CreateInsert.cs index d6b5dd40..97261a2e 100644 --- a/ksqlDb.RestApi.Client/KSql/RestApi/Statements/CreateInsert.cs +++ b/ksqlDb.RestApi.Client/KSql/RestApi/Statements/CreateInsert.cs @@ -51,11 +51,11 @@ internal string Generate(InsertValues insertValues, InsertProperties? inse valuesStringBuilder.Append(", "); } - columnsStringBuilder.Append(memberInfo.Format(insertProperties.IdentifierEscaping)); + columnsStringBuilder.Append(memberInfo.Format(insertProperties.IdentifierEscaping, modelBuilder)); var type = GetMemberType(memberInfo); - var value = GetValue(insertValues, insertProperties, memberInfo, type, mi => IdentifierUtil.Format(mi, insertProperties.IdentifierEscaping)); + var value = GetValue(insertValues, insertProperties, memberInfo, type, mi => IdentifierUtil.Format(mi, insertProperties.IdentifierEscaping, modelBuilder)); valuesStringBuilder.Append(value); } @@ -69,12 +69,12 @@ internal string Generate(InsertValues insertValues, InsertProperties? inse private object GetValue(InsertValues insertValues, InsertProperties insertProperties, MemberInfo memberInfo, Type type, Func formatter) { - var hasValue = insertValues.PropertyValues.ContainsKey(memberInfo.Format(insertProperties.IdentifierEscaping)); + var hasValue = insertValues.PropertyValues.ContainsKey(memberInfo.Format(insertProperties.IdentifierEscaping, modelBuilder)); object value; if (hasValue) - value = insertValues.PropertyValues[memberInfo.Format(insertProperties.IdentifierEscaping)]; + value = insertValues.PropertyValues[memberInfo.Format(insertProperties.IdentifierEscaping, modelBuilder)]; else value = new CreateKSqlValue(modelBuilder).ExtractValue(insertValues.Entity, insertProperties, memberInfo, type, formatter); diff --git a/ksqlDb.RestApi.Client/KSql/RestApi/Statements/KSqlTypeTranslator.cs b/ksqlDb.RestApi.Client/KSql/RestApi/Statements/KSqlTypeTranslator.cs index 1d5859cc..c0c392b4 100644 --- a/ksqlDb.RestApi.Client/KSql/RestApi/Statements/KSqlTypeTranslator.cs +++ b/ksqlDb.RestApi.Client/KSql/RestApi/Statements/KSqlTypeTranslator.cs @@ -107,7 +107,7 @@ internal IEnumerable GetProperties(Type type, IdentifierEscaping escapin var ksqlType = Translate(memberType, escaping); - string columnDefinition = $"{memberInfo.Format(escaping)} {ksqlType}{ExploreAttributes(type, memberInfo, memberType)}"; + string columnDefinition = $"{memberInfo.Format(escaping, modelBuilder)} {ksqlType}{ExploreAttributes(type, memberInfo, memberType)}"; ksqlProperties.Add(columnDefinition); } diff --git a/ksqlDb.RestApi.Client/Metadata/EntityMetadata.cs b/ksqlDb.RestApi.Client/Metadata/EntityMetadata.cs index ebe9c1a9..7d209ec2 100644 --- a/ksqlDb.RestApi.Client/Metadata/EntityMetadata.cs +++ b/ksqlDb.RestApi.Client/Metadata/EntityMetadata.cs @@ -1,3 +1,4 @@ +using System.Linq.Expressions; using System.Reflection; namespace ksqlDb.RestApi.Client.Metadata @@ -12,15 +13,14 @@ internal sealed class EntityMetadata public IEnumerable FieldsMetadata => FieldsMetadataDict.Values; - internal bool Add(MemberInfo memberInfo) + + private readonly IList fieldMemberExpressions = new List(); + + internal bool Add(MemberExpression memberExpression) { - if (!FieldsMetadataDict.ContainsKey(memberInfo)) + if (fieldMemberExpressions.All(c => c.Type != memberExpression.Type && c.Member != memberExpression.Member)) { - var fieldMetadata = new FieldMetadata - { - MemberInfo = memberInfo - }; - FieldsMetadataDict[memberInfo] = fieldMetadata; + fieldMemberExpressions.Add(memberExpression); return true; } @@ -33,9 +33,9 @@ internal bool Add(MemberInfo memberInfo) c.MemberInfo.DeclaringType == memberInfo.DeclaringType && c.MemberInfo.Name == memberInfo.Name); } - internal MemberInfo? TryGetMemberInfo(string memberInfoName) + internal MemberExpression? TryGetMemberExpression(string memberInfoName) { - return FieldsMetadata.Where(c => c.MemberInfo.Name == memberInfoName).Select(c => c.MemberInfo).FirstOrDefault(); + return fieldMemberExpressions.Where(c => c.Member.Name == memberInfoName).Select(c => c).FirstOrDefault(); } } } diff --git a/ksqlDb.RestApi.Client/Metadata/FieldMetadata.cs b/ksqlDb.RestApi.Client/Metadata/FieldMetadata.cs index e7132647..27a7b9c5 100644 --- a/ksqlDb.RestApi.Client/Metadata/FieldMetadata.cs +++ b/ksqlDb.RestApi.Client/Metadata/FieldMetadata.cs @@ -9,5 +9,6 @@ internal record FieldMetadata public bool HasHeaders { get; internal set; } internal string Path { get; init; } = null!; internal string FullPath { get; init; } = null!; + public string? ColumnName { get; set; } } } From db09da2d9cf0c263897d9e9107e0d6c22a0397c5 Mon Sep 17 00:00:00 2001 From: Tomas Fabian Date: Fri, 31 May 2024 16:36:42 +0200 Subject: [PATCH 02/17] [ksqlDB.RestApi.Client]: HasColumnName to Fluent API unit tests --- .../FluentAPI/Builders/ModelBuilderTests.cs | 18 ++++ .../Extensions/TypeExtensionsTests.cs | 27 +++++- .../Linq/QbservableGroupByExtensionsTests.cs | 7 ++ .../KSql/Query/Context/TestableDbProvider.cs | 13 ++- ...torTests.cs => KSqlQueryGeneratorTests.cs} | 74 +++++++++++++++- .../KSql/Query/KSqlVisitorTests.cs | 37 +++++++- .../KSql/Query/Visitors/JoinVisitorTests.cs | 3 +- .../Generators/StatementGeneratorTests.cs | 2 +- .../RestApi/Generators/TypeGeneratorTests.cs | 45 +++++++++- .../RestApi/RowValueJsonSerializerTests.cs | 88 ++++++++++++++++++- .../RestApi/Statements/CreateEntityTests.cs | 19 ++-- .../RestApi/Statements/CreateInsertTests.cs | 76 ++++++++++++---- .../TestableKSqlDbQueryStreamProvider.cs | 3 +- .../Metadata/EntityMetadataTests.cs | 33 ++++--- 14 files changed, 391 insertions(+), 54 deletions(-) rename Tests/ksqlDB.RestApi.Client.Tests/KSql/Query/{KSqlQueryLanguageVisitorTests.cs => KSqlQueryGeneratorTests.cs} (96%) diff --git a/Tests/ksqlDB.RestApi.Client.Tests/FluentAPI/Builders/ModelBuilderTests.cs b/Tests/ksqlDB.RestApi.Client.Tests/FluentAPI/Builders/ModelBuilderTests.cs index 33cb7003..4fb188d0 100644 --- a/Tests/ksqlDB.RestApi.Client.Tests/FluentAPI/Builders/ModelBuilderTests.cs +++ b/Tests/ksqlDB.RestApi.Client.Tests/FluentAPI/Builders/ModelBuilderTests.cs @@ -67,6 +67,24 @@ public void Property_IgnoreField() entityMetadata!.FieldsMetadata.First(c => c.MemberInfo.Name == nameof(Payment.Description)).Ignore.Should().BeTrue(); } + [Test] + public void Property_HasColumnName() + { + //Arrange + var columnName = "desc"; + + //Act + var fieldTypeBuilder = builder.Entity() + .Property(b => b.Description) + .HasColumnName(columnName); + + //Assert + fieldTypeBuilder.Should().NotBeNull(); + var entityMetadata = builder.GetEntities().FirstOrDefault(c => c.Type == typeof(Payment)); + entityMetadata.Should().NotBeNull(); + entityMetadata!.FieldsMetadata.First(c => c.MemberInfo.Name == nameof(Payment.Description)).ColumnName.Should().Be(columnName); + } + [Test] public void MultiplePropertiesForSameType() { diff --git a/Tests/ksqlDB.RestApi.Client.Tests/Infrastructure/Extensions/TypeExtensionsTests.cs b/Tests/ksqlDB.RestApi.Client.Tests/Infrastructure/Extensions/TypeExtensionsTests.cs index e03c8bb5..6962123a 100644 --- a/Tests/ksqlDB.RestApi.Client.Tests/Infrastructure/Extensions/TypeExtensionsTests.cs +++ b/Tests/ksqlDB.RestApi.Client.Tests/Infrastructure/Extensions/TypeExtensionsTests.cs @@ -2,6 +2,7 @@ using System.Text; using System.Text.Json.Serialization; using FluentAssertions; +using ksqlDb.RestApi.Client.FluentAPI.Builders; using ksqlDB.RestApi.Client.Infrastructure.Extensions; using ksqlDB.RestApi.Client.KSql.Linq; using ksqlDB.RestApi.Client.KSql.Query; @@ -373,7 +374,7 @@ public void GetMemberName() var member = type.GetProperty(nameof(MySensor.Title)); //Act - var memberName = member!.GetMemberName(); + var memberName = member!.GetMemberName(new ModelBuilder()); //Assert memberName.Should().Be(nameof(MySensor.Title)); @@ -388,9 +389,31 @@ public void GetMemberName_JsonPropertyNameOverride() //Act var member = type.GetProperty(nameof(MySensor.SensorId2)); - var memberName = member!.GetMemberName(); + var memberName = member!.GetMemberName(new ModelBuilder()); //Assert memberName.Should().Be("SensorId"); } + + [Test] + public void GetMemberName_ModelBuilderHasColumnName() + { + //Arrange + var columnName = "Id"; + + var modelBuilder = new ModelBuilder(); + modelBuilder.Entity() + .Property(c => c.SensorId2) + .HasColumnName(columnName); + + var type = typeof(MySensor); + + //Act + var member = type.GetProperty(nameof(MySensor.SensorId2)); + + var memberName = member!.GetMemberName(modelBuilder); + + //Assert + memberName.Should().Be(columnName); + } } diff --git a/Tests/ksqlDB.RestApi.Client.Tests/KSql/Linq/QbservableGroupByExtensionsTests.cs b/Tests/ksqlDB.RestApi.Client.Tests/KSql/Linq/QbservableGroupByExtensionsTests.cs index b5c626ba..6583a7b0 100644 --- a/Tests/ksqlDB.RestApi.Client.Tests/KSql/Linq/QbservableGroupByExtensionsTests.cs +++ b/Tests/ksqlDB.RestApi.Client.Tests/KSql/Linq/QbservableGroupByExtensionsTests.cs @@ -1,4 +1,5 @@ using FluentAssertions; +using ksqlDb.RestApi.Client.FluentAPI.Builders; using ksqlDB.RestApi.Client.KSql.Linq; using ksqlDB.RestApi.Client.KSql.Query.Context; using ksqlDB.RestApi.Client.KSql.Query.Windows; @@ -507,6 +508,12 @@ public TestableDbProvider(string ksqlDbUrl, string httpResponse) }); } + public TestableDbProvider(KSqlDBContextOptions contextOptions, ModelBuilder modelBuilder) + : base(contextOptions, modelBuilder) + { + RegisterKSqlQueryGenerator = false; + } + public TestableDbProvider(KSqlDBContextOptions contextOptions) : base(contextOptions) { diff --git a/Tests/ksqlDB.RestApi.Client.Tests/KSql/Query/Context/TestableDbProvider.cs b/Tests/ksqlDB.RestApi.Client.Tests/KSql/Query/Context/TestableDbProvider.cs index 7f3d8204..3d25ac67 100644 --- a/Tests/ksqlDB.RestApi.Client.Tests/KSql/Query/Context/TestableDbProvider.cs +++ b/Tests/ksqlDB.RestApi.Client.Tests/KSql/Query/Context/TestableDbProvider.cs @@ -1,3 +1,4 @@ +using ksqlDb.RestApi.Client.FluentAPI.Builders; using ksqlDB.RestApi.Client.KSql.Query; using ksqlDB.RestApi.Client.KSql.Query.Context; using ksqlDB.RestApi.Client.KSql.RestApi; @@ -8,12 +9,20 @@ namespace ksqlDb.RestApi.Client.Tests.KSql.Query.Context; public class TestableDbProvider : KSqlDBContext { - public TestableDbProvider(string ksqlDbUrl) : base(ksqlDbUrl) + public TestableDbProvider(string ksqlDbUrl) + : base(ksqlDbUrl) { InitMocks(); } - public TestableDbProvider(KSqlDBContextOptions contextOptions) : base(contextOptions) + public TestableDbProvider(KSqlDBContextOptions contextOptions) + : base(contextOptions) + { + InitMocks(); + } + + public TestableDbProvider(KSqlDBContextOptions contextOptions, ModelBuilder modelBuilder) + : base(contextOptions, modelBuilder) { InitMocks(); } diff --git a/Tests/ksqlDB.RestApi.Client.Tests/KSql/Query/KSqlQueryLanguageVisitorTests.cs b/Tests/ksqlDB.RestApi.Client.Tests/KSql/Query/KSqlQueryGeneratorTests.cs similarity index 96% rename from Tests/ksqlDB.RestApi.Client.Tests/KSql/Query/KSqlQueryLanguageVisitorTests.cs rename to Tests/ksqlDB.RestApi.Client.Tests/KSql/Query/KSqlQueryGeneratorTests.cs index 369c7715..3f68f12d 100644 --- a/Tests/ksqlDB.RestApi.Client.Tests/KSql/Query/KSqlQueryLanguageVisitorTests.cs +++ b/Tests/ksqlDB.RestApi.Client.Tests/KSql/Query/KSqlQueryGeneratorTests.cs @@ -1,5 +1,6 @@ using System.Text.Json.Serialization; using FluentAssertions; +using ksqlDb.RestApi.Client.FluentAPI.Builders; using ksqlDB.RestApi.Client.KSql.Linq; using ksqlDB.RestApi.Client.KSql.Query; using ksqlDB.RestApi.Client.KSql.Query.Context; @@ -19,7 +20,7 @@ namespace ksqlDb.RestApi.Client.Tests.KSql.Query; #pragma warning disable CA1861 -public class KSqlQueryLanguageVisitorTests : TestBase +public class KSqlQueryGeneratorTests : TestBase { private KSqlQueryGenerator ClassUnderTest { get; set; } = null!; @@ -87,6 +88,77 @@ public void BuildKSql_SelectPropertyWithJsonPropertyNameAttribute() ksql.Should().BeEquivalentTo(expectedKsql.ReplaceLineEndings()); } + [Test] + public void BuildKSql_ModelBuilder_HasColumnNameOverride() + { + //Arrange + string idColumnName = "Id"; + ModelBuilder modelBuilder = new ModelBuilder(); + modelBuilder.Entity() + .Property(c => c.SensorId2) + .HasColumnName(idColumnName); + + var query = new TestableDbProvider(contextOptions, modelBuilder) + .CreatePushQuery() + .Where(c => c.SensorId2 == "1") + .Select(c => c.SensorId2); + + queryContext = new QueryContext() + { + ModelBuilder = modelBuilder + }; + + //Act + var ksql = ClassUnderTest.BuildKSql(query.Expression, queryContext); + + //Assert + string expectedKsql = + @$"SELECT {idColumnName} FROM {nameof(MySensor)}s +WHERE {idColumnName} = '1' EMIT CHANGES;"; + + ksql.Should().BeEquivalentTo(expectedKsql.ReplaceLineEndings()); + } + + private class Base + { + public int Id { get; set; } + } + private class Derived : Base + { + public string Description { get; set; } + } + + [Test] + public void BuildKSql_ModelBuilder_HasColumnNameOverride_ForPropertyInBaseClass() + { + //Arrange + string idColumnName = "SensorId"; + ModelBuilder modelBuilder = new ModelBuilder(); + modelBuilder.Entity() + .Property(c => c.Id) + .HasColumnName(idColumnName); + + var query = new TestableDbProvider(contextOptions, modelBuilder) + .CreatePushQuery() + .Where(c => c.Id == 1) + .Select(c => c.Id); + + queryContext = new QueryContext() + { + ModelBuilder = modelBuilder + }; + + //Act + var ksql = ClassUnderTest.BuildKSql(query.Expression, queryContext); + + //Assert + string expectedKsql = + @$"SELECT {idColumnName} FROM {nameof(Derived)}s +WHERE {idColumnName} = 1 EMIT CHANGES;"; + + ksql.Should().BeEquivalentTo(expectedKsql.ReplaceLineEndings()); + } + [Test] public void BuildKSql_SelectFromJoinPropertyWithJsonPropertyNameAttribute() { diff --git a/Tests/ksqlDB.RestApi.Client.Tests/KSql/Query/KSqlVisitorTests.cs b/Tests/ksqlDB.RestApi.Client.Tests/KSql/Query/KSqlVisitorTests.cs index 61773fd7..f6f2b0a3 100644 --- a/Tests/ksqlDB.RestApi.Client.Tests/KSql/Query/KSqlVisitorTests.cs +++ b/Tests/ksqlDB.RestApi.Client.Tests/KSql/Query/KSqlVisitorTests.cs @@ -333,13 +333,13 @@ public void MemberAccess_BuildKSql_PrintsNameOfTheProperty() public void Predicate_BuildKSql_PrintsOperatorAndOperands() { //Arrange - Expression> predicate = l => l.Latitude != "ahoj svet"; + Expression> predicate = l => l.Latitude != "40.71427000"; //Act var query = ClassUnderTest.BuildKSql(predicate); //Assert - query.Should().BeEquivalentTo($"{nameof(Location.Latitude)} != 'ahoj svet'"); + query.Should().BeEquivalentTo($"{nameof(Location.Latitude)} != '40.71427000'"); } private record Update @@ -364,7 +364,7 @@ public void Field_BuildKSql_PrintsFieldName() public void PredicateCompareWithVariable_BuildKSql_PrintsOperatorAndOperands() { //Arrange - string value = "ahoj svet"; + string value = "40.71427000"; Expression> predicate = l => l.Latitude != value; @@ -450,6 +450,37 @@ public void PredicateDeeplyNestedArrayProperty_BuildKSql_PrintsAllFields() query.Should().BeEquivalentTo("ARRAY_LENGTH(After->Model->Capabilities) > 0"); } + [Test] + public void MemberAccess_BuildKSql_JsonPropertyName() + { + //Arrange + Expression> predicate = l => l.Message; + + //Act + var query = ClassUnderTest.BuildKSql(predicate); + + //Assert + query.Should().BeEquivalentTo($"{nameof(Tweet.Message).ToUpper()}"); + } + + [Test] + public void MemberAccess_BuildKSql_ModelBuilderHasColumnName() + { + //Arrange + var amountColumnName = "amount"; + modelBuilder.Entity() + .Property(c => c.Amount) + .HasColumnName(amountColumnName); + + Expression> predicate = l => l.Amount; + + //Act + var query = ClassUnderTest.BuildKSql(predicate); + + //Assert + query.Should().BeEquivalentTo(amountColumnName); + } + #endregion #region Generics diff --git a/Tests/ksqlDB.RestApi.Client.Tests/KSql/Query/Visitors/JoinVisitorTests.cs b/Tests/ksqlDB.RestApi.Client.Tests/KSql/Query/Visitors/JoinVisitorTests.cs index 3faab34e..f4194783 100644 --- a/Tests/ksqlDB.RestApi.Client.Tests/KSql/Query/Visitors/JoinVisitorTests.cs +++ b/Tests/ksqlDB.RestApi.Client.Tests/KSql/Query/Visitors/JoinVisitorTests.cs @@ -1,4 +1,5 @@ using FluentAssertions; +using ksqlDb.RestApi.Client.FluentAPI.Builders; using ksqlDB.RestApi.Client.KSql.Linq; using ksqlDB.RestApi.Client.KSql.Query.Context; using ksqlDB.RestApi.Client.KSql.Query.Functions; @@ -24,7 +25,7 @@ public override void TestInitialize() base.TestInitialize(); var contextOptions = new KSqlDBContextOptions(TestParameters.KsqlDbUrl); - KSqlDbContext = new KSqlDBContext(contextOptions); + KSqlDbContext = new KSqlDBContext(contextOptions, new ModelBuilder()); } private static string MovieAlias => "movie"; diff --git a/Tests/ksqlDB.RestApi.Client.Tests/KSql/RestApi/Generators/StatementGeneratorTests.cs b/Tests/ksqlDB.RestApi.Client.Tests/KSql/RestApi/Generators/StatementGeneratorTests.cs index 2dceecf5..dc5f0966 100644 --- a/Tests/ksqlDB.RestApi.Client.Tests/KSql/RestApi/Generators/StatementGeneratorTests.cs +++ b/Tests/ksqlDB.RestApi.Client.Tests/KSql/RestApi/Generators/StatementGeneratorTests.cs @@ -36,7 +36,7 @@ private string GetExpectedClauses(bool isTable) return @$" MyMovies ( Id INT{keyClause} KEY, Title VARCHAR, - Release_Year INT, + ReleaseYear INT, NumberOfDays ARRAY, Dictionary MAP, Dictionary2 MAP, diff --git a/Tests/ksqlDB.RestApi.Client.Tests/KSql/RestApi/Generators/TypeGeneratorTests.cs b/Tests/ksqlDB.RestApi.Client.Tests/KSql/RestApi/Generators/TypeGeneratorTests.cs index 3843e39d..5cedd7fb 100644 --- a/Tests/ksqlDB.RestApi.Client.Tests/KSql/RestApi/Generators/TypeGeneratorTests.cs +++ b/Tests/ksqlDB.RestApi.Client.Tests/KSql/RestApi/Generators/TypeGeneratorTests.cs @@ -1,3 +1,4 @@ +using System.Text.Json.Serialization; using FluentAssertions; using ksqlDb.RestApi.Client.FluentAPI.Builders; using ksqlDB.RestApi.Client.KSql.RestApi.Enums; @@ -10,7 +11,13 @@ namespace ksqlDb.RestApi.Client.Tests.KSql.RestApi.Generators; public class TypeGeneratorTests { - private readonly ModelBuilder modelBuilder = new(); + private ModelBuilder modelBuilder; + + [SetUp] + public void TestInitialize() + { + modelBuilder = new(); + } [Test] public void CreateType() @@ -25,6 +32,42 @@ public void CreateType() .Be($"CREATE TYPE {nameof(Address)} AS STRUCT;"); } + private record Test + { + [JsonPropertyName("Id")] + public int Override { get; set; } + } + + [Test] + public void CreateType_JsonPropertyName() + { + //Arrange + + //Act + var statement = new TypeGenerator(modelBuilder).Print(new TypeProperties()); + + //Assert + statement.Should() + .Be($"CREATE TYPE {nameof(Test)} AS STRUCT;"); + } + + [Test] + public void CreateType_ModelBuilder_HasColumnName() + { + //Arrange + string columnName = "No"; + modelBuilder.Entity
() + .Property(b => b.Number) + .HasColumnName(columnName); + + //Act + var statement = new TypeGenerator(modelBuilder).Print
(new TypeProperties()); + + //Assert + statement.Should() + .Be($"CREATE TYPE {nameof(Address)} AS STRUCT<{columnName} INT, Street VARCHAR, City VARCHAR>;"); + } + [Test] public void CreateType_WithTypeName() { diff --git a/Tests/ksqlDB.RestApi.Client.Tests/KSql/RestApi/RowValueJsonSerializerTests.cs b/Tests/ksqlDB.RestApi.Client.Tests/KSql/RestApi/RowValueJsonSerializerTests.cs index f6175769..34298930 100644 --- a/Tests/ksqlDB.RestApi.Client.Tests/KSql/RestApi/RowValueJsonSerializerTests.cs +++ b/Tests/ksqlDB.RestApi.Client.Tests/KSql/RestApi/RowValueJsonSerializerTests.cs @@ -1,3 +1,4 @@ +using System.Text.Json.Serialization; using FluentAssertions; using ksqlDB.RestApi.Client.KSql.Query; using ksqlDb.RestApi.Client.KSql.Query.Context.Options; @@ -8,12 +9,15 @@ using UnitTests; using Assert = Microsoft.VisualStudio.TestTools.UnitTesting.Assert; using ksqlDb.RestApi.Client.Tests.KSql.RestApi.Generators; +using System.Text.Json.Serialization.Metadata; +using ksqlDb.RestApi.Client.FluentAPI.Builders; namespace ksqlDb.RestApi.Client.Tests.KSql.RestApi; public class RowValueJsonSerializerTests : TestBase { private RowValueJsonSerializer ClassUnderTest { get; set; } = null!; + private readonly ModelBuilder modelBuilder = new(); [SetUp] public override void TestInitialize() @@ -193,7 +197,7 @@ public void Deserialize_RecordAsClass() var queryStreamHeader = new QueryStreamHeader() { ColumnTypes = [KSqlTypes.Int, KSqlTypes.Varchar, KSqlTypes.Int, KSqlTypes.BigInt], - ColumnNames = ["ID", "TITLE", "RELEASE_NAME", "ROWTIME"], + ColumnNames = ["ID", "TITLE", "RELEASE_YEAR", "ROWTIME"], }; ClassUnderTest = new RowValueJsonSerializer(queryStreamHeader); @@ -206,6 +210,88 @@ public void Deserialize_RecordAsClass() //Assert rowValue!.Value.Id.Should().Be(2); + rowValue!.Value.Title.Should().Be("Die Hard"); + rowValue!.Value.Release_Year.Should().Be(1988); + } + + private class Movie2 : Record + { + public string Title { get; set; } = null!; + public int Id { get; set; } + [JsonPropertyName("RELEASE_YEAR")] + public int ReleaseYear { get; set; } + } + + [Test] + public void Deserialize_RecordAsClass_JsonPropertyName() + { + //Arrange + var queryStreamHeader = new QueryStreamHeader() + { + ColumnTypes = [KSqlTypes.Int, KSqlTypes.Varchar, KSqlTypes.Int, KSqlTypes.BigInt], + ColumnNames = ["ID", "TITLE", "RELEASE_YEAR", "ROWTIME"], + }; + + ClassUnderTest = new RowValueJsonSerializer(queryStreamHeader); + + string rawJson = "[2,\"Die Hard\",1988,1670438716925]"; + var jsonSerializerOptions = KSqlDbJsonSerializerOptions.CreateInstance(); + + //Act + var rowValue = ClassUnderTest.Deserialize(rawJson, jsonSerializerOptions); + + //Assert + rowValue!.Value.Id.Should().Be(2); + rowValue!.Value.Title.Should().Be("Die Hard"); + rowValue!.Value.ReleaseYear.Should().Be(1988); + } + + private class Inner + { + public string Deep { get; init; } + } + + private class Movie3 : Record + { + public Inner Inner { get; init; } + public string Title { get; set; } = null!; + public int Id { get; set; } + public int ReleaseYear { get; set; } + } + + [Test] + public void Deserialize_RecordAsClass_ModelBuilderHasColumnName() + { + //Arrange + var queryStreamHeader = new QueryStreamHeader() + { + ColumnTypes = [KSqlTypes.Int, KSqlTypes.Varchar, KSqlTypes.Int, KSqlTypes.BigInt], + ColumnNames = ["ID", "TITLE", "RELEASE_YEAR", "ROWTIME"], + }; + + modelBuilder.Entity() + .Property(c => c.ReleaseYear) + .HasColumnName("RELEASE_YEAR"); + + ClassUnderTest = new RowValueJsonSerializer(queryStreamHeader); + + string rawJson = "[2,\"Die Hard\",1988,1670438716925]"; + var jsonSerializerOptions = KSqlDbJsonSerializerOptions.CreateInstance(); + jsonSerializerOptions.TypeInfoResolver = new DefaultJsonTypeInfoResolver() + { + Modifiers = + { + (jsonTypeInfo) => KSqlDbQueryStreamProvider.JsonPropertyNameModifier(jsonTypeInfo, modelBuilder) + } + }; + + //Act + var rowValue = ClassUnderTest.Deserialize(rawJson, jsonSerializerOptions); + + //Assert + rowValue!.Value.Id.Should().Be(2); + rowValue!.Value.Title.Should().Be("Die Hard"); + rowValue!.Value.ReleaseYear.Should().Be(1988); } private enum MyEnum diff --git a/Tests/ksqlDB.RestApi.Client.Tests/KSql/RestApi/Statements/CreateEntityTests.cs b/Tests/ksqlDB.RestApi.Client.Tests/KSql/RestApi/Statements/CreateEntityTests.cs index 18064297..689a6251 100644 --- a/Tests/ksqlDB.RestApi.Client.Tests/KSql/RestApi/Statements/CreateEntityTests.cs +++ b/Tests/ksqlDB.RestApi.Client.Tests/KSql/RestApi/Statements/CreateEntityTests.cs @@ -18,6 +18,8 @@ public class CreateEntityTests private EntityCreationMetadata creationMetadata = null!; private readonly ModelBuilder modelBuilder = new(); + private const string MovieIdColumnName = "MovieId"; + [SetUp] public void Init() { @@ -27,6 +29,10 @@ public void Init() Partitions = 1, Replicas = 1 }; + + modelBuilder.Entity() + .Property(c => c.Id) + .HasColumnName(MovieIdColumnName); } private static readonly Pluralizer EnglishPluralizationService = new(); @@ -41,27 +47,27 @@ private static string CreateExpectedStatement(string creationClause, bool hasPri return escaping switch { Never => @$"{creationClause} {entityName} ( - Id INT {key}, + {MovieIdColumnName} INT {key}, Title VARCHAR, - Release_Year INT, + ReleaseYear INT, NumberOfDays ARRAY, Dictionary MAP, Dictionary2 MAP, Field DOUBLE ) WITH ( KAFKA_TOPIC='{nameof(MyMovie)}', VALUE_FORMAT='Json', PARTITIONS='1', REPLICAS='1' );".ReplaceLineEndings(), Keywords => @$"{creationClause} {entityName} ( - Id INT {key}, + {MovieIdColumnName} INT {key}, Title VARCHAR, - Release_Year INT, + ReleaseYear INT, NumberOfDays ARRAY, Dictionary MAP, Dictionary2 MAP, Field DOUBLE ) WITH ( KAFKA_TOPIC='{nameof(MyMovie)}', VALUE_FORMAT='Json', PARTITIONS='1', REPLICAS='1' );".ReplaceLineEndings(), Always => @$"{creationClause} `{entityName}` ( - `Id` INT {key}, + `{MovieIdColumnName}` INT {key}, `Title` VARCHAR, - `Release_Year` INT, + `ReleaseYear` INT, `NumberOfDays` ARRAY, `Dictionary` MAP, `Dictionary2` MAP, @@ -542,6 +548,7 @@ internal class MyMovie public string Title { get; set; } = null!; + [JsonPropertyName("ReleaseYear")] public int Release_Year { get; set; } public int[] NumberOfDays { get; init; } = null!; diff --git a/Tests/ksqlDB.RestApi.Client.Tests/KSql/RestApi/Statements/CreateInsertTests.cs b/Tests/ksqlDB.RestApi.Client.Tests/KSql/RestApi/Statements/CreateInsertTests.cs index c4e14924..dc5e9aaa 100644 --- a/Tests/ksqlDB.RestApi.Client.Tests/KSql/RestApi/Statements/CreateInsertTests.cs +++ b/Tests/ksqlDB.RestApi.Client.Tests/KSql/RestApi/Statements/CreateInsertTests.cs @@ -8,7 +8,6 @@ using ksqlDB.RestApi.Client.KSql.RestApi.Statements.Annotations; using ksqlDB.RestApi.Client.KSql.RestApi.Statements.Properties; using ksqlDb.RestApi.Client.Tests.KSql.RestApi.Generators; -using ksqlDb.RestApi.Client.Tests.Models.Movies; using NUnit.Framework; using static ksqlDB.RestApi.Client.KSql.RestApi.Enums.IdentifierEscaping; @@ -24,11 +23,25 @@ public void Init() modelBuilder = new(); } + private class Movie + { + public string Title { get; set; } = null!; + [Key] + public int Id { get; set; } + [JsonPropertyName("ReleaseYear")] + public int ReleaseYear { get; set; } + + [IgnoreByInserts] + public int IgnoreMe { get; set; } + + public IEnumerable ReadOnly { get; } = new[] { 1, 2 }; + } + public static IEnumerable<(IdentifierEscaping, string)> GenerateTestCases() { - yield return (Never, "INSERT INTO Movies (Title, Id, Release_Year) VALUES ('Title', 1, 1988);"); - yield return (Keywords, "INSERT INTO Movies (Title, Id, Release_Year) VALUES ('Title', 1, 1988);"); - yield return (Always, "INSERT INTO `Movies` (`Title`, `Id`, `Release_Year`) VALUES ('Title', 1, 1988);"); + yield return (Never, "INSERT INTO Movies (Title, Id, ReleaseYear) VALUES ('Title', 1, 1988);"); + yield return (Keywords, "INSERT INTO Movies (Title, Id, ReleaseYear) VALUES ('Title', 1, 1988);"); + yield return (Always, "INSERT INTO `Movies` (`Title`, `Id`, `ReleaseYear`) VALUES ('Title', 1, 1988);"); } [TestCaseSource(nameof(GenerateTestCases))] @@ -36,7 +49,34 @@ public void Generate((IdentifierEscaping escaping, string expected) testCase) { //Arrange var (escaping, expected) = testCase; - var movie = new Movie { Id = 1, Release_Year = 1988, Title = "Title" }; + var movie = new Movie { Id = 1, ReleaseYear = 1988, Title = "Title" }; + + //Act + var statement = new CreateInsert(modelBuilder).Generate(movie, new InsertProperties { IdentifierEscaping = escaping }); + + //Assert + statement.Should().Be(expected); + } + + private const string MovieIdColumnName = "MovieId"; + + public static IEnumerable<(IdentifierEscaping, string)> GenerateHasColumnNameTestCases() + { + yield return (Never, $"INSERT INTO Movies ({nameof(Movie.Title)}, {MovieIdColumnName}, ReleaseYear) VALUES ('Title', 1, 1988);"); + yield return (Keywords, $"INSERT INTO Movies ({nameof(Movie.Title)}, {MovieIdColumnName}, ReleaseYear) VALUES ('Title', 1, 1988);"); + yield return (Always, $"INSERT INTO `Movies` (`{nameof(Movie.Title)}`, `{MovieIdColumnName}`, `ReleaseYear`) VALUES ('Title', 1, 1988);"); + } + + [TestCaseSource(nameof(GenerateHasColumnNameTestCases))] + public void ModelBuilder_HasColumnName((IdentifierEscaping escaping, string expected) testCase) + { + //Arrange + modelBuilder.Entity() + .Property(c => c.Id) + .HasColumnName(MovieIdColumnName); + + var (escaping, expected) = testCase; + var movie = new Movie { Id = 1, ReleaseYear = 1988, Title = "Title" }; //Act var statement = new CreateInsert(modelBuilder).Generate(movie, new InsertProperties { IdentifierEscaping = escaping }); @@ -47,9 +87,9 @@ public void Generate((IdentifierEscaping escaping, string expected) testCase) public static IEnumerable<(IdentifierEscaping, string)> GenerateOverrideEntityNameTestCases() { - yield return (Never, "INSERT INTO TestNames (Title, Id, Release_Year) VALUES ('Title', 1, 1988);"); - yield return (Keywords, "INSERT INTO TestNames (Title, Id, Release_Year) VALUES ('Title', 1, 1988);"); - yield return (Always, "INSERT INTO `TestNames` (`Title`, `Id`, `Release_Year`) VALUES ('Title', 1, 1988);"); + yield return (Never, "INSERT INTO TestNames (Title, Id, ReleaseYear) VALUES ('Title', 1, 1988);"); + yield return (Keywords, "INSERT INTO TestNames (Title, Id, ReleaseYear) VALUES ('Title', 1, 1988);"); + yield return (Always, "INSERT INTO `TestNames` (`Title`, `Id`, `ReleaseYear`) VALUES ('Title', 1, 1988);"); } [TestCaseSource(nameof(GenerateOverrideEntityNameTestCases))] @@ -57,7 +97,7 @@ public void Generate_OverrideEntityName((IdentifierEscaping escaping, string exp { //Arrange var (escaping, expected) = testCase; - var movie = new Movie { Id = 1, Release_Year = 1988, Title = "Title" }; + var movie = new Movie { Id = 1, ReleaseYear = 1988, Title = "Title" }; var insertProperties = new InsertProperties { EntityName = "TestName", @@ -75,7 +115,7 @@ public void Generate_OverrideEntityName((IdentifierEscaping escaping, string exp public void Generate_OverrideEntityName_ShouldNotPluralize() { //Arrange - var movie = new Movie { Id = 1, Release_Year = 1988, Title = "Title" }; + var movie = new Movie { Id = 1, ReleaseYear = 1988, Title = "Title" }; var insertProperties = new InsertProperties { EntityName = "TestName", @@ -86,16 +126,16 @@ public void Generate_OverrideEntityName_ShouldNotPluralize() string statement = new CreateInsert(modelBuilder).Generate(movie, insertProperties); //Assert - statement.Should().Be($"INSERT INTO {insertProperties.EntityName} (Title, Id, Release_Year) VALUES ('Title', 1, 1988);"); + statement.Should().Be($"INSERT INTO {insertProperties.EntityName} (Title, Id, ReleaseYear) VALUES ('Title', 1, 1988);"); } [Test] public void Generate_UseModelBuilder_Ignore() { //Arrange - modelBuilder.Entity().Property(c => c.Release_Year).Ignore(); + modelBuilder.Entity().Property(c => c.ReleaseYear).Ignore(); - var movie = new Movie { Id = 1, Release_Year = 1988, Title = "Title" }; + var movie = new Movie { Id = 1, ReleaseYear = 1988, Title = "Title" }; var insertProperties = new InsertProperties { EntityName = "TestName", @@ -113,7 +153,7 @@ public void Generate_UseModelBuilder_Ignore() public void Generate_ShouldNotPluralizeEntityName() { //Arrange - var movie = new Movie { Id = 1, Release_Year = 1988, Title = "Title" }; + var movie = new Movie { Id = 1, ReleaseYear = 1988, Title = "Title" }; var insertProperties = new InsertProperties { ShouldPluralizeEntityName = false @@ -123,7 +163,7 @@ public void Generate_ShouldNotPluralizeEntityName() string statement = new CreateInsert(modelBuilder).Generate(movie, insertProperties); //Assert - statement.Should().Be($"INSERT INTO {nameof(Movie)} (Title, Id, Release_Year) VALUES ('Title', 1, 1988);"); + statement.Should().Be($"INSERT INTO {nameof(Movie)} (Title, Id, ReleaseYear) VALUES ('Title', 1, 1988);"); } public record Book(string Title, string Author); @@ -375,9 +415,9 @@ public class Kafka_table_order public static IEnumerable<(IdentifierEscaping, string)> IncludeReadOnlyPropertiedTestCases() { - yield return (Never, $"INSERT INTO {nameof(Movie)}s (Title, Id, Release_Year, ReadOnly) VALUES (NULL, 1, 0, ARRAY[1, 2]);"); - yield return (Keywords, $"INSERT INTO {nameof(Movie)}s (Title, Id, Release_Year, ReadOnly) VALUES (NULL, 1, 0, ARRAY[1, 2]);"); - yield return (Always, $"INSERT INTO `{nameof(Movie)}s` (`Title`, `Id`, `Release_Year`, `ReadOnly`) VALUES (NULL, 1, 0, ARRAY[1, 2]);"); + yield return (Never, $"INSERT INTO {nameof(Movie)}s (Title, Id, ReleaseYear, ReadOnly) VALUES (NULL, 1, 0, ARRAY[1, 2]);"); + yield return (Keywords, $"INSERT INTO {nameof(Movie)}s (Title, Id, ReleaseYear, ReadOnly) VALUES (NULL, 1, 0, ARRAY[1, 2]);"); + yield return (Always, $"INSERT INTO `{nameof(Movie)}s` (`Title`, `Id`, `ReleaseYear`, `ReadOnly`) VALUES (NULL, 1, 0, ARRAY[1, 2]);"); } [TestCaseSource(nameof(IncludeReadOnlyPropertiedTestCases))] diff --git a/Tests/ksqlDB.RestApi.Client.Tests/KSql/RestApi/TestableKSqlDbQueryStreamProvider.cs b/Tests/ksqlDB.RestApi.Client.Tests/KSql/RestApi/TestableKSqlDbQueryStreamProvider.cs index 742541e3..bf374323 100644 --- a/Tests/ksqlDB.RestApi.Client.Tests/KSql/RestApi/TestableKSqlDbQueryStreamProvider.cs +++ b/Tests/ksqlDB.RestApi.Client.Tests/KSql/RestApi/TestableKSqlDbQueryStreamProvider.cs @@ -1,4 +1,5 @@ using System.Net; +using ksqlDb.RestApi.Client.FluentAPI.Builders; using ksqlDB.RestApi.Client.KSql.RestApi; using ksqlDb.RestApi.Client.Tests.Helpers; using ksqlDb.RestApi.Client.Tests.Helpers.Http; @@ -12,7 +13,7 @@ namespace ksqlDb.RestApi.Client.Tests.KSql.RestApi; internal class TestableKSqlDbQueryStreamProvider : KSqlDbQueryStreamProvider { public TestableKSqlDbQueryStreamProvider(IHttpClientFactory httpClientFactory, ILogger? logger = null) - : base(httpClientFactory, TestKSqlDBContextOptions.Instance, logger) + : base(httpClientFactory, new ModelBuilder(), TestKSqlDBContextOptions.Instance, logger) { } diff --git a/Tests/ksqlDB.RestApi.Client.Tests/Metadata/EntityMetadataTests.cs b/Tests/ksqlDB.RestApi.Client.Tests/Metadata/EntityMetadataTests.cs index a7e2103f..0afd15c4 100644 --- a/Tests/ksqlDB.RestApi.Client.Tests/Metadata/EntityMetadataTests.cs +++ b/Tests/ksqlDB.RestApi.Client.Tests/Metadata/EntityMetadataTests.cs @@ -21,61 +21,60 @@ public void TestInitialize() public void Add_MemberWasStored() { //Arrange - var memberInfo = GetTitleMemberInfo(); + var memberExpression = GetTitleMemberExpression(); //Act - var added = entityMetadata.Add(memberInfo); + var added = entityMetadata.Add(memberExpression); //Assert added.Should().BeTrue(); } - private static MemberInfo GetTitleMemberInfo() + private static MemberExpression GetTitleMemberExpression() { Expression> expression = foo => new { foo.Title }; var argument = ((NewExpression)expression.Body).Arguments[0]; - var memberInfo = ((MemberExpression)argument).Member; - return memberInfo; + return (MemberExpression)argument; } [Test] public void Add_MemberWasNotStoredSecondTime() { //Arrange - var memberInfo = GetTitleMemberInfo(); - entityMetadata.Add(memberInfo); + var memberExpression = GetTitleMemberExpression(); + entityMetadata.Add(memberExpression); //Act - var added = entityMetadata.Add(GetTitleMemberInfo()); + var added = entityMetadata.Add(GetTitleMemberExpression()); //Assert added.Should().BeFalse(); } [Test] - public void TryGetMemberInfo_UnknownMemberName_NullIsReturned() + public void TryGetMemberExpression_UnknownMemberName_NullIsReturned() { //Arrange - var memberInfo = GetTitleMemberInfo(); - entityMetadata.Add(memberInfo); + var memberExpression = GetTitleMemberExpression(); + entityMetadata.Add(memberExpression); //Act - var result = entityMetadata.TryGetMemberInfo(nameof(Foo.Title)); + var result = entityMetadata.TryGetMemberExpression(nameof(Foo.Title)); //Assert result.Should().NotBeNull(); - result!.GetCustomAttribute().Should().NotBeNull(); + result!.Member.GetCustomAttribute().Should().NotBeNull(); } [Test] - public void TryGetMemberInfo_KnownMemberName_MemberInfoIsReturned() + public void TryGetMemberExpression_KnownMemberName_MemberExpressionIsReturned() { //Arrange - var memberInfo = GetTitleMemberInfo(); - entityMetadata.Add(memberInfo); + var memberExpression = GetTitleMemberExpression(); + entityMetadata.Add(memberExpression); //Act - var result = entityMetadata.TryGetMemberInfo(nameof(Foo.Id)); + var result = entityMetadata.TryGetMemberExpression(nameof(Foo.Id)); //Assert result.Should().BeNull(); From 8f67db6ea8dce5535ec678a7a97ec840e15a3d3f Mon Sep 17 00:00:00 2001 From: Tomas Fabian Date: Fri, 31 May 2024 16:51:54 +0200 Subject: [PATCH 03/17] [ksqlDB.RestApi.Client]: HasColumnName to Fluent API sample --- .../ModelBuilders/PaymentModelBuilder.cs | 6 +++++- .../ksqlDB.RestApi.Client.Samples.csproj | 4 ++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/Samples/ksqlDB.RestApi.Client.Sample/ModelBuilders/PaymentModelBuilder.cs b/Samples/ksqlDB.RestApi.Client.Sample/ModelBuilders/PaymentModelBuilder.cs index 8595f077..3328afe7 100644 --- a/Samples/ksqlDB.RestApi.Client.Sample/ModelBuilders/PaymentModelBuilder.cs +++ b/Samples/ksqlDB.RestApi.Client.Sample/ModelBuilders/PaymentModelBuilder.cs @@ -25,6 +25,10 @@ public async Task InitModelAndCreateStreamAsync(CancellationToken cancellationTo .Property(b => b.Amount) .Decimal(precision: 10, scale: 2); + modelBuilder.Entity() + .Property(b => b.Description) + .HasColumnName("desc"); + string header = "abc"; modelBuilder.Entity() .Property(c => c.Header) @@ -59,7 +63,7 @@ private IKSqlDbRestApiClient ConfigureRestApiClientWithServicesCollection(Servic { c.UseKSqlDb(ksqlDbUrl); - c.ReplaceHttpClient(_ => { }) + c.ReplaceHttpClient(_ => { }) .AddHttpMessageHandler(_ => new Program.DebugHandler()); }) .AddSingleton(builder); diff --git a/Samples/ksqlDB.RestApi.Client.Sample/ksqlDB.RestApi.Client.Samples.csproj b/Samples/ksqlDB.RestApi.Client.Sample/ksqlDB.RestApi.Client.Samples.csproj index 07522d56..0acc2a7e 100644 --- a/Samples/ksqlDB.RestApi.Client.Sample/ksqlDB.RestApi.Client.Samples.csproj +++ b/Samples/ksqlDB.RestApi.Client.Sample/ksqlDB.RestApi.Client.Samples.csproj @@ -9,8 +9,8 @@ - - + + From 9ebae01043b2e28dd3269d11c82a2a56f99cc168 Mon Sep 17 00:00:00 2001 From: Tomas Fabian Date: Fri, 31 May 2024 17:06:42 +0200 Subject: [PATCH 04/17] [ksqlDB.RestApi.Client]: HasColumnName to Fluent API sample --- .../FluentAPI/Builders/ModelBuilderTests.cs | 18 +++++++-------- .../FluentAPI/Builders/IMetadataProvider.cs | 11 ++++++++++ .../FluentAPI/Builders/ModelBuilder.cs | 16 ++++++++++---- .../Extensions/TypeExtensions.cs | 8 +++---- .../KSql/Query/Context/KSqlDBContext.cs | 1 + .../KSql/RestApi/Generators/TypeGenerator.cs | 6 ++--- .../KSql/RestApi/KSqlDbQueryStreamProvider.cs | 4 ++-- .../KSql/RestApi/Parsers/IdentifierUtil.cs | 22 +++++++++---------- .../KSql/RestApi/Statements/CreateEntity.cs | 8 +++---- .../KSql/RestApi/Statements/CreateInsert.cs | 18 +++++++-------- .../RestApi/Statements/CreateKSqlValue.cs | 2 +- .../KSql/RestApi/Statements/EntityInfo.cs | 4 ++-- .../RestApi/Statements/KSqlTypeTranslator.cs | 10 ++++----- .../Translators/DecimalTypeTranslator.cs | 2 +- 14 files changed, 74 insertions(+), 56 deletions(-) create mode 100644 ksqlDb.RestApi.Client/FluentAPI/Builders/IMetadataProvider.cs diff --git a/Tests/ksqlDB.RestApi.Client.Tests/FluentAPI/Builders/ModelBuilderTests.cs b/Tests/ksqlDB.RestApi.Client.Tests/FluentAPI/Builders/ModelBuilderTests.cs index 4fb188d0..4b3d8c81 100644 --- a/Tests/ksqlDB.RestApi.Client.Tests/FluentAPI/Builders/ModelBuilderTests.cs +++ b/Tests/ksqlDB.RestApi.Client.Tests/FluentAPI/Builders/ModelBuilderTests.cs @@ -62,7 +62,7 @@ public void Property_IgnoreField() //Assert fieldTypeBuilder.Should().NotBeNull(); - var entityMetadata = builder.GetEntities().FirstOrDefault(c => c.Type == typeof(Payment)); + var entityMetadata = ((IMetadataProvider)builder).GetEntities().FirstOrDefault(c => c.Type == typeof(Payment)); entityMetadata.Should().NotBeNull(); entityMetadata!.FieldsMetadata.First(c => c.MemberInfo.Name == nameof(Payment.Description)).Ignore.Should().BeTrue(); } @@ -80,7 +80,7 @@ public void Property_HasColumnName() //Assert fieldTypeBuilder.Should().NotBeNull(); - var entityMetadata = builder.GetEntities().FirstOrDefault(c => c.Type == typeof(Payment)); + var entityMetadata = ((IMetadataProvider)builder).GetEntities().FirstOrDefault(c => c.Type == typeof(Payment)); entityMetadata.Should().NotBeNull(); entityMetadata!.FieldsMetadata.First(c => c.MemberInfo.Name == nameof(Payment.Description)).ColumnName.Should().Be(columnName); } @@ -101,7 +101,7 @@ public void MultiplePropertiesForSameType() //Assert fieldTypeBuilder.Should().NotBeNull(); - var entityMetadata = builder.GetEntities().FirstOrDefault(c => c.Type == typeof(Payment)); + var entityMetadata = ((IMetadataProvider)builder).GetEntities().FirstOrDefault(c => c.Type == typeof(Payment)); entityMetadata.Should().NotBeNull(); entityMetadata!.FieldsMetadata.First(c => c.MemberInfo.Name == nameof(Payment.Description)).Ignore.Should().BeTrue(); entityMetadata.FieldsMetadata.First(c => c.MemberInfo.Name == nameof(Payment.Amount)).Ignore.Should().BeTrue(); @@ -123,7 +123,7 @@ public void Property_IgnoreNestedField() //Assert fieldTypeBuilder.Should().NotBeNull(); - var entityMetadata = builder.GetEntities().FirstOrDefault(c => c.Type == typeof(Composite)); + var entityMetadata = ((IMetadataProvider)builder).GetEntities().FirstOrDefault(c => c.Type == typeof(Composite)); entityMetadata.Should().NotBeNull(); var memberInfo = GetTitleMemberInfo(); entityMetadata!.FieldsMetadata.First(c => c.MemberInfo == memberInfo).Ignore.Should().BeTrue(); @@ -139,7 +139,7 @@ public void AddConventionForDecimal() builder.AddConvention(decimalTypeConvention); //Assert - builder.Conventions[typeof(decimal)].Should().BeEquivalentTo(decimalTypeConvention); + ((IMetadataProvider)builder).Conventions[typeof(decimal)].Should().BeEquivalentTo(decimalTypeConvention); } private class PaymentConfiguration : IFromItemTypeConfiguration @@ -161,7 +161,7 @@ public void FromItemTypeConfiguration() builder.Apply(configuration); //Assert - var entityMetadata = builder.GetEntities().FirstOrDefault(c => c.Type == typeof(Payment)); + var entityMetadata = ((IMetadataProvider)builder).GetEntities().FirstOrDefault(c => c.Type == typeof(Payment)); entityMetadata.Should().NotBeNull(); entityMetadata!.FieldsMetadata.First(c => c.MemberInfo.Name == nameof(Payment.Description)).Ignore.Should().BeTrue(); } @@ -188,7 +188,7 @@ public void Decimal_ConfigurePrecisionAndScale() //Assert fieldTypeBuilder.Should().NotBeNull(); - var entityMetadata = builder.GetEntities().FirstOrDefault(c => c.Type == typeof(Payment)); + var entityMetadata = ((IMetadataProvider)builder).GetEntities().FirstOrDefault(c => c.Type == typeof(Payment)); entityMetadata.Should().NotBeNull(); var metadata = (DecimalFieldMetadata)entityMetadata!.FieldsMetadata.First(c => c.MemberInfo.Name == nameof(Payment.Amount)); @@ -209,7 +209,7 @@ public void Header() //Assert fieldTypeBuilder.Should().NotBeNull(); - var entityMetadata = builder.GetEntities().FirstOrDefault(c => c.Type == typeof(Payment)); + var entityMetadata = ((IMetadataProvider)builder).GetEntities().FirstOrDefault(c => c.Type == typeof(Payment)); entityMetadata.Should().NotBeNull(); var metadata = (BytesArrayFieldMetadata)entityMetadata!.FieldsMetadata.First(c => c.MemberInfo.Name == nameof(Payment.Header)); @@ -228,7 +228,7 @@ public void Headers() //Assert fieldTypeBuilder.Should().NotBeNull(); - var entityMetadata = builder.GetEntities().FirstOrDefault(c => c.Type == typeof(Payment)); + var entityMetadata = ((IMetadataProvider)builder).GetEntities().FirstOrDefault(c => c.Type == typeof(Payment)); entityMetadata.Should().NotBeNull(); var metadata = entityMetadata!.FieldsMetadata.First(c => c.MemberInfo.Name == nameof(Payment.Header)); diff --git a/ksqlDb.RestApi.Client/FluentAPI/Builders/IMetadataProvider.cs b/ksqlDb.RestApi.Client/FluentAPI/Builders/IMetadataProvider.cs new file mode 100644 index 00000000..cbfaf943 --- /dev/null +++ b/ksqlDb.RestApi.Client/FluentAPI/Builders/IMetadataProvider.cs @@ -0,0 +1,11 @@ +using ksqlDb.RestApi.Client.FluentAPI.Builders.Configuration; +using ksqlDb.RestApi.Client.Metadata; + +namespace ksqlDb.RestApi.Client.FluentAPI.Builders +{ + internal interface IMetadataProvider + { + internal IEnumerable GetEntities(); + IDictionary Conventions { get; } + } +} diff --git a/ksqlDb.RestApi.Client/FluentAPI/Builders/ModelBuilder.cs b/ksqlDb.RestApi.Client/FluentAPI/Builders/ModelBuilder.cs index cc3d2ed8..6c5474e8 100644 --- a/ksqlDb.RestApi.Client/FluentAPI/Builders/ModelBuilder.cs +++ b/ksqlDb.RestApi.Client/FluentAPI/Builders/ModelBuilder.cs @@ -6,12 +6,20 @@ namespace ksqlDb.RestApi.Client.FluentAPI.Builders /// /// Represents a builder for configuring the model. /// - public class ModelBuilder + public class ModelBuilder : IMetadataProvider { private readonly IDictionary builders = new Dictionary(); - internal readonly IDictionary Conventions = new Dictionary(); + private readonly IDictionary conventions = new Dictionary(); - internal IEnumerable GetEntities() + IDictionary IMetadataProvider.Conventions + { + get + { + return conventions; + } + } + + IEnumerable IMetadataProvider.GetEntities() { return builders.Values.Select(c => c.Metadata); } @@ -37,7 +45,7 @@ public ModelBuilder Apply(IFromItemTypeConfiguration configura /// The current instance. public ModelBuilder AddConvention(IConventionConfiguration configuration) { - Conventions.Add(configuration.Type, configuration); + conventions.Add(configuration.Type, configuration); return this; } diff --git a/ksqlDb.RestApi.Client/Infrastructure/Extensions/TypeExtensions.cs b/ksqlDb.RestApi.Client/Infrastructure/Extensions/TypeExtensions.cs index 77a955b5..2e2f7c3b 100644 --- a/ksqlDb.RestApi.Client/Infrastructure/Extensions/TypeExtensions.cs +++ b/ksqlDb.RestApi.Client/Infrastructure/Extensions/TypeExtensions.cs @@ -105,16 +105,16 @@ internal static string ExtractTypeName(this Type type) return attribute; } - internal static string GetMemberName(this MemberExpression memberExpression, ModelBuilder? modelBuilder) + internal static string GetMemberName(this MemberExpression memberExpression, IMetadataProvider? metadataProvider) { - var entityMetadata = modelBuilder?.GetEntities().FirstOrDefault(c => c.Type == memberExpression.Expression?.Type); + var entityMetadata = metadataProvider?.GetEntities().FirstOrDefault(c => c.Type == memberExpression.Expression?.Type); return memberExpression.Member.GetMemberName(entityMetadata); } - internal static string GetMemberName(this MemberInfo memberInfo, ModelBuilder? modelBuilder) + internal static string GetMemberName(this MemberInfo memberInfo, IMetadataProvider? metadataProvider) { - var entityMetadata = modelBuilder?.GetEntities().FirstOrDefault(c => c.Type == memberInfo.DeclaringType); + var entityMetadata = metadataProvider?.GetEntities().FirstOrDefault(c => c.Type == memberInfo.DeclaringType); return memberInfo.GetMemberName(entityMetadata); } diff --git a/ksqlDb.RestApi.Client/KSql/Query/Context/KSqlDBContext.cs b/ksqlDb.RestApi.Client/KSql/Query/Context/KSqlDBContext.cs index 01d6c8e2..25c9c8a9 100644 --- a/ksqlDb.RestApi.Client/KSql/Query/Context/KSqlDBContext.cs +++ b/ksqlDb.RestApi.Client/KSql/Query/Context/KSqlDBContext.cs @@ -74,6 +74,7 @@ protected override void OnConfigureServices(IServiceCollection serviceCollection base.OnConfigureServices(serviceCollection, contextOptions); serviceCollection.TryAddSingleton(modelBuilder); + serviceCollection.TryAddSingleton(modelBuilder); serviceCollection.RegisterEndpointDependencies(contextOptions); } diff --git a/ksqlDb.RestApi.Client/KSql/RestApi/Generators/TypeGenerator.cs b/ksqlDb.RestApi.Client/KSql/RestApi/Generators/TypeGenerator.cs index f4363f5c..d1d856ae 100644 --- a/ksqlDb.RestApi.Client/KSql/RestApi/Generators/TypeGenerator.cs +++ b/ksqlDb.RestApi.Client/KSql/RestApi/Generators/TypeGenerator.cs @@ -9,9 +9,9 @@ namespace ksqlDB.RestApi.Client.KSql.RestApi.Generators; -internal sealed class TypeGenerator(ModelBuilder modelBuilder) : EntityInfo(modelBuilder) +internal sealed class TypeGenerator(ModelBuilder metadataProvider) : EntityInfo(metadataProvider) { - private readonly KSqlTypeTranslator typeTranslator = new(modelBuilder); + private readonly KSqlTypeTranslator typeTranslator = new(metadataProvider); internal string Print(TypeProperties properties) { @@ -36,7 +36,7 @@ private void PrintProperties(StringBuilder stringBuilder, IdentifierEscaping var ksqlType = typeTranslator.Translate(type, escaping); - var memberName = memberInfo.GetMemberName(modelBuilder); + var memberName = memberInfo.GetMemberName(metadataProvider); var columnDefinition = $"{EscapeName(memberName, escaping)} {ksqlType}{typeTranslator.ExploreAttributes(typeof(T), memberInfo, type)}"; ksqlProperties.Add(columnDefinition); } diff --git a/ksqlDb.RestApi.Client/KSql/RestApi/KSqlDbQueryStreamProvider.cs b/ksqlDb.RestApi.Client/KSql/RestApi/KSqlDbQueryStreamProvider.cs index 67eba0a0..9fc7c250 100644 --- a/ksqlDb.RestApi.Client/KSql/RestApi/KSqlDbQueryStreamProvider.cs +++ b/ksqlDb.RestApi.Client/KSql/RestApi/KSqlDbQueryStreamProvider.cs @@ -85,9 +85,9 @@ internal void JsonPropertyNameModifier(JsonTypeInfo jsonTypeInfo) JsonPropertyNameModifier(jsonTypeInfo, modelBuilder); } - internal static void JsonPropertyNameModifier(JsonTypeInfo jsonTypeInfo, ModelBuilder modelBuilder) + internal static void JsonPropertyNameModifier(JsonTypeInfo jsonTypeInfo, IMetadataProvider metadataProvider) { - var entityMetadata = modelBuilder.GetEntities().FirstOrDefault(c => c.Type == jsonTypeInfo.Type); + var entityMetadata = metadataProvider.GetEntities().FirstOrDefault(c => c.Type == jsonTypeInfo.Type); foreach (var typeInfoProperty in jsonTypeInfo.Properties) { diff --git a/ksqlDb.RestApi.Client/KSql/RestApi/Parsers/IdentifierUtil.cs b/ksqlDb.RestApi.Client/KSql/RestApi/Parsers/IdentifierUtil.cs index 5744919f..ba1c67a7 100644 --- a/ksqlDb.RestApi.Client/KSql/RestApi/Parsers/IdentifierUtil.cs +++ b/ksqlDb.RestApi.Client/KSql/RestApi/Parsers/IdentifierUtil.cs @@ -51,27 +51,25 @@ Keywords when IsValid(identifier) && SystemColumns.IsValid(identifier) => identi }; } - /// - /// Format the identifier, except when it is a PseudoColumn. - /// - /// the memberExpression with the identifier - /// the format - /// the model builder - /// the identifier modified based on the provided format - public static string Format(MemberExpression memberExpression, IdentifierEscaping escaping, ModelBuilder? modelBuilder = null) + internal static string Format(MemberExpression memberExpression, IdentifierEscaping escaping, IMetadataProvider? metadataProvider = null) { return escaping switch { - Never => memberExpression.GetMemberName(modelBuilder), + Never => memberExpression.GetMemberName(metadataProvider), Keywords when memberExpression.Member.GetCustomAttribute() != null => memberExpression.Member.Name, - Keywords when IsValid(memberExpression.GetMemberName(modelBuilder)) && SystemColumns.IsValid(memberExpression.GetMemberName(modelBuilder)) => memberExpression.GetMemberName(modelBuilder), - Keywords => string.Concat("`", memberExpression.GetMemberName(modelBuilder), "`"), + Keywords when IsValid(memberExpression.GetMemberName(metadataProvider)) && SystemColumns.IsValid(memberExpression.GetMemberName(metadataProvider)) => memberExpression.GetMemberName(metadataProvider), + Keywords => string.Concat("`", memberExpression.GetMemberName(metadataProvider), "`"), Always when memberExpression.Member.GetCustomAttribute() != null => memberExpression.Member.Name, - Always => string.Concat("`", memberExpression.GetMemberName(modelBuilder), "`"), + Always => string.Concat("`", memberExpression.GetMemberName(metadataProvider), "`"), _ => throw new ArgumentOutOfRangeException(nameof(escaping), escaping, "Non-exhaustive match.") }; } + internal static string Format(MemberInfo memberInfo, IdentifierEscaping escaping, IMetadataProvider? modelBuilder = null) + { + return Format(memberInfo, escaping, modelBuilder as ModelBuilder); + } + /// /// Format the identifier, except when it is a PseudoColumn. /// diff --git a/ksqlDb.RestApi.Client/KSql/RestApi/Statements/CreateEntity.cs b/ksqlDb.RestApi.Client/KSql/RestApi/Statements/CreateEntity.cs index 14fba8dc..a035fe40 100644 --- a/ksqlDb.RestApi.Client/KSql/RestApi/Statements/CreateEntity.cs +++ b/ksqlDb.RestApi.Client/KSql/RestApi/Statements/CreateEntity.cs @@ -9,11 +9,11 @@ namespace ksqlDB.RestApi.Client.KSql.RestApi.Statements; -internal sealed class CreateEntity(ModelBuilder modelBuilder) : EntityInfo(modelBuilder) +internal sealed class CreateEntity(IMetadataProvider metadataProvider) : EntityInfo(metadataProvider) { private readonly StringBuilder stringBuilder = new(); - private readonly KSqlTypeTranslator typeTranslator = new(modelBuilder); + private readonly KSqlTypeTranslator typeTranslator = new(metadataProvider); internal string Print(StatementContext statementContext, EntityCreationMetadata metadata, bool? ifNotExists) { @@ -49,7 +49,7 @@ private void PrintProperties(StatementContext statementContext, EntityCreatio var ksqlType = typeTranslator.Translate(type, metadata.IdentifierEscaping); - var columnName = IdentifierUtil.Format(memberInfo, metadata.IdentifierEscaping, modelBuilder); + var columnName = IdentifierUtil.Format(memberInfo, metadata.IdentifierEscaping, metadataProvider); string columnDefinition = $"\t{columnName} {ksqlType}{typeTranslator.ExploreAttributes(typeof(T), memberInfo, type)}"; columnDefinition += TryAttachKey(statementContext.KSqlEntityType, memberInfo); @@ -85,7 +85,7 @@ private void PrintCreateOrReplace(StatementContext statementContext, EntityCr private string TryAttachKey(KSqlEntityType entityType, MemberInfo memberInfo) { - var entityMetadata = modelBuilder.GetEntities().FirstOrDefault(c => c.Type == typeof(T)); + var entityMetadata = metadataProvider.GetEntities().FirstOrDefault(c => c.Type == typeof(T)); var primaryKey = entityMetadata?.PrimaryKeyMemberInfo; diff --git a/ksqlDb.RestApi.Client/KSql/RestApi/Statements/CreateInsert.cs b/ksqlDb.RestApi.Client/KSql/RestApi/Statements/CreateInsert.cs index 97261a2e..acc938c1 100644 --- a/ksqlDb.RestApi.Client/KSql/RestApi/Statements/CreateInsert.cs +++ b/ksqlDb.RestApi.Client/KSql/RestApi/Statements/CreateInsert.cs @@ -10,12 +10,12 @@ namespace ksqlDB.RestApi.Client.KSql.RestApi.Statements; internal sealed class CreateInsert : EntityInfo { - private readonly ModelBuilder modelBuilder; + private readonly ModelBuilder metadataProvider; - public CreateInsert(ModelBuilder modelBuilder) - : base(modelBuilder) + public CreateInsert(ModelBuilder metadataProvider) + : base(metadataProvider) { - this.modelBuilder = modelBuilder; + this.metadataProvider = metadataProvider; } internal string Generate(T entity, InsertProperties? insertProperties = null) @@ -51,11 +51,11 @@ internal string Generate(InsertValues insertValues, InsertProperties? inse valuesStringBuilder.Append(", "); } - columnsStringBuilder.Append(memberInfo.Format(insertProperties.IdentifierEscaping, modelBuilder)); + columnsStringBuilder.Append(memberInfo.Format(insertProperties.IdentifierEscaping, metadataProvider)); var type = GetMemberType(memberInfo); - var value = GetValue(insertValues, insertProperties, memberInfo, type, mi => IdentifierUtil.Format(mi, insertProperties.IdentifierEscaping, modelBuilder)); + var value = GetValue(insertValues, insertProperties, memberInfo, type, mi => IdentifierUtil.Format(mi, insertProperties.IdentifierEscaping, metadataProvider)); valuesStringBuilder.Append(value); } @@ -69,14 +69,14 @@ internal string Generate(InsertValues insertValues, InsertProperties? inse private object GetValue(InsertValues insertValues, InsertProperties insertProperties, MemberInfo memberInfo, Type type, Func formatter) { - var hasValue = insertValues.PropertyValues.ContainsKey(memberInfo.Format(insertProperties.IdentifierEscaping, modelBuilder)); + var hasValue = insertValues.PropertyValues.ContainsKey(memberInfo.Format(insertProperties.IdentifierEscaping, metadataProvider)); object value; if (hasValue) - value = insertValues.PropertyValues[memberInfo.Format(insertProperties.IdentifierEscaping, modelBuilder)]; + value = insertValues.PropertyValues[memberInfo.Format(insertProperties.IdentifierEscaping, metadataProvider)]; else - value = new CreateKSqlValue(modelBuilder).ExtractValue(insertValues.Entity, insertProperties, memberInfo, type, formatter); + value = new CreateKSqlValue(metadataProvider).ExtractValue(insertValues.Entity, insertProperties, memberInfo, type, formatter); return value; } diff --git a/ksqlDb.RestApi.Client/KSql/RestApi/Statements/CreateKSqlValue.cs b/ksqlDb.RestApi.Client/KSql/RestApi/Statements/CreateKSqlValue.cs index fdacb72a..ee82a62b 100644 --- a/ksqlDb.RestApi.Client/KSql/RestApi/Statements/CreateKSqlValue.cs +++ b/ksqlDb.RestApi.Client/KSql/RestApi/Statements/CreateKSqlValue.cs @@ -11,7 +11,7 @@ namespace ksqlDB.RestApi.Client.KSql.RestApi.Statements; #nullable disable -internal sealed class CreateKSqlValue(ModelBuilder modelBuilder) : EntityInfo(modelBuilder) +internal sealed class CreateKSqlValue(ModelBuilder metadataProvider) : EntityInfo(metadataProvider) { public object ExtractValue(T inputValue, IValueFormatters valueFormatters, MemberInfo memberInfo, Type type, Func formatter) { diff --git a/ksqlDb.RestApi.Client/KSql/RestApi/Statements/EntityInfo.cs b/ksqlDb.RestApi.Client/KSql/RestApi/Statements/EntityInfo.cs index 4e847fe3..4f7654dd 100644 --- a/ksqlDb.RestApi.Client/KSql/RestApi/Statements/EntityInfo.cs +++ b/ksqlDb.RestApi.Client/KSql/RestApi/Statements/EntityInfo.cs @@ -5,7 +5,7 @@ namespace ksqlDB.RestApi.Client.KSql.RestApi.Statements; -internal class EntityInfo(ModelBuilder modelBuilder) +internal class EntityInfo(IMetadataProvider metadataProvider) { protected static readonly EntityProvider EntityProvider = new(); @@ -24,7 +24,7 @@ protected IEnumerable Members(Type type, bool? includeReadOnly = nul .OfType() .Concat(fields); - var entityMetadata = modelBuilder.GetEntities().FirstOrDefault(c => c.Type == type); + var entityMetadata = metadataProvider.GetEntities().FirstOrDefault(c => c.Type == type); return properties.Where(c => { diff --git a/ksqlDb.RestApi.Client/KSql/RestApi/Statements/KSqlTypeTranslator.cs b/ksqlDb.RestApi.Client/KSql/RestApi/Statements/KSqlTypeTranslator.cs index c0c392b4..21417cba 100644 --- a/ksqlDb.RestApi.Client/KSql/RestApi/Statements/KSqlTypeTranslator.cs +++ b/ksqlDb.RestApi.Client/KSql/RestApi/Statements/KSqlTypeTranslator.cs @@ -9,10 +9,10 @@ namespace ksqlDB.RestApi.Client.KSql.RestApi.Statements { - internal sealed class KSqlTypeTranslator(ModelBuilder modelBuilder) : EntityInfo(modelBuilder) + internal sealed class KSqlTypeTranslator(IMetadataProvider metadataProvider) : EntityInfo(metadataProvider) { - private readonly ModelBuilder modelBuilder = modelBuilder; - private readonly DecimalTypeTranslator decimalTypeTranslator = new(modelBuilder); + private readonly IMetadataProvider metadataProvider = metadataProvider; + private readonly DecimalTypeTranslator decimalTypeTranslator = new(metadataProvider); internal string Translate(Type type, IdentifierEscaping escaping = IdentifierEscaping.Never) { @@ -107,7 +107,7 @@ internal IEnumerable GetProperties(Type type, IdentifierEscaping escapin var ksqlType = Translate(memberType, escaping); - string columnDefinition = $"{memberInfo.Format(escaping, modelBuilder)} {ksqlType}{ExploreAttributes(type, memberInfo, memberType)}"; + string columnDefinition = $"{memberInfo.Format(escaping, metadataProvider as ModelBuilder)} {ksqlType}{ExploreAttributes(type, memberInfo, memberType)}"; ksqlProperties.Add(columnDefinition); } @@ -132,7 +132,7 @@ internal string ExploreAttributes(Type? parentType, MemberInfo memberInfo, Type private string TryGetHeaderMarker(Type? parentType, MemberInfo memberInfo) { - var entityMetadata = modelBuilder.GetEntities().FirstOrDefault(c => c.Type == parentType); + var entityMetadata = metadataProvider.GetEntities().FirstOrDefault(c => c.Type == parentType); var fieldMetadata = entityMetadata?.GetFieldMetadataBy(memberInfo); if (fieldMetadata?.HasHeaders ?? false) diff --git a/ksqlDb.RestApi.Client/KSql/RestApi/Statements/Translators/DecimalTypeTranslator.cs b/ksqlDb.RestApi.Client/KSql/RestApi/Statements/Translators/DecimalTypeTranslator.cs index ac97cada..0e30fe54 100644 --- a/ksqlDb.RestApi.Client/KSql/RestApi/Statements/Translators/DecimalTypeTranslator.cs +++ b/ksqlDb.RestApi.Client/KSql/RestApi/Statements/Translators/DecimalTypeTranslator.cs @@ -7,7 +7,7 @@ namespace ksqlDb.RestApi.Client.KSql.RestApi.Statements.Translators { - internal class DecimalTypeTranslator(ModelBuilder modelBuilder) + internal class DecimalTypeTranslator(IMetadataProvider modelBuilder) { internal bool TryGetDecimal(Type? parentType, MemberInfo memberInfo, out string? @decimal) { From ad341840e067dfbc1eb88f11ad4a6aa1b295e09a Mon Sep 17 00:00:00 2001 From: Tomas Fabian Date: Fri, 31 May 2024 17:11:32 +0200 Subject: [PATCH 05/17] [ksqlDB.RestApi.Client]: HasColumnName to Fluent API documentation --- docs/modelbuilder.md | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/docs/modelbuilder.md b/docs/modelbuilder.md index 5659a984..53d2511f 100644 --- a/docs/modelbuilder.md +++ b/docs/modelbuilder.md @@ -223,3 +223,25 @@ CREATE STREAM Movie ( ``` The `WithHeaders` function within the FLUENT API takes precedence over the `HeadersAttribute`. + +### HasColumnName +**v6.1.0** +The `HasColumnName` function is employed during JSON deserialization and code generation, particularly in tasks like crafting CREATE STREAM or INSERT INTO statements. + +The below code demonstrates how to use the `HasColumnName` method in the Fluent API to override the property name `Description` to `Desc` during code generation: + +```C# +using ksqlDB.RestApi.Client.KSql.RestApi.Enums; + +modelBuilder.Entity() + .Property(b => b.Description) + .HasColumnName("Desc"); + +var statement = new CreateInsert(modelBuilder).Generate(movie, new InsertProperties { IdentifierEscaping = IdentifierEscaping.Keywords }); +``` + +The KSQL snippet illustrates an example INSERT statement with the overridden column names, showing how it corresponds to the Fluent API configuration: + +```SQL +INSERT INTO Payments (Id, Amount, Desc) VALUES ('1', 33, 'Purchase'); +``` From 57ab7a0d72a1ce75cb7552034f3f562384be436f Mon Sep 17 00:00:00 2001 From: Tomas Fabian Date: Fri, 31 May 2024 17:12:06 +0200 Subject: [PATCH 06/17] [ksqlDB.RestApi.Client]: HasColumnName to Fluent API release candidate --- ksqlDb.RestApi.Client/ChangeLog.md | 12 +++++++++--- ksqlDb.RestApi.Client/ksqlDb.RestApi.Client.csproj | 4 ++-- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/ksqlDb.RestApi.Client/ChangeLog.md b/ksqlDb.RestApi.Client/ChangeLog.md index 6207496b..63766413 100644 --- a/ksqlDb.RestApi.Client/ChangeLog.md +++ b/ksqlDb.RestApi.Client/ChangeLog.md @@ -1,13 +1,19 @@ # ksqlDB.RestApi.Client +# 6.1.0-rc.1 +- added `HasColumnName` to the Fluent API to allow overriding property names during JSON deserialization and code generation. + +## BugFix +- fixed missing usage/evaluation of the `JsonPropertyNameAttribute` during the creation of types. + # 6.0.2 # BugFix -- C# decimal is mapped to STRUCT type. #81 +- C# decimal is mapped to STRUCT type #81 # 6.0.1 -# BugFix +## BugFix - requests with `KSqlDbRestApiClient` can result in 431 error codes #80 # 6.0.0 @@ -17,7 +23,7 @@ - introduced distinct parameters specifically tailored for pull queries. This modification results in a breaking change. Before this update, the parameters sent to both the 'query' and 'query-stream' endpoints were shared between pull and push queries. #77 - see also [breakingchanges.md](https://github.com/tomasfabian/ksqlDB.RestApi.Client-DotNet/blob/main/docs/breaking_changes.md#v600) -# BugFix +## BugFix - CreateQueryStream doesn't always use configured parameters. The PullQuery functionality was overriding the options configured for the PushQuery. #75 reported by @jbkuczma # 5.1.0 diff --git a/ksqlDb.RestApi.Client/ksqlDb.RestApi.Client.csproj b/ksqlDb.RestApi.Client/ksqlDb.RestApi.Client.csproj index 0445ec30..acad7df9 100644 --- a/ksqlDb.RestApi.Client/ksqlDb.RestApi.Client.csproj +++ b/ksqlDb.RestApi.Client/ksqlDb.RestApi.Client.csproj @@ -15,8 +15,8 @@ Documentation for the library can be found at https://github.com/tomasfabian/ksqlDB.RestApi.Client-DotNet/blob/main/README.md. ksql ksqlDB LINQ .NET csharp push query - 6.0.2 - 6.0.2.0 + 6.1.0-rc.1 + 6.1.0.0 12.0 enable https://github.com/tomasfabian/ksqlDB.RestApi.Client-DotNet/blob/main/ksqlDb.RestApi.Client/ChangeLog.md From 309cb3eeea8a85f20308627647481fff66e0f74b Mon Sep 17 00:00:00 2001 From: Tomas Fabian Date: Fri, 31 May 2024 17:29:30 +0200 Subject: [PATCH 07/17] [ksqlDB.RestApi.Client]: IMetadataProvider injection instead of ModelBuilder --- .../KSql/RestApi/Generators/TypeGenerator.cs | 2 +- .../KSql/RestApi/KSqlDbQueryStreamProvider.cs | 8 ++++---- .../KSql/RestApi/Statements/CreateInsert.cs | 18 +++++++++--------- .../KSql/RestApi/Statements/CreateKSqlValue.cs | 2 +- 4 files changed, 15 insertions(+), 15 deletions(-) diff --git a/ksqlDb.RestApi.Client/KSql/RestApi/Generators/TypeGenerator.cs b/ksqlDb.RestApi.Client/KSql/RestApi/Generators/TypeGenerator.cs index d1d856ae..a800d032 100644 --- a/ksqlDb.RestApi.Client/KSql/RestApi/Generators/TypeGenerator.cs +++ b/ksqlDb.RestApi.Client/KSql/RestApi/Generators/TypeGenerator.cs @@ -9,7 +9,7 @@ namespace ksqlDB.RestApi.Client.KSql.RestApi.Generators; -internal sealed class TypeGenerator(ModelBuilder metadataProvider) : EntityInfo(metadataProvider) +internal sealed class TypeGenerator(IMetadataProvider metadataProvider) : EntityInfo(metadataProvider) { private readonly KSqlTypeTranslator typeTranslator = new(metadataProvider); diff --git a/ksqlDb.RestApi.Client/KSql/RestApi/KSqlDbQueryStreamProvider.cs b/ksqlDb.RestApi.Client/KSql/RestApi/KSqlDbQueryStreamProvider.cs index 9fc7c250..ddd10ffd 100644 --- a/ksqlDb.RestApi.Client/KSql/RestApi/KSqlDbQueryStreamProvider.cs +++ b/ksqlDb.RestApi.Client/KSql/RestApi/KSqlDbQueryStreamProvider.cs @@ -15,12 +15,12 @@ namespace ksqlDB.RestApi.Client.KSql.RestApi { internal class KSqlDbQueryStreamProvider : KSqlDbProvider { - private readonly ModelBuilder modelBuilder; + private readonly IMetadataProvider metadataProvider; - public KSqlDbQueryStreamProvider(IHttpClientFactory httpClientFactory, ModelBuilder modelBuilder, KSqlDbProviderOptions options, ILogger logger = null) + public KSqlDbQueryStreamProvider(IHttpClientFactory httpClientFactory, IMetadataProvider metadataProvider, KSqlDbProviderOptions options, ILogger logger = null) : base(httpClientFactory, options, logger) { - this.modelBuilder = modelBuilder ?? throw new ArgumentNullException(nameof(modelBuilder)); + this.metadataProvider = metadataProvider ?? throw new ArgumentNullException(nameof(metadataProvider)); #if NETCOREAPP3_1 AppContext.SetSwitch("System.Net.Http.SocketsHttpHandler.Http2UnencryptedSupport", true); #endif @@ -82,7 +82,7 @@ protected override JsonSerializerOptions OnCreateJsonSerializerOptions() internal void JsonPropertyNameModifier(JsonTypeInfo jsonTypeInfo) { - JsonPropertyNameModifier(jsonTypeInfo, modelBuilder); + JsonPropertyNameModifier(jsonTypeInfo, metadataProvider); } internal static void JsonPropertyNameModifier(JsonTypeInfo jsonTypeInfo, IMetadataProvider metadataProvider) diff --git a/ksqlDb.RestApi.Client/KSql/RestApi/Statements/CreateInsert.cs b/ksqlDb.RestApi.Client/KSql/RestApi/Statements/CreateInsert.cs index acc938c1..97261a2e 100644 --- a/ksqlDb.RestApi.Client/KSql/RestApi/Statements/CreateInsert.cs +++ b/ksqlDb.RestApi.Client/KSql/RestApi/Statements/CreateInsert.cs @@ -10,12 +10,12 @@ namespace ksqlDB.RestApi.Client.KSql.RestApi.Statements; internal sealed class CreateInsert : EntityInfo { - private readonly ModelBuilder metadataProvider; + private readonly ModelBuilder modelBuilder; - public CreateInsert(ModelBuilder metadataProvider) - : base(metadataProvider) + public CreateInsert(ModelBuilder modelBuilder) + : base(modelBuilder) { - this.metadataProvider = metadataProvider; + this.modelBuilder = modelBuilder; } internal string Generate(T entity, InsertProperties? insertProperties = null) @@ -51,11 +51,11 @@ internal string Generate(InsertValues insertValues, InsertProperties? inse valuesStringBuilder.Append(", "); } - columnsStringBuilder.Append(memberInfo.Format(insertProperties.IdentifierEscaping, metadataProvider)); + columnsStringBuilder.Append(memberInfo.Format(insertProperties.IdentifierEscaping, modelBuilder)); var type = GetMemberType(memberInfo); - var value = GetValue(insertValues, insertProperties, memberInfo, type, mi => IdentifierUtil.Format(mi, insertProperties.IdentifierEscaping, metadataProvider)); + var value = GetValue(insertValues, insertProperties, memberInfo, type, mi => IdentifierUtil.Format(mi, insertProperties.IdentifierEscaping, modelBuilder)); valuesStringBuilder.Append(value); } @@ -69,14 +69,14 @@ internal string Generate(InsertValues insertValues, InsertProperties? inse private object GetValue(InsertValues insertValues, InsertProperties insertProperties, MemberInfo memberInfo, Type type, Func formatter) { - var hasValue = insertValues.PropertyValues.ContainsKey(memberInfo.Format(insertProperties.IdentifierEscaping, metadataProvider)); + var hasValue = insertValues.PropertyValues.ContainsKey(memberInfo.Format(insertProperties.IdentifierEscaping, modelBuilder)); object value; if (hasValue) - value = insertValues.PropertyValues[memberInfo.Format(insertProperties.IdentifierEscaping, metadataProvider)]; + value = insertValues.PropertyValues[memberInfo.Format(insertProperties.IdentifierEscaping, modelBuilder)]; else - value = new CreateKSqlValue(metadataProvider).ExtractValue(insertValues.Entity, insertProperties, memberInfo, type, formatter); + value = new CreateKSqlValue(modelBuilder).ExtractValue(insertValues.Entity, insertProperties, memberInfo, type, formatter); return value; } diff --git a/ksqlDb.RestApi.Client/KSql/RestApi/Statements/CreateKSqlValue.cs b/ksqlDb.RestApi.Client/KSql/RestApi/Statements/CreateKSqlValue.cs index ee82a62b..4d8673ed 100644 --- a/ksqlDb.RestApi.Client/KSql/RestApi/Statements/CreateKSqlValue.cs +++ b/ksqlDb.RestApi.Client/KSql/RestApi/Statements/CreateKSqlValue.cs @@ -11,7 +11,7 @@ namespace ksqlDB.RestApi.Client.KSql.RestApi.Statements; #nullable disable -internal sealed class CreateKSqlValue(ModelBuilder metadataProvider) : EntityInfo(metadataProvider) +internal sealed class CreateKSqlValue(IMetadataProvider metadataProvider) : EntityInfo(metadataProvider) { public object ExtractValue(T inputValue, IValueFormatters valueFormatters, MemberInfo memberInfo, Type type, Func formatter) { From 0120655b713347de7221316c741eb306c3538a3f Mon Sep 17 00:00:00 2001 From: Tomas Fabian Date: Sat, 1 Jun 2024 10:39:06 +0200 Subject: [PATCH 08/17] [ksqlDB.RestApi.Client]: added JsonTypeInfoResolver improvements and unit test --- .../RestApi/Json/JsonTypeInfoResolverTests.cs | 39 +++++++++++++++++++ .../KSql/RestApi/Json/JsonTypeInfoResolver.cs | 12 +++--- 2 files changed, 46 insertions(+), 5 deletions(-) create mode 100644 Tests/ksqlDB.RestApi.Client.Tests/KSql/RestApi/Json/JsonTypeInfoResolverTests.cs diff --git a/Tests/ksqlDB.RestApi.Client.Tests/KSql/RestApi/Json/JsonTypeInfoResolverTests.cs b/Tests/ksqlDB.RestApi.Client.Tests/KSql/RestApi/Json/JsonTypeInfoResolverTests.cs new file mode 100644 index 00000000..e8c6bdf7 --- /dev/null +++ b/Tests/ksqlDB.RestApi.Client.Tests/KSql/RestApi/Json/JsonTypeInfoResolverTests.cs @@ -0,0 +1,39 @@ +using System.Text.Json; +using System.Text.Json.Serialization.Metadata; +using FluentAssertions; +using ksqlDb.RestApi.Client.Tests.Models; +using NUnit.Framework; +using UnitTests; +using JsonTypeInfoResolver = ksqlDb.RestApi.Client.KSql.RestApi.Json.JsonTypeInfoResolver; + +namespace ksqlDb.RestApi.Client.Tests.KSql.RestApi.Json +{ + public class JsonTypeInfoResolverTests : TestBase + { + [Test] + public void GetTypeInfo() + { + //Arrange + var resolver = new JsonTypeInfoResolver(new DefaultJsonTypeInfoResolver()) + { + Modifiers = { ToUpperPropertyNameModifier } + }; + + //Act + var typeInfo = resolver.GetTypeInfo(typeof(Tweet), new JsonSerializerOptions()); + + //Assert + typeInfo!.Type.Should().Be(typeof(Tweet)); + typeInfo!.Type.Properties().Count().Should().Be(5); + typeInfo.Properties.Any(c => c.Name == nameof(Tweet.Amount).ToUpper()).Should().BeTrue(); + } + + internal void ToUpperPropertyNameModifier(JsonTypeInfo jsonTypeInfo) + { + foreach (var jsonPropertyInfo in jsonTypeInfo.Properties) + { + jsonPropertyInfo.Name = jsonPropertyInfo.Name.ToUpper(); + } + } + } +} diff --git a/ksqlDb.RestApi.Client/KSql/RestApi/Json/JsonTypeInfoResolver.cs b/ksqlDb.RestApi.Client/KSql/RestApi/Json/JsonTypeInfoResolver.cs index 18ad4766..9942b45f 100644 --- a/ksqlDb.RestApi.Client/KSql/RestApi/Json/JsonTypeInfoResolver.cs +++ b/ksqlDb.RestApi.Client/KSql/RestApi/Json/JsonTypeInfoResolver.cs @@ -13,13 +13,15 @@ internal class JsonTypeInfoResolver(IJsonTypeInfoResolver typeInfoResolver) : IJ public virtual JsonTypeInfo? GetTypeInfo(Type type, JsonSerializerOptions options) { var typeInfo = typeInfoResolver.GetTypeInfo(type, options); + if (typeInfo == null) + return null; - if (modifiers != null) + if (modifiers == null) + return typeInfo; + + foreach (var modifier in modifiers) { - foreach (Action modifier in modifiers) - { - modifier(typeInfo); - } + modifier(typeInfo); } return typeInfo; From 4c7a9e1997f5149b9805a7ee87ec6f2cf1aaacf6 Mon Sep 17 00:00:00 2001 From: Tomas Fabian Date: Sat, 1 Jun 2024 11:15:29 +0200 Subject: [PATCH 09/17] [ksqlDB.RestApi.Client]: fixed null reference exception in KSqlDbQueryStreamProvider.JsonPropertyNameModifier --- ksqlDb.RestApi.Client/KSql/RestApi/KSqlDbQueryStreamProvider.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ksqlDb.RestApi.Client/KSql/RestApi/KSqlDbQueryStreamProvider.cs b/ksqlDb.RestApi.Client/KSql/RestApi/KSqlDbQueryStreamProvider.cs index ddd10ffd..6f0e994d 100644 --- a/ksqlDb.RestApi.Client/KSql/RestApi/KSqlDbQueryStreamProvider.cs +++ b/ksqlDb.RestApi.Client/KSql/RestApi/KSqlDbQueryStreamProvider.cs @@ -92,7 +92,7 @@ internal static void JsonPropertyNameModifier(JsonTypeInfo jsonTypeInfo, IMetada foreach (var typeInfoProperty in jsonTypeInfo.Properties) { var fieldMetadata = - entityMetadata?.FieldsMetadata.FirstOrDefault(c => c.MemberInfo.Name == typeInfoProperty.Name); + entityMetadata?.FieldsMetadata?.FirstOrDefault(c => c.MemberInfo.Name == typeInfoProperty.Name); if (fieldMetadata != null && !string.IsNullOrEmpty(fieldMetadata.ColumnName)) typeInfoProperty.Name = fieldMetadata.ColumnName; From 85381b4598282a18c0e417f66cf4fe037b1d51b6 Mon Sep 17 00:00:00 2001 From: Tomas Fabian Date: Sun, 2 Jun 2024 09:30:43 +0200 Subject: [PATCH 10/17] [ksqlDB.RestApi.Client]: JsonPropertyNameModifier moved to KSqlDbProvider --- .../RestApi/TestableKSqlDbQueryProvider.cs | 7 +-- .../KSql/RestApi/KSqlDbProvider.cs | 51 ++++++++++++++++-- .../KSql/RestApi/KSqlDbQueryStreamProvider.cs | 52 +------------------ .../KSql/RestApi/Query/KSqlDbQueryProvider.cs | 5 +- .../ksqlDb.RestApi.Client.csproj | 2 +- 5 files changed, 57 insertions(+), 60 deletions(-) diff --git a/Tests/ksqlDB.RestApi.Client.Tests/KSql/RestApi/TestableKSqlDbQueryProvider.cs b/Tests/ksqlDB.RestApi.Client.Tests/KSql/RestApi/TestableKSqlDbQueryProvider.cs index 519235b1..d332443a 100644 --- a/Tests/ksqlDB.RestApi.Client.Tests/KSql/RestApi/TestableKSqlDbQueryProvider.cs +++ b/Tests/ksqlDB.RestApi.Client.Tests/KSql/RestApi/TestableKSqlDbQueryProvider.cs @@ -1,4 +1,5 @@ -using ksqlDB.RestApi.Client.KSql.RestApi.Http; +using ksqlDb.RestApi.Client.FluentAPI.Builders; +using ksqlDB.RestApi.Client.KSql.RestApi.Http; using ksqlDB.RestApi.Client.KSql.RestApi.Query; using ksqlDb.RestApi.Client.Tests.Fakes.Http; @@ -7,7 +8,7 @@ namespace ksqlDb.RestApi.Client.Tests.KSql.RestApi; internal class TestableKSqlDbQueryProvider : KSqlDbQueryProvider { public TestableKSqlDbQueryProvider(IHttpV1ClientFactory httpClientFactory) - : base(httpClientFactory, TestKSqlDBContextOptions.Instance) + : base(httpClientFactory, new ModelBuilder(), TestKSqlDBContextOptions.Instance) { } @@ -17,4 +18,4 @@ protected override HttpClient OnCreateHttpClient() { return FakeHttpClient.CreateWithResponse(QueryResponse);; } -} \ No newline at end of file +} diff --git a/ksqlDb.RestApi.Client/KSql/RestApi/KSqlDbProvider.cs b/ksqlDb.RestApi.Client/KSql/RestApi/KSqlDbProvider.cs index dd63e230..ea6382dd 100644 --- a/ksqlDb.RestApi.Client/KSql/RestApi/KSqlDbProvider.cs +++ b/ksqlDb.RestApi.Client/KSql/RestApi/KSqlDbProvider.cs @@ -2,10 +2,13 @@ using System.Runtime.CompilerServices; using System.Text; using System.Text.Json; +using System.Text.Json.Serialization.Metadata; +using ksqlDb.RestApi.Client.FluentAPI.Builders; using ksqlDb.RestApi.Client.KSql.Query.Context.Options; using ksqlDB.RestApi.Client.KSql.RestApi.Query; using Microsoft.Extensions.Logging; using IHttpClientFactory = ksqlDB.RestApi.Client.KSql.RestApi.Http.IHttpClientFactory; +using JsonTypeInfoResolver = ksqlDb.RestApi.Client.KSql.RestApi.Json.JsonTypeInfoResolver; namespace ksqlDB.RestApi.Client.KSql.RestApi; @@ -13,12 +16,14 @@ namespace ksqlDB.RestApi.Client.KSql.RestApi; internal abstract class KSqlDbProvider : IKSqlDbProvider { private readonly IHttpClientFactory httpClientFactory; + private readonly IMetadataProvider metadataProvider; private readonly KSqlDbProviderOptions options; private readonly ILogger logger; - protected KSqlDbProvider(IHttpClientFactory httpClientFactory, KSqlDbProviderOptions options, ILogger logger = null) + protected KSqlDbProvider(IHttpClientFactory httpClientFactory, IMetadataProvider metadataProvider, KSqlDbProviderOptions options, ILogger logger = null) { this.httpClientFactory = httpClientFactory ?? throw new ArgumentNullException(nameof(httpClientFactory)); + this.metadataProvider = metadataProvider ?? throw new ArgumentNullException(nameof(metadataProvider)); this.options = options ?? throw new ArgumentNullException(nameof(options)); this.logger = logger; } @@ -155,7 +160,7 @@ private async IAsyncEnumerable ConsumeAsync(StreamReader streamReader, Sem { if (cancellationToken.IsCancellationRequested) yield break; - + var rawData = await streamReader #if NET7_0_OR_GREATER .ReadLineAsync(cancellationToken) @@ -196,7 +201,47 @@ protected JsonSerializerOptions GetOrCreateJsonSerializerOptions() protected virtual JsonSerializerOptions OnCreateJsonSerializerOptions() { - return options.JsonSerializerOptions; + var jsonSerializerOptions = options.JsonSerializerOptions; + + if (jsonSerializerOptions.TypeInfoResolver == null) + { + var defaultJsonTypeInfoResolver = new DefaultJsonTypeInfoResolver(); + var resolver = new JsonTypeInfoResolver(defaultJsonTypeInfoResolver) + { + Modifiers = { JsonPropertyNameModifier } + }; + jsonSerializerOptions.TypeInfoResolver = resolver; + } + else if (jsonSerializerOptions.TypeInfoResolver is not JsonTypeInfoResolver) + { + var resolver = new JsonTypeInfoResolver(jsonSerializerOptions.TypeInfoResolver) + { + Modifiers = { JsonPropertyNameModifier } + }; + + jsonSerializerOptions.TypeInfoResolver = resolver; + } + + return jsonSerializerOptions; + } + + internal void JsonPropertyNameModifier(JsonTypeInfo jsonTypeInfo) + { + JsonPropertyNameModifier(jsonTypeInfo, metadataProvider); + } + + internal static void JsonPropertyNameModifier(JsonTypeInfo jsonTypeInfo, IMetadataProvider metadataProvider) + { + var entityMetadata = metadataProvider.GetEntities().FirstOrDefault(c => c.Type == jsonTypeInfo.Type); + + foreach (var typeInfoProperty in jsonTypeInfo.Properties) + { + var fieldMetadata = + entityMetadata?.FieldsMetadata?.FirstOrDefault(c => c.MemberInfo.Name == typeInfoProperty.Name); + + if (fieldMetadata != null && !string.IsNullOrEmpty(fieldMetadata.ColumnName)) + typeInfoProperty.Name = fieldMetadata.ColumnName; + } } protected virtual HttpRequestMessage CreateQueryHttpRequestMessage(HttpClient httpClient, object parameters) diff --git a/ksqlDb.RestApi.Client/KSql/RestApi/KSqlDbQueryStreamProvider.cs b/ksqlDb.RestApi.Client/KSql/RestApi/KSqlDbQueryStreamProvider.cs index 6f0e994d..ed4794d3 100644 --- a/ksqlDb.RestApi.Client/KSql/RestApi/KSqlDbQueryStreamProvider.cs +++ b/ksqlDb.RestApi.Client/KSql/RestApi/KSqlDbQueryStreamProvider.cs @@ -1,26 +1,21 @@ #if !NETSTANDARD using System.Net; using System.Text.Json; -using System.Text.Json.Serialization.Metadata; using ksqlDb.RestApi.Client.FluentAPI.Builders; using ksqlDb.RestApi.Client.KSql.Query.Context.Options; using ksqlDB.RestApi.Client.KSql.RestApi.Exceptions; using ksqlDB.RestApi.Client.KSql.RestApi.Responses; using Microsoft.Extensions.Logging; using IHttpClientFactory = ksqlDB.RestApi.Client.KSql.RestApi.Http.IHttpClientFactory; -using JsonTypeInfoResolver = ksqlDb.RestApi.Client.KSql.RestApi.Json.JsonTypeInfoResolver; #nullable disable namespace ksqlDB.RestApi.Client.KSql.RestApi { internal class KSqlDbQueryStreamProvider : KSqlDbProvider { - private readonly IMetadataProvider metadataProvider; - public KSqlDbQueryStreamProvider(IHttpClientFactory httpClientFactory, IMetadataProvider metadataProvider, KSqlDbProviderOptions options, ILogger logger = null) - : base(httpClientFactory, options, logger) + : base(httpClientFactory, metadataProvider, options, logger) { - this.metadataProvider = metadataProvider ?? throw new ArgumentNullException(nameof(metadataProvider)); #if NETCOREAPP3_1 AppContext.SetSwitch("System.Net.Http.SocketsHttpHandler.Http2UnencryptedSupport", true); #endif @@ -54,51 +49,6 @@ protected override RowValue OnLineRead(string rawJson) return default; } - protected override JsonSerializerOptions OnCreateJsonSerializerOptions() - { - var jsonSerializerOptions = base.OnCreateJsonSerializerOptions(); - - if (jsonSerializerOptions.TypeInfoResolver == null) - { - var defaultJsonTypeInfoResolver = new DefaultJsonTypeInfoResolver(); - var resolver = new JsonTypeInfoResolver(defaultJsonTypeInfoResolver) - { - Modifiers = { JsonPropertyNameModifier } - }; - jsonSerializerOptions.TypeInfoResolver = resolver; - } - else if(jsonSerializerOptions.TypeInfoResolver is not JsonTypeInfoResolver) - { - var resolver = new JsonTypeInfoResolver(jsonSerializerOptions.TypeInfoResolver) - { - Modifiers = { JsonPropertyNameModifier } - }; - - jsonSerializerOptions.TypeInfoResolver = resolver; - } - - return jsonSerializerOptions; - } - - internal void JsonPropertyNameModifier(JsonTypeInfo jsonTypeInfo) - { - JsonPropertyNameModifier(jsonTypeInfo, metadataProvider); - } - - internal static void JsonPropertyNameModifier(JsonTypeInfo jsonTypeInfo, IMetadataProvider metadataProvider) - { - var entityMetadata = metadataProvider.GetEntities().FirstOrDefault(c => c.Type == jsonTypeInfo.Type); - - foreach (var typeInfoProperty in jsonTypeInfo.Properties) - { - var fieldMetadata = - entityMetadata?.FieldsMetadata?.FirstOrDefault(c => c.MemberInfo.Name == typeInfoProperty.Name); - - if (fieldMetadata != null && !string.IsNullOrEmpty(fieldMetadata.ColumnName)) - typeInfoProperty.Name = fieldMetadata.ColumnName; - } - } - private static void OnError(string rawJson) { var errorResponse = JsonSerializer.Deserialize(rawJson); diff --git a/ksqlDb.RestApi.Client/KSql/RestApi/Query/KSqlDbQueryProvider.cs b/ksqlDb.RestApi.Client/KSql/RestApi/Query/KSqlDbQueryProvider.cs index 148a1027..4ed1eb15 100644 --- a/ksqlDb.RestApi.Client/KSql/RestApi/Query/KSqlDbQueryProvider.cs +++ b/ksqlDb.RestApi.Client/KSql/RestApi/Query/KSqlDbQueryProvider.cs @@ -5,14 +5,15 @@ using ksqlDB.RestApi.Client.KSql.RestApi.Parsers; using ksqlDB.RestApi.Client.KSql.RestApi.Responses.Query; using Microsoft.Extensions.Logging; +using ksqlDb.RestApi.Client.FluentAPI.Builders; namespace ksqlDB.RestApi.Client.KSql.RestApi.Query; #nullable disable internal class KSqlDbQueryProvider : KSqlDbProvider { - public KSqlDbQueryProvider(IHttpV1ClientFactory httpClientFactory, KSqlDbProviderOptions options, ILogger logger = null) - : base(httpClientFactory, options, logger) + public KSqlDbQueryProvider(IHttpV1ClientFactory httpClientFactory, IMetadataProvider metadataProvider, KSqlDbProviderOptions options, ILogger logger = null) + : base(httpClientFactory, metadataProvider, options, logger) { } diff --git a/ksqlDb.RestApi.Client/ksqlDb.RestApi.Client.csproj b/ksqlDb.RestApi.Client/ksqlDb.RestApi.Client.csproj index acad7df9..23575b61 100644 --- a/ksqlDb.RestApi.Client/ksqlDb.RestApi.Client.csproj +++ b/ksqlDb.RestApi.Client/ksqlDb.RestApi.Client.csproj @@ -15,7 +15,7 @@ Documentation for the library can be found at https://github.com/tomasfabian/ksqlDB.RestApi.Client-DotNet/blob/main/README.md. ksql ksqlDB LINQ .NET csharp push query - 6.1.0-rc.1 + 6.1.0-rc.2 6.1.0.0 12.0 enable From 2ae4ea6ed97f855bb812254dc876daddef40e61e Mon Sep 17 00:00:00 2001 From: Tomas Fabian Date: Sun, 2 Jun 2024 11:44:07 +0200 Subject: [PATCH 11/17] [ksqlDB.RestApi.Client]: added JsonSerializerOptionsExtensions --- .../Json/JsonSerializerOptionsExtensions.cs | 30 +++++++++++++++++++ .../KSql/RestApi/Json/JsonTypeInfoResolver.cs | 6 ++-- .../KSql/RestApi/KSqlDbProvider.cs | 25 ++-------------- 3 files changed, 36 insertions(+), 25 deletions(-) create mode 100644 ksqlDb.RestApi.Client/KSql/RestApi/Json/JsonSerializerOptionsExtensions.cs diff --git a/ksqlDb.RestApi.Client/KSql/RestApi/Json/JsonSerializerOptionsExtensions.cs b/ksqlDb.RestApi.Client/KSql/RestApi/Json/JsonSerializerOptionsExtensions.cs new file mode 100644 index 00000000..9307d96b --- /dev/null +++ b/ksqlDb.RestApi.Client/KSql/RestApi/Json/JsonSerializerOptionsExtensions.cs @@ -0,0 +1,30 @@ +using System.Text.Json; +using System.Text.Json.Serialization.Metadata; + +namespace ksqlDb.RestApi.Client.KSql.RestApi.Json +{ + internal static class JsonSerializerOptionsExtensions + { + public static void WithModifier(this JsonSerializerOptions jsonSerializerOptions, Action modifier) + { + if (jsonSerializerOptions.TypeInfoResolver == null) + { + var defaultJsonTypeInfoResolver = new DefaultJsonTypeInfoResolver(); + var resolver = new JsonTypeInfoResolver(defaultJsonTypeInfoResolver) + { + Modifiers = { modifier } + }; + jsonSerializerOptions.TypeInfoResolver = resolver; + } + else if (jsonSerializerOptions.TypeInfoResolver is not JsonTypeInfoResolver) + { + var resolver = new JsonTypeInfoResolver(jsonSerializerOptions.TypeInfoResolver) + { + Modifiers = { modifier } + }; + + jsonSerializerOptions.TypeInfoResolver = resolver; + } + } + } +} diff --git a/ksqlDb.RestApi.Client/KSql/RestApi/Json/JsonTypeInfoResolver.cs b/ksqlDb.RestApi.Client/KSql/RestApi/Json/JsonTypeInfoResolver.cs index 9942b45f..ad76cfdd 100644 --- a/ksqlDb.RestApi.Client/KSql/RestApi/Json/JsonTypeInfoResolver.cs +++ b/ksqlDb.RestApi.Client/KSql/RestApi/Json/JsonTypeInfoResolver.cs @@ -5,14 +5,14 @@ namespace ksqlDb.RestApi.Client.KSql.RestApi.Json { internal class JsonTypeInfoResolver(IJsonTypeInfoResolver typeInfoResolver) : IJsonTypeInfoResolver { - private readonly IJsonTypeInfoResolver typeInfoResolver = typeInfoResolver ?? throw new ArgumentNullException(nameof(typeInfoResolver)); + internal IJsonTypeInfoResolver TypeInfoResolver => typeInfoResolver ?? throw new ArgumentNullException(nameof(typeInfoResolver)); - public IList> Modifiers => modifiers ??= new List>(); private IList>? modifiers; + internal IList> Modifiers => modifiers ??= new List>(); public virtual JsonTypeInfo? GetTypeInfo(Type type, JsonSerializerOptions options) { - var typeInfo = typeInfoResolver.GetTypeInfo(type, options); + var typeInfo = TypeInfoResolver.GetTypeInfo(type, options); if (typeInfo == null) return null; diff --git a/ksqlDb.RestApi.Client/KSql/RestApi/KSqlDbProvider.cs b/ksqlDb.RestApi.Client/KSql/RestApi/KSqlDbProvider.cs index ea6382dd..1c7375b0 100644 --- a/ksqlDb.RestApi.Client/KSql/RestApi/KSqlDbProvider.cs +++ b/ksqlDb.RestApi.Client/KSql/RestApi/KSqlDbProvider.cs @@ -5,10 +5,10 @@ using System.Text.Json.Serialization.Metadata; using ksqlDb.RestApi.Client.FluentAPI.Builders; using ksqlDb.RestApi.Client.KSql.Query.Context.Options; +using ksqlDb.RestApi.Client.KSql.RestApi.Json; using ksqlDB.RestApi.Client.KSql.RestApi.Query; using Microsoft.Extensions.Logging; using IHttpClientFactory = ksqlDB.RestApi.Client.KSql.RestApi.Http.IHttpClientFactory; -using JsonTypeInfoResolver = ksqlDb.RestApi.Client.KSql.RestApi.Json.JsonTypeInfoResolver; namespace ksqlDB.RestApi.Client.KSql.RestApi; @@ -201,28 +201,9 @@ protected JsonSerializerOptions GetOrCreateJsonSerializerOptions() protected virtual JsonSerializerOptions OnCreateJsonSerializerOptions() { - var jsonSerializerOptions = options.JsonSerializerOptions; + options.JsonSerializerOptions.WithModifier(JsonPropertyNameModifier); - if (jsonSerializerOptions.TypeInfoResolver == null) - { - var defaultJsonTypeInfoResolver = new DefaultJsonTypeInfoResolver(); - var resolver = new JsonTypeInfoResolver(defaultJsonTypeInfoResolver) - { - Modifiers = { JsonPropertyNameModifier } - }; - jsonSerializerOptions.TypeInfoResolver = resolver; - } - else if (jsonSerializerOptions.TypeInfoResolver is not JsonTypeInfoResolver) - { - var resolver = new JsonTypeInfoResolver(jsonSerializerOptions.TypeInfoResolver) - { - Modifiers = { JsonPropertyNameModifier } - }; - - jsonSerializerOptions.TypeInfoResolver = resolver; - } - - return jsonSerializerOptions; + return options.JsonSerializerOptions; } internal void JsonPropertyNameModifier(JsonTypeInfo jsonTypeInfo) From 2f07eb86c16a46b5a51cbef79bccbb0f6bf40d33 Mon Sep 17 00:00:00 2001 From: Tomas Fabian Date: Sun, 2 Jun 2024 11:44:38 +0200 Subject: [PATCH 12/17] [ksqlDB.RestApi.Client]: added JsonSerializerOptionsExtensions unit tests --- .../JsonSerializerOptionsExtensionsTests.cs | 57 +++++++++++++++++++ 1 file changed, 57 insertions(+) create mode 100644 Tests/ksqlDB.RestApi.Client.Tests/KSql/RestApi/Json/JsonSerializerOptionsExtensionsTests.cs diff --git a/Tests/ksqlDB.RestApi.Client.Tests/KSql/RestApi/Json/JsonSerializerOptionsExtensionsTests.cs b/Tests/ksqlDB.RestApi.Client.Tests/KSql/RestApi/Json/JsonSerializerOptionsExtensionsTests.cs new file mode 100644 index 00000000..81abb148 --- /dev/null +++ b/Tests/ksqlDB.RestApi.Client.Tests/KSql/RestApi/Json/JsonSerializerOptionsExtensionsTests.cs @@ -0,0 +1,57 @@ +using System.Text.Json; +using System.Text.Json.Serialization.Metadata; +using FluentAssertions; +using ksqlDb.RestApi.Client.KSql.RestApi.Json; +using NUnit.Framework; + +namespace ksqlDb.RestApi.Client.Tests.KSql.RestApi.Json +{ + public class JsonSerializerOptionsExtensionsTests + { + [Test] + public void WithModifier() + { + //Arrange + var jsonSerializerOptions = new JsonSerializerOptions(); + + void Modifier(JsonTypeInfo typeInfo) + { + } + + //Act + jsonSerializerOptions.WithModifier(Modifier); + + //Assert + jsonSerializerOptions.TypeInfoResolver.Should().NotBeNull(); + jsonSerializerOptions.TypeInfoResolver.Should().BeOfType(); + var jsonTypeInfoResolver = jsonSerializerOptions.TypeInfoResolver as Client.KSql.RestApi.Json.JsonTypeInfoResolver; + jsonTypeInfoResolver!.Modifiers.Should().Contain(Modifier); + jsonTypeInfoResolver!.TypeInfoResolver.Should().BeOfType(); + } + + [Test] + public void WithModifier_TypeInfoResolverContainsValue_WasDecoratedOnce() + { + //Arrange + var jsonSerializerOptions = new JsonSerializerOptions() + { + TypeInfoResolver = new DefaultJsonTypeInfoResolver() + }; + + void Modifier(JsonTypeInfo typeInfo) + { + } + + //Act + jsonSerializerOptions.WithModifier(Modifier); + jsonSerializerOptions.WithModifier(Modifier); + + //Assert + jsonSerializerOptions.TypeInfoResolver.Should().NotBeNull(); + jsonSerializerOptions.TypeInfoResolver.Should().BeOfType(); + var jsonTypeInfoResolver = jsonSerializerOptions.TypeInfoResolver as Client.KSql.RestApi.Json.JsonTypeInfoResolver; + jsonTypeInfoResolver!.Modifiers.Should().Contain(Modifier); + jsonTypeInfoResolver!.TypeInfoResolver.Should().BeEquivalentTo(jsonTypeInfoResolver.TypeInfoResolver); + } + } +} From 6e9a86ce6544e97a0633dd33b9d1c15d5be340d4 Mon Sep 17 00:00:00 2001 From: Tomas Fabian Date: Sun, 2 Jun 2024 11:45:07 +0200 Subject: [PATCH 13/17] [ksqlDB.RestApi.Client]: added KSqlDbProvider.JsonPropertyNameModifier unit tests --- .../KSql/RestApi/KSqlDbProviderTests.cs | 73 +++++++++++++++++-- 1 file changed, 68 insertions(+), 5 deletions(-) diff --git a/Tests/ksqlDB.RestApi.Client.Tests/KSql/RestApi/KSqlDbProviderTests.cs b/Tests/ksqlDB.RestApi.Client.Tests/KSql/RestApi/KSqlDbProviderTests.cs index c2edb3ad..9c2c15d4 100644 --- a/Tests/ksqlDB.RestApi.Client.Tests/KSql/RestApi/KSqlDbProviderTests.cs +++ b/Tests/ksqlDB.RestApi.Client.Tests/KSql/RestApi/KSqlDbProviderTests.cs @@ -1,4 +1,8 @@ +using System.Text.Json; +using System.Text.Json.Serialization.Metadata; using FluentAssertions; +using ksqlDb.RestApi.Client.FluentAPI.Builders; +using ksqlDB.RestApi.Client.KSql.RestApi; using ksqlDB.RestApi.Client.KSql.RestApi.Exceptions; using ksqlDB.RestApi.Client.KSql.RestApi.Parameters; using ksqlDb.RestApi.Client.Tests.Fakes.Logging; @@ -34,7 +38,7 @@ public async Task Run_LogInformation() var queryParameters = new QueryStreamParameters(); //Act - var tweets = await ClassUnderTest.Run(queryParameters).ToListAsync(); + await ClassUnderTest.Run(queryParameters).ToListAsync(); //Assert LoggerMock.VerifyLog(LogLevel.Information, Times.Once); @@ -177,12 +181,11 @@ public async Task LogError() try { //Act - var tweets = await ClassUnderTest.Run(queryParameters).ToListAsync(); - - //Assert + await ClassUnderTest.Run(queryParameters).ToListAsync(); } catch (Exception) { + //Assert LoggerMock.VerifyLog(LogLevel.Error, Times.Once); } } @@ -196,7 +199,7 @@ public async Task Run_Disposed_NothingWasReceived() //Act IAsyncEnumerable tweets = ClassUnderTest.Run(queryParameters, cts.Token); - cts.Cancel(); + await cts.CancelAsync(); //Assert var receivedTweets = new List(); @@ -237,4 +240,64 @@ public async Task Run_DonNotDisposeHttpClient() //Assert ClassUnderTest.LastUsedHttpClient.IsDisposed.Should().BeTrue(); } + + private class DomainObject + { + public int Id { get; set; } + } + + #region JsonPropertyNameModifier + + [Test] + public void JsonPropertyNameModifier() + { + //Arrange + var modelBuilder = new ModelBuilder(); + var jsonTypeInfo = new DefaultJsonTypeInfoResolver().GetTypeInfo(typeof(DomainObject), new JsonSerializerOptions()); + + //Act + KSqlDbProvider.JsonPropertyNameModifier(jsonTypeInfo, modelBuilder); + + //Assert + jsonTypeInfo.Properties[0].Name.Should().Be(nameof(DomainObject.Id)); + } + + [Test] + public void JsonPropertyNameModifier_ModelBuilder_HasColumnNameOverride() + { + //Arrange + var idColumnName = "id"; + var modelBuilder = new ModelBuilder(); + modelBuilder.Entity() + .Property(c => c.Id) + .HasColumnName(idColumnName); + + var jsonTypeInfo = new DefaultJsonTypeInfoResolver().GetTypeInfo(typeof(DomainObject), new JsonSerializerOptions()); + + //Act + KSqlDbProvider.JsonPropertyNameModifier(jsonTypeInfo, modelBuilder); + + //Assert + jsonTypeInfo.Properties[0].Name.Should().Be(idColumnName); + } + + [Test] + public void JsonPropertyNameModifier_ModelBuilder_WithoutHasColumnNameOverride() + { + //Arrange + var modelBuilder = new ModelBuilder(); + modelBuilder.Entity() + .Property(c => c.Id) + .WithHeaders(); + + var jsonTypeInfo = new DefaultJsonTypeInfoResolver().GetTypeInfo(typeof(DomainObject), new JsonSerializerOptions()); + + //Act + KSqlDbProvider.JsonPropertyNameModifier(jsonTypeInfo, modelBuilder); + + //Assert + jsonTypeInfo.Properties[0].Name.Should().Be(nameof(DomainObject.Id)); + } + + #endregion } From 0742222bfcab7051dfb249fee45762336580ae5f Mon Sep 17 00:00:00 2001 From: Tomas Fabian Date: Mon, 3 Jun 2024 07:41:22 +0200 Subject: [PATCH 14/17] [ksqlDB.RestApi.Client]: added TypeExtensionsTests.GetMemberNameunit tests --- .../Extensions/TypeExtensionsTests.cs | 64 +++++++++++++++++++ .../KSql/Query/KSqlQueryGeneratorTests.cs | 21 ++---- 2 files changed, 71 insertions(+), 14 deletions(-) diff --git a/Tests/ksqlDB.RestApi.Client.Tests/Infrastructure/Extensions/TypeExtensionsTests.cs b/Tests/ksqlDB.RestApi.Client.Tests/Infrastructure/Extensions/TypeExtensionsTests.cs index 6962123a..647c234e 100644 --- a/Tests/ksqlDB.RestApi.Client.Tests/Infrastructure/Extensions/TypeExtensionsTests.cs +++ b/Tests/ksqlDB.RestApi.Client.Tests/Infrastructure/Extensions/TypeExtensionsTests.cs @@ -1,4 +1,6 @@ using System.Collections; +using System.Linq.Expressions; +using System.Reflection; using System.Text; using System.Text.Json.Serialization; using FluentAssertions; @@ -358,6 +360,8 @@ public void TryGetAttribute() attribute.Should().BeOfType(); } + #region GetMemberName + private record MySensor { [JsonPropertyName("SensorId")] @@ -416,4 +420,64 @@ public void GetMemberName_ModelBuilderHasColumnName() //Assert memberName.Should().Be(columnName); } + + [Test] + public void GetMemberName_MemberExpression_ModelBuilderHasColumnName() + { + //Arrange + var columnName = "Id"; + Expression> expression = c => c.SensorId2; + + var modelBuilder = new ModelBuilder(); + modelBuilder.Entity() + .Property(expression) + .HasColumnName(columnName); + + //Act + var memberName = ((MemberExpression)expression.Body).GetMemberName(modelBuilder); + + //Assert + memberName.Should().Be(columnName); + } + + [Test] + public void GetMemberName_FromMemberExpression_JsonPropertyNameAttributeWasUsed() + { + //Arrange + Expression> expression = c => c.SensorId2; + + var modelBuilder = new ModelBuilder(); + modelBuilder.Entity() + .Property(expression) + .WithHeaders(); + var memberExpression = (MemberExpression) expression.Body; + + //Act + var memberName = memberExpression.GetMemberName(modelBuilder); + + //Assert + var jsonPropertyNameAttribute = memberExpression.Member.GetCustomAttribute(); + memberName.Should().Be(jsonPropertyNameAttribute?.Name); + } + + [Test] + public void GetMemberName_FromMemberExpression_PropertyNameWasUsed() + { + //Arrange + Expression> expression = c => c.Title; + + var modelBuilder = new ModelBuilder(); + modelBuilder.Entity() + .Property(expression) + .WithHeaders(); + var memberExpression = (MemberExpression)expression.Body; + + //Act + var memberName = memberExpression.GetMemberName(modelBuilder); + + //Assert + memberName.Should().Be(nameof(MySensor.Title)); + } + + #endregion } diff --git a/Tests/ksqlDB.RestApi.Client.Tests/KSql/Query/KSqlQueryGeneratorTests.cs b/Tests/ksqlDB.RestApi.Client.Tests/KSql/Query/KSqlQueryGeneratorTests.cs index 3f68f12d..eb307451 100644 --- a/Tests/ksqlDB.RestApi.Client.Tests/KSql/Query/KSqlQueryGeneratorTests.cs +++ b/Tests/ksqlDB.RestApi.Client.Tests/KSql/Query/KSqlQueryGeneratorTests.cs @@ -27,6 +27,7 @@ public class KSqlQueryGeneratorTests : TestBase readonly string streamName = nameof(Location) + "s"; private KSqlDBContextOptions contextOptions = null!; + private ModelBuilder modelBuilder = null!; private QueryContext queryContext = null!; [SetUp] @@ -35,7 +36,11 @@ public override void TestInitialize() base.TestInitialize(); contextOptions = new KSqlDBContextOptions(TestParameters.KsqlDbUrl); - queryContext = new QueryContext(); + modelBuilder = new ModelBuilder(); + queryContext = new QueryContext() + { + ModelBuilder = modelBuilder + }; ClassUnderTest = new KSqlQueryGenerator(contextOptions); } @@ -93,7 +98,6 @@ public void BuildKSql_ModelBuilder_HasColumnNameOverride() { //Arrange string idColumnName = "Id"; - ModelBuilder modelBuilder = new ModelBuilder(); modelBuilder.Entity() .Property(c => c.SensorId2) .HasColumnName(idColumnName); @@ -103,11 +107,6 @@ public void BuildKSql_ModelBuilder_HasColumnNameOverride() .Where(c => c.SensorId2 == "1") .Select(c => c.SensorId2); - queryContext = new QueryContext() - { - ModelBuilder = modelBuilder - }; - //Act var ksql = ClassUnderTest.BuildKSql(query.Expression, queryContext); @@ -133,7 +132,6 @@ public void BuildKSql_ModelBuilder_HasColumnNameOverride_ForPropertyInBaseClass( { //Arrange string idColumnName = "SensorId"; - ModelBuilder modelBuilder = new ModelBuilder(); modelBuilder.Entity() .Property(c => c.Id) .HasColumnName(idColumnName); @@ -142,12 +140,7 @@ public void BuildKSql_ModelBuilder_HasColumnNameOverride_ForPropertyInBaseClass( .CreatePushQuery() .Where(c => c.Id == 1) .Select(c => c.Id); - - queryContext = new QueryContext() - { - ModelBuilder = modelBuilder - }; - + //Act var ksql = ClassUnderTest.BuildKSql(query.Expression, queryContext); From 26e29b249c2cbdd789b4c2f89aa302359b6b4dab Mon Sep 17 00:00:00 2001 From: Tomas Fabian Date: Tue, 4 Jun 2024 07:48:32 +0200 Subject: [PATCH 15/17] [ksqlDB.RestApi.Client]: added ModelBuilder integration tests --- .../Infrastructure/IntegrationTests.cs | 6 +- .../KSql/Linq/ModelBuilderTests.cs | 140 ++++++++++++++++++ 2 files changed, 144 insertions(+), 2 deletions(-) create mode 100644 Tests/ksqlDB.RestApi.Client.IntegrationTests/KSql/Linq/ModelBuilderTests.cs diff --git a/Tests/ksqlDB.RestApi.Client.IntegrationTests/Infrastructure/IntegrationTests.cs b/Tests/ksqlDB.RestApi.Client.IntegrationTests/Infrastructure/IntegrationTests.cs index 37fbbcac..cab51245 100644 --- a/Tests/ksqlDB.RestApi.Client.IntegrationTests/Infrastructure/IntegrationTests.cs +++ b/Tests/ksqlDB.RestApi.Client.IntegrationTests/Infrastructure/IntegrationTests.cs @@ -1,3 +1,4 @@ +using ksqlDb.RestApi.Client.FluentAPI.Builders; using ksqlDb.RestApi.Client.IntegrationTests.KSql.RestApi; using ksqlDB.RestApi.Client.KSql.Query.Context; using ksqlDB.RestApi.Client.KSql.Query.Options; @@ -32,7 +33,7 @@ public override void TestInitialize() Context = CreateKSqlDbContext(EndpointType.QueryStream); } - protected KSqlDBContext CreateKSqlDbContext(EndpointType endpointType) + protected KSqlDBContext CreateKSqlDbContext(EndpointType endpointType, ModelBuilder? modelBuilder = null) { ContextOptions = new KSqlDBContextOptions(KSqlDbRestApiProvider.KsqlDbUrl) { @@ -40,7 +41,8 @@ protected KSqlDBContext CreateKSqlDbContext(EndpointType endpointType) EndpointType = endpointType }; - return new KSqlDBContext(ContextOptions); + modelBuilder ??= new ModelBuilder(); + return new KSqlDBContext(ContextOptions, modelBuilder); } [TestCleanup] diff --git a/Tests/ksqlDB.RestApi.Client.IntegrationTests/KSql/Linq/ModelBuilderTests.cs b/Tests/ksqlDB.RestApi.Client.IntegrationTests/KSql/Linq/ModelBuilderTests.cs new file mode 100644 index 00000000..f3fb4fdb --- /dev/null +++ b/Tests/ksqlDB.RestApi.Client.IntegrationTests/KSql/Linq/ModelBuilderTests.cs @@ -0,0 +1,140 @@ +using System.Text.Json.Serialization; +using FluentAssertions; +using ksqlDb.RestApi.Client.FluentAPI.Builders; +using ksqlDb.RestApi.Client.IntegrationTests.Helpers; +using ksqlDb.RestApi.Client.IntegrationTests.Models; +using ksqlDB.RestApi.Client.KSql.Linq; +using ksqlDB.RestApi.Client.KSql.RestApi; +using ksqlDB.RestApi.Client.KSql.RestApi.Enums; +using ksqlDB.RestApi.Client.KSql.RestApi.Http; +using ksqlDB.RestApi.Client.KSql.RestApi.Statements; +using ksqlDB.RestApi.Client.KSql.RestApi.Statements.Properties; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using NUnit.Framework; + +namespace ksqlDb.RestApi.Client.IntegrationTests.KSql.Linq +{ + public class ModelBuilderTests : Infrastructure.IntegrationTests + { + protected static string StreamName = nameof(ModelBuilderTests); + private static readonly string TopicName = StreamName; + private static KSqlDbRestApiClient kSqlDbRestApiClient = null!; + private static ModelBuilder modelBuilder = null!; + + [OneTimeSetUp] + public static async Task ClassInitialize() + { + await InitializeDatabase(); + } + + public record Tweet : Record + { + public int Id { get; set; } + [JsonPropertyName("MESSAGE")] + public string Message { get; set; } = null!; + public bool IsRobot { get; set; } + public double Amount { get; set; } + public decimal AccountBalance { get; set; } + } + + public static readonly Tweet Tweet1 = new() + { + Id = 1, + Message = "Hello world", + IsRobot = true, + Amount = 0.00042, + }; + + public static readonly Tweet Tweet2 = new() + { + Id = 2, + Message = "Wall-e", + IsRobot = false, + Amount = 1, + }; + + protected static async Task InitializeDatabase() + { + modelBuilder = new ModelBuilder(); + modelBuilder.Entity() + .Property(c => c.Id) + .HasColumnName("TweetId"); + modelBuilder.Entity() + .Property(c => c.AccountBalance) + .Ignore(); + + var httpClient = new HttpClient + { + BaseAddress = new Uri(TestConfig.KSqlDbUrl) + }; + kSqlDbRestApiClient = new KSqlDbRestApiClient(new HttpClientFactory(httpClient), modelBuilder); + + var entityCreationMetadata = new EntityCreationMetadata(TopicName, 1) + { + EntityName = StreamName, + ShouldPluralizeEntityName = false, + IdentifierEscaping = IdentifierEscaping.Always + }; + var result = await kSqlDbRestApiClient.CreateStreamAsync(entityCreationMetadata, true); + result.IsSuccess().Should().BeTrue(); + + var insertProperties = new InsertProperties() + { + EntityName = StreamName, + IdentifierEscaping = IdentifierEscaping.Always + }; + result = await kSqlDbRestApiClient.InsertIntoAsync(Tweet1, insertProperties); + result.IsSuccess().Should().BeTrue(); + + result = await kSqlDbRestApiClient.InsertIntoAsync(Tweet2, insertProperties); + result.IsSuccess().Should().BeTrue(); + } + + [OneTimeTearDown] + public static async Task ClassCleanup() + { + var dropFromItemProperties = new DropFromItemProperties + { + IdentifierEscaping = IdentifierEscaping.Always, + ShouldPluralizeEntityName = false, + EntityName = StreamName, + UseIfExistsClause = true, + DeleteTopic = true, + }; + await kSqlDbRestApiClient.DropStreamAsync(dropFromItemProperties); + } + + [SetUp] + public override void TestInitialize() + { + base.TestInitialize(); + + Context = CreateKSqlDbContext(ksqlDB.RestApi.Client.KSql.Query.Options.EndpointType.QueryStream, modelBuilder); + } + + protected virtual IQbservable QuerySource => + Context.CreatePushQuery($"`{StreamName}`"); + + [Test] + public async Task Select() + { + //Arrange + int expectedItemsCount = 2; + + var source = QuerySource + .ToAsyncEnumerable(); + + //Act + var actualValues = await CollectActualValues(source, expectedItemsCount); + + //Assert + var expectedValues = new List + { + Tweet1, Tweet2 + }; + + expectedItemsCount.Should().Be(actualValues.Count); + CollectionAssert.AreEqual(expectedValues, actualValues); + } + } +} From 41f9793c869e9cc2579a2bee2f7f1e039111b8f0 Mon Sep 17 00:00:00 2001 From: Tomas Fabian Date: Wed, 5 Jun 2024 17:19:38 +0200 Subject: [PATCH 16/17] [ksqlDB.RestApi.Client]: Added Fluent API HasColumnName function to README.md --- README.md | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 42fe1cc2..c51eeab2 100644 --- a/README.md +++ b/README.md @@ -369,10 +369,14 @@ modelBuilder.Entity() modelBuilder.Entity() .Property(b => b.Description) - .Ignore(); + .HasColumnName("Desc"); modelBuilder.Entity() .HasKey(c => c.Id); + +modelBuilder.Entity() + .Property(b => b.Secret) + .Ignore(); ``` C# entity definitions: @@ -389,6 +393,7 @@ record Account { public string Id { get; set; } = null!; public decimal Balance { get; set; } + public string Secret { get; set; } } ``` @@ -408,7 +413,7 @@ responseMessage = await restApiProvider.CreateTableAsync(entityCreation Generated KSQL: ```SQL -CREATE TYPE Payment AS STRUCT; +CREATE TYPE Payment AS STRUCT; CREATE TABLE IF NOT EXISTS Accounts ( Id VARCHAR PRIMARY KEY, @@ -416,7 +421,8 @@ CREATE TABLE IF NOT EXISTS Accounts ( ) WITH ( KAFKA_TOPIC='Account', VALUE_FORMAT='Json', PARTITIONS='3', REPLICAS='3' ); ``` -The `Description` field in the `Payment` type is ignored during code generation, and the `Id` field in the `Account` table is marked as the **primary key**. +The Description property within the `Payment` type has been customized to override the resulting column name as "Desc". +Additionally, the `Id` property within the `Account` table has been designated as the **primary key**, while the `Secret` property is disregarded during code generation. ### Aggregation functions List of supported ksqldb [aggregation functions](https://github.com/tomasfabian/ksqlDB.RestApi.Client-DotNet/blob/main/docs/aggregations.md): From da8304774ff6855d48cb38480d54b73487435161 Mon Sep 17 00:00:00 2001 From: Tomas Fabian Date: Fri, 7 Jun 2024 16:39:15 +0200 Subject: [PATCH 17/17] [ksqlDB.RestApi.Client]: release v6.1.0 --- ksqlDb.RestApi.Client/ChangeLog.md | 2 +- ksqlDb.RestApi.Client/ksqlDb.RestApi.Client.csproj | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/ksqlDb.RestApi.Client/ChangeLog.md b/ksqlDb.RestApi.Client/ChangeLog.md index 63766413..6302d3c2 100644 --- a/ksqlDb.RestApi.Client/ChangeLog.md +++ b/ksqlDb.RestApi.Client/ChangeLog.md @@ -1,6 +1,6 @@ # ksqlDB.RestApi.Client -# 6.1.0-rc.1 +# 6.1.0 - added `HasColumnName` to the Fluent API to allow overriding property names during JSON deserialization and code generation. ## BugFix diff --git a/ksqlDb.RestApi.Client/ksqlDb.RestApi.Client.csproj b/ksqlDb.RestApi.Client/ksqlDb.RestApi.Client.csproj index 23575b61..b9c744e7 100644 --- a/ksqlDb.RestApi.Client/ksqlDb.RestApi.Client.csproj +++ b/ksqlDb.RestApi.Client/ksqlDb.RestApi.Client.csproj @@ -15,7 +15,7 @@ Documentation for the library can be found at https://github.com/tomasfabian/ksqlDB.RestApi.Client-DotNet/blob/main/README.md. ksql ksqlDB LINQ .NET csharp push query - 6.1.0-rc.2 + 6.1.0 6.1.0.0 12.0 enable