Skip to content

Commit

Permalink
Merge pull request #83 from tomasfabian/82-enhancing-fluent-api-imple…
Browse files Browse the repository at this point in the history
…menting-the-hascolumnname-method

82 enhancing fluent API implementing the HasColumnName method
  • Loading branch information
tomasfabian authored Jun 7, 2024
2 parents 910042c + da83047 commit 61d6f60
Show file tree
Hide file tree
Showing 50 changed files with 1,057 additions and 149 deletions.
12 changes: 9 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -369,10 +369,14 @@ modelBuilder.Entity<Payment>()

modelBuilder.Entity<Payment>()
.Property(b => b.Description)
.Ignore();
.HasColumnName("Desc");

modelBuilder.Entity<Account>()
.HasKey(c => c.Id);

modelBuilder.Entity<Account>()
.Property(b => b.Secret)
.Ignore();
```

C# entity definitions:
Expand All @@ -389,6 +393,7 @@ record Account
{
public string Id { get; set; } = null!;
public decimal Balance { get; set; }
public string Secret { get; set; }
}
```

Expand All @@ -408,15 +413,16 @@ responseMessage = await restApiProvider.CreateTableAsync<Account>(entityCreation
Generated KSQL:

```SQL
CREATE TYPE Payment AS STRUCT<Id VARCHAR, Amount DECIMAL(10,2)>;
CREATE TYPE Payment AS STRUCT<Id VARCHAR, Amount DECIMAL(10,2), Desc VARCHAR>;

CREATE TABLE IF NOT EXISTS Accounts (
Id VARCHAR PRIMARY KEY,
Balance DECIMAL(14,14)
) 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):
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,10 @@ public async Task InitModelAndCreateStreamAsync(CancellationToken cancellationTo
.Property(b => b.Amount)
.Decimal(precision: 10, scale: 2);

modelBuilder.Entity<Payment>()
.Property(b => b.Description)
.HasColumnName("desc");

string header = "abc";
modelBuilder.Entity<PocoWithHeader>()
.Property(c => c.Header)
Expand Down Expand Up @@ -59,7 +63,7 @@ private IKSqlDbRestApiClient ConfigureRestApiClientWithServicesCollection(Servic
{
c.UseKSqlDb(ksqlDbUrl);

c.ReplaceHttpClient<ksqlDB.RestApi.Client.KSql.RestApi.Http.IHttpClientFactory, ksqlDB.RestApi.Client.KSql.RestApi.Http.HttpClientFactory>(_ => { })
c.ReplaceHttpClient<KSql.RestApi.Http.IHttpClientFactory, KSql.RestApi.Http.HttpClientFactory>(_ => { })
.AddHttpMessageHandler(_ => new Program.DebugHandler());
})
.AddSingleton(builder);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@
</PropertyGroup>

<ItemGroup>
<PackageReference Include="ksqlDb.RestApi.Client" Version="6.0.0" />
<!-- <ProjectReference Include="..\..\ksqlDb.RestApi.Client\ksqlDb.RestApi.Client.csproj" /> -->
<!-- <PackageReference Include="ksqlDb.RestApi.Client" Version="6.1.0" /> -->
<ProjectReference Include="..\..\ksqlDb.RestApi.Client\ksqlDb.RestApi.Client.csproj" />
<!-- <PackageReference Include="ksqlDb.RestApi.Client.ProtoBuf" Version="4.0.0" /> -->
<ProjectReference Include="..\..\ksqlDb.RestApi.Client.ProtoBuf\ksqlDb.RestApi.Client.ProtoBuf.csproj" />

Expand Down
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -32,15 +33,16 @@ 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)
{
ShouldPluralizeFromItemName = false,
EndpointType = endpointType
};

return new KSqlDBContext(ContextOptions);
modelBuilder ??= new ModelBuilder();
return new KSqlDBContext(ContextOptions, modelBuilder);
}

[TestCleanup]
Expand Down
Original file line number Diff line number Diff line change
@@ -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<Tweet>()
.Property(c => c.Id)
.HasColumnName("TweetId");
modelBuilder.Entity<Tweet>()
.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<Tweet>(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<Models.Tweet>(dropFromItemProperties);
}

[SetUp]
public override void TestInitialize()
{
base.TestInitialize();

Context = CreateKSqlDbContext(ksqlDB.RestApi.Client.KSql.Query.Options.EndpointType.QueryStream, modelBuilder);
}

protected virtual IQbservable<Tweet> QuerySource =>
Context.CreatePushQuery<Tweet>($"`{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<Tweet>
{
Tweet1, Tweet2
};

expectedItemsCount.Should().Be(actualValues.Count);
CollectionAssert.AreEqual(expectedValues, actualValues);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -62,11 +62,29 @@ 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();
}

[Test]
public void Property_HasColumnName()
{
//Arrange
var columnName = "desc";

//Act
var fieldTypeBuilder = builder.Entity<Payment>()
.Property(b => b.Description)
.HasColumnName(columnName);

//Assert
fieldTypeBuilder.Should().NotBeNull();
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);
}

[Test]
public void MultiplePropertiesForSameType()
{
Expand All @@ -83,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();
Expand All @@ -105,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();
Expand All @@ -121,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<Payment>
Expand All @@ -143,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();
}
Expand All @@ -170,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));

Expand All @@ -191,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));

Expand All @@ -210,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));

Expand Down
Loading

0 comments on commit 61d6f60

Please sign in to comment.