From 81f9571072a7978fe8b65dd9645d30e351138acd Mon Sep 17 00:00:00 2001 From: crupest Date: Mon, 12 Dec 2022 20:30:55 +0800 Subject: Develop secret api. v31 --- .../CrupestApi.Commons/Crud/ColumnInfo.cs | 35 +++++++++++-- .../CrupestApi.Commons/Crud/ColumnMetadata.cs | 6 +-- .../CrupestApi.Commons/Crud/ColumnTypeInfo.cs | 44 +++++++++++++++++ .../CrupestApi.Commons/Crud/CrudService.cs | 6 +-- .../Crud/CrudWebApplicationExtensions.cs | 2 - .../CrupestApi.Commons/Crud/EntityJsonHelper.cs | 49 +++++++++++++------ .../CrupestApi.Commons/Crud/InternalException.cs | 15 ------ .../CrupestApi/CrupestApi.Commons/Crud/ParamMap.cs | 8 +++ .../CrupestApi.Commons/Crud/TableInfo.cs | 57 +++++++++++----------- .../CrupestApi.Commons/Crud/UserException.cs | 15 ++++++ .../CrupestApi/CrupestApi.Commons/Json.cs | 28 ----------- 11 files changed, 166 insertions(+), 99 deletions(-) delete mode 100644 docker/crupest-api/CrupestApi/CrupestApi.Commons/Crud/InternalException.cs create mode 100644 docker/crupest-api/CrupestApi/CrupestApi.Commons/Crud/UserException.cs (limited to 'docker/crupest-api/CrupestApi/CrupestApi.Commons') 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; } - /// Called after SELECT. Please use multicast if you want to customize it because there are many default behavior in it.Called after SELECT. Please use multicast if you want to customize it because there are many default behavior in it. + /// + /// value(in): + /// null => not found in SELECT result + /// DbNullValue => database NULL + /// others => database value + /// value(out): + /// null/DbNullValue => return null + /// others => return as is + /// public ColumnHookAction AfterSelect; /// Called before INSERT. Please use multicast if you want to customize it because there are many default behavior in it. + /// + /// 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 + /// public ColumnHookAction BeforeInsert; /// Called before UPDATE. Please use multicast if you want to customize it because there are many default behavior in it.Set value to null to delete the update item so it will not change. Set value to to update the column to NULL. + /// + /// 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 + /// 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; /// /// 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 /// /// This indicates that you take care of generate this column value when create entity. User calling the api can not specify the value. /// - public const string ClientGenerate = nameof(ColumnAttribute.DefaultEmptyForString); + public const string Generated = nameof(ColumnAttribute.Generated); /// /// 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 /// public bool DefaultEmptyForString { get; init; } - /// - public bool ClientGenerate { get; init; } + /// + public bool Generated { get; init; } /// 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 GetAll(); IColumnTypeInfo Get(Type clrType); + + IList GetAllCustom() + { + return GetAll().Where(t => !t.IsSimple).ToList(); + } } public class SimpleColumnTypeInfo : IColumnTypeInfo @@ -105,9 +113,18 @@ public class SimpleColumnTypeInfo : IColumnTypeInfo public class DateTimeColumnTypeInfo : IColumnTypeInfo { + private JsonConverter _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 +{ + 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 _typeMap = new Dictionary(); @@ -147,6 +186,11 @@ public class ColumnTypeProvider : IColumnTypeProvider _typeMap.Add(IColumnTypeInfo.DateTimeColumnTypeInfo.ClrType, IColumnTypeInfo.DateTimeColumnTypeInfo); } + public IReadOnlyList 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 : IDisposable where TEntity : class protected readonly TableInfo _table; protected readonly string? _connectionName; protected readonly IDbConnection _dbConnection; - protected readonly EntityJsonHelper _jsonHelper; + protected readonly EntityJsonHelper _jsonHelper; private readonly ILogger> _logger; - public CrudService(string? connectionName, ITableInfoFactory tableInfoFactory, IDbConnectionFactory dbConnectionFactory, ILoggerFactory loggerFactory) + public CrudService(string? connectionName, ITableInfoFactory tableInfoFactory, IDbConnectionFactory dbConnectionFactory, EntityJsonHelper jsonHelper, ILoggerFactory loggerFactory) { _connectionName = connectionName; _table = tableInfoFactory.Get(typeof(TEntity)); _dbConnection = dbConnectionFactory.Get(_connectionName); - _jsonHelper = new EntityJsonHelper(_table); + _jsonHelper = jsonHelper; _logger = loggerFactory.CreateLogger>(); 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>(); - 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. +/// +/// Contains all you need to do with json. +/// +public class EntityJsonHelper 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(null); + Debug.Assert(entity is null || entity is TEntity); + return JsonSerializer.SerializeToDocument((TEntity?)entity, _jsonSerializerOptions); + } - var result = new Dictionary(); + public virtual TEntity? ConvertJsonToEntity(JsonDocument json) + { + var entity = json.Deserialize(); + 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/InternalException.cs b/docker/crupest-api/CrupestApi/CrupestApi.Commons/Crud/InternalException.cs deleted file mode 100644 index 1a10b97..0000000 --- a/docker/crupest-api/CrupestApi/CrupestApi.Commons/Crud/InternalException.cs +++ /dev/null @@ -1,15 +0,0 @@ -namespace CrupestApi.Commons.Crud; - -/// -/// This exception means the exception is caused by user and can be safely shown to user. -/// -[System.Serializable] -public class UserException : Exception -{ - public UserException() { } - public UserException(string message) : base(message) { } - public UserException(string message, System.Exception inner) : base(message, inner) { } - protected UserException( - System.Runtime.Serialization.SerializationInfo info, - System.Runtime.Serialization.StreamingContext context) : base(info, context) { } -} \ No newline at end of file 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; /// /// 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. /// +/// +/// If value is DbNullValue, it will be treated as null. +/// public record ParamInfo(string Name, object? Value, string? ColumnName = null); public class ParamList : List @@ -41,6 +44,11 @@ public class ParamList : List return this.SingleOrDefault(p => p.Name.Equals(name, StringComparison.OrdinalIgnoreCase)) is not null; } + public T? Get(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; +/// +/// Contains all you need to manipulate a table. +/// public class TableInfo { private readonly IColumnTypeProvider _columnTypeProvider; @@ -30,7 +33,6 @@ public class TableInfo ColumnInfo? primaryKeyColumn = null; ColumnInfo? keyColumn = null; - List columnProperties = new(); List 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>(() => ColumnInfos.Select(c => c.ColumnName).ToList()); + _lazyColumnNameList = new Lazy>(() => 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 ColumnInfos { get; } + public IReadOnlyList Columns { get; } + public IReadOnlyList PropertyColumns => Columns.Where(c => c.PropertyInfo is not null).ToList(); public ColumnInfo PrimaryKeyColumn { get; } /// /// Maybe not the primary key. But acts as primary key. /// /// public ColumnInfo KeyColumn { get; } - public IReadOnlyList ColumnProperties { get; } + public IReadOnlyList ColumnProperties => PropertyColumns.Select(c => c.PropertyInfo!).ToList(); public IReadOnlyList NonColumnProperties { get; } public IReadOnlyList 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 sqlNameSet = new HashSet(); - 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(ColumnInfos.Select(c => c.ColumnName)); + var columnNameSet = new HashSet(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/UserException.cs b/docker/crupest-api/CrupestApi/CrupestApi.Commons/Crud/UserException.cs new file mode 100644 index 0000000..1a10b97 --- /dev/null +++ b/docker/crupest-api/CrupestApi/CrupestApi.Commons/Crud/UserException.cs @@ -0,0 +1,15 @@ +namespace CrupestApi.Commons.Crud; + +/// +/// This exception means the exception is caused by user and can be safely shown to user. +/// +[System.Serializable] +public class UserException : Exception +{ + public UserException() { } + public UserException(string message) : base(message) { } + public UserException(string message, System.Exception inner) : base(message, inner) { } + protected UserException( + System.Runtime.Serialization.SerializationInfo info, + System.Runtime.Serialization.StreamingContext context) : base(info, context) { } +} \ No newline at end of file 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(); -- cgit v1.2.3