aboutsummaryrefslogtreecommitdiff
path: root/docker/crupest-api/CrupestApi/CrupestApi.Commons
diff options
context:
space:
mode:
authorcrupest <crupest@outlook.com>2022-12-08 14:52:58 +0800
committercrupest <crupest@outlook.com>2022-12-20 20:32:52 +0800
commit62c92f97358e2a98271aaf11fdd5626e21cf4689 (patch)
tree2ac7a1f7f8492876a3b245959ef68f92f8dec0ae /docker/crupest-api/CrupestApi/CrupestApi.Commons
parent78396f289ab50ce414bd8f65af8854ffb52fff48 (diff)
downloadcrupest-62c92f97358e2a98271aaf11fdd5626e21cf4689.tar.gz
crupest-62c92f97358e2a98271aaf11fdd5626e21cf4689.tar.bz2
crupest-62c92f97358e2a98271aaf11fdd5626e21cf4689.zip
Develop secret api. v18
Diffstat (limited to 'docker/crupest-api/CrupestApi/CrupestApi.Commons')
-rw-r--r--docker/crupest-api/CrupestApi/CrupestApi.Commons/Crud/ColumnInfo.cs57
-rw-r--r--docker/crupest-api/CrupestApi/CrupestApi.Commons/Crud/ColumnTypeInfo.cs2
-rw-r--r--docker/crupest-api/CrupestApi/CrupestApi.Commons/Crud/CrudService.cs84
-rw-r--r--docker/crupest-api/CrupestApi/CrupestApi.Commons/Crud/DbConnectionFactory.cs9
-rw-r--r--docker/crupest-api/CrupestApi/CrupestApi.Commons/Crud/TableInfo.cs202
-rw-r--r--docker/crupest-api/CrupestApi/CrupestApi.Commons/Crud/UpdateClause.cs15
6 files changed, 253 insertions, 116 deletions
diff --git a/docker/crupest-api/CrupestApi/CrupestApi.Commons/Crud/ColumnInfo.cs b/docker/crupest-api/CrupestApi/CrupestApi.Commons/Crud/ColumnInfo.cs
index e60b202..e67b7c0 100644
--- a/docker/crupest-api/CrupestApi/CrupestApi.Commons/Crud/ColumnInfo.cs
+++ b/docker/crupest-api/CrupestApi/CrupestApi.Commons/Crud/ColumnInfo.cs
@@ -4,20 +4,42 @@ using System.Text;
namespace CrupestApi.Commons.Crud;
+public class ColumnHooks
+{
+ public delegate void ColumnHookAction(ColumnInfo column, ref object? value);
+
+ public ColumnHooks(ColumnHookAction afterGet, ColumnHookAction beforeSet)
+ {
+ AfterGet = afterGet;
+ BeforeSet = beforeSet;
+ }
+
+ // Called after SELECT.
+ public ColumnHookAction AfterGet;
+
+ // Called before UPDATE and INSERT.
+ public ColumnHookAction BeforeSet;
+}
+
public class ColumnInfo
{
private readonly AggregateColumnMetadata _metadata = new AggregateColumnMetadata();
- public ColumnInfo(Type entityType, IColumnMetadata metadata, Type clrType, IColumnTypeProvider typeProvider)
+ public ColumnInfo(TableInfo table, IColumnMetadata metadata, Type clrType, IColumnTypeProvider typeProvider)
{
- EntityType = entityType;
+ Table = table;
_metadata.Add(metadata);
ColumnType = typeProvider.Get(clrType);
+
+ Hooks = new ColumnHooks(
+ new ColumnHooks.ColumnHookAction(OnAfterGet),
+ new ColumnHooks.ColumnHookAction(OnBeforeSet)
+ );
}
- public ColumnInfo(PropertyInfo propertyInfo, IColumnTypeProvider typeProvider)
+ public ColumnInfo(TableInfo table, PropertyInfo propertyInfo, IColumnTypeProvider typeProvider)
{
- EntityType = propertyInfo.DeclaringType!;
+ Table = table;
ColumnType = typeProvider.Get(propertyInfo.PropertyType);
var columnAttribute = propertyInfo.GetCustomAttribute<ColumnAttribute>();
@@ -25,9 +47,14 @@ public class ColumnInfo
{
_metadata.Add(columnAttribute);
}
+
+ Hooks = new ColumnHooks(
+ new ColumnHooks.ColumnHookAction(OnAfterGet),
+ new ColumnHooks.ColumnHookAction(OnBeforeSet)
+ );
}
- public Type EntityType { get; }
+ public TableInfo Table { get; }
// If null, there is no corresponding property.
public PropertyInfo? PropertyInfo { get; } = null;
@@ -35,6 +62,26 @@ public class ColumnInfo
public IColumnTypeInfo ColumnType { get; }
+ public ColumnHooks Hooks { get; }
+
+ private void TryCoerceStringFromNullToEmpty(ref object? value)
+ {
+ if (ColumnType.ClrType == typeof(string) && (Metadata.GetValueOrDefault<bool?>(ColumnMetadataKeys.DefaultEmptyForString) ?? false) && value is null)
+ {
+ value = "";
+ }
+ }
+
+ protected void OnAfterGet(ColumnInfo column, ref object? value)
+ {
+ TryCoerceStringFromNullToEmpty(ref value);
+ }
+
+ protected void OnBeforeSet(ColumnInfo column, ref object? value)
+ {
+ TryCoerceStringFromNullToEmpty(ref value);
+ }
+
public string ColumnName
{
get
diff --git a/docker/crupest-api/CrupestApi/CrupestApi.Commons/Crud/ColumnTypeInfo.cs b/docker/crupest-api/CrupestApi/CrupestApi.Commons/Crud/ColumnTypeInfo.cs
index 679cb4c..14960c7 100644
--- a/docker/crupest-api/CrupestApi/CrupestApi.Commons/Crud/ColumnTypeInfo.cs
+++ b/docker/crupest-api/CrupestApi/CrupestApi.Commons/Crud/ColumnTypeInfo.cs
@@ -4,6 +4,7 @@ using System.Text.Json.Serialization;
namespace CrupestApi.Commons.Crud;
+// TODO: Implement this.
public interface IColumnTypeInfo
{
Type ClrType { get; }
@@ -40,6 +41,7 @@ public interface IColumnTypeInfo
}
}
+// TODO: Implement and register this service.
public interface IColumnTypeProvider
{
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 7da6ac7..9402d69 100644
--- a/docker/crupest-api/CrupestApi/CrupestApi.Commons/Crud/CrudService.cs
+++ b/docker/crupest-api/CrupestApi/CrupestApi.Commons/Crud/CrudService.cs
@@ -1,89 +1,39 @@
+using System.Data;
using Dapper;
-using Microsoft.Data.Sqlite;
using Microsoft.Extensions.Options;
namespace CrupestApi.Commons.Crud;
-public class CrudService<TEntity>
+// TODO: Implement and register this service.
+public class CrudService<TEntity> : IDisposable
{
protected readonly TableInfo _table;
+ protected readonly IDbConnection _dbConnection;
protected readonly IOptionsSnapshot<CrupestApiConfig> _crupestApiOptions;
private readonly ILogger<CrudService<TEntity>> _logger;
- public CrudService(ServiceProvider services)
+ public CrudService(ITableInfoFactory tableInfoFactory, IDbConnectionFactory dbConnectionFactory, IOptionsSnapshot<CrupestApiConfig> crupestApiOptions, ILogger<CrudService<TEntity>> logger)
{
- _table = new TableInfo(typeof(TEntity));
- _crupestApiOptions = services.GetRequiredService<IOptionsSnapshot<CrupestApiConfig>>();
- _logger = services.GetRequiredService<ILogger<CrudService<TEntity>>>();
- }
-
- public virtual string GetDbConnectionString()
- {
- var fileName = Path.Combine(_crupestApiOptions.Value.DataDir, "crupest-api.db");
+ _table = tableInfoFactory.Get(typeof(TEntity));
+ _dbConnection = dbConnectionFactory.Get();
+ _crupestApiOptions = crupestApiOptions;
+ _logger = logger;
- return new SqliteConnectionStringBuilder()
+ if (!_table.CheckExistence(_dbConnection))
{
- DataSource = fileName,
- Mode = SqliteOpenMode.ReadWriteCreate
- }.ToString();
- }
-
-
- public async Task<SqliteConnection> CreateDbConnection()
- {
- var connection = new SqliteConnection(GetDbConnectionString());
- await connection.OpenAsync();
- return connection;
- }
-
- public virtual async Task DoInitializeDatabase(SqliteConnection connection)
- {
- await using var transaction = await connection.BeginTransactionAsync();
- await connection.ExecuteAsync(_table.GenerateCreateTableSql(), transaction: transaction);
- await transaction.CommitAsync();
- }
-
- public virtual async Task<SqliteConnection> EnsureDatabase()
- {
- var connection = await CreateDbConnection();
- var exist = await _table.CheckExistence(connection);
- if (!exist)
- {
- await DoInitializeDatabase(connection);
+ DoInitializeDatabase(_dbConnection);
}
- return connection;
- }
-
-
- public virtual async Task<IEnumerable<TEntity>> QueryAsync(WhereClause? where = null, OrderByClause? orderBy = null, int? skip = null, int? limit = null)
- {
- var connection = await EnsureDatabase();
- DynamicParameters parameters;
- var sql = _table.GenerateSelectSql(where, orderBy, skip, limit, out parameters);
- return await connection.QueryAsync<TEntity>(sql, parameters);
- }
-
- public virtual async Task<int> InsertAsync(InsertClause insert)
- {
- var connection = await EnsureDatabase();
- DynamicParameters parameters;
- var sql = _table.GenerateInsertSql(insert, out parameters);
- return await connection.ExecuteAsync(sql, parameters);
}
- public virtual async Task<int> UpdateAsync(WhereClause? where, UpdateClause update)
+ public virtual void DoInitializeDatabase(IDbConnection connection)
{
- var connection = await EnsureDatabase();
- DynamicParameters parameters;
- var sql = _table.GenerateUpdateSql(where, update, out parameters);
- return await connection.ExecuteAsync(sql, parameters);
+ using var transaction = connection.BeginTransaction();
+ connection.Execute(_table.GenerateCreateTableSql(), transaction: transaction);
+ transaction.Commit();
}
- public virtual async Task<int> DeleteAsync(WhereClause? where)
+ public void Dispose()
{
- var connection = await EnsureDatabase();
- DynamicParameters parameters;
- var sql = _table.GenerateDeleteSql(where, out parameters);
- return await connection.ExecuteAsync(sql, parameters);
+ _dbConnection.Dispose();
}
}
diff --git a/docker/crupest-api/CrupestApi/CrupestApi.Commons/Crud/DbConnectionFactory.cs b/docker/crupest-api/CrupestApi/CrupestApi.Commons/Crud/DbConnectionFactory.cs
new file mode 100644
index 0000000..80d1b22
--- /dev/null
+++ b/docker/crupest-api/CrupestApi/CrupestApi.Commons/Crud/DbConnectionFactory.cs
@@ -0,0 +1,9 @@
+using System.Data;
+
+namespace CrupestApi.Commons.Crud;
+
+// TODO: Implement and register this service.
+public interface IDbConnectionFactory
+{
+ IDbConnection Get(string? name = null);
+}
diff --git a/docker/crupest-api/CrupestApi/CrupestApi.Commons/Crud/TableInfo.cs b/docker/crupest-api/CrupestApi/CrupestApi.Commons/Crud/TableInfo.cs
index 103442c..73082c0 100644
--- a/docker/crupest-api/CrupestApi/CrupestApi.Commons/Crud/TableInfo.cs
+++ b/docker/crupest-api/CrupestApi/CrupestApi.Commons/Crud/TableInfo.cs
@@ -1,7 +1,8 @@
using System.Data;
+using System.Diagnostics;
+using System.Reflection;
using System.Text;
using Dapper;
-using Microsoft.Data.Sqlite;
namespace CrupestApi.Commons.Crud;
@@ -29,15 +30,26 @@ public class TableInfo
bool hasPrimaryKey = false;
bool hasId = false;
+ List<PropertyInfo> columnProperties = new();
+ List<PropertyInfo> nonColumnProperties = new();
+
foreach (var property in properties)
{
- var columnInfo = new ColumnInfo(property, _columnTypeProvider);
- columnInfos.Add(columnInfo);
- if (columnInfo.IsPrimaryKey)
- hasPrimaryKey = true;
- if (columnInfo.ColumnName.Equals("id", StringComparison.OrdinalIgnoreCase))
+ if (PropertyIsColumn(property))
{
- hasId = true;
+ var columnInfo = new ColumnInfo(this, property, _columnTypeProvider);
+ columnInfos.Add(columnInfo);
+ if (columnInfo.IsPrimaryKey)
+ hasPrimaryKey = true;
+ if (columnInfo.ColumnName.Equals("id", StringComparison.OrdinalIgnoreCase))
+ {
+ hasId = true;
+ }
+ columnProperties.Add(property);
+ }
+ else
+ {
+ nonColumnProperties.Add(property);
}
}
@@ -49,15 +61,17 @@ public class TableInfo
}
ColumnInfos = columnInfos;
+ ColumnProperties = columnProperties;
+ NonColumnProperties = nonColumnProperties;
CheckValidity();
- _lazyColumnNameList = new Lazy<List<string>>(() => ColumnInfos.Select(c => c.SqlColumnName).ToList());
+ _lazyColumnNameList = new Lazy<List<string>>(() => ColumnInfos.Select(c => c.ColumnName).ToList());
}
private ColumnInfo CreateAutoIdColumn()
{
- return new ColumnInfo(EntityType,
+ return new ColumnInfo(this,
new ColumnAttribute
{
ColumnName = "Id",
@@ -71,8 +85,29 @@ public class TableInfo
public Type EntityType { get; }
public string TableName { get; }
public IReadOnlyList<ColumnInfo> ColumnInfos { get; }
+ public IReadOnlyList<PropertyInfo> ColumnProperties { get; }
+ public IReadOnlyList<PropertyInfo> NonColumnProperties { get; }
public IReadOnlyList<string> ColumnNameList => _lazyColumnNameList.Value;
+ protected bool PropertyIsColumn(PropertyInfo property)
+ {
+ var columnAttribute = property.GetCustomAttribute<ColumnAttribute>();
+ if (columnAttribute is null) return false;
+ return true;
+ }
+
+ public ColumnInfo GetColumn(string columnName)
+ {
+ foreach (var column in ColumnInfos)
+ {
+ if (column.ColumnName.Equals(columnName, StringComparison.OrdinalIgnoreCase))
+ {
+ return column;
+ }
+ }
+ throw new KeyNotFoundException("No such column with given name.");
+ }
+
public void CheckValidity()
{
// Check if there is only one primary key.
@@ -152,6 +187,17 @@ CREATE TABLE {tableName}(
}
}
+ public void CheckColumnName(string columnName)
+ {
+ foreach (var c in columnName)
+ {
+ if (char.IsWhiteSpace(c))
+ {
+ throw new Exception("White space found in column name, which might be an sql injection attack!");
+ }
+ }
+ }
+
public void CheckRelatedColumns(IClause? clause)
{
if (clause is not null)
@@ -159,6 +205,7 @@ CREATE TABLE {tableName}(
var relatedColumns = clause.GetRelatedColumns();
foreach (var column in relatedColumns)
{
+ CheckColumnName(column);
if (!ColumnNameList.Contains(column))
{
throw new ArgumentException($"Column {column} is not in the table.");
@@ -268,62 +315,137 @@ CREATE TABLE {tableName}(
return (result.ToString(), parameters);
}
- // TODO: Continue...
- public string GenerateUpdateSql(IWhereClause? whereClause, UpdateClause updateClause)
+ public (string sql, DynamicParameters parameters) GenerateUpdateSql(IWhereClause? whereClause, IUpdateClause updateClause)
{
- var relatedColumns = new HashSet<string>();
- if (whereClause is not null)
- relatedColumns.UnionWith(((IClause)whereClause).GetRelatedColumns() ?? Enumerable.Empty<string>());
- relatedColumns.UnionWith(updateClause.GetRelatedColumns());
- foreach (var column in relatedColumns)
- {
- if (!ColumnNameList.Contains(column))
- {
- throw new ArgumentException($"Column {column} is not in the table.");
- }
- }
+ CheckRelatedColumns(whereClause);
+ CheckRelatedColumns(updateClause);
- parameters = new DynamicParameters();
+ var parameters = new DynamicParameters();
StringBuilder sb = new StringBuilder("UPDATE ");
sb.Append(TableName);
sb.Append(" SET ");
- sb.Append(updateClause.GenerateSql(parameters));
+ var (updateSql, updateParameters) = updateClause.GenerateSql();
+ sb.Append(updateSql);
+ parameters.AddDynamicParams(updateParameters);
if (whereClause is not null)
{
sb.Append(" WHERE ");
- sb.Append(whereClause.GenerateSql(parameters));
+ var (whereSql, whereParameters) = whereClause.GenerateSql();
+ sb.Append(whereSql);
+ parameters.AddDynamicParams(whereParameters);
}
sb.Append(';');
- return sb.ToString();
+ return (sb.ToString(), parameters);
}
- public string GenerateDeleteSql(WhereClause? whereClause, out DynamicParameters parameters)
+ public (string sql, DynamicParameters parameters) GenerateDeleteSql(IWhereClause? whereClause)
{
+ CheckRelatedColumns(whereClause);
+
+ var parameters = new DynamicParameters();
+
+ StringBuilder sb = new StringBuilder("DELETE FROM ");
+ sb.Append(TableName);
if (whereClause is not null)
{
- var relatedColumns = ((IClause)whereClause).GetRelatedColumns() ?? new List<string>();
- foreach (var column in relatedColumns)
+ sb.Append(" WHERE ");
+ var (whereSql, whereParameters) = whereClause.GenerateSql();
+ parameters.AddDynamicParams(whereParameters);
+ sb.Append(whereSql);
+ }
+ sb.Append(';');
+
+ return (sb.ToString(), parameters);
+ }
+
+ private object? ClearNonColumnProperties(object? entity)
+ {
+ Debug.Assert(entity is null || entity.GetType() == EntityType);
+ if (entity is null) return entity;
+ foreach (var property in NonColumnProperties)
+ {
+ // Clear any non-column properties.
+ property.SetValue(entity, Activator.CreateInstance(property.PropertyType));
+ }
+ return entity;
+ }
+
+ private object? CallColumnHook(object? entity, string hookName)
+ {
+ Debug.Assert(entity is null || entity.GetType() == EntityType);
+ if (entity is null) return entity;
+ foreach (var column in ColumnInfos)
+ {
+ var property = column.PropertyInfo;
+ if (property is not null)
{
- if (!ColumnNameList.Contains(column))
+ var value = property.GetValue(entity);
+
+ switch (hookName)
{
- throw new ArgumentException($"Column {column} is not in the table.");
- }
+ case "AfterGet":
+ column.Hooks.AfterGet(column, ref value);
+ break;
+ case "BeforeSet":
+ column.Hooks.BeforeSet(column, ref value);
+ break;
+ default:
+ throw new Exception("Unknown hook.");
+ };
+
+ property.SetValue(entity, value);
}
+
+ }
+ return entity;
+ }
+
+ public virtual IEnumerable<object?> Select(IDbConnection dbConnection, IWhereClause? where = null, IOrderByClause? orderBy = null, int? skip = null, int? limit = null)
+ {
+ var (sql, parameters) = GenerateSelectSql(where, orderBy, skip, limit);
+ return dbConnection.Query(EntityType, sql, parameters).Select(e => CallColumnHook(ClearNonColumnProperties(e), "AfterGet"));
+ }
+
+ public virtual int Insert(IDbConnection dbConnection, IInsertClause insert)
+ {
+ var (sql, parameters) = GenerateInsertSql(insert);
+
+ foreach (var item in insert.Items)
+ {
+ var column = GetColumn(item.ColumnName);
+ var value = item.Value;
+ column.Hooks.BeforeSet?.Invoke(column, ref value);
+ item.Value = value;
}
- parameters = new DynamicParameters();
+ return dbConnection.Execute(sql, parameters);
+ }
- StringBuilder sb = new StringBuilder("DELETE FROM ");
- sb.Append(TableName);
- if (whereClause is not null)
+ public virtual int Update(IDbConnection dbConnection, IWhereClause? where, IUpdateClause update)
+ {
+ var (sql, parameters) = GenerateUpdateSql(where, update);
+
+ foreach (var item in update.Items)
{
- sb.Append(" WHERE ");
- sb.Append(whereClause.GenerateSql(parameters));
+ var column = GetColumn(item.ColumnName);
+ var value = item.Value;
+ column.Hooks.BeforeSet?.Invoke(column, ref value);
+ item.Value = value;
}
- sb.Append(';');
+ return dbConnection.Execute(sql, parameters);
+ }
- return sb.ToString();
+ public virtual int Delete(IDbConnection dbConnection, IWhereClause? where)
+ {
+ var (sql, parameters) = GenerateDeleteSql(where);
+ return dbConnection.Execute(sql, parameters);
}
}
+
+// TODO: Implement and register this service.
+public interface ITableInfoFactory
+{
+ TableInfo Get(Type type);
+}
diff --git a/docker/crupest-api/CrupestApi/CrupestApi.Commons/Crud/UpdateClause.cs b/docker/crupest-api/CrupestApi/CrupestApi.Commons/Crud/UpdateClause.cs
index 7cb5edf..b9cafee 100644
--- a/docker/crupest-api/CrupestApi/CrupestApi.Commons/Crud/UpdateClause.cs
+++ b/docker/crupest-api/CrupestApi/CrupestApi.Commons/Crud/UpdateClause.cs
@@ -15,8 +15,13 @@ public class UpdateItem
public object? Value { get; set; }
}
-// TODO: Continue...
-public class UpdateClause
+public interface IUpdateClause : IClause
+{
+ List<UpdateItem> Items { get; }
+ (string sql, DynamicParameters parameters) GenerateSql();
+}
+
+public class UpdateClause : IUpdateClause
{
public List<UpdateItem> Items { get; } = new List<UpdateItem>();
@@ -51,8 +56,10 @@ public class UpdateClause
return Items.Select(i => i.ColumnName).ToList();
}
- public string GenerateSql(DynamicParameters parameters)
+ public (string sql, DynamicParameters parameters) GenerateSql()
{
+ var parameters = new DynamicParameters();
+
StringBuilder result = new StringBuilder();
foreach (var item in Items)
@@ -66,6 +73,6 @@ public class UpdateClause
result.Append($"{item.ColumnName} = @{parameterName}");
}
- return result.ToString();
+ return (result.ToString(), parameters);
}
}