diff options
Diffstat (limited to 'docker/crupest-api/CrupestApi/CrupestApi.Commons')
-rw-r--r-- | docker/crupest-api/CrupestApi/CrupestApi.Commons/Crud/ColumnInfo.cs | 35 | ||||
-rw-r--r-- | docker/crupest-api/CrupestApi/CrupestApi.Commons/Crud/ColumnMetadata.cs | 6 | ||||
-rw-r--r-- | docker/crupest-api/CrupestApi/CrupestApi.Commons/Crud/ColumnTypeInfo.cs | 44 | ||||
-rw-r--r-- | docker/crupest-api/CrupestApi/CrupestApi.Commons/Crud/CrudService.cs | 6 | ||||
-rw-r--r-- | docker/crupest-api/CrupestApi/CrupestApi.Commons/Crud/CrudWebApplicationExtensions.cs | 2 | ||||
-rw-r--r-- | docker/crupest-api/CrupestApi/CrupestApi.Commons/Crud/EntityJsonHelper.cs | 49 | ||||
-rw-r--r-- | docker/crupest-api/CrupestApi/CrupestApi.Commons/Crud/ParamMap.cs | 8 | ||||
-rw-r--r-- | docker/crupest-api/CrupestApi/CrupestApi.Commons/Crud/TableInfo.cs | 57 | ||||
-rw-r--r-- | docker/crupest-api/CrupestApi/CrupestApi.Commons/Crud/UserException.cs (renamed from docker/crupest-api/CrupestApi/CrupestApi.Commons/Crud/InternalException.cs) | 0 | ||||
-rw-r--r-- | docker/crupest-api/CrupestApi/CrupestApi.Commons/Json.cs | 28 |
10 files changed, 151 insertions, 84 deletions
diff --git a/docker/crupest-api/CrupestApi/CrupestApi.Commons/Crud/ColumnInfo.cs b/docker/crupest-api/CrupestApi/CrupestApi.Commons/Crud/ColumnInfo.cs index 37ae971..6e29de0 100644 --- a/docker/crupest-api/CrupestApi/CrupestApi.Commons/Crud/ColumnInfo.cs +++ b/docker/crupest-api/CrupestApi/CrupestApi.Commons/Crud/ColumnInfo.cs @@ -19,14 +19,41 @@ public class ColumnHooks BeforeUpdate = beforeUpdate; } - /// <summary>Called after SELECT. Please use multicast if you want to customize it because there are many default behavior in it.</summary + /// <summary>Called after SELECT. Please use multicast if you want to customize it because there are many default behavior in it.</summary> + /// <remarks> + /// value(in): + /// null => not found in SELECT result + /// DbNullValue => database NULL + /// others => database value + /// value(out): + /// null/DbNullValue => return null + /// others => return as is + /// </remarks> public ColumnHookAction AfterSelect; /// <summary>Called before INSERT. Please use multicast if you want to customize it because there are many default behavior in it.</summary> + /// <remarks> + /// value(in): + /// null => not specified by insert clause + /// DbNullValue => specified as database NULL + /// other => specified as other value + /// value(out): + /// null/DbNullValue => save database NULL + /// other => save the value as is + /// </remarks> public ColumnHookAction BeforeInsert; /// <summary>Called before UPDATE. Please use multicast if you want to customize it because there are many default behavior in it.</summary - /// <remarks>Set value to null to delete the update item so it will not change. Set value to <see cref="DbNullValue"/> to update the column to NULL.</remarks> + /// <remarks> + /// value(in): + /// null => not specified by update clause + /// DbNullValue => specified as database NULL + /// other => specified as other value + /// value(out): + /// null => not update + /// DbNullValue => update to database NULL + /// other => update to the value + /// </remarks> public ColumnHookAction BeforeUpdate; } @@ -86,7 +113,7 @@ public class ColumnInfo public bool IsPrimaryKey => Metadata.GetValueOrDefault(ColumnMetadataKeys.IsPrimaryKey) is true; public bool IsAutoIncrement => Metadata.GetValueOrDefault(ColumnMetadataKeys.IsAutoIncrement) is true; public bool IsNotNull => IsPrimaryKey || Metadata.GetValueOrDefault(ColumnMetadataKeys.NotNull) is true; - public bool IsClientGenerate => Metadata.GetValueOrDefault(ColumnMetadataKeys.ClientGenerate) is true; + public bool IsGenerated => Metadata.GetValueOrDefault(ColumnMetadataKeys.Generated) is true; public bool IsNoUpdate => Metadata.GetValueOrDefault(ColumnMetadataKeys.NoUpdate) is true; /// <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. @@ -148,7 +175,7 @@ public class ColumnInfo protected void OnBeforeInsert(ColumnInfo column, ref object? value) { - if (column.IsClientGenerate && value is not null) + if (column.IsGenerated && value is not null) { throw new Exception($"'{column.ColumnName}' can't be set manually. It is auto generated."); } diff --git a/docker/crupest-api/CrupestApi/CrupestApi.Commons/Crud/ColumnMetadata.cs b/docker/crupest-api/CrupestApi/CrupestApi.Commons/Crud/ColumnMetadata.cs index e7c74f3..c02f776 100644 --- a/docker/crupest-api/CrupestApi/CrupestApi.Commons/Crud/ColumnMetadata.cs +++ b/docker/crupest-api/CrupestApi/CrupestApi.Commons/Crud/ColumnMetadata.cs @@ -16,7 +16,7 @@ public static class ColumnMetadataKeys /// <summary> /// This indicates that you take care of generate this column value when create entity. User calling the api can not specify the value. /// </summary> - public const string ClientGenerate = nameof(ColumnAttribute.DefaultEmptyForString); + public const string Generated = nameof(ColumnAttribute.Generated); /// <summary> /// The default value generator method name in entity type. Default to null, aka, search for ColumnNameDefaultValueGenerator. @@ -119,8 +119,8 @@ public class ColumnAttribute : Attribute, IColumnMetadata /// <seealso cref="ColumnMetadataKeys.DefaultEmptyForString"/> public bool DefaultEmptyForString { get; init; } - /// <seealso cref="ColumnMetadataKeys.ClientGenerate"/> - public bool ClientGenerate { get; init; } + /// <seealso cref="ColumnMetadataKeys.Generated"/> + public bool Generated { get; init; } /// <seealso cref="ColumnMetadataKeys.DefaultValueGenerator"/> public string? DefaultValueGenerator { get; init; } diff --git a/docker/crupest-api/CrupestApi/CrupestApi.Commons/Crud/ColumnTypeInfo.cs b/docker/crupest-api/CrupestApi/CrupestApi.Commons/Crud/ColumnTypeInfo.cs index c678e0e..2f15e50 100644 --- a/docker/crupest-api/CrupestApi/CrupestApi.Commons/Crud/ColumnTypeInfo.cs +++ b/docker/crupest-api/CrupestApi/CrupestApi.Commons/Crud/ColumnTypeInfo.cs @@ -77,6 +77,8 @@ public interface IColumnTypeInfo }; } + JsonConverter? JsonConverter { get { return null; } } + // You must override this method if ClrType != DatabaseClrType object? ConvertFromDatabase(object? databaseValue) { @@ -94,7 +96,13 @@ public interface IColumnTypeInfo public interface IColumnTypeProvider { + IReadOnlyList<IColumnTypeInfo> GetAll(); IColumnTypeInfo Get(Type clrType); + + IList<IColumnTypeInfo> GetAllCustom() + { + return GetAll().Where(t => !t.IsSimple).ToList(); + } } public class SimpleColumnTypeInfo<T> : IColumnTypeInfo @@ -105,9 +113,18 @@ public class SimpleColumnTypeInfo<T> : IColumnTypeInfo public class DateTimeColumnTypeInfo : IColumnTypeInfo { + private JsonConverter<DateTime> _jsonConverter; + + public DateTimeColumnTypeInfo() + { + _jsonConverter = new DateTimeJsonConverter(this); + } + public Type ClrType => typeof(DateTime); public Type DatabaseClrType => typeof(string); + public JsonConverter JsonConverter => _jsonConverter; + public object? ConvertToDatabase(object? value) { if (value is null) return null; @@ -130,6 +147,28 @@ public class DateTimeColumnTypeInfo : IColumnTypeInfo } } +public class DateTimeJsonConverter : JsonConverter<DateTime> +{ + private readonly DateTimeColumnTypeInfo _typeInfo; + + public DateTimeJsonConverter(DateTimeColumnTypeInfo typeInfo) + { + _typeInfo = typeInfo; + } + + public override DateTime Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + var databaseValue = reader.GetString(); + return (DateTime)_typeInfo.ConvertFromDatabase(databaseValue)!; + } + + public override void Write(Utf8JsonWriter writer, DateTime value, JsonSerializerOptions options) + { + var databaseValue = _typeInfo.ConvertToDatabase(value); + writer.WriteStringValue((string)databaseValue!); + } +} + public class ColumnTypeProvider : IColumnTypeProvider { private Dictionary<Type, IColumnTypeInfo> _typeMap = new Dictionary<Type, IColumnTypeInfo>(); @@ -147,6 +186,11 @@ public class ColumnTypeProvider : IColumnTypeProvider _typeMap.Add(IColumnTypeInfo.DateTimeColumnTypeInfo.ClrType, IColumnTypeInfo.DateTimeColumnTypeInfo); } + public IReadOnlyList<IColumnTypeInfo> GetAll() + { + return _typeMap.Values.ToList(); + } + // This is thread-safe. public IColumnTypeInfo Get(Type clrType) { diff --git a/docker/crupest-api/CrupestApi/CrupestApi.Commons/Crud/CrudService.cs b/docker/crupest-api/CrupestApi/CrupestApi.Commons/Crud/CrudService.cs index f0af62a..7944c18 100644 --- a/docker/crupest-api/CrupestApi/CrupestApi.Commons/Crud/CrudService.cs +++ b/docker/crupest-api/CrupestApi/CrupestApi.Commons/Crud/CrudService.cs @@ -10,15 +10,15 @@ public class CrudService<TEntity> : IDisposable where TEntity : class protected readonly TableInfo _table; protected readonly string? _connectionName; protected readonly IDbConnection _dbConnection; - protected readonly EntityJsonHelper _jsonHelper; + protected readonly EntityJsonHelper<TEntity> _jsonHelper; private readonly ILogger<CrudService<TEntity>> _logger; - public CrudService(string? connectionName, ITableInfoFactory tableInfoFactory, IDbConnectionFactory dbConnectionFactory, ILoggerFactory loggerFactory) + public CrudService(string? connectionName, ITableInfoFactory tableInfoFactory, IDbConnectionFactory dbConnectionFactory, EntityJsonHelper<TEntity> jsonHelper, ILoggerFactory loggerFactory) { _connectionName = connectionName; _table = tableInfoFactory.Get(typeof(TEntity)); _dbConnection = dbConnectionFactory.Get(_connectionName); - _jsonHelper = new EntityJsonHelper(_table); + _jsonHelper = jsonHelper; _logger = loggerFactory.CreateLogger<CrudService<TEntity>>(); if (!_table.CheckExistence(_dbConnection)) diff --git a/docker/crupest-api/CrupestApi/CrupestApi.Commons/Crud/CrudWebApplicationExtensions.cs b/docker/crupest-api/CrupestApi/CrupestApi.Commons/Crud/CrudWebApplicationExtensions.cs index 46b2e5b..60a0d5b 100644 --- a/docker/crupest-api/CrupestApi/CrupestApi.Commons/Crud/CrudWebApplicationExtensions.cs +++ b/docker/crupest-api/CrupestApi/CrupestApi.Commons/Crud/CrudWebApplicationExtensions.cs @@ -7,9 +7,7 @@ public static class CrudWebApplicationExtensions app.MapGet(path, async (context) => { var crudService = context.RequestServices.GetRequiredService<CrudService<TEntity>>(); - var result = crudService.SelectAsJson(null); - await context.ResponseJsonAsync(result); }); diff --git a/docker/crupest-api/CrupestApi/CrupestApi.Commons/Crud/EntityJsonHelper.cs b/docker/crupest-api/CrupestApi/CrupestApi.Commons/Crud/EntityJsonHelper.cs index a1e4583..6f2f446 100644 --- a/docker/crupest-api/CrupestApi/CrupestApi.Commons/Crud/EntityJsonHelper.cs +++ b/docker/crupest-api/CrupestApi/CrupestApi.Commons/Crud/EntityJsonHelper.cs @@ -1,36 +1,55 @@ +using System.Diagnostics; using System.Text.Json; +using Microsoft.Extensions.Options; namespace CrupestApi.Commons.Crud; -public class EntityJsonHelper +// TODO: Register this. +/// <summary> +/// Contains all you need to do with json. +/// </summary> +public class EntityJsonHelper<TEntity> where TEntity : class { private readonly TableInfo _table; + private readonly JsonSerializerOptions _jsonSerializerOptions; - public EntityJsonHelper(TableInfo table) + public EntityJsonHelper(TableInfoFactory tableInfoFactory) { - _table = table; + _table = tableInfoFactory.Get(typeof(TEntity)); + _jsonSerializerOptions = new JsonSerializerOptions(); + _jsonSerializerOptions.AllowTrailingCommas = true; + _jsonSerializerOptions.PropertyNameCaseInsensitive = true; + _jsonSerializerOptions.PropertyNamingPolicy = JsonNamingPolicy.CamelCase; + foreach (var type in _table.Columns.Select(c => c.ColumnType)) + { + if (type.JsonConverter is not null) + { + _jsonSerializerOptions.Converters.Add(type.JsonConverter); + } + } } public virtual JsonDocument ConvertEntityToJson(object? entity) { - if (entity is null) return JsonSerializer.SerializeToDocument<object?>(null); + Debug.Assert(entity is null || entity is TEntity); + return JsonSerializer.SerializeToDocument<TEntity?>((TEntity?)entity, _jsonSerializerOptions); + } - var result = new Dictionary<string, object?>(); + public virtual TEntity? ConvertJsonToEntity(JsonDocument json) + { + var entity = json.Deserialize<TEntity>(); + if (entity is null) return null; - foreach (var column in _table.ColumnInfos) + foreach (var column in _table.Columns) { - if (column.PropertyInfo is not null) + var propertyValue = column.PropertyInfo?.GetValue(entity); + + if ((column.IsAutoIncrement || column.IsGenerated) && propertyValue is not null) { - result.Add(column.ColumnName, column.PropertyInfo.GetValue(entity)); + throw new Exception("You can't specify this property because it is auto generated."); } } - return JsonSerializer.SerializeToDocument(result); - } - - public virtual object? ConvertJsonToEntity(JsonDocument? json) - { - // TODO: Implement this. - throw new NotImplementedException(); + return entity; } } diff --git a/docker/crupest-api/CrupestApi/CrupestApi.Commons/Crud/ParamMap.cs b/docker/crupest-api/CrupestApi/CrupestApi.Commons/Crud/ParamMap.cs index 841ba40..37d77ca 100644 --- a/docker/crupest-api/CrupestApi/CrupestApi.Commons/Crud/ParamMap.cs +++ b/docker/crupest-api/CrupestApi/CrupestApi.Commons/Crud/ParamMap.cs @@ -6,6 +6,9 @@ namespace CrupestApi.Commons.Crud; /// <summary> /// <see cref="ColumnName"/> is an optional column name related to the param. You may use it to do some column related things. Like use a more accurate conversion. /// </summary> +/// <remarks> +/// If value is DbNullValue, it will be treated as null. +/// </remarks> public record ParamInfo(string Name, object? Value, string? ColumnName = null); public class ParamList : List<ParamInfo> @@ -41,6 +44,11 @@ public class ParamList : List<ParamInfo> return this.SingleOrDefault(p => p.Name.Equals(name, StringComparison.OrdinalIgnoreCase)) is not null; } + public T? Get<T>(string key) + { + return (T?)this.SingleOrDefault(p => p.Name.Equals(key, StringComparison.OrdinalIgnoreCase))?.Value; + } + public object? this[string key] { get diff --git a/docker/crupest-api/CrupestApi/CrupestApi.Commons/Crud/TableInfo.cs b/docker/crupest-api/CrupestApi/CrupestApi.Commons/Crud/TableInfo.cs index 58a4396..b511b68 100644 --- a/docker/crupest-api/CrupestApi/CrupestApi.Commons/Crud/TableInfo.cs +++ b/docker/crupest-api/CrupestApi/CrupestApi.Commons/Crud/TableInfo.cs @@ -5,6 +5,9 @@ using Dapper; namespace CrupestApi.Commons.Crud; +/// <summary> +/// Contains all you need to manipulate a table. +/// </summary> public class TableInfo { private readonly IColumnTypeProvider _columnTypeProvider; @@ -30,7 +33,6 @@ public class TableInfo ColumnInfo? primaryKeyColumn = null; ColumnInfo? keyColumn = null; - List<PropertyInfo> columnProperties = new(); List<PropertyInfo> nonColumnProperties = new(); foreach (var property in properties) @@ -55,7 +57,6 @@ public class TableInfo } keyColumn = columnInfo; } - columnProperties.Add(property); } else { @@ -75,15 +76,14 @@ public class TableInfo keyColumn = primaryKeyColumn; } - ColumnInfos = columnInfos; + Columns = columnInfos; PrimaryKeyColumn = primaryKeyColumn; KeyColumn = keyColumn; - ColumnProperties = columnProperties; NonColumnProperties = nonColumnProperties; CheckValidity(); - _lazyColumnNameList = new Lazy<List<string>>(() => ColumnInfos.Select(c => c.ColumnName).ToList()); + _lazyColumnNameList = new Lazy<List<string>>(() => Columns.Select(c => c.ColumnName).ToList()); } private ColumnInfo CreateAutoIdColumn() @@ -101,14 +101,15 @@ public class TableInfo public Type EntityType { get; } public string TableName { get; } - public IReadOnlyList<ColumnInfo> ColumnInfos { get; } + public IReadOnlyList<ColumnInfo> Columns { get; } + public IReadOnlyList<ColumnInfo> PropertyColumns => Columns.Where(c => c.PropertyInfo is not null).ToList(); public ColumnInfo PrimaryKeyColumn { get; } /// <summary> /// Maybe not the primary key. But acts as primary key. /// </summary> /// <seealso cref="ColumnMetadataKeys.ActAsKey"/> public ColumnInfo KeyColumn { get; } - public IReadOnlyList<PropertyInfo> ColumnProperties { get; } + public IReadOnlyList<PropertyInfo> ColumnProperties => PropertyColumns.Select(c => c.PropertyInfo!).ToList(); public IReadOnlyList<PropertyInfo> NonColumnProperties { get; } public IReadOnlyList<string> ColumnNameList => _lazyColumnNameList.Value; @@ -121,7 +122,7 @@ public class TableInfo public ColumnInfo GetColumn(string columnName) { - foreach (var column in ColumnInfos) + foreach (var column in Columns) { if (column.ColumnName.Equals(columnName, StringComparison.OrdinalIgnoreCase)) { @@ -136,7 +137,7 @@ public class TableInfo // Check if there is only one primary key. bool hasPrimaryKey = false; bool hasKey = false; - foreach (var column in ColumnInfos) + foreach (var column in Columns) { if (column.IsPrimaryKey) { @@ -155,7 +156,7 @@ public class TableInfo // Check two columns have the same sql name. HashSet<string> sqlNameSet = new HashSet<string>(); - foreach (var column in ColumnInfos) + foreach (var column in Columns) { if (sqlNameSet.Contains(column.ColumnName)) throw new Exception($"Two columns have the same sql name '{column.ColumnName}'."); @@ -167,7 +168,7 @@ public class TableInfo { var sb = new StringBuilder(); - foreach (var column in ColumnInfos) + foreach (var column in Columns) { if (column.Index == ColumnIndexType.None) continue; @@ -180,7 +181,7 @@ public class TableInfo public string GenerateCreateTableSql(bool createIndex = true, string? dbProviderId = null) { var tableName = TableName; - var columnSql = string.Join(",\n", ColumnInfos.Select(c => c.GenerateCreateTableColumnString(dbProviderId))); + var columnSql = string.Join(",\n", Columns.Select(c => c.GenerateCreateTableColumnString(dbProviderId))); var sql = $@" CREATE TABLE {tableName}( @@ -286,7 +287,7 @@ CREATE TABLE {tableName}( public void CheckInsertClause(IInsertClause insertClause) { - var columnNameSet = new HashSet<string>(ColumnInfos.Select(c => c.ColumnName)); + var columnNameSet = new HashSet<string>(Columns.Select(c => c.ColumnName)); foreach (var item in insertClause.Items) { @@ -419,7 +420,7 @@ CREATE TABLE {tableName}( return queryResult.Select(d => { Type dynamicType = d.GetType(); - foreach (var column in ColumnInfos) + foreach (var column in Columns) { object? value = null; var dynamicProperty = dynamicType.GetProperty(column.ColumnName); @@ -437,9 +438,9 @@ CREATE TABLE {tableName}( column.Hooks.AfterSelect(column, ref value); } - if (dynamicProperty is not null && value is not null) + if (dynamicProperty is not null) { - if (value is DbNullValue) + if (value is null || value is DbNullValue) dynamicProperty.SetValue(d, null); else dynamicProperty.SetValue(d, value); @@ -464,7 +465,7 @@ CREATE TABLE {tableName}( Type dynamicType = d.GetType(); Type resultType = typeof(TResult); - foreach (var column in ColumnInfos) + foreach (var column in Columns) { var dynamicProperty = dynamicType.GetProperty(column.ColumnName); // TODO: Maybe we can do better to get result property in case ColumnName is set to another value. @@ -498,28 +499,26 @@ CREATE TABLE {tableName}( var realInsert = InsertClause.Create(); - foreach (var column in ColumnInfos) + foreach (var column in Columns) { InsertItem? item = insert.Items.SingleOrDefault(i => i.ColumnName == column.ColumnName); if (item is null) { object? value = null; column.Hooks.BeforeInsert(column, ref value); - if (value is not null) - if (value is DbNullValue) - realInsert.Add(column.ColumnName, null); - else - realInsert.Add(column.ColumnName, value); + if (value is null || value is DbNullValue) + realInsert.Add(column.ColumnName, null); + else + realInsert.Add(column.ColumnName, value); } else { object? value = item.Value ?? DbNullValue.Instance; column.Hooks.BeforeInsert(column, ref value); - if (value is not null) - if (value is DbNullValue) - realInsert.Add(column.ColumnName, null); - else - realInsert.Add(column.ColumnName, value); + if (value is null || value is DbNullValue) + realInsert.Add(column.ColumnName, null); + else + realInsert.Add(column.ColumnName, value); } if (item?.ColumnName == KeyColumn.ColumnName) @@ -543,7 +542,7 @@ CREATE TABLE {tableName}( { var realUpdate = UpdateClause.Create(); - foreach (var column in ColumnInfos) + foreach (var column in Columns) { UpdateItem? item = update.Items.FirstOrDefault(i => i.ColumnName == column.ColumnName); diff --git a/docker/crupest-api/CrupestApi/CrupestApi.Commons/Crud/InternalException.cs b/docker/crupest-api/CrupestApi/CrupestApi.Commons/Crud/UserException.cs index 1a10b97..1a10b97 100644 --- a/docker/crupest-api/CrupestApi/CrupestApi.Commons/Crud/InternalException.cs +++ b/docker/crupest-api/CrupestApi/CrupestApi.Commons/Crud/UserException.cs diff --git a/docker/crupest-api/CrupestApi/CrupestApi.Commons/Json.cs b/docker/crupest-api/CrupestApi/CrupestApi.Commons/Json.cs index 8c4b34d..60b18e4 100644 --- a/docker/crupest-api/CrupestApi/CrupestApi.Commons/Json.cs +++ b/docker/crupest-api/CrupestApi/CrupestApi.Commons/Json.cs @@ -5,34 +5,6 @@ namespace CrupestApi.Commons; public static class CrupestApiJsonExtensions { - public static object? CheckJsonValueNotArrayOrObject(this JsonElement value) - { - if (value.ValueKind == JsonValueKind.Null && value.ValueKind == JsonValueKind.Undefined) - { - return null; - } - else if (value.ValueKind == JsonValueKind.True) - { - return true; - } - else if (value.ValueKind == JsonValueKind.False) - { - return false; - } - else if (value.ValueKind == JsonValueKind.Number) - { - return value.GetDouble(); - } - else if (value.ValueKind == JsonValueKind.String) - { - return value.GetString(); - } - else - { - throw new Exception("Only value not array or object is allowed."); - } - } - public static IServiceCollection AddJsonOptions(this IServiceCollection services) { services.AddOptions<JsonSerializerOptions>(); |