Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support for strongly typed string keys (Id property) #146

Draft
wants to merge 4 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions source/Nevermore.IntegrationTests/Advanced/HooksFixture.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,11 @@ public void ShouldCallHooks()

using var transaction = Store.BeginTransaction();

var customer = new Customer {Id = "C131", FirstName = "Fred", LastName = "Freddy"};
var customer = new Customer {Id = "C131".ToCustomerId(), FirstName = "Fred", LastName = "Freddy"};
transaction.Insert(customer);
AssertLogged(log, "BeforeInsert", "AfterInsert");

customer = transaction.Load<Customer>("C131");
customer = transaction.Load<Customer, CustomerId>("C131".ToCustomerId());

transaction.Update(customer);
AssertLogged(log, "BeforeUpdate", "AfterUpdate");
Expand Down
28 changes: 14 additions & 14 deletions source/Nevermore.IntegrationTests/KeyAllocatorFixture.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ public void ShouldAllocateKeysInChunks()
var allocatorA = new KeyAllocator(Store, 10);
var allocatorB = new KeyAllocator(Store, 10);

// A gets 1-10
// A gets 1-10
AssertNext(allocatorA, "Todos", 1);
AssertNext(allocatorA, "Todos", 2);
AssertNext(allocatorA, "Todos", 3);
Expand Down Expand Up @@ -76,7 +76,7 @@ public void ShouldAllocateInParallel()
const int allocationCount = 20;
const int threadCount = 10;

var projectIds = new ConcurrentBag<string>();
var customerIds = new ConcurrentBag<CustomerId>();
var deploymentIds = new ConcurrentBag<string>();
var random = new Random(1);

Expand All @@ -90,22 +90,22 @@ public void ShouldAllocateInParallel()
var sequence = random.Next(3);
if (sequence == 0)
{
var id = transaction.AllocateId(typeof (Customer));
projectIds.Add(id);
var id = transaction.AllocateId(typeof (Customer), typeof(CustomerId));
customerIds.Add((CustomerId)id);
transaction.Commit();
}
else if (sequence == 1)
{
// Abandon some transactions (just projects to make it easier)
var id = transaction.AllocateId(typeof(Customer));
var id = transaction.AllocateId(typeof(Customer), typeof(CustomerId));
// Abandoned Ids are not returned to the pool
projectIds.Add(id);
customerIds.Add((CustomerId)id);
transaction.Dispose();
}
else if (sequence == 2)
{
var id = transaction.AllocateId(typeof(Order));
deploymentIds.Add(id);
var id = transaction.AllocateId(typeof(Order), typeof(string));
deploymentIds.Add((string)id);
transaction.Commit();
}
}
Expand All @@ -115,21 +115,21 @@ public void ShouldAllocateInParallel()
Task.WaitAll(tasks);
Func<string, int> removePrefix = x => int.Parse(x.Split('-')[1]);

var projectIdsAfter = projectIds.Select(removePrefix).OrderBy(x => x).ToArray();
var customerIdsAfter = customerIds.Select(x => removePrefix(x.Value)).OrderBy(x => x).ToArray();
var deploymentIdsAfter = deploymentIds.Select(removePrefix).OrderBy(x => x).ToArray();

projectIdsAfter.Distinct().Count().Should().Be(projectIdsAfter.Length);
customerIdsAfter.Distinct().Count().Should().Be(customerIdsAfter.Length);
deploymentIdsAfter.Distinct().Count().Should().Be(deploymentIdsAfter.Length);

// Check that there are no gaps in sequence

var firstProjectId = projectIdsAfter.First();
var lastProjectId = projectIdsAfter.Last();
var firstProjectId = customerIdsAfter.First();
var lastProjectId = customerIdsAfter.Last();

var expectedProjectIds = Enumerable.Range(firstProjectId, lastProjectId - firstProjectId + 1)
.ToList();

projectIdsAfter.Should().BeEquivalentTo(expectedProjectIds);
customerIdsAfter.Should().BeEquivalentTo(expectedProjectIds);
}

static void AssertNext(KeyAllocator allocator, string collection, int expected)
Expand Down
54 changes: 53 additions & 1 deletion source/Nevermore.IntegrationTests/Model/Customer.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using System;
using Nevermore.IntegrationTests.Contracts;

namespace Nevermore.IntegrationTests.Model
Expand All @@ -9,7 +10,7 @@ public Customer()
Roles = new ReferenceCollection();
}

public string Id { get; set; }
public CustomerId Id { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
public ReferenceCollection Roles { get; }
Expand All @@ -18,4 +19,55 @@ public Customer()
public string ApiKey { get; set; }
public string[] Passphrases { get; set; }
}

public class CustomerId
{
internal CustomerId(string value)
{
Value = value;
}

public string Value { get; }

public override string ToString()
{
return Value;
}

public static bool operator !=(CustomerId? a, CustomerId? b)
{
return !(a == b);
}
public static bool operator ==(CustomerId? a, CustomerId? b)
{
return (a is null && b is null) ||
(!(a is null) && a.Equals(b));
}

protected bool Equals(CustomerId other)
{
return Value == other.Value;
}

public override bool Equals(object? obj)
{
if (ReferenceEquals(null, obj)) return false;
if (ReferenceEquals(this, obj)) return true;
if (obj.GetType() != GetType()) return false;
return Equals((CustomerId) obj);
}

public override int GetHashCode()
{
return (Value != null ? Value.GetHashCode() : 0);
}
}

public static class CustomerIdExtensionMethods
{
public static CustomerId? ToCustomerId(this string? value)
{
return string.IsNullOrWhiteSpace(value) ? null : new CustomerId(value);
}
}
}
24 changes: 24 additions & 0 deletions source/Nevermore.IntegrationTests/Model/CustomerMap.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
using System;
using System.Data.Common;
using Nevermore.Advanced.TypeHandlers;
using Nevermore.Mapping;

namespace Nevermore.IntegrationTests.Model
Expand All @@ -14,4 +17,25 @@ public CustomerMap()
Unique("UniqueCustomerNames", new[] { "FirstName", "LastName" }, "Customers must have a unique name");
}
}

class CustomerIdTypeHandler : ITypeHandler
{
public bool CanConvert(Type objectType)
{
return objectType == typeof(CustomerId);
}

public object ReadDatabase(DbDataReader reader, int columnIndex)
{
if (reader.IsDBNull(columnIndex))
return default(CustomerId);
var text = reader.GetString(columnIndex);
return new CustomerId(text);
}

public void WriteDatabase(DbParameter parameter, object value)
{
parameter.Value = ((CustomerId)value)?.Value;
}
}
}
30 changes: 15 additions & 15 deletions source/Nevermore.IntegrationTests/RelationalStoreFixture.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,16 +15,16 @@ public void ShouldGenerateIdsUnlessExplicitlyAssigned()
// The Id columns allow you to give records an ID, or use an auto-generated, unique ID
using (var transaction = Store.BeginTransaction())
{
var customer1 = new Customer {Id = "Customers-Alice", FirstName = "Alice", LastName = "Apple", LuckyNumbers = new[] {12, 13}, Nickname = "Ally", Roles = {"web-server", "app-server"}};
var customer1 = new Customer {Id = "Customers-Alice".ToCustomerId(), FirstName = "Alice", LastName = "Apple", LuckyNumbers = new[] {12, 13}, Nickname = "Ally", Roles = {"web-server", "app-server"}};
var customer2 = new Customer {FirstName = "Bob", LastName = "Banana", LuckyNumbers = new[] {12, 13}, Nickname = "B-man", Roles = {"web-server", "app-server"}};
var customer3 = new Customer {FirstName = "Charlie", LastName = "Cherry", LuckyNumbers = new[] {12, 13}, Nickname = "Chazza", Roles = {"web-server", "app-server"}};
transaction.Insert(customer1);
transaction.Insert(customer2);
transaction.Insert(customer3, new InsertOptions { CustomAssignedId = "Customers-Chazza"});
transaction.Insert(customer3, new InsertOptions { CustomAssignedId = "Customers-Chazza".ToCustomerId() });

customer1.Id.Should().Be("Customers-Alice");
customer2.Id.Should().StartWith("Customers-");
customer3.Id.Should().Be("Customers-Chazza");
customer1.Id.Value.Should().Be("Customers-Alice");
customer2.Id.Value.Should().StartWith("Customers-");
customer3.Id.Value.Should().Be("Customers-Chazza");

transaction.Commit();
}
Expand Down Expand Up @@ -82,7 +82,7 @@ public void ShouldPersistCollectionsToAllowInSearches()
[Test]
public void ShouldHandleIdsWithInOperand()
{
string customerId;
CustomerId customerId;
using (var transaction = Store.BeginTransaction())
{
var customer = new Customer {FirstName = "Alice", LastName = "Apple"};
Expand All @@ -94,7 +94,7 @@ public void ShouldHandleIdsWithInOperand()
using (var transaction = Store.BeginTransaction())
{
var customer = transaction.Query<Customer>()
.Where("Id", ArraySqlOperand.In, new[] {customerId})
.Where("Id", ArraySqlOperand.In, new[] {customerId.Value})
.Stream()
.Single();
customer.FirstName.Should().Be("Alice");
Expand Down Expand Up @@ -233,7 +233,7 @@ public void ShouldShowFriendlyUniqueConstraintErrors()
[Test]
public void ShouldPersistAndLoadReferenceCollectionsOnSingleDocuments()
{
var customerId = string.Empty;
CustomerId? customerId = null;
using (var transaction = Store.BeginTransaction())
{
var customer = new Customer {FirstName = "Alice", LastName = "Apple", LuckyNumbers = new[] {12, 13}, Nickname = "Ally", Roles = {"web-server", "app-server"}};
Expand All @@ -243,7 +243,7 @@ public void ShouldPersistAndLoadReferenceCollectionsOnSingleDocuments()
}
using (var transaction = Store.BeginTransaction())
{
var loadedCustomer = transaction.Load<Customer>(customerId);
var loadedCustomer = transaction.Load<Customer, CustomerId>(customerId);
loadedCustomer.Roles.Count.Should().Be(2);
}
}
Expand All @@ -255,8 +255,8 @@ public void ShouldUseIdPassedInToInsertMethod()
using (var transaction = Store.BeginTransaction())
{
var customer = new Customer {FirstName = "Alice", LastName = "Apple", LuckyNumbers = new[] {12, 13}, Nickname = "Ally", Roles = {"web-server", "app-server"}};
transaction.Insert(customer, new InsertOptions { CustomAssignedId = "12345" });
Assert.That(customer.Id, Is.EqualTo("12345"), "Id passed in should be used");
transaction.Insert(customer, new InsertOptions { CustomAssignedId = "12345".ToCustomerId() });
Assert.That(customer.Id?.Value, Is.EqualTo("12345"), "Id passed in should be used");
}
}

Expand All @@ -265,9 +265,9 @@ public void ShouldUseIdPassedInIfSame()
{
using (var transaction = Store.BeginTransaction())
{
var customer = new Customer {Id = "12345", FirstName = "Alice", LastName = "Apple", LuckyNumbers = new[] {12, 13}, Nickname = "Ally", Roles = {"web-server", "app-server"}};
transaction.Insert(customer, new InsertOptions { CustomAssignedId = "12345" });
Assert.That(customer.Id, Is.EqualTo("12345"), "Id passed in should be used if same");
var customer = new Customer {Id = "12345".ToCustomerId(), FirstName = "Alice", LastName = "Apple", LuckyNumbers = new[] {12, 13}, Nickname = "Ally", Roles = {"web-server", "app-server"}};
transaction.Insert(customer, new InsertOptions { CustomAssignedId = "12345".ToCustomerId() });
Assert.That(customer.Id?.Value, Is.EqualTo("12345"), "Id passed in should be used if same");
}
}

Expand All @@ -278,7 +278,7 @@ public void ShouldThrowIfConflictingIdsPassedIn()
{
Assert.Throws<ArgumentException>(() =>
{
var customer = new Customer {Id = "123456", FirstName = "Alice", LastName = "Apple", LuckyNumbers = new[] {12, 13}, Nickname = "Ally", Roles = {"web-server", "app-server"}};
var customer = new Customer {Id = "123456".ToCustomerId(), FirstName = "Alice", LastName = "Apple", LuckyNumbers = new[] {12, 13}, Nickname = "Ally", Roles = {"web-server", "app-server"}};
transaction.Insert(customer, new InsertOptions { CustomAssignedId = "12345" });
});
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ public void LoadWithMultipleIdsWithDifferentLength()
Type = ProductType.Normal
};
trn.Insert(product1);

var product2 = new Product
{
Id = "Products-133",
Expand All @@ -53,9 +53,9 @@ public void LoadWithMultipleIdsWithDifferentLength()
};
trn.Insert(product2);
trn.Commit();

var ids = new[] {product1.Id, product2.Id};

var products = trn.LoadMany<Product>(ids);

products.Should().HaveCount(ids.Length);
Expand Down Expand Up @@ -83,13 +83,13 @@ public void LoadStreamWithMoreThan2100Ids()
public void StoreNonInheritedTypesSerializesCorrectly()
{
using var trn = Store.BeginTransaction();

var customer = new Customer
{
FirstName = "Bob",
LastName = "Tester",
Nickname = "Bob the builder",
Id = "Customers-01"
Id = "Customers-01".ToCustomerId()
};

trn.Insert(customer);
Expand Down Expand Up @@ -218,7 +218,7 @@ public void StoreStringInheritedTypesSerializeCorrectly()
brandToTestSerialization.JSON.Should().Be("{\"Description\":\"Details for Brand A.\"}");
}
}

[Test]
public void StoreAndLoadStringInheritedTypes()
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ protected FixtureWithRelationalStore()
new DocumentWithRowVersionMap());

config.TypeHandlers.Register(new ReferenceCollectionTypeHandler());
config.TypeHandlers.Register(new CustomerIdTypeHandler());

config.InstanceTypeResolvers.Register(new ProductTypeResolver());
config.InstanceTypeResolvers.Register(new BrandTypeResolver());

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 @@ -52,7 +52,7 @@ static bool IsNullable(ColumnMapping column)

static string GetDatabaseType(ColumnMapping column)
{
var dbType = DatabaseTypeConverter.AsDbType(column.Type);
var dbType = column.Type.IsStronglyTypedId() ? DbType.String : DatabaseTypeConverter.AsDbType(column.Type);

switch (dbType)
{
Expand Down
6 changes: 3 additions & 3 deletions source/Nevermore.Tests/Delete/DeleteQueryBuilderFixture.cs
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ public DeleteQueryBuilderFixture()
mappings = Substitute.For<IDocumentMapRegistry>();
mappings.Resolve<object>().Returns(c => new EmptyMap());
mappings.Resolve(Arg.Any<Type>()).Returns(c => new EmptyMap());

queryExecutor = Substitute.For<IWriteQueryExecutor>();
queryExecutor.ExecuteNonQuery(Arg.Any<PreparedCommand>()).Returns(info =>
{
Expand All @@ -43,7 +43,7 @@ IDeleteQueryBuilder<TDocument> CreateQueryBuilder<TDocument>() where TDocument :
configuration.DocumentSerializer = new NewtonsoftDocumentSerializer(configuration);
return new DeleteQueryBuilder<TDocument>(
new UniqueParameterNameGenerator(),
new DataModificationQueryBuilder(configuration, s => null),
new DataModificationQueryBuilder(configuration, (s, _) => null),
queryExecutor
);
}
Expand Down Expand Up @@ -164,7 +164,7 @@ public void VariablesCasingIsNormalisedForWhere()
.Parameter("OTHERVAR", "Bar")
.Delete();
#pragma warning restore NV0006

parameters.Count.Should().Be(2);
foreach (var parameter in parameters)
query.Should().Contain("@" + parameter.Key, "Should contain @" + parameter.Key);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ public DataModificationQueryBuilderFixture()
new OtherMap());
builder = new DataModificationQueryBuilder(
configuration,
m => idAllocator()
(m, t) => idAllocator()
);
}

Expand Down
Loading