Skip to content

Commit

Permalink
OctopusDeploy#113 Improve support for non-string ID columns
Browse files Browse the repository at this point in the history
  • Loading branch information
tunger committed Jul 15, 2020
1 parent 345985d commit e2657ed
Show file tree
Hide file tree
Showing 20 changed files with 147 additions and 70 deletions.
2 changes: 1 addition & 1 deletion source/Nevermore.Benchmarks/LoadManyBenchmark.cs
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ public override void SetUp()
[Benchmark]
public List<Customer> LoadMany()
{
var result = transaction.Load<Customer>(allIdsRandomlySorted.Take(NumberToLoad));
var result = transaction.LoadMany<Customer>(allIdsRandomlySorted.Take(NumberToLoad).ToArray());
if (result.Count != NumberToLoad)
throw new Exception();
return result;
Expand Down
4 changes: 2 additions & 2 deletions source/Nevermore.IntegrationTests/Advanced/HooksFixture.cs
Original file line number Diff line number Diff line change
Expand Up @@ -67,8 +67,8 @@ public AuditHook(StringBuilder log)
public void AfterInsert<TDocument>(TDocument document, DocumentMap map, IWriteTransaction transaction) where TDocument : class => log.AppendLine(nameof(AfterInsert));
public void BeforeUpdate<TDocument>(TDocument document, DocumentMap map, IWriteTransaction transaction) where TDocument : class => log.AppendLine(nameof(BeforeUpdate));
public void AfterUpdate<TDocument>(TDocument document, DocumentMap map, IWriteTransaction transaction) where TDocument : class => log.AppendLine(nameof(AfterUpdate));
public void BeforeDelete<TDocument>(string id, DocumentMap map, IWriteTransaction transaction) where TDocument : class => log.AppendLine(nameof(BeforeDelete));
public void AfterDelete<TDocument>(string id, DocumentMap map, IWriteTransaction transaction) where TDocument : class => log.AppendLine(nameof(AfterDelete));
public void BeforeDelete<TDocument>(object id, DocumentMap map, IWriteTransaction transaction) where TDocument : class => log.AppendLine(nameof(BeforeDelete));
public void AfterDelete<TDocument>(object id, DocumentMap map, IWriteTransaction transaction) where TDocument : class => log.AppendLine(nameof(AfterDelete));
public void BeforeCommit(IWriteTransaction transaction) => log.AppendLine(nameof(BeforeCommit));
public void AfterCommit(IWriteTransaction transaction) => log.AppendLine(nameof(AfterCommit));
}
Expand Down
13 changes: 13 additions & 0 deletions source/Nevermore.IntegrationTests/Model/Message.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
using System;

namespace Nevermore.IntegrationTests.Model
{
public class Message
{
public Guid Id { get; set; }

public string Sender { get; set; }

public string Body { get; set; }
}
}
13 changes: 13 additions & 0 deletions source/Nevermore.IntegrationTests/Model/MessageMap.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
using Nevermore.Mapping;

