From 7695284f37acb05928e875021a0416c46d1f444e Mon Sep 17 00:00:00 2001 From: slewis74 Date: Fri, 11 Jun 2021 17:58:01 +1000 Subject: [PATCH 01/25] added PrimaryKeyHandlers --- .../KeyAllocatorFixture.cs | 10 +-- .../Delete/DeleteQueryBuilderFixture.cs | 11 ++- source/Nevermore/Advanced/ReadTransaction.cs | 7 +- source/Nevermore/Advanced/WriteTransaction.cs | 37 ++++++--- source/Nevermore/ConfigurationExtensions.cs | 7 +- .../IRelationalStoreConfiguration.cs | 19 ++--- source/Nevermore/IWriteQueryExecutor.cs | 22 ++--- source/Nevermore/Mapping/DocumentMap.cs | 71 +++++++--------- .../Nevermore/Mapping/DocumentMapRegistry.cs | 20 ++--- .../Mapping/IIdColumnMappingBuilder.cs | 8 +- .../Nevermore/Mapping/IPrimaryKeyHandler.cs | 12 +++ source/Nevermore/Mapping/IdColumnMapping.cs | 43 ++++++++-- source/Nevermore/Mapping/PrimaryKeyHandler.cs | 80 +++++++++++++++++++ .../Mapping/PrimaryKeyHandlerRegistry.cs | 61 ++++++++++++++ .../Nevermore/RelationalStoreConfiguration.cs | 8 ++ .../Util/DataModificationQueryBuilder.cs | 17 ++-- 16 files changed, 330 insertions(+), 103 deletions(-) create mode 100644 source/Nevermore/Mapping/IPrimaryKeyHandler.cs create mode 100644 source/Nevermore/Mapping/PrimaryKeyHandler.cs create mode 100644 source/Nevermore/Mapping/PrimaryKeyHandlerRegistry.cs diff --git a/source/Nevermore.IntegrationTests/KeyAllocatorFixture.cs b/source/Nevermore.IntegrationTests/KeyAllocatorFixture.cs index 034756cf..3a307023 100644 --- a/source/Nevermore.IntegrationTests/KeyAllocatorFixture.cs +++ b/source/Nevermore.IntegrationTests/KeyAllocatorFixture.cs @@ -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); @@ -90,21 +90,21 @@ public void ShouldAllocateInParallel() var sequence = random.Next(3); if (sequence == 0) { - var id = transaction.AllocateId(typeof (Customer)); + var id = transaction.AllocateId(typeof (Customer)); projectIds.Add(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)); // Abandoned Ids are not returned to the pool projectIds.Add(id); transaction.Dispose(); } else if (sequence == 2) { - var id = transaction.AllocateId(typeof(Order)); + var id = transaction.AllocateId(typeof(Order)); deploymentIds.Add(id); transaction.Commit(); } @@ -120,7 +120,7 @@ public void ShouldAllocateInParallel() projectIdsAfter.Distinct().Count().Should().Be(projectIdsAfter.Length); deploymentIdsAfter.Distinct().Count().Should().Be(deploymentIdsAfter.Length); - + // Check that there are no gaps in sequence var firstProjectId = projectIdsAfter.First(); diff --git a/source/Nevermore.Tests/Delete/DeleteQueryBuilderFixture.cs b/source/Nevermore.Tests/Delete/DeleteQueryBuilderFixture.cs index 18906628..615704f4 100644 --- a/source/Nevermore.Tests/Delete/DeleteQueryBuilderFixture.cs +++ b/source/Nevermore.Tests/Delete/DeleteQueryBuilderFixture.cs @@ -6,7 +6,6 @@ using Nevermore.Mapping; using Nevermore.Querying; using Nevermore.Util; -using Newtonsoft.Json; using NSubstitute; using NUnit.Framework; @@ -27,7 +26,7 @@ public DeleteQueryBuilderFixture() mappings = Substitute.For(); mappings.Resolve().Returns(c => new EmptyMap()); mappings.Resolve(Arg.Any()).Returns(c => new EmptyMap()); - + queryExecutor = Substitute.For(); queryExecutor.ExecuteNonQuery(Arg.Any()).Returns(info => { @@ -164,7 +163,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); @@ -227,7 +226,13 @@ public void VariablesCasingIsNormalisedForWhereIn() } } + internal class Empty + {} + internal class EmptyMap : DocumentMap { + public EmptyMap() : base(typeof(Empty), "Empty") + { + } } } \ No newline at end of file diff --git a/source/Nevermore/Advanced/ReadTransaction.cs b/source/Nevermore/Advanced/ReadTransaction.cs index 995167c8..c4b13d8d 100644 --- a/source/Nevermore/Advanced/ReadTransaction.cs +++ b/source/Nevermore/Advanced/ReadTransaction.cs @@ -12,6 +12,7 @@ using Microsoft.Data.SqlClient; using Nevermore.Diagnositcs; using Nevermore.Diagnostics; +using Nevermore.Mapping; using Nevermore.Querying.AST; using Nevermore.Transient; @@ -544,7 +545,11 @@ PreparedCommand PrepareLoad(TKey id) 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}}; + var idHandler = configuration.PrimaryKeyHandlerRegistry.Resolve(mapping); + var args = new CommandParameterValues + { + { "Id", idHandler is IPrimitivePrimaryKeyHandler primitive ? primitive.GetPrimitiveValue(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); } diff --git a/source/Nevermore/Advanced/WriteTransaction.cs b/source/Nevermore/Advanced/WriteTransaction.cs index 13cac468..2dae3016 100644 --- a/source/Nevermore/Advanced/WriteTransaction.cs +++ b/source/Nevermore/Advanced/WriteTransaction.cs @@ -208,32 +208,49 @@ public IDeleteQueryBuilder DeleteQuery() where TDocument : return new DeleteQueryBuilder(ParameterNameGenerator, builder, this); } - public string AllocateId(Type documentType) + public TKey AllocateId(Type documentType) { var mapping = configuration.DocumentMaps.Resolve(documentType); - return AllocateId(mapping); + return AllocateIdForMapping(mapping); } - public string AllocateId() + public TKey AllocateId() { var mapping = configuration.DocumentMaps.Resolve(); - return AllocateId(mapping); + return AllocateIdForMapping(mapping); } - string AllocateId(DocumentMap mapping) + TKey AllocateIdForMapping(DocumentMap mapping) { - return AllocateId(mapping.TableName, mapping.IdFormat); + var handler = configuration.PrimaryKeyHandlerRegistry.Resolve(mapping); + if (handler == null || !(handler is IPrimitivePrimaryKeyHandler primitivePrimaryKeyHandler)) + throw new InvalidOperationException($"Primary key handler could not be resolved for type {mapping.Type}, or it is configured to use an identity key handler."); + + if (typeof(TKey) != primitivePrimaryKeyHandler.Type) + throw new ArgumentException($"The given key type of {typeof(TKey).Name} does not match the document maps primary key handler type {primitivePrimaryKeyHandler.Type.Name}"); + + return (TKey) AllocateIdUsingHandler(mapping, primitivePrimaryKeyHandler); } - public string AllocateId(string tableName, string idPrefix) + object AllocateId(DocumentMap mapping) + { + var handler = configuration.PrimaryKeyHandlerRegistry.Resolve(mapping); + if (handler == null || !(handler is IPrimitivePrimaryKeyHandler primitivePrimaryKeyHandler)) + throw new InvalidOperationException($"Primary key handler could not be resolved for type {mapping.Type}, or it is configured to use an identity key handler."); + + return AllocateIdUsingHandler(mapping, primitivePrimaryKeyHandler); + } + + object AllocateIdUsingHandler(DocumentMap mapping, IPrimitivePrimaryKeyHandler primitivePrimaryKeyHandler) { - return AllocateId(tableName, key => $"{idPrefix}-{key}"); + var key = keyAllocator.NextId(mapping.TableName); + return primitivePrimaryKeyHandler.FormatKey(mapping.TableName, key); } - string AllocateId(string tableName, Func idFormatter) + public string AllocateId(string tableName, string idPrefix) { var key = keyAllocator.NextId(tableName); - return idFormatter(key); + return $"{idPrefix}-{key}"; } public void Commit() diff --git a/source/Nevermore/ConfigurationExtensions.cs b/source/Nevermore/ConfigurationExtensions.cs index 85dce581..1d99358c 100644 --- a/source/Nevermore/ConfigurationExtensions.cs +++ b/source/Nevermore/ConfigurationExtensions.cs @@ -1,3 +1,4 @@ +#nullable enable using System; using Nevermore.Advanced; using Nevermore.Advanced.Serialization; @@ -18,10 +19,10 @@ public static void UseJsonNetSerialization(this IRelationalStoreConfiguration co callback(jsonNet.SerializerSettings); } - internal static string GetSchemaNameOrDefault(this IRelationalStoreConfiguration configuration, string schemaName) + internal static string GetSchemaNameOrDefault(this IRelationalStoreConfiguration configuration, string? schemaName) { - return schemaName - ?? configuration.DefaultSchema + return schemaName + ?? configuration.DefaultSchema ?? NevermoreDefaults.FallbackDefaultSchemaName; } diff --git a/source/Nevermore/IRelationalStoreConfiguration.cs b/source/Nevermore/IRelationalStoreConfiguration.cs index b096147a..5d34c7fe 100644 --- a/source/Nevermore/IRelationalStoreConfiguration.cs +++ b/source/Nevermore/IRelationalStoreConfiguration.cs @@ -13,39 +13,40 @@ public interface IRelationalStoreConfiguration { string ApplicationName { get; set; } string ConnectionString { get; } - + /// /// Gets or sets whether synchronous operations are allowed. The default is true. Set to /// false to have Nevermore throw a when /// calling a synchronous operation. /// public bool AllowSynchronousOperations { get; set; } - + /// /// Gets or sets the default schema name (e.g., 'dbo') that will be used as a prefix on all statements. Can be /// overridden on each document map. /// string DefaultSchema { get; set; } - + IDocumentMapRegistry DocumentMaps { get; } IDocumentSerializer DocumentSerializer { get; set; } IReaderStrategyRegistry ReaderStrategies { get; } ITypeHandlerRegistry TypeHandlers { get; } IInstanceTypeRegistry InstanceTypeResolvers { get; } + IPrimaryKeyHandlerRegistry PrimaryKeyHandlerRegistry { get; } IRelatedDocumentStore RelatedDocumentStore { get; set; } IQueryLogger QueryLogger { get; set; } - + /// /// Hooks can be used to apply general logic when documents are inserted, updated or deleted. /// IHookRegistry Hooks { get; } - + /// /// Gets or sets the factory that creates SQL commands. Set this if you want to control how commands are set up, - /// or add a decorator to capture diagnostic information. + /// or add a decorator to capture diagnostic information. /// ISqlCommandFactory CommandFactory { get; set; } - + /// /// Gets or sets the key block size that will be used for the key allocator. A higher number enables less /// SQL queries to get new blocks, but increases fragmentation. @@ -54,11 +55,11 @@ public interface IRelationalStoreConfiguration /// /// Gets or sets whether Multiple Active Result Sets is enabled. On .NET Core 3.1 this has issues on Linux, so it - /// defaults to false. + /// defaults to false. /// /// https://docs.microsoft.com/en-us/sql/relational-databases/native-client/features/using-multiple-active-result-sets-mars?view=sql-server-ver15 bool ForceMultipleActiveResultSets { get; set; } - + /// /// Gets or sets whether to actively detect similar queries being executed in a way that is slightly different, /// resulting in duplicate query plans being created. diff --git a/source/Nevermore/IWriteQueryExecutor.cs b/source/Nevermore/IWriteQueryExecutor.cs index ceeddf51..df79982e 100644 --- a/source/Nevermore/IWriteQueryExecutor.cs +++ b/source/Nevermore/IWriteQueryExecutor.cs @@ -41,7 +41,7 @@ public interface IWriteQueryExecutor : IReadQueryExecutor /// /// Immediately inserts multiple items into a specific table. Useful for up to a few hundred items, but not more - /// (depends on the number of properties in each item). + /// (depends on the number of properties in each item). /// /// The type of document being inserted. /// The document instances to insert (will be formed into a multiple VALUES for a single SQL INSERT. @@ -68,7 +68,7 @@ public interface IWriteQueryExecutor : IReadQueryExecutor Task InsertManyAsync(IReadOnlyCollection documents, InsertOptions options, CancellationToken cancellationToken = default) where TDocument : class; /// - /// Updates an existing document in the database. + /// Updates an existing document in the database. /// /// The type of document being updated. /// The document to update. @@ -76,7 +76,7 @@ public interface IWriteQueryExecutor : IReadQueryExecutor void Update(TDocument document, UpdateOptions options = null) where TDocument : class; /// - /// Updates an existing document in the database. + /// Updates an existing document in the database. /// /// The type of document being updated. /// The document to update. @@ -84,7 +84,7 @@ public interface IWriteQueryExecutor : IReadQueryExecutor Task UpdateAsync(TDocument document, CancellationToken cancellationToken = default) where TDocument : class; /// - /// Updates an existing document in the database. + /// Updates an existing document in the database. /// /// The type of document being updated. /// The document to update. @@ -226,26 +226,30 @@ public interface IWriteQueryExecutor : IReadQueryExecutor /// /// Allocate an ID for the specified type. The type must be mapped. - /// If the mapping specifies a SingletonId, that is returned + /// If the mapping specifies a SingletonId, that is returned. /// /// /// - string AllocateId(Type documentType); + /// Will be thrown if the requested key type does not match the mapped document's key type. + TKey AllocateId(Type documentType); /// - /// Allocates an ID using the specified table name. Any mapping for that table is not used. + /// Allocates an ID using the specified table name. Any mapping for that table is not used. The configuration's PrimaryKeyHandlerRegistry must contain + /// an entry for the key type (primitives are handled out of the box). /// /// /// /// string AllocateId(string tableName, string idPrefix); - + /// /// Allocate an ID for the specified type. The type must be mapped. /// If the mapping specifies a SingletonId, that is returned /// /// The type of document. + /// The key type to allocate, e.g. int, string, MyStringTinyType. /// - string AllocateId(); + /// Will be thrown if the requested key type does not match the mapped document's key type. + TKey AllocateId(); } } \ No newline at end of file diff --git a/source/Nevermore/Mapping/DocumentMap.cs b/source/Nevermore/Mapping/DocumentMap.cs index 36a46c51..edbd6785 100644 --- a/source/Nevermore/Mapping/DocumentMap.cs +++ b/source/Nevermore/Mapping/DocumentMap.cs @@ -1,3 +1,4 @@ +#nullable enable using System; using System.Collections.Generic; using System.Linq.Expressions; @@ -18,7 +19,7 @@ protected DocumentMap() /// /// Gets or sets the name of the schema containing the table that this document will be stored in. /// - protected string SchemaName + protected string? SchemaName { get => map.SchemaName; set => map.SchemaName = value; @@ -34,22 +35,12 @@ protected string TableName } /// - /// Gets or sets the prefix to be used before IDs. If you override , this property won't - /// be used. + /// Gets or sets the primary key handler. /// - protected string IdPrefix + protected IPrimaryKeyHandler? PrimaryKeyHandler { - get => map.IdPrefix; - set => map.IdPrefix = value; - } - - /// - /// Gets or sets a formatting function used to format generated document IDs. Examples: i => "C" + i; - /// - protected Func IdFormat - { - get => map.IdFormat; - set => map.IdFormat = value; + get => map.PrimaryKeyHandler; + set => map.PrimaryKeyHandler = value; } /// @@ -78,6 +69,8 @@ protected JsonStorageFormat JsonStorageFormat /// A builder to further configure the ID. protected IIdColumnMappingBuilder Id() { + if (map.IdColumn is null) + throw new InvalidOperationException($"Id was called for {typeof(TDocument)} but no Id property was found on this type."); return map.IdColumn; } @@ -89,7 +82,7 @@ protected IIdColumnMappingBuilder Id() /// A builder to further configure the ID. protected IIdColumnMappingBuilder Id(Expression> property) { - return Id(null, property); + return Id(null, property); } /// @@ -99,7 +92,7 @@ protected IIdColumnMappingBuilder Id(ExpressionAn expression that accesses the property. E.g., c => c.FirstName /// The property type of the Id column. /// A builder to further configure the ID. - protected IIdColumnMappingBuilder Id(string columnName, Expression> property) + protected IIdColumnMappingBuilder Id(string? columnName, Expression> property) { var prop = GetPropertyInfo(property) ?? throw new Exception("The expression for the Id column must be a property."); @@ -129,7 +122,7 @@ protected IColumnMappingBuilder TypeResolutionColumn(ExpressionAn expression that accesses the property. E.g., c => c.FirstName /// The property type of the column. /// A builder to further configure the column mapping. - protected IColumnMappingBuilder TypeResolutionColumn(string columnName, Expression> property) + protected IColumnMappingBuilder TypeResolutionColumn(string? columnName, Expression> property) { var prop = GetPropertyInfo(property) ?? throw new Exception("The expression for the Type Resolution column must be a property."); @@ -174,7 +167,7 @@ protected IColumnMappingBuilder Column(string columnName, Expression< /// A func called when reading data from the database and setting it on the object. /// The property type of the column. /// A builder to further configure the column mapping. - protected IColumnMappingBuilder Column(string columnName, Expression> getter, Action setter) + protected IColumnMappingBuilder Column(string? columnName, Expression>? getter, Action? setter) { var property = GetPropertyInfo(getter ?? throw new ArgumentNullException(nameof(getter))); if (property != null && setter == null) @@ -193,21 +186,21 @@ protected IColumnMappingBuilder Column(string columnName, Expression< /// A custom property handler. /// If handler uses a property, the property, so it can be excluded from JSON. Can be null. /// A builder to further configure the column mapping. - protected IColumnMappingBuilder Column(string columnName, Type propertyType, IPropertyHandler handler, PropertyInfo prop = null) + protected IColumnMappingBuilder Column(string columnName, Type propertyType, IPropertyHandler handler, PropertyInfo? prop = null) { var column = new ColumnMapping(columnName, propertyType, handler, prop); map.Columns.Add(column); return column; } - protected RelatedDocumentsMapping RelatedDocuments(Expression>> property, string tableName = DocumentMap.RelatedDocumentTableName, string schemaName = null) + protected RelatedDocumentsMapping RelatedDocuments(Expression>> property, string tableName = DocumentMap.RelatedDocumentTableName, string? schemaName = null) { var mapping = new RelatedDocumentsMapping(GetPropertyInfo(property), tableName, schemaName); map.RelatedDocumentsMappings.Add(mapping); return mapping; } - PropertyInfo GetPropertyInfo(Expression> propertyLambda) + PropertyInfo? GetPropertyInfo(Expression> propertyLambda) { var member = propertyLambda.Body as MemberExpression; if (member == null) @@ -255,17 +248,14 @@ protected UniqueRule Unique(string constraintName, string[] columnNames, string static DocumentMap InitializeDefault() { - return new DocumentMap + return new DocumentMap(typeof(TDocument), typeof(TDocument).Name) { - Type = typeof(TDocument), IdColumn = GetDefaultIdColumn(), - TableName = typeof(TDocument).Name, - IdPrefix = typeof(TDocument).Name + "s", - JsonStorageFormat = JsonStorageFormat.TextOnly + JsonStorageFormat = Mapping.JsonStorageFormat.TextOnly }; } - static IdColumnMapping GetDefaultIdColumn() + static IdColumnMapping? GetDefaultIdColumn() { var properties = typeof(TDocument).GetProperties(); foreach (var property in properties) @@ -289,22 +279,23 @@ public class DocumentMap { public const string RelatedDocumentTableName = "RelatedDocument"; - public DocumentMap() + public DocumentMap(Type type, string tableName) { + Type = type; + TableName = tableName; Columns = new List(); UniqueConstraints = new List(); RelatedDocumentsMappings = new List(); - IdFormat = key => $"{IdPrefix}-{key}"; } public Type Type { get; set; } - public IdColumnMapping IdColumn { get; set; } - public ColumnMapping RowVersionColumn { get; set; } - public ColumnMapping TypeResolutionColumn { get; set; } + public IdColumnMapping? IdColumn { get; set; } + public ColumnMapping? RowVersionColumn { get; set; } + public ColumnMapping? TypeResolutionColumn { get; set; } public JsonStorageFormat JsonStorageFormat { get; set; } public string TableName { get; set; } - public string IdPrefix { get; set; } - public Func IdFormat { get; set; } + + public IPrimaryKeyHandler? PrimaryKeyHandler { get; set; } public bool ExpectLargeDocuments { get; set; } @@ -314,11 +305,11 @@ public DocumentMap() public List Columns { get; } public List UniqueConstraints { get; } public List RelatedDocumentsMappings { get; } - public string SchemaName { get; set; } + public string? SchemaName { get; set; } public bool IsRowVersioningEnabled => RowVersionColumn != null; - public bool IsIdentityId => IdColumn.IsIdentity; + public bool IsIdentityId => IdColumn?.PrimaryKeyHandler is IIdentityPrimaryKeyHandler; public bool HasModificationOutputs =>IsRowVersioningEnabled || IsIdentityId; @@ -327,7 +318,7 @@ public void Validate() if (IdColumn == null) throw new InvalidOperationException($"There is no Id property on the document type {Type.FullName}"); - if (TypeResolutionColumn != null && JsonStorageFormat == JsonStorageFormat.NoJson) + if (TypeResolutionColumn != null && JsonStorageFormat == Mapping.JsonStorageFormat.NoJson) throw new InvalidOperationException($"The document map for type {Type.FullName} has a TypeColumn, but also uses the NoJson storage mode, which is not allowed."); try @@ -341,9 +332,9 @@ public void Validate() } } - public object GetId(object document) + public object? GetId(object? document) { - if (document == null) + if (document == null || IdColumn == null) return null; var readerWriter = IdColumn.PropertyHandler; diff --git a/source/Nevermore/Mapping/DocumentMapRegistry.cs b/source/Nevermore/Mapping/DocumentMapRegistry.cs index 8b86b937..cb58cb48 100644 --- a/source/Nevermore/Mapping/DocumentMapRegistry.cs +++ b/source/Nevermore/Mapping/DocumentMapRegistry.cs @@ -1,9 +1,9 @@ +#nullable enable using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; using System.Reflection; -using Nevermore.Querying.AST; namespace Nevermore.Mapping { @@ -14,7 +14,7 @@ public class DocumentMapRegistry : IDocumentMapRegistry public DocumentMapRegistry() { } - + public List GetAll() { return new List(mappings.Values); @@ -35,7 +35,7 @@ public void Register(params IDocumentMap[] mappingsToAdd) { Register(mappingsToAdd.AsEnumerable()); } - + public void Register(IEnumerable mappingsToAdd) { foreach (var mapping in mappingsToAdd) @@ -50,14 +50,14 @@ public bool ResolveOptional(Type type, out DocumentMap map) // Walk up the inheritance chain and make sure there's only one map for the document. var currentType = type; - + while (true) { if (mappings.TryGetValue(currentType, out var m)) { maps.Add(m); } - + currentType = currentType.GetTypeInfo().BaseType; if (currentType == typeof(object) || currentType == null) break; @@ -65,7 +65,7 @@ public bool ResolveOptional(Type type, out DocumentMap map) if (maps.Count > 1) throw new InvalidOperationException($"More than one document map is registered against the type '{type.FullName}'. The following maps could apply: " + string.Join(", ", maps.Select(m => m.GetType().FullName))); - + map = maps.SingleOrDefault(); return map != null; } @@ -87,11 +87,11 @@ public DocumentMap Resolve(Type type) { throw NotRegistered(type); } - + return mapping; } - - public object GetId(object instance) + + public object? GetId(object instance) { if (instance == null) throw new ArgumentNullException(nameof(instance)); @@ -102,7 +102,7 @@ public object GetId(object instance) return map.GetId(instance); } - + static Exception NotRegistered(Type type) { return new InvalidOperationException($"To be used for this operation, the class '{type.FullName}' must have a document map that is registered with this relational store. Types without a document map cannot be used for this operation."); diff --git a/source/Nevermore/Mapping/IIdColumnMappingBuilder.cs b/source/Nevermore/Mapping/IIdColumnMappingBuilder.cs index 894b1d49..339fdcfb 100644 --- a/source/Nevermore/Mapping/IIdColumnMappingBuilder.cs +++ b/source/Nevermore/Mapping/IIdColumnMappingBuilder.cs @@ -1,4 +1,6 @@ -namespace Nevermore.Mapping +using System; + +namespace Nevermore.Mapping { public interface IIdColumnMappingBuilder : IColumnMappingBuilder { @@ -7,5 +9,9 @@ public interface IIdColumnMappingBuilder : IColumnMappingBuilder /// /// This will also reset the PropertyHandler IIdColumnMappingBuilder Identity(); + + IIdColumnMappingBuilder KeyHandler(IPrimaryKeyHandler primaryKeyHandler); + + IIdColumnMappingBuilder IdPrefix(Func<(string tableName, int key), string> idPrefix); } } \ No newline at end of file diff --git a/source/Nevermore/Mapping/IPrimaryKeyHandler.cs b/source/Nevermore/Mapping/IPrimaryKeyHandler.cs new file mode 100644 index 00000000..d35e46ce --- /dev/null +++ b/source/Nevermore/Mapping/IPrimaryKeyHandler.cs @@ -0,0 +1,12 @@ +using System; + +namespace Nevermore.Mapping +{ + public interface IPrimaryKeyHandler + { + Type Type { get; } + } + + public interface IPrimaryKeyHandler : IPrimaryKeyHandler + {} +} \ No newline at end of file diff --git a/source/Nevermore/Mapping/IdColumnMapping.cs b/source/Nevermore/Mapping/IdColumnMapping.cs index dfa713f1..817969e2 100644 --- a/source/Nevermore/Mapping/IdColumnMapping.cs +++ b/source/Nevermore/Mapping/IdColumnMapping.cs @@ -1,4 +1,5 @@ -using System; +#nullable enable +using System; using System.Collections.Generic; using System.Reflection; @@ -19,26 +20,58 @@ internal IdColumnMapping(string columnName, Type type, IPropertyHandler handler, : base(columnName, type, handler, property) { } - public bool IsIdentity { get; private set; } + public IPrimaryKeyHandler? PrimaryKeyHandler { get; private set; } /// public IIdColumnMappingBuilder Identity() + { + ValidateForIdentityUse(); + + if (!(PrimaryKeyHandler is null) && !(PrimaryKeyHandler is IIdentityPrimaryKeyHandler)) + throw new InvalidOperationException($"{nameof(KeyHandler)} has already been set to a non-identity handler."); + + var handlerType = typeof(IdentityPrimaryKeyHandler<>).MakeGenericType(Type); + var keyHandler = (IIdentityPrimaryKeyHandler)Activator.CreateInstance(handlerType); + + return KeyHandler(keyHandler); + } + + void ValidateForIdentityUse() { if (!ValidIdentityTypes.Contains(Type)) throw new InvalidOperationException($"The type {Type.Name} is not supported for Identity columns. Identity columns must be one of 'short', 'int' or 'long'."); if (hasCustomPropertyHandler) throw new InvalidOperationException("Unable to configure an Identity Id column with a custom PropertyHandler"); + } - IsIdentity = true; - Direction = ColumnDirection.FromDatabase; + public IIdColumnMappingBuilder KeyHandler(IPrimaryKeyHandler primaryKeyHandler) + { + if (!(PrimaryKeyHandler is null) && Direction == ColumnDirection.FromDatabase && !(primaryKeyHandler is IIdentityPrimaryKeyHandler)) + throw new InvalidOperationException($"{nameof(KeyHandler)} can only be called with an IIdentityPrimaryKeyHandler, once {nameof(Identity)} has been called."); + + if (primaryKeyHandler is IIdentityPrimaryKeyHandler) + { + ValidateForIdentityUse(); + Direction = ColumnDirection.FromDatabase; + } + + PrimaryKeyHandler = primaryKeyHandler; return this; } + public IIdColumnMappingBuilder IdPrefix(Func<(string tableName, int key), string> idPrefix) + { + if (!(PrimaryKeyHandler is null) && Direction == ColumnDirection.FromDatabase && PrimaryKeyHandler is IIdentityPrimaryKeyHandler) + throw new InvalidOperationException($"{nameof(IdPrefix)} cannot be set when an identity key handler has been configured."); + + return KeyHandler(new StringPrimaryKeyHandler(idPrefix)); + } + protected override void SetCustomPropertyHandler(IPropertyHandler propertyHandler) { - if (IsIdentity) + if (PrimaryKeyHandler is IIdentityPrimaryKeyHandler) throw new InvalidOperationException("Unable to configure an Identity Id column with a custom PropertyHandler"); hasCustomPropertyHandler = true; diff --git a/source/Nevermore/Mapping/PrimaryKeyHandler.cs b/source/Nevermore/Mapping/PrimaryKeyHandler.cs new file mode 100644 index 00000000..d261d742 --- /dev/null +++ b/source/Nevermore/Mapping/PrimaryKeyHandler.cs @@ -0,0 +1,80 @@ +#nullable enable +using System; +using System.Diagnostics.CodeAnalysis; + +namespace Nevermore.Mapping +{ + public interface IPrimitivePrimaryKeyHandler : IPrimaryKeyHandler + { + [return: NotNullIfNotNull("id")] + object? GetPrimitiveValue(TKey id); + + object FormatKey(string tableName, int key); + } + + public abstract class PrimitivePrimaryKeyHandler : IPrimitivePrimaryKeyHandler + { + public Type Type => typeof(T); + + [return: NotNullIfNotNull("id")] + public virtual object? GetPrimitiveValue(TKey id) + { + return id; + } + + public abstract object FormatKey(string tableName, int key); + } + + public interface IStringBasedPrimitivePrimaryKeyHandler : IPrimaryKeyHandler + { + void SetIdPrefix(Func<(string tableName, int key), string> idPrefix); + } + + class StringPrimaryKeyHandler : PrimitivePrimaryKeyHandler, IStringBasedPrimitivePrimaryKeyHandler + { + Func<(string tableName, int key), string> idPrefixFunc; + public StringPrimaryKeyHandler(Func<(string tableName, int key), string>? idPrefix = null) + { + idPrefixFunc = idPrefix ?? (x => $"{x.tableName}s-{x.key}"); + } + + public void SetIdPrefix(Func<(string tableName, int key), string> idPrefix) + { + idPrefixFunc = idPrefix; + } + + public override object FormatKey(string tableName, int key) + { + return idPrefixFunc((tableName, key)); + } + } + + class IntPrimaryKeyHandler : PrimitivePrimaryKeyHandler + { + public override object FormatKey(string tableName, int key) + { + return key; + } + } + + class LongPrimaryKeyHandler : PrimitivePrimaryKeyHandler + { + public override object FormatKey(string tableName, int key) + { + return key; + } + } + + class GuidPrimaryKeyHandler : IPrimaryKeyHandler + { + public Type Type => typeof(Guid); + } + + public interface IIdentityPrimaryKeyHandler : IPrimaryKeyHandler + {} + + public class IdentityPrimaryKeyHandler : IIdentityPrimaryKeyHandler + { + public Type Type => typeof(T); + } +} \ No newline at end of file diff --git a/source/Nevermore/Mapping/PrimaryKeyHandlerRegistry.cs b/source/Nevermore/Mapping/PrimaryKeyHandlerRegistry.cs new file mode 100644 index 00000000..df11b5a0 --- /dev/null +++ b/source/Nevermore/Mapping/PrimaryKeyHandlerRegistry.cs @@ -0,0 +1,61 @@ +#nullable enable +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; + +namespace Nevermore.Mapping +{ + public interface IPrimaryKeyHandlerRegistry + { + void Register(IPrimaryKeyHandler strategy); + + IPrimaryKeyHandler? Resolve(DocumentMap documentMap); + } + + class PrimaryKeyHandlerRegistry : IPrimaryKeyHandlerRegistry + { + // maps key type to IPrimaryKeyHandler concrete type + readonly ConcurrentDictionary mappings = new ConcurrentDictionary(); + + public void Register(IPrimaryKeyHandler handler) + { + mappings[handler.Type] = handler; + } + + public IPrimaryKeyHandler? Resolve(DocumentMap documentMap) + { + if (!(documentMap.PrimaryKeyHandler is null)) + return documentMap.PrimaryKeyHandler; + + if (documentMap.IdColumn is null) + throw new InvalidOperationException($"Map for document type {documentMap.Type.Name} does not specify an Id column."); + + var idType = documentMap.IdColumn.Type; + + var maps = new List(); + + // Walk up the inheritance chain and make sure there's only one map for the document. + var currentType = idType; + + while (true) + { + if (mappings.TryGetValue(currentType, out var m)) + { + maps.Add(m); + } + + currentType = currentType.GetTypeInfo().BaseType; + if (currentType == typeof(object) || currentType == null) + break; + } + + if (maps.Count > 1) + throw new InvalidOperationException($"More than one key allocation strategy is registered against the type '{idType.FullName}'. The following maps could apply: " + string.Join(", ", maps.Select(m => m.GetType().FullName))); + + var strategy = maps.SingleOrDefault(); + return strategy; + } + } +} \ No newline at end of file diff --git a/source/Nevermore/RelationalStoreConfiguration.cs b/source/Nevermore/RelationalStoreConfiguration.cs index c05a9620..623b979a 100644 --- a/source/Nevermore/RelationalStoreConfiguration.cs +++ b/source/Nevermore/RelationalStoreConfiguration.cs @@ -46,6 +46,12 @@ public RelationalStoreConfiguration(Func connectionStringFunc) TypeHandlers = new TypeHandlerRegistry(); + PrimaryKeyHandlerRegistry = new PrimaryKeyHandlerRegistry(); + PrimaryKeyHandlerRegistry.Register(new StringPrimaryKeyHandler()); + PrimaryKeyHandlerRegistry.Register(new IntPrimaryKeyHandler()); + PrimaryKeyHandlerRegistry.Register(new LongPrimaryKeyHandler()); + PrimaryKeyHandlerRegistry.Register(new GuidPrimaryKeyHandler()); + AllowSynchronousOperations = true; QueryLogger = new DefaultQueryLogger(); @@ -81,6 +87,8 @@ public RelationalStoreConfiguration(Func connectionStringFunc) public ITypeHandlerRegistry TypeHandlers { get; } public IInstanceTypeRegistry InstanceTypeResolvers { get; } + public IPrimaryKeyHandlerRegistry PrimaryKeyHandlerRegistry { get; } + /// /// MARS: https://docs.microsoft.com/en-us/sql/relational-databases/native-client/features/using-multiple-active-result-sets-mars?view=sql-server-ver15 /// diff --git a/source/Nevermore/Util/DataModificationQueryBuilder.cs b/source/Nevermore/Util/DataModificationQueryBuilder.cs index 190cd40e..715ac438 100644 --- a/source/Nevermore/Util/DataModificationQueryBuilder.cs +++ b/source/Nevermore/Util/DataModificationQueryBuilder.cs @@ -24,9 +24,9 @@ internal class DataModificationQueryBuilder readonly IDocumentMapRegistry mappings; readonly IDocumentSerializer serializer; readonly IRelationalStoreConfiguration configuration; - readonly Func keyAllocator; + readonly Func keyAllocator; - public DataModificationQueryBuilder(IRelationalStoreConfiguration configuration, Func keyAllocator) + public DataModificationQueryBuilder(IRelationalStoreConfiguration configuration, Func keyAllocator) { this.mappings = configuration.DocumentMaps; this.serializer = configuration.DocumentSerializer; @@ -39,7 +39,7 @@ public PreparedCommand PrepareInsert(IReadOnlyList documents, InsertOpti options ??= InsertOptions.Default; var mapping = GetMapping(documents); - if (mapping.IdColumn.IsIdentity && options.CustomAssignedId != null) + if (mapping.IdColumn?.PrimaryKeyHandler is IIdentityPrimaryKeyHandler && options.CustomAssignedId != null) throw new InvalidOperationException($"{nameof(InsertOptions)}.{nameof(InsertOptions.CustomAssignedId)} is not supported for identity Id columns."); var sb = new StringBuilder(); @@ -189,7 +189,7 @@ void AppendInsertStatement(StringBuilder sb, DocumentMap mapping, string tableNa { var columns = new List(); - if (includeDefaultModelColumns && !mapping.IsIdentityId) + if (includeDefaultModelColumns && !(mapping.IdColumn is null) && !mapping.IsIdentityId) columns.Add(mapping.IdColumn.ColumnName); columns.AddRange(mapping.WritableIndexedColumns().Select(c => c.ColumnName)); @@ -266,7 +266,7 @@ void Append(string prefix) sb.AppendLine(outputSelect); } - CommandParameterValues GetDocumentParameters(Func allocateId, object customAssignedId, IReadOnlyList documents, DocumentMap mapping, DataModification dataModification) + CommandParameterValues GetDocumentParameters(Func allocateId, object customAssignedId, IReadOnlyList documents, DocumentMap mapping, DataModification dataModification) { if (documents.Count == 1) return GetDocumentParameters(allocateId, customAssignedId, CustomIdAssignmentBehavior.ThrowIfIdAlreadySetToDifferentValue, documents[0], mapping, dataModification, ""); @@ -293,8 +293,11 @@ enum DataModification Update } - CommandParameterValues GetDocumentParameters(Func allocateId, object customAssignedId, CustomIdAssignmentBehavior? customIdAssignmentBehavior, object document, DocumentMap mapping, DataModification dataModification, string prefix = null) + CommandParameterValues GetDocumentParameters(Func allocateId, object customAssignedId, CustomIdAssignmentBehavior? customIdAssignmentBehavior, object document, DocumentMap mapping, DataModification dataModification, string prefix = null) { + if (mapping.IdColumn is null) + throw new InvalidOperationException($"Map for {mapping.Type.Name} do not specify an Id column"); + var id = mapping.IdColumn.PropertyHandler.Read(document); if (customIdAssignmentBehavior == CustomIdAssignmentBehavior.ThrowIfIdAlreadySetToDifferentValue && @@ -302,7 +305,7 @@ CommandParameterValues GetDocumentParameters(Func allocateI throw new ArgumentException("Do not pass a different Id when one is already set on the document"); //we never want to allocate id's if the Id column is an Identity - if (!mapping.IdColumn.IsIdentity && mapping.IdColumn.Type == typeof(string) && string.IsNullOrWhiteSpace((string) id)) + if (!(mapping.IdColumn.PrimaryKeyHandler is IIdentityPrimaryKeyHandler) && mapping.IdColumn.Type == typeof(string) && string.IsNullOrWhiteSpace((string) id)) { id = string.IsNullOrWhiteSpace(customAssignedId as string) ? allocateId(mapping) : customAssignedId; mapping.IdColumn.PropertyHandler.Write(document, id); From fe08fb63c75c3c3187926e168fa8153a966ebeeb Mon Sep 17 00:00:00 2001 From: slewis74 Date: Mon, 14 Jun 2021 15:51:54 +1000 Subject: [PATCH 02/25] Rounding out the PrimaryKeyHandler support, including Load and AllocateId changes --- source/Nevermore/Advanced/ReadTransaction.cs | 8 ++---- source/Nevermore/IReadQueryExecutor.cs | 13 ++++++++-- source/Nevermore/Mapping/PrimaryKeyHandler.cs | 13 ++++++---- .../Util/DataModificationQueryBuilder.cs | 26 ++++++++++++------- 4 files changed, 38 insertions(+), 22 deletions(-) diff --git a/source/Nevermore/Advanced/ReadTransaction.cs b/source/Nevermore/Advanced/ReadTransaction.cs index c4b13d8d..6029e6f7 100644 --- a/source/Nevermore/Advanced/ReadTransaction.cs +++ b/source/Nevermore/Advanced/ReadTransaction.cs @@ -82,7 +82,7 @@ public async Task OpenAsync(IsolationLevel isolationLevel) } [Pure] - private TDocument Load(TKey id) where TDocument : class + public TDocument Load(TKey id) where TDocument : class { return Stream(PrepareLoad(id)).FirstOrDefault(); } @@ -545,11 +545,7 @@ PreparedCommand PrepareLoad(TKey id) 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 idHandler = configuration.PrimaryKeyHandlerRegistry.Resolve(mapping); - var args = new CommandParameterValues - { - { "Id", idHandler is IPrimitivePrimaryKeyHandler primitive ? primitive.GetPrimitiveValue(id) : id } - }; + var args = new CommandParameterValues {{ "Id", mapping.IdColumn.PrimaryKeyHandler is IPrimitivePrimaryKeyHandler primitive ? primitive.GetPrimitiveValue(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); } diff --git a/source/Nevermore/IReadQueryExecutor.cs b/source/Nevermore/IReadQueryExecutor.cs index e432d87f..357d00b6 100644 --- a/source/Nevermore/IReadQueryExecutor.cs +++ b/source/Nevermore/IReadQueryExecutor.cs @@ -45,6 +45,15 @@ public interface IReadQueryExecutor /// The document, or null if the document is not found. [Pure] TDocument Load(Guid id) where TDocument : class; + /// + /// Loads a single document given its ID. If the item is not found, returns null. + /// + /// The type of document being queried. Results from the database will be mapped to this type. + /// The type of the Id + /// The Id of the document to find. + /// The document, or null if the document is not found. + [Pure] TDocument Load(TKey id) where TDocument : class; + /// /// Loads a single document given its ID. If the item is not found, returns null. /// @@ -523,7 +532,7 @@ public interface IReadQueryExecutor [Pure] IAsyncEnumerable StreamAsync(string query, CommandParameterValues args = null, TimeSpan? commandTimeout = null, CancellationToken cancellationToken = default); /// - /// Executes a query that returns strongly typed documents. + /// Executes a query that returns strongly typed documents. /// /// Everything needed to run the query. /// The type of document being queried. Results from the database will be mapped to this type. @@ -531,7 +540,7 @@ public interface IReadQueryExecutor [Pure] IEnumerable Stream(PreparedCommand preparedCommand); /// - /// Executes a query that returns strongly typed documents. + /// Executes a query that returns strongly typed documents. /// /// Everything needed to run the query. /// Token to use to cancel the command. diff --git a/source/Nevermore/Mapping/PrimaryKeyHandler.cs b/source/Nevermore/Mapping/PrimaryKeyHandler.cs index d261d742..7d258d8d 100644 --- a/source/Nevermore/Mapping/PrimaryKeyHandler.cs +++ b/source/Nevermore/Mapping/PrimaryKeyHandler.cs @@ -7,7 +7,7 @@ namespace Nevermore.Mapping public interface IPrimitivePrimaryKeyHandler : IPrimaryKeyHandler { [return: NotNullIfNotNull("id")] - object? GetPrimitiveValue(TKey id); + object? GetPrimitiveValue(object? id); object FormatKey(string tableName, int key); } @@ -17,7 +17,7 @@ public abstract class PrimitivePrimaryKeyHandler : IPrimitivePrimaryKeyHandle public Type Type => typeof(T); [return: NotNullIfNotNull("id")] - public virtual object? GetPrimitiveValue(TKey id) + public virtual object? GetPrimitiveValue(object? id) { return id; } @@ -25,7 +25,7 @@ public abstract class PrimitivePrimaryKeyHandler : IPrimitivePrimaryKeyHandle public abstract object FormatKey(string tableName, int key); } - public interface IStringBasedPrimitivePrimaryKeyHandler : IPrimaryKeyHandler + public interface IStringBasedPrimitivePrimaryKeyHandler : IPrimitivePrimaryKeyHandler { void SetIdPrefix(Func<(string tableName, int key), string> idPrefix); } @@ -65,9 +65,12 @@ public override object FormatKey(string tableName, int key) } } - class GuidPrimaryKeyHandler : IPrimaryKeyHandler + class GuidPrimaryKeyHandler : PrimitivePrimaryKeyHandler { - public Type Type => typeof(Guid); + public override object FormatKey(string tableName, int key) + { + return key; + } } public interface IIdentityPrimaryKeyHandler : IPrimaryKeyHandler diff --git a/source/Nevermore/Util/DataModificationQueryBuilder.cs b/source/Nevermore/Util/DataModificationQueryBuilder.cs index 715ac438..70762754 100644 --- a/source/Nevermore/Util/DataModificationQueryBuilder.cs +++ b/source/Nevermore/Util/DataModificationQueryBuilder.cs @@ -300,21 +300,29 @@ CommandParameterValues GetDocumentParameters(Func allocateI var id = mapping.IdColumn.PropertyHandler.Read(document); + if (customAssignedId != null && customAssignedId.GetType() != mapping.IdColumn.Type) + throw new ArgumentException($"The given custom Id '{customAssignedId}' must be of type ({mapping.IdColumn.Type.Name}), to match the model's Id property"); + if (customIdAssignmentBehavior == CustomIdAssignmentBehavior.ThrowIfIdAlreadySetToDifferentValue && - customAssignedId != null && id != null && customAssignedId != id) + customAssignedId != null && id != null && !customAssignedId.Equals(id)) throw new ArgumentException("Do not pass a different Id when one is already set on the document"); + var result = new CommandParameterValues(); + //we never want to allocate id's if the Id column is an Identity - if (!(mapping.IdColumn.PrimaryKeyHandler is IIdentityPrimaryKeyHandler) && mapping.IdColumn.Type == typeof(string) && string.IsNullOrWhiteSpace((string) id)) + var keyHandler = configuration.PrimaryKeyHandlerRegistry.Resolve(mapping); + if (keyHandler is IPrimitivePrimaryKeyHandler primitiveKeyHandler) { - id = string.IsNullOrWhiteSpace(customAssignedId as string) ? allocateId(mapping) : customAssignedId; - mapping.IdColumn.PropertyHandler.Write(document, id); - } + // check whether the object's Id has already been provided, if not then we'll either use the one from the InsertOptions or we'll generate one + if (id == null) + { + id = customAssignedId == null || (customAssignedId is string assignedId && string.IsNullOrWhiteSpace(assignedId)) ? allocateId(mapping) : customAssignedId; + mapping.IdColumn.PropertyHandler.Write(document, id); + } - var result = new CommandParameterValues - { - [$"{prefix}{mapping.IdColumn.ColumnName}"] = id - }; + var primitiveValue = primitiveKeyHandler.GetPrimitiveValue(id); + result[$"{prefix}{mapping.IdColumn.ColumnName}"] = primitiveValue; + } switch (mapping.JsonStorageFormat) { From f68a5263d2e7c23f0b2b2ff2d8403ac6b9efb4c4 Mon Sep 17 00:00:00 2001 From: slewis74 Date: Mon, 14 Jun 2021 15:52:40 +1000 Subject: [PATCH 03/25] Updated tests to include a sample of TinyType usage, on the Customer table --- .../Advanced/HooksFixture.cs | 4 +- .../KeyAllocatorFixture.cs | 22 +++---- .../Model/Customer.cs | 18 +++++- .../Model/StringTinyTypeIdTypeHandler.cs | 60 +++++++++++++++++++ .../Model/TinyType.cs | 53 ++++++++++++++++ .../RelationalStoreFixture.cs | 30 +++++----- .../RelationalTransaction/LoadFixture.cs | 12 ++-- .../SetUp/FixtureWithRelationalStore.cs | 4 ++ .../SetUp/SchemaGenerator.cs | 3 +- source/Nevermore.sln.DotSettings | 4 ++ 10 files changed, 174 insertions(+), 36 deletions(-) create mode 100644 source/Nevermore.IntegrationTests/Model/StringTinyTypeIdTypeHandler.cs create mode 100644 source/Nevermore.IntegrationTests/Model/TinyType.cs diff --git a/source/Nevermore.IntegrationTests/Advanced/HooksFixture.cs b/source/Nevermore.IntegrationTests/Advanced/HooksFixture.cs index a9039fa7..9cde393f 100644 --- a/source/Nevermore.IntegrationTests/Advanced/HooksFixture.cs +++ b/source/Nevermore.IntegrationTests/Advanced/HooksFixture.cs @@ -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("C131"); + customer = transaction.Load("C131".ToCustomerId()); transaction.Update(customer); AssertLogged(log, "BeforeUpdate", "AfterUpdate"); diff --git a/source/Nevermore.IntegrationTests/KeyAllocatorFixture.cs b/source/Nevermore.IntegrationTests/KeyAllocatorFixture.cs index 3a307023..9a97ecec 100644 --- a/source/Nevermore.IntegrationTests/KeyAllocatorFixture.cs +++ b/source/Nevermore.IntegrationTests/KeyAllocatorFixture.cs @@ -76,7 +76,7 @@ public void ShouldAllocateInParallel() const int allocationCount = 20; const int threadCount = 10; - var projectIds = new ConcurrentBag(); + var customerIds = new ConcurrentBag(); var deploymentIds = new ConcurrentBag(); var random = new Random(1); @@ -90,21 +90,21 @@ public void ShouldAllocateInParallel() var sequence = random.Next(3); if (sequence == 0) { - var id = transaction.AllocateId(typeof (Customer)); - projectIds.Add(id); + var id = transaction.AllocateId(); + customerIds.Add(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(); // Abandoned Ids are not returned to the pool - projectIds.Add(id); + customerIds.Add(id); transaction.Dispose(); } else if (sequence == 2) { - var id = transaction.AllocateId(typeof(Order)); + var id = transaction.AllocateId(); deploymentIds.Add(id); transaction.Commit(); } @@ -115,21 +115,21 @@ public void ShouldAllocateInParallel() Task.WaitAll(tasks); Func 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) diff --git a/source/Nevermore.IntegrationTests/Model/Customer.cs b/source/Nevermore.IntegrationTests/Model/Customer.cs index b77d27cc..3e4e242b 100644 --- a/source/Nevermore.IntegrationTests/Model/Customer.cs +++ b/source/Nevermore.IntegrationTests/Model/Customer.cs @@ -1,3 +1,4 @@ +#nullable enable using Nevermore.IntegrationTests.Contracts; namespace Nevermore.IntegrationTests.Model @@ -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; } @@ -18,4 +19,19 @@ public Customer() public string ApiKey { get; set; } public string[] Passphrases { get; set; } } + + public class CustomerId : StringTinyType + { + internal CustomerId(string value) : base(value) + { + } + } + + public static class CustomerIdExtensionMethods + { + public static CustomerId? ToCustomerId(this string? value) + { + return string.IsNullOrWhiteSpace(value) ? null : new CustomerId(value); + } + } } \ No newline at end of file diff --git a/source/Nevermore.IntegrationTests/Model/StringTinyTypeIdTypeHandler.cs b/source/Nevermore.IntegrationTests/Model/StringTinyTypeIdTypeHandler.cs new file mode 100644 index 00000000..b77744aa --- /dev/null +++ b/source/Nevermore.IntegrationTests/Model/StringTinyTypeIdTypeHandler.cs @@ -0,0 +1,60 @@ +using System; +using System.Data; +using System.Data.Common; +using Nevermore.Advanced.TypeHandlers; +using Nevermore.Mapping; + +namespace Nevermore.IntegrationTests.Model +{ + class StringTinyTypeIdTypeHandler : ITypeHandler where T : TinyType + { + public bool CanConvert(Type objectType) + { + return objectType == typeof(T); + } + + public object? ReadDatabase(DbDataReader reader, int columnIndex) + { + if (reader.IsDBNull(columnIndex)) return null; + var value = reader.GetString(columnIndex); + if (string.IsNullOrWhiteSpace(value)) return null; + return TinyType.Create(value); + } + + public void WriteDatabase(DbParameter parameter, object value) + { + parameter.DbType = DbType.String; + if (value is T wrapped) + { + parameter.Value = wrapped.Value; + } + else + { + parameter.Value = null; + } + } + } + + class StringTinyTypeIdKeyHandler : IStringBasedPrimitivePrimaryKeyHandler + where T : StringTinyType + { + public Type Type => typeof(T); + + public object? GetPrimitiveValue(object? id) + { + if (!(id is StringTinyType stringTinyType)) + throw new ArgumentException($"Expected the id to be a {typeof(T).Name}"); + return stringTinyType.Value; + } + + public object FormatKey(string tableName, int key) + { + return TinyType.Create($"{tableName}s-{key}"); + } + + public void SetIdPrefix(Func<(string tableName, int key), string> idPrefix) + { + throw new NotImplementedException(); + } + } +} \ No newline at end of file diff --git a/source/Nevermore.IntegrationTests/Model/TinyType.cs b/source/Nevermore.IntegrationTests/Model/TinyType.cs new file mode 100644 index 00000000..ef6f7db0 --- /dev/null +++ b/source/Nevermore.IntegrationTests/Model/TinyType.cs @@ -0,0 +1,53 @@ +using System; +using System.Globalization; +using System.Reflection; + +namespace Nevermore.IntegrationTests.Model +{ + public class TinyType + { + internal TinyType(T value) + { + Value = value; + } + + public T Value { get; } + + public override string ToString() + { + return Value.ToString(); + } + + 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 Value.Equals(((TinyType) obj).Value); + } + + public override int GetHashCode() + { + return (Value != null ? Value.GetHashCode() : 0); + } + + public static TinyType Create(Type tinyType, T value) + { + const BindingFlags bindingFlags = BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic; + var instance = Activator.CreateInstance(tinyType, bindingFlags, null, new object[] {value}, CultureInfo.CurrentCulture); + return (TinyType) instance; + } + + public static TTinyType Create(T value) where TTinyType : TinyType + { + return (TTinyType) Create(typeof(TTinyType), value); + } + } + + public class StringTinyType : TinyType + { + internal StringTinyType(string value) : base(value) + { + } + } +} \ No newline at end of file diff --git a/source/Nevermore.IntegrationTests/RelationalStoreFixture.cs b/source/Nevermore.IntegrationTests/RelationalStoreFixture.cs index 121ca03c..6ccdae5b 100644 --- a/source/Nevermore.IntegrationTests/RelationalStoreFixture.cs +++ b/source/Nevermore.IntegrationTests/RelationalStoreFixture.cs @@ -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(); } @@ -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"}; @@ -94,7 +94,7 @@ public void ShouldHandleIdsWithInOperand() using (var transaction = Store.BeginTransaction()) { var customer = transaction.Query() - .Where("Id", ArraySqlOperand.In, new[] {customerId}) + .Where("Id", ArraySqlOperand.In, new[] {customerId.Value}) .Stream() .Single(); customer.FirstName.Should().Be("Alice"); @@ -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"}}; @@ -243,7 +243,7 @@ public void ShouldPersistAndLoadReferenceCollectionsOnSingleDocuments() } using (var transaction = Store.BeginTransaction()) { - var loadedCustomer = transaction.Load(customerId); + var loadedCustomer = transaction.Load(customerId); loadedCustomer.Roles.Count.Should().Be(2); } } @@ -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"); } } @@ -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"); } } @@ -278,7 +278,7 @@ public void ShouldThrowIfConflictingIdsPassedIn() { Assert.Throws(() => { - 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" }); }); } diff --git a/source/Nevermore.IntegrationTests/RelationalTransaction/LoadFixture.cs b/source/Nevermore.IntegrationTests/RelationalTransaction/LoadFixture.cs index eea2d561..2151cc7e 100644 --- a/source/Nevermore.IntegrationTests/RelationalTransaction/LoadFixture.cs +++ b/source/Nevermore.IntegrationTests/RelationalTransaction/LoadFixture.cs @@ -39,7 +39,7 @@ public void LoadWithMultipleIdsWithDifferentLength() Type = ProductType.Normal }; trn.Insert(product1); - + var product2 = new Product { Id = "Products-133", @@ -49,9 +49,9 @@ public void LoadWithMultipleIdsWithDifferentLength() }; trn.Insert(product2); trn.Commit(); - + var ids = new[] {product1.Id, product2.Id}; - + var products = trn.LoadMany(ids); products.Should().HaveCount(ids.Length); @@ -75,13 +75,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); @@ -202,7 +202,7 @@ public void StoreStringInheritedTypesSerializeCorrectly() var brandToTestSerialization = allBrands.Single(x => x.Name == "Brand A"); brandToTestSerialization.JSON.Should().Be("{\"Description\":\"Details for Brand A.\"}"); } - + [Test] public void StoreAndLoadStringInheritedTypes() { diff --git a/source/Nevermore.IntegrationTests/SetUp/FixtureWithRelationalStore.cs b/source/Nevermore.IntegrationTests/SetUp/FixtureWithRelationalStore.cs index 32244a36..ce648d64 100644 --- a/source/Nevermore.IntegrationTests/SetUp/FixtureWithRelationalStore.cs +++ b/source/Nevermore.IntegrationTests/SetUp/FixtureWithRelationalStore.cs @@ -41,6 +41,10 @@ protected FixtureWithRelationalStore() config.DocumentMaps.Register(documentMaps); config.TypeHandlers.Register(new ReferenceCollectionTypeHandler()); + config.TypeHandlers.Register(new StringTinyTypeIdTypeHandler()); + + config.PrimaryKeyHandlerRegistry.Register(new StringTinyTypeIdKeyHandler()); + config.InstanceTypeResolvers.Register(new ProductTypeResolver()); config.InstanceTypeResolvers.Register(new BrandTypeResolver()); diff --git a/source/Nevermore.IntegrationTests/SetUp/SchemaGenerator.cs b/source/Nevermore.IntegrationTests/SetUp/SchemaGenerator.cs index 86a45b89..0a5d469f 100644 --- a/source/Nevermore.IntegrationTests/SetUp/SchemaGenerator.cs +++ b/source/Nevermore.IntegrationTests/SetUp/SchemaGenerator.cs @@ -1,6 +1,7 @@ using System.Data; using System.Linq; using System.Text; +using Nevermore.IntegrationTests.Model; using Nevermore.Mapping; using Nevermore.Util; @@ -58,7 +59,7 @@ static bool IsNullable(ColumnMapping column) static string GetDatabaseType(ColumnMapping column) { - var dbType = DatabaseTypeConverter.AsDbType(column.Type); + var dbType = typeof(StringTinyType).IsAssignableFrom(column.Type) ? DbType.String : DatabaseTypeConverter.AsDbType(column.Type); switch (dbType) { diff --git a/source/Nevermore.sln.DotSettings b/source/Nevermore.sln.DotSettings index 4fe9f9c8..dee3ac84 100644 --- a/source/Nevermore.sln.DotSettings +++ b/source/Nevermore.sln.DotSettings @@ -4,6 +4,10 @@ <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /> <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /> <Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" /> + True + True + True + True True True True \ No newline at end of file From 22ccb0b57fd7014cbe1d1cd165b2742f78baf9c2 Mon Sep 17 00:00:00 2001 From: slewis74 Date: Mon, 14 Jun 2021 16:14:04 +1000 Subject: [PATCH 04/25] cleanup/split into separate files --- .../Model/Customer.cs | 3 +- .../Model/StringTinyTypeIdKeyHandler.cs | 29 +++++++ .../Model/StringTinyTypeIdTypeHandler.cs | 27 +----- .../Model/TinyType.cs | 19 +++-- .../RelationalStoreFixture.cs | 5 +- .../Mapping/GuidPrimaryKeyHandler.cs | 12 +++ .../Mapping/IIdentityPrimaryKeyHandler.cs | 5 ++ .../Mapping/IPrimitivePrimaryKeyHandler.cs | 13 +++ .../IStringBasedPrimitivePrimaryKeyHandler.cs | 9 ++ .../Mapping/IdentityPrimaryKeyHandler.cs | 9 ++ .../Nevermore/Mapping/IntPrimaryKeyHandler.cs | 10 +++ .../Mapping/LongPrimaryKeyHandler.cs | 10 +++ source/Nevermore/Mapping/PrimaryKeyHandler.cs | 83 ------------------- .../Mapping/PrimitivePrimaryKeyHandler.cs | 19 +++++ .../Mapping/StringPrimaryKeyHandler.cs | 24 ++++++ 15 files changed, 157 insertions(+), 120 deletions(-) create mode 100644 source/Nevermore.IntegrationTests/Model/StringTinyTypeIdKeyHandler.cs create mode 100644 source/Nevermore/Mapping/GuidPrimaryKeyHandler.cs create mode 100644 source/Nevermore/Mapping/IIdentityPrimaryKeyHandler.cs create mode 100644 source/Nevermore/Mapping/IPrimitivePrimaryKeyHandler.cs create mode 100644 source/Nevermore/Mapping/IStringBasedPrimitivePrimaryKeyHandler.cs create mode 100644 source/Nevermore/Mapping/IdentityPrimaryKeyHandler.cs create mode 100644 source/Nevermore/Mapping/IntPrimaryKeyHandler.cs create mode 100644 source/Nevermore/Mapping/LongPrimaryKeyHandler.cs delete mode 100644 source/Nevermore/Mapping/PrimaryKeyHandler.cs create mode 100644 source/Nevermore/Mapping/PrimitivePrimaryKeyHandler.cs create mode 100644 source/Nevermore/Mapping/StringPrimaryKeyHandler.cs diff --git a/source/Nevermore.IntegrationTests/Model/Customer.cs b/source/Nevermore.IntegrationTests/Model/Customer.cs index 3e4e242b..7aae7599 100644 --- a/source/Nevermore.IntegrationTests/Model/Customer.cs +++ b/source/Nevermore.IntegrationTests/Model/Customer.cs @@ -1,4 +1,3 @@ -#nullable enable using Nevermore.IntegrationTests.Contracts; namespace Nevermore.IntegrationTests.Model @@ -27,6 +26,8 @@ internal CustomerId(string value) : base(value) } } +#nullable enable + public static class CustomerIdExtensionMethods { public static CustomerId? ToCustomerId(this string? value) diff --git a/source/Nevermore.IntegrationTests/Model/StringTinyTypeIdKeyHandler.cs b/source/Nevermore.IntegrationTests/Model/StringTinyTypeIdKeyHandler.cs new file mode 100644 index 00000000..ecf3ff08 --- /dev/null +++ b/source/Nevermore.IntegrationTests/Model/StringTinyTypeIdKeyHandler.cs @@ -0,0 +1,29 @@ +#nullable enable +using System; +using Nevermore.Mapping; + +namespace Nevermore.IntegrationTests.Model +{ + class StringTinyTypeIdKeyHandler : IStringBasedPrimitivePrimaryKeyHandler + where T : StringTinyType + { + public Type Type => typeof(T); + + public object? GetPrimitiveValue(object? id) + { + if (!(id is StringTinyType stringTinyType)) + throw new ArgumentException($"Expected the id to be a {typeof(T).Name}"); + return stringTinyType.Value; + } + + public object FormatKey(string tableName, int key) + { + return TinyType.Create($"{tableName}s-{key}")!; + } + + public void SetIdPrefix(Func<(string tableName, int key), string> idPrefix) + { + throw new NotImplementedException(); + } + } +} \ No newline at end of file diff --git a/source/Nevermore.IntegrationTests/Model/StringTinyTypeIdTypeHandler.cs b/source/Nevermore.IntegrationTests/Model/StringTinyTypeIdTypeHandler.cs index b77744aa..5deabc8c 100644 --- a/source/Nevermore.IntegrationTests/Model/StringTinyTypeIdTypeHandler.cs +++ b/source/Nevermore.IntegrationTests/Model/StringTinyTypeIdTypeHandler.cs @@ -1,8 +1,8 @@ -using System; +#nullable enable +using System; using System.Data; using System.Data.Common; using Nevermore.Advanced.TypeHandlers; -using Nevermore.Mapping; namespace Nevermore.IntegrationTests.Model { @@ -34,27 +34,4 @@ public void WriteDatabase(DbParameter parameter, object value) } } } - - class StringTinyTypeIdKeyHandler : IStringBasedPrimitivePrimaryKeyHandler - where T : StringTinyType - { - public Type Type => typeof(T); - - public object? GetPrimitiveValue(object? id) - { - if (!(id is StringTinyType stringTinyType)) - throw new ArgumentException($"Expected the id to be a {typeof(T).Name}"); - return stringTinyType.Value; - } - - public object FormatKey(string tableName, int key) - { - return TinyType.Create($"{tableName}s-{key}"); - } - - public void SetIdPrefix(Func<(string tableName, int key), string> idPrefix) - { - throw new NotImplementedException(); - } - } } \ No newline at end of file diff --git a/source/Nevermore.IntegrationTests/Model/TinyType.cs b/source/Nevermore.IntegrationTests/Model/TinyType.cs index ef6f7db0..b11d84d4 100644 --- a/source/Nevermore.IntegrationTests/Model/TinyType.cs +++ b/source/Nevermore.IntegrationTests/Model/TinyType.cs @@ -1,4 +1,5 @@ -using System; +#nullable enable +using System; using System.Globalization; using System.Reflection; @@ -13,9 +14,9 @@ internal TinyType(T value) public T Value { get; } - public override string ToString() + public override string? ToString() { - return Value.ToString(); + return Value?.ToString(); } public override bool Equals(object? obj) @@ -23,7 +24,7 @@ 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 Value.Equals(((TinyType) obj).Value); + return !(Value is null) && Value.Equals(((TinyType) obj).Value); } public override int GetHashCode() @@ -31,16 +32,16 @@ public override int GetHashCode() return (Value != null ? Value.GetHashCode() : 0); } - public static TinyType Create(Type tinyType, T value) + public static TinyType? Create(Type tinyType, T value) { const BindingFlags bindingFlags = BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic; - var instance = Activator.CreateInstance(tinyType, bindingFlags, null, new object[] {value}, CultureInfo.CurrentCulture); - return (TinyType) instance; + var instance = Activator.CreateInstance(tinyType, bindingFlags, null, new object[] { value! }, CultureInfo.CurrentCulture); + return instance as TinyType; } - public static TTinyType Create(T value) where TTinyType : TinyType + public static TTinyType? Create(T value) where TTinyType : TinyType { - return (TTinyType) Create(typeof(TTinyType), value); + return (TTinyType?) Create(typeof(TTinyType), value); } } diff --git a/source/Nevermore.IntegrationTests/RelationalStoreFixture.cs b/source/Nevermore.IntegrationTests/RelationalStoreFixture.cs index 6ccdae5b..120bc526 100644 --- a/source/Nevermore.IntegrationTests/RelationalStoreFixture.cs +++ b/source/Nevermore.IntegrationTests/RelationalStoreFixture.cs @@ -1,4 +1,5 @@ -using System; +#nullable enable +using System; using System.Linq; using FluentAssertions; using Nevermore.IntegrationTests.Model; @@ -22,7 +23,7 @@ public void ShouldGenerateIdsUnlessExplicitlyAssigned() transaction.Insert(customer2); transaction.Insert(customer3, new InsertOptions { CustomAssignedId = "Customers-Chazza".ToCustomerId() }); - customer1.Id.Value.Should().Be("Customers-Alice"); + customer1.Id!.Value.Should().Be("Customers-Alice"); customer2.Id.Value.Should().StartWith("Customers-"); customer3.Id.Value.Should().Be("Customers-Chazza"); diff --git a/source/Nevermore/Mapping/GuidPrimaryKeyHandler.cs b/source/Nevermore/Mapping/GuidPrimaryKeyHandler.cs new file mode 100644 index 00000000..54364cc6 --- /dev/null +++ b/source/Nevermore/Mapping/GuidPrimaryKeyHandler.cs @@ -0,0 +1,12 @@ +using System; + +namespace Nevermore.Mapping +{ + class GuidPrimaryKeyHandler : PrimitivePrimaryKeyHandler + { + public override object FormatKey(string tableName, int key) + { + return key; + } + } +} \ No newline at end of file diff --git a/source/Nevermore/Mapping/IIdentityPrimaryKeyHandler.cs b/source/Nevermore/Mapping/IIdentityPrimaryKeyHandler.cs new file mode 100644 index 00000000..81e36d99 --- /dev/null +++ b/source/Nevermore/Mapping/IIdentityPrimaryKeyHandler.cs @@ -0,0 +1,5 @@ +namespace Nevermore.Mapping +{ + public interface IIdentityPrimaryKeyHandler : IPrimaryKeyHandler + {} +} \ No newline at end of file diff --git a/source/Nevermore/Mapping/IPrimitivePrimaryKeyHandler.cs b/source/Nevermore/Mapping/IPrimitivePrimaryKeyHandler.cs new file mode 100644 index 00000000..d90a8055 --- /dev/null +++ b/source/Nevermore/Mapping/IPrimitivePrimaryKeyHandler.cs @@ -0,0 +1,13 @@ +#nullable enable +using System.Diagnostics.CodeAnalysis; + +namespace Nevermore.Mapping +{ + public interface IPrimitivePrimaryKeyHandler : IPrimaryKeyHandler + { + [return: NotNullIfNotNull("id")] + object? GetPrimitiveValue(object? id); + + object FormatKey(string tableName, int key); + } +} \ No newline at end of file diff --git a/source/Nevermore/Mapping/IStringBasedPrimitivePrimaryKeyHandler.cs b/source/Nevermore/Mapping/IStringBasedPrimitivePrimaryKeyHandler.cs new file mode 100644 index 00000000..3c2aef10 --- /dev/null +++ b/source/Nevermore/Mapping/IStringBasedPrimitivePrimaryKeyHandler.cs @@ -0,0 +1,9 @@ +using System; + +namespace Nevermore.Mapping +{ + public interface IStringBasedPrimitivePrimaryKeyHandler : IPrimitivePrimaryKeyHandler + { + void SetIdPrefix(Func<(string tableName, int key), string> idPrefix); + } +} \ No newline at end of file diff --git a/source/Nevermore/Mapping/IdentityPrimaryKeyHandler.cs b/source/Nevermore/Mapping/IdentityPrimaryKeyHandler.cs new file mode 100644 index 00000000..54dd1da9 --- /dev/null +++ b/source/Nevermore/Mapping/IdentityPrimaryKeyHandler.cs @@ -0,0 +1,9 @@ +using System; + +namespace Nevermore.Mapping +{ + public class IdentityPrimaryKeyHandler : IIdentityPrimaryKeyHandler + { + public Type Type => typeof(T); + } +} \ No newline at end of file diff --git a/source/Nevermore/Mapping/IntPrimaryKeyHandler.cs b/source/Nevermore/Mapping/IntPrimaryKeyHandler.cs new file mode 100644 index 00000000..8762136b --- /dev/null +++ b/source/Nevermore/Mapping/IntPrimaryKeyHandler.cs @@ -0,0 +1,10 @@ +namespace Nevermore.Mapping +{ + class IntPrimaryKeyHandler : PrimitivePrimaryKeyHandler + { + public override object FormatKey(string tableName, int key) + { + return key; + } + } +} \ No newline at end of file diff --git a/source/Nevermore/Mapping/LongPrimaryKeyHandler.cs b/source/Nevermore/Mapping/LongPrimaryKeyHandler.cs new file mode 100644 index 00000000..2acc0327 --- /dev/null +++ b/source/Nevermore/Mapping/LongPrimaryKeyHandler.cs @@ -0,0 +1,10 @@ +namespace Nevermore.Mapping +{ + class LongPrimaryKeyHandler : PrimitivePrimaryKeyHandler + { + public override object FormatKey(string tableName, int key) + { + return key; + } + } +} \ No newline at end of file diff --git a/source/Nevermore/Mapping/PrimaryKeyHandler.cs b/source/Nevermore/Mapping/PrimaryKeyHandler.cs deleted file mode 100644 index 7d258d8d..00000000 --- a/source/Nevermore/Mapping/PrimaryKeyHandler.cs +++ /dev/null @@ -1,83 +0,0 @@ -#nullable enable -using System; -using System.Diagnostics.CodeAnalysis; - -namespace Nevermore.Mapping -{ - public interface IPrimitivePrimaryKeyHandler : IPrimaryKeyHandler - { - [return: NotNullIfNotNull("id")] - object? GetPrimitiveValue(object? id); - - object FormatKey(string tableName, int key); - } - - public abstract class PrimitivePrimaryKeyHandler : IPrimitivePrimaryKeyHandler - { - public Type Type => typeof(T); - - [return: NotNullIfNotNull("id")] - public virtual object? GetPrimitiveValue(object? id) - { - return id; - } - - public abstract object FormatKey(string tableName, int key); - } - - public interface IStringBasedPrimitivePrimaryKeyHandler : IPrimitivePrimaryKeyHandler - { - void SetIdPrefix(Func<(string tableName, int key), string> idPrefix); - } - - class StringPrimaryKeyHandler : PrimitivePrimaryKeyHandler, IStringBasedPrimitivePrimaryKeyHandler - { - Func<(string tableName, int key), string> idPrefixFunc; - public StringPrimaryKeyHandler(Func<(string tableName, int key), string>? idPrefix = null) - { - idPrefixFunc = idPrefix ?? (x => $"{x.tableName}s-{x.key}"); - } - - public void SetIdPrefix(Func<(string tableName, int key), string> idPrefix) - { - idPrefixFunc = idPrefix; - } - - public override object FormatKey(string tableName, int key) - { - return idPrefixFunc((tableName, key)); - } - } - - class IntPrimaryKeyHandler : PrimitivePrimaryKeyHandler - { - public override object FormatKey(string tableName, int key) - { - return key; - } - } - - class LongPrimaryKeyHandler : PrimitivePrimaryKeyHandler - { - public override object FormatKey(string tableName, int key) - { - return key; - } - } - - class GuidPrimaryKeyHandler : PrimitivePrimaryKeyHandler - { - public override object FormatKey(string tableName, int key) - { - return key; - } - } - - public interface IIdentityPrimaryKeyHandler : IPrimaryKeyHandler - {} - - public class IdentityPrimaryKeyHandler : IIdentityPrimaryKeyHandler - { - public Type Type => typeof(T); - } -} \ No newline at end of file diff --git a/source/Nevermore/Mapping/PrimitivePrimaryKeyHandler.cs b/source/Nevermore/Mapping/PrimitivePrimaryKeyHandler.cs new file mode 100644 index 00000000..c838dc95 --- /dev/null +++ b/source/Nevermore/Mapping/PrimitivePrimaryKeyHandler.cs @@ -0,0 +1,19 @@ +#nullable enable +using System; +using System.Diagnostics.CodeAnalysis; + +namespace Nevermore.Mapping +{ + public abstract class PrimitivePrimaryKeyHandler : IPrimitivePrimaryKeyHandler + { + public Type Type => typeof(T); + + [return: NotNullIfNotNull("id")] + public virtual object? GetPrimitiveValue(object? id) + { + return id; + } + + public abstract object FormatKey(string tableName, int key); + } +} \ No newline at end of file diff --git a/source/Nevermore/Mapping/StringPrimaryKeyHandler.cs b/source/Nevermore/Mapping/StringPrimaryKeyHandler.cs new file mode 100644 index 00000000..b0cdacee --- /dev/null +++ b/source/Nevermore/Mapping/StringPrimaryKeyHandler.cs @@ -0,0 +1,24 @@ +#nullable enable +using System; + +namespace Nevermore.Mapping +{ + class StringPrimaryKeyHandler : PrimitivePrimaryKeyHandler, IStringBasedPrimitivePrimaryKeyHandler + { + Func<(string tableName, int key), string> idPrefixFunc; + public StringPrimaryKeyHandler(Func<(string tableName, int key), string>? idPrefix = null) + { + idPrefixFunc = idPrefix ?? (x => $"{x.tableName}s-{x.key}"); + } + + public void SetIdPrefix(Func<(string tableName, int key), string> idPrefix) + { + idPrefixFunc = idPrefix; + } + + public override object FormatKey(string tableName, int key) + { + return idPrefixFunc((tableName, key)); + } + } +} \ No newline at end of file From 1106bc181c069d794bdfffe64ece660e0d89620b Mon Sep 17 00:00:00 2001 From: slewis74 Date: Mon, 14 Jun 2021 20:38:41 +1000 Subject: [PATCH 05/25] Adjustments to how the Id prefix can be set, as well as the Format --- .../Model/StringTinyTypeIdKeyHandler.cs | 7 +++- .../Mapping/IIdColumnMappingBuilder.cs | 17 +++++++- .../IStringBasedPrimitivePrimaryKeyHandler.cs | 12 +++++- source/Nevermore/Mapping/IdColumnMapping.cs | 40 +++++++++++++++++-- .../Mapping/StringPrimaryKeyHandler.cs | 25 +++++++++--- 5 files changed, 90 insertions(+), 11 deletions(-) diff --git a/source/Nevermore.IntegrationTests/Model/StringTinyTypeIdKeyHandler.cs b/source/Nevermore.IntegrationTests/Model/StringTinyTypeIdKeyHandler.cs index ecf3ff08..b3a6ae57 100644 --- a/source/Nevermore.IntegrationTests/Model/StringTinyTypeIdKeyHandler.cs +++ b/source/Nevermore.IntegrationTests/Model/StringTinyTypeIdKeyHandler.cs @@ -21,7 +21,12 @@ public object FormatKey(string tableName, int key) return TinyType.Create($"{tableName}s-{key}")!; } - public void SetIdPrefix(Func<(string tableName, int key), string> idPrefix) + public void SetPrefix(Func idPrefix) + { + throw new NotImplementedException(); + } + + public void SetFormat(Func<(string idPrefix, int key), string> format) { throw new NotImplementedException(); } diff --git a/source/Nevermore/Mapping/IIdColumnMappingBuilder.cs b/source/Nevermore/Mapping/IIdColumnMappingBuilder.cs index 339fdcfb..ec720cc4 100644 --- a/source/Nevermore/Mapping/IIdColumnMappingBuilder.cs +++ b/source/Nevermore/Mapping/IIdColumnMappingBuilder.cs @@ -10,8 +10,23 @@ public interface IIdColumnMappingBuilder : IColumnMappingBuilder /// This will also reset the PropertyHandler IIdColumnMappingBuilder Identity(); + /// + /// Explicitly set a primary key handler. + /// + /// The primary key handler. + /// IIdColumnMappingBuilder KeyHandler(IPrimaryKeyHandler primaryKeyHandler); - IIdColumnMappingBuilder IdPrefix(Func<(string tableName, int key), string> idPrefix); + /// + /// Set a function that when given the TableName will return key prefix string. + /// + /// The function to call back to get the prefix. + IIdColumnMappingBuilder Prefix(Func idPrefix); + + /// + /// Set a function that format a key value, given a prefix and a key number. + /// + /// The function to call back to format the id. + IIdColumnMappingBuilder Format(Func<(string idPrefix, int key), string> format); } } \ No newline at end of file diff --git a/source/Nevermore/Mapping/IStringBasedPrimitivePrimaryKeyHandler.cs b/source/Nevermore/Mapping/IStringBasedPrimitivePrimaryKeyHandler.cs index 3c2aef10..4d2ee46a 100644 --- a/source/Nevermore/Mapping/IStringBasedPrimitivePrimaryKeyHandler.cs +++ b/source/Nevermore/Mapping/IStringBasedPrimitivePrimaryKeyHandler.cs @@ -4,6 +4,16 @@ namespace Nevermore.Mapping { public interface IStringBasedPrimitivePrimaryKeyHandler : IPrimitivePrimaryKeyHandler { - void SetIdPrefix(Func<(string tableName, int key), string> idPrefix); + /// + /// Set a function that when given the TableName will return key prefix string. + /// + /// The function to call back to get the prefix. + void SetPrefix(Func idPrefix); + + /// + /// Set a function that format a key value, given a prefix and a key number. + /// + /// The function to call back to format the id. + void SetFormat(Func<(string idPrefix, int key), string> format); } } \ No newline at end of file diff --git a/source/Nevermore/Mapping/IdColumnMapping.cs b/source/Nevermore/Mapping/IdColumnMapping.cs index 817969e2..b07779bb 100644 --- a/source/Nevermore/Mapping/IdColumnMapping.cs +++ b/source/Nevermore/Mapping/IdColumnMapping.cs @@ -61,12 +61,46 @@ public IIdColumnMappingBuilder KeyHandler(IPrimaryKeyHandler primaryKeyHandler) return this; } - public IIdColumnMappingBuilder IdPrefix(Func<(string tableName, int key), string> idPrefix) + /// + /// Set a function that when given the TableName will return key prefix string. + /// + /// The function to call back to get the prefix. + public IIdColumnMappingBuilder Prefix(Func idPrefix) { if (!(PrimaryKeyHandler is null) && Direction == ColumnDirection.FromDatabase && PrimaryKeyHandler is IIdentityPrimaryKeyHandler) - throw new InvalidOperationException($"{nameof(IdPrefix)} cannot be set when an identity key handler has been configured."); + throw new InvalidOperationException($"{nameof(Prefix)} cannot be set when an identity key handler has been configured."); - return KeyHandler(new StringPrimaryKeyHandler(idPrefix)); + if (PrimaryKeyHandler == null) + return KeyHandler(new StringPrimaryKeyHandler(idPrefix)); + + if (PrimaryKeyHandler is IStringBasedPrimitivePrimaryKeyHandler stringIdHandler) + { + stringIdHandler.SetPrefix(idPrefix); + return this; + } + + throw new InvalidOperationException($"Cannot set the Id prefix when the PrimaryKeyHandler is of type {PrimaryKeyHandler.GetType().Name}"); + } + + /// + /// Set a function that format a key value, given a prefix and a key number. + /// + /// The function to call back to format the id. + public IIdColumnMappingBuilder Format(Func<(string idPrefix, int key), string> format) + { + if (!(PrimaryKeyHandler is null) && Direction == ColumnDirection.FromDatabase && PrimaryKeyHandler is IIdentityPrimaryKeyHandler) + throw new InvalidOperationException($"{nameof(Format)} cannot be set when an identity key handler has been configured."); + + if (PrimaryKeyHandler == null) + return KeyHandler(new StringPrimaryKeyHandler(format: format)); + + if (PrimaryKeyHandler is IStringBasedPrimitivePrimaryKeyHandler stringIdHandler) + { + stringIdHandler.SetFormat(format); + return this; + } + + throw new InvalidOperationException($"Cannot set the key format when the PrimaryKeyHandler is of type {PrimaryKeyHandler.GetType().Name}"); } protected override void SetCustomPropertyHandler(IPropertyHandler propertyHandler) diff --git a/source/Nevermore/Mapping/StringPrimaryKeyHandler.cs b/source/Nevermore/Mapping/StringPrimaryKeyHandler.cs index b0cdacee..6d1190fb 100644 --- a/source/Nevermore/Mapping/StringPrimaryKeyHandler.cs +++ b/source/Nevermore/Mapping/StringPrimaryKeyHandler.cs @@ -5,20 +5,35 @@ namespace Nevermore.Mapping { class StringPrimaryKeyHandler : PrimitivePrimaryKeyHandler, IStringBasedPrimitivePrimaryKeyHandler { - Func<(string tableName, int key), string> idPrefixFunc; - public StringPrimaryKeyHandler(Func<(string tableName, int key), string>? idPrefix = null) + Func idPrefixFunc; + Func<(string idPrefix, int key), string> formatFunc; + public StringPrimaryKeyHandler(Func? idPrefix = null, Func<(string idPrefix, int key), string>? format = null) { - idPrefixFunc = idPrefix ?? (x => $"{x.tableName}s-{x.key}"); + idPrefixFunc = idPrefix ?? (x => $"{x}s"); + formatFunc = format ?? (x => $"{x.idPrefix}-{x.key}"); } - public void SetIdPrefix(Func<(string tableName, int key), string> idPrefix) + /// + /// Set a function that when given the TableName will return key prefix string. + /// + /// The function to call back to get the prefix. + public void SetPrefix(Func idPrefix) { idPrefixFunc = idPrefix; } + /// + /// Set a function that format a key value, given a prefix and a key number. + /// + /// The function to call back to format the id. + public void SetFormat(Func<(string idPrefix, int key), string> format) + { + formatFunc = format; + } + public override object FormatKey(string tableName, int key) { - return idPrefixFunc((tableName, key)); + return formatFunc((idPrefixFunc(tableName), key)); } } } \ No newline at end of file From e419ff12788c48d410e7a3b27f88855939e8342f Mon Sep 17 00:00:00 2001 From: slewis74 Date: Tue, 15 Jun 2021 08:55:07 +1000 Subject: [PATCH 06/25] renamed PrimaryKeyHandlerRegistry property to PrimaryKeyHandlers, for consistency with the other property names --- .../SetUp/FixtureWithRelationalStore.cs | 2 +- source/Nevermore/Advanced/WriteTransaction.cs | 4 ++-- source/Nevermore/IRelationalStoreConfiguration.cs | 2 +- source/Nevermore/RelationalStoreConfiguration.cs | 12 ++++++------ .../Nevermore/Util/DataModificationQueryBuilder.cs | 2 +- 5 files changed, 11 insertions(+), 11 deletions(-) diff --git a/source/Nevermore.IntegrationTests/SetUp/FixtureWithRelationalStore.cs b/source/Nevermore.IntegrationTests/SetUp/FixtureWithRelationalStore.cs index ce648d64..6da759d3 100644 --- a/source/Nevermore.IntegrationTests/SetUp/FixtureWithRelationalStore.cs +++ b/source/Nevermore.IntegrationTests/SetUp/FixtureWithRelationalStore.cs @@ -43,7 +43,7 @@ protected FixtureWithRelationalStore() config.TypeHandlers.Register(new ReferenceCollectionTypeHandler()); config.TypeHandlers.Register(new StringTinyTypeIdTypeHandler()); - config.PrimaryKeyHandlerRegistry.Register(new StringTinyTypeIdKeyHandler()); + config.PrimaryKeyHandlers.Register(new StringTinyTypeIdKeyHandler()); config.InstanceTypeResolvers.Register(new ProductTypeResolver()); config.InstanceTypeResolvers.Register(new BrandTypeResolver()); diff --git a/source/Nevermore/Advanced/WriteTransaction.cs b/source/Nevermore/Advanced/WriteTransaction.cs index 2dae3016..1f72a97d 100644 --- a/source/Nevermore/Advanced/WriteTransaction.cs +++ b/source/Nevermore/Advanced/WriteTransaction.cs @@ -222,7 +222,7 @@ public TKey AllocateId() TKey AllocateIdForMapping(DocumentMap mapping) { - var handler = configuration.PrimaryKeyHandlerRegistry.Resolve(mapping); + var handler = configuration.PrimaryKeyHandlers.Resolve(mapping); if (handler == null || !(handler is IPrimitivePrimaryKeyHandler primitivePrimaryKeyHandler)) throw new InvalidOperationException($"Primary key handler could not be resolved for type {mapping.Type}, or it is configured to use an identity key handler."); @@ -234,7 +234,7 @@ TKey AllocateIdForMapping(DocumentMap mapping) object AllocateId(DocumentMap mapping) { - var handler = configuration.PrimaryKeyHandlerRegistry.Resolve(mapping); + var handler = configuration.PrimaryKeyHandlers.Resolve(mapping); if (handler == null || !(handler is IPrimitivePrimaryKeyHandler primitivePrimaryKeyHandler)) throw new InvalidOperationException($"Primary key handler could not be resolved for type {mapping.Type}, or it is configured to use an identity key handler."); diff --git a/source/Nevermore/IRelationalStoreConfiguration.cs b/source/Nevermore/IRelationalStoreConfiguration.cs index 5d34c7fe..3dc27e3a 100644 --- a/source/Nevermore/IRelationalStoreConfiguration.cs +++ b/source/Nevermore/IRelationalStoreConfiguration.cs @@ -32,7 +32,7 @@ public interface IRelationalStoreConfiguration IReaderStrategyRegistry ReaderStrategies { get; } ITypeHandlerRegistry TypeHandlers { get; } IInstanceTypeRegistry InstanceTypeResolvers { get; } - IPrimaryKeyHandlerRegistry PrimaryKeyHandlerRegistry { get; } + IPrimaryKeyHandlerRegistry PrimaryKeyHandlers { get; } IRelatedDocumentStore RelatedDocumentStore { get; set; } IQueryLogger QueryLogger { get; set; } diff --git a/source/Nevermore/RelationalStoreConfiguration.cs b/source/Nevermore/RelationalStoreConfiguration.cs index 623b979a..08bbaf4f 100644 --- a/source/Nevermore/RelationalStoreConfiguration.cs +++ b/source/Nevermore/RelationalStoreConfiguration.cs @@ -46,11 +46,11 @@ public RelationalStoreConfiguration(Func connectionStringFunc) TypeHandlers = new TypeHandlerRegistry(); - PrimaryKeyHandlerRegistry = new PrimaryKeyHandlerRegistry(); - PrimaryKeyHandlerRegistry.Register(new StringPrimaryKeyHandler()); - PrimaryKeyHandlerRegistry.Register(new IntPrimaryKeyHandler()); - PrimaryKeyHandlerRegistry.Register(new LongPrimaryKeyHandler()); - PrimaryKeyHandlerRegistry.Register(new GuidPrimaryKeyHandler()); + PrimaryKeyHandlers = new PrimaryKeyHandlerRegistry(); + PrimaryKeyHandlers.Register(new StringPrimaryKeyHandler()); + PrimaryKeyHandlers.Register(new IntPrimaryKeyHandler()); + PrimaryKeyHandlers.Register(new LongPrimaryKeyHandler()); + PrimaryKeyHandlers.Register(new GuidPrimaryKeyHandler()); AllowSynchronousOperations = true; @@ -87,7 +87,7 @@ public RelationalStoreConfiguration(Func connectionStringFunc) public ITypeHandlerRegistry TypeHandlers { get; } public IInstanceTypeRegistry InstanceTypeResolvers { get; } - public IPrimaryKeyHandlerRegistry PrimaryKeyHandlerRegistry { get; } + public IPrimaryKeyHandlerRegistry PrimaryKeyHandlers { get; } /// /// MARS: https://docs.microsoft.com/en-us/sql/relational-databases/native-client/features/using-multiple-active-result-sets-mars?view=sql-server-ver15 diff --git a/source/Nevermore/Util/DataModificationQueryBuilder.cs b/source/Nevermore/Util/DataModificationQueryBuilder.cs index 70762754..c1ebff35 100644 --- a/source/Nevermore/Util/DataModificationQueryBuilder.cs +++ b/source/Nevermore/Util/DataModificationQueryBuilder.cs @@ -310,7 +310,7 @@ CommandParameterValues GetDocumentParameters(Func allocateI var result = new CommandParameterValues(); //we never want to allocate id's if the Id column is an Identity - var keyHandler = configuration.PrimaryKeyHandlerRegistry.Resolve(mapping); + var keyHandler = configuration.PrimaryKeyHandlers.Resolve(mapping); if (keyHandler is IPrimitivePrimaryKeyHandler primitiveKeyHandler) { // check whether the object's Id has already been provided, if not then we'll either use the one from the InsertOptions or we'll generate one From 1061a4c4c5956f243db7d74cd4a36e2c6c200328 Mon Sep 17 00:00:00 2001 From: slewis74 Date: Tue, 15 Jun 2021 09:08:37 +1000 Subject: [PATCH 07/25] Add ability to get the key prefix from the configuration, Octopus server uses this a lot for related documents determination --- .../Model/StringTinyTypeIdKeyHandler.cs | 7 ++++++- .../Mapping/IStringBasedPrimitivePrimaryKeyHandler.cs | 7 +++++++ source/Nevermore/Mapping/StringPrimaryKeyHandler.cs | 7 ++++++- 3 files changed, 19 insertions(+), 2 deletions(-) diff --git a/source/Nevermore.IntegrationTests/Model/StringTinyTypeIdKeyHandler.cs b/source/Nevermore.IntegrationTests/Model/StringTinyTypeIdKeyHandler.cs index b3a6ae57..072de53f 100644 --- a/source/Nevermore.IntegrationTests/Model/StringTinyTypeIdKeyHandler.cs +++ b/source/Nevermore.IntegrationTests/Model/StringTinyTypeIdKeyHandler.cs @@ -18,7 +18,7 @@ class StringTinyTypeIdKeyHandler : IStringBasedPrimitivePrimaryKeyHandler public object FormatKey(string tableName, int key) { - return TinyType.Create($"{tableName}s-{key}")!; + return TinyType.Create($"{GetPrefix(tableName)}-{key}")!; } public void SetPrefix(Func idPrefix) @@ -26,6 +26,11 @@ public void SetPrefix(Func idPrefix) throw new NotImplementedException(); } + public string GetPrefix(string tableName) + { + return $"{tableName}s"; + } + public void SetFormat(Func<(string idPrefix, int key), string> format) { throw new NotImplementedException(); diff --git a/source/Nevermore/Mapping/IStringBasedPrimitivePrimaryKeyHandler.cs b/source/Nevermore/Mapping/IStringBasedPrimitivePrimaryKeyHandler.cs index 4d2ee46a..29d4402b 100644 --- a/source/Nevermore/Mapping/IStringBasedPrimitivePrimaryKeyHandler.cs +++ b/source/Nevermore/Mapping/IStringBasedPrimitivePrimaryKeyHandler.cs @@ -10,6 +10,13 @@ public interface IStringBasedPrimitivePrimaryKeyHandler : IPrimitivePrimaryKeyHa /// The function to call back to get the prefix. void SetPrefix(Func idPrefix); + /// + /// Given a tableName, get the prefix for the key. + /// + /// + /// The key prefix for the given tableName + string GetPrefix(string tableName); + /// /// Set a function that format a key value, given a prefix and a key number. /// diff --git a/source/Nevermore/Mapping/StringPrimaryKeyHandler.cs b/source/Nevermore/Mapping/StringPrimaryKeyHandler.cs index 6d1190fb..2701de9d 100644 --- a/source/Nevermore/Mapping/StringPrimaryKeyHandler.cs +++ b/source/Nevermore/Mapping/StringPrimaryKeyHandler.cs @@ -22,6 +22,11 @@ public void SetPrefix(Func idPrefix) idPrefixFunc = idPrefix; } + public string GetPrefix(string tableName) + { + return idPrefixFunc(tableName); + } + /// /// Set a function that format a key value, given a prefix and a key number. /// @@ -33,7 +38,7 @@ public void SetFormat(Func<(string idPrefix, int key), string> format) public override object FormatKey(string tableName, int key) { - return formatFunc((idPrefixFunc(tableName), key)); + return formatFunc((GetPrefix(tableName), key)); } } } \ No newline at end of file From 72d171e75469ec0145ea6c8613008f0a0f2e3816 Mon Sep 17 00:00:00 2001 From: slewis74 Date: Thu, 17 Jun 2021 22:14:14 +1000 Subject: [PATCH 08/25] Including more of the methods that allow TKey when loading documents --- source/Nevermore/Advanced/ReadTransaction.cs | 60 +++++++-- source/Nevermore/IReadQueryExecutor.cs | 122 +++++++++++++++++++ 2 files changed, 173 insertions(+), 9 deletions(-) diff --git a/source/Nevermore/Advanced/ReadTransaction.cs b/source/Nevermore/Advanced/ReadTransaction.cs index 6029e6f7..d6d54857 100644 --- a/source/Nevermore/Advanced/ReadTransaction.cs +++ b/source/Nevermore/Advanced/ReadTransaction.cs @@ -99,7 +99,8 @@ public TDocument Load(TKey id) where TDocument : class [Pure] public TDocument Load(Guid id) where TDocument : class => Load(id); - private async Task LoadAsync(TKey id, CancellationToken cancellationToken = default) where TDocument : class + [Pure] + public async Task LoadAsync(TKey id, CancellationToken cancellationToken = default) where TDocument : class { var results = StreamAsync(PrepareLoad(id), cancellationToken); await foreach (var row in results.WithCancellation(cancellationToken)) @@ -123,35 +124,48 @@ public Task LoadAsync(long id, CancellationToken cancellat public Task LoadAsync(Guid id, CancellationToken cancellationToken = default) where TDocument : class => LoadAsync(id, cancellationToken); - private List LoadMany(IEnumerable ids) where TDocument : class + [Pure] + public List LoadMany(params TKey[] ids) where TDocument : class + => LoadStream(ids).ToList(); + + [Pure] + public List LoadMany(IEnumerable ids) where TDocument : class => LoadStream(ids).ToList(); + [Pure] public List LoadMany(params string[] ids) where TDocument : class => LoadStream(ids).ToList(); + [Pure] public List LoadMany(params int[] ids) where TDocument : class => LoadStream(ids).ToList(); + [Pure] public List LoadMany(params long[] ids) where TDocument : class => LoadStream(ids).ToList(); + [Pure] public List LoadMany(params Guid[] ids) where TDocument : class => LoadStream(ids).ToList(); + [Pure] public List LoadMany(IEnumerable ids) where TDocument : class => LoadStream(ids).ToList(); + [Pure] public List LoadMany(IEnumerable ids) where TDocument : class => LoadStream(ids).ToList(); + [Pure] public List LoadMany(IEnumerable ids) where TDocument : class => LoadStream(ids).ToList(); + [Pure] public List LoadMany(IEnumerable ids) where TDocument : class => LoadStream(ids).ToList(); [Pure] - private async Task> LoadManyAsync(IEnumerable ids, CancellationToken cancellationToken = default) where TDocument : class + public async Task> LoadManyAsync(IEnumerable ids, CancellationToken cancellationToken = default) where TDocument : class { var results = new List(); await foreach (var item in LoadStreamAsync(ids, cancellationToken)) @@ -179,7 +193,7 @@ public Task> LoadManyAsync(IEnumerable ids, Can => LoadManyAsync(ids, cancellationToken); [Pure] - private TDocument LoadRequired(TKey id) where TDocument : class + public TDocument LoadRequired(TKey id) where TDocument : class { var result = Load(id); if (result == null) @@ -204,7 +218,7 @@ public TDocument LoadRequired(Guid id) where TDocument : class => LoadRequired(id); [Pure] - private async Task LoadRequiredAsync(TKey id, CancellationToken cancellationToken = default) where TDocument : class + public async Task LoadRequiredAsync(TKey id, CancellationToken cancellationToken = default) where TDocument : class { var result = await LoadAsync(id, cancellationToken); if (result == null) @@ -228,7 +242,15 @@ public Task LoadRequiredAsync(long id, CancellationToken c public Task LoadRequiredAsync(Guid id, CancellationToken cancellationToken = default) where TDocument : class => LoadRequiredAsync(id, cancellationToken); - private List LoadManyRequired(IEnumerable ids) where TDocument : class + [Pure] + public List LoadManyRequired(params TKey[] ids) where TDocument : class + => LoadManyRequiredInternal(ids); + + [Pure] + public List LoadManyRequired(IEnumerable ids) where TDocument : class + => LoadManyRequiredInternal(ids); + + List LoadManyRequiredInternal(IEnumerable ids) where TDocument : class { var idList = ids.Distinct().ToList(); var results = LoadMany(idList); @@ -241,31 +263,40 @@ private List LoadManyRequired(IEnumerable ids) return results; } + [Pure] public List LoadManyRequired(params string[] ids) where TDocument : class => LoadManyRequired(ids); + [Pure] public List LoadManyRequired(params int[] ids) where TDocument : class => LoadManyRequired(ids); + [Pure] public List LoadManyRequired(params long[] ids) where TDocument : class => LoadManyRequired(ids); + [Pure] public List LoadManyRequired(params Guid[] ids) where TDocument : class => LoadManyRequired(ids); + [Pure] public List LoadManyRequired(IEnumerable ids) where TDocument : class => LoadManyRequired(ids); + [Pure] public List LoadManyRequired(IEnumerable ids) where TDocument : class => LoadManyRequired(ids); + [Pure] public List LoadManyRequired(IEnumerable ids) where TDocument : class => LoadManyRequired(ids); + [Pure] public List LoadManyRequired(IEnumerable ids) where TDocument : class => LoadManyRequired(ids); - private async Task> LoadManyRequiredAsync(IEnumerable ids, CancellationToken cancellationToken = default) where TDocument : class + [Pure] + public async Task> LoadManyRequiredAsync(IEnumerable ids, CancellationToken cancellationToken = default) where TDocument : class { var idList = ids.Distinct().ToArray(); var results = await LoadManyAsync(idList, cancellationToken); @@ -278,20 +309,31 @@ private async Task> LoadManyRequiredAsync(IEnum return results; } + [Pure] public Task> LoadManyRequiredAsync(IEnumerable ids, CancellationToken cancellationToken = default) where TDocument : class => LoadManyRequiredAsync(ids, cancellationToken); + [Pure] public Task> LoadManyRequiredAsync(IEnumerable ids, CancellationToken cancellationToken = default) where TDocument : class => LoadManyRequiredAsync(ids, cancellationToken); + [Pure] public Task> LoadManyRequiredAsync(IEnumerable ids, CancellationToken cancellationToken = default) where TDocument : class => LoadManyRequiredAsync(ids, cancellationToken); + [Pure] public Task> LoadManyRequiredAsync(IEnumerable ids, CancellationToken cancellationToken = default) where TDocument : class => LoadManyRequiredAsync(ids, cancellationToken); [Pure] - private IEnumerable LoadStream(IEnumerable ids) where TDocument : class + public IEnumerable LoadStream(params TKey[] ids) where TDocument : class + => LoadStreamInternal(ids); + + [Pure] + public IEnumerable LoadStream(IEnumerable ids) where TDocument : class + => LoadStreamInternal(ids); + + IEnumerable LoadStreamInternal(IEnumerable ids) where TDocument : class { var idList = ids.Where(id => id != null).Distinct().ToList(); @@ -331,7 +373,7 @@ public IEnumerable LoadStream(params Guid[] ids) where TDo => LoadStream(ids); [Pure] - private async IAsyncEnumerable LoadStreamAsync(IEnumerable ids, [EnumeratorCancellation] CancellationToken cancellationToken = default) where TDocument : class + public async IAsyncEnumerable LoadStreamAsync(IEnumerable ids, [EnumeratorCancellation] CancellationToken cancellationToken = default) where TDocument : class { var idList = ids.Where(id => id != null).Distinct().ToList(); if (idList.Count == 0) diff --git a/source/Nevermore/IReadQueryExecutor.cs b/source/Nevermore/IReadQueryExecutor.cs index 357d00b6..ae226c4c 100644 --- a/source/Nevermore/IReadQueryExecutor.cs +++ b/source/Nevermore/IReadQueryExecutor.cs @@ -90,6 +90,16 @@ public interface IReadQueryExecutor /// The document, or null if the document is not found. [Pure] Task LoadAsync(Guid id, CancellationToken cancellationToken = default) where TDocument : class; + /// + /// Loads a single document given its ID. If the item is not found, returns null. + /// + /// The type of document being queried. Results from the database will be mapped to this type. + /// The type of the Id + /// The Id of the document to find. + /// Token to use to cancel the command. + /// The document, or null if the document is not found. + [Pure] Task LoadAsync(TKey id, CancellationToken cancellationToken = default) where TDocument : class; + /// /// Loads a set of documents by their ID's. Documents that are not found are excluded from the result list (that is, /// the results may contain less items than the number of ID's queried for). @@ -162,6 +172,26 @@ public interface IReadQueryExecutor /// The documents. [Pure] List LoadMany(IEnumerable ids) where TDocument : class; + /// + /// Loads a set of documents by their ID's. Documents that are not found are excluded from the result list (that is, + /// the results may contain less items than the number of ID's queried for). + /// + /// The type of document being queried. Results from the database will be mapped to this type. + /// The primary key type of the document + /// A collection of ID's to query by. + /// The documents. + [Pure] List LoadMany(params TKey[] ids) where TDocument : class; + + /// + /// Loads a set of documents by their ID's. Documents that are not found are excluded from the result list (that is, + /// the results may contain less items than the number of ID's queried for). + /// + /// The type of document being queried. Results from the database will be mapped to this type. + /// The primary key type of the document + /// A collection of ID's to query by. + /// The documents. + [Pure] List LoadMany(IEnumerable ids) where TDocument : class; + /// /// Loads a set of documents by their ID's. Documents that are not found are excluded from the result list (that is, /// the results may contain less items than the number of ID's queried for). @@ -202,6 +232,17 @@ public interface IReadQueryExecutor /// The documents. [Pure] Task> LoadManyAsync(IEnumerable ids, CancellationToken cancellationToken = default) where TDocument : class; + /// + /// Loads a set of documents by their ID's. Documents that are not found are excluded from the result list (that is, + /// the results may contain less items than the number of ID's queried for). + /// + /// The type of document being queried. Results from the database will be mapped to this type. + /// The primary key type of the document + /// A collection of ID's to query by. + /// Token to use to cancel the command. + /// The documents. + [Pure] Task> LoadManyAsync(IEnumerable ids, CancellationToken cancellationToken = default) where TDocument : class; + /// /// Loads a set of documents by their ID's. Documents that are not found are excluded from the result list (that is, /// the results may contain less items than the number of ID's queried for). @@ -274,6 +315,26 @@ public interface IReadQueryExecutor /// The documents as a lazy loaded stream. [Pure] IEnumerable LoadStream(IEnumerable ids) where TDocument : class; + /// + /// Loads a set of documents by their ID's. Documents that are not found are excluded from the result list (that is, + /// the results may contain less items than the number of ID's queried for). + /// + /// The type of document being queried. Results from the database will be mapped to this type. + /// The primary key type of the document + /// A collection of ID's to query by. + /// The documents as a lazy loaded stream. + [Pure] IEnumerable LoadStream(params TKey[] ids) where TDocument : class; + + /// + /// Loads a set of documents by their ID's. Documents that are not found are excluded from the result list (that is, + /// the results may contain less items than the number of ID's queried for). + /// + /// The type of document being queried. Results from the database will be mapped to this type. + /// The primary key type of the document + /// A collection of ID's to query by. + /// The documents as a lazy loaded stream. + [Pure] IEnumerable LoadStream(IEnumerable ids) where TDocument : class; + /// /// Loads a set of documents by their ID's. Documents that are not found are excluded from the result list (that is, /// the results may contain less items than the number of ID's queried for). @@ -314,6 +375,17 @@ public interface IReadQueryExecutor /// The documents as a lazy loaded stream. [Pure] IAsyncEnumerable LoadStreamAsync(IEnumerable ids, CancellationToken cancellationToken = default) where TDocument : class; + /// + /// Loads a set of documents by their ID's. Documents that are not found are excluded from the result list (that is, + /// the results may contain less items than the number of ID's queried for). + /// + /// The type of document being queried. Results from the database will be mapped to this type. + /// The primary key type of the document + /// A collection of ID's to query by. + /// Token to use to cancel the command. + /// The documents as a lazy loaded stream. + [Pure] IAsyncEnumerable LoadStreamAsync(IEnumerable ids, CancellationToken cancellationToken = default) where TDocument : class; + /// /// Loads a single document given its ID. If the item is not found, throws a . /// @@ -346,6 +418,15 @@ public interface IReadQueryExecutor /// The document, or null if the document is not found. [Pure] TDocument LoadRequired(Guid id) where TDocument : class; + /// + /// Loads a single document given its ID. If the item is not found, throws a . + /// + /// The type of document being queried. Results from the database will be mapped to this type. + /// The primary key type of the document + /// The Id of the document to find. + /// The document, or null if the document is not found. + [Pure] TDocument LoadRequired(TKey id) where TDocument : class; + /// /// Loads a single document given its ID. If the item is not found, throws a . /// @@ -382,6 +463,16 @@ public interface IReadQueryExecutor /// The document, or null if the document is not found. [Pure] Task LoadRequiredAsync(Guid id, CancellationToken cancellationToken = default) where TDocument : class; + /// + /// Loads a single document given its ID. If the item is not found, throws a . + /// + /// The type of document being queried. Results from the database will be mapped to this type. + /// The primary key type of the document + /// The Id of the document to find. + /// Token to use to cancel the command. + /// The document, or null if the document is not found. + [Pure] Task LoadRequiredAsync(TKey id, CancellationToken cancellationToken = default) where TDocument : class; + /// /// Loads a set of documents by their ID's. If any of the documents are not found, a /// will be thrown. @@ -454,6 +545,26 @@ public interface IReadQueryExecutor /// The documents. [Pure] List LoadManyRequired(IEnumerable ids) where TDocument : class; + /// + /// Loads a set of documents by their ID's. If any of the documents are not found, a + /// will be thrown. + /// + /// The type of document being queried. Results from the database will be mapped to this type. + /// The primary key type of the document + /// A collection of ID's to query by. + /// The documents. + [Pure] List LoadManyRequired(params TKey[] ids) where TDocument : class; + + /// + /// Loads a set of documents by their ID's. If any of the documents are not found, a + /// will be thrown. + /// + /// The type of document being queried. Results from the database will be mapped to this type. + /// The primary key type of the document + /// A collection of ID's to query by. + /// The documents. + [Pure] List LoadManyRequired(IEnumerable ids) where TDocument : class; + /// /// Loads a set of documents by their ID's. If any of the documents are not found, a /// will be thrown. @@ -494,6 +605,17 @@ public interface IReadQueryExecutor /// The documents. [Pure] Task> LoadManyRequiredAsync(IEnumerable ids, CancellationToken cancellationToken = default) where TDocument : class; + /// + /// Loads a set of documents by their ID's. If any of the documents are not found, a + /// will be thrown. + /// + /// The type of document being queried. Results from the database will be mapped to this type. + /// The primary key type of the document + /// A collection of ID's to query by. + /// Token to use to cancel the command. + /// The documents. + [Pure] Task> LoadManyRequiredAsync(IEnumerable ids, CancellationToken cancellationToken = default) where TDocument : class; + /// /// Begins building a query that returns strongly typed documents. /// From 5b8ac76b176bd5ab2291512c70aa54d152da43de Mon Sep 17 00:00:00 2001 From: slewis74 Date: Thu, 17 Jun 2021 22:58:28 +1000 Subject: [PATCH 09/25] cleanup --- source/Nevermore/Advanced/ReadTransaction.cs | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/source/Nevermore/Advanced/ReadTransaction.cs b/source/Nevermore/Advanced/ReadTransaction.cs index d6d54857..6f34515b 100644 --- a/source/Nevermore/Advanced/ReadTransaction.cs +++ b/source/Nevermore/Advanced/ReadTransaction.cs @@ -244,13 +244,10 @@ public Task LoadRequiredAsync(Guid id, CancellationToken c [Pure] public List LoadManyRequired(params TKey[] ids) where TDocument : class - => LoadManyRequiredInternal(ids); + => LoadManyRequired(ids.AsEnumerable()); [Pure] public List LoadManyRequired(IEnumerable ids) where TDocument : class - => LoadManyRequiredInternal(ids); - - List LoadManyRequiredInternal(IEnumerable ids) where TDocument : class { var idList = ids.Distinct().ToList(); var results = LoadMany(idList); @@ -327,13 +324,10 @@ public Task> LoadManyRequiredAsync(IEnumerable [Pure] public IEnumerable LoadStream(params TKey[] ids) where TDocument : class - => LoadStreamInternal(ids); + => LoadStream(ids.AsEnumerable()); [Pure] public IEnumerable LoadStream(IEnumerable ids) where TDocument : class - => LoadStreamInternal(ids); - - IEnumerable LoadStreamInternal(IEnumerable ids) where TDocument : class { var idList = ids.Where(id => id != null).Distinct().ToList(); From 077542d29abf3c523af5ea71125e0bfb8c4e9c2d Mon Sep 17 00:00:00 2001 From: slewis74 Date: Fri, 18 Jun 2021 09:13:20 +1000 Subject: [PATCH 10/25] Fixed IReadQueryExecutor interface to correctly express nullable reference types that are in use --- .../RelationalStoreFixture.cs | 2 +- source/Nevermore/Advanced/QueryBuilder.cs | 12 ++-- source/Nevermore/Advanced/ReadTransaction.cs | 67 ++++++++++--------- source/Nevermore/Advanced/WriteTransaction.cs | 16 +++-- source/Nevermore/IReadQueryExecutor.cs | 41 ++++++------ source/Nevermore/ITransactionDiagnostic.cs | 3 +- source/Nevermore/Mapping/KeyAllocator.cs | 4 +- 7 files changed, 77 insertions(+), 68 deletions(-) diff --git a/source/Nevermore.IntegrationTests/RelationalStoreFixture.cs b/source/Nevermore.IntegrationTests/RelationalStoreFixture.cs index 120bc526..7c31e196 100644 --- a/source/Nevermore.IntegrationTests/RelationalStoreFixture.cs +++ b/source/Nevermore.IntegrationTests/RelationalStoreFixture.cs @@ -245,7 +245,7 @@ public void ShouldPersistAndLoadReferenceCollectionsOnSingleDocuments() using (var transaction = Store.BeginTransaction()) { var loadedCustomer = transaction.Load(customerId); - loadedCustomer.Roles.Count.Should().Be(2); + loadedCustomer!.Roles.Count.Should().Be(2); } } diff --git a/source/Nevermore/Advanced/QueryBuilder.cs b/source/Nevermore/Advanced/QueryBuilder.cs index 2e53a68a..77902378 100644 --- a/source/Nevermore/Advanced/QueryBuilder.cs +++ b/source/Nevermore/Advanced/QueryBuilder.cs @@ -90,7 +90,7 @@ public IArrayParametersQueryBuilder WhereParameterized(string fieldName { return new ArrayParametersQueryBuilder(AddAlwaysFalseWhere(), parameterNamesList); } - + selectBuilder.AddWhere(new ArrayWhereParameter(fieldName, operand, parameterNamesList)); IQueryBuilder builder = this; return new ArrayParametersQueryBuilder(parameterNamesList.Aggregate(builder, (b, p) => b.Parameter(p)), parameterNamesList); @@ -258,7 +258,7 @@ public int Count() var clonedSelectBuilder = selectBuilder.Clone(); clonedSelectBuilder.AddColumnSelection(new SelectCountSource()); var count = readQueryExecutor.ExecuteScalar(clonedSelectBuilder.GenerateSelect().GenerateSql(), paramValues, RetriableOperation.Select, commandTimeout); - return count; + return count.GetValueOrDefault(); } public async Task CountAsync(CancellationToken cancellationToken = default) @@ -266,7 +266,7 @@ public async Task CountAsync(CancellationToken cancellationToken = default) var clonedSelectBuilder = selectBuilder.Clone(); clonedSelectBuilder.AddColumnSelection(new SelectCountSource()); var count = await readQueryExecutor.ExecuteScalarAsync(clonedSelectBuilder.GenerateSelect().GenerateSql(), paramValues, RetriableOperation.Select, commandTimeout, cancellationToken); - return count; + return count.GetValueOrDefault(); } public bool Any() @@ -398,10 +398,10 @@ public async Task> ToListAsync(int skip, int take, CancellationTok { results.Add(item); } - + return results; } - + SubquerySelectBuilder BuildToList(int skip, int take, out CommandParameterValues parmeterValues) { const string rowNumberColumnName = "RowNum"; @@ -454,7 +454,7 @@ public async Task> ToListAsync(CancellationToken cancellationToken { var results = new List(); - await foreach (var item in StreamAsync(cancellationToken)) + await foreach (var item in StreamAsync(cancellationToken)) results.Add(item); return results; diff --git a/source/Nevermore/Advanced/ReadTransaction.cs b/source/Nevermore/Advanced/ReadTransaction.cs index 6f34515b..5309a881 100644 --- a/source/Nevermore/Advanced/ReadTransaction.cs +++ b/source/Nevermore/Advanced/ReadTransaction.cs @@ -1,3 +1,4 @@ +#nullable enable using System; using System.Collections.Generic; using System.Data; @@ -27,9 +28,9 @@ public class ReadTransaction : IReadTransaction, ITransactionDiagnostic readonly RetriableOperation operationsToRetry; readonly IRelationalStoreConfiguration configuration; readonly ITableAliasGenerator tableAliasGenerator = new TableAliasGenerator(); - readonly string name; + readonly string? name; - DbConnection connection; + DbConnection? connection; protected IUniqueParameterNameGenerator ParameterNameGenerator { get; } = new UniqueParameterNameGenerator(); @@ -40,7 +41,7 @@ public class ReadTransaction : IReadTransaction, ITransactionDiagnostic public IDictionary State { get; } - public ReadTransaction(RelationalTransactionRegistry registry, RetriableOperation operationsToRetry, IRelationalStoreConfiguration configuration, string name = null) + public ReadTransaction(RelationalTransactionRegistry registry, RetriableOperation operationsToRetry, IRelationalStoreConfiguration configuration, string? name = null) { State = new Dictionary(); this.registry = registry; @@ -52,7 +53,7 @@ public ReadTransaction(RelationalTransactionRegistry registry, RetriableOperatio registry.Add(this); } - protected DbTransaction Transaction { get; private set; } + protected DbTransaction? Transaction { get; private set; } public void Open() { @@ -72,35 +73,35 @@ public async Task OpenAsync() public void Open(IsolationLevel isolationLevel) { Open(); - Transaction = connection.BeginTransaction(isolationLevel); + Transaction = connection!.BeginTransaction(isolationLevel); } public async Task OpenAsync(IsolationLevel isolationLevel) { await OpenAsync(); - Transaction = await connection.BeginTransactionAsync(isolationLevel); + Transaction = await connection!.BeginTransactionAsync(isolationLevel); } [Pure] - public TDocument Load(TKey id) where TDocument : class + public TDocument? Load(TKey id) where TDocument : class { return Stream(PrepareLoad(id)).FirstOrDefault(); } [Pure] - public TDocument Load(string id) where TDocument : class => Load(id); + public TDocument? Load(string id) where TDocument : class => Load(id); [Pure] - public TDocument Load(int id) where TDocument : class => Load(id); + public TDocument? Load(int id) where TDocument : class => Load(id); [Pure] - public TDocument Load(long id) where TDocument : class => Load(id); + public TDocument? Load(long id) where TDocument : class => Load(id); [Pure] - public TDocument Load(Guid id) where TDocument : class => Load(id); + public TDocument? Load(Guid id) where TDocument : class => Load(id); [Pure] - public async Task LoadAsync(TKey id, CancellationToken cancellationToken = default) where TDocument : class + public async Task LoadAsync(TKey id, CancellationToken cancellationToken = default) where TDocument : class { var results = StreamAsync(PrepareLoad(id), cancellationToken); await foreach (var row in results.WithCancellation(cancellationToken)) @@ -109,19 +110,19 @@ public async Task LoadAsync(TKey id, CancellationTok } [Pure] - public Task LoadAsync(string id, CancellationToken cancellationToken = default) where TDocument : class + public Task LoadAsync(string id, CancellationToken cancellationToken = default) where TDocument : class => LoadAsync(id, cancellationToken); [Pure] - public Task LoadAsync(int id, CancellationToken cancellationToken = default) where TDocument : class + public Task LoadAsync(int id, CancellationToken cancellationToken = default) where TDocument : class => LoadAsync(id, cancellationToken); [Pure] - public Task LoadAsync(long id, CancellationToken cancellationToken = default) where TDocument : class + public Task LoadAsync(long id, CancellationToken cancellationToken = default) where TDocument : class => LoadAsync(id, cancellationToken); [Pure] - public Task LoadAsync(Guid id, CancellationToken cancellationToken = default) where TDocument : class + public Task LoadAsync(Guid id, CancellationToken cancellationToken = default) where TDocument : class => LoadAsync(id, cancellationToken); [Pure] @@ -398,7 +399,7 @@ public IAsyncEnumerable LoadStreamAsync(IEnumerable public ITableSourceQueryBuilder Query() where TRecord : class { var map = configuration.DocumentMaps.Resolve(typeof(TRecord)); - return new TableSourceQueryBuilder(map.TableName, configuration.GetSchemaNameOrDefault(map), map.IdColumn.ColumnName, this, tableAliasGenerator, ParameterNameGenerator, new CommandParameterValues(), new Parameters(), new ParameterDefaults()); + return new TableSourceQueryBuilder(map.TableName, configuration.GetSchemaNameOrDefault(map), map.IdColumn?.ColumnName, this, tableAliasGenerator, ParameterNameGenerator, new CommandParameterValues(), new Parameters(), new ParameterDefaults()); } public ISubquerySourceBuilder RawSqlQuery(string query) where TRecord : class @@ -406,12 +407,12 @@ public ISubquerySourceBuilder RawSqlQuery(string query) where return new SubquerySourceBuilder(new RawSql(query), this, tableAliasGenerator, ParameterNameGenerator, new CommandParameterValues(), new Parameters(), new ParameterDefaults()); } - public IEnumerable Stream(string query, CommandParameterValues args, TimeSpan? commandTimeout = null) + public IEnumerable Stream(string query, CommandParameterValues? args = null, TimeSpan? commandTimeout = null) { return Stream(new PreparedCommand(query, args, RetriableOperation.Select, commandBehavior: CommandBehavior.SequentialAccess | CommandBehavior.SingleResult, commandTimeout: commandTimeout)); } - public IAsyncEnumerable StreamAsync(string query, CommandParameterValues args = null, TimeSpan? commandTimeout = null, CancellationToken cancellationToken = default) + public IAsyncEnumerable StreamAsync(string query, CommandParameterValues? args = null, TimeSpan? commandTimeout = null, CancellationToken cancellationToken = default) { return StreamAsync(new PreparedCommand(query, args, RetriableOperation.Select, commandBehavior: CommandBehavior.SequentialAccess | CommandBehavior.SingleResult, commandTimeout: commandTimeout), cancellationToken); } @@ -489,12 +490,12 @@ void AddCommandTrace(string commandText) } } - public int ExecuteNonQuery(string query, CommandParameterValues args, TimeSpan? commandTimeout = null) + public int ExecuteNonQuery(string query, CommandParameterValues? args = null, TimeSpan? commandTimeout = null) { return ExecuteNonQuery(new PreparedCommand(query, args, RetriableOperation.None, commandBehavior: CommandBehavior.Default, commandTimeout: commandTimeout)); } - public Task ExecuteNonQueryAsync(string query, CommandParameterValues args = null, TimeSpan? commandTimeout = null, CancellationToken cancellationToken = default) + public Task ExecuteNonQueryAsync(string query, CommandParameterValues? args = null, TimeSpan? commandTimeout = null, CancellationToken cancellationToken = default) { return ExecuteNonQueryAsync(new PreparedCommand(query, args, RetriableOperation.None, commandBehavior: CommandBehavior.Default, commandTimeout: commandTimeout), cancellationToken); } @@ -511,17 +512,17 @@ public async Task ExecuteNonQueryAsync(PreparedCommand preparedCommand, Can return await command.ExecuteNonQueryAsync(cancellationToken); } - public TResult ExecuteScalar(string query, CommandParameterValues args, RetriableOperation operation, TimeSpan? commandTimeout = null) + public TResult? ExecuteScalar(string query, CommandParameterValues? args = null, RetriableOperation retriableOperation = RetriableOperation.Select, TimeSpan? commandTimeout = null) where TResult : struct { - return ExecuteScalar(new PreparedCommand(query, args, operation, commandTimeout: commandTimeout)); + return ExecuteScalar(new PreparedCommand(query, args, retriableOperation, commandTimeout: commandTimeout)); } - public Task ExecuteScalarAsync(string query, CommandParameterValues args = null, RetriableOperation retriableOperation = RetriableOperation.Select, TimeSpan? commandTimeout = null, CancellationToken cancellationToken = default) + public Task ExecuteScalarAsync(string query, CommandParameterValues? args = null, RetriableOperation retriableOperation = RetriableOperation.Select, TimeSpan? commandTimeout = null, CancellationToken cancellationToken = default) where TResult : struct { return ExecuteScalarAsync(new PreparedCommand(query, args, retriableOperation, null, commandTimeout), cancellationToken); } - public TResult ExecuteScalar(PreparedCommand preparedCommand) + public TResult? ExecuteScalar(PreparedCommand preparedCommand) where TResult : struct { using var command = CreateCommand(preparedCommand); var result = command.ExecuteScalar(); @@ -530,7 +531,7 @@ public TResult ExecuteScalar(PreparedCommand preparedCommand) return (TResult) result; } - public async Task ExecuteScalarAsync(PreparedCommand preparedCommand, CancellationToken cancellationToken = default) + public async Task ExecuteScalarAsync(PreparedCommand preparedCommand, CancellationToken cancellationToken = default) where TResult : struct { using var command = CreateCommand(preparedCommand); var result = await command.ExecuteScalarAsync(cancellationToken); @@ -539,12 +540,12 @@ public async Task ExecuteScalarAsync(PreparedCommand preparedC return (TResult) result; } - public DbDataReader ExecuteReader(string query, CommandParameterValues args = null, TimeSpan? commandTimeout = null) + public DbDataReader ExecuteReader(string query, CommandParameterValues? args = null, TimeSpan? commandTimeout = null) { return ExecuteReader(new PreparedCommand(query, args, RetriableOperation.Select, commandTimeout: commandTimeout)); } - public Task ExecuteReaderAsync(string query, CommandParameterValues args = null, TimeSpan? commandTimeout = null, CancellationToken cancellationToken = default) + public Task ExecuteReaderAsync(string query, CommandParameterValues? args = null, TimeSpan? commandTimeout = null, CancellationToken cancellationToken = default) { return ExecuteReaderAsync(new PreparedCommand(query, args, RetriableOperation.Select, commandTimeout: commandTimeout), cancellationToken); } @@ -577,8 +578,8 @@ PreparedCommand PrepareLoad(TKey id) { var mapping = configuration.DocumentMaps.Resolve(typeof(TDocument)); - if (mapping.IdColumn.Type != typeof(TKey)) - throw new ArgumentException($"Provided Id of type '{id.GetType().FullName}' does not match configured type of '{mapping.IdColumn.Type.FullName}'."); + if (mapping.IdColumn?.Type != typeof(TKey)) + 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", mapping.IdColumn.PrimaryKeyHandler is IPrimitivePrimaryKeyHandler primitive ? primitive.GetPrimitiveValue(id) : id }}; @@ -589,8 +590,8 @@ PreparedCommand PrepareLoadMany(IEnumerable idList) { var mapping = configuration.DocumentMaps.Resolve(typeof(TDocument)); - if (mapping.IdColumn.Type != typeof(TKey)) - throw new ArgumentException($"Provided Id of type '{typeof(TKey).FullName}' does not match configured type of '{mapping.IdColumn.Type.FullName}'."); + if (mapping.IdColumn?.Type != typeof(TKey)) + throw new ArgumentException($"Provided Id of type '{typeof(TKey).FullName}' does not match configured type of '{mapping.IdColumn?.Type.FullName}'."); var param = new CommandParameterValues(); param.AddTable("criteriaTable", idList.ToList()); @@ -665,7 +666,7 @@ public override string ToString() return $"{CreatedTime} - {connection?.State} - {name}"; } - string ITransactionDiagnostic.Name => name; + string? ITransactionDiagnostic.Name => name; void ITransactionDiagnostic.WriteCurrentTransactions(StringBuilder output) { diff --git a/source/Nevermore/Advanced/WriteTransaction.cs b/source/Nevermore/Advanced/WriteTransaction.cs index 1f72a97d..d860f5c2 100644 --- a/source/Nevermore/Advanced/WriteTransaction.cs +++ b/source/Nevermore/Advanced/WriteTransaction.cs @@ -255,6 +255,8 @@ public string AllocateId(string tableName, string idPrefix) public void Commit() { + if (Transaction is null) + throw new InvalidOperationException("There is no current transaction, call Open/OpenAsync to start a transaction"); if (!configuration.AllowSynchronousOperations) throw new SynchronousOperationsDisabledException(); configuration.Hooks.BeforeCommit(this); @@ -264,6 +266,8 @@ public void Commit() public async Task CommitAsync(CancellationToken cancellationToken = default) { + if (Transaction is null) + throw new InvalidOperationException("There is no current transaction, call Open/OpenAsync to start a transaction"); await configuration.Hooks.BeforeCommitAsync(this); await Transaction.CommitAsync(cancellationToken); await configuration.Hooks.AfterCommitAsync(this); @@ -325,7 +329,7 @@ void ApplyNewRowVersionIfRequired(TDocument document, DocumentMap map if (output?.RowVersion == null) throw new StaleDataException($"Modification failed for '{typeof(TDocument).Name}' document with '{mapping.GetId(document)}' Id because submitted data was out of date. Refresh the document and try again."); - mapping.RowVersionColumn.PropertyHandler.Write(document, output.RowVersion); + mapping.RowVersionColumn!.PropertyHandler.Write(document, output.RowVersion); } void ApplyIdentityIdsIfRequired(IReadOnlyList documentList, DocumentMap mapping, DataModificationOutput[] outputs) @@ -346,7 +350,7 @@ void ApplyIdentityIdsIfRequired(TDocument document, DocumentMap mappi throw new InvalidOperationException( $"Modification failed for '{typeof(TDocument).Name}' document with '{mapping.GetId(document)}' Id because the server failed to return a new Identity id."); - mapping.IdColumn.PropertyHandler.Write(document, output.Id); + mapping.IdColumn!.PropertyHandler.Write(document, output.Id); } class DataModificationOutput @@ -360,10 +364,10 @@ public static DataModificationOutput Read(DbDataReader reader, DocumentMap map, if (map.IsRowVersioningEnabled) output.RowVersion = - reader.GetFieldValue(map.RowVersionColumn.ColumnName); + reader.GetFieldValue(map.RowVersionColumn!.ColumnName); if (map.IsIdentityId && isInsert) - output.Id = reader.GetFieldValue(map.IdColumn.ColumnName); + output.Id = reader.GetFieldValue(map.IdColumn!.ColumnName); return output; } @@ -374,10 +378,10 @@ public static async Task ReadAsync(DbDataReader reader, if (map.IsRowVersioningEnabled) output.RowVersion = - await reader.GetFieldValueAsync(map.RowVersionColumn.ColumnName, cancellationToken); + await reader.GetFieldValueAsync(map.RowVersionColumn!.ColumnName, cancellationToken); if (map.IsIdentityId && isInsert) - output.Id = await reader.GetFieldValueAsync(map.IdColumn.ColumnName, cancellationToken); + output.Id = await reader.GetFieldValueAsync(map.IdColumn!.ColumnName, cancellationToken); return output; } diff --git a/source/Nevermore/IReadQueryExecutor.cs b/source/Nevermore/IReadQueryExecutor.cs index ae226c4c..22d99f7d 100644 --- a/source/Nevermore/IReadQueryExecutor.cs +++ b/source/Nevermore/IReadQueryExecutor.cs @@ -1,3 +1,4 @@ +#nullable enable using System; using System.Collections.Generic; using System.Data.Common; @@ -19,7 +20,7 @@ public interface IReadQueryExecutor /// The type of document being queried. Results from the database will be mapped to this type. /// The Id of the document to find. /// The document, or null if the document is not found. - [Pure] TDocument Load(string id) where TDocument : class; + [Pure] TDocument? Load(string id) where TDocument : class; /// /// Loads a single document given its ID. If the item is not found, returns null. @@ -27,7 +28,7 @@ public interface IReadQueryExecutor /// The type of document being queried. Results from the database will be mapped to this type. /// The Id of the document to find. /// The document, or null if the document is not found. - [Pure] TDocument Load(int id) where TDocument : class; + [Pure] TDocument? Load(int id) where TDocument : class; /// /// Loads a single document given its ID. If the item is not found, returns null. @@ -35,7 +36,7 @@ public interface IReadQueryExecutor /// The type of document being queried. Results from the database will be mapped to this type. /// The Id of the document to find. /// The document, or null if the document is not found. - [Pure] TDocument Load(long id) where TDocument : class; + [Pure] TDocument? Load(long id) where TDocument : class; /// /// Loads a single document given its ID. If the item is not found, returns null. @@ -43,7 +44,7 @@ public interface IReadQueryExecutor /// The type of document being queried. Results from the database will be mapped to this type. /// The Id of the document to find. /// The document, or null if the document is not found. - [Pure] TDocument Load(Guid id) where TDocument : class; + [Pure] TDocument? Load(Guid id) where TDocument : class; /// /// Loads a single document given its ID. If the item is not found, returns null. @@ -52,7 +53,7 @@ public interface IReadQueryExecutor /// The type of the Id /// The Id of the document to find. /// The document, or null if the document is not found. - [Pure] TDocument Load(TKey id) where TDocument : class; + [Pure] TDocument? Load(TKey id) where TDocument : class; /// /// Loads a single document given its ID. If the item is not found, returns null. @@ -61,7 +62,7 @@ public interface IReadQueryExecutor /// The Id of the document to find. /// Token to use to cancel the command. /// The document, or null if the document is not found. - [Pure] Task LoadAsync(string id, CancellationToken cancellationToken = default) where TDocument : class; + [Pure] Task LoadAsync(string id, CancellationToken cancellationToken = default) where TDocument : class; /// /// Loads a single document given its ID. If the item is not found, returns null. @@ -70,7 +71,7 @@ public interface IReadQueryExecutor /// The Id of the document to find. /// Token to use to cancel the command. /// The document, or null if the document is not found. - [Pure] Task LoadAsync(int id, CancellationToken cancellationToken = default) where TDocument : class; + [Pure] Task LoadAsync(int id, CancellationToken cancellationToken = default) where TDocument : class; /// /// Loads a single document given its ID. If the item is not found, returns null. @@ -79,7 +80,7 @@ public interface IReadQueryExecutor /// The Id of the document to find. /// Token to use to cancel the command. /// The document, or null if the document is not found. - [Pure] Task LoadAsync(long id, CancellationToken cancellationToken = default) where TDocument : class; + [Pure] Task LoadAsync(long id, CancellationToken cancellationToken = default) where TDocument : class; /// /// Loads a single document given its ID. If the item is not found, returns null. @@ -88,7 +89,7 @@ public interface IReadQueryExecutor /// The Id of the document to find. /// Token to use to cancel the command. /// The document, or null if the document is not found. - [Pure] Task LoadAsync(Guid id, CancellationToken cancellationToken = default) where TDocument : class; + [Pure] Task LoadAsync(Guid id, CancellationToken cancellationToken = default) where TDocument : class; /// /// Loads a single document given its ID. If the item is not found, returns null. @@ -98,7 +99,7 @@ public interface IReadQueryExecutor /// The Id of the document to find. /// Token to use to cancel the command. /// The document, or null if the document is not found. - [Pure] Task LoadAsync(TKey id, CancellationToken cancellationToken = default) where TDocument : class; + [Pure] Task LoadAsync(TKey id, CancellationToken cancellationToken = default) where TDocument : class; /// /// Loads a set of documents by their ID's. Documents that are not found are excluded from the result list (that is, @@ -640,7 +641,7 @@ public interface IReadQueryExecutor /// Any arguments to pass to the query as command parameters. /// A custom timeout to use for the command instead of the default. /// A stream of resulting documents. - [Pure] IEnumerable Stream(string query, CommandParameterValues args = null, TimeSpan? commandTimeout = null); + [Pure] IEnumerable Stream(string query, CommandParameterValues? args = null, TimeSpan? commandTimeout = null); /// /// Executes a query that returns strongly typed documents. @@ -651,7 +652,7 @@ public interface IReadQueryExecutor /// A custom timeout to use for the command instead of the default. /// Token to use to cancel the command. /// A stream of resulting documents. - [Pure] IAsyncEnumerable StreamAsync(string query, CommandParameterValues args = null, TimeSpan? commandTimeout = null, CancellationToken cancellationToken = default); + [Pure] IAsyncEnumerable StreamAsync(string query, CommandParameterValues? args = null, TimeSpan? commandTimeout = null, CancellationToken cancellationToken = default); /// /// Executes a query that returns strongly typed documents. @@ -700,7 +701,7 @@ public interface IReadQueryExecutor /// Any arguments to pass to the query as command parameters. /// A custom timeout to use for the command instead of the default. /// The number of rows affected. - int ExecuteNonQuery(string query, CommandParameterValues args = null, TimeSpan? commandTimeout = null); + int ExecuteNonQuery(string query, CommandParameterValues? args = null, TimeSpan? commandTimeout = null); /// /// Executes a query that returns no results, typically one that will write to the database. It can also be @@ -711,7 +712,7 @@ public interface IReadQueryExecutor /// A custom timeout to use for the command instead of the default. /// Token to use to cancel the command. /// Depends on the query, but typically the number of rows affected. - Task ExecuteNonQueryAsync(string query, CommandParameterValues args = null, TimeSpan? commandTimeout = null, CancellationToken cancellationToken = default); + Task ExecuteNonQueryAsync(string query, CommandParameterValues? args = null, TimeSpan? commandTimeout = null, CancellationToken cancellationToken = default); /// /// Executes a query that returns no results, typically one that will write to the database. It can also be @@ -739,7 +740,7 @@ public interface IReadQueryExecutor /// The type of operation being performed. The retry policy on the transaction will then decide whether it's safe to retry this command if it fails. /// A custom timeout to use for the command instead of the default. /// A scalar value. - TResult ExecuteScalar(string query, CommandParameterValues args = null, RetriableOperation retriableOperation = RetriableOperation.Select, TimeSpan? commandTimeout = null); + TResult? ExecuteScalar(string query, CommandParameterValues? args = null, RetriableOperation retriableOperation = RetriableOperation.Select, TimeSpan? commandTimeout = null) where TResult : struct; /// /// Executes a query that returns a scalar value (e.g., SELECT query that returns a count). @@ -751,7 +752,7 @@ public interface IReadQueryExecutor /// A custom timeout to use for the command instead of the default. /// Token to use to cancel the command. /// A scalar value. - Task ExecuteScalarAsync(string query, CommandParameterValues args = null, RetriableOperation retriableOperation = RetriableOperation.Select, TimeSpan? commandTimeout = null, CancellationToken cancellationToken = default); + Task ExecuteScalarAsync(string query, CommandParameterValues? args = null, RetriableOperation retriableOperation = RetriableOperation.Select, TimeSpan? commandTimeout = null, CancellationToken cancellationToken = default) where TResult : struct; /// /// Executes a query that returns a scalar value (e.g., SELECT query that returns a count). @@ -759,7 +760,7 @@ public interface IReadQueryExecutor /// The scalar value type to return. The DB result will be cast to this type. If the result is null, it will return the default value for the type.. /// The command to execute. /// A scalar value. - TResult ExecuteScalar(PreparedCommand preparedCommand); + TResult? ExecuteScalar(PreparedCommand preparedCommand) where TResult : struct; /// /// Executes a query that returns a scalar value (e.g., SELECT query that returns a count). @@ -768,7 +769,7 @@ public interface IReadQueryExecutor /// The command to execute. /// Token to use to cancel the command. /// A scalar value. - Task ExecuteScalarAsync(PreparedCommand preparedCommand, CancellationToken cancellationToken = default); + Task ExecuteScalarAsync(PreparedCommand preparedCommand, CancellationToken cancellationToken = default) where TResult : struct; /// /// Executes a query that returns a data reader that you can process manually. @@ -777,7 +778,7 @@ public interface IReadQueryExecutor /// Any arguments to pass to the query as command parameters. /// A custom timeout to use for the command instead of the default. /// A data reader. - [Pure] DbDataReader ExecuteReader(string query, CommandParameterValues args = null, TimeSpan? commandTimeout = null); + [Pure] DbDataReader ExecuteReader(string query, CommandParameterValues? args = null, TimeSpan? commandTimeout = null); /// /// Executes a query that returns a data reader that you can process manually. @@ -787,7 +788,7 @@ public interface IReadQueryExecutor /// A custom timeout to use for the command instead of the default. /// Token to use to cancel the command. /// A data reader. - [Pure] Task ExecuteReaderAsync(string query, CommandParameterValues args = null, TimeSpan? commandTimeout = null, CancellationToken cancellationToken = default); + [Pure] Task ExecuteReaderAsync(string query, CommandParameterValues? args = null, TimeSpan? commandTimeout = null, CancellationToken cancellationToken = default); /// /// Executes a query that returns a data reader that you can process manually. diff --git a/source/Nevermore/ITransactionDiagnostic.cs b/source/Nevermore/ITransactionDiagnostic.cs index a37bbe5a..827c2868 100644 --- a/source/Nevermore/ITransactionDiagnostic.cs +++ b/source/Nevermore/ITransactionDiagnostic.cs @@ -1,10 +1,11 @@ +#nullable enable using System.Text; namespace Nevermore { internal interface ITransactionDiagnostic { - public string Name { get; } + public string? Name { get; } public void WriteCurrentTransactions(StringBuilder output); } } \ No newline at end of file diff --git a/source/Nevermore/Mapping/KeyAllocator.cs b/source/Nevermore/Mapping/KeyAllocator.cs index e8f3bf25..55fb684e 100644 --- a/source/Nevermore/Mapping/KeyAllocator.cs +++ b/source/Nevermore/Mapping/KeyAllocator.cs @@ -92,8 +92,10 @@ int GetNextMaxValue() parameters.CommandType = CommandType.StoredProcedure; var result = transaction.ExecuteScalar("GetNextKeyBlock", parameters); + if (result is null) + throw new InvalidOperationException("GetNextKeyBlock returned a null value"); transaction.Commit(); - return result; + return result.Value; } } From 706f33be709905c1f804b579a71dc580a3319c15 Mon Sep 17 00:00:00 2001 From: slewis74 Date: Fri, 18 Jun 2021 10:23:37 +1000 Subject: [PATCH 11/25] rolling back the nullable reference type changes to ExecuteScalar --- source/Nevermore/Advanced/QueryBuilder.cs | 4 ++-- source/Nevermore/Advanced/ReadTransaction.cs | 12 ++++++------ source/Nevermore/IReadQueryExecutor.cs | 8 ++++---- source/Nevermore/Mapping/KeyAllocator.cs | 4 +--- 4 files changed, 13 insertions(+), 15 deletions(-) diff --git a/source/Nevermore/Advanced/QueryBuilder.cs b/source/Nevermore/Advanced/QueryBuilder.cs index 77902378..1128baa3 100644 --- a/source/Nevermore/Advanced/QueryBuilder.cs +++ b/source/Nevermore/Advanced/QueryBuilder.cs @@ -258,7 +258,7 @@ public int Count() var clonedSelectBuilder = selectBuilder.Clone(); clonedSelectBuilder.AddColumnSelection(new SelectCountSource()); var count = readQueryExecutor.ExecuteScalar(clonedSelectBuilder.GenerateSelect().GenerateSql(), paramValues, RetriableOperation.Select, commandTimeout); - return count.GetValueOrDefault(); + return count; } public async Task CountAsync(CancellationToken cancellationToken = default) @@ -266,7 +266,7 @@ public async Task CountAsync(CancellationToken cancellationToken = default) var clonedSelectBuilder = selectBuilder.Clone(); clonedSelectBuilder.AddColumnSelection(new SelectCountSource()); var count = await readQueryExecutor.ExecuteScalarAsync(clonedSelectBuilder.GenerateSelect().GenerateSql(), paramValues, RetriableOperation.Select, commandTimeout, cancellationToken); - return count.GetValueOrDefault(); + return count; } public bool Any() diff --git a/source/Nevermore/Advanced/ReadTransaction.cs b/source/Nevermore/Advanced/ReadTransaction.cs index 5309a881..e59c1db2 100644 --- a/source/Nevermore/Advanced/ReadTransaction.cs +++ b/source/Nevermore/Advanced/ReadTransaction.cs @@ -512,31 +512,31 @@ public async Task ExecuteNonQueryAsync(PreparedCommand preparedCommand, Can return await command.ExecuteNonQueryAsync(cancellationToken); } - public TResult? ExecuteScalar(string query, CommandParameterValues? args = null, RetriableOperation retriableOperation = RetriableOperation.Select, TimeSpan? commandTimeout = null) where TResult : struct + public TResult ExecuteScalar(string query, CommandParameterValues? args = null, RetriableOperation retriableOperation = RetriableOperation.Select, TimeSpan? commandTimeout = null) { return ExecuteScalar(new PreparedCommand(query, args, retriableOperation, commandTimeout: commandTimeout)); } - public Task ExecuteScalarAsync(string query, CommandParameterValues? args = null, RetriableOperation retriableOperation = RetriableOperation.Select, TimeSpan? commandTimeout = null, CancellationToken cancellationToken = default) where TResult : struct + public Task ExecuteScalarAsync(string query, CommandParameterValues? args = null, RetriableOperation retriableOperation = RetriableOperation.Select, TimeSpan? commandTimeout = null, CancellationToken cancellationToken = default) { return ExecuteScalarAsync(new PreparedCommand(query, args, retriableOperation, null, commandTimeout), cancellationToken); } - public TResult? ExecuteScalar(PreparedCommand preparedCommand) where TResult : struct + public TResult ExecuteScalar(PreparedCommand preparedCommand) { using var command = CreateCommand(preparedCommand); var result = command.ExecuteScalar(); if (result == DBNull.Value) - return default; + return default!; return (TResult) result; } - public async Task ExecuteScalarAsync(PreparedCommand preparedCommand, CancellationToken cancellationToken = default) where TResult : struct + public async Task ExecuteScalarAsync(PreparedCommand preparedCommand, CancellationToken cancellationToken = default) { using var command = CreateCommand(preparedCommand); var result = await command.ExecuteScalarAsync(cancellationToken); if (result == DBNull.Value) - return default; + return default!; return (TResult) result; } diff --git a/source/Nevermore/IReadQueryExecutor.cs b/source/Nevermore/IReadQueryExecutor.cs index 22d99f7d..1ec3f524 100644 --- a/source/Nevermore/IReadQueryExecutor.cs +++ b/source/Nevermore/IReadQueryExecutor.cs @@ -740,7 +740,7 @@ public interface IReadQueryExecutor /// The type of operation being performed. The retry policy on the transaction will then decide whether it's safe to retry this command if it fails. /// A custom timeout to use for the command instead of the default. /// A scalar value. - TResult? ExecuteScalar(string query, CommandParameterValues? args = null, RetriableOperation retriableOperation = RetriableOperation.Select, TimeSpan? commandTimeout = null) where TResult : struct; + TResult ExecuteScalar(string query, CommandParameterValues? args = null, RetriableOperation retriableOperation = RetriableOperation.Select, TimeSpan? commandTimeout = null); /// /// Executes a query that returns a scalar value (e.g., SELECT query that returns a count). @@ -752,7 +752,7 @@ public interface IReadQueryExecutor /// A custom timeout to use for the command instead of the default. /// Token to use to cancel the command. /// A scalar value. - Task ExecuteScalarAsync(string query, CommandParameterValues? args = null, RetriableOperation retriableOperation = RetriableOperation.Select, TimeSpan? commandTimeout = null, CancellationToken cancellationToken = default) where TResult : struct; + Task ExecuteScalarAsync(string query, CommandParameterValues? args = null, RetriableOperation retriableOperation = RetriableOperation.Select, TimeSpan? commandTimeout = null, CancellationToken cancellationToken = default); /// /// Executes a query that returns a scalar value (e.g., SELECT query that returns a count). @@ -760,7 +760,7 @@ public interface IReadQueryExecutor /// The scalar value type to return. The DB result will be cast to this type. If the result is null, it will return the default value for the type.. /// The command to execute. /// A scalar value. - TResult? ExecuteScalar(PreparedCommand preparedCommand) where TResult : struct; + TResult ExecuteScalar(PreparedCommand preparedCommand); /// /// Executes a query that returns a scalar value (e.g., SELECT query that returns a count). @@ -769,7 +769,7 @@ public interface IReadQueryExecutor /// The command to execute. /// Token to use to cancel the command. /// A scalar value. - Task ExecuteScalarAsync(PreparedCommand preparedCommand, CancellationToken cancellationToken = default) where TResult : struct; + Task ExecuteScalarAsync(PreparedCommand preparedCommand, CancellationToken cancellationToken = default); /// /// Executes a query that returns a data reader that you can process manually. diff --git a/source/Nevermore/Mapping/KeyAllocator.cs b/source/Nevermore/Mapping/KeyAllocator.cs index 55fb684e..e8f3bf25 100644 --- a/source/Nevermore/Mapping/KeyAllocator.cs +++ b/source/Nevermore/Mapping/KeyAllocator.cs @@ -92,10 +92,8 @@ int GetNextMaxValue() parameters.CommandType = CommandType.StoredProcedure; var result = transaction.ExecuteScalar("GetNextKeyBlock", parameters); - if (result is null) - throw new InvalidOperationException("GetNextKeyBlock returned a null value"); transaction.Commit(); - return result.Value; + return result; } } From a6266abd801d9c557fcafa1612252bb372cb4f90 Mon Sep 17 00:00:00 2001 From: slewis74 Date: Mon, 21 Jun 2021 16:22:04 +1000 Subject: [PATCH 12/25] don't use the term TinyType --- .../Model/Customer.cs | 2 +- .../Model/StringTinyTypeIdKeyHandler.cs | 10 +++++----- .../Model/StringTinyTypeIdTypeHandler.cs | 4 ++-- .../Model/TinyType.cs | 20 +++++++++---------- .../SetUp/FixtureWithRelationalStore.cs | 4 ++-- .../SetUp/SchemaGenerator.cs | 2 +- source/Nevermore/IWriteQueryExecutor.cs | 2 +- 7 files changed, 22 insertions(+), 22 deletions(-) diff --git a/source/Nevermore.IntegrationTests/Model/Customer.cs b/source/Nevermore.IntegrationTests/Model/Customer.cs index 7aae7599..8a682af7 100644 --- a/source/Nevermore.IntegrationTests/Model/Customer.cs +++ b/source/Nevermore.IntegrationTests/Model/Customer.cs @@ -19,7 +19,7 @@ public Customer() public string[] Passphrases { get; set; } } - public class CustomerId : StringTinyType + public class CustomerId : StringCustomIdType { internal CustomerId(string value) : base(value) { diff --git a/source/Nevermore.IntegrationTests/Model/StringTinyTypeIdKeyHandler.cs b/source/Nevermore.IntegrationTests/Model/StringTinyTypeIdKeyHandler.cs index 072de53f..5a4316d4 100644 --- a/source/Nevermore.IntegrationTests/Model/StringTinyTypeIdKeyHandler.cs +++ b/source/Nevermore.IntegrationTests/Model/StringTinyTypeIdKeyHandler.cs @@ -4,21 +4,21 @@ namespace Nevermore.IntegrationTests.Model { - class StringTinyTypeIdKeyHandler : IStringBasedPrimitivePrimaryKeyHandler - where T : StringTinyType + class StringCustomIdTypeIdKeyHandler : IStringBasedPrimitivePrimaryKeyHandler + where T : StringCustomIdType { public Type Type => typeof(T); public object? GetPrimitiveValue(object? id) { - if (!(id is StringTinyType stringTinyType)) + if (!(id is StringCustomIdType stringCustomType)) throw new ArgumentException($"Expected the id to be a {typeof(T).Name}"); - return stringTinyType.Value; + return stringCustomType.Value; } public object FormatKey(string tableName, int key) { - return TinyType.Create($"{GetPrefix(tableName)}-{key}")!; + return CustomIdType.Create($"{GetPrefix(tableName)}-{key}")!; } public void SetPrefix(Func idPrefix) diff --git a/source/Nevermore.IntegrationTests/Model/StringTinyTypeIdTypeHandler.cs b/source/Nevermore.IntegrationTests/Model/StringTinyTypeIdTypeHandler.cs index 5deabc8c..0dbfb33a 100644 --- a/source/Nevermore.IntegrationTests/Model/StringTinyTypeIdTypeHandler.cs +++ b/source/Nevermore.IntegrationTests/Model/StringTinyTypeIdTypeHandler.cs @@ -6,7 +6,7 @@ namespace Nevermore.IntegrationTests.Model { - class StringTinyTypeIdTypeHandler : ITypeHandler where T : TinyType + class StringCustomIdTypeHandler : ITypeHandler where T : CustomIdType { public bool CanConvert(Type objectType) { @@ -18,7 +18,7 @@ public bool CanConvert(Type objectType) if (reader.IsDBNull(columnIndex)) return null; var value = reader.GetString(columnIndex); if (string.IsNullOrWhiteSpace(value)) return null; - return TinyType.Create(value); + return CustomIdType.Create(value); } public void WriteDatabase(DbParameter parameter, object value) diff --git a/source/Nevermore.IntegrationTests/Model/TinyType.cs b/source/Nevermore.IntegrationTests/Model/TinyType.cs index b11d84d4..f3abd024 100644 --- a/source/Nevermore.IntegrationTests/Model/TinyType.cs +++ b/source/Nevermore.IntegrationTests/Model/TinyType.cs @@ -5,9 +5,9 @@ namespace Nevermore.IntegrationTests.Model { - public class TinyType + public class CustomIdType { - internal TinyType(T value) + internal CustomIdType(T value) { Value = value; } @@ -24,7 +24,7 @@ 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 !(Value is null) && Value.Equals(((TinyType) obj).Value); + return !(Value is null) && Value.Equals(((CustomIdType) obj).Value); } public override int GetHashCode() @@ -32,22 +32,22 @@ public override int GetHashCode() return (Value != null ? Value.GetHashCode() : 0); } - public static TinyType? Create(Type tinyType, T value) + public static CustomIdType? Create(Type customType, T value) { const BindingFlags bindingFlags = BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic; - var instance = Activator.CreateInstance(tinyType, bindingFlags, null, new object[] { value! }, CultureInfo.CurrentCulture); - return instance as TinyType; + var instance = Activator.CreateInstance(customType, bindingFlags, null, new object[] { value! }, CultureInfo.CurrentCulture); + return instance as CustomIdType; } - public static TTinyType? Create(T value) where TTinyType : TinyType + public static TCustomIdType? Create(T value) where TCustomIdType : CustomIdType { - return (TTinyType?) Create(typeof(TTinyType), value); + return (TCustomIdType?) Create(typeof(TCustomIdType), value); } } - public class StringTinyType : TinyType + public class StringCustomIdType : CustomIdType { - internal StringTinyType(string value) : base(value) + internal StringCustomIdType(string value) : base(value) { } } diff --git a/source/Nevermore.IntegrationTests/SetUp/FixtureWithRelationalStore.cs b/source/Nevermore.IntegrationTests/SetUp/FixtureWithRelationalStore.cs index 6da759d3..fd62e219 100644 --- a/source/Nevermore.IntegrationTests/SetUp/FixtureWithRelationalStore.cs +++ b/source/Nevermore.IntegrationTests/SetUp/FixtureWithRelationalStore.cs @@ -41,9 +41,9 @@ protected FixtureWithRelationalStore() config.DocumentMaps.Register(documentMaps); config.TypeHandlers.Register(new ReferenceCollectionTypeHandler()); - config.TypeHandlers.Register(new StringTinyTypeIdTypeHandler()); + config.TypeHandlers.Register(new StringCustomIdTypeHandler()); - config.PrimaryKeyHandlers.Register(new StringTinyTypeIdKeyHandler()); + config.PrimaryKeyHandlers.Register(new StringCustomIdTypeIdKeyHandler()); config.InstanceTypeResolvers.Register(new ProductTypeResolver()); config.InstanceTypeResolvers.Register(new BrandTypeResolver()); diff --git a/source/Nevermore.IntegrationTests/SetUp/SchemaGenerator.cs b/source/Nevermore.IntegrationTests/SetUp/SchemaGenerator.cs index 0a5d469f..d241bd81 100644 --- a/source/Nevermore.IntegrationTests/SetUp/SchemaGenerator.cs +++ b/source/Nevermore.IntegrationTests/SetUp/SchemaGenerator.cs @@ -59,7 +59,7 @@ static bool IsNullable(ColumnMapping column) static string GetDatabaseType(ColumnMapping column) { - var dbType = typeof(StringTinyType).IsAssignableFrom(column.Type) ? DbType.String : DatabaseTypeConverter.AsDbType(column.Type); + var dbType = typeof(StringCustomIdType).IsAssignableFrom(column.Type) ? DbType.String : DatabaseTypeConverter.AsDbType(column.Type); switch (dbType) { diff --git a/source/Nevermore/IWriteQueryExecutor.cs b/source/Nevermore/IWriteQueryExecutor.cs index df79982e..9585e0be 100644 --- a/source/Nevermore/IWriteQueryExecutor.cs +++ b/source/Nevermore/IWriteQueryExecutor.cs @@ -247,7 +247,7 @@ public interface IWriteQueryExecutor : IReadQueryExecutor /// If the mapping specifies a SingletonId, that is returned /// /// The type of document. - /// The key type to allocate, e.g. int, string, MyStringTinyType. + /// The key type to allocate, e.g. int, string, MyCustomStringType. /// /// Will be thrown if the requested key type does not match the mapped document's key type. TKey AllocateId(); From 2427cb4695bcee60fdf0e88f581a94f5e224c333 Mon Sep 17 00:00:00 2001 From: slewis74 Date: Mon, 21 Jun 2021 17:00:32 +1000 Subject: [PATCH 13/25] renamed GetPrimitiveValue and removed FormatKey from IPrimitivePrimaryKeyHandler, it only makes sense for the IStringBasedPrimitivePrimaryKeyHandler handlers --- .../Model/StringTinyTypeIdKeyHandler.cs | 2 +- source/Nevermore/Advanced/ReadTransaction.cs | 2 +- source/Nevermore/Advanced/WriteTransaction.cs | 4 +++- source/Nevermore/Mapping/GuidPrimaryKeyHandler.cs | 4 ---- source/Nevermore/Mapping/IPrimitivePrimaryKeyHandler.cs | 4 +--- .../Mapping/IStringBasedPrimitivePrimaryKeyHandler.cs | 2 ++ source/Nevermore/Mapping/IntPrimaryKeyHandler.cs | 4 ---- source/Nevermore/Mapping/LongPrimaryKeyHandler.cs | 4 ---- source/Nevermore/Mapping/PrimitivePrimaryKeyHandler.cs | 4 +--- source/Nevermore/Mapping/StringPrimaryKeyHandler.cs | 2 +- source/Nevermore/Util/DataModificationQueryBuilder.cs | 2 +- 11 files changed, 11 insertions(+), 23 deletions(-) diff --git a/source/Nevermore.IntegrationTests/Model/StringTinyTypeIdKeyHandler.cs b/source/Nevermore.IntegrationTests/Model/StringTinyTypeIdKeyHandler.cs index 5a4316d4..7cb4f289 100644 --- a/source/Nevermore.IntegrationTests/Model/StringTinyTypeIdKeyHandler.cs +++ b/source/Nevermore.IntegrationTests/Model/StringTinyTypeIdKeyHandler.cs @@ -9,7 +9,7 @@ class StringCustomIdTypeIdKeyHandler : IStringBasedPrimitivePrimaryKeyHandler { public Type Type => typeof(T); - public object? GetPrimitiveValue(object? id) + public object? ConvertToPrimitiveValue(object? id) { if (!(id is StringCustomIdType stringCustomType)) throw new ArgumentException($"Expected the id to be a {typeof(T).Name}"); diff --git a/source/Nevermore/Advanced/ReadTransaction.cs b/source/Nevermore/Advanced/ReadTransaction.cs index e59c1db2..b3a49808 100644 --- a/source/Nevermore/Advanced/ReadTransaction.cs +++ b/source/Nevermore/Advanced/ReadTransaction.cs @@ -582,7 +582,7 @@ PreparedCommand PrepareLoad(TKey id) 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", mapping.IdColumn.PrimaryKeyHandler is IPrimitivePrimaryKeyHandler primitive ? primitive.GetPrimitiveValue(id) : id }}; + var args = new CommandParameterValues {{ "Id", mapping.IdColumn.PrimaryKeyHandler is IPrimitivePrimaryKeyHandler primitive ? primitive.ConvertToPrimitiveValue(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); } diff --git a/source/Nevermore/Advanced/WriteTransaction.cs b/source/Nevermore/Advanced/WriteTransaction.cs index d860f5c2..548ff1e5 100644 --- a/source/Nevermore/Advanced/WriteTransaction.cs +++ b/source/Nevermore/Advanced/WriteTransaction.cs @@ -244,7 +244,9 @@ object AllocateId(DocumentMap mapping) object AllocateIdUsingHandler(DocumentMap mapping, IPrimitivePrimaryKeyHandler primitivePrimaryKeyHandler) { var key = keyAllocator.NextId(mapping.TableName); - return primitivePrimaryKeyHandler.FormatKey(mapping.TableName, key); + if (primitivePrimaryKeyHandler is IStringBasedPrimitivePrimaryKeyHandler stringBasedKeyHandler) + return stringBasedKeyHandler.FormatKey(mapping.TableName, key); + return key; } public string AllocateId(string tableName, string idPrefix) diff --git a/source/Nevermore/Mapping/GuidPrimaryKeyHandler.cs b/source/Nevermore/Mapping/GuidPrimaryKeyHandler.cs index 54364cc6..9b9062c6 100644 --- a/source/Nevermore/Mapping/GuidPrimaryKeyHandler.cs +++ b/source/Nevermore/Mapping/GuidPrimaryKeyHandler.cs @@ -4,9 +4,5 @@ namespace Nevermore.Mapping { class GuidPrimaryKeyHandler : PrimitivePrimaryKeyHandler { - public override object FormatKey(string tableName, int key) - { - return key; - } } } \ No newline at end of file diff --git a/source/Nevermore/Mapping/IPrimitivePrimaryKeyHandler.cs b/source/Nevermore/Mapping/IPrimitivePrimaryKeyHandler.cs index d90a8055..65dca800 100644 --- a/source/Nevermore/Mapping/IPrimitivePrimaryKeyHandler.cs +++ b/source/Nevermore/Mapping/IPrimitivePrimaryKeyHandler.cs @@ -6,8 +6,6 @@ namespace Nevermore.Mapping public interface IPrimitivePrimaryKeyHandler : IPrimaryKeyHandler { [return: NotNullIfNotNull("id")] - object? GetPrimitiveValue(object? id); - - object FormatKey(string tableName, int key); + object? ConvertToPrimitiveValue(object? id); } } \ No newline at end of file diff --git a/source/Nevermore/Mapping/IStringBasedPrimitivePrimaryKeyHandler.cs b/source/Nevermore/Mapping/IStringBasedPrimitivePrimaryKeyHandler.cs index 29d4402b..53d46ad1 100644 --- a/source/Nevermore/Mapping/IStringBasedPrimitivePrimaryKeyHandler.cs +++ b/source/Nevermore/Mapping/IStringBasedPrimitivePrimaryKeyHandler.cs @@ -22,5 +22,7 @@ public interface IStringBasedPrimitivePrimaryKeyHandler : IPrimitivePrimaryKeyHa /// /// The function to call back to format the id. void SetFormat(Func<(string idPrefix, int key), string> format); + + object FormatKey(string tableName, int key); } } \ No newline at end of file diff --git a/source/Nevermore/Mapping/IntPrimaryKeyHandler.cs b/source/Nevermore/Mapping/IntPrimaryKeyHandler.cs index 8762136b..1a0b966b 100644 --- a/source/Nevermore/Mapping/IntPrimaryKeyHandler.cs +++ b/source/Nevermore/Mapping/IntPrimaryKeyHandler.cs @@ -2,9 +2,5 @@ namespace Nevermore.Mapping { class IntPrimaryKeyHandler : PrimitivePrimaryKeyHandler { - public override object FormatKey(string tableName, int key) - { - return key; - } } } \ No newline at end of file diff --git a/source/Nevermore/Mapping/LongPrimaryKeyHandler.cs b/source/Nevermore/Mapping/LongPrimaryKeyHandler.cs index 2acc0327..62ad6d0a 100644 --- a/source/Nevermore/Mapping/LongPrimaryKeyHandler.cs +++ b/source/Nevermore/Mapping/LongPrimaryKeyHandler.cs @@ -2,9 +2,5 @@ namespace Nevermore.Mapping { class LongPrimaryKeyHandler : PrimitivePrimaryKeyHandler { - public override object FormatKey(string tableName, int key) - { - return key; - } } } \ No newline at end of file diff --git a/source/Nevermore/Mapping/PrimitivePrimaryKeyHandler.cs b/source/Nevermore/Mapping/PrimitivePrimaryKeyHandler.cs index c838dc95..9df5a5d4 100644 --- a/source/Nevermore/Mapping/PrimitivePrimaryKeyHandler.cs +++ b/source/Nevermore/Mapping/PrimitivePrimaryKeyHandler.cs @@ -9,11 +9,9 @@ public abstract class PrimitivePrimaryKeyHandler : IPrimitivePrimaryKeyHandle public Type Type => typeof(T); [return: NotNullIfNotNull("id")] - public virtual object? GetPrimitiveValue(object? id) + public virtual object? ConvertToPrimitiveValue(object? id) { return id; } - - public abstract object FormatKey(string tableName, int key); } } \ No newline at end of file diff --git a/source/Nevermore/Mapping/StringPrimaryKeyHandler.cs b/source/Nevermore/Mapping/StringPrimaryKeyHandler.cs index 2701de9d..e864926d 100644 --- a/source/Nevermore/Mapping/StringPrimaryKeyHandler.cs +++ b/source/Nevermore/Mapping/StringPrimaryKeyHandler.cs @@ -36,7 +36,7 @@ public void SetFormat(Func<(string idPrefix, int key), string> format) formatFunc = format; } - public override object FormatKey(string tableName, int key) + public object FormatKey(string tableName, int key) { return formatFunc((GetPrefix(tableName), key)); } diff --git a/source/Nevermore/Util/DataModificationQueryBuilder.cs b/source/Nevermore/Util/DataModificationQueryBuilder.cs index c1ebff35..6f471df5 100644 --- a/source/Nevermore/Util/DataModificationQueryBuilder.cs +++ b/source/Nevermore/Util/DataModificationQueryBuilder.cs @@ -320,7 +320,7 @@ CommandParameterValues GetDocumentParameters(Func allocateI mapping.IdColumn.PropertyHandler.Write(document, id); } - var primitiveValue = primitiveKeyHandler.GetPrimitiveValue(id); + var primitiveValue = primitiveKeyHandler.ConvertToPrimitiveValue(id); result[$"{prefix}{mapping.IdColumn.ColumnName}"] = primitiveValue; } From 82e4bd413667473e7a1dd7f984832d1f8c1abc3f Mon Sep 17 00:00:00 2001 From: slewis74 Date: Mon, 21 Jun 2021 21:42:36 +1000 Subject: [PATCH 14/25] Refactored the DocumentMap and IdColumnMapping builders to be better builders, and take config in to make sure that every document map ends up with an IdColumn that has a PrimaryKeyHandler and specifies whether it is an Identity --- .../Advanced/MappingFixture.cs | 16 +-- .../Model/StringTinyTypeIdKeyHandler.cs | 3 +- .../RelatedDocumentTableFixture.cs | 4 +- .../SetUp/FixtureWithRelationalStore.cs | 9 +- .../CommandParameterValuesFixture.cs | 12 +- source/Nevermore/Advanced/ReadTransaction.cs | 8 +- source/Nevermore/Advanced/WriteTransaction.cs | 26 ++-- source/Nevermore/Mapping/DocumentMap.cs | 118 ++++++++---------- .../Nevermore/Mapping/DocumentMapRegistry.cs | 6 +- .../Mapping/GuidPrimaryKeyHandler.cs | 6 +- source/Nevermore/Mapping/IDocumentMap.cs | 2 +- .../Mapping/IIdColumnMappingBuilder.cs | 7 ++ .../Mapping/IIdentityPrimaryKeyHandler.cs | 5 - .../Nevermore/Mapping/IPrimaryKeyHandler.cs | 22 +++- .../Mapping/IPrimitivePrimaryKeyHandler.cs | 11 -- .../IStringBasedPrimitivePrimaryKeyHandler.cs | 4 +- source/Nevermore/Mapping/IdColumnMapping.cs | 61 ++++++--- .../Mapping/IdentityPrimaryKeyHandler.cs | 9 -- .../Nevermore/Mapping/IntPrimaryKeyHandler.cs | 6 +- .../Mapping/LongPrimaryKeyHandler.cs | 6 +- .../Mapping/PrimaryKeyHandlerRegistry.cs | 20 +-- .../Mapping/PrimitivePrimaryKeyHandler.cs | 4 +- .../Mapping/StringPrimaryKeyHandler.cs | 7 +- .../Nevermore/RelationalStoreConfiguration.cs | 7 +- .../Util/DataModificationQueryBuilder.cs | 14 +-- 25 files changed, 209 insertions(+), 184 deletions(-) delete mode 100644 source/Nevermore/Mapping/IIdentityPrimaryKeyHandler.cs delete mode 100644 source/Nevermore/Mapping/IPrimitivePrimaryKeyHandler.cs delete mode 100644 source/Nevermore/Mapping/IdentityPrimaryKeyHandler.cs diff --git a/source/Nevermore.IntegrationTests/Advanced/MappingFixture.cs b/source/Nevermore.IntegrationTests/Advanced/MappingFixture.cs index e237d13c..42d52237 100644 --- a/source/Nevermore.IntegrationTests/Advanced/MappingFixture.cs +++ b/source/Nevermore.IntegrationTests/Advanced/MappingFixture.cs @@ -19,7 +19,7 @@ class User // Test accessibility public string Prop1 { get; set; } - public string Prop2 { get; } = "Hello"; + public string Prop2 { get; } = "Hello"; public string Prop3 { get; private set; } public string Prop4 { get; protected set; } public string Prop5 { get; private set; } @@ -31,13 +31,13 @@ public void SetOtherProps(string prop3, string prop4, string prop5) Prop5 = prop5; } } - + enum Education { School, HighSchool, College } public override void OneTimeSetUp() { base.OneTimeSetUp(); - + NoMonkeyBusiness(); KeepDataBetweenTests(); @@ -86,7 +86,7 @@ public void ShouldFailIfReadOnlyNotMapped() [Test, Order(2)] public void ShouldWorkIfProp2IsSaveOnly() { - var map = ((IDocumentMap)new UserMap()).Build(); + var map = ((IDocumentMap)new UserMap()).Build(Configuration.PrimaryKeyHandlers); // Pretend the user edited their document map to set it to SaveOnly ((IColumnMappingBuilder) map.Columns.Single(c => c.ColumnName == "Prop2")).SaveOnly(); Configuration.DocumentMaps.Register(map); @@ -105,9 +105,9 @@ public void ShouldInsert() Prop1 = "Prop1", FirstName = "Fred" }; - + user.SetOtherProps("Prop3", "Prop4", "Prop5"); - + transaction.Insert(user); transaction.Update(user); transaction.Commit(); @@ -117,7 +117,7 @@ public void ShouldInsert() public void ShouldLoad() { using var transaction = Store.BeginTransaction(); - + var user = transaction.Load("users-123"); user.Should().NotBeNull(); user.Age.Should().Be(18); @@ -132,7 +132,7 @@ public void ShouldLoad() public void ShouldDelete() { using var transaction = Store.BeginTransaction(); - + transaction.Delete("users-123"); var user = transaction.Load("users-123"); user.Should().BeNull(); diff --git a/source/Nevermore.IntegrationTests/Model/StringTinyTypeIdKeyHandler.cs b/source/Nevermore.IntegrationTests/Model/StringTinyTypeIdKeyHandler.cs index 7cb4f289..19deeccd 100644 --- a/source/Nevermore.IntegrationTests/Model/StringTinyTypeIdKeyHandler.cs +++ b/source/Nevermore.IntegrationTests/Model/StringTinyTypeIdKeyHandler.cs @@ -16,8 +16,9 @@ class StringCustomIdTypeIdKeyHandler : IStringBasedPrimitivePrimaryKeyHandler return stringCustomType.Value; } - public object FormatKey(string tableName, int key) + public object GetNextKey(IKeyAllocator keyAllocator, string tableName) { + var key = keyAllocator.NextId(tableName); return CustomIdType.Create($"{GetPrefix(tableName)}-{key}")!; } diff --git a/source/Nevermore.IntegrationTests/RelatedDocumentTableFixture.cs b/source/Nevermore.IntegrationTests/RelatedDocumentTableFixture.cs index 0a17b53a..f1a52719 100644 --- a/source/Nevermore.IntegrationTests/RelatedDocumentTableFixture.cs +++ b/source/Nevermore.IntegrationTests/RelatedDocumentTableFixture.cs @@ -245,7 +245,7 @@ public void DeleteWithQueryBuilder() RelatedDocumentRow[] GetReferencesFromDb() { - var map = ((IDocumentMap)new OrderMap()).Build().RelatedDocumentsMappings.First(); + var map = ((IDocumentMap)new OrderMap()).Build(Configuration.PrimaryKeyHandlers).RelatedDocumentsMappings.First(); Func Callback() => reader @@ -302,7 +302,7 @@ public override int GetHashCode() return hashCode; } } - + public static bool operator ==(RelatedDocumentRow left, RelatedDocumentRow right) => Equals(left, right); public static bool operator !=(RelatedDocumentRow left, RelatedDocumentRow right) => !Equals(left, right); } diff --git a/source/Nevermore.IntegrationTests/SetUp/FixtureWithRelationalStore.cs b/source/Nevermore.IntegrationTests/SetUp/FixtureWithRelationalStore.cs index fd62e219..7511da85 100644 --- a/source/Nevermore.IntegrationTests/SetUp/FixtureWithRelationalStore.cs +++ b/source/Nevermore.IntegrationTests/SetUp/FixtureWithRelationalStore.cs @@ -38,7 +38,6 @@ protected FixtureWithRelationalStore() ApplicationName = "Nevermore-IntegrationTests", DefaultSchema = "TestSchema" }; - config.DocumentMaps.Register(documentMaps); config.TypeHandlers.Register(new ReferenceCollectionTypeHandler()); config.TypeHandlers.Register(new StringCustomIdTypeHandler()); @@ -48,12 +47,14 @@ protected FixtureWithRelationalStore() config.InstanceTypeResolvers.Register(new ProductTypeResolver()); config.InstanceTypeResolvers.Register(new BrandTypeResolver()); + config.DocumentMaps.Register(documentMaps); + config.UseJsonNetSerialization(settings => { settings.ConstructorHandling = ConstructorHandling.AllowNonPublicDefaultConstructor; }); - GenerateSchemaAutomatically(documentMaps); + GenerateSchemaAutomatically(config, documentMaps); Store = new RelationalStore(config); } @@ -87,14 +88,14 @@ protected void KeepDataBetweenTests() resetBetweenTests = false; } - void GenerateSchemaAutomatically(params IDocumentMap[] mappings) + void GenerateSchemaAutomatically(RelationalStoreConfiguration configuration, params IDocumentMap[] mappings) { try { var schema = new StringBuilder(); foreach (var map in mappings) { - SchemaGenerator.WriteTableSchema(map.Build(), null, schema); + SchemaGenerator.WriteTableSchema(map.Build(configuration.PrimaryKeyHandlers), null, schema); } integrationTestDatabase.ExecuteScript(schema.ToString()); } diff --git a/source/Nevermore.Tests/CommandParameterValuesFixture.cs b/source/Nevermore.Tests/CommandParameterValuesFixture.cs index 9342b245..e096971b 100644 --- a/source/Nevermore.Tests/CommandParameterValuesFixture.cs +++ b/source/Nevermore.Tests/CommandParameterValuesFixture.cs @@ -25,7 +25,7 @@ public void ShouldReplaceParametersWithExactMatch() command.Parameters["someId_2"].Value.Should().Be("id2"); command.Parameters["someIdentifier"].Value.Should().Be("value"); } - + [Test] public void ShouldReplaceParametersWithExactMatchEndOfQuery() { @@ -91,19 +91,19 @@ public Map() Column(c => c.LastName).MaxLength(256); } } - + [Test] public void ShouldUseMaxLengthsWhenAvailable() { var mapping = new Map(); - + var parameters = new CommandParameterValues(); parameters["FirstName"] = "Fred"; parameters["LastName"] = "Fred"; - + var command = new SqlCommand($"SELECT BLAH BLAH"); - - parameters.ContributeTo(command, new TypeHandlerRegistry(), ((IDocumentMap)mapping).Build()); + + parameters.ContributeTo(command, new TypeHandlerRegistry(), ((IDocumentMap)mapping).Build(new PrimaryKeyHandlerRegistry())); command.Parameters["FirstName"].Size.Should().Be(128); command.Parameters["LastName"].Size.Should().Be(256); diff --git a/source/Nevermore/Advanced/ReadTransaction.cs b/source/Nevermore/Advanced/ReadTransaction.cs index b3a49808..ed0042c1 100644 --- a/source/Nevermore/Advanced/ReadTransaction.cs +++ b/source/Nevermore/Advanced/ReadTransaction.cs @@ -13,7 +13,6 @@ using Microsoft.Data.SqlClient; using Nevermore.Diagnositcs; using Nevermore.Diagnostics; -using Nevermore.Mapping; using Nevermore.Querying.AST; using Nevermore.Transient; @@ -578,11 +577,14 @@ PreparedCommand PrepareLoad(TKey id) { var mapping = configuration.DocumentMaps.Resolve(typeof(TDocument)); - if (mapping.IdColumn?.Type != typeof(TKey)) + if (mapping.IdColumn is null) + throw new InvalidOperationException($"Cannot load {mapping.Type.Name} by Id, as no Id column has been mapped."); + + if (mapping.IdColumn.Type != typeof(TKey)) 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", mapping.IdColumn.PrimaryKeyHandler is IPrimitivePrimaryKeyHandler primitive ? primitive.ConvertToPrimitiveValue(id) : id }}; + var args = new CommandParameterValues {{ "Id", mapping.IdColumn.PrimaryKeyHandler.ConvertToPrimitiveValue(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); } diff --git a/source/Nevermore/Advanced/WriteTransaction.cs b/source/Nevermore/Advanced/WriteTransaction.cs index 548ff1e5..209d3fd7 100644 --- a/source/Nevermore/Advanced/WriteTransaction.cs +++ b/source/Nevermore/Advanced/WriteTransaction.cs @@ -222,31 +222,25 @@ public TKey AllocateId() TKey AllocateIdForMapping(DocumentMap mapping) { - var handler = configuration.PrimaryKeyHandlers.Resolve(mapping); - if (handler == null || !(handler is IPrimitivePrimaryKeyHandler primitivePrimaryKeyHandler)) - throw new InvalidOperationException($"Primary key handler could not be resolved for type {mapping.Type}, or it is configured to use an identity key handler."); + if (mapping.IdColumn?.Direction == ColumnDirection.FromDatabase) + throw new InvalidOperationException($"The document map for {mapping.Type} is configured to use an identity key handler."); - if (typeof(TKey) != primitivePrimaryKeyHandler.Type) - throw new ArgumentException($"The given key type of {typeof(TKey).Name} does not match the document maps primary key handler type {primitivePrimaryKeyHandler.Type.Name}"); - - return (TKey) AllocateIdUsingHandler(mapping, primitivePrimaryKeyHandler); + return (TKey) AllocateIdUsingHandler(mapping); } object AllocateId(DocumentMap mapping) { - var handler = configuration.PrimaryKeyHandlers.Resolve(mapping); - if (handler == null || !(handler is IPrimitivePrimaryKeyHandler primitivePrimaryKeyHandler)) - throw new InvalidOperationException($"Primary key handler could not be resolved for type {mapping.Type}, or it is configured to use an identity key handler."); + if (mapping.IdColumn?.Direction == ColumnDirection.FromDatabase) + throw new InvalidOperationException($"The document map for {mapping.Type} is configured to use an identity key handler."); - return AllocateIdUsingHandler(mapping, primitivePrimaryKeyHandler); + return AllocateIdUsingHandler(mapping); } - object AllocateIdUsingHandler(DocumentMap mapping, IPrimitivePrimaryKeyHandler primitivePrimaryKeyHandler) + object AllocateIdUsingHandler(DocumentMap mapping) { - var key = keyAllocator.NextId(mapping.TableName); - if (primitivePrimaryKeyHandler is IStringBasedPrimitivePrimaryKeyHandler stringBasedKeyHandler) - return stringBasedKeyHandler.FormatKey(mapping.TableName, key); - return key; + if (mapping.IdColumn is null || mapping.IsIdentityId) + throw new InvalidOperationException($"Cannot allocate an id when an Id column has not been mapped."); + return mapping.IdColumn.PrimaryKeyHandler.GetNextKey(keyAllocator, mapping.TableName); } public string AllocateId(string tableName, string idPrefix) diff --git a/source/Nevermore/Mapping/DocumentMap.cs b/source/Nevermore/Mapping/DocumentMap.cs index edbd6785..58aa5fb6 100644 --- a/source/Nevermore/Mapping/DocumentMap.cs +++ b/source/Nevermore/Mapping/DocumentMap.cs @@ -10,58 +10,39 @@ namespace Nevermore.Mapping { public abstract class DocumentMap : IDocumentMap { - readonly DocumentMap map = InitializeDefault(); + IIdColumnMappingBuilder? idColumn; + ColumnMapping? rowVersionColumn; + ColumnMapping? typeResolutionColumn; + readonly List columns = new List(); + readonly List relatedDocumentsMappings = new List(); + readonly List uniqueConstraints = new List(); protected DocumentMap() { + TableName = typeof(TDocument).Name; } /// /// Gets or sets the name of the schema containing the table that this document will be stored in. /// - protected string? SchemaName - { - get => map.SchemaName; - set => map.SchemaName = value; - } + protected string? SchemaName { get; set; } /// /// Gets or sets the name of the table that this document will be stored in. /// - protected string TableName - { - get => map.TableName; - set => map.TableName = value; - } - - /// - /// Gets or sets the primary key handler. - /// - protected IPrimaryKeyHandler? PrimaryKeyHandler - { - get => map.PrimaryKeyHandler; - set => map.PrimaryKeyHandler = value; - } + protected string TableName { get; set; } /// /// Tells Nevermore whether to expect large documents or not. Defaults to false, since most tables tend to only /// have small documents. However, this property is self-tuning: if Nevermore reads or writes a document /// larger than 1K, it will set this to true. /// - protected bool ExpectLargeDocuments - { - get => map.ExpectLargeDocuments; - set => map.ExpectLargeDocuments = value; - } + protected bool ExpectLargeDocuments { get; set; } /// /// Gets or sets the JSON storage mode. See https://github.com/OctopusDeploy/Nevermore/wiki/Compression for details. /// - protected JsonStorageFormat JsonStorageFormat - { - get => map.JsonStorageFormat; - set => map.JsonStorageFormat = value; - } + protected JsonStorageFormat JsonStorageFormat { get; set; } /// /// Configures the ID of the document. @@ -69,9 +50,10 @@ protected JsonStorageFormat JsonStorageFormat /// A builder to further configure the ID. protected IIdColumnMappingBuilder Id() { - if (map.IdColumn is null) - throw new InvalidOperationException($"Id was called for {typeof(TDocument)} but no Id property was found on this type."); - return map.IdColumn; + idColumn = GetDefaultIdColumn(); + if (idColumn is null) + throw new InvalidOperationException($"Unable to determine default Id property for {typeof(TDocument).Name}."); + return idColumn; } /// @@ -96,8 +78,8 @@ protected IIdColumnMappingBuilder Id(string? columnName, Expression @@ -126,9 +108,9 @@ protected IColumnMappingBuilder TypeResolutionColumn(string? columnNa { var prop = GetPropertyInfo(property) ?? throw new Exception("The expression for the Type Resolution column must be a property."); - map.TypeResolutionColumn = new ColumnMapping(columnName ?? prop.Name, typeof(TProperty), new PropertyHandler(prop), prop); - map.Columns.Add(map.TypeResolutionColumn); - return map.TypeResolutionColumn; + typeResolutionColumn = new ColumnMapping(columnName ?? prop.Name, typeof(TProperty), new PropertyHandler(prop), prop); + columns.Add(typeResolutionColumn); + return typeResolutionColumn; } /// @@ -144,7 +126,7 @@ protected IColumnMappingBuilder Column(Expression(Expression> getter) { - map.RowVersionColumn = (ColumnMapping)Column(null, getter, null).LoadOnly(); + rowVersionColumn = (ColumnMapping)Column(null, getter, null).LoadOnly(); } /// @@ -189,14 +171,14 @@ protected IColumnMappingBuilder Column(string? columnName, Expression protected IColumnMappingBuilder Column(string columnName, Type propertyType, IPropertyHandler handler, PropertyInfo? prop = null) { var column = new ColumnMapping(columnName, propertyType, handler, prop); - map.Columns.Add(column); + columns.Add(column); return column; } protected RelatedDocumentsMapping RelatedDocuments(Expression>> property, string tableName = DocumentMap.RelatedDocumentTableName, string? schemaName = null) { var mapping = new RelatedDocumentsMapping(GetPropertyInfo(property), tableName, schemaName); - map.RelatedDocumentsMappings.Add(mapping); + relatedDocumentsMappings.Add(mapping); return mapping; } @@ -210,7 +192,8 @@ protected RelatedDocumentsMapping RelatedDocuments(Expression(); } - public Type Type { get; set; } + public Type Type { get; } public IdColumnMapping? IdColumn { get; set; } public ColumnMapping? RowVersionColumn { get; set; } public ColumnMapping? TypeResolutionColumn { get; set; } public JsonStorageFormat JsonStorageFormat { get; set; } - public string TableName { get; set; } - - public IPrimaryKeyHandler? PrimaryKeyHandler { get; set; } + public string TableName { get; } public bool ExpectLargeDocuments { get; set; } @@ -309,7 +301,7 @@ public DocumentMap(Type type, string tableName) public bool IsRowVersioningEnabled => RowVersionColumn != null; - public bool IsIdentityId => IdColumn?.PrimaryKeyHandler is IIdentityPrimaryKeyHandler; + public bool IsIdentityId => IdColumn?.Direction == ColumnDirection.FromDatabase; public bool HasModificationOutputs =>IsRowVersioningEnabled || IsIdentityId; @@ -318,7 +310,7 @@ public void Validate() if (IdColumn == null) throw new InvalidOperationException($"There is no Id property on the document type {Type.FullName}"); - if (TypeResolutionColumn != null && JsonStorageFormat == Mapping.JsonStorageFormat.NoJson) + if (TypeResolutionColumn != null && JsonStorageFormat == JsonStorageFormat.NoJson) throw new InvalidOperationException($"The document map for type {Type.FullName} has a TypeColumn, but also uses the NoJson storage mode, which is not allowed."); try diff --git a/source/Nevermore/Mapping/DocumentMapRegistry.cs b/source/Nevermore/Mapping/DocumentMapRegistry.cs index cb58cb48..64ea7b2d 100644 --- a/source/Nevermore/Mapping/DocumentMapRegistry.cs +++ b/source/Nevermore/Mapping/DocumentMapRegistry.cs @@ -9,10 +9,12 @@ namespace Nevermore.Mapping { public class DocumentMapRegistry : IDocumentMapRegistry { + readonly IPrimaryKeyHandlerRegistry primaryKeyHandlerRegistry; readonly ConcurrentDictionary mappings = new ConcurrentDictionary(); - public DocumentMapRegistry() + public DocumentMapRegistry(IPrimaryKeyHandlerRegistry primaryKeyHandlerRegistry) { + this.primaryKeyHandlerRegistry = primaryKeyHandlerRegistry; } public List GetAll() @@ -40,7 +42,7 @@ public void Register(IEnumerable mappingsToAdd) { foreach (var mapping in mappingsToAdd) { - Register(mapping.Build()); + Register(mapping.Build(primaryKeyHandlerRegistry)); } } diff --git a/source/Nevermore/Mapping/GuidPrimaryKeyHandler.cs b/source/Nevermore/Mapping/GuidPrimaryKeyHandler.cs index 9b9062c6..4565a162 100644 --- a/source/Nevermore/Mapping/GuidPrimaryKeyHandler.cs +++ b/source/Nevermore/Mapping/GuidPrimaryKeyHandler.cs @@ -2,7 +2,11 @@ namespace Nevermore.Mapping { - class GuidPrimaryKeyHandler : PrimitivePrimaryKeyHandler + class GuidPrimaryKeyHandler : PrimaryKeyHandler { + public override object GetNextKey(IKeyAllocator keyAllocator, string tableName) + { + return Guid.NewGuid(); + } } } \ No newline at end of file diff --git a/source/Nevermore/Mapping/IDocumentMap.cs b/source/Nevermore/Mapping/IDocumentMap.cs index f5d3552a..d55c9f4b 100644 --- a/source/Nevermore/Mapping/IDocumentMap.cs +++ b/source/Nevermore/Mapping/IDocumentMap.cs @@ -2,6 +2,6 @@ namespace Nevermore.Mapping { public interface IDocumentMap { - DocumentMap Build(); + DocumentMap Build(IPrimaryKeyHandlerRegistry primaryKeyHandlerRegistry); } } \ No newline at end of file diff --git a/source/Nevermore/Mapping/IIdColumnMappingBuilder.cs b/source/Nevermore/Mapping/IIdColumnMappingBuilder.cs index ec720cc4..0ce5555f 100644 --- a/source/Nevermore/Mapping/IIdColumnMappingBuilder.cs +++ b/source/Nevermore/Mapping/IIdColumnMappingBuilder.cs @@ -28,5 +28,12 @@ public interface IIdColumnMappingBuilder : IColumnMappingBuilder /// /// The function to call back to format the id. IIdColumnMappingBuilder Format(Func<(string idPrefix, int key), string> format); + + /// + /// Builds the IdColumnMapping. + /// + /// The primary key handler registry, which must be populated with the required handler prior to the DocumentMaps being registered. + /// The built column mapping. + IdColumnMapping Build(IPrimaryKeyHandlerRegistry primaryKeyHandlerRegistry); } } \ No newline at end of file diff --git a/source/Nevermore/Mapping/IIdentityPrimaryKeyHandler.cs b/source/Nevermore/Mapping/IIdentityPrimaryKeyHandler.cs deleted file mode 100644 index 81e36d99..00000000 --- a/source/Nevermore/Mapping/IIdentityPrimaryKeyHandler.cs +++ /dev/null @@ -1,5 +0,0 @@ -namespace Nevermore.Mapping -{ - public interface IIdentityPrimaryKeyHandler : IPrimaryKeyHandler - {} -} \ No newline at end of file diff --git a/source/Nevermore/Mapping/IPrimaryKeyHandler.cs b/source/Nevermore/Mapping/IPrimaryKeyHandler.cs index d35e46ce..e25a7289 100644 --- a/source/Nevermore/Mapping/IPrimaryKeyHandler.cs +++ b/source/Nevermore/Mapping/IPrimaryKeyHandler.cs @@ -1,12 +1,28 @@ +#nullable enable using System; +using System.Diagnostics.CodeAnalysis; namespace Nevermore.Mapping { public interface IPrimaryKeyHandler { Type Type { get; } - } - public interface IPrimaryKeyHandler : IPrimaryKeyHandler - {} + /// + /// Convert the given id value to the underlying primitive type required for Sql command parameters. + /// + /// The id to convert. + /// The converted id. + [return: NotNullIfNotNull("id")] + object? ConvertToPrimitiveValue(object? id); + + /// + /// Get the next key for the given table. + /// + /// The keyAllocator has to be passed here as it is tied to the RelationalStore instance, and thus can't be specified at configuration time in the constructor. + /// The key allocator to use getting the next id from. + /// The table name the key is required for. + /// The next key, as the type that matches the model object's Id property type. ConvertToPrimitiveValue should be called with this value if it is to be used as a Sql parameter. + object GetNextKey(IKeyAllocator keyAllocator, string tableName); + } } \ No newline at end of file diff --git a/source/Nevermore/Mapping/IPrimitivePrimaryKeyHandler.cs b/source/Nevermore/Mapping/IPrimitivePrimaryKeyHandler.cs deleted file mode 100644 index 65dca800..00000000 --- a/source/Nevermore/Mapping/IPrimitivePrimaryKeyHandler.cs +++ /dev/null @@ -1,11 +0,0 @@ -#nullable enable -using System.Diagnostics.CodeAnalysis; - -namespace Nevermore.Mapping -{ - public interface IPrimitivePrimaryKeyHandler : IPrimaryKeyHandler - { - [return: NotNullIfNotNull("id")] - object? ConvertToPrimitiveValue(object? id); - } -} \ No newline at end of file diff --git a/source/Nevermore/Mapping/IStringBasedPrimitivePrimaryKeyHandler.cs b/source/Nevermore/Mapping/IStringBasedPrimitivePrimaryKeyHandler.cs index 53d46ad1..0c499732 100644 --- a/source/Nevermore/Mapping/IStringBasedPrimitivePrimaryKeyHandler.cs +++ b/source/Nevermore/Mapping/IStringBasedPrimitivePrimaryKeyHandler.cs @@ -2,7 +2,7 @@ namespace Nevermore.Mapping { - public interface IStringBasedPrimitivePrimaryKeyHandler : IPrimitivePrimaryKeyHandler + public interface IStringBasedPrimitivePrimaryKeyHandler : IPrimaryKeyHandler { /// /// Set a function that when given the TableName will return key prefix string. @@ -22,7 +22,5 @@ public interface IStringBasedPrimitivePrimaryKeyHandler : IPrimitivePrimaryKeyHa /// /// The function to call back to format the id. void SetFormat(Func<(string idPrefix, int key), string> format); - - object FormatKey(string tableName, int key); } } \ No newline at end of file diff --git a/source/Nevermore/Mapping/IdColumnMapping.cs b/source/Nevermore/Mapping/IdColumnMapping.cs index b07779bb..5738a2e6 100644 --- a/source/Nevermore/Mapping/IdColumnMapping.cs +++ b/source/Nevermore/Mapping/IdColumnMapping.cs @@ -5,7 +5,22 @@ namespace Nevermore.Mapping { - public class IdColumnMapping : ColumnMapping, IIdColumnMappingBuilder + public class IdColumnMapping : ColumnMapping + { + internal IdColumnMapping(string columnName, Type type, IPropertyHandler handler, PropertyInfo property, bool isIdentity, IPrimaryKeyHandler primaryKeyHandler) + : base(columnName, type, handler, property) + { + IsIdentity = isIdentity; + Direction = IsIdentity ? ColumnDirection.FromDatabase : ColumnDirection.Both; + PrimaryKeyHandler = primaryKeyHandler; + } + + public bool IsIdentity { get; } + + public IPrimaryKeyHandler PrimaryKeyHandler { get; } + } + + public class IdColumnMappingBuilder : ColumnMapping, IIdColumnMappingBuilder { static readonly HashSet ValidIdentityTypes = new HashSet { @@ -16,24 +31,25 @@ public class IdColumnMapping : ColumnMapping, IIdColumnMappingBuilder bool hasCustomPropertyHandler; - internal IdColumnMapping(string columnName, Type type, IPropertyHandler handler, PropertyInfo property) - : base(columnName, type, handler, property) - { } + internal IdColumnMappingBuilder(string columnName, Type type, IPropertyHandler handler, PropertyInfo property) : base(columnName, type, handler, property) + { + } + + bool IsIdentity { get; set; } - public IPrimaryKeyHandler? PrimaryKeyHandler { get; private set; } + IPrimaryKeyHandler? PrimaryKeyHandler { get; set; } /// public IIdColumnMappingBuilder Identity() { ValidateForIdentityUse(); - if (!(PrimaryKeyHandler is null) && !(PrimaryKeyHandler is IIdentityPrimaryKeyHandler)) + if (!(PrimaryKeyHandler is null)) throw new InvalidOperationException($"{nameof(KeyHandler)} has already been set to a non-identity handler."); - var handlerType = typeof(IdentityPrimaryKeyHandler<>).MakeGenericType(Type); - var keyHandler = (IIdentityPrimaryKeyHandler)Activator.CreateInstance(handlerType); + IsIdentity = true; - return KeyHandler(keyHandler); + return this; } void ValidateForIdentityUse() @@ -47,15 +63,9 @@ void ValidateForIdentityUse() public IIdColumnMappingBuilder KeyHandler(IPrimaryKeyHandler primaryKeyHandler) { - if (!(PrimaryKeyHandler is null) && Direction == ColumnDirection.FromDatabase && !(primaryKeyHandler is IIdentityPrimaryKeyHandler)) + if (!(PrimaryKeyHandler is null) && Direction == ColumnDirection.FromDatabase) throw new InvalidOperationException($"{nameof(KeyHandler)} can only be called with an IIdentityPrimaryKeyHandler, once {nameof(Identity)} has been called."); - if (primaryKeyHandler is IIdentityPrimaryKeyHandler) - { - ValidateForIdentityUse(); - Direction = ColumnDirection.FromDatabase; - } - PrimaryKeyHandler = primaryKeyHandler; return this; @@ -67,7 +77,7 @@ public IIdColumnMappingBuilder KeyHandler(IPrimaryKeyHandler primaryKeyHandler) /// The function to call back to get the prefix. public IIdColumnMappingBuilder Prefix(Func idPrefix) { - if (!(PrimaryKeyHandler is null) && Direction == ColumnDirection.FromDatabase && PrimaryKeyHandler is IIdentityPrimaryKeyHandler) + if (Direction == ColumnDirection.FromDatabase) throw new InvalidOperationException($"{nameof(Prefix)} cannot be set when an identity key handler has been configured."); if (PrimaryKeyHandler == null) @@ -88,7 +98,7 @@ public IIdColumnMappingBuilder Prefix(Func idPrefix) /// The function to call back to format the id. public IIdColumnMappingBuilder Format(Func<(string idPrefix, int key), string> format) { - if (!(PrimaryKeyHandler is null) && Direction == ColumnDirection.FromDatabase && PrimaryKeyHandler is IIdentityPrimaryKeyHandler) + if (!(PrimaryKeyHandler is null) && Direction == ColumnDirection.FromDatabase) throw new InvalidOperationException($"{nameof(Format)} cannot be set when an identity key handler has been configured."); if (PrimaryKeyHandler == null) @@ -105,11 +115,24 @@ public IIdColumnMappingBuilder Format(Func<(string idPrefix, int key), string> f protected override void SetCustomPropertyHandler(IPropertyHandler propertyHandler) { - if (PrimaryKeyHandler is IIdentityPrimaryKeyHandler) + if (Direction == ColumnDirection.FromDatabase) throw new InvalidOperationException("Unable to configure an Identity Id column with a custom PropertyHandler"); hasCustomPropertyHandler = true; base.SetCustomPropertyHandler(propertyHandler); } + + public IdColumnMapping Build(IPrimaryKeyHandlerRegistry primaryKeyHandlerRegistry) + { + var primaryKeyHandler = PrimaryKeyHandler; + if (primaryKeyHandler is null) + primaryKeyHandler = primaryKeyHandlerRegistry.Resolve(Type); + + if (primaryKeyHandler is null) + throw new InvalidOperationException($"Unable to determine a primary key handler for type {Type.Name}. This could happen if the custom PrimaryKeyHandlers are not registered prior to registering the DocumentMaps"); + + var mapping = new IdColumnMapping(ColumnName, Type, PropertyHandler, Property, IsIdentity, primaryKeyHandler); + return mapping; + } } } \ No newline at end of file diff --git a/source/Nevermore/Mapping/IdentityPrimaryKeyHandler.cs b/source/Nevermore/Mapping/IdentityPrimaryKeyHandler.cs deleted file mode 100644 index 54dd1da9..00000000 --- a/source/Nevermore/Mapping/IdentityPrimaryKeyHandler.cs +++ /dev/null @@ -1,9 +0,0 @@ -using System; - -namespace Nevermore.Mapping -{ - public class IdentityPrimaryKeyHandler : IIdentityPrimaryKeyHandler - { - public Type Type => typeof(T); - } -} \ No newline at end of file diff --git a/source/Nevermore/Mapping/IntPrimaryKeyHandler.cs b/source/Nevermore/Mapping/IntPrimaryKeyHandler.cs index 1a0b966b..8cbf9f87 100644 --- a/source/Nevermore/Mapping/IntPrimaryKeyHandler.cs +++ b/source/Nevermore/Mapping/IntPrimaryKeyHandler.cs @@ -1,6 +1,10 @@ namespace Nevermore.Mapping { - class IntPrimaryKeyHandler : PrimitivePrimaryKeyHandler + class IntPrimaryKeyHandler : PrimaryKeyHandler { + public override object GetNextKey(IKeyAllocator keyAllocator, string tableName) + { + return keyAllocator.NextId(tableName); + } } } \ No newline at end of file diff --git a/source/Nevermore/Mapping/LongPrimaryKeyHandler.cs b/source/Nevermore/Mapping/LongPrimaryKeyHandler.cs index 62ad6d0a..361a80be 100644 --- a/source/Nevermore/Mapping/LongPrimaryKeyHandler.cs +++ b/source/Nevermore/Mapping/LongPrimaryKeyHandler.cs @@ -1,6 +1,10 @@ namespace Nevermore.Mapping { - class LongPrimaryKeyHandler : PrimitivePrimaryKeyHandler + class LongPrimaryKeyHandler : PrimaryKeyHandler { + public override object GetNextKey(IKeyAllocator keyAllocator, string tableName) + { + return keyAllocator.NextId(tableName); + } } } \ No newline at end of file diff --git a/source/Nevermore/Mapping/PrimaryKeyHandlerRegistry.cs b/source/Nevermore/Mapping/PrimaryKeyHandlerRegistry.cs index df11b5a0..5b3bf408 100644 --- a/source/Nevermore/Mapping/PrimaryKeyHandlerRegistry.cs +++ b/source/Nevermore/Mapping/PrimaryKeyHandlerRegistry.cs @@ -11,7 +11,7 @@ public interface IPrimaryKeyHandlerRegistry { void Register(IPrimaryKeyHandler strategy); - IPrimaryKeyHandler? Resolve(DocumentMap documentMap); + IPrimaryKeyHandler? Resolve(Type columnType); } class PrimaryKeyHandlerRegistry : IPrimaryKeyHandlerRegistry @@ -19,20 +19,22 @@ class PrimaryKeyHandlerRegistry : IPrimaryKeyHandlerRegistry // maps key type to IPrimaryKeyHandler concrete type readonly ConcurrentDictionary mappings = new ConcurrentDictionary(); + public PrimaryKeyHandlerRegistry() + { + mappings.TryAdd(typeof(string), new StringPrimaryKeyHandler()); + mappings.TryAdd(typeof(int), new IntPrimaryKeyHandler()); + mappings.TryAdd(typeof(long), new LongPrimaryKeyHandler()); + mappings.TryAdd(typeof(Guid), new GuidPrimaryKeyHandler()); + } + public void Register(IPrimaryKeyHandler handler) { mappings[handler.Type] = handler; } - public IPrimaryKeyHandler? Resolve(DocumentMap documentMap) + public IPrimaryKeyHandler? Resolve(Type columnType) { - if (!(documentMap.PrimaryKeyHandler is null)) - return documentMap.PrimaryKeyHandler; - - if (documentMap.IdColumn is null) - throw new InvalidOperationException($"Map for document type {documentMap.Type.Name} does not specify an Id column."); - - var idType = documentMap.IdColumn.Type; + var idType = columnType; var maps = new List(); diff --git a/source/Nevermore/Mapping/PrimitivePrimaryKeyHandler.cs b/source/Nevermore/Mapping/PrimitivePrimaryKeyHandler.cs index 9df5a5d4..79718c1f 100644 --- a/source/Nevermore/Mapping/PrimitivePrimaryKeyHandler.cs +++ b/source/Nevermore/Mapping/PrimitivePrimaryKeyHandler.cs @@ -4,7 +4,7 @@ namespace Nevermore.Mapping { - public abstract class PrimitivePrimaryKeyHandler : IPrimitivePrimaryKeyHandler + public abstract class PrimaryKeyHandler : IPrimaryKeyHandler { public Type Type => typeof(T); @@ -13,5 +13,7 @@ public abstract class PrimitivePrimaryKeyHandler : IPrimitivePrimaryKeyHandle { return id; } + + public abstract object GetNextKey(IKeyAllocator keyAllocator, string tableName); } } \ No newline at end of file diff --git a/source/Nevermore/Mapping/StringPrimaryKeyHandler.cs b/source/Nevermore/Mapping/StringPrimaryKeyHandler.cs index e864926d..1167ee10 100644 --- a/source/Nevermore/Mapping/StringPrimaryKeyHandler.cs +++ b/source/Nevermore/Mapping/StringPrimaryKeyHandler.cs @@ -3,7 +3,7 @@ namespace Nevermore.Mapping { - class StringPrimaryKeyHandler : PrimitivePrimaryKeyHandler, IStringBasedPrimitivePrimaryKeyHandler + class StringPrimaryKeyHandler : PrimaryKeyHandler, IStringBasedPrimitivePrimaryKeyHandler { Func idPrefixFunc; Func<(string idPrefix, int key), string> formatFunc; @@ -36,9 +36,10 @@ public void SetFormat(Func<(string idPrefix, int key), string> format) formatFunc = format; } - public object FormatKey(string tableName, int key) + public override object GetNextKey(IKeyAllocator keyAllocator, string tableName) { - return formatFunc((GetPrefix(tableName), key)); + var nextKey = keyAllocator.NextId(tableName); + return formatFunc((GetPrefix(tableName), nextKey)); } } } \ No newline at end of file diff --git a/source/Nevermore/RelationalStoreConfiguration.cs b/source/Nevermore/RelationalStoreConfiguration.cs index 08bbaf4f..97322d60 100644 --- a/source/Nevermore/RelationalStoreConfiguration.cs +++ b/source/Nevermore/RelationalStoreConfiguration.cs @@ -27,7 +27,6 @@ public RelationalStoreConfiguration(string connectionString) : this(() => connec public RelationalStoreConfiguration(Func connectionStringFunc) { CommandFactory = new SqlCommandFactory(); - DocumentMaps = new DocumentMapRegistry(); KeyBlockSize = NevermoreDefaults.DefaultKeyBlockSize; InstanceTypeResolvers = new InstanceTypeRegistry(); RelatedDocumentStore = new EmptyRelatedDocumentStore(); @@ -47,10 +46,8 @@ public RelationalStoreConfiguration(Func connectionStringFunc) TypeHandlers = new TypeHandlerRegistry(); PrimaryKeyHandlers = new PrimaryKeyHandlerRegistry(); - PrimaryKeyHandlers.Register(new StringPrimaryKeyHandler()); - PrimaryKeyHandlers.Register(new IntPrimaryKeyHandler()); - PrimaryKeyHandlers.Register(new LongPrimaryKeyHandler()); - PrimaryKeyHandlers.Register(new GuidPrimaryKeyHandler()); + + DocumentMaps = new DocumentMapRegistry(PrimaryKeyHandlers); AllowSynchronousOperations = true; diff --git a/source/Nevermore/Util/DataModificationQueryBuilder.cs b/source/Nevermore/Util/DataModificationQueryBuilder.cs index 6f471df5..d65ce099 100644 --- a/source/Nevermore/Util/DataModificationQueryBuilder.cs +++ b/source/Nevermore/Util/DataModificationQueryBuilder.cs @@ -39,7 +39,7 @@ public PreparedCommand PrepareInsert(IReadOnlyList documents, InsertOpti options ??= InsertOptions.Default; var mapping = GetMapping(documents); - if (mapping.IdColumn?.PrimaryKeyHandler is IIdentityPrimaryKeyHandler && options.CustomAssignedId != null) + if (mapping.IdColumn?.Direction == ColumnDirection.FromDatabase && options.CustomAssignedId != null) throw new InvalidOperationException($"{nameof(InsertOptions)}.{nameof(InsertOptions.CustomAssignedId)} is not supported for identity Id columns."); var sb = new StringBuilder(); @@ -309,9 +309,8 @@ CommandParameterValues GetDocumentParameters(Func allocateI var result = new CommandParameterValues(); - //we never want to allocate id's if the Id column is an Identity - var keyHandler = configuration.PrimaryKeyHandlers.Resolve(mapping); - if (keyHandler is IPrimitivePrimaryKeyHandler primitiveKeyHandler) + // we never want to allocate id's if the Id column is an Identity + if (!mapping.IdColumn.IsIdentity) { // check whether the object's Id has already been provided, if not then we'll either use the one from the InsertOptions or we'll generate one if (id == null) @@ -319,11 +318,12 @@ CommandParameterValues GetDocumentParameters(Func allocateI id = customAssignedId == null || (customAssignedId is string assignedId && string.IsNullOrWhiteSpace(assignedId)) ? allocateId(mapping) : customAssignedId; mapping.IdColumn.PropertyHandler.Write(document, id); } - - var primitiveValue = primitiveKeyHandler.ConvertToPrimitiveValue(id); - result[$"{prefix}{mapping.IdColumn.ColumnName}"] = primitiveValue; } + var keyHandler = mapping.IdColumn.PrimaryKeyHandler; + var primitiveValue = keyHandler.ConvertToPrimitiveValue(id); + result[$"{prefix}{mapping.IdColumn.ColumnName}"] = primitiveValue; + switch (mapping.JsonStorageFormat) { case JsonStorageFormat.TextOnly: From df2c99e41bd117e1f9f5cad688a73f37931223cb Mon Sep 17 00:00:00 2001 From: Pawel Pabich Date: Mon, 21 Jun 2021 23:20:08 +1000 Subject: [PATCH 15/25] Failing test for custom prefix --- .../Model/DocumentWithCustomPrefix.cs | 15 +++++++++++++++ .../Model/DocumentWithCustomPrefixMap.cs | 15 +++++++++++++++ .../RelationalTransaction/LoadFixture.cs | 16 ++++++++++++++++ .../SetUp/FixtureWithRelationalStore.cs | 4 +++- 4 files changed, 49 insertions(+), 1 deletion(-) create mode 100644 source/Nevermore.IntegrationTests/Model/DocumentWithCustomPrefix.cs create mode 100644 source/Nevermore.IntegrationTests/Model/DocumentWithCustomPrefixMap.cs diff --git a/source/Nevermore.IntegrationTests/Model/DocumentWithCustomPrefix.cs b/source/Nevermore.IntegrationTests/Model/DocumentWithCustomPrefix.cs new file mode 100644 index 00000000..888f4c44 --- /dev/null +++ b/source/Nevermore.IntegrationTests/Model/DocumentWithCustomPrefix.cs @@ -0,0 +1,15 @@ +namespace Nevermore.IntegrationTests.Model +{ + public class DocumentWithCustomPrefix + { + public CustomPrefixId Id { get; set; } + public string Name { get; set; } + } + + public class CustomPrefixId : StringCustomIdType + { + internal CustomPrefixId(string value) : base(value) + { + } + } +} \ No newline at end of file diff --git a/source/Nevermore.IntegrationTests/Model/DocumentWithCustomPrefixMap.cs b/source/Nevermore.IntegrationTests/Model/DocumentWithCustomPrefixMap.cs new file mode 100644 index 00000000..937a0665 --- /dev/null +++ b/source/Nevermore.IntegrationTests/Model/DocumentWithCustomPrefixMap.cs @@ -0,0 +1,15 @@ +using Nevermore.Mapping; + +namespace Nevermore.IntegrationTests.Model +{ + public class DocumentWithCustomPrefixMap : DocumentMap + { + public const string CustomPrefix = "CustomPrefix"; + + public DocumentWithCustomPrefixMap() + { + Id().Prefix(_ => CustomPrefix); + Column(m => m.Name); + } + } +} \ No newline at end of file diff --git a/source/Nevermore.IntegrationTests/RelationalTransaction/LoadFixture.cs b/source/Nevermore.IntegrationTests/RelationalTransaction/LoadFixture.cs index 2151cc7e..6b28ce14 100644 --- a/source/Nevermore.IntegrationTests/RelationalTransaction/LoadFixture.cs +++ b/source/Nevermore.IntegrationTests/RelationalTransaction/LoadFixture.cs @@ -422,5 +422,21 @@ public void LoadManyByWrongIdType_ShouldThrowArgumentException() target.ShouldThrow().Which.Message.Should().Be("Provided Id of type 'System.String' does not match configured type of 'System.Guid'."); } + + [Test] + public void StoreAndLoadWithCustomPrefix() + { + using (var trn = Store.BeginTransaction()) + { + var document = new DocumentWithCustomPrefix() + { + Name = "test" + }; + + trn.Insert(document); + + document.Id.Value.Should().StartWith(DocumentWithCustomPrefixMap.CustomPrefix); + } + } } } \ No newline at end of file diff --git a/source/Nevermore.IntegrationTests/SetUp/FixtureWithRelationalStore.cs b/source/Nevermore.IntegrationTests/SetUp/FixtureWithRelationalStore.cs index 7511da85..6b5d206a 100644 --- a/source/Nevermore.IntegrationTests/SetUp/FixtureWithRelationalStore.cs +++ b/source/Nevermore.IntegrationTests/SetUp/FixtureWithRelationalStore.cs @@ -29,7 +29,8 @@ protected FixtureWithRelationalStore() new MessageWithGuidIdMap(), new DocumentWithRowVersionMap(), new DocumentWithIdentityIdMap(), - new DocumentWithIdentityIdAndRowVersionMap() + new DocumentWithIdentityIdAndRowVersionMap(), + new DocumentWithCustomPrefixMap() }; var config = new RelationalStoreConfiguration(ConnectionString) @@ -43,6 +44,7 @@ protected FixtureWithRelationalStore() config.TypeHandlers.Register(new StringCustomIdTypeHandler()); config.PrimaryKeyHandlers.Register(new StringCustomIdTypeIdKeyHandler()); + config.PrimaryKeyHandlers.Register(new StringCustomIdTypeIdKeyHandler()); config.InstanceTypeResolvers.Register(new ProductTypeResolver()); config.InstanceTypeResolvers.Register(new BrandTypeResolver()); From fd30294a10a95cbef7538f701a26d1b8c60dd937 Mon Sep 17 00:00:00 2001 From: Pawel Pabich Date: Tue, 22 Jun 2021 00:06:46 +1000 Subject: [PATCH 16/25] The usage of custom Prefix resulted in a wrong type of id handler being assigned. --- .../Model/{TinyType.cs => CustomIdType.cs} | 0 .../Model/CustomPrefixIdKeyHandler.cs | 11 ++++ .../Model/DocumentWithCustomPrefixMap.cs | 4 +- ...andler.cs => StringCustomIdTypeHandler.cs} | 0 ...r.cs => StringCustomIdTypeIdKeyHandler.cs} | 25 +++------ .../RelationalTransaction/LoadFixture.cs | 2 +- .../SetUp/FixtureWithRelationalStore.cs | 2 +- .../Mapping/IIdColumnMappingBuilder.cs | 21 +------ .../IStringBasedPrimitivePrimaryKeyHandler.cs | 26 --------- source/Nevermore/Mapping/IdColumnMapping.cs | 55 ------------------- .../Mapping/PrimitivePrimaryKeyHandler.cs | 2 +- .../Mapping/StringPrimaryKeyHandler.cs | 36 +++--------- 12 files changed, 31 insertions(+), 153 deletions(-) rename source/Nevermore.IntegrationTests/Model/{TinyType.cs => CustomIdType.cs} (100%) create mode 100644 source/Nevermore.IntegrationTests/Model/CustomPrefixIdKeyHandler.cs rename source/Nevermore.IntegrationTests/Model/{StringTinyTypeIdTypeHandler.cs => StringCustomIdTypeHandler.cs} (100%) rename source/Nevermore.IntegrationTests/Model/{StringTinyTypeIdKeyHandler.cs => StringCustomIdTypeIdKeyHandler.cs} (54%) delete mode 100644 source/Nevermore/Mapping/IStringBasedPrimitivePrimaryKeyHandler.cs diff --git a/source/Nevermore.IntegrationTests/Model/TinyType.cs b/source/Nevermore.IntegrationTests/Model/CustomIdType.cs similarity index 100% rename from source/Nevermore.IntegrationTests/Model/TinyType.cs rename to source/Nevermore.IntegrationTests/Model/CustomIdType.cs diff --git a/source/Nevermore.IntegrationTests/Model/CustomPrefixIdKeyHandler.cs b/source/Nevermore.IntegrationTests/Model/CustomPrefixIdKeyHandler.cs new file mode 100644 index 00000000..7a8f80f0 --- /dev/null +++ b/source/Nevermore.IntegrationTests/Model/CustomPrefixIdKeyHandler.cs @@ -0,0 +1,11 @@ +namespace Nevermore.IntegrationTests.Model +{ + class CustomPrefixIdKeyHandler : StringCustomIdTypeIdKeyHandler + { + public const string CustomPrefix = "CustomPrefix"; + + public CustomPrefixIdKeyHandler():base(CustomPrefix) + { + } + } +} \ No newline at end of file diff --git a/source/Nevermore.IntegrationTests/Model/DocumentWithCustomPrefixMap.cs b/source/Nevermore.IntegrationTests/Model/DocumentWithCustomPrefixMap.cs index 937a0665..f294808b 100644 --- a/source/Nevermore.IntegrationTests/Model/DocumentWithCustomPrefixMap.cs +++ b/source/Nevermore.IntegrationTests/Model/DocumentWithCustomPrefixMap.cs @@ -4,11 +4,9 @@ namespace Nevermore.IntegrationTests.Model { public class DocumentWithCustomPrefixMap : DocumentMap { - public const string CustomPrefix = "CustomPrefix"; - public DocumentWithCustomPrefixMap() { - Id().Prefix(_ => CustomPrefix); + Id(); Column(m => m.Name); } } diff --git a/source/Nevermore.IntegrationTests/Model/StringTinyTypeIdTypeHandler.cs b/source/Nevermore.IntegrationTests/Model/StringCustomIdTypeHandler.cs similarity index 100% rename from source/Nevermore.IntegrationTests/Model/StringTinyTypeIdTypeHandler.cs rename to source/Nevermore.IntegrationTests/Model/StringCustomIdTypeHandler.cs diff --git a/source/Nevermore.IntegrationTests/Model/StringTinyTypeIdKeyHandler.cs b/source/Nevermore.IntegrationTests/Model/StringCustomIdTypeIdKeyHandler.cs similarity index 54% rename from source/Nevermore.IntegrationTests/Model/StringTinyTypeIdKeyHandler.cs rename to source/Nevermore.IntegrationTests/Model/StringCustomIdTypeIdKeyHandler.cs index 19deeccd..ab2ce4f1 100644 --- a/source/Nevermore.IntegrationTests/Model/StringTinyTypeIdKeyHandler.cs +++ b/source/Nevermore.IntegrationTests/Model/StringCustomIdTypeIdKeyHandler.cs @@ -4,11 +4,17 @@ namespace Nevermore.IntegrationTests.Model { - class StringCustomIdTypeIdKeyHandler : IStringBasedPrimitivePrimaryKeyHandler + class StringCustomIdTypeIdKeyHandler : IPrimaryKeyHandler where T : StringCustomIdType { + readonly string? customPrefix; public Type Type => typeof(T); + public StringCustomIdTypeIdKeyHandler(string? customPrefix = null) + { + this.customPrefix = customPrefix; + } + public object? ConvertToPrimitiveValue(object? id) { if (!(id is StringCustomIdType stringCustomType)) @@ -19,22 +25,7 @@ class StringCustomIdTypeIdKeyHandler : IStringBasedPrimitivePrimaryKeyHandler public object GetNextKey(IKeyAllocator keyAllocator, string tableName) { var key = keyAllocator.NextId(tableName); - return CustomIdType.Create($"{GetPrefix(tableName)}-{key}")!; - } - - public void SetPrefix(Func idPrefix) - { - throw new NotImplementedException(); - } - - public string GetPrefix(string tableName) - { - return $"{tableName}s"; - } - - public void SetFormat(Func<(string idPrefix, int key), string> format) - { - throw new NotImplementedException(); + return CustomIdType.Create($"{customPrefix ?? tableName}s-{key}")!; } } } \ No newline at end of file diff --git a/source/Nevermore.IntegrationTests/RelationalTransaction/LoadFixture.cs b/source/Nevermore.IntegrationTests/RelationalTransaction/LoadFixture.cs index 6b28ce14..b7d138c8 100644 --- a/source/Nevermore.IntegrationTests/RelationalTransaction/LoadFixture.cs +++ b/source/Nevermore.IntegrationTests/RelationalTransaction/LoadFixture.cs @@ -435,7 +435,7 @@ public void StoreAndLoadWithCustomPrefix() trn.Insert(document); - document.Id.Value.Should().StartWith(DocumentWithCustomPrefixMap.CustomPrefix); + document.Id.Value.Should().StartWith(CustomPrefixIdKeyHandler.CustomPrefix); } } } diff --git a/source/Nevermore.IntegrationTests/SetUp/FixtureWithRelationalStore.cs b/source/Nevermore.IntegrationTests/SetUp/FixtureWithRelationalStore.cs index 6b5d206a..c76b8f84 100644 --- a/source/Nevermore.IntegrationTests/SetUp/FixtureWithRelationalStore.cs +++ b/source/Nevermore.IntegrationTests/SetUp/FixtureWithRelationalStore.cs @@ -44,7 +44,7 @@ protected FixtureWithRelationalStore() config.TypeHandlers.Register(new StringCustomIdTypeHandler()); config.PrimaryKeyHandlers.Register(new StringCustomIdTypeIdKeyHandler()); - config.PrimaryKeyHandlers.Register(new StringCustomIdTypeIdKeyHandler()); + config.PrimaryKeyHandlers.Register(new CustomPrefixIdKeyHandler()); config.InstanceTypeResolvers.Register(new ProductTypeResolver()); config.InstanceTypeResolvers.Register(new BrandTypeResolver()); diff --git a/source/Nevermore/Mapping/IIdColumnMappingBuilder.cs b/source/Nevermore/Mapping/IIdColumnMappingBuilder.cs index 0ce5555f..0414569b 100644 --- a/source/Nevermore/Mapping/IIdColumnMappingBuilder.cs +++ b/source/Nevermore/Mapping/IIdColumnMappingBuilder.cs @@ -9,26 +9,7 @@ public interface IIdColumnMappingBuilder : IColumnMappingBuilder /// /// This will also reset the PropertyHandler IIdColumnMappingBuilder Identity(); - - /// - /// Explicitly set a primary key handler. - /// - /// The primary key handler. - /// - IIdColumnMappingBuilder KeyHandler(IPrimaryKeyHandler primaryKeyHandler); - - /// - /// Set a function that when given the TableName will return key prefix string. - /// - /// The function to call back to get the prefix. - IIdColumnMappingBuilder Prefix(Func idPrefix); - - /// - /// Set a function that format a key value, given a prefix and a key number. - /// - /// The function to call back to format the id. - IIdColumnMappingBuilder Format(Func<(string idPrefix, int key), string> format); - + /// /// Builds the IdColumnMapping. /// diff --git a/source/Nevermore/Mapping/IStringBasedPrimitivePrimaryKeyHandler.cs b/source/Nevermore/Mapping/IStringBasedPrimitivePrimaryKeyHandler.cs deleted file mode 100644 index 0c499732..00000000 --- a/source/Nevermore/Mapping/IStringBasedPrimitivePrimaryKeyHandler.cs +++ /dev/null @@ -1,26 +0,0 @@ -using System; - -namespace Nevermore.Mapping -{ - public interface IStringBasedPrimitivePrimaryKeyHandler : IPrimaryKeyHandler - { - /// - /// Set a function that when given the TableName will return key prefix string. - /// - /// The function to call back to get the prefix. - void SetPrefix(Func idPrefix); - - /// - /// Given a tableName, get the prefix for the key. - /// - /// - /// The key prefix for the given tableName - string GetPrefix(string tableName); - - /// - /// Set a function that format a key value, given a prefix and a key number. - /// - /// The function to call back to format the id. - void SetFormat(Func<(string idPrefix, int key), string> format); - } -} \ No newline at end of file diff --git a/source/Nevermore/Mapping/IdColumnMapping.cs b/source/Nevermore/Mapping/IdColumnMapping.cs index 5738a2e6..ff021eaf 100644 --- a/source/Nevermore/Mapping/IdColumnMapping.cs +++ b/source/Nevermore/Mapping/IdColumnMapping.cs @@ -44,9 +44,6 @@ public IIdColumnMappingBuilder Identity() { ValidateForIdentityUse(); - if (!(PrimaryKeyHandler is null)) - throw new InvalidOperationException($"{nameof(KeyHandler)} has already been set to a non-identity handler."); - IsIdentity = true; return this; @@ -61,58 +58,6 @@ void ValidateForIdentityUse() throw new InvalidOperationException("Unable to configure an Identity Id column with a custom PropertyHandler"); } - public IIdColumnMappingBuilder KeyHandler(IPrimaryKeyHandler primaryKeyHandler) - { - if (!(PrimaryKeyHandler is null) && Direction == ColumnDirection.FromDatabase) - throw new InvalidOperationException($"{nameof(KeyHandler)} can only be called with an IIdentityPrimaryKeyHandler, once {nameof(Identity)} has been called."); - - PrimaryKeyHandler = primaryKeyHandler; - - return this; - } - - /// - /// Set a function that when given the TableName will return key prefix string. - /// - /// The function to call back to get the prefix. - public IIdColumnMappingBuilder Prefix(Func idPrefix) - { - if (Direction == ColumnDirection.FromDatabase) - throw new InvalidOperationException($"{nameof(Prefix)} cannot be set when an identity key handler has been configured."); - - if (PrimaryKeyHandler == null) - return KeyHandler(new StringPrimaryKeyHandler(idPrefix)); - - if (PrimaryKeyHandler is IStringBasedPrimitivePrimaryKeyHandler stringIdHandler) - { - stringIdHandler.SetPrefix(idPrefix); - return this; - } - - throw new InvalidOperationException($"Cannot set the Id prefix when the PrimaryKeyHandler is of type {PrimaryKeyHandler.GetType().Name}"); - } - - /// - /// Set a function that format a key value, given a prefix and a key number. - /// - /// The function to call back to format the id. - public IIdColumnMappingBuilder Format(Func<(string idPrefix, int key), string> format) - { - if (!(PrimaryKeyHandler is null) && Direction == ColumnDirection.FromDatabase) - throw new InvalidOperationException($"{nameof(Format)} cannot be set when an identity key handler has been configured."); - - if (PrimaryKeyHandler == null) - return KeyHandler(new StringPrimaryKeyHandler(format: format)); - - if (PrimaryKeyHandler is IStringBasedPrimitivePrimaryKeyHandler stringIdHandler) - { - stringIdHandler.SetFormat(format); - return this; - } - - throw new InvalidOperationException($"Cannot set the key format when the PrimaryKeyHandler is of type {PrimaryKeyHandler.GetType().Name}"); - } - protected override void SetCustomPropertyHandler(IPropertyHandler propertyHandler) { if (Direction == ColumnDirection.FromDatabase) diff --git a/source/Nevermore/Mapping/PrimitivePrimaryKeyHandler.cs b/source/Nevermore/Mapping/PrimitivePrimaryKeyHandler.cs index 79718c1f..21554f2c 100644 --- a/source/Nevermore/Mapping/PrimitivePrimaryKeyHandler.cs +++ b/source/Nevermore/Mapping/PrimitivePrimaryKeyHandler.cs @@ -6,7 +6,7 @@ namespace Nevermore.Mapping { public abstract class PrimaryKeyHandler : IPrimaryKeyHandler { - public Type Type => typeof(T); + public virtual Type Type => typeof(T); [return: NotNullIfNotNull("id")] public virtual object? ConvertToPrimitiveValue(object? id) diff --git a/source/Nevermore/Mapping/StringPrimaryKeyHandler.cs b/source/Nevermore/Mapping/StringPrimaryKeyHandler.cs index 1167ee10..299f33b2 100644 --- a/source/Nevermore/Mapping/StringPrimaryKeyHandler.cs +++ b/source/Nevermore/Mapping/StringPrimaryKeyHandler.cs @@ -3,43 +3,21 @@ namespace Nevermore.Mapping { - class StringPrimaryKeyHandler : PrimaryKeyHandler, IStringBasedPrimitivePrimaryKeyHandler + class StringPrimaryKeyHandler : PrimaryKeyHandler { - Func idPrefixFunc; - Func<(string idPrefix, int key), string> formatFunc; + readonly Func idPrefix; + readonly Func<(string idPrefix, int key), string> format; + public StringPrimaryKeyHandler(Func? idPrefix = null, Func<(string idPrefix, int key), string>? format = null) { - idPrefixFunc = idPrefix ?? (x => $"{x}s"); - formatFunc = format ?? (x => $"{x.idPrefix}-{x.key}"); - } - - /// - /// Set a function that when given the TableName will return key prefix string. - /// - /// The function to call back to get the prefix. - public void SetPrefix(Func idPrefix) - { - idPrefixFunc = idPrefix; - } - - public string GetPrefix(string tableName) - { - return idPrefixFunc(tableName); - } - - /// - /// Set a function that format a key value, given a prefix and a key number. - /// - /// The function to call back to format the id. - public void SetFormat(Func<(string idPrefix, int key), string> format) - { - formatFunc = format; + this.idPrefix = idPrefix ?? (tableName => $"{tableName}s"); + this.format = format ?? (x => $"{x.idPrefix}-{x.key}"); } public override object GetNextKey(IKeyAllocator keyAllocator, string tableName) { var nextKey = keyAllocator.NextId(tableName); - return formatFunc((GetPrefix(tableName), nextKey)); + return format((idPrefix(tableName), nextKey)); } } } \ No newline at end of file From 15c83d8a4d13797aecd6ccb0d97ff340c16a9e74 Mon Sep 17 00:00:00 2001 From: Pawel Pabich Date: Tue, 22 Jun 2021 09:23:42 +1000 Subject: [PATCH 17/25] Added an easier to use escape hatch for people that use custom prefix --- .../DocumentWithCustomPrefixAndStringId.cs | 8 ++++++++ .../DocumentWithCustomPrefixAndStringIdMap.cs | 15 +++++++++++++++ .../RelationalTransaction/LoadFixture.cs | 18 +++++++++++++++++- .../SetUp/FixtureWithRelationalStore.cs | 3 ++- .../Mapping/IIdColumnMappingBuilder.cs | 9 ++++++++- source/Nevermore/Mapping/IdColumnMapping.cs | 6 ++++++ 6 files changed, 56 insertions(+), 3 deletions(-) create mode 100644 source/Nevermore.IntegrationTests/Model/DocumentWithCustomPrefixAndStringId.cs create mode 100644 source/Nevermore.IntegrationTests/Model/DocumentWithCustomPrefixAndStringIdMap.cs diff --git a/source/Nevermore.IntegrationTests/Model/DocumentWithCustomPrefixAndStringId.cs b/source/Nevermore.IntegrationTests/Model/DocumentWithCustomPrefixAndStringId.cs new file mode 100644 index 00000000..6665ce82 --- /dev/null +++ b/source/Nevermore.IntegrationTests/Model/DocumentWithCustomPrefixAndStringId.cs @@ -0,0 +1,8 @@ +namespace Nevermore.IntegrationTests.Model +{ + public class DocumentWithCustomPrefixAndStringId + { + public string Id { get; set; } + public string Name { get; set; } + } +} \ No newline at end of file diff --git a/source/Nevermore.IntegrationTests/Model/DocumentWithCustomPrefixAndStringIdMap.cs b/source/Nevermore.IntegrationTests/Model/DocumentWithCustomPrefixAndStringIdMap.cs new file mode 100644 index 00000000..876b1991 --- /dev/null +++ b/source/Nevermore.IntegrationTests/Model/DocumentWithCustomPrefixAndStringIdMap.cs @@ -0,0 +1,15 @@ +using Nevermore.Mapping; + +namespace Nevermore.IntegrationTests.Model +{ + public class DocumentWithCustomPrefixAndStringIdMap : DocumentMap + { + public const string CustomPrefix = "CustomPrefix"; + + public DocumentWithCustomPrefixAndStringIdMap() + { + Id().KeyHandler(new StringPrimaryKeyHandler(_ => CustomPrefix)); + Column(m => m.Name); + } + } +} \ No newline at end of file diff --git a/source/Nevermore.IntegrationTests/RelationalTransaction/LoadFixture.cs b/source/Nevermore.IntegrationTests/RelationalTransaction/LoadFixture.cs index b7d138c8..b8dd8c17 100644 --- a/source/Nevermore.IntegrationTests/RelationalTransaction/LoadFixture.cs +++ b/source/Nevermore.IntegrationTests/RelationalTransaction/LoadFixture.cs @@ -424,7 +424,7 @@ public void LoadManyByWrongIdType_ShouldThrowArgumentException() } [Test] - public void StoreAndLoadWithCustomPrefix() + public void StoreAndLoadWithCustomIdAndCustomPrefix() { using (var trn = Store.BeginTransaction()) { @@ -438,5 +438,21 @@ public void StoreAndLoadWithCustomPrefix() document.Id.Value.Should().StartWith(CustomPrefixIdKeyHandler.CustomPrefix); } } + + [Test] + public void StoreAndLoadWithCustomPrefix() + { + using (var trn = Store.BeginTransaction()) + { + var document = new DocumentWithCustomPrefixAndStringId() + { + Name = "test" + }; + + trn.Insert(document); + + document.Id.Should().StartWith(DocumentWithCustomPrefixAndStringIdMap.CustomPrefix); + } + } } } \ No newline at end of file diff --git a/source/Nevermore.IntegrationTests/SetUp/FixtureWithRelationalStore.cs b/source/Nevermore.IntegrationTests/SetUp/FixtureWithRelationalStore.cs index c76b8f84..eb391a0b 100644 --- a/source/Nevermore.IntegrationTests/SetUp/FixtureWithRelationalStore.cs +++ b/source/Nevermore.IntegrationTests/SetUp/FixtureWithRelationalStore.cs @@ -30,7 +30,8 @@ protected FixtureWithRelationalStore() new DocumentWithRowVersionMap(), new DocumentWithIdentityIdMap(), new DocumentWithIdentityIdAndRowVersionMap(), - new DocumentWithCustomPrefixMap() + new DocumentWithCustomPrefixMap(), + new DocumentWithCustomPrefixAndStringIdMap() }; var config = new RelationalStoreConfiguration(ConnectionString) diff --git a/source/Nevermore/Mapping/IIdColumnMappingBuilder.cs b/source/Nevermore/Mapping/IIdColumnMappingBuilder.cs index 0414569b..806b0326 100644 --- a/source/Nevermore/Mapping/IIdColumnMappingBuilder.cs +++ b/source/Nevermore/Mapping/IIdColumnMappingBuilder.cs @@ -9,7 +9,14 @@ public interface IIdColumnMappingBuilder : IColumnMappingBuilder /// /// This will also reset the PropertyHandler IIdColumnMappingBuilder Identity(); - + + /// + /// Explicitly set a primary key handler. + /// + /// The primary key handler. + /// + IIdColumnMappingBuilder KeyHandler(IPrimaryKeyHandler primaryKeyHandler); + /// /// Builds the IdColumnMapping. /// diff --git a/source/Nevermore/Mapping/IdColumnMapping.cs b/source/Nevermore/Mapping/IdColumnMapping.cs index ff021eaf..bfa2c9ad 100644 --- a/source/Nevermore/Mapping/IdColumnMapping.cs +++ b/source/Nevermore/Mapping/IdColumnMapping.cs @@ -49,6 +49,12 @@ public IIdColumnMappingBuilder Identity() return this; } + public IIdColumnMappingBuilder KeyHandler(IPrimaryKeyHandler primaryKeyHandler) + { + PrimaryKeyHandler = primaryKeyHandler; + return this; + } + void ValidateForIdentityUse() { if (!ValidIdentityTypes.Contains(Type)) From 162d66a2c5c0e1c823c7b2f18c5a22bc98e4f446 Mon Sep 17 00:00:00 2001 From: Pawel Pabich Date: Tue, 22 Jun 2021 12:21:25 +1000 Subject: [PATCH 18/25] This was added by mistake --- source/Nevermore/Mapping/PrimitivePrimaryKeyHandler.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/source/Nevermore/Mapping/PrimitivePrimaryKeyHandler.cs b/source/Nevermore/Mapping/PrimitivePrimaryKeyHandler.cs index 21554f2c..79718c1f 100644 --- a/source/Nevermore/Mapping/PrimitivePrimaryKeyHandler.cs +++ b/source/Nevermore/Mapping/PrimitivePrimaryKeyHandler.cs @@ -6,7 +6,7 @@ namespace Nevermore.Mapping { public abstract class PrimaryKeyHandler : IPrimaryKeyHandler { - public virtual Type Type => typeof(T); + public Type Type => typeof(T); [return: NotNullIfNotNull("id")] public virtual object? ConvertToPrimitiveValue(object? id) From a9c48d82f7f76b733d3dbf6df08b32dacbf5a5c3 Mon Sep 17 00:00:00 2001 From: slewis74 Date: Tue, 22 Jun 2021 12:23:16 +1000 Subject: [PATCH 19/25] Fixed the Delete/DeleteAsync methods on IWriteQueryExecutor, to clean up how keys are handled in there --- .../Advanced/HooksFixture.cs | 2 +- .../Advanced/NoJsonFixture.cs | 23 +++++------ .../RelatedDocumentTableFixture.cs | 2 +- .../RelationalTransaction/DeleteFixture.cs | 2 +- source/Nevermore/Advanced/WriteTransaction.cs | 41 ++++++++++--------- source/Nevermore/IWriteQueryExecutor.cs | 37 +++++++++++++++-- 6 files changed, 68 insertions(+), 39 deletions(-) diff --git a/source/Nevermore.IntegrationTests/Advanced/HooksFixture.cs b/source/Nevermore.IntegrationTests/Advanced/HooksFixture.cs index 9cde393f..06fee314 100644 --- a/source/Nevermore.IntegrationTests/Advanced/HooksFixture.cs +++ b/source/Nevermore.IntegrationTests/Advanced/HooksFixture.cs @@ -28,7 +28,7 @@ public void ShouldCallHooks() transaction.Update(customer); AssertLogged(log, "BeforeUpdate", "AfterUpdate"); - transaction.Delete(customer); + transaction.Delete(customer); AssertLogged(log, "BeforeDelete", "AfterDelete"); transaction.Commit(); diff --git a/source/Nevermore.IntegrationTests/Advanced/NoJsonFixture.cs b/source/Nevermore.IntegrationTests/Advanced/NoJsonFixture.cs index 414ff3f7..e601666e 100644 --- a/source/Nevermore.IntegrationTests/Advanced/NoJsonFixture.cs +++ b/source/Nevermore.IntegrationTests/Advanced/NoJsonFixture.cs @@ -1,7 +1,4 @@ -using System.Collections.Generic; -using System.Data; using FluentAssertions; -using Microsoft.Data.SqlClient.Server; using Nevermore.IntegrationTests.SetUp; using Nevermore.Mapping; using NUnit.Framework; @@ -14,9 +11,9 @@ public override void OneTimeSetUp() { base.OneTimeSetUp(); NoMonkeyBusiness(); - + ExecuteSql("create table TestSchema.Car([Id] nvarchar(50), [Name] nvarchar(100))"); - + Store.Configuration.DocumentMaps.Register(new CarMap()); } @@ -25,23 +22,23 @@ class Car public string Id { get; set; } public string Name { get; set; } } - - class CarMap : DocumentMap + + class CarMap : DocumentMap { public CarMap() { Column(m => m.Name); - + JsonStorageFormat = JsonStorageFormat.NoJson; } } - + [Test] public void ShouldMapWithoutJson() { using var transaction = Store.BeginTransaction(); transaction.Insert(new Car { Name = "Volvo" }); - + var car = transaction.Load("Cars-1"); car.Should().NotBeNull(); car.Name.Should().Be("Volvo"); @@ -54,9 +51,9 @@ public void ShouldMapWithoutJson() car.Name.Should().Be("Bertie"); transaction.Query().Count().Should().Be(1); - - transaction.Delete(car); - + + transaction.Delete(car); + transaction.Query().Count().Should().Be(0); car = transaction.Load("Cars-1"); diff --git a/source/Nevermore.IntegrationTests/RelatedDocumentTableFixture.cs b/source/Nevermore.IntegrationTests/RelatedDocumentTableFixture.cs index f1a52719..c34b1ca0 100644 --- a/source/Nevermore.IntegrationTests/RelatedDocumentTableFixture.cs +++ b/source/Nevermore.IntegrationTests/RelatedDocumentTableFixture.cs @@ -108,7 +108,7 @@ public void WhenTheOrderIsDeleted() var order = new Order() {Id = orderId}; using (var trn = Store.BeginTransaction()) { - trn.Delete(order); + trn.Delete(order); trn.Commit(); } } diff --git a/source/Nevermore.IntegrationTests/RelationalTransaction/DeleteFixture.cs b/source/Nevermore.IntegrationTests/RelationalTransaction/DeleteFixture.cs index fe77c34f..bc93edc9 100644 --- a/source/Nevermore.IntegrationTests/RelationalTransaction/DeleteFixture.cs +++ b/source/Nevermore.IntegrationTests/RelationalTransaction/DeleteFixture.cs @@ -16,7 +16,7 @@ public void DeleteByEntity() using (var trn = Store.BeginTransaction()) { var product = trn.Load(id); - trn.Delete(product); + trn.Delete(product); trn.Commit(); } diff --git a/source/Nevermore/Advanced/WriteTransaction.cs b/source/Nevermore/Advanced/WriteTransaction.cs index 209d3fd7..72bfeca5 100644 --- a/source/Nevermore/Advanced/WriteTransaction.cs +++ b/source/Nevermore/Advanced/WriteTransaction.cs @@ -135,24 +135,24 @@ public async Task UpdateAsync(TDocument document, UpdateOptions optio } public void Delete(string id, DeleteOptions options = null) where TDocument : class - => Delete((object) id, options); + => Delete(id, options); public void Delete(int id, DeleteOptions options = null) where TDocument : class - => Delete((object) id, options); + => Delete(id, options); public void Delete(long id, DeleteOptions options = null) where TDocument : class - => Delete((object) id, options); + => Delete(id, options); public void Delete(Guid id, DeleteOptions options = null) where TDocument : class - => Delete((object) id, options); + => Delete(id, options); - public void Delete(TDocument document, DeleteOptions options = null) where TDocument : class + public void Delete(TDocument document, DeleteOptions options = null) where TDocument : class { - var id = configuration.DocumentMaps.GetId(document); - Delete(id, options); + var id = (TKey)configuration.DocumentMaps.GetId(document); + Delete(id, options); } - void Delete(object id, DeleteOptions options = null) where TDocument : class + public void Delete(TKey id, DeleteOptions options = null) where TDocument : class { var command = builder.PrepareDelete(id, options); configuration.Hooks.BeforeDelete(id, command.Mapping, this); @@ -172,30 +172,31 @@ public Task DeleteAsync(long id, CancellationToken cancellationToken public Task DeleteAsync(Guid id, CancellationToken cancellationToken = default) where TDocument : class => DeleteAsync(id, null, cancellationToken); - public Task DeleteAsync(TDocument document, CancellationToken cancellationToken = default) where TDocument : class - { - return DeleteAsync(document, null, cancellationToken); - } + public Task DeleteAsync(TDocument document, CancellationToken cancellationToken = default) where TDocument : class + => DeleteAsync(document, null, cancellationToken); public Task DeleteAsync(string id, DeleteOptions options, CancellationToken cancellationToken = default) where TDocument : class - => DeleteAsync((object) id, options, cancellationToken); + => DeleteAsync(id, options, cancellationToken); public Task DeleteAsync(int id, DeleteOptions options, CancellationToken cancellationToken = default) where TDocument : class - => DeleteAsync((object) id, options, cancellationToken); + => DeleteAsync(id, options, cancellationToken); public Task DeleteAsync(long id, DeleteOptions options, CancellationToken cancellationToken = default) where TDocument : class - => DeleteAsync((object) id, options, cancellationToken); + => DeleteAsync(id, options, cancellationToken); public Task DeleteAsync(Guid id, DeleteOptions options, CancellationToken cancellationToken = default) where TDocument : class - => DeleteAsync((object) id, options, cancellationToken); + => DeleteAsync(id, options, cancellationToken); - public Task DeleteAsync(TDocument document, DeleteOptions options, CancellationToken cancellationToken = default) where TDocument : class + public Task DeleteAsync(TDocument document, DeleteOptions options, CancellationToken cancellationToken = default) where TDocument : class { - var id = configuration.DocumentMaps.GetId(document); - return DeleteAsync(id, options, cancellationToken); + var id = (TKey)configuration.DocumentMaps.GetId(document); + return DeleteAsync(id, options, cancellationToken); } - async Task DeleteAsync(object id, DeleteOptions options, CancellationToken cancellationToken = default) where TDocument : class + public Task DeleteAsync(TKey id, CancellationToken cancellationToken = default) where TDocument : class + => DeleteAsync(id, null, cancellationToken); + + public async Task DeleteAsync(TKey id, DeleteOptions options, CancellationToken cancellationToken = default) where TDocument : class { var command = builder.PrepareDelete(id, options); await configuration.Hooks.BeforeDeleteAsync(id, command.Mapping, this); diff --git a/source/Nevermore/IWriteQueryExecutor.cs b/source/Nevermore/IWriteQueryExecutor.cs index 9585e0be..d6df3c5b 100644 --- a/source/Nevermore/IWriteQueryExecutor.cs +++ b/source/Nevermore/IWriteQueryExecutor.cs @@ -92,6 +92,15 @@ public interface IWriteQueryExecutor : IReadQueryExecutor /// Token to use to cancel the command. Task UpdateAsync(TDocument document, UpdateOptions options, CancellationToken cancellationToken = default) where TDocument : class; + /// + /// Deletes an existing document from the database by its ID. + /// + /// The type of document being deleted. + /// The document's key type + /// The id of the document to delete. + /// Advanced options for the delete operation. + void Delete(TKey id, DeleteOptions options = null) where TDocument : class; + /// /// Deletes an existing document from the database by its ID. /// @@ -128,9 +137,19 @@ public interface IWriteQueryExecutor : IReadQueryExecutor /// Deletes an existing document from the database. /// /// The type of document being deleted. + /// The document's key type /// The document to delete. /// Advanced options for the delete operation. - void Delete(TDocument document, DeleteOptions options = null) where TDocument : class; + void Delete(TDocument document, DeleteOptions options = null) where TDocument : class; + + /// + /// Deletes an existing document from the database by its ID. + /// + /// The type of document being deleted. + /// The document's key type + /// The id of the document to delete. + /// Token to use to cancel the command. + Task DeleteAsync(TKey id, CancellationToken cancellationToken = default) where TDocument : class; /// /// Deletes an existing document from the database by its ID. @@ -168,9 +187,20 @@ public interface IWriteQueryExecutor : IReadQueryExecutor /// Deletes an existing document from the database. /// /// The type of document being deleted. + /// The document's key type /// The document to delete. /// Token to use to cancel the command. - Task DeleteAsync(TDocument document, CancellationToken cancellationToken = default) where TDocument : class; + Task DeleteAsync(TDocument document, CancellationToken cancellationToken = default) where TDocument : class; + + /// + /// Deletes an existing document from the database by its ID. + /// + /// The type of document being deleted. + /// The document's key type + /// The id of the document to delete. + /// Advanced options for the delete operation. + /// Token to use to cancel the command. + Task DeleteAsync(TKey id, DeleteOptions options, CancellationToken cancellationToken = default) where TDocument : class; /// /// Deletes an existing document from the database by its ID. @@ -212,10 +242,11 @@ public interface IWriteQueryExecutor : IReadQueryExecutor /// Deletes an existing document from the database. /// /// The type of document being deleted. + /// The document's key type /// The document to delete. /// Advanced options for the delete operation. /// Token to use to cancel the command. - Task DeleteAsync(TDocument document, DeleteOptions options, CancellationToken cancellationToken = default) where TDocument : class; + Task DeleteAsync(TDocument document, DeleteOptions options, CancellationToken cancellationToken = default) where TDocument : class; /// /// Creates a deletion query for a strongly typed document. From 7401f6ae3b8c9e1ddcaa7b5816fd4ceed5834e1f Mon Sep 17 00:00:00 2001 From: Pawel Pabich Date: Tue, 22 Jun 2021 15:03:44 +1000 Subject: [PATCH 20/25] Actually load the data --- .../RelationalTransaction/LoadFixture.cs | 4 ++++ .../SetUp/FixtureWithRelationalStore.cs | 1 + 2 files changed, 5 insertions(+) diff --git a/source/Nevermore.IntegrationTests/RelationalTransaction/LoadFixture.cs b/source/Nevermore.IntegrationTests/RelationalTransaction/LoadFixture.cs index b8dd8c17..c5af8e3e 100644 --- a/source/Nevermore.IntegrationTests/RelationalTransaction/LoadFixture.cs +++ b/source/Nevermore.IntegrationTests/RelationalTransaction/LoadFixture.cs @@ -436,6 +436,8 @@ public void StoreAndLoadWithCustomIdAndCustomPrefix() trn.Insert(document); document.Id.Value.Should().StartWith(CustomPrefixIdKeyHandler.CustomPrefix); + + trn.LoadRequired(document.Id); } } @@ -452,6 +454,8 @@ public void StoreAndLoadWithCustomPrefix() trn.Insert(document); document.Id.Should().StartWith(DocumentWithCustomPrefixAndStringIdMap.CustomPrefix); + + trn.LoadRequired(document.Id); } } } diff --git a/source/Nevermore.IntegrationTests/SetUp/FixtureWithRelationalStore.cs b/source/Nevermore.IntegrationTests/SetUp/FixtureWithRelationalStore.cs index eb391a0b..e438b965 100644 --- a/source/Nevermore.IntegrationTests/SetUp/FixtureWithRelationalStore.cs +++ b/source/Nevermore.IntegrationTests/SetUp/FixtureWithRelationalStore.cs @@ -43,6 +43,7 @@ protected FixtureWithRelationalStore() config.TypeHandlers.Register(new ReferenceCollectionTypeHandler()); config.TypeHandlers.Register(new StringCustomIdTypeHandler()); + config.TypeHandlers.Register(new StringCustomIdTypeHandler()); config.PrimaryKeyHandlers.Register(new StringCustomIdTypeIdKeyHandler()); config.PrimaryKeyHandlers.Register(new CustomPrefixIdKeyHandler()); From 0a19e303ca64d836270f64f98d4d31b1d95652c5 Mon Sep 17 00:00:00 2001 From: slewis74 Date: Tue, 22 Jun 2021 15:44:35 +1000 Subject: [PATCH 21/25] Make the built in primary key handler types public, so they can be created to pass to the `KeyHandler` method for the IdColumn, to customise the id handling for a given document map --- source/Nevermore/Mapping/GuidPrimaryKeyHandler.cs | 2 +- source/Nevermore/Mapping/IntPrimaryKeyHandler.cs | 2 +- source/Nevermore/Mapping/LongPrimaryKeyHandler.cs | 2 +- source/Nevermore/Mapping/PrimaryKeyHandlerRegistry.cs | 7 ++++++- source/Nevermore/Mapping/StringPrimaryKeyHandler.cs | 4 ++-- 5 files changed, 11 insertions(+), 6 deletions(-) diff --git a/source/Nevermore/Mapping/GuidPrimaryKeyHandler.cs b/source/Nevermore/Mapping/GuidPrimaryKeyHandler.cs index 4565a162..95eb5fd5 100644 --- a/source/Nevermore/Mapping/GuidPrimaryKeyHandler.cs +++ b/source/Nevermore/Mapping/GuidPrimaryKeyHandler.cs @@ -2,7 +2,7 @@ namespace Nevermore.Mapping { - class GuidPrimaryKeyHandler : PrimaryKeyHandler + public sealed class GuidPrimaryKeyHandler : PrimaryKeyHandler { public override object GetNextKey(IKeyAllocator keyAllocator, string tableName) { diff --git a/source/Nevermore/Mapping/IntPrimaryKeyHandler.cs b/source/Nevermore/Mapping/IntPrimaryKeyHandler.cs index 8cbf9f87..f0e970de 100644 --- a/source/Nevermore/Mapping/IntPrimaryKeyHandler.cs +++ b/source/Nevermore/Mapping/IntPrimaryKeyHandler.cs @@ -1,6 +1,6 @@ namespace Nevermore.Mapping { - class IntPrimaryKeyHandler : PrimaryKeyHandler + public sealed class IntPrimaryKeyHandler : PrimaryKeyHandler { public override object GetNextKey(IKeyAllocator keyAllocator, string tableName) { diff --git a/source/Nevermore/Mapping/LongPrimaryKeyHandler.cs b/source/Nevermore/Mapping/LongPrimaryKeyHandler.cs index 361a80be..2ea41193 100644 --- a/source/Nevermore/Mapping/LongPrimaryKeyHandler.cs +++ b/source/Nevermore/Mapping/LongPrimaryKeyHandler.cs @@ -1,6 +1,6 @@ namespace Nevermore.Mapping { - class LongPrimaryKeyHandler : PrimaryKeyHandler + public sealed class LongPrimaryKeyHandler : PrimaryKeyHandler { public override object GetNextKey(IKeyAllocator keyAllocator, string tableName) { diff --git a/source/Nevermore/Mapping/PrimaryKeyHandlerRegistry.cs b/source/Nevermore/Mapping/PrimaryKeyHandlerRegistry.cs index 5b3bf408..32b38345 100644 --- a/source/Nevermore/Mapping/PrimaryKeyHandlerRegistry.cs +++ b/source/Nevermore/Mapping/PrimaryKeyHandlerRegistry.cs @@ -9,7 +9,12 @@ namespace Nevermore.Mapping { public interface IPrimaryKeyHandlerRegistry { - void Register(IPrimaryKeyHandler strategy); + /// + /// Registers a mapping of key type to PrimaryKeyHandler type. StringPrimaryKeyHandler, IntPrimaryKeyHandler, LongPrimaryKeyHandler, and GuidPrimaryKeyHandler + /// are registered by default, with default settings. Calling register again for them will overwrite the default, you can use this if you want to override a + /// global default. + /// + void Register(IPrimaryKeyHandler handler); IPrimaryKeyHandler? Resolve(Type columnType); } diff --git a/source/Nevermore/Mapping/StringPrimaryKeyHandler.cs b/source/Nevermore/Mapping/StringPrimaryKeyHandler.cs index 299f33b2..79b48010 100644 --- a/source/Nevermore/Mapping/StringPrimaryKeyHandler.cs +++ b/source/Nevermore/Mapping/StringPrimaryKeyHandler.cs @@ -3,11 +3,11 @@ namespace Nevermore.Mapping { - class StringPrimaryKeyHandler : PrimaryKeyHandler + public sealed class StringPrimaryKeyHandler : PrimaryKeyHandler { readonly Func idPrefix; readonly Func<(string idPrefix, int key), string> format; - + public StringPrimaryKeyHandler(Func? idPrefix = null, Func<(string idPrefix, int key), string>? format = null) { this.idPrefix = idPrefix ?? (tableName => $"{tableName}s"); From 9d35dea3452ef2e411a536599e3993cc2746cb02 Mon Sep 17 00:00:00 2001 From: slewis74 Date: Tue, 22 Jun 2021 18:10:12 +1000 Subject: [PATCH 22/25] Changed the StringPrimaryKeyHandler so it takes a string for the prefix, rather than the Func --- .../Model/DocumentWithCustomPrefixAndStringIdMap.cs | 2 +- source/Nevermore/Mapping/StringPrimaryKeyHandler.cs | 9 +++++---- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/source/Nevermore.IntegrationTests/Model/DocumentWithCustomPrefixAndStringIdMap.cs b/source/Nevermore.IntegrationTests/Model/DocumentWithCustomPrefixAndStringIdMap.cs index 876b1991..fa9c450d 100644 --- a/source/Nevermore.IntegrationTests/Model/DocumentWithCustomPrefixAndStringIdMap.cs +++ b/source/Nevermore.IntegrationTests/Model/DocumentWithCustomPrefixAndStringIdMap.cs @@ -8,7 +8,7 @@ public class DocumentWithCustomPrefixAndStringIdMap : DocumentMap CustomPrefix)); + Id().KeyHandler(new StringPrimaryKeyHandler(CustomPrefix)); Column(m => m.Name); } } diff --git a/source/Nevermore/Mapping/StringPrimaryKeyHandler.cs b/source/Nevermore/Mapping/StringPrimaryKeyHandler.cs index 79b48010..c0cb44ae 100644 --- a/source/Nevermore/Mapping/StringPrimaryKeyHandler.cs +++ b/source/Nevermore/Mapping/StringPrimaryKeyHandler.cs @@ -5,19 +5,20 @@ namespace Nevermore.Mapping { public sealed class StringPrimaryKeyHandler : PrimaryKeyHandler { - readonly Func idPrefix; readonly Func<(string idPrefix, int key), string> format; - public StringPrimaryKeyHandler(Func? idPrefix = null, Func<(string idPrefix, int key), string>? format = null) + public StringPrimaryKeyHandler(string? idPrefix = null, Func<(string idPrefix, int key), string>? format = null) { - this.idPrefix = idPrefix ?? (tableName => $"{tableName}s"); + IdPrefix = idPrefix; this.format = format ?? (x => $"{x.idPrefix}-{x.key}"); } + public string? IdPrefix { get; private set; } + public override object GetNextKey(IKeyAllocator keyAllocator, string tableName) { var nextKey = keyAllocator.NextId(tableName); - return format((idPrefix(tableName), nextKey)); + return format((IdPrefix ?? $"{tableName}s", nextKey)); } } } \ No newline at end of file From 788eb921b3cad5e2343fab07e6ccbe19bd31a515 Mon Sep 17 00:00:00 2001 From: slewis74 Date: Mon, 28 Jun 2021 14:46:55 +1000 Subject: [PATCH 23/25] Fixed IdColumnMapping constructor, it wasn't setting MaxLength and Direction correctly --- source/Nevermore/Mapping/ColumnMapping.cs | 9 ++++----- source/Nevermore/Mapping/IdColumnMapping.cs | 14 ++++++++------ 2 files changed, 12 insertions(+), 11 deletions(-) diff --git a/source/Nevermore/Mapping/ColumnMapping.cs b/source/Nevermore/Mapping/ColumnMapping.cs index fc0294bf..c4140b5f 100644 --- a/source/Nevermore/Mapping/ColumnMapping.cs +++ b/source/Nevermore/Mapping/ColumnMapping.cs @@ -8,7 +8,6 @@ public class ColumnMapping : IColumnMappingBuilder { const int DefaultPrimaryKeyIdLength = 50; const int DefaultMaxForeignKeyIdLength = 50; - int? maxLength; internal ColumnMapping(string columnName, Type type, IPropertyHandler handler, PropertyInfo property) { @@ -21,11 +20,11 @@ internal ColumnMapping(string columnName, Type type, IPropertyHandler handler, P return; if (Property.Name == "Id") { - maxLength = DefaultPrimaryKeyIdLength; + MaxLength = DefaultPrimaryKeyIdLength; } else if (Property.Name.EndsWith("Id")) // Foreign keys { - maxLength = DefaultMaxForeignKeyIdLength; + MaxLength = DefaultMaxForeignKeyIdLength; } } @@ -34,13 +33,13 @@ internal ColumnMapping(string columnName, Type type, IPropertyHandler handler, P public IPropertyHandler PropertyHandler { get; private set; } public PropertyInfo Property { get; } - public int? MaxLength => maxLength; + public int? MaxLength { get; protected set; } public ColumnDirection Direction { get; protected set; } IColumnMappingBuilder IColumnMappingBuilder.MaxLength(int max) { - maxLength = max; + MaxLength = max; return this; } diff --git a/source/Nevermore/Mapping/IdColumnMapping.cs b/source/Nevermore/Mapping/IdColumnMapping.cs index bfa2c9ad..a396efcd 100644 --- a/source/Nevermore/Mapping/IdColumnMapping.cs +++ b/source/Nevermore/Mapping/IdColumnMapping.cs @@ -7,12 +7,13 @@ namespace Nevermore.Mapping { public class IdColumnMapping : ColumnMapping { - internal IdColumnMapping(string columnName, Type type, IPropertyHandler handler, PropertyInfo property, bool isIdentity, IPrimaryKeyHandler primaryKeyHandler) - : base(columnName, type, handler, property) + internal IdColumnMapping(IdColumnMappingBuilder idColumn, IPrimaryKeyHandler primaryKeyHandler) + : base(idColumn.ColumnName, idColumn.Type, idColumn.PropertyHandler, idColumn.Property) { - IsIdentity = isIdentity; - Direction = IsIdentity ? ColumnDirection.FromDatabase : ColumnDirection.Both; + IsIdentity = idColumn.IsIdentity; PrimaryKeyHandler = primaryKeyHandler; + Direction = idColumn.Direction; + MaxLength = idColumn.MaxLength; } public bool IsIdentity { get; } @@ -35,7 +36,7 @@ internal IdColumnMappingBuilder(string columnName, Type type, IPropertyHandler h { } - bool IsIdentity { get; set; } + public bool IsIdentity { get; private set; } IPrimaryKeyHandler? PrimaryKeyHandler { get; set; } @@ -45,6 +46,7 @@ public IIdColumnMappingBuilder Identity() ValidateForIdentityUse(); IsIdentity = true; + Direction = ColumnDirection.FromDatabase; return this; } @@ -82,7 +84,7 @@ public IdColumnMapping Build(IPrimaryKeyHandlerRegistry primaryKeyHandlerRegistr if (primaryKeyHandler is null) throw new InvalidOperationException($"Unable to determine a primary key handler for type {Type.Name}. This could happen if the custom PrimaryKeyHandlers are not registered prior to registering the DocumentMaps"); - var mapping = new IdColumnMapping(ColumnName, Type, PropertyHandler, Property, IsIdentity, primaryKeyHandler); + var mapping = new IdColumnMapping(this, primaryKeyHandler); return mapping; } } From 7b439e036f3dc74e62441cb0da49dad58ff10ed9 Mon Sep 17 00:00:00 2001 From: slewis74 Date: Wed, 30 Jun 2021 15:14:35 +1000 Subject: [PATCH 24/25] Octopus server testing showed up an issue with LoadMany's query preparation code, it didn't cater for custom primary key handlers. The handlers now supply it with the information it needs. --- .../Model/StringCustomIdTypeIdKeyHandler.cs | 8 ++- .../RelationalStoreFixture.cs | 29 ++++++++++ source/Nevermore/Advanced/ReadTransaction.cs | 2 +- source/Nevermore/CommandParameterValues.cs | 54 ++++++------------- .../Mapping/GuidPrimaryKeyHandler.cs | 5 ++ .../Nevermore/Mapping/IPrimaryKeyHandler.cs | 10 ++++ .../Nevermore/Mapping/IntPrimaryKeyHandler.cs | 6 +++ .../Mapping/LongPrimaryKeyHandler.cs | 6 +++ .../Mapping/PrimitivePrimaryKeyHandler.cs | 3 ++ .../Mapping/StringPrimaryKeyHandler.cs | 5 ++ 10 files changed, 89 insertions(+), 39 deletions(-) diff --git a/source/Nevermore.IntegrationTests/Model/StringCustomIdTypeIdKeyHandler.cs b/source/Nevermore.IntegrationTests/Model/StringCustomIdTypeIdKeyHandler.cs index ab2ce4f1..4542fac9 100644 --- a/source/Nevermore.IntegrationTests/Model/StringCustomIdTypeIdKeyHandler.cs +++ b/source/Nevermore.IntegrationTests/Model/StringCustomIdTypeIdKeyHandler.cs @@ -1,5 +1,7 @@ #nullable enable using System; +using System.Data; +using Microsoft.Data.SqlClient.Server; using Nevermore.Mapping; namespace Nevermore.IntegrationTests.Model @@ -8,13 +10,17 @@ class StringCustomIdTypeIdKeyHandler : IPrimaryKeyHandler where T : StringCustomIdType { readonly string? customPrefix; - public Type Type => typeof(T); public StringCustomIdTypeIdKeyHandler(string? customPrefix = null) { this.customPrefix = customPrefix; } + public Type Type => typeof(T); + + public SqlMetaData GetSqlMetaData(string name) + => new SqlMetaData(name, SqlDbType.NVarChar, 300); + public object? ConvertToPrimitiveValue(object? id) { if (!(id is StringCustomIdType stringCustomType)) diff --git a/source/Nevermore.IntegrationTests/RelationalStoreFixture.cs b/source/Nevermore.IntegrationTests/RelationalStoreFixture.cs index 7c31e196..d388024b 100644 --- a/source/Nevermore.IntegrationTests/RelationalStoreFixture.cs +++ b/source/Nevermore.IntegrationTests/RelationalStoreFixture.cs @@ -1,5 +1,6 @@ #nullable enable using System; +using System.Collections.Generic; using System.Linq; using FluentAssertions; using Nevermore.IntegrationTests.Model; @@ -102,6 +103,34 @@ public void ShouldHandleIdsWithInOperand() } } + [Test] + public void ShouldHandleLoadManyWithCustomKeyType() + { + CustomerId customerId; + var ids = new List(); + using (var transaction = Store.BeginTransaction()) + { + var customer1 = new Customer {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 = {"db-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); + transaction.Commit(); + ids.Add(customer1.Id); + ids.Add(customer3.Id); + ids.Add(customer2.Id); + } + + using (var transaction = Store.BeginTransaction()) + { + var customers = transaction.LoadMany(ids); + customers.SingleOrDefault(c => c.FirstName == "Bob").Should().NotBeNull("Bob's entry should be returned"); + customers.SingleOrDefault(c => c.FirstName == "Charlie").Should().NotBeNull("Charlie's entry should be returned"); + customers.SingleOrDefault(c => c.FirstName == "Alice").Should().NotBeNull("Alice's entry should be returned"); + } + } + [Test] public void ShouldMultiSelect() { diff --git a/source/Nevermore/Advanced/ReadTransaction.cs b/source/Nevermore/Advanced/ReadTransaction.cs index ed0042c1..6cbee6ce 100644 --- a/source/Nevermore/Advanced/ReadTransaction.cs +++ b/source/Nevermore/Advanced/ReadTransaction.cs @@ -596,7 +596,7 @@ PreparedCommand PrepareLoadMany(IEnumerable idList) throw new ArgumentException($"Provided Id of type '{typeof(TKey).FullName}' does not match configured type of '{mapping.IdColumn?.Type.FullName}'."); var param = new CommandParameterValues(); - param.AddTable("criteriaTable", idList.ToList()); + param.AddTable("criteriaTable", idList.ToList(), configuration); var statement = $"SELECT s.* FROM [{configuration.GetSchemaNameOrDefault(mapping)}].[{mapping.TableName}] s INNER JOIN @criteriaTable t on t.[ParameterValue] = s.[{mapping.IdColumn.ColumnName}] order by s.[{mapping.IdColumn.ColumnName}]"; return new PreparedCommand(statement, param, RetriableOperation.Select, mapping, commandBehavior: CommandBehavior.SingleResult | CommandBehavior.SequentialAccess); } diff --git a/source/Nevermore/CommandParameterValues.cs b/source/Nevermore/CommandParameterValues.cs index e8b06cda..e6600017 100644 --- a/source/Nevermore/CommandParameterValues.cs +++ b/source/Nevermore/CommandParameterValues.cs @@ -56,49 +56,29 @@ public CommandParameterValues(object args) public CommandType CommandType { get; set; } - public void AddTable(string name, IReadOnlyCollection ids) + public void AddTable(string name, IReadOnlyCollection ids, IRelationalStoreConfiguration configuration) { - var idColumnMetadata = GetSqlMetaData(ids, "ParameterValue"); + var primaryKeyHandler = configuration.PrimaryKeyHandlers.Resolve(typeof(T)); + if (primaryKeyHandler is null) + throw new InvalidOperationException($"Unable to locate primary key handler for type {typeof(T).Name}"); + + var idColumnMetadata = primaryKeyHandler.GetSqlMetaData("ParameterValue"); var dataRecords = ids.Where(v => v != null).Select(v => { var record = new SqlDataRecord(idColumnMetadata); - record.SetValue(0, v); + record.SetValue(0, primaryKeyHandler.ConvertToPrimitiveValue(v)); return record; }).ToList(); - + AddTable(name, new TableValuedParameter("dbo.[ParameterList]", dataRecords)); } - + public void AddTable(string name, TableValuedParameter tvp) { Add(name, tvp); } - static SqlMetaData GetSqlMetaData(IReadOnlyCollection ids, string name) - { - var valueType = typeof(T); - var notSupportedErrorMsg = $"'{valueType.Name}' is not a valid ID type, supported types are: {nameof(String)}, {nameof(Int32)}, {nameof(Int64)} and {nameof(Guid)}."; - switch (Type.GetTypeCode(valueType)) - { - //TODO: May consider dynamically resolving the 'ParameterValue' column length for User-Defined Table Type 'ParameterList' based on each DocumentMap configuration later. - //TODO: The fixed length of 300 is a temporary solution which match our table schema in Script0002-ParameterList.sql - case TypeCode.String: return new SqlMetaData(name, SqlDbType.NVarChar, 300); - case TypeCode.Int32: return new SqlMetaData(name, SqlDbType.Int); - case TypeCode.Int64: return new SqlMetaData(name, SqlDbType.BigInt); - case TypeCode.Object: - { - if (valueType == typeof(Guid)) - { - return new SqlMetaData(name, SqlDbType.UniqueIdentifier); - } - - throw new NotSupportedException(notSupportedErrorMsg); - } - default: throw new NotSupportedException(notSupportedErrorMsg); - } - } - void AddFromParametersObject(object args) { if (args == null) @@ -139,7 +119,7 @@ protected virtual void ContributeParameter(IDbCommand command, ITypeHandlerRegis command.Parameters.Add(p); return; } - + if (value is TableValuedParameter tvp && command is SqlCommand sqlCommand) { var p = sqlCommand.Parameters.Add(name, SqlDbType.Structured); @@ -154,9 +134,9 @@ protected virtual void ContributeParameter(IDbCommand command, ITypeHandlerRegis var i = 0; var inClauseValues = ((IEnumerable) value).Cast().ToList(); - + ListExtender.ExtendListRepeatingLastValue(inClauseValues); - + foreach (var inClauseValue in inClauseValues) { i++; @@ -181,7 +161,7 @@ protected virtual void ContributeParameter(IDbCommand command, ITypeHandlerRegis var columnType = DatabaseTypeConverter.AsDbType(value.GetType()); if (columnType == null) throw new InvalidOperationException($"Cannot map type '{value.GetType().FullName}' to a DbType. Consider providing a custom ITypeHandler."); - + var param = new SqlParameter(); param.ParameterName = name; param.DbType = columnType.Value; @@ -192,11 +172,11 @@ protected virtual void ContributeParameter(IDbCommand command, ITypeHandlerRegis var size = GetBestSizeBucket(text); if (size > 0) { - param.Size = size; + param.Size = size; } } - // To assist SQL's query plan caching, assign a parameter size for our + // To assist SQL's query plan caching, assign a parameter size for our // common id lookups where possible. if (mapping != null && mapping.IdColumn != null @@ -223,11 +203,11 @@ protected virtual void ContributeParameter(IDbCommand command, ITypeHandlerRegis command.Parameters.Add(param); } - + // By default all string parameters have their size automatically assigned based on the length of the string. // This results in a different query plan depending on the size of the text used. The query plan ends up like this: - // + // // (@firstname nvarchar(24))SELECT TOP 100 * FROM dbo.[Customer] WHERE ([FirstName] <> @firstname) ORDER BY [Id] // (@firstname nvarchar(44))SELECT TOP 100 * FROM dbo.[Customer] WHERE ([FirstName] <> @firstname) ORDER BY [Id] // (@firstname nvarchar(47))SELECT TOP 100 * FROM dbo.[Customer] WHERE ([FirstName] <> @firstname) ORDER BY [Id] diff --git a/source/Nevermore/Mapping/GuidPrimaryKeyHandler.cs b/source/Nevermore/Mapping/GuidPrimaryKeyHandler.cs index 95eb5fd5..9d329d3d 100644 --- a/source/Nevermore/Mapping/GuidPrimaryKeyHandler.cs +++ b/source/Nevermore/Mapping/GuidPrimaryKeyHandler.cs @@ -1,9 +1,14 @@ using System; +using System.Data; +using Microsoft.Data.SqlClient.Server; namespace Nevermore.Mapping { public sealed class GuidPrimaryKeyHandler : PrimaryKeyHandler { + public override SqlMetaData GetSqlMetaData(string name) + => new SqlMetaData(name, SqlDbType.UniqueIdentifier); + public override object GetNextKey(IKeyAllocator keyAllocator, string tableName) { return Guid.NewGuid(); diff --git a/source/Nevermore/Mapping/IPrimaryKeyHandler.cs b/source/Nevermore/Mapping/IPrimaryKeyHandler.cs index e25a7289..32f54db9 100644 --- a/source/Nevermore/Mapping/IPrimaryKeyHandler.cs +++ b/source/Nevermore/Mapping/IPrimaryKeyHandler.cs @@ -1,13 +1,23 @@ #nullable enable using System; using System.Diagnostics.CodeAnalysis; +using Microsoft.Data.SqlClient.Server; namespace Nevermore.Mapping { public interface IPrimaryKeyHandler { + /// + /// The property type this handler is responsible for. + /// Type Type { get; } + /// + /// Gets the SqlMetaData to use for keys of this type. This is used for TableValueParameters. + /// + /// The name of the column. + SqlMetaData GetSqlMetaData(string name); + /// /// Convert the given id value to the underlying primitive type required for Sql command parameters. /// diff --git a/source/Nevermore/Mapping/IntPrimaryKeyHandler.cs b/source/Nevermore/Mapping/IntPrimaryKeyHandler.cs index f0e970de..218234bf 100644 --- a/source/Nevermore/Mapping/IntPrimaryKeyHandler.cs +++ b/source/Nevermore/Mapping/IntPrimaryKeyHandler.cs @@ -1,7 +1,13 @@ +using System.Data; +using Microsoft.Data.SqlClient.Server; + namespace Nevermore.Mapping { public sealed class IntPrimaryKeyHandler : PrimaryKeyHandler { + public override SqlMetaData GetSqlMetaData(string name) + => new SqlMetaData(name, SqlDbType.Int); + public override object GetNextKey(IKeyAllocator keyAllocator, string tableName) { return keyAllocator.NextId(tableName); diff --git a/source/Nevermore/Mapping/LongPrimaryKeyHandler.cs b/source/Nevermore/Mapping/LongPrimaryKeyHandler.cs index 2ea41193..91fc04d6 100644 --- a/source/Nevermore/Mapping/LongPrimaryKeyHandler.cs +++ b/source/Nevermore/Mapping/LongPrimaryKeyHandler.cs @@ -1,7 +1,13 @@ +using System.Data; +using Microsoft.Data.SqlClient.Server; + namespace Nevermore.Mapping { public sealed class LongPrimaryKeyHandler : PrimaryKeyHandler { + public override SqlMetaData GetSqlMetaData(string name) + => new SqlMetaData(name, SqlDbType.BigInt); + public override object GetNextKey(IKeyAllocator keyAllocator, string tableName) { return keyAllocator.NextId(tableName); diff --git a/source/Nevermore/Mapping/PrimitivePrimaryKeyHandler.cs b/source/Nevermore/Mapping/PrimitivePrimaryKeyHandler.cs index 79718c1f..57e2eea6 100644 --- a/source/Nevermore/Mapping/PrimitivePrimaryKeyHandler.cs +++ b/source/Nevermore/Mapping/PrimitivePrimaryKeyHandler.cs @@ -1,6 +1,7 @@ #nullable enable using System; using System.Diagnostics.CodeAnalysis; +using Microsoft.Data.SqlClient.Server; namespace Nevermore.Mapping { @@ -8,6 +9,8 @@ public abstract class PrimaryKeyHandler : IPrimaryKeyHandler { public Type Type => typeof(T); + public abstract SqlMetaData GetSqlMetaData(string name); + [return: NotNullIfNotNull("id")] public virtual object? ConvertToPrimitiveValue(object? id) { diff --git a/source/Nevermore/Mapping/StringPrimaryKeyHandler.cs b/source/Nevermore/Mapping/StringPrimaryKeyHandler.cs index c0cb44ae..36de3e8d 100644 --- a/source/Nevermore/Mapping/StringPrimaryKeyHandler.cs +++ b/source/Nevermore/Mapping/StringPrimaryKeyHandler.cs @@ -1,5 +1,7 @@ #nullable enable using System; +using System.Data; +using Microsoft.Data.SqlClient.Server; namespace Nevermore.Mapping { @@ -13,6 +15,9 @@ public StringPrimaryKeyHandler(string? idPrefix = null, Func<(string idPrefix, i this.format = format ?? (x => $"{x.idPrefix}-{x.key}"); } + public override SqlMetaData GetSqlMetaData(string name) + => new SqlMetaData(name, SqlDbType.NVarChar, 300); + public string? IdPrefix { get; private set; } public override object GetNextKey(IKeyAllocator keyAllocator, string tableName) From 459f6eb3e850facd6def991f4815fd8a7c15c127 Mon Sep 17 00:00:00 2001 From: slewis74 Date: Tue, 6 Jul 2021 12:12:51 +1000 Subject: [PATCH 25/25] Re-instated `.In` extension method for use on custom primary key types --- source/Nevermore/ListExtensions.cs | 43 +++++++----------------------- source/Nevermore/Nevermore.csproj | 2 +- 2 files changed, 11 insertions(+), 34 deletions(-) diff --git a/source/Nevermore/ListExtensions.cs b/source/Nevermore/ListExtensions.cs index 486c2042..2674beb5 100644 --- a/source/Nevermore/ListExtensions.cs +++ b/source/Nevermore/ListExtensions.cs @@ -1,3 +1,4 @@ +#nullable enable using System; using System.Collections.Generic; using System.Linq; @@ -8,43 +9,19 @@ public static class ListExtensions { /// /// Same as Contains but in the opposite calling structure. In Nevermore LINQ queries, translates to "WHERE X IN (@param1, @param2)...". - /// In other cases calls collection.Contains(value). + /// In other cases calls collection.Contains(value). /// /// The value to search for /// The collection to search within. /// True if exists in - public static bool In(this T value, IEnumerable collection) where T : struct, IConvertible + public static bool In(this T? value, IEnumerable collection) { return collection.Contains(value); } - - /// - /// Same as Contains but in the opposite calling structure. In Nevermore LINQ queries, translates to "WHERE X IN (@param1, @param2)...". - /// In other cases calls collection.Contains(value). - /// - /// The value to search for - /// The collection to search within. - /// True if exists in - public static bool In(this T? value, IEnumerable collection) where T : struct, IConvertible - { - return collection.Contains(value); - } - - /// - /// Same as Contains but in the opposite calling structure. In Nevermore LINQ queries, translates to "WHERE X IN (@param1, @param2)...". - /// In other cases calls collection.Contains(value). - /// - /// The value to search for - /// The collection to search within. - /// True if exists in - public static bool In(this string value, IEnumerable collection) - { - return collection.Contains(value); - } - + /// /// Same as !Contains but in the opposite calling structure. In Nevermore LINQ queries, translates to "WHERE X NOT IN (@param1, @param2)...". - /// In other cases calls !collection.Contains(value). + /// In other cases calls !collection.Contains(value). /// /// The value to search for /// The collection to search within. @@ -53,11 +30,11 @@ public static bool NotIn(this T value, IEnumerable collection) where T : s { return !collection.Contains(value); } - - + + /// /// Same as !Contains but in the opposite calling structure. In Nevermore LINQ queries, translates to "WHERE X NOT IN (@param1, @param2)...". - /// In other cases calls !collection.Contains(value). + /// In other cases calls !collection.Contains(value). /// /// The value to search for /// The collection to search within. @@ -66,10 +43,10 @@ public static bool NotIn(this T? value, IEnumerable collection) where T : { return !collection.Contains(value); } - + /// /// Same as !Contains but in the opposite calling structure. In Nevermore LINQ queries, translates to "WHERE X NOT IN (@param1, @param2)...". - /// In other cases calls !collection.Contains(value). + /// In other cases calls !collection.Contains(value). /// /// The value to search for /// The collection to search within. diff --git a/source/Nevermore/Nevermore.csproj b/source/Nevermore/Nevermore.csproj index af38f423..a41eb14b 100644 --- a/source/Nevermore/Nevermore.csproj +++ b/source/Nevermore/Nevermore.csproj @@ -27,7 +27,7 @@ true $(AllowedOutputExtensionsInPackageBuildOutputFolder);.pdb netstandard2.1 - 8 + 9 CS1591