diff options
Diffstat (limited to 'docker/crupest-api')
5 files changed, 159 insertions, 82 deletions
diff --git a/docker/crupest-api/CrupestApi/CrupestApi.Commons/Crud/ColumnInfo.cs b/docker/crupest-api/CrupestApi/CrupestApi.Commons/Crud/ColumnInfo.cs index 6e29de0..ae5081d 100644 --- a/docker/crupest-api/CrupestApi/CrupestApi.Commons/Crud/ColumnInfo.cs +++ b/docker/crupest-api/CrupestApi/CrupestApi.Commons/Crud/ColumnInfo.cs @@ -21,6 +21,7 @@ public class ColumnHooks /// <summary>Called after SELECT. Please use multicast if you want to customize it because there are many default behavior in it.</summary> /// <remarks> + /// Called after column type transformation. /// value(in): /// null => not found in SELECT result /// DbNullValue => database NULL @@ -33,6 +34,7 @@ public class ColumnHooks /// <summary>Called before INSERT. Please use multicast if you want to customize it because there are many default behavior in it.</summary> /// <remarks> + /// Called before column type transformation. /// value(in): /// null => not specified by insert clause /// DbNullValue => specified as database NULL @@ -45,6 +47,7 @@ public class ColumnHooks /// <summary>Called before UPDATE. Please use multicast if you want to customize it because there are many default behavior in it.</summary /// <remarks> + /// Called before column type transformation. /// value(in): /// null => not specified by update clause /// DbNullValue => specified as database NULL @@ -115,6 +118,7 @@ public class ColumnInfo public bool IsNotNull => IsPrimaryKey || Metadata.GetValueOrDefault(ColumnMetadataKeys.NotNull) is true; public bool IsGenerated => Metadata.GetValueOrDefault(ColumnMetadataKeys.Generated) is true; public bool IsNoUpdate => Metadata.GetValueOrDefault(ColumnMetadataKeys.NoUpdate) is true; + public bool CanBeGenerated => DefaultValueGeneratorMethod is not null; /// <summary> /// This only returns metadata value. It doesn't not fall back to primary column. If you want to get the real key column, go to table info. /// </summary> @@ -122,7 +126,6 @@ public class ColumnInfo /// <seealso cref="TableInfo.KeyColumn"/> public bool IsSpecifiedAsKey => Metadata.GetValueOrDefault(ColumnMetadataKeys.ActAsKey) is true; public ColumnIndexType Index => Metadata.GetValueOrDefault<ColumnIndexType?>(ColumnMetadataKeys.Index) ?? ColumnIndexType.None; - public UpdateBehavior UpdateBehavior => Metadata.GetValueOrDefault<UpdateBehavior?>(ColumnMetadataKeys.UpdateBehavior) ?? UpdateBehavior.NullIsNotUpdate; /// <summary> /// The real column name. Maybe set in metadata or just the property name. @@ -160,53 +163,71 @@ public class ColumnInfo } } - private void TryCoerceStringFromNullToEmpty(ref object? value) + public MethodInfo ValidatorMethod { - if (ColumnType.ClrType == typeof(string) && (Metadata.GetValueOrDefault<bool?>(ColumnMetadataKeys.DefaultEmptyForString) is true) && value is DbNullValue) + get { - value = ""; + object? value = Metadata.GetValueOrDefault(ColumnMetadataKeys.Validator); + Debug.Assert(value is null || value is string); + if (value is null) + { + return GetType().GetMethod(nameof(DefaultValidator))!; + } + else + { + string methodName = (string)value; + return Table.EntityType.GetMethod(methodName, BindingFlags.Static) ?? throw new Exception("The validator does not exist."); + } } } - protected void OnAfterSelect(ColumnInfo column, ref object? value) + public void InvokeValidator(object value) { - TryCoerceStringFromNullToEmpty(ref value); + ValidatorMethod.Invoke(null, new object?[] { this, value }); } - protected void OnBeforeInsert(ColumnInfo column, ref object? value) + public static void DefaultValidator(ColumnInfo column, object value) { - if (column.IsGenerated && value is not null) + if (column.IsNotNull && value is DbNullValue) { - throw new Exception($"'{column.ColumnName}' can't be set manually. It is auto generated."); + throw new Exception("The column can't be null."); } + } + + public object? InvokeDefaultValueGenerator() + { + return DefaultValueGeneratorMethod?.Invoke(null, new object?[] { this }); + } + + public static object? DefaultDefaultValueGenerator(ColumnInfo column) + { + return DbNullValue.Instance; + } - var defaultValueGenerator = DefaultValueGeneratorMethod; - if (defaultValueGenerator is not null && value is null) + private void TryCoerceStringFromNullToEmpty(ref object? value) + { + if (ColumnType.ClrType == typeof(string) && (Metadata.GetValueOrDefault<bool?>(ColumnMetadataKeys.DefaultEmptyForString) is true) && value is DbNullValue) { - value = defaultValueGenerator.Invoke(null, null); + value = ""; } + } + protected void OnAfterSelect(ColumnInfo column, ref object? value) + { TryCoerceStringFromNullToEmpty(ref value); + } - if (IsNotNull && (value is null || value is DbNullValue)) - { - throw new Exception($"'{column.ColumnName}' can't be null."); - } + protected void OnBeforeInsert(ColumnInfo column, ref object? value) + { + TryCoerceStringFromNullToEmpty(ref value); } protected void OnBeforeUpdate(ColumnInfo column, ref object? value) { - if (column.IsNoUpdate && value is not null) - { - throw new Exception($"'{column.ColumnName}' is not updatable."); - } + if (IsNoUpdate && value is not null) + throw new Exception("The column can't be updated."); TryCoerceStringFromNullToEmpty(ref value); - - if (IsNotNull && value is DbNullValue) - { - throw new Exception($"'{column.ColumnName}' can't be null."); - } } public string GenerateCreateTableColumnString(string? dbProviderId = null) diff --git a/docker/crupest-api/CrupestApi/CrupestApi.Commons/Crud/ColumnMetadata.cs b/docker/crupest-api/CrupestApi/CrupestApi.Commons/Crud/ColumnMetadata.cs index c02f776..1082ea4 100644 --- a/docker/crupest-api/CrupestApi/CrupestApi.Commons/Crud/ColumnMetadata.cs +++ b/docker/crupest-api/CrupestApi/CrupestApi.Commons/Crud/ColumnMetadata.cs @@ -20,11 +20,18 @@ public static class ColumnMetadataKeys /// <summary> /// The default value generator method name in entity type. Default to null, aka, search for ColumnNameDefaultValueGenerator. + /// Generator has signature <code>static void DefaultValueGenerator(ColumnInfo column)</code> /// </summary> - /// <returns></returns> public const string DefaultValueGenerator = nameof(ColumnAttribute.DefaultValueGenerator); /// <summary> + /// The validator method name in entity type. Default to null, aka, the default validator. + /// Validator has signature <code>static void Validator(ColumnInfo column, object value)</code> + /// Value param is never null. If you want to mean NULL, it should be a <see cref="DbNullValue"/>. + /// </summary> + public const string Validator = nameof(ColumnAttribute.Validator); + + /// <summary> /// The column can only be set when inserted, can't be changed in update. /// </summary> /// <returns></returns> @@ -34,24 +41,6 @@ public static class ColumnMetadataKeys /// This column acts as key when get one entity for http get method in path. /// </summary> public const string ActAsKey = nameof(ColumnAttribute.ActAsKey); - - /// <summary> - /// Define what to do when update. - /// </summary> - public const string UpdateBehavior = nameof(ColumnAttribute.UpdateBehavior); -} - -[Flags] -public enum UpdateBehavior -{ - /// <summary> - /// Null value means do not update that column. - /// </summary> - NullIsNotUpdate = 0, - /// <summary> - /// Null value means set to null. - /// </summary> - NullIsSetNull = 1 } public interface IColumnMetadata @@ -125,15 +114,15 @@ public class ColumnAttribute : Attribute, IColumnMetadata /// <seealso cref="ColumnMetadataKeys.DefaultValueGenerator"/> public string? DefaultValueGenerator { get; init; } + /// <seealso cref="ColumnMetadataKeys.Validator"/> + public string? Validator { get; init; } + /// <seealso cref="ColumnMetadataKeys.NoUpdate"/> public bool NoUpdate { get; init; } /// <seealso cref="ColumnMetadataKeys.ActAsKey"/> public bool ActAsKey { get; init; } - /// <seealso cref="ColumnMetadataKeys.UpdateBehavior"> - public UpdateBehavior UpdateBehavior { get; init; } = UpdateBehavior.NullIsNotUpdate; - public bool TryGetValue(string key, out object? value) { var property = GetType().GetProperty(key); diff --git a/docker/crupest-api/CrupestApi/CrupestApi.Commons/Crud/EntityJsonHelper.cs b/docker/crupest-api/CrupestApi/CrupestApi.Commons/Crud/EntityJsonHelper.cs index 19208d8..bbbbb4c 100644 --- a/docker/crupest-api/CrupestApi/CrupestApi.Commons/Crud/EntityJsonHelper.cs +++ b/docker/crupest-api/CrupestApi/CrupestApi.Commons/Crud/EntityJsonHelper.cs @@ -53,4 +53,42 @@ public class EntityJsonHelper<TEntity> where TEntity : class var dictionary = ConvertEntityToDictionary(entity); return JsonSerializer.Serialize(dictionary, _jsonSerializerOptions); } + + public virtual TEntity ConvertDictionaryToEntityForInsert(IReadOnlyDictionary<string, object?> dictionary) + { + var result = Activator.CreateInstance<TEntity>()!; + + foreach (var column in _table.PropertyColumns) + { + var propertyInfo = column.PropertyInfo!; + var value = dictionary.GetValueOrDefault(column.ColumnName); + if (column.IsGenerated) + { + if (value is not null) + { + throw new UserException($"{propertyInfo.Name} is auto generated. Don't specify it."); + } + } + + if (value is null) + { + if (column.IsNotNull && !column.CanBeGenerated) + { + throw new UserException($"{propertyInfo.Name} can't be null."); + } + propertyInfo.SetValue(result, null); + } + else + { + // Check type + var columnType = column.ColumnType; + if (columnType.ClrType.IsAssignableFrom(value.GetType())) + propertyInfo.SetValue(result, value); + else + throw new UserException($"{propertyInfo.Name} is of wrong type."); + } + } + + return result; + } } diff --git a/docker/crupest-api/CrupestApi/CrupestApi.Commons/Crud/README.md b/docker/crupest-api/CrupestApi/CrupestApi.Commons/Crud/README.md index 589b0a8..22289cb 100644 --- a/docker/crupest-api/CrupestApi/CrupestApi.Commons/Crud/README.md +++ b/docker/crupest-api/CrupestApi/CrupestApi.Commons/Crud/README.md @@ -10,20 +10,33 @@ The ultimate CRUD scaffold finally comes. 1. Create select `what`, where clause, order clause, `Offset` and `Limit`. 2. Check clauses' related columns are valid. Then generate sql string and param list. -3. Convert param list to `Dapper` dynamic params. Execute sql and get `dynamic`s. -4. Run hook `AfterSelect` for every column. +3. Convert param list to `Dapper` dynamic params with proper type conversion in `IColumnTypeInfo`. Execute sql and get `dynamic`s. +4. For each column: + 1. If column not in query result is null, null will be used to call hooks. + 2. If column is `NULL`, `DbNullValue` will be used to call hooks. + 3. Otherwise run conversion in `IColumnTypeInfo`. + 4. Run hook `AfterSelect` for every column. 5. Convert `dynamic`s to `TEntity`s. ### Insert 1. Create insert clause consisting of insert items. 2. Check clauses' related columns are valid. Then generate sql string and param list. -3. Run hook `BeforeInsert` for every column. -4. Convert param list to `Dapper` dynamic params. Execute sql and return `KeyColumn` value. +3. For each column: + 1. If insert item exits and value is not null but the column `IsGenerated` is true, throw exception. + 2. If insert item does not exist or value is `null` for that column, use default value generator to generate value. However, `DbNullValue` always means use `NULL` for that column. + 3. Coerce null to `DbNullValue`. + 4. Run hook `BeforeInsert`. + 5. Coerce null to `DbNullValue`. + 6. Run validator. +4. Convert param list to `Dapper` dynamic params with proper type conversion in `IColumnTypeInfo`. Execute sql and return `KeyColumn` value. ### Update -1. Create update clause consisting of update items, where clause. +1. Create update clause consisted of update items, where clause. 2. Check clauses' related columns are valid. Then generate sql string and param list. -3. Run hook `BeforeUpdate` for every column. -4. Convert param list to `Dapper` dynamic params. Execute sql and get count of affected rows. +3. For each column: + 1. If insert item does not exist, `null` will be used to call hooks. However, `DbNullValue` always means use `NULL` for that column. + 2. Run hook `BeforeInsert`. If value is null, it means do not update this column. + 3. Run validator if `value` is not null. +4. Convert param list to `Dapper` dynamic params with proper type conversion in `IColumnTypeInfo`. Execute sql and get count of affected rows. diff --git a/docker/crupest-api/CrupestApi/CrupestApi.Commons/Crud/TableInfo.cs b/docker/crupest-api/CrupestApi/CrupestApi.Commons/Crud/TableInfo.cs index b511b68..15b6320 100644 --- a/docker/crupest-api/CrupestApi/CrupestApi.Commons/Crud/TableInfo.cs +++ b/docker/crupest-api/CrupestApi/CrupestApi.Commons/Crud/TableInfo.cs @@ -431,7 +431,7 @@ CREATE TABLE {tableName}( else { value = dynamicProperty.GetValue(d); - if (value is null || value is DbNullValue) + if (value is null) value = DbNullValue.Instance; else value = column.ColumnType.ConvertFromDatabase(value); @@ -502,25 +502,44 @@ CREATE TABLE {tableName}( foreach (var column in Columns) { InsertItem? item = insert.Items.SingleOrDefault(i => i.ColumnName == column.ColumnName); - if (item is null) + object? value; + if (item is null || item.Value is null) { - object? value = null; - column.Hooks.BeforeInsert(column, ref value); - if (value is null || value is DbNullValue) - realInsert.Add(column.ColumnName, null); - else - realInsert.Add(column.ColumnName, value); + value = null; } else { - object? value = item.Value ?? DbNullValue.Instance; - column.Hooks.BeforeInsert(column, ref value); - if (value is null || value is DbNullValue) - realInsert.Add(column.ColumnName, null); - else - realInsert.Add(column.ColumnName, value); + value = item.Value; + } + + if (column.IsGenerated && value is not null) + { + throw new Exception("The column is generated. You can't specify it explicitly."); + } + + if (value is null) + { + value = column.InvokeDefaultValueGenerator(); + } + + if (value is null) + { + value = DbNullValue.Instance; } + column.Hooks.BeforeInsert(column, ref value); + + if (value is null) + value = DbNullValue.Instance; + + column.InvokeValidator(value); + + if (value is DbNullValue) + realInsert.Add(column.ColumnName, null); + else + realInsert.Add(column.ColumnName, value); + + if (item?.ColumnName == KeyColumn.ColumnName) { key = item.Value; @@ -546,25 +565,22 @@ CREATE TABLE {tableName}( { UpdateItem? item = update.Items.FirstOrDefault(i => i.ColumnName == column.ColumnName); + object? value; if (item is null) { - object? value = null; - column.Hooks.BeforeUpdate(column, ref value); - if (value is not null) - if (value is DbNullValue) - realUpdate.Add(column.ColumnName, null); - else - realUpdate.Add(column.ColumnName, value); + value = null; } else { - object? value = item.Value ?? DbNullValue.Instance; - column.Hooks.BeforeUpdate(column, ref value); - if (value is not null) - if (value is DbNullValue) - realUpdate.Add(column.ColumnName, null); - else - realUpdate.Add(column.ColumnName, value); + value = item.Value ?? DbNullValue.Instance; + } + + column.Hooks.BeforeUpdate(column, ref value); + + if (value is not null) + { + column.InvokeValidator(value); + realUpdate.Add(column.ColumnName, value); } } |