namespace Nevermore.IntegrationTests.Model
{
public class MessageMap : DocumentMap<Message>
{
public MessageMap()
{
Id(x => x.Id);
Column(x => x.Sender);
}
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
using System.Linq;
using System;
using System.Collections;
using System.Linq;
using FluentAssertions;
using Nevermore.IntegrationTests.Model;
using Nevermore.IntegrationTests.SetUp;
Expand All @@ -23,7 +25,7 @@ public void LoadWithMultipleIds()
{
using (var trn = Store.BeginTransaction())
{
trn.Load<Product>(new[] { "A", "B" });
trn.LoadMany<Product>(new[] { "A", "B" }.ToArray());
}
}

Expand All @@ -32,7 +34,7 @@ public void LoadWithMoreThan2100Ids()
{
using (var trn = Store.BeginTransaction())
{
trn.Load<Product>(Enumerable.Range(1, 3000).Select(n => "ID-" + n));
trn.LoadMany<Product>(Enumerable.Range(1, 3000).Select(n => "ID-" + n).ToArray());
}
}

Expand All @@ -41,7 +43,7 @@ public void LoadStreamWithMoreThan2100Ids()
{
using (var trn = Store.BeginTransaction())
{
trn.LoadStream<Product>(Enumerable.Range(1, 3000).Select(n => "ID-" + n));
trn.LoadStream<Product>(Enumerable.Range(1, 3000).Select(n => "ID-" + n).ToArray());
}
}

Expand Down Expand Up @@ -287,5 +289,48 @@ void InsertProductAndLineItems(string productName, decimal productPrice, IWriteT
trn.Insert(lineItem);
}
}

[Test]
public void StoreAndLoadAnyIdTypes()
{
using (var trn = Store.BeginTransaction())
{
var messageA = new Message { Id = Guid.NewGuid(), Sender = "Sender A", Body = "Body of Message A" };

trn.Insert<Message>(messageA);
trn.Commit();

var loadedMessageA = trn.Load<Message>(messageA.Id);

loadedMessageA.Sender.Should().Be(messageA.Sender);
loadedMessageA.Body.Should().Be(messageA.Body);
}
}

[Test]
public void StoreAndLoadManyAnyIdTypes()
{
using (var trn = Store.BeginTransaction())
{
var messageA = new Message { Id = Guid.NewGuid(), Sender = "Sender A", Body = "Body of Message A" };
var messageB = new Message { Id = Guid.NewGuid(), Sender = "Sender B", Body = "Body of Message B" };

trn.Insert(messageA);
trn.Insert(messageB);
trn.Commit();

var loadedMessages = trn.LoadMany<Message>(new object[] { messageA.Id, messageB.Id });
loadedMessages.Count.Should().Be(2);

var loadedMessageA = loadedMessages.Single(x => x.Id == messageA.Id);
var loadedMessageB = loadedMessages.Single(x => x.Id == messageB.Id);

loadedMessageA.Sender.Should().Be(messageA.Sender);
loadedMessageA.Body.Should().Be(messageA.Body);

loadedMessageB.Sender.Should().Be(messageB.Sender);
loadedMessageB.Body.Should().Be(messageB.Body);
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,8 @@ protected FixtureWithRelationalStore()
new ProductMap(),
new LineItemMap(),
new MachineMap(),
new OrderMap());
new OrderMap(),
new MessageMap());

config.TypeHandlers.Register(new ReferenceCollectionTypeHandler());
config.InstanceTypeResolvers.Register(new ProductTypeResolver());
Expand All @@ -43,7 +44,8 @@ protected FixtureWithRelationalStore()
new CustomerMap(),
new LineItemMap(),
new BrandMap(),
new MachineMap());
new MachineMap(),
new MessageMap());

Store = new RelationalStore(config);
}
Expand Down
2 changes: 1 addition & 1 deletion source/Nevermore.IntegrationTests/SetUp/SchemaGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ public static void WriteTableSchema(DocumentMap mapping, string tableNameOverrid
{
var tableName = tableNameOverride ?? mapping.TableName;
result.AppendLine("CREATE TABLE [TestSchema].[" + tableName + "] (");
result.AppendFormat(" [Id] NVARCHAR(50) NOT NULL CONSTRAINT [PK_{0}_Id] PRIMARY KEY CLUSTERED, ", tableName).AppendLine();
result.Append($" [Id] {GetDatabaseType(mapping.IdColumn)} NOT NULL CONSTRAINT [PK_{tableName}_Id] PRIMARY KEY CLUSTERED, ").AppendLine();

foreach (var column in mapping.WritableIndexedColumns())
{
Expand Down
8 changes: 4 additions & 4 deletions source/Nevermore/Advanced/Hooks/HookRegistry.cs
Original file line number Diff line number Diff line change
Expand Up @@ -33,12 +33,12 @@ public void AfterUpdate<TDocument>(TDocument document, DocumentMap map, IWriteTr
foreach (var hook in hooks) hook.AfterUpdate(document, map, transaction);
}

public void BeforeDelete<TDocument>(string id, DocumentMap map, IWriteTransaction transaction) where TDocument : class
public void BeforeDelete<TDocument>(object id, DocumentMap map, IWriteTransaction transaction) where TDocument : class
{
foreach (var hook in hooks) hook.BeforeDelete<TDocument>(id, map, transaction);
}

public void AfterDelete<TDocument>(string id, DocumentMap map, IWriteTransaction transaction) where TDocument : class
public void AfterDelete<TDocument>(object id, DocumentMap map, IWriteTransaction transaction) where TDocument : class
{
foreach (var hook in hooks) hook.AfterDelete<TDocument>(id, map, transaction);
}
Expand Down Expand Up @@ -73,12 +73,12 @@ public async Task AfterUpdateAsync<TDocument>(TDocument document, DocumentMap ma
foreach (var hook in hooks) await hook.AfterUpdateAsync(document, map, transaction);
}

public async Task BeforeDeleteAsync<TDocument>(string id, DocumentMap map, IWriteTransaction transaction) where TDocument : class
public async Task BeforeDeleteAsync<TDocument>(object id, DocumentMap map, IWriteTransaction transaction) where TDocument : class
{
foreach (var hook in hooks) await hook.BeforeDeleteAsync<TDocument>(id, map, transaction);
}

public async Task AfterDeleteAsync<TDocument>(string id, DocumentMap map, IWriteTransaction transaction) where TDocument : class
public async Task AfterDeleteAsync<TDocument>(object id, DocumentMap map, IWriteTransaction transaction) where TDocument : class
{
foreach (var hook in hooks) await hook.AfterDeleteAsync<TDocument>(id, map, transaction);
}
Expand Down
8 changes: 4 additions & 4 deletions source/Nevermore/Advanced/Hooks/IHook.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,17 +10,17 @@ void BeforeInsert<TDocument>(TDocument document, DocumentMap map, IWriteTransact
void AfterInsert<TDocument>(TDocument document, DocumentMap map, IWriteTransaction transaction) where TDocument : class {}
void BeforeUpdate<TDocument>(TDocument document, DocumentMap map, IWriteTransaction transaction) where TDocument : class {}
void AfterUpdate<TDocument>(TDocument document, DocumentMap map, IWriteTransaction transaction) where TDocument : class {}
void BeforeDelete<TDocument>(string id, DocumentMap map, IWriteTransaction transaction) where TDocument : class {}
void AfterDelete<TDocument>(string id, DocumentMap map, IWriteTransaction transaction) where TDocument : class {}
void BeforeDelete<TDocument>(object id, DocumentMap map, IWriteTransaction transaction) where TDocument : class {}
void AfterDelete<TDocument>(object id, DocumentMap map, IWriteTransaction transaction) where TDocument : class {}
void BeforeCommit(IWriteTransaction transaction) {}
void AfterCommit(IWriteTransaction transaction) {}

Task BeforeInsertAsync<TDocument>(TDocument document, DocumentMap map, IWriteTransaction transaction) where TDocument : class => Task.CompletedTask;
Task AfterInsertAsync<TDocument>(TDocument document, DocumentMap map, IWriteTransaction transaction) where TDocument : class => Task.CompletedTask;
Task BeforeUpdateAsync<TDocument>(TDocument document, DocumentMap map, IWriteTransaction transaction) where TDocument : class => Task.CompletedTask;
Task AfterUpdateAsync<TDocument>(TDocument document, DocumentMap map, IWriteTransaction transaction) where TDocument : class => Task.CompletedTask;
Task BeforeDeleteAsync<TDocument>(string id, DocumentMap map, IWriteTransaction transaction) where TDocument : class => Task.CompletedTask;
Task AfterDeleteAsync<TDocument>(string id, DocumentMap map, IWriteTransaction transaction) where TDocument : class => Task.CompletedTask;
Task BeforeDeleteAsync<TDocument>(object id, DocumentMap map, IWriteTransaction transaction) where TDocument : class => Task.CompletedTask;
Task AfterDeleteAsync<TDocument>(object id, DocumentMap map, IWriteTransaction transaction) where TDocument : class => Task.CompletedTask;
Task BeforeCommitAsync(IWriteTransaction transaction) => Task.CompletedTask;
Task AfterCommitAsync(IWriteTransaction transaction) => Task.CompletedTask;
}
Expand Down
40 changes: 22 additions & 18 deletions source/Nevermore/Advanced/ReadTransaction.cs
Original file line number Diff line number Diff line change
Expand Up @@ -81,24 +81,24 @@ public async Task OpenAsync(IsolationLevel isolationLevel)
}

[Pure]
public TDocument Load<TDocument>(string id) where TDocument : class
public TDocument Load<TDocument>(object id) where TDocument : class
{
return Stream<TDocument>(PrepareLoad<TDocument>(id)).FirstOrDefault();
}

public async Task<TDocument> LoadAsync<TDocument>(string id, CancellationToken cancellationToken = default) where TDocument : class
public async Task<TDocument> LoadAsync<TDocument>(object id, CancellationToken cancellationToken = default) where TDocument : class
{
var results = StreamAsync<TDocument>(PrepareLoad<TDocument>(id), cancellationToken);
await foreach (var row in results.WithCancellation(cancellationToken))
return row;
return null;
}

public List<TDocument> Load<TDocument>(IEnumerable<string> ids) where TDocument : class
public List<TDocument> LoadMany<TDocument>(params object[] ids) where TDocument : class
=> LoadStream<TDocument>(ids).ToList();

[Pure]
public async Task<List<TDocument>> LoadAsync<TDocument>(IEnumerable<string> ids, CancellationToken cancellationToken = default) where TDocument : class
public async Task<List<TDocument>> LoadManyAsync<TDocument>(object[] ids, CancellationToken cancellationToken = default) where TDocument : class
{
var results = new List<TDocument>();
await foreach (var item in LoadStreamAsync<TDocument>(ids, cancellationToken))
Expand All @@ -110,7 +110,7 @@ public async Task<List<TDocument>> LoadAsync<TDocument>(IEnumerable<string> ids,
}

[Pure]
public TDocument LoadRequired<TDocument>(string id) where TDocument : class
public TDocument LoadRequired<TDocument>(object id) where TDocument : class
{
var result = Load<TDocument>(id);
if (result == null)
Expand All @@ -119,18 +119,18 @@ public TDocument LoadRequired<TDocument>(string id) where TDocument : class
}

[Pure]
public async Task<TDocument> LoadRequiredAsync<TDocument>(string id, CancellationToken cancellationToken = default) where TDocument : class
public async Task<TDocument> LoadRequiredAsync<TDocument>(object id, CancellationToken cancellationToken = default) where TDocument : class
{
var result = await LoadAsync<TDocument>(id, cancellationToken);
if (result == null)
throw new ResourceNotFoundException(id);
return result;
}

public List<TDocument> LoadRequired<TDocument>(IEnumerable<string> ids) where TDocument : class
public List<TDocument> LoadManyRequired<TDocument>(params object[] ids) where TDocument : class
{
var idList = ids.Distinct().ToList();
var results = Load<TDocument>(idList);
var results = LoadMany<TDocument>(idList);
if (results.Count != idList.Count)
{
var firstMissing = idList.FirstOrDefault(id => results.All(record => configuration.DocumentMaps.GetId(record) != id));
Expand All @@ -140,11 +140,11 @@ public List<TDocument> LoadRequired<TDocument>(IEnumerable<string> ids) where TD
return results;
}

public async Task<List<TDocument>> LoadRequiredAsync<TDocument>(IEnumerable<string> ids, CancellationToken cancellationToken = default) where TDocument : class
public async Task<List<TDocument>> LoadManyRequiredAsync<TDocument>(object[] ids, CancellationToken cancellationToken = default) where TDocument : class
{
var idList = ids.Distinct().ToList();
var results = await LoadAsync<TDocument>(idList, cancellationToken);
if (results.Count != idList.Count)
var idList = ids.Distinct().ToArray();
var results = await LoadManyAsync<TDocument>(idList, cancellationToken);
if (results.Count != idList.Length)
{
var firstMissing = idList.FirstOrDefault(id => results.All(record => configuration.DocumentMaps.GetId(record) != id));
throw new ResourceNotFoundException(firstMissing);
Expand All @@ -154,16 +154,16 @@ public async Task<List<TDocument>> LoadRequiredAsync<TDocument>(IEnumerable<stri
}

[Pure]
public IEnumerable<TDocument> LoadStream<TDocument>(IEnumerable<string> ids) where TDocument : class
public IEnumerable<TDocument> LoadStream<TDocument>(params object[] ids) where TDocument : class
{
var idList = ids.Where(id => !string.IsNullOrWhiteSpace(id)).Distinct().ToList();
var idList = ids.Where(id => id != null).Distinct().ToList();
return idList.Count == 0 ? new List<TDocument>() : Stream<TDocument>(PrepareLoadMany<TDocument>(idList));
}

[Pure]
public async IAsyncEnumerable<TDocument> LoadStreamAsync<TDocument>(IEnumerable<string> ids, [EnumeratorCancellation] CancellationToken cancellationToken = default) where TDocument : class
public async IAsyncEnumerable<TDocument> LoadStreamAsync<TDocument>(object[] ids, [EnumeratorCancellation] CancellationToken cancellationToken = default) where TDocument : class
{
var idList = ids.Where(id => !string.IsNullOrWhiteSpace(id)).Distinct().ToList();
var idList = ids.Where(id => id != null).Distinct().ToList();
if (idList.Count == 0)
yield break;

Expand Down Expand Up @@ -339,15 +339,19 @@ public async Task<DbDataReader> ExecuteReaderAsync(PreparedCommand preparedComma
return await command.ExecuteReaderAsync(cancellationToken);
}

PreparedCommand PrepareLoad<TDocument>(string id)
PreparedCommand PrepareLoad<TDocument>(object id)
{
var mapping = configuration.DocumentMaps.Resolve(typeof(TDocument));

if (mapping.IdColumn.Type != id.GetType())
throw new ArgumentException($"Provided Id of type '{id.GetType().FullName}' does not match configured type of '{mapping.IdColumn.Type.FullName}");

var tableName = mapping.TableName;
var args = new CommandParameterValues {{"Id", id}};
return new PreparedCommand($"SELECT TOP 1 * FROM [{configuration.GetSchemaNameOrDefault(mapping)}].[{tableName}] WHERE [{mapping.IdColumn.ColumnName}] = @Id", args, RetriableOperation.Select, mapping, commandBehavior: CommandBehavior.SingleResult | CommandBehavior.SingleRow | CommandBehavior.SequentialAccess);
}

PreparedCommand PrepareLoadMany<TDocument>(IEnumerable<string> idList)
PreparedCommand PrepareLoadMany<TDocument>(IEnumerable<object> idList)
{
var mapping = configuration.DocumentMaps.Resolve(typeof(TDocument));
var tableName = mapping.TableName;
Expand Down
6 changes: 3 additions & 3 deletions source/Nevermore/Advanced/WriteTransaction.cs
Original file line number Diff line number Diff line change
Expand Up @@ -123,20 +123,20 @@ public Task DeleteAsync<TDocument>(TDocument document, DeleteOptions options, Ca
return DeleteAsync<TDocument>(id, options, cancellationToken);
}

public void Delete<TDocument>(string id, DeleteOptions options = null) where TDocument : class
public void Delete<TDocument>(object id, DeleteOptions options = null) where TDocument : class
{
var command = builder.PrepareDelete<TDocument>(id, options);
configuration.Hooks.BeforeDelete<TDocument>(id, command.Mapping, this);
ExecuteNonQuery(command);
configuration.Hooks.AfterDelete<TDocument>(id, command.Mapping, this);
}

public Task DeleteAsync<TDocument>(string id, CancellationToken cancellationToken = default) where TDocument : class
public Task DeleteAsync<TDocument>(object id, CancellationToken cancellationToken = default) where TDocument : class
{
return DeleteAsync(id, null, cancellationToken);
}

public async Task DeleteAsync<TDocument>(string id, DeleteOptions options, CancellationToken cancellationToken = default) where TDocument : class
public async Task DeleteAsync<TDocument>(object id, DeleteOptions options, CancellationToken cancellationToken = default) where TDocument : class
{
var command = builder.PrepareDelete<TDocument>(id, options);
await configuration.Hooks.BeforeDeleteAsync<TDocument>(id, command.Mapping, this);
Expand Down
Loading

0 comments on commit e2657ed

Please sign in to comment.