Skip to content

Commit

Permalink
v3.26.0 (#123)
Browse files Browse the repository at this point in the history
- *Enhancement:* Enable JSON serialization of database parameter values; added `DatabaseParameterCollection.AddJsonParameter` method and associated `JsonParam`, `JsonParamWhen` and `JsonParamWith` extension methods.
- *Enhancement:* Updated (simplified) `EventOutboxEnqueueBase` to pass events to the underlying stored procedures as JSON versus existing TVP removing database dependency on a UDT (user-defined type).
  • Loading branch information
chullybun authored Oct 2, 2024
1 parent 3ab586c commit 61e2ded
Show file tree
Hide file tree
Showing 10 changed files with 202 additions and 43 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@

Represents the **NuGet** versions.

## v3.26.0
- *Enhancement:* Enable JSON serialization of database parameter values; added `DatabaseParameterCollection.AddJsonParameter` method and associated `JsonParam`, `JsonParamWhen` and `JsonParamWith` extension methods.
- *Enhancement:* Updated (simplified) `EventOutboxEnqueueBase` to pass events to the underlying stored procedures as JSON versus existing TVP removing database dependency on a UDT (user-defined type).

## v3.25.5
- *Fixed:* Fixed the unit testing `CreateServiceBusMessage` extension method so that it no longer invokes a `TesterBase.ResetHost` (this reset should now be invoked explicitly by the developer as required).

Expand Down
2 changes: 1 addition & 1 deletion Common.targets
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<Project>
<PropertyGroup>
<Version>3.25.5</Version>
<Version>3.25.6</Version>
<LangVersion>preview</LangVersion>
<Authors>Avanade</Authors>
<Company>Avanade</Company>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
CREATE OR ALTER PROCEDURE [Hr].[spGetEmployees]
@Ids AS NVARCHAR(MAX)
AS
BEGIN
SET NOCOUNT ON;

-- Select the requested data.
SELECT * FROM [Hr].[Employee] WHERE [EmployeeId] IN (SELECT VALUE FROM OPENJSON(@Ids))
END
1 change: 1 addition & 0 deletions samples/My.Hr/My.Hr.Database/My.Hr.Database.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@

<ItemGroup>
<None Remove="Migrations\20190101-000001-create-Hr-schema.sql" />
<None Remove="Migrations\20240930-132603-hr-spgetemployees.sql" />
</ItemGroup>

<ItemGroup>
Expand Down
48 changes: 48 additions & 0 deletions samples/My.Hr/My.Hr.UnitTest/DatabaseTest.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
using CoreEx.Database;
using Microsoft.Extensions.DependencyInjection;
using My.Hr.Api;
using My.Hr.Business.Data;
using My.Hr.Business.Models;
using NUnit.Framework;
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using UnitTestEx.NUnit;

namespace My.Hr.UnitTest
{
[TestFixture]
[Category("WithDB")]
public class DatabaseTest
{
[OneTimeSetUp]
public static Task Init() => EmployeeControllerTest.Init();

[Test]
public async Task DatabaseParameters_JsonParameter()
{
using var test = ApiTester.Create<Startup>();
using var scope = test.Services.CreateScope();

var hrdb = scope.ServiceProvider.GetRequiredService<IDatabase>();

var ids = new List<Guid>();
await hrdb.SqlStatement("SELECT * FROM [Hr].[Employee]").SelectAsync(dr =>
{
ids.Add(dr.GetValue<Guid>("EmployeeId"));
return true;
});

var ids2 = new List<Guid>();
var c = hrdb.StoredProcedure("[Hr].[spGetEmployees]").JsonParamWith("", "ids", () => ids);

await c.SelectAsync(dr =>
{
ids2.Add(dr.GetValue<Guid>("EmployeeId"));
return true;
});

Assert.That(ids, Is.EquivalentTo(ids2));
}
}
}
88 changes: 53 additions & 35 deletions src/CoreEx.Database.SqlServer/Outbox/EventOutboxEnqueueBase.cs
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
// Copyright (c) Avanade. Licensed under the MIT License. See https://github.com/Avanade/CoreEx

using CoreEx.Events;
using CoreEx.Json;
using Microsoft.Extensions.Logging;
using System;
using System.Collections.Generic;
using System.Data;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Text;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;

Expand Down Expand Up @@ -37,11 +38,6 @@ public abstract class EventOutboxEnqueueBase(IDatabase database, ILogger<EventOu
/// </summary>
protected ILogger Logger { get; } = logger.ThrowIfNull(nameof(logger));

/// <summary>
/// Gets the database type name for the <see cref="TableValuedParameter"/>.
/// </summary>
protected abstract string DbTvpTypeName { get; }

/// <summary>
/// Gets the event outbox <i>enqueue</i> stored procedure name.
/// </summary>
Expand Down Expand Up @@ -119,7 +115,7 @@ public async Task SendAsync(IEnumerable<EventSendData> events, CancellationToken

sw = Stopwatch.StartNew();
await Database.StoredProcedure(EnqueueStoredProcedure)
.TableValuedParam("@EventList", CreateTableValuedParameter(events, unsentEvents))
.Param("@EventList", CreateEventsJsonForDatabase(events, unsentEvents))
.NonQueryAsync(cancellationToken).ConfigureAwait(false);

sw.Stop();
Expand All @@ -130,40 +126,62 @@ await Database.StoredProcedure(EnqueueStoredProcedure)
}

/// <summary>
/// Creates the TVP from the list.
/// Creates the events JSON to send to the database.
/// </summary>
private TableValuedParameter CreateTableValuedParameter(IEnumerable<EventSendData> list, IEnumerable<EventSendData> unsentList)
private string CreateEventsJsonForDatabase(IEnumerable<EventSendData> list, IEnumerable<EventSendData> unsentList)
{
var dt = new DataTable();
dt.Columns.Add(EventIdColumnName, typeof(string));
dt.Columns.Add("EventDequeued", typeof(bool));
dt.Columns.Add(nameof(EventSendData.Destination), typeof(string));
dt.Columns.Add(nameof(EventSendData.Subject), typeof(string));
dt.Columns.Add(nameof(EventSendData.Action), typeof(string));
dt.Columns.Add(nameof(EventSendData.Type), typeof(string));
dt.Columns.Add(nameof(EventSendData.Source), typeof(string));
dt.Columns.Add(nameof(EventSendData.Timestamp), typeof(DateTimeOffset));
dt.Columns.Add(nameof(EventSendData.CorrelationId), typeof(string));
dt.Columns.Add(nameof(EventSendData.Key), typeof(string));
dt.Columns.Add(nameof(EventSendData.TenantId), typeof(string));
dt.Columns.Add(nameof(EventSendData.PartitionKey), typeof(string));
dt.Columns.Add(nameof(EventSendData.ETag), typeof(string));
dt.Columns.Add(nameof(EventSendData.Attributes), typeof(byte[]));
dt.Columns.Add(nameof(EventSendData.Data), typeof(byte[]));

var tvp = new TableValuedParameter(DbTvpTypeName, dt);
using var stream = new MemoryStream();
using var json = new Utf8JsonWriter(stream);

json.WriteStartArray();

foreach (var item in list)
{
var attributes = item.Attributes == null || item.Attributes.Count == 0 ? new BinaryData(Array.Empty<byte>()) : JsonSerializer.Default.SerializeToBinaryData(item.Attributes);
json.WriteStartObject();
if (item.Id is not null)
json.WriteString(EventIdColumnName, item.Id);

json.WriteBoolean("EventDequeued", !unsentList.Contains(item));
json.WriteString(nameof(EventSendData.Destination), item.Destination ?? DefaultDestination ?? throw new InvalidOperationException($"The {nameof(DefaultDestination)} must have a non-null value."));

if (item.Subject is not null)
json.WriteString(nameof(EventSendData.Subject), item.Subject);

if (item.Action is not null)
json.WriteString(nameof(EventSendData.Action), item.Action);

if (item.Type is not null)
json.WriteString(nameof(EventSendData.Type), item.Type);

tvp.AddRow(item.Id, !unsentList.Contains(item),
item.Destination ?? DefaultDestination ?? throw new InvalidOperationException($"The {nameof(DefaultDestination)} must have a non-null value."),
item.Subject, item.Action, item.Type, item.Source, item.Timestamp, item.CorrelationId, item.Key, item.TenantId,
item.PartitionKey ?? DefaultPartitionKey ?? throw new InvalidOperationException($"The {nameof(DefaultPartitionKey)} must have a non-null value."),
item.ETag, attributes.ToArray(), item.Data?.ToArray());
if (item.Source is not null)
json.WriteString(nameof(EventSendData.Source), item.Source?.ToString());

if (item.Timestamp is not null)
json.WriteString(nameof(EventSendData.Timestamp), (DateTimeOffset)item.Timestamp);

if (item.CorrelationId is not null)
json.WriteString(nameof(EventSendData.CorrelationId), item.CorrelationId);

if (item.Key is not null)
json.WriteString(nameof(EventSendData.Key), item.Key);

if (item.TenantId is not null)
json.WriteString(nameof(EventSendData.TenantId), item.TenantId);

json.WriteString(nameof(EventSendData.PartitionKey), item.PartitionKey ?? DefaultPartitionKey ?? throw new InvalidOperationException($"The {nameof(DefaultPartitionKey)} must have a non-null value."));

if (item.ETag is not null)
json.WriteString(nameof(EventSendData.ETag), item.ETag);

json.WriteBase64String(nameof(EventSendData.Attributes), item.Attributes == null || item.Attributes.Count == 0 ? new BinaryData([]) : Json.JsonSerializer.Default.SerializeToBinaryData(item.Attributes));
json.WriteBase64String(nameof(EventSendData.Data), item.Data ?? new BinaryData([]));
json.WriteEndObject();
}

return tvp;
json.WriteEndArray();
json.Flush();

return Encoding.UTF8.GetString(stream.ToArray());
}

/// <inheritdoc/>
Expand Down
4 changes: 4 additions & 0 deletions src/CoreEx.Database/Database.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

using CoreEx.Database.Extended;
using CoreEx.Entities;
using CoreEx.Json;
using CoreEx.Mapping.Converters;
using CoreEx.Results;
using Microsoft.Extensions.Logging;
Expand Down Expand Up @@ -66,6 +67,9 @@ public class Database<TConnection>(Func<TConnection> create, DbProviderFactory p
/// <inheritdoc/>
public virtual IConverter RowVersionConverter => throw new NotImplementedException();

/// <inheritdoc/>
public IJsonSerializer JsonSerializer { get; set; } = ExecutionContext.GetService<IJsonSerializer>() ?? CoreEx.Json.JsonSerializer.Default;

/// <inheritdoc/>
public DbConnection GetConnection() => _dbConn is not null ? _dbConn : Invokers.Invoker.RunSync(() => GetConnectionAsync());

Expand Down
20 changes: 20 additions & 0 deletions src/CoreEx.Database/DatabaseParameterCollection.cs
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,26 @@ public DbParameter AddParameter(string name, DbType dbType, int size, ParameterD
return p;
}

/// <summary>
/// Adds the named parameter and value serialized as a JSON <see cref="string"/> to the <see cref="DbCommand.Parameters"/>.
/// </summary>
/// <param name="name">The parameter name.</param>
/// <param name="value">The parameter value.</param>
/// <returns>A <see cref="DbParameter"/>.</returns>
/// <remarks>Where the <paramref name="value"/> is <see langword="null"/> then <see cref="DBNull.Value"/> will be used.</remarks>
public DbParameter AddJsonParameter(string name, object? value)
{
var p = Database.Provider.CreateParameter() ?? throw new InvalidOperationException($"The {nameof(DbProviderFactory)}.{nameof(DbProviderFactory.CreateParameter)} returned a null.");
p.ParameterName = ParameterizeName(name);
if (value is null)
p.Value = DBNull.Value;
else
p.Value = Database.JsonSerializer.Serialize(value);

_parameters.Add(p);
return p;
}

/// <summary>
/// Adds an <see cref="int"/> <see cref="ParameterDirection.ReturnValue"/> parameter.
/// </summary>
Expand Down
7 changes: 7 additions & 0 deletions src/CoreEx.Database/IDatabase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

using CoreEx.Database.Extended;
using CoreEx.Entities;
using CoreEx.Json;
using CoreEx.Mapping.Converters;
using CoreEx.Results;
using Microsoft.Extensions.Logging;
Expand Down Expand Up @@ -69,6 +70,12 @@ public interface IDatabase : IAsyncDisposable, IDisposable
/// </summary>
IConverter RowVersionConverter { get; }

/// <summary>
/// Gets the <see cref="IJsonSerializer"/> used to automatically serialize complex objects and <see cref="System.Collections.IEnumerable"/> parameters types to JSON.
/// </summary>
/// <remarks>See <see cref="DatabaseParameterCollection.AddParameter(string, object?, System.Data.ParameterDirection)"/>.</remarks>
IJsonSerializer JsonSerializer => CoreEx.ExecutionContext.GetService<IJsonSerializer>() ?? CoreEx.Json.JsonSerializer.Default;

/// <summary>
/// Gets the <see cref="DbConnection"/>.
/// </summary>
Expand Down
Loading

0 comments on commit 61e2ded

Please sign in to comment.