From 62426bb3a34d8e9b363726b8b563e10c1362aab8 Mon Sep 17 00:00:00 2001 From: Elivo Date: Sun, 12 Dec 2021 09:36:35 +0800 Subject: [PATCH] Apewer-6.6.0 --- Apewer.Source/Source/Access.cs | 503 +++++++----- Apewer.Source/Source/DbClient.cs | 449 +++++++---- Apewer.Source/Source/MySql.cs | 502 ++---------- Apewer.Source/Source/SqlClient.cs | 730 +++++------------- Apewer.Source/Source/SqlClientThin.cs | 528 ------------- Apewer.Source/Source/Sqlite.cs | 657 ++++------------ Apewer.Web/AspNetBridge/ApiController.cs | 67 ++ Apewer.Web/AspNetBridge/Attributes.cs | 83 ++ Apewer.Web/AspNetBridge/BridgeController.cs | 211 +++++ .../ContentDispositionHeaderValue.cs | 20 + Apewer.Web/AspNetBridge/HttpActionResult.cs | 39 + Apewer.Web/AspNetBridge/HttpContent.cs | 26 + Apewer.Web/AspNetBridge/HttpContentHeaders.cs | 23 + .../AspNetBridge/HttpResponseMessage.cs | 49 ++ Apewer.Web/AspNetBridge/JsonResult.cs | 23 + .../AspNetBridge/MediaTypeHeaderValue.cs | 20 + Apewer.Web/AspNetBridge/RouteItem.cs | 196 +++++ Apewer.Web/AspNetBridge/StreamContent.cs | 18 + Apewer.Web/AspNetBridge/_Delegates.cs | 12 + Apewer.Web/Internals/ApiHelper.cs | 139 +++- Apewer.Web/Web/ApiProcessor.cs | 4 +- Apewer.Web/Web/ApiProgram.cs | 16 +- Apewer.Web/Web/AspNetCoreProvider.cs | 2 +- Apewer.Web/Web/HttpListenerProvider.cs | 2 +- Apewer.Web/Web/WebsiteProvider.cs | 10 +- Apewer.Web/WebConfig40.xml | 3 + Apewer.Web/WebConfig461.xml | 3 + Apewer.Web/WebConfigStd.xml | 3 + Apewer/Apewer.props | 3 +- Apewer/Class.cs | 201 ++--- Apewer/Json.cs | 277 +++---- Apewer/NetworkUtility.cs | 121 +++ Apewer/NumberUtility.cs | 2 +- Apewer/ObjectSet.cs | 35 +- Apewer/RuntimeUtility.cs | 2 +- Apewer/Source/IDbAdo.cs | 43 +- Apewer/Source/IDbOrm.cs | 36 +- Apewer/Source/SourceUtility.cs | 62 ++ Apewer/Web/ApiModel.cs | 221 ++++-- Apewer/Web/ApiOptions.cs | 4 +- Apewer/Web/ApiProvider.cs | 5 +- Apewer/Web/ApiRequest.cs | 3 + Apewer/Web/ApiUtility.cs | 3 +- Apewer/Web/StaticController.cs | 123 +-- Apewer/_Delegates.cs | 21 + Apewer/_Extensions.cs | 40 +- ChangeLog.md | 3 + 47 files changed, 2695 insertions(+), 2848 deletions(-) delete mode 100644 Apewer.Source/Source/SqlClientThin.cs create mode 100644 Apewer.Web/AspNetBridge/ApiController.cs create mode 100644 Apewer.Web/AspNetBridge/Attributes.cs create mode 100644 Apewer.Web/AspNetBridge/BridgeController.cs create mode 100644 Apewer.Web/AspNetBridge/ContentDispositionHeaderValue.cs create mode 100644 Apewer.Web/AspNetBridge/HttpActionResult.cs create mode 100644 Apewer.Web/AspNetBridge/HttpContent.cs create mode 100644 Apewer.Web/AspNetBridge/HttpContentHeaders.cs create mode 100644 Apewer.Web/AspNetBridge/HttpResponseMessage.cs create mode 100644 Apewer.Web/AspNetBridge/JsonResult.cs create mode 100644 Apewer.Web/AspNetBridge/MediaTypeHeaderValue.cs create mode 100644 Apewer.Web/AspNetBridge/RouteItem.cs create mode 100644 Apewer.Web/AspNetBridge/StreamContent.cs create mode 100644 Apewer.Web/AspNetBridge/_Delegates.cs diff --git a/Apewer.Source/Source/Access.cs b/Apewer.Source/Source/Access.cs index 8857cd5..0eb8674 100644 --- a/Apewer.Source/Source/Access.cs +++ b/Apewer.Source/Source/Access.cs @@ -7,6 +7,7 @@ using System.Data; using System.Data.OleDb; using System.IO; using System.Text; +using static Apewer.Source.SourceUtility; #endif namespace Apewer.Source @@ -24,261 +25,335 @@ namespace Apewer.Source #if NETFRAMEWORK - public partial class Access : IDbAdo + public partial class Access : DbClient { #region 连接 string _connstr = null; - OleDbConnection _connection = null; - Timeout _timeout = null; - - /// 获取或设置日志记录。 - public Logger Logger { get; set; } - - /// 获取或设置超时。 - public Timeout Timeout { get => _timeout; } /// 构造函数。 - public Access(string connectrionString, Timeout timeout) + public Access(string connectrionString, Timeout timeout = null) : base(timeout) { _connstr = connectrionString; - _timeout = timeout ?? Timeout.Default; } - #endregion - - #region 连接 - - /// 获取当前的 OldDbConnection 对象。 - public IDbConnection Connection { get => _connection; } + OleDbConnection OleDbConnection { get => (OleDbConnection)Connection; } - /// 数据库是否已经连接。 - public bool Online - { - get - { - if (_connection != null) - { - if (_connection.State == ConnectionState.Open) return true; - } - return false; - } - } + #endregion - /// 连接数据库,若未连接则尝试连接。 - /// 错误信息。 - public string Connect() - { - if (_connection == null) - { - _connection = new OleDbConnection(); - _connection.ConnectionString = _connstr; - } - else - { - if (_connection.State == ConnectionState.Open) return null; - } - try - { - _connection.Open(); - if (_connection.State == ConnectionState.Open) return null; - } - catch (Exception ex) - { - Logger.Error(nameof(Access), "Connect", ex, _connstr); - Close(); - return ex.Message; - } - return "连接失败。"; - } + #region public - /// 释放对象所占用的系统资源。 - public void Close() + /// + public override string[] ColumnNames(string tableName) { - if (_connection != null) + // OleDbConnection.GetOleDbSchemaTable(OleDbSchemaGuid.Tables, new object[] { null, null, null, "TABLE" }); + var table = OleDbConnection.GetOleDbSchemaTable(OleDbSchemaGuid.Columns, new object[] { null, null, tableName.ToString(), null }); + using (var query = new Query(table)) { - if (_transaction != null) + var names = new ArrayBuilder(); + for (var i = 0; i < query.Rows; i++) { - if (_autocommit) Commit(); - else Rollback(); + names.Add(query.Text(i, "COLUMN_NAME")); } - _connection.Close(); - _connection.Dispose(); - _connection = null; + return names.Export(); } } - /// 释放资源。 - public void Dispose() => Close(); + /// + public override string[] StoreNames() => throw new InvalidOperationException(); - #endregion - - #region Transaction + /// + public override string[] TableNames() => TextColumn("select name from msysobjects where type=1 and flags = 0"); - private IDbTransaction _transaction = null; - private bool _autocommit = false; + /// + public override string Insert(object record, string table = null) + { + if (record == null) return "参数无效。"; + FixProperties(record); - /// 启动事务。 - public string Begin(bool commit = true) => Begin(commit, null); + var structure = TableStructure.Parse(record.GetType()); + if (structure == null) return "无法解析记录模型。"; + if (string.IsNullOrEmpty(table)) table = structure.Name; + if (string.IsNullOrEmpty(table)) return "表名称无效。"; - /// 启动事务。 - public string Begin(bool commit, Class isolation) - { - var connect = Connect(); - if (connect.NotEmpty()) return connect; - if (_transaction != null) return "存在已启动的事务,无法再次启动。"; - try - { - _transaction = isolation ? _connection.BeginTransaction(isolation.Value) : _connection.BeginTransaction(); - _autocommit = commit; - return null; - } - catch (Exception ex) + // 排除字段。 + var excluded = new List(); + foreach (var ca in structure.Columns) { - Logger.Error(nameof(Access), "Begin", ex.Message()); - return ex.Message(); + // 排除不使用 ORM 的属性。 + if (ca.Independent || ca.Incremental) excluded.Add(ca.Field); } - } - /// 提交事务。 - public string Commit() - { - if (_transaction == null) return "事务不存在。"; - try - { - _transaction.Commit(); - RuntimeUtility.Dispose(_transaction); - _transaction = null; - return null; - } - catch (Exception ex) + var ps = structure.CreateParameters(record, Parameter, excluded); + var psc = ps.Length; + if (psc < 1) return "数据模型不包含字段。"; + + var names = new List(psc); + var values = new List(psc); + foreach (var column in ps) { - RuntimeUtility.Dispose(_transaction); - _transaction = null; - Logger.Error(nameof(Access), "Commit", ex.Message()); - return ex.Message(); + //names.Add(TextGenerator.Merge("[", column, "]")); + names.Add(TextUtility.Merge(column)); + values.Add("@" + column); } + var sb = new StringBuilder(); + sb.Append("insert into [", table, "](", string.Join(", ", names.ToArray()), ") "); + sb.Append("values(", string.Join(", ", values.ToArray()), "); "); + var sql = sb.ToString(); + + var execute = Execute(sql, ps); + if (execute.Success) return TextUtility.Empty; + return execute.Message; } - /// 从挂起状态回滚事务。 - public string Rollback() + /// + public override string Update(IRecord record, string table = null) { - if (_transaction == null) return "事务不存在。"; - try + if (record == null) return "参数无效。"; + FixProperties(record); + SetUpdated(record); + + var structure = TableStructure.Parse(record.GetType()); + if (structure == null) return "无法解析记录模型。"; + if (structure.Independent) return "无法更新带有 Independent 特性的模型。"; + if (string.IsNullOrEmpty(table)) table = structure.Name; + if (string.IsNullOrEmpty(table)) return "表名称无效。"; + + // 排除字段。 + var excluded = new List(); + if (structure.Key != null) excluded.Add(structure.Key.Field); + foreach (var ca in structure.Columns) { - _transaction.Rollback(); - RuntimeUtility.Dispose(_transaction); - _transaction = null; - return null; + // 排除不使用 ORM 的属性、自增属性和主键属性。 + if (ca.Independent || ca.Incremental || ca.PrimaryKey) excluded.Add(ca.Field); } - catch (Exception ex) + + var ps = structure.CreateParameters(record, Parameter, excluded); + var psc = ps.Length; + if (psc < 1) return "数据模型不包含字段。"; + + var items = new List(); + foreach (var p in ps) { - RuntimeUtility.Dispose(_transaction); - _transaction = null; - Logger.Error(nameof(Access), "Rollback", ex.Message); - return ex.Message(); + var pn = p.ParameterName; + items.Add(TextUtility.Merge("[", pn, "] = @", pn)); } + var key = record.Key.SafeKey(); + var sql = $"update [{table}] set {string.Join(", ", items.ToArray())} where [{structure.Key.Field}]='{key}'"; + + var execute = Execute(sql, ps); + if (execute.Success) return TextUtility.Empty; + return execute.Message; } #endregion - #region 查询和执行 + #region protected - /// 使用 SQL 语句进行查询。 - public IQuery Query(string sql) => Query(sql, null); + /// + protected override string NewConnectionString() => _connstr; - /// 使用 SQL 语句进行查询。 - public IQuery Query(string sql, IEnumerable parameters) - { - if (sql.IsBlank()) return Example.InvalidQueryStatement; + /// + protected override IDbConnection CreateConnection() => new OleDbConnection(); - var connected = Connect(); - if (connected.NotEmpty()) return new Query(false, connected); + /// + protected override IDataAdapter CreateDataAdapter(IDbCommand command) => new OleDbDataAdapter((OleDbCommand)command); - try + /// + protected override IDataParameter CreateParameter() => new OleDbParameter(); + + /// + protected override string Initialize(TableStructure structure) + { + var model = structure.Model; + + // 检查现存表。 + var exists = false; + var tables = TableNames(); + if (tables.Length > 0) { - using (var command = new OleDbCommand()) + var lower = structure.Name.ToLower(); + foreach (var table in tables) { - command.Connection = _connection; - command.CommandTimeout = Timeout.Query; - command.CommandText = sql; - if (parameters != null) + if (TextUtility.IsBlank(table)) continue; + if (table.ToLower() == lower) { - foreach (var p in parameters) - { - if (p != null) command.Parameters.Add(p); - } - } - using (var ds = new DataSet()) - { - using (var da = new OleDbDataAdapter(sql, _connection)) - { - var name = "table_" + Guid.NewGuid().ToString("n"); - da.Fill(ds, name); - var table = ds.Tables[name]; - return new Query(table); - } + exists = true; + break; } } } - catch (Exception exception) + + if (exists) { - Logger.Error(nameof(Access), "Query", exception, sql); - return new Query(exception); - } - } + // 获取已存在的列名。 + var columns = ColumnNames(structure.Name); + if (columns.Length > 0) + { + var lower = new List(); + foreach (var column in columns) + { + if (TextUtility.IsBlank(column)) continue; + lower.Add(column.ToLower()); + } + columns = lower.ToArray(); + } - /// 执行 SQL 语句。 - public IExecute Execute(string sql) => Execute(sql, null); + // 增加列。 + foreach (var column in structure.Columns) + { + // 检查 Independent 特性。 + if (structure.Independent && column.Independent) continue; - /// 执行 SQL 语句,并加入参数。 - public IExecute Execute(string sql, IEnumerable parameters) - { - if (sql.IsBlank()) return Example.InvalidExecuteStatement; + // 去重。 + var lower = column.Field.ToLower(); + if (columns.Contains(lower)) continue; - var connected = Connect(); - if (connected.NotEmpty()) return new Execute(false, connected); + var type = Declaration(column); + if (string.IsNullOrEmpty(type)) return TextUtility.Merge("类型 ", column.Type.ToString(), " 不受支持。"); - var inTransaction = _transaction != null; - if (!inTransaction) Begin(); - try + var sql = TextUtility.Merge("alter table [", structure.Name, "] add ", type, "; "); + var execute = Execute(sql); + if (execute.Success == false) return execute.Message; + } + return TextUtility.Empty; + } + else { - using (var command = new OleDbCommand()) + var sqlcolumns = new List(); + foreach (var column in structure.Columns) { - command.Connection = _connection; - command.Transaction = (OleDbTransaction)_transaction; - command.CommandTimeout = Timeout.Execute; - command.CommandText = sql; - if (parameters != null) + // 检查 Independent 特性。 + if (structure.Independent && column.Independent) continue; + + var type = Declaration(column); + if (!column.Independent) { - foreach (var parameter in parameters) - { - if (parameter == null) continue; - command.Parameters.Add(parameter); - } + if (column.PrimaryKey) type = type + " primary key"; + if (column.Incremental) type += " identity"; } - var rows = command.ExecuteNonQuery(); - if (!inTransaction) Commit(); // todo 此处应该检查事务提交产生的错误。 - return new Execute(true, rows); + + if (string.IsNullOrEmpty(type)) return TextUtility.Merge("类型 ", column.Type.ToString(), " 不受支持。"); + sqlcolumns.Add(type); } + if (sqlcolumns.Count < 1) return $"无法对类型 {model.FullName} 创建表:没有定义任何字段。"; + var sql = TextUtility.Merge("create table [", structure.Name, "](", string.Join(", ", sqlcolumns.ToArray()), "); "); + var execute = Execute(sql); + if (execute.Success) return TextUtility.Empty; + return execute.Message; } - catch (Exception exception) - { - Logger.Error(nameof(Access), "Execute", exception, sql); - if (!inTransaction) Rollback(); - return new Execute(exception); - } + + } + + /// + protected override string KeysSql(string tableName, string keyField, string flagField, long flagValue) + { + if (flagValue == 0) return $"select [{keyField}] from [{tableName}]"; + else return $"select [{keyField}] from [{tableName}] where [{flagField}] = {flagValue}"; + } + + /// + protected override string RecordSql(string tableName, string keyField, string keyValue, string flagField, long flagValue) + { + if (flagValue == 0) return $"select top 1 * from [{tableName}] where [{keyField}] = '{keyValue}'"; + else return $"select top 1 * from [{tableName}] where [{keyField}] = '{keyValue}' and [{flagField}] = {flagValue}"; + } + + /// + protected override string RecordsSql(string tableName, string flagField, long flagValue) + { + if (flagValue == 0) return $"select * from [{tableName}]"; + else return $"select * from [{tableName}] where [{flagField}] = {flagValue}"; } #endregion #region Parameter - /// 创建参数列表。 - public static List NewParameterList() + /// 创建参数。 + /// + /// + static OleDbParameter Parameter(Parameter parameter) + { + if (parameter == null) throw new InvalidOperationException("参数无效。"); + return Parameter(parameter.Name, parameter.Type, parameter.Size, parameter.Value); + } + + /// 创建参数。 + public static OleDbParameter Parameter(string name, ColumnType type, int size, object value) { - return new List(); + var vname = TextUtility.Trim(name); + if (TextUtility.IsBlank(vname)) return null; + + var vtype = OleDbType.BigInt; + switch (type) + { + case ColumnType.Boolean: + vtype = OleDbType.Boolean; + break; + case ColumnType.Bytes: + vtype = OleDbType.LongVarBinary; + break; + case ColumnType.Integer: + vtype = OleDbType.Integer; + break; + case ColumnType.Float: + vtype = OleDbType.Double; + break; + case ColumnType.DateTime: + vtype = OleDbType.Date; + break; + case ColumnType.VarChar: + case ColumnType.VarChar191: + case ColumnType.VarCharMax: + vtype = OleDbType.VarChar; + break; + case ColumnType.NVarChar: + case ColumnType.NVarChar191: + case ColumnType.NVarCharMax: + vtype = OleDbType.VarWChar; + break; + case ColumnType.Text: + vtype = OleDbType.LongVarChar; + break; + case ColumnType.NText: + vtype = OleDbType.LongVarWChar; + break; + default: + throw new InvalidOperationException(TextUtility.Merge("类型 ", type.ToString(), " 不受支持。")); + } + + var vsize = size; + switch (type) + { + case ColumnType.VarChar: + vsize = NumberUtility.Restrict(vsize, 0, 8000); + break; + case ColumnType.NVarChar: + vsize = NumberUtility.Restrict(vsize, 0, 4000); + break; + case ColumnType.VarChar191: + case ColumnType.NVarChar191: + vsize = NumberUtility.Restrict(vsize, 0, 191); + break; + default: + vsize = 0; + break; + } + + var vvalue = value; + if (vvalue is string && vvalue != null && vsize > 0) + { + vvalue = TextUtility.Left((string)vvalue, vsize); + } + + var parameter = new OleDbParameter(); + parameter.ParameterName = vname; + parameter.OleDbType = vtype; + parameter.Value = vvalue; + if (vsize > 0) parameter.Size = vsize; + return parameter; } /// 创建参数。 @@ -328,6 +403,62 @@ namespace Apewer.Source #endregion + #region static + + static string Declaration(ColumnAttribute column) + { + var type = TextUtility.Empty; + var vcolumn = column; + var length = Math.Max(0, vcolumn.Length); + switch (vcolumn.Type) + { + case ColumnType.Boolean: + type = "bit"; + break; + case ColumnType.Integer: + type = "money"; + break; + case ColumnType.Float: + type = "float"; + break; + case ColumnType.Bytes: + type = "binary"; + break; + case ColumnType.DateTime: + type = "datetime"; + break; + case ColumnType.VarChar: + type = TextUtility.Merge("varchar(", Math.Min(8000, length).ToString(), ")"); + break; + case ColumnType.VarChar191: + type = TextUtility.Merge("varchar(191)"); + break; + case ColumnType.VarCharMax: + type = TextUtility.Merge("varchar(max)"); + break; + case ColumnType.Text: + type = TextUtility.Merge("text"); + break; + case ColumnType.NVarChar: + type = TextUtility.Merge("nvarchar(", Math.Min(4000, length).ToString(), ")"); + break; + case ColumnType.NVarChar191: + type = TextUtility.Merge("nvarchar(191)"); + break; + case ColumnType.NVarCharMax: + type = TextUtility.Merge("nvarchar(max)"); + break; + case ColumnType.NText: + type = TextUtility.Merge("ntext"); + break; + default: + return TextUtility.Empty; + } + return TextUtility.Merge("[", vcolumn.Field, "] ", type); + } + + #endregion + } /// 使用 Microsoft.Jet.OLEDB.4.0 访问 Access 97 - 2003 数据库文件。 diff --git a/Apewer.Source/Source/DbClient.cs b/Apewer.Source/Source/DbClient.cs index e54d61f..ea0611a 100644 --- a/Apewer.Source/Source/DbClient.cs +++ b/Apewer.Source/Source/DbClient.cs @@ -1,17 +1,14 @@ -#if DEBUG - -using System; +using System; using System.Collections.Generic; using System.Data; using System.Data.Common; +using System.Reflection; using System.Text; namespace Apewer.Source { /// 数据库客户端基类。 - - public abstract class DbClient : IDbClient { @@ -19,7 +16,10 @@ namespace Apewer.Source public virtual Logger Logger { get; set; } /// - public DbClient(Timeout timeout) { _timeout = timeout ?? Timeout.Default; } + internal DbClient(Timeout timeout) + { + _timeout = timeout ?? Timeout.Default; + } #region connection @@ -27,6 +27,9 @@ namespace Apewer.Source IDbConnection _conn = null; string _str = null; + /// + internal protected object Locker = new object(); + /// public Timeout Timeout { get => _timeout; set => _timeout = value ?? Timeout.Default; } @@ -44,15 +47,11 @@ namespace Apewer.Source { if (_conn == null) { - _str = NewConnectionString(); - _conn = NewConnection(); - _conn.ConnectionString = _str; - } - else - { - if (_conn.State == ConnectionState.Open) return null; + _conn = CreateConnection(); + _conn.ConnectionString = NewConnectionString(); } + if (_conn.State == ConnectionState.Open) return null; try { _conn.Open(); @@ -63,12 +62,32 @@ namespace Apewer.Source } catch (Exception ex) { - Logger.Error(this, "Connect", ex.GetType().Name, ex.Message, _str); + Logger.Error(this, "Connect", ex.Message, _str); Close(); return ex.Message; } } + /// 更改已打开的数据库。 + public string Change(string store) + { + if (store.IsEmpty()) return "未指定数据名称。"; + + var connect = Connect(); + if (connect.NotEmpty()) return connect; + + try + { + Connection.ChangeDatabase(store); + return null; + } + catch (Exception ex) + { + Logger.Error(this, "Change", ex.Message, _str); + return ex.Message(); + } + } + /// 关闭连接,并释放对象所占用的系统资源。 public void Close() { @@ -78,15 +97,18 @@ namespace Apewer.Source { if (_autocommit) Commit(); else Rollback(); + _transaction = null; } - _conn.Close(); - _conn.Dispose(); - _conn = null; + try { _conn.Close(); } catch { } } } /// 关闭连接,释放对象所占用的系统资源,并清除连接信息。 - public void Dispose() { Close(); } + public void Dispose() + { + Close(); + RuntimeUtility.Dispose(_conn); + } #endregion @@ -95,19 +117,10 @@ namespace Apewer.Source private IDbTransaction _transaction = null; private bool _autocommit = false; - /// - /// 启动事务,可指定事务锁定行为。 - /// Chaos
无法覆盖隔离级别更高的事务中的挂起的更改。
- /// ReadCommitted
在正在读取数据时保持共享锁,以避免脏读,但是在事务结束之前可以更改数据,从而导致不可重复的读取或幻像数据。
- /// ReadUncommitted
可以进行脏读,意思是说,不发布共享锁,也不接受独占锁。
- /// RepeatableRead
在查询中使用的所有数据上放置锁,以防止其他用户更新这些数据。 防止不可重复的读取,但是仍可以有幻像行。
- /// Serializable
在 System.Data.DataSet 上放置范围锁,以防止在事务完成之前由其他用户更新行或向数据集中插入行。
- /// Snapshot
通过在一个应用程序正在修改数据时存储另一个应用程序可以读取的相同数据版本来减少阻止。 表示您无法从一个事务中看到在其他事务中进行的更改,即便重新查询也是如此。
- /// Unspecified = -1
正在使用与指定隔离级别不同的隔离级别,但是无法确定该级别。
- ///
- /// 在连接的生命周期结束时未结束事务,指定 TRUE 将自动提交,指定 FALSE 将自动回滚。 - /// 指定事务锁定行为,不指定时将使用默认值。 - public string Begin(bool commit = false, Class isolation = null) + /// 获取已启动的事务。 + public IDbTransaction Transaction { get => _transaction; } + + string Begin(bool commit, Class isolation) { if (_transaction != null) return "已存在未完成的事务,无法再次启动。"; var connect = Connect(); @@ -125,6 +138,33 @@ namespace Apewer.Source } } + /// + /// 使用默认的事务锁定行为启动事务。 + /// Chaos
无法覆盖隔离级别更高的事务中的挂起的更改。
+ /// ReadCommitted
在正在读取数据时保持共享锁,以避免脏读,但是在事务结束之前可以更改数据,从而导致不可重复的读取或幻像数据。
+ /// ReadUncommitted
可以进行脏读,意思是说,不发布共享锁,也不接受独占锁。
+ /// RepeatableRead
在查询中使用的所有数据上放置锁,以防止其他用户更新这些数据。 防止不可重复的读取,但是仍可以有幻像行。
+ /// Serializable
在 System.Data.DataSet 上放置范围锁,以防止在事务完成之前由其他用户更新行或向数据集中插入行。
+ /// Snapshot
通过在一个应用程序正在修改数据时存储另一个应用程序可以读取的相同数据版本来减少阻止。 表示您无法从一个事务中看到在其他事务中进行的更改,即便重新查询也是如此。
+ /// Unspecified = -1
正在使用与指定隔离级别不同的隔离级别,但是无法确定该级别。
+ ///
+ /// 在连接的生命周期结束时未结束事务,指定 TRUE 将自动提交,指定 FALSE 将自动回滚。 + public string Begin(bool commit = false) => Begin(commit, null); + + /// + /// 使用指定的事务锁定行为启动事务。 + /// Chaos
无法覆盖隔离级别更高的事务中的挂起的更改。
+ /// ReadCommitted
在正在读取数据时保持共享锁,以避免脏读,但是在事务结束之前可以更改数据,从而导致不可重复的读取或幻像数据。
+ /// ReadUncommitted
可以进行脏读,意思是说,不发布共享锁,也不接受独占锁。
+ /// RepeatableRead
在查询中使用的所有数据上放置锁,以防止其他用户更新这些数据。 防止不可重复的读取,但是仍可以有幻像行。
+ /// Serializable
在 System.Data.DataSet 上放置范围锁,以防止在事务完成之前由其他用户更新行或向数据集中插入行。
+ /// Snapshot
通过在一个应用程序正在修改数据时存储另一个应用程序可以读取的相同数据版本来减少阻止。 表示您无法从一个事务中看到在其他事务中进行的更改,即便重新查询也是如此。
+ /// Unspecified = -1
正在使用与指定隔离级别不同的隔离级别,但是无法确定该级别。
+ ///
+ /// 在连接的生命周期结束时未结束事务,指定 TRUE 将自动提交,指定 FALSE 将自动回滚。 + /// 指定事务锁定行为,不指定时将使用默认值。 + public string Begin(bool commit, IsolationLevel isolation) => Begin(commit, new Class(isolation)); + /// 提交事务。 public string Commit() { @@ -170,10 +210,7 @@ namespace Apewer.Source #region ado /// 查询。 - public IQuery Query(string sql) => Query(sql, null); - - /// 查询。 - public IQuery Query(string sql, IEnumerable parameters) + public IQuery Query(string sql, IEnumerable parameters = null) { if (TextUtility.IsEmpty(sql)) return new Query(false, "语句无效。"); @@ -182,7 +219,7 @@ namespace Apewer.Source try { - using (var command = NewCommand()) + using (var command = _conn.CreateCommand()) { command.Connection = _conn; if (_timeout != null) command.CommandTimeout = Timeout.Query; @@ -194,82 +231,81 @@ namespace Apewer.Source if (parameter != null) command.Parameters.Add(parameter); } } - var ex = null as Exception; var da = null as IDataAdapter; - try { da = NewDataAdapter(command); } + try { da = CreateDataAdapter(command); } catch (Exception adapterEx) { ex = adapterEx; } - if (ex == null) + if (ex != null) { - using (var ds = new DataSet()) - { - da.Fill(ds); - if (ds.Tables.Count > 0) - { - var tables = new DataTable[ds.Tables.Count]; - ds.Tables.CopyTo(tables, 0); - return new Query(tables, true); - } - else - { - Logger.Error(this, "Query", "查询结果不包含任何数据表。", sql); - return new Query(false, "查询结果不包含任何数据表。"); - } - } + Logger.Error(this, "Query", ex.Message, sql); + return new Query(ex); } - else + + using (var ds = new DataSet()) { - Logger.Error(this, "Query", ex.GetType().FullName, ex.Message, sql); - return new Query(ex); + da.Fill(ds); + if (ds.Tables.Count > 0) + { + var tables = new DataTable[ds.Tables.Count]; + ds.Tables.CopyTo(tables, 0); + Logger.Info(this, "Query", sql); + return new Query(tables, true); + } + else + { + Logger.Error(this, "Query", "查询结果不包含任何数据表。", sql); + return new Query(false, "查询结果不包含任何数据表。"); + } } } } catch (Exception exception) { - Logger.Error(this, "Query", exception.GetType().FullName, exception.Message, sql); + Logger.Error(this, "Query", exception.Message, sql); return new Query(exception); } } - /// 执行。 - public IExecute Execute(string sql) => Execute(sql, null); - - /// 执行单条 Transact-SQL 语句,并加入参数。 - public IExecute Execute(string sql, IEnumerable parameters) + /// 执行 SQL 语句,并加入参数。 + public IExecute Execute(string sql, IEnumerable parameters = null, bool autoTransaction = true) { if (TextUtility.IsEmpty(sql)) return new Execute(false, "语句无效。"); - var connected = Connect(); - if (connected.NotEmpty()) return new Execute(false, connected); - - var inTransaction = _transaction != null; - if (!inTransaction) Begin(); - try + lock (Locker) { + var connected = Connect(); + if (connected.NotEmpty()) return new Execute(false, connected); - using (var command = NewCommand()) + var inTransaction = _transaction != null; + if (autoTransaction && !inTransaction) Begin(); + try { - command.Connection = _conn; - command.Transaction = (DbTransaction)_transaction; - if (Timeout != null) command.CommandTimeout = Timeout.Execute; - command.CommandText = sql; - if (parameters != null) + + using (var command = _conn.CreateCommand()) { - foreach (var parameter in parameters) + command.Connection = _conn; + if (autoTransaction) command.Transaction = (DbTransaction)_transaction; + if (Timeout != null) command.CommandTimeout = Timeout.Execute; + command.CommandText = sql; + if (parameters != null) { - if (parameter != null) command.Parameters.Add(parameter); + foreach (var parameter in parameters) + { + if (parameter != null) command.Parameters.Add(parameter); + } } + var rows = command.ExecuteNonQuery(); + if (autoTransaction && !inTransaction) Commit(); // todo 此处应该检查事务提交产生的错误。 + Logger.Info(this, "Execute", sql); + return new Execute(true, rows); } - var rows = command.ExecuteNonQuery(); - if (!inTransaction) Commit(); // todo 此处应该检查事务提交产生的错误。 - return new Execute(true, rows); } - } - catch (Exception exception) - { - Logger.Error(this, "Execute", exception, sql); - if (!inTransaction) Rollback(); - return new Execute(exception); + catch (Exception exception) + { + Logger.Error(this, "Execute", exception.Message, sql); + if (autoTransaction && !inTransaction) Rollback(); + return new Execute(exception); + } } } @@ -294,12 +330,69 @@ namespace Apewer.Source #endregion + #region parameter + + /// 创建参数。 + /// + public IDataParameter Parameter(string name, object value) + { + if (name.IsEmpty()) throw new ArgumentNullException(nameof(name)); + + var p = CreateParameter(); + p.ParameterName = name; + p.Value = value; + return p; + } + + /// 创建参数。 + /// + public IDataParameter Parameter(string name, object value, DbType type) + { + if (name.IsEmpty()) throw new ArgumentNullException(nameof(name)); + + var p = CreateParameter(); + p.ParameterName = name; + p.Value = value; + p.DbType = type; + return p; + } + + #endregion + #region orm - /// 使用指定语句查询,获取查询结果。 - /// 要执行的 SQL 语句。 - /// 为 SQL 语句提供的参数。 - public Result Query(string sql, IEnumerable parameters = null) where T : class, new() => SourceUtility.As(Query(typeof(T), sql, parameters)); + /// 初始化指定类型,以创建表或增加字段。 + /// 错误信息。当成功时候返回空字符串。 + public string Initialize() where T : class, new() => Initialize(typeof(T)); + + /// 初始化指定类型,以创建表或增加字段。 + /// 要初始化的类型。 + /// 错误信息。当成功时候返回空字符串。 + public string Initialize(Type model) + { + if (model == null) return "参数 model 无效。"; + if (!model.IsClass) return $"类型 <{model.FullName}> 不是类。"; + if (model.IsAbstract) return $"类型 <{model.FullName}> 是抽象类。"; + if (!RuntimeUtility.CanNew(model)) return $"类型 <{model.FullName}> 无法创建新实例。"; + + var structure = TableStructure.Parse(model); + if (structure == null) return "无法解析记录模型。"; + + // 连接数据库。 + var connect = Connect(); + if (connect.NotEmpty()) return connect; + + return Initialize(structure); + } + + /// + protected abstract string Initialize(TableStructure structure); + + /// 插入记录。 + /// 要插入的记录实体。 + /// 插入到指定表。当不指定时,由 record 类型决定。 + /// 错误信息。当成功时候返回空字符串。 + public abstract string Insert(object record, string table = null); /// 使用指定语句查询,获取查询结果。 /// 目标记录的类型。 @@ -319,21 +412,103 @@ namespace Apewer.Source try { var array = SourceUtility.Fill(query, model); - result = new Result(array); + return new Result(array); } - catch (Exception ex) - { - result = new Result(ex); - } - } - else - { - result = new Result(query.Message); + catch (Exception ex) { return new Result(ex); } } - return result; + else return new Result(query.Message); } } + /// 使用指定语句查询,获取查询结果。 + /// 要执行的 SQL 语句。 + /// 为 SQL 语句提供的参数。 + public Result Query(string sql, IEnumerable parameters = null) where T : class, new() => Query(typeof(T), sql, parameters).As(); + + #endregion + + #region record + + /// 更新记录。 + /// 要更新的记录实体。 + /// 更新指定的表。当不指定时,由 record 类型决定。 + /// 错误信息。当成功时候返回空字符串。 + public abstract string Update(IRecord record, string table = null); + + /// + protected abstract string KeysSql(string tableName, string keyField, string flagField, long flagValue); + + /// 获取指定类型的主键,按 Flag 属性筛选。 + /// 要查询的类型。 + /// 要求目标记录具有的 Flag 属性,当指定 0 时忽略此要求。 + /// + public Result Keys(Type model, long flag = 0) + { + if (model == null) throw new ArgumentNullException(nameof(model)); + + var ts = TableStructure.Parse(model); + if (ts.Name.IsEmpty()) return new Result($"类型 <{model.FullName}> 中不包含表名称。"); + if (ts.Key == null || ts.Key.Field.IsEmpty()) return new Result($"类型 <{model.FullName}> 中不包含 Key 的字段。"); + if (ts.Flag == null || ts.Flag.Field.IsEmpty()) return new Result($"类型 <{model.FullName}> 中不包含 Flag 的字段。"); + + var sql = KeysSql(ts.Name, ts.Key.Field, ts.Flag.Field, flag); + return new Result(TextColumn(sql)); + } + + /// 获取指定类型的主键,按 Flag 属性筛选。 + /// 要求目标记录具有的 Flag 属性,当指定 0 时忽略此要求。 + public Result Keys(long flag = 0) where T : class, IRecord, new() => Keys(typeof(T), flag); + + /// + protected abstract string RecordSql(string tableName, string keyField, string keyValue, string flagField, long flagValue); + + /// 获取具有指定 Key 的记录,并要求记录具有指定的 Flag 属性。 + /// 目标记录的类型。 + /// 目标记录的主键。 + /// 要求目标记录具有的 Flag 属性,当指定 0 时忽略此要求。 + public Result Record(Type model, string key, long flag = 0) + { + if (model == null) throw new ArgumentNullException(nameof(model)); + + var ts = TableStructure.Parse(model); + if (ts.Name.IsEmpty()) return new Result($"类型 <{model.FullName}> 中不包含表名称。"); + if (ts.Key == null || ts.Key.Field.IsEmpty()) return new Result($"类型 <{model.FullName}> 中不包含 Key 的字段。"); + if (ts.Flag == null || ts.Flag.Field.IsEmpty()) return new Result($"类型 <{model.FullName}> 中不包含 Flag 的字段。"); + + var sql = RecordSql(ts.Name, ts.Key.Field, key, ts.Flag.Field, flag); + var records = Query(model, sql, null); + if (records) return new Result(records.Value.First()); + else return new Result(records.Message); + } + + /// 获取具有指定 Key 的记录,并要求记录具有指定的 Flag 属性。 + /// 目标记录的主键。 + /// 要求目标记录具有的 Flag 属性,当指定 0 时忽略此要求。 + public Result Record(string key, long flag = 0) where T : class, IRecord, new() => Record(typeof(T), key, flag).As(); + + /// + protected abstract string RecordsSql(string tableName, string flagField, long flagValue); + + /// 查询所有记录,可按 Flag 筛选。 + /// 目标记录的类型。 + /// 要求目标记录具有的 Flag 属性,当指定 0 时忽略此要求。 + public Result Records(Type model, long flag = 0) + { + if (model == null) throw new ArgumentNullException(nameof(model)); + + var ts = TableStructure.Parse(model); + if (ts.Name.IsEmpty()) return new Result($"类型 <{model.FullName}> 中不包含表名称。"); + if (ts.Key == null || ts.Key.Field.IsEmpty()) return new Result($"类型 <{model.FullName}> 中不包含 Key 的字段。"); + if (ts.Flag == null || ts.Flag.Field.IsEmpty()) return new Result($"类型 <{model.FullName}> 中不包含 Flag 的字段。"); + + var sql = RecordsSql(ts.Name, ts.Flag.Field, flag); + return Query(model, sql, null); + } + + /// 查询所有记录,可按 Flag 筛选。 + /// 要求目标记录具有的 Flag 属性,当指定 0 时忽略此要求。 + public Result Records(long flag = 0) where T : class, IRecord, new() => Records(typeof(T), flag).As(); + #endregion #region static @@ -359,17 +534,14 @@ namespace Apewer.Source /// 为 Ado 创建连接字符串。 protected abstract string NewConnectionString(); - /// 为 Ado 创建 IDbConnection 对象。 - protected abstract IDbConnection NewConnection(); - - /// 为 Ado 创建 IDbCommand 对象。 - protected abstract IDbCommand NewCommand(); - /// 为 Ado 创建 IDataAdapter 对象。 - protected abstract IDataAdapter NewDataAdapter(IDbCommand command); + protected abstract IDataAdapter CreateDataAdapter(IDbCommand command); - // /// 为 Ado 创建 IDataParameter 对象。 - // protected abstract IDataParameter NewDataParameter(); + /// 为 Ado 创建 IDbConnection 对象。 + protected abstract IDbConnection CreateConnection(); + + /// 为 Ado 创建 IDataParameter 对象。 + protected abstract IDataParameter CreateParameter(); #endregion @@ -384,67 +556,8 @@ namespace Apewer.Source /// 查询表中的所有列名。 public abstract string[] ColumnNames(string tableName); - /// 获取列信息。 - public abstract ColumnInfo[] ColumnsInfo(string tableName); - - /// 初始化指定类型,以创建表或增加字段。 - /// 错误信息。当成功时候返回空字符串。 - public string Initialize() where T : class, new() => Initialize(typeof(T)); - - #endregion - - #region IDbClientOrm - - /// 初始化指定类型,以创建表或增加字段。 - /// 要初始化的类型。 - /// 错误信息。当成功时候返回空字符串。 - public abstract string Initialize(Type model); - - /// 插入记录。 - /// 要插入的记录实体。 - /// 插入到指定表。当不指定时,由 record 类型决定。 - /// 错误信息。当成功时候返回空字符串。 - public abstract string Insert(object record, string table = null); - - /// 更新记录。 - /// 要插入的记录实体。 - /// 插入到指定表。当不指定时,由 record 类型决定。 - /// 错误信息。当成功时候返回空字符串。 - public abstract string Update(IRecord record, string table = null); - - /// 获取指定类型的主键,按 Flag 属性筛选。 - /// 要查询的类型。 - /// 要求目标记录具有的 Flag 属性,当指定 0 时忽略此要求。 - public abstract Result Keys(Type model, long flag = 0); - - /// 获取指定类型的主键,按 Flag 属性筛选。 - /// 要求目标记录具有的 Flag 属性,当指定 0 时忽略此要求。 - public abstract Result Keys(long flag = 0) where T : class, IRecord, new(); - - /// 获取具有指定 Key 的记录,并要求记录具有指定的 Flag 属性。 - /// 目标记录的类型。 - /// 目标记录的主键。 - /// 要求目标记录具有的 Flag 属性,当指定 0 时忽略此要求。 - public abstract Result Get(Type model, string key, long flag = 0); - - /// 获取具有指定 Key 的记录,并要求记录具有指定的 Flag 属性。 - /// 目标记录的主键。 - /// 要求目标记录具有的 Flag 属性,当指定 0 时忽略此要求。 - public abstract Result Get(string key, long flag = 0) where T : class, IRecord, new(); - - /// 查询所有记录。 - /// 目标记录的类型。 - /// 要求目标记录具有的 Flag 属性,当指定 0 时忽略此要求。 - public abstract Result Query(Type model, long flag = 0); - - /// 查询所有记录。 - /// 要求目标记录具有的 Flag 属性,当指定 0 时忽略此要求。 - public abstract Result Query(long flag = 0) where T : class, IRecord, new(); - #endregion } } - -#endif diff --git a/Apewer.Source/Source/MySql.cs b/Apewer.Source/Source/MySql.cs index ce4f9eb..f740249 100644 --- a/Apewer.Source/Source/MySql.cs +++ b/Apewer.Source/Source/MySql.cs @@ -7,6 +7,7 @@ using System; using System.Collections.Generic; using System.Data; using System.Net; +using System.Security.Cryptography.X509Certificates; using System.Text; using System.Transactions; @@ -16,331 +17,71 @@ namespace Apewer.Source { /// - public class MySql : IDbClient + public class MySql : DbClient { - #region 基础 + #region connection - private Timeout _timeout = null; - private string _connectionstring = null; - - /// 获取或设置日志记录。 - public Logger Logger { get; set; } - - /// 超时设定。 - public Timeout Timeout { get => _timeout; } + private string _connstr = null; /// 创建实例。 - public MySql(string connnectionString, Timeout timeout = default) + public MySql(string connnectionString, Timeout timeout = default) : base(timeout) { - _connectionstring = connnectionString; - _timeout = timeout ?? Timeout.Default; + _connstr = connnectionString; } - /// 获取当前的 MySqlConnection 对象。 - public IDbConnection Connection { get => _connection; } - /// 构建连接字符串以创建实例。 - public MySql(string address, string store, string user, string pass, Timeout timeout = null) + public MySql(string address, string store, string user, string pass, Timeout timeout = null) : base(timeout) { - _timeout = timeout ?? Timeout.Default; - var a = address ?? ""; var s = store ?? ""; var u = user ?? ""; var p = pass ?? ""; var cs = $"server={a}; database={s}; uid={u}; pwd={p}; "; - _connectionstring = cs; - _storename = new Class(s); - } - - #endregion - - #region 日志。 - - private void LogError(string action, Exception ex, string addtion) - { - var logger = Logger; - if (logger != null) logger.Error(this, "MySQL", action, ex.GetType().FullName, ex.Message, addtion); + _connstr = cs; } #endregion - #region Connection - - private MySqlConnection _connection = null; + #region override /// - public bool Online { get => _connection == null ? false : (_connection.State == ConnectionState.Open); } - - /// 连接字符串。 - public string ConnectionString { get => _connectionstring; } + protected override string NewConnectionString() => _connstr; /// - public string Connect() - { - if (_connection == null) - { - _connection = new MySqlConnection(); - _connection.ConnectionString = _connectionstring; - } - else - { - if (_connection.State == ConnectionState.Open) return null; - } - - // try - { - _connection.Open(); - switch (_connection.State) - { - case ConnectionState.Open: return null; - default: return $"连接失败,当前处于 {_connection.State} 状态。"; - } - } - // catch (Exception ex) - // { - // LogError("Connection", ex, _connection.ConnectionString); - // Close(); - // return false; - // } - } + protected override IDataAdapter CreateDataAdapter(IDbCommand command) => new MySqlDataAdapter((MySqlCommand)command); /// - public void Close() - { - if (_connection != null) - { - if (_transaction != null) - { - if (_autocommit) Commit(); - else Rollback(); - } - _connection.Close(); - _connection.Dispose(); - _connection = null; - } - } + protected override IDbConnection CreateConnection() => new MySqlConnection(); /// - public void Dispose() { Close(); } - - #endregion - - #region Transaction - - private IDbTransaction _transaction = null; - private bool _autocommit = false; - - /// 启动事务。 - public string Begin(bool commit = true) => Begin(commit, null); - - /// 启动事务。 - public string Begin(bool commit, Class isolation) - { - if (Connect() != null) return "未连接。"; - if (_transaction != null) return "存在已启动的事务,无法再次启动。"; - try - { - _transaction = isolation ? _connection.BeginTransaction(isolation.Value) : _connection.BeginTransaction(); - _autocommit = commit; - return null; - } - catch (Exception ex) - { - Logger.Error(nameof(MySql), "Commit", ex.Message()); - return ex.Message(); - } - } - - /// 提交事务。 - public string Commit() - { - if (_transaction == null) return "事务不存在。"; - try - { - _transaction.Commit(); - RuntimeUtility.Dispose(_transaction); - _transaction = null; - return null; - } - catch (Exception ex) - { - RuntimeUtility.Dispose(_transaction); - _transaction = null; - Logger.Error(nameof(MySql), "Commit", ex.Message()); - return ex.Message(); - } - } - - /// 从挂起状态回滚事务。 - public string Rollback() - { - if (_transaction == null) return "事务不存在。"; - try - { - _transaction.Rollback(); - RuntimeUtility.Dispose(_transaction); - _transaction = null; - return null; - } - catch (Exception ex) - { - RuntimeUtility.Dispose(_transaction); - _transaction = null; - Logger.Error(nameof(MySql), "Rollback", ex.Message()); - return ex.Message(); - } - } - - #endregion - - #region SQL + protected override IDataParameter CreateParameter() => new MySqlParameter(); /// - public IQuery Query(string sql, IEnumerable parameters) + public override string[] StoreNames() { - if (sql.IsBlank()) return Example.InvalidQueryStatement; - - var connected = Connect(); - if (connected.NotEmpty()) return Example.InvalidQueryConnection; - - try - { - using (var command = new MySqlCommand()) - { - command.Connection = _connection; - command.CommandTimeout = _timeout.Query; - command.CommandText = sql; - if (parameters != null) - { - foreach (var p in parameters) - { - if (p != null) command.Parameters.Add(p); - } - } - using (var ds = new DataSet()) - { - using (var da = new MySqlDataAdapter(command)) - { - var name = "table_" + Guid.NewGuid().ToString("n"); - da.Fill(ds, name); - var table = ds.Tables[name]; - return new Query(table); - } - } - } - } - catch (Exception exception) - { - Logger.Error(nameof(MySql), "Query", exception, sql); - return new Query(exception); - } + throw new NotImplementedException(); } /// - public IExecute Execute(string sql, IEnumerable parameters) + public override string[] TableNames() { - if (sql.IsBlank()) return Example.InvalidExecuteStatement; - - var connected = Connect(); - if (connected.NotEmpty()) return new Execute(false, connected); - - var inTransaction = _transaction != null; - if (!inTransaction) Begin(); - try - { - using (var command = new MySqlCommand()) - { - command.Connection = _connection; - command.Transaction = (MySqlTransaction)_transaction; - command.CommandTimeout = _timeout.Execute; - command.CommandText = sql; - if (parameters != null) - { - foreach (var parameter in parameters) - { - if (parameter == null) continue; - command.Parameters.Add(parameter); - } - } - var rows = command.ExecuteNonQuery(); - if (!inTransaction) Commit(); // todo 此处应该检查事务提交产生的错误。 - return new Execute(true, rows); - } - } - catch (Exception exception) - { - Logger.Error(nameof(MySql), "Execute", exception, sql); - if (!inTransaction) Rollback(); - return new Execute(exception); - } + var store = StoreName(); + var sql = $"select table_name from information_schema.tables where table_schema='{store}' and table_type='base table'"; + return TextColumn(sql); } /// - public IQuery Query(string sql) => Query(sql, null); - - /// - public IExecute Execute(string sql, IEnumerable parameters) + public override string[] ColumnNames(string tableName) { - var dps = null as List; - if (parameters != null) - { - var count = parameters.Count(); - dps = new List(count); - foreach (var p in parameters) - { - var dp = Parameter(p); - dps.Add(dp); - } - } - return Execute(sql, dps); + var store = StoreName(); + var table = TextUtility.AntiInject(tableName); + var sql = $"select column_name from information_schema.columns where table_schema='{store}' and table_name='{table}'"; + return TextColumn(sql); } /// - public IExecute Execute(string sql) => Execute(sql, null as IEnumerable); - - #endregion - - #region ORM - - private Class _storename = null; - - private string StoreName() - { - if (_storename) return _storename.Value; - _storename = new Class(Internals.TextHelper.ParseConnectionString(_connectionstring).GetValue("database")); - return _storename.Value ?? ""; - } - - private string[] FirstColumn(string sql) - { - using (var query = Query(sql) as Query) return query.ReadColumn(); - } - - /// - public string[] TableNames() - { - var sql = TextUtility.Merge("select table_name from information_schema.tables where table_schema='", StoreName(), "' and table_type='base table';"); - return FirstColumn(sql); - } - - /// - public string[] ViewNames() - { - var sql = TextUtility.Merge("select table_name from information_schema.tables where table_schema='", StoreName(), "' and table_type='view';"); - return FirstColumn(sql); - } - - /// - public string[] ColumnNames(string table) - { - var sql = TextUtility.Merge("select column_name from information_schema.columns where table_schema='", StoreName(), "' and table_name='", TextUtility.AntiInject(table), "';"); - return FirstColumn(sql); - } - - /// 获取用于创建表的语句。 - private string GetCreateStetement(TableStructure structure) + protected override string Initialize(TableStructure structure) { // 检查现存表。 var exists = false; @@ -392,7 +133,10 @@ namespace Apewer.Source sqlsb.Append("alter table `", structure.Name, "` add column ", type, "; "); } var sql = sqlsb.ToString(); - return sql; + if (sql.IsEmpty()) return null; + + var execute = Execute(sql); + return execute.Success ? null : execute.Message; } else { @@ -427,54 +171,13 @@ namespace Apewer.Source var sqlPrimaryKey = primarykeys.Count < 1 ? "" : (", primary key (" + string.Join(", ", primarykeys.ToArray()) + ")"); sql = TextUtility.Merge("create table `", table, "`(", joined, sqlPrimaryKey, ") engine=innodb default charset=utf8mb4; "); - return sql; - } - } - - /// - private string Initialize(Type model, out string sql) - { - if (model == null) - { - sql = null; - return "指定的类型无效。"; - } - - var structure = TableStructure.Parse(model); - if (structure == null) - { - sql = null; - return "无法解析记录模型。"; - } - - // 连接数据库。 - var connect = Connect(); - if (connect.NotEmpty()) - { - sql = null; - return $"连接数据库失败。({connect})"; - } - - sql = GetCreateStetement(structure); - if (sql.NotEmpty()) - { var execute = Execute(sql); - if (!execute.Success) return execute.Message; + return execute.Success ? null : execute.Message; } - return null; } - /// - public string Initialize(Type model) => Initialize(model, out string sql); - - /// - public string Initialize() where T : class, new() => Initialize(typeof(T)); - - /// - public string Initialize(Record model) => (model == null) ? "参数无效。" : Initialize(model.GetType()); - /// 插入记录。返回错误信息。 - public string Insert(object record, string table = null) + public override string Insert(object record, string table = null) { if (record == null) return "参数无效。"; SourceUtility.FixProperties(record); @@ -515,7 +218,7 @@ namespace Apewer.Source /// 更新记录,实体中的 Key 属性不被更新。返回错误信息。 /// 无法更新带有 Independent 特性的模型(缺少 Key 属性)。 - public string Update(IRecord record, string table = null) + public override string Update(IRecord record, string table = null) { if (record == null) return "参数无效。"; FixProperties(record); @@ -547,7 +250,7 @@ namespace Apewer.Source items.Add(TextUtility.Merge("`", pn, "` = @", pn)); } var key = record.Key.SafeKey(); - var sql = $"update `{table}` set {string.Join(", ", items)} where `_key`='{key}'; "; + var sql = $"update `{table}` set {string.Join(", ", items)} where `{structure.Key.Field}`='{key}'; "; var execute = Execute(sql, ps); if (execute.Success) return TextUtility.Empty; @@ -555,78 +258,60 @@ namespace Apewer.Source } /// - public Result Query(Type model, string sql, IEnumerable parameters = null) => SourceUtility.Query(this, model, sql, parameters); + protected override string KeysSql(string tableName, string keyField, string flagField, long flagValue) + { + if (flagValue == 0) return $"select `{keyField}` from `{tableName}`"; + else return $"select `{keyField}` from `{tableName}` where `{flagField}` = {flagValue}"; + } /// - public Result Query(string sql, IEnumerable parameters = null) where T : class, new() + protected override string RecordSql(string tableName, string keyField, string keyValue, string flagField, long flagValue) { - var query = Query(sql, parameters); - if (!query.Success) return new Result(query.Message); - var records = query.Fill(); - query.Dispose(); - - var result = new Result(records); - return result; + if (flagValue == 0) return $"select * from `{tableName}` where `{keyField}` = '{keyValue}' limit 1"; + else return $"select * from `{tableName}` where `{keyField}` = '{keyValue}' and `{flagField}` = {flagValue} limit 1"; } - /// 获取所有记录。Flag 为 0 时将忽略 Flag 条件。 - public Result Query(Type model, long flag = 0) => SourceUtility.Query(this, model, (tn) => + /// + protected override string RecordsSql(string tableName, string flagField, long flagValue) { - if (flag == 0) return $"select * from `{tn}`; "; - return $"select * from `{tn}` where `_flag`={flag}; "; - }); + if (flagValue == 0) return $"select * from `{tableName}`"; + else return $"select * from `{tableName}` where `{flagField}` = {flagValue}"; + } - /// 获取所有记录。Flag 为 0 时将忽略 Flag 条件。 - public Result Query(long flag = 0) where T : class, IRecord, new() => SourceUtility.Query(this, (tn) => - { - if (flag == 0) return $"select * from `{tn}`; "; - return $"select * from `{tn}` where `_flag`={flag}; "; - }); + #endregion - /// 获取记录。 - /// 填充的记录模型。 - /// 要跳过的记录数,可用最小值为 0。 - /// 要获取的记录数,可用最小值为 1。 - public Result Query(Type model, int skip, int count) + #region special + + string StoreName() => Internals.TextHelper.ParseConnectionString(_connstr).GetValue("database") ?? ""; + + /// 获取所有视图的名称。 + public string[] ViewNames() { - if (skip < 0) return new Result("参数 skip 超出了范围。"); - if (count < 1) return new Result("参数 count 超出了范围。"); - return SourceUtility.Query(this, model, (tn) => $"select * from `{tn}` limit {skip}, {count}; "); + var store = StoreName(); + var sql = $"select table_name from information_schema.tables where table_schema='{store}' and table_type='view'"; + return TextColumn(sql); } /// 获取记录。 + /// 填充的记录模型。 /// 要跳过的记录数,可用最小值为 0。 /// 要获取的记录数,可用最小值为 1。 - public Result Query(int skip, int count) where T : class, IRecord, new() + public Result Range(Type model, int skip, int count) where T : class, new() { + if (model == null) return new Result("参数 model 无效。"); if (skip < 0) return new Result("参数 skip 超出了范围。"); if (count < 1) return new Result("参数 count 超出了范围。"); - return SourceUtility.Query(this, (tn) => $"select * from `{tn}` limit {skip}, {count}; "); - } - - /// 获取记录。 - public Result Get(Type model, string key, long flag = 0) => SourceUtility.Get(this, model, key, (tn, sk) => - { - if (flag == 0) return $"select * from `{tn}` where `_key`='{sk}' limit 1;"; - return $"select * from `{tn}` where `_key`='{sk}' and `_flag`={flag} limit 1;"; - }); - - /// 获取记录。 - public Result Get(string key, long flag = 0) where T : class, IRecord, new() => SourceUtility.Get(this, key, (tn, sk) => - { - if (flag == 0) return $"select * from `{tn}` where `_key`='{sk}' limit 1;"; - return $"select * from `{tn}` where `_key`='{sk}' and `_flag`={flag} limit 1;"; - }); - /// >获取指定类型的主键,按 Flag 属性筛选。 - public Result Keys(Type model, long flag = 0) => SourceUtility.Keys(this, model, (tn) => - { - if (flag == 0) return $"select `_key` from `{tn}`;"; - return $"select `_key` from `{tn}` where `_flag`={flag};"; - }); + var ts = TableStructure.Parse(model); + if (ts.Name.IsEmpty()) return new Result($"无法解析类型 {model.FullName}。"); - /// >获取指定类型的主键,按 Flag 属性筛选。 - public Result Keys(long flag = 0) where T : class, IRecord, new() => Keys(typeof(T), flag); + var sql = $"select * from `{ts.Name}` limit {skip}, {count}"; + using (var query = Query(sql)) + { + if (!query.Success) return new Result(query.Message); + return new Result(query.Fill()); + } + } /// 对表添加列,返回错误信息。 /// 记录类型。 @@ -636,7 +321,7 @@ namespace Apewer.Source /// public string AddColumn(string column, ColumnType type, int length = 0) where T : class, IRecord { - var columnName = SafeColumn(column); + var columnName = column.SafeName(); if (columnName.IsEmpty()) return "列名无效。"; var ta = TableAttribute.Parse(typeof(T)); @@ -686,55 +371,6 @@ namespace Apewer.Source #region static - /// 对文本转义,符合 SQL 安全性。可根据字段类型限制 UTF-8 字节数,默认为 0 时不限制字节数。 - public static string Escape(string text, int bytes = 0) - { - if (text.IsEmpty()) return ""; - - var t = text ?? ""; - t = t.Replace("\\", "\\\\"); - t = t.Replace("'", "\\'"); - t = t.Replace("\n", "\\n"); - t = t.Replace("\r", "\\r"); - t = t.Replace("\b", "\\b"); - t = t.Replace("\t", "\\t"); - t = t.Replace("\f", "\\f"); - - if (bytes > 5) - { - if (t.Bytes(Encoding.UTF8).Length > bytes) - { - while (true) - { - t = t.Substring(0, t.Length - 1); - if (t.Bytes(Encoding.UTF8).Length <= (bytes - 4)) break; - } - t = t + " ..."; - } - } - - return t; - } - - /// - public static string SafeTable(string table) - { - const string chars = "0123456789_-ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"; - var safety = TextUtility.Restrict(table, chars).Lower(); - var pc = 0; - for (var i = 0; i < safety.Length; i++) - { - if (safety[i] == '-') pc += 1; - else break; - } - if (pc == safety.Length) return ""; - if (pc > 0) safety = safety.Substring(pc); - return safety; - } - - /// - public static string SafeColumn(string column) => SafeTable(column); - /// /// /// diff --git a/Apewer.Source/Source/SqlClient.cs b/Apewer.Source/Source/SqlClient.cs index 995381b..e944b8c 100644 --- a/Apewer.Source/Source/SqlClient.cs +++ b/Apewer.Source/Source/SqlClient.cs @@ -20,32 +20,22 @@ namespace Apewer.Source /// [Serializable] - public class SqlClient : IDbClient + public class SqlClient : DbClient { - #region 变量、构造函数 + #region connection - private Timeout _timeout = null; - private string _connectionstring = ""; - - /// 获取或设置日志记录。 - public Logger Logger { get; set; } - - /// 超时设定。 - public Timeout Timeout { get => _timeout; } + private string _connstr = ""; /// 使用连接字符串创建数据库连接实例。 - public SqlClient(string connectionString, Timeout timeout = null) + public SqlClient(string connectionString, Timeout timeout = null) : base(timeout) { - _timeout = timeout ?? Timeout.Default; - _connectionstring = connectionString ?? ""; + _connstr = connectionString ?? ""; } /// 使用连接凭据创建数据库连接实例。 - public SqlClient(string address, string store, string user, string pass, Timeout timeout = null) + public SqlClient(string address, string store, string user, string pass, Timeout timeout = null) : base(timeout) { - if (timeout == null) timeout = Timeout.Default; - var a = address ?? ""; var s = store ?? ""; var u = user ?? ""; @@ -57,487 +47,28 @@ namespace Apewer.Source cs += $"user id = {u}; "; if (!string.IsNullOrEmpty(p)) cs += $"password = {p}; "; } - cs += $"connection timeout = {timeout.Connect}; "; - - _timeout = timeout ?? Timeout.Default; - _connectionstring = cs; - } - - #endregion - - #region Ado - Connection - - private SqlConnection _db = null; - - /// 连接字符串。 - public string ConnectionString { get => _connectionstring; } - - /// 获取当前的 SqlConnection 对象。 - public IDbConnection Connection { get => _db; } - - /// 数据库是否已经连接。 - public bool Online - { - get - { - if (_db == null) return false; - return (_db.State == ConnectionState.Open); - } - } - - /// 连接数据库,若未连接则尝试连接,获取连接成功的状态。 - public string Connect() - { - if (_db == null) - { - _db = new SqlConnection(); - _db.ConnectionString = _connectionstring; - } - else - { - if (_db.State == ConnectionState.Open) return null; - } - try - { - _db.Open(); - switch (_db.State) - { - case ConnectionState.Open: return null; - default: return $"连接失败,当前处于 {_db.State} 状态。"; - } - } - catch (Exception ex) - { - Logger.Error(nameof(SqlClient), "Connection", ex, _db.ConnectionString); - Close(); - return ex.Message; - } - } - - /// 改变 - /// - /// - public string Change(string store) - { - if (store.IsEmpty()) return "未指定数据名称。"; - - var connect = Connect(); - if (connect.NotEmpty()) return connect; - - try - { - _db.ChangeDatabase(store); - return null; - } - catch (Exception ex) { return ex.Message(); } - } - - /// 关闭连接,并释放对象所占用的系统资源。 - public void Close() - { - if (_db != null) - { - if (_transaction != null) - { - if (_autocommit) Commit(); - else Rollback(); - } - _db.Close(); - _db.Dispose(); - _db = null; - } - } - - /// 关闭连接,释放对象所占用的系统资源,并清除连接信息。 - public void Dispose() => Close(); - - #endregion - - #region Ado - Transaction - - private IDbTransaction _transaction = null; - private bool _autocommit = false; - - /// 启动事务。 - public string Begin(bool commit = true) => Begin(commit, null); - - /// 启动事务。 - public string Begin(bool commit, Class isolation) - { - var connect = Connect(); - if (connect.NotEmpty()) return connect; - if (_transaction != null) return "存在已启动的事务,无法再次启动。"; - try - { - _transaction = isolation ? _db.BeginTransaction(isolation.Value) : _db.BeginTransaction(); - _autocommit = commit; - return null; - } - catch (Exception ex) - { - Logger.Error(nameof(SqlClient), "Begin", ex.Message()); - return ex.Message(); - } - } - - /// 提交事务。 - public string Commit() - { - if (_transaction == null) return "事务不存在。"; - try - { - _transaction.Commit(); - RuntimeUtility.Dispose(_transaction); - _transaction = null; - return null; - } - catch (Exception ex) - { - RuntimeUtility.Dispose(_transaction); - _transaction = null; - Logger.Error(nameof(SqlClient), "Commit", ex.Message()); - return ex.Message(); - } - } - - /// 从挂起状态回滚事务。 - public string Rollback() - { - if (_transaction == null) return "事务不存在。"; - try - { - _transaction.Rollback(); - RuntimeUtility.Dispose(_transaction); - _transaction = null; - return null; - } - catch (Exception ex) - { - RuntimeUtility.Dispose(_transaction); - _transaction = null; - Logger.Error(nameof(SqlClient), "Rollback", ex.Message); - return ex.Message(); - } - } - - #endregion - - #region Ado - SQL - - /// 查询。 - public IQuery Query(string sql) => Query(sql, null); - - /// 查询。 - public IQuery Query(string sql, IEnumerable parameters) - { - if (TextUtility.IsBlank(sql)) return Example.InvalidQueryStatement; - var connected = Connect(); - if (connected.NotEmpty()) return new Query(false, connected); - - try - { - using (var command = new SqlCommand()) - { - command.Connection = _db; - command.CommandTimeout = _timeout.Query; - command.CommandText = sql; - if (parameters != null) - { - foreach (var p in parameters) - { - if (p != null) command.Parameters.Add(p); - } - } - using (var dataset = new DataSet()) - { - using (var da = new SqlDataAdapter(command)) - { - var name = "table_" + Guid.NewGuid().ToString("n"); - da.Fill(dataset, name); - var table = dataset.Tables[name]; - return new Query(table); - } - } - } - } - catch (Exception exception) - { - Logger.Error(nameof(SqlClient), "Query", exception, sql); - return new Query(exception); - } - } - - /// 执行。 - public IExecute Execute(string sql) => Execute(sql, null, true); - - /// 执行单条 Transact-SQL 语句,并加入参数。 - public IExecute Execute(string sql, IEnumerable parameters) => Execute(sql, parameters, true); - - /// 执行单条 Transact-SQL 语句,并加入参数。 - IExecute Execute(string sql, IEnumerable parameters, bool requireTransaction) - { - if (TextUtility.IsBlank(sql)) return Example.InvalidExecuteStatement; - - var connected = Connect(); - if (connected.NotEmpty()) return new Execute(false, connected); - - var inTransaction = _transaction != null; - if (requireTransaction && !inTransaction) Begin(); - try - { - using (var command = new SqlCommand()) - { - command.Connection = _db; - if (requireTransaction) command.Transaction = (SqlTransaction)_transaction; - command.CommandTimeout = _timeout.Execute; - command.CommandText = sql; - if (parameters != null) - { - foreach (var parameter in parameters) - { - if (parameter != null) command.Parameters.Add(parameter); - } - } - var rows = command.ExecuteNonQuery(); - if (requireTransaction && !inTransaction) Commit(); // todo 此处应该检查事务提交产生的错误。 - return new Execute(true, rows); - } - } - catch (Exception exception) - { - Logger.Error(nameof(SqlClient), "Execute", exception, sql); - if (requireTransaction && !inTransaction) Rollback(); - return new Execute(exception); - } - } - - /// 批量插入,必须在 DataTable 中指定表名,或指定 tableName 参数。 - /// - /// - public void BulkCopy(DataTable table, string tableName = null) - { - // 检查 table 参数。 - if (table == null) throw new ArgumentNullException(nameof(table)); - if (table.Rows.Count < 1) return; - - // 检查 tableName 参数。 - if (tableName.IsEmpty()) tableName = table.TableName; - if (tableName.IsEmpty()) throw new ArgumentException("未指定表名。"); + if (timeout != null) cs += $"connection timeout = {timeout.Connect}; "; - // 连接数据库。 - var connect = Connect(); - if (connect.NotEmpty()) throw new Exception(connect); - - // 批量插入。 - var bc = null as SqlBulkCopy; - try - { - bc = new SqlBulkCopy(_db); - bc.DestinationTableName = tableName; - bc.BatchSize = table.Rows.Count; - bc.WriteToServer(table); - } - catch (Exception ex) - { - try { bc.Close(); } catch { } - throw ex; - } + _connstr = cs; } #endregion - #region ORM + #region override /// 查询数据库中的所有表名。 - public string[] TableNames() - { - var list = new List(); - if (Connect().IsEmpty()) - { - var sql = "select [name] from [sysobjects] where [type] = 'u' order by [name]; "; - var query = (Query)Query(sql); - for (int r = 0; r < query.Rows; r++) - { - var cell = query.Text(r, 0); - if (TextUtility.IsBlank(cell)) continue; - list.Add(cell); - } - query.Dispose(); - } - return list.ToArray(); - } + public override string[] TableNames() => TextColumn("select [name] from [sysobjects] where [type] = 'u' order by [name]"); /// 查询数据库实例中的所有数据库名。 - public string[] StoreNames() - { - var list = new List(); - if (Connect().IsEmpty()) - { - var sql = "select [name] from [master]..[sysdatabases] order by [name]; "; - var query = (Query)Query(sql); - for (int r = 0; r < query.Rows; r++) - { - var cell = query.Text(r, 0); - if (TextUtility.IsBlank(cell)) continue; - if (cell == "master") continue; - if (cell == "model") continue; - if (cell == "msdb") continue; - if (cell == "tempdb") continue; - list.Add(cell); - } - query.Dispose(); - } - return list.ToArray(); - } + public override string[] StoreNames() => TextColumn("select [name] from [master]..[sysdatabases] order by [name]", new string[] { "master", "model", "msdb", "tempdb" }); /// 查询表中的所有列名。 - public string[] ColumnNames(string tableName) - { - var list = new List(); - if (Connect().IsEmpty()) - { - var table = TextUtility.AntiInject(tableName); - var sql = TextUtility.Merge("select [name] from [syscolumns] where [id] = object_id('", table, "'); "); - var query = (Query)Query(sql); - for (int r = 0; r < query.Rows; r++) - { - var cell = query.Text(r, 0); - if (TextUtility.IsBlank(cell)) continue; - list.Add(cell); - } - query.Dispose(); - } - return list.ToArray(); - } - - /// 创建数据库,返回错误信息。 - /// 数据库名称。 - /// 日志文件的最大 MB 数,指定非正数将不限制增长。 - /// 成功时候返回 NULL 值,失败时返回错误信息。 - public string CreateStore(string name, int logMaxSizeMB = 1024) - { - var store = name.Escape().ToTrim(); - if (store.IsEmpty()) return "创建失败:未指定数据库名称。"; - if (ConnectionString.IsEmpty()) return "创建失败:未指定数据库连接方式。"; - - using (var source = new SqlClient(ConnectionString)) - { - var connect = source.Connect(); - if (connect.NotEmpty()) return "创建失败:" + connect; - - var schema = source.SimpleCell("select default_schema_name from sys.database_principals where type = 'S' and name=user_name()"); - if (schema.IsEmpty()) return "创建失败:无法获取默认模式名称。"; - - var refPath = source.SimpleCell(@"select f.physical_name path from sys.filegroups g left join sys.database_files f on f.data_space_id = g.data_space_id where g.name = 'PRIMARY' and g.type = 'FG' and g.is_default = 1 and g.filegroup_guid is null"); - if (refPath.IsEmpty()) return "创建失败:无法获取文件路径。"; - - var dir = Path.GetDirectoryName(refPath); - var mdfPath = Path.Combine(dir, store) + ".mdf"; - var ldfPath = Path.Combine(dir, store) + "_log.ldf"; - - // 创建库。 - var maxLog = logMaxSizeMB > 0 ? $"{logMaxSizeMB}MB" : "UNLIMITED"; - var sql1 = $@" -CREATE DATABASE [{store}] -ON PRIMARY -( - NAME = N'{store}', - FILENAME = N'{mdfPath}', - SIZE = 4MB, - MAXSIZE = UNLIMITED, - FILEGROWTH = 4MB -) -LOG ON -( - NAME = N'{store}_log', - FILENAME = N'{ldfPath}', - SIZE = 1MB, - MAXSIZE = {maxLog}, - FILEGROWTH = 1MB -) -COLLATE Chinese_PRC_CI_AS -"; - var create = source.Execute(sql1, null, false); - if (!create.Success) return TextUtility.Merge("创建失败:", create.Message); - - // 设置兼容性级别。 - var sql2 = $"alter database [{store}] set compatibility_level = 0"; - source.Execute(sql2, null, false); - - // 设置恢复默认为“简单” - var sql3 = $"alter database [{store}] set recovery simple"; - source.Execute(sql3, null, false); - - return null; - } - } - - static string XType(int xtype) - { - switch (xtype) - { - case 34: return "image"; - case 35: return "text"; - case 36: return "uniqueidentifier"; - case 48: return "tinyint"; - case 52: return "smallint"; - case 56: return "int"; - case 58: return "smalldatetime"; - case 59: return "real"; - case 60: return "money"; - case 61: return "datetime"; - case 62: return "float"; - case 98: return "sql_variant"; - case 99: return "ntext"; - case 104: return "bit"; - case 106: return "decimal"; - case 108: return "numeric"; - case 122: return "smallmoney"; - case 127: return "bigint"; - case 165: return "varbinary"; - case 167: return "varchar"; - case 173: return "binary"; - case 175: return "char"; - case 189: return "timestamp"; - case 231: return "nvarchar"; - case 239: return "nchar"; - case 241: return "xml"; - } - return null; - } - - /// 获取列信息。 - public ColumnInfo[] ColumnsInfo(string tableName) - { - if (tableName.IsEmpty()) throw new ArgumentNullException(nameof(tableName)); - var sql = $"select name, xtype, length from syscolumns where id = object_id('{tableName}') "; - using (var query = Query(sql)) - { - var ab = new ArrayBuilder(); - for (var i = 0; i < query.Rows; i++) - { - var info = new ColumnInfo(); - info.Name = query.Text(i, "name"); - info.Type = XType(query.Int32(i, "xtype")); - info.Length = query.Int32(i, "length"); - ab.Add(info); - } - return ab.Export(); - } - } - - /// 创建表,当表不存在时创建表,当现存表中缺少模型中属性对应的列时增加列。成功时返回空字符串,发生异常时返回异常信息。 - public string Initialize() where T : class, new() => Initialize(typeof(T)); + public override string[] ColumnNames(string tableName) => TextColumn($"select [name] from [syscolumns] where [id] = object_id('{TextUtility.AntiInject(tableName)}')"); /// 创建表,当表不存在时创建表,当现存表中缺少模型中属性对应的列时增加列。成功时返回空字符串,发生异常时返回异常信息。 - public string Initialize(Type model) + protected override string Initialize(TableStructure structure) { - var structure = TableStructure.Parse(model); - if (structure == null) return "无法解析记录模型。"; - - // 连接数据库。 - var connect = Connect(); - if (connect.NotEmpty()) return connect; + var model = structure.Model; // 检查现存表。 var exists = false; @@ -617,7 +148,7 @@ COLLATE Chinese_PRC_CI_AS } /// 插入记录。返回错误信息。 - public string Insert(object record, string table = null) + public override string Insert(object record, string table = null) { if (record == null) return "参数无效。"; FixProperties(record); @@ -658,8 +189,8 @@ COLLATE Chinese_PRC_CI_AS } /// 更新记录,实体中的 Key 属性不被更新。返回错误信息。 - /// 无法更新带有 Independent 特性的模型(缺少 Key 属性)。 - public string Update(IRecord record, string table = null) + /// 不更新带有 Independent 特性的模型(缺少 Key 属性)。 + public override string Update(IRecord record, string table = null) { if (record == null) return "参数无效。"; FixProperties(record); @@ -691,69 +222,198 @@ COLLATE Chinese_PRC_CI_AS items.Add(TextUtility.Merge("[", pn, "] = @", pn)); } var key = record.Key.SafeKey(); - var sql = TextUtility.Merge("update [", table, "] set ", string.Join(", ", items.ToArray()), " where [_key]='", key, "'; "); + var sql = $"update [{table}] set {string.Join(", ", items.ToArray())} where [{structure.Key.Field}]='{key}'"; var execute = Execute(sql, ps); if (execute.Success) return TextUtility.Empty; return execute.Message; } - /// 获取按指定语句查询到的所有记录。 - public Result Query(Type model, string sql, IEnumerable parameters = null) => SourceUtility.Query(this, model, sql, parameters); + /// + protected override string NewConnectionString() => _connstr; - /// 获取按指定语句查询到的所有记录。 - public Result Query(string sql, IEnumerable parameters = null) where T : class, new() - { - var query = Query(sql, parameters); - if (!query.Success) return new Result(query.Message); - var records = query.Fill(); - query.Dispose(); + /// + protected override IDataAdapter CreateDataAdapter(IDbCommand command) => new SqlDataAdapter((SqlCommand)command); + + /// + protected override IDbConnection CreateConnection() => new SqlConnection(); - var result = new Result(records); - return result; + /// + protected override IDataParameter CreateParameter() => new SqlParameter(); + + /// + protected override string KeysSql(string tableName, string keyField, string flagField, long flagValue) + { + if (flagValue == 0) return $"select [{keyField}] from [{tableName}]"; + else return $"select [{keyField}] from [{tableName}] where [{flagField}] = {flagValue}"; } - /// 获取记录。 - public Result Query(Type model, long flag = 0) => SourceUtility.Query(this, model, (tn) => + /// + protected override string RecordSql(string tableName, string keyField, string keyValue, string flagField, long flagValue) { - if (flag == 0) return $"select * from [{tn}]; "; - return $"select * from [{tn}] where _flag={flag}; "; - }); + if (flagValue == 0) return $"select top 1 * from [{tableName}] where [{keyField}] = '{keyValue}'"; + else return $"select top 1 * from [{tableName}] where [{keyField}] = '{keyValue}' and [{flagField}] = {flagValue}"; + } - /// 获取记录。 - public Result Query(long flag = 0) where T : class, IRecord, new() => SourceUtility.Query(this, (tn) => + /// + protected override string RecordsSql(string tableName, string flagField, long flagValue) { - if (flag == 0) return $"select * from [{tn}]; "; - return $"select * from [{tn}] where _flag={flag}; "; - }); + if (flagValue == 0) return $"select * from [{tableName}]"; + else return $"select * from [{tableName}] where [{flagField}] = {flagValue}"; + } + + #endregion + + #region special - /// 获取具有指定 Key 的记录。 - public Result Get(Type model, string key, long flag = 0) => SourceUtility.Get(this, model, key, (tn, sk) => + /// 获取列信息。 + public ColumnInfo[] ColumnsInfo(string tableName) { - if (flag == 0) return $"select top 1 * from [{tn}] _key='{sk}'; "; - return $"select top 1 * from [{tn}] where _key='{sk}' and _key='{sk}'; "; - }); + if (tableName.IsEmpty()) throw new ArgumentNullException(nameof(tableName)); + var sql = $"select name, xtype, length from syscolumns where id = object_id('{tableName}') "; + using (var query = Query(sql)) + { + var ab = new ArrayBuilder(); + for (var i = 0; i < query.Rows; i++) + { + var info = new ColumnInfo(); + info.Name = query.Text(i, "name"); + info.Type = XType(query.Int32(i, "xtype")); + info.Length = query.Int32(i, "length"); + ab.Add(info); + } + return ab.Export(); + } + } - /// 获取具有指定 Key 的记录。 - public Result Get(string key, long flag = 0) where T : class, IRecord, new() => SourceUtility.Get(this, key, (tn, sk) => + /// 批量插入,必须在 DataTable 中指定表名,或指定 tableName 参数。 + /// + /// + public void BulkCopy(DataTable table, string tableName = null) { - if (flag == 0) return $"select top 1 * from [{tn}] _key='{sk}'; "; - return $"select top 1 * from [{tn}] where _key='{sk}' and _key='{sk}'; "; - }); + // 检查 table 参数。 + if (table == null) throw new ArgumentNullException(nameof(table)); + if (table.Rows.Count < 1) return; + + // 检查 tableName 参数。 + if (tableName.IsEmpty()) tableName = table.TableName; + if (tableName.IsEmpty()) throw new ArgumentException("未指定表名。"); + + // 连接数据库。 + var connect = Connect(); + if (connect.NotEmpty()) throw new Exception(connect); - /// 查询有效的 Key 值。 - public Result Keys(Type model, long flag = 0) => SourceUtility.Keys(this, model, (tn) => + // 批量插入。 + var bc = null as SqlBulkCopy; + try + { + bc = new SqlBulkCopy((SqlConnection)Connection); + bc.DestinationTableName = tableName; + bc.BatchSize = table.Rows.Count; + bc.WriteToServer(table); + } + catch (Exception ex) + { + try { bc.Close(); } catch { } + throw ex; + } + } + + /// 创建数据库,返回错误信息。 + /// 数据库名称。 + /// 日志文件的最大 MB 数,指定非正数将不限制增长。 + /// 成功时候返回 NULL 值,失败时返回错误信息。 + public string CreateStore(string name, int logMaxSizeMB = 1024) { - if (flag == 0) return $"select _key from [{tn}]; "; - return $"select _key from [{tn}] where _flag={flag}; "; - }); + var store = name.Escape().ToTrim(); + if (store.IsEmpty()) return "创建失败:未指定数据库名称。"; + if (ConnectionString.IsEmpty()) return "创建失败:未指定数据库连接方式。"; - /// 查询有效的 Key 值。 - public Result Keys(long flag = 0) where T : class, IRecord, new() => Keys(typeof(T), flag); + using (var source = new SqlClient(ConnectionString)) + { + var connect = source.Connect(); + if (connect.NotEmpty()) return "创建失败:" + connect; - #endregion + var schema = source.SimpleCell("select default_schema_name from sys.database_principals where type = 'S' and name=user_name()"); + if (schema.IsEmpty()) return "创建失败:无法获取默认模式名称。"; - #region public static + var refPath = source.SimpleCell(@"select f.physical_name path from sys.filegroups g left join sys.database_files f on f.data_space_id = g.data_space_id where g.name = 'PRIMARY' and g.type = 'FG' and g.is_default = 1 and g.filegroup_guid is null"); + if (refPath.IsEmpty()) return "创建失败:无法获取文件路径。"; + + var dir = Path.GetDirectoryName(refPath); + var mdfPath = Path.Combine(dir, store) + ".mdf"; + var ldfPath = Path.Combine(dir, store) + "_log.ldf"; + + // 创建库。 + var maxLog = logMaxSizeMB > 0 ? $"{logMaxSizeMB}MB" : "UNLIMITED"; + var sql1 = $@" +CREATE DATABASE [{store}] +ON PRIMARY +( + NAME = N'{store}', + FILENAME = N'{mdfPath}', + SIZE = 4MB, + MAXSIZE = UNLIMITED, + FILEGROWTH = 4MB +) +LOG ON +( + NAME = N'{store}_log', + FILENAME = N'{ldfPath}', + SIZE = 1MB, + MAXSIZE = {maxLog}, + FILEGROWTH = 1MB +) +COLLATE Chinese_PRC_CI_AS +"; + var create = source.Execute(sql1, null, false); + if (!create.Success) return TextUtility.Merge("创建失败:", create.Message); + + // 设置兼容性级别。 + var sql2 = $"alter database [{store}] set compatibility_level = 0"; + source.Execute(sql2, null, false); + + // 设置恢复默认为“简单” + var sql3 = $"alter database [{store}] set recovery simple"; + source.Execute(sql3, null, false); + + return null; + } + } + + static string XType(int xtype) + { + switch (xtype) + { + case 34: return "image"; + case 35: return "text"; + case 36: return "uniqueidentifier"; + case 48: return "tinyint"; + case 52: return "smallint"; + case 56: return "int"; + case 58: return "smalldatetime"; + case 59: return "real"; + case 60: return "money"; + case 61: return "datetime"; + case 62: return "float"; + case 98: return "sql_variant"; + case 99: return "ntext"; + case 104: return "bit"; + case 106: return "decimal"; + case 108: return "numeric"; + case 122: return "smallmoney"; + case 127: return "bigint"; + case 165: return "varbinary"; + case 167: return "varchar"; + case 173: return "binary"; + case 175: return "char"; + case 189: return "timestamp"; + case 231: return "nvarchar"; + case 239: return "nchar"; + case 241: return "xml"; + } + return null; + } #if NET20 || NET40 @@ -777,22 +437,6 @@ COLLATE Chinese_PRC_CI_AS #endif - /// 指定的连接凭据是否符合连接要求,默认指定 master 数据库。 - public static bool Proven(string address, string user, string pass) => Proven(address, "master", user, pass); - - /// 指定的连接凭据是否符合连接要求。 - public static bool Proven(string address, string store, string user, string pass) - { - var a = string.IsNullOrEmpty(address); - var s = string.IsNullOrEmpty(store); - var u = string.IsNullOrEmpty(user); - var p = string.IsNullOrEmpty(pass); - if (a) return false; - if (s) return false; - if (u && !p) return false; - return true; - } - /// 创建参数。 /// /// diff --git a/Apewer.Source/Source/SqlClientThin.cs b/Apewer.Source/Source/SqlClientThin.cs deleted file mode 100644 index 1e2ba9f..0000000 --- a/Apewer.Source/Source/SqlClientThin.cs +++ /dev/null @@ -1,528 +0,0 @@ -#if DEBUG - -/* 2021.11.28 */ - -using Apewer; -using System; -using System.Collections.Generic; -using System.Data; -using System.Data.Common; -using System.Text; - -using static Apewer.Source.SourceUtility; -using System.Data.SqlClient; - -#if NETFRAMEWORK -using System.Data.Sql; -#endif - -namespace Apewer.Source -{ - - /// - [Serializable] - - public class SqlClientThin : DbClient, IDbClient - { - - #region - - string _str = null; - - /// 使用连接字符串创建数据库连接实例。 - public SqlClientThin(string connectionString, Timeout timeout = null) : base(timeout) - { - _str = connectionString ?? ""; - } - - /// 使用连接凭据创建数据库连接实例。 - public SqlClientThin(string address, string store, string user, string pass, Timeout timeout = null) : base(timeout) - { - if (timeout == null) timeout = Timeout.Default; - - var a = address ?? ""; - var s = store ?? ""; - var u = user ?? ""; - var p = pass ?? ""; - var cs = $"data source = {a}; initial catalog = {s}; "; - if (string.IsNullOrEmpty(u)) cs += "integrated security = sspi; "; - else - { - cs += $"user id = {u}; "; - if (!string.IsNullOrEmpty(p)) cs += $"password = {p}; "; - } - cs += $"connection timeout = {timeout.Connect}; "; - - _str = cs; - } - - /// 为 Ado 创建连接字符串。 - protected override string NewConnectionString() => _str; - - /// 为 Ado 创建 IDbConnection 对象。 - protected override IDbConnection NewConnection() => new SqlConnection(); - - /// 为 Ado 创建 IDbCommand 对象。 - protected override IDbCommand NewCommand() => new SqlCommand(); - - /// 为 Ado 创建 IDataAdapter 对象。 - protected override IDataAdapter NewDataAdapter(IDbCommand command) => new SqlDataAdapter((SqlCommand)command); - - #endregion - - #region ORM - - /// 查询数据库中的所有表名。 - public override string[] TableNames() => TextColumn("select [name] from [sysobjects] where [type] = 'u' order by [name]; "); - - /// 查询数据库实例中的所有数据库名。 - public override string[] StoreNames() => TextColumn("select [name] from [master]..[sysdatabases] order by [name]; ", new string[] { "master", "model", "msdb", "tempdb" }); - - /// 查询表中的所有列名。 - public override string[] ColumnNames(string tableName) => TextColumn($"select [name] from [syscolumns] where [id] = object_id('{tableName}'); "); - - /// 获取列信息。 - public override ColumnInfo[] ColumnsInfo(string tableName) - { - if (tableName.IsEmpty()) throw new ArgumentNullException(nameof(tableName)); - var sql = $"select name, xtype, length from syscolumns where id = object_id('{tableName}') "; - using (var query = Query(sql)) - { - var ab = new ArrayBuilder(); - for (var i = 0; i < query.Rows; i++) - { - var info = new ColumnInfo(); - info.Name = query.Text(i, "name"); - info.Type = XType(query.Int32(i, "xtype")); - info.Length = query.Int32(i, "length"); - ab.Add(info); - } - return ab.Export(); - } - } - - /// 创建表,当表不存在时创建表,当现存表中缺少模型中属性对应的列时增加列。成功时返回空字符串,发生异常时返回异常信息。 - public override string Initialize(Type model) - { - var structure = TableStructure.Parse(model); - if (structure == null) return "无法解析记录模型。"; - - // 连接数据库。 - var connect = Connect(); - if (connect.NotEmpty()) return connect; - - // 检查现存表。 - var exists = false; - var tables = TableNames(); - if (tables.Length > 0) - { - var lower = structure.Name.ToLower(); - foreach (var table in tables) - { - if (TextUtility.IsBlank(table)) continue; - if (table.ToLower() == lower) - { - exists = true; - break; - } - } - } - - if (exists) - { - // 获取已存在的列名。 - var columns = ColumnNames(structure.Name); - if (columns.Length > 0) - { - var lower = new List(); - foreach (var column in columns) - { - if (TextUtility.IsBlank(column)) continue; - lower.Add(column.ToLower()); - } - columns = lower.ToArray(); - } - - // 增加列。 - foreach (var column in structure.Columns) - { - // 检查 Independent 特性。 - if (structure.Independent && column.Independent) continue; - - // 去重。 - var lower = column.Field.ToLower(); - if (columns.Contains(lower)) continue; - - var type = Declaration(column); - if (string.IsNullOrEmpty(type)) return TextUtility.Merge("类型 ", column.Type.ToString(), " 不受支持。"); - - var sql = TextUtility.Merge("alter table [", structure.Name, "] add ", type, "; "); - var execute = Execute(sql); - if (execute.Success == false) return execute.Message; - } - return TextUtility.Empty; - } - else - { - var sqlcolumns = new List(); - foreach (var column in structure.Columns) - { - // 检查 Independent 特性。 - if (structure.Independent && column.Independent) continue; - - var type = Declaration(column); - if (!column.Independent && column.Property.Name == "Key") type = type + " primary key"; - - if (string.IsNullOrEmpty(type)) return TextUtility.Merge("类型 ", column.Type.ToString(), " 不受支持。"); - sqlcolumns.Add(type); - } - var sql = TextUtility.Merge("create table [", structure.Name, "](", string.Join(", ", sqlcolumns.ToArray()), "); "); - var execute = Execute(sql); - if (execute.Success) return TextUtility.Empty; - return execute.Message; - } - } - - /// 插入记录。返回错误信息。 - public override string Insert(object record, string table = null) - { - if (record == null) return "参数无效。"; - FixProperties(record); - - var structure = TableStructure.Parse(record.GetType()); - if (structure == null) return "无法解析记录模型。"; - if (string.IsNullOrEmpty(table)) table = structure.Name; - if (string.IsNullOrEmpty(table)) return "表名称无效。"; - - var ps = structure.CreateParameters(record, Parameter, null); - var psc = ps.Length; - if (psc < 1) return "数据模型不包含字段。"; - - var names = new List(psc); - var values = new List(psc); - foreach (var column in ps) - { - //names.Add(TextGenerator.Merge("[", column, "]")); - names.Add(TextUtility.Merge(column)); - values.Add("@" + column); - } - var sb = new StringBuilder(); - sb.Append("insert into [", table, "](", string.Join(", ", names.ToArray()), ") "); - sb.Append("values(", string.Join(", ", values.ToArray()), "); "); - var sql = sb.ToString(); - - var execute = Execute(sql, ps); - if (execute.Success) return TextUtility.Empty; - return execute.Message; - } - - /// 更新记录,实体中的 Key 属性不被更新。返回错误信息。 - /// 无法更新带有 Independent 特性的模型(缺少 Key 属性)。 - public override string Update(IRecord record, string table = null) - { - if (record == null) return "参数无效。"; - FixProperties(record); - SetUpdated(record); - - var structure = TableStructure.Parse(record.GetType()); - if (structure == null) return "无法解析记录模型。"; - if (structure.Independent) return "无法更新带有 Independent 特性的模型。"; - if (string.IsNullOrEmpty(table)) table = structure.Name; - if (string.IsNullOrEmpty(table)) return "表名称无效。"; - - var ps = structure.CreateParameters(record, Parameter, null); - var psc = ps.Length; - if (psc < 1) return "数据模型不包含字段。"; - - var items = new List(); - foreach (var p in ps) - { - var pn = p.ParameterName; - items.Add(TextUtility.Merge("[", pn, "] = @", pn)); - } - var key = record.Key.SafeKey(); - var sql = TextUtility.Merge("update [", table, "] set ", string.Join(", ", items.ToArray()), " where [_key]='", key, "'; "); - - var execute = Execute(sql, ps); - if (execute.Success) return TextUtility.Empty; - return execute.Message; - } - - /// 获取记录。 - public override Result Query(Type model, long flag = 0) => SourceUtility.Query(this, model, (tn) => - { - if (flag == 0) return $"select * from [{tn}]; "; - return $"select * from [{tn}] where _flag={flag}; "; - }); - - /// 获取记录。 - public override Result Query(long flag = 0) => SourceUtility.Query(this, (tn) => - { - if (flag == 0) return $"select * from [{tn}]; "; - return $"select * from [{tn}] where _flag={flag}; "; - }); - - /// 获取具有指定 Key 的记录。 - public override Result Get(Type model, string key, long flag = 0) => SourceUtility.Get(this, model, key, (tn, sk) => - { - if (flag == 0) return $"select top 1 * from [{tn}] _key='{sk}'; "; - return $"select top 1 * from [{tn}] where _key='{sk}' and _key='{sk}'; "; - }); - - /// 获取具有指定 Key 的记录。 - public override Result Get(string key, long flag = 0) => SourceUtility.Get(this, key, (tn, sk) => - { - if (flag == 0) return $"select top 1 * from [{tn}] _key='{sk}'; "; - return $"select top 1 * from [{tn}] where _key='{sk}' and _key='{sk}'; "; - }); - - /// 查询有效的 Key 值。 - public override Result Keys(Type model, long flag = 0) => SourceUtility.Keys(this, model, (tn) => - { - if (flag == 0) return $"select _key from [{tn}]; "; - return $"select _key from [{tn}] where _flag={flag}; "; - }); - - /// 查询有效的 Key 值。 - public override Result Keys(long flag = 0) => Keys(typeof(T), flag); - - #endregion - - #region public static - -#if NET20 || NET40 - - /// 枚举本地网络中服务器的名称。 - public static SqlServerSource[] EnumerateServer() - { - var list = new List(); - - // 表中列名:ServerName、InstanceName、IsClustered、Version。 - using (var query = new Query(SqlDataSourceEnumerator.Instance.GetDataSources())) - { - for (int i = 0; i < query.Rows; i++) - { - var item = new SqlServerSource(); - item.ServerName = query.Text(i, "ServerName"); - list.Add(item); - } - } - return list.ToArray(); - } - -#endif - - /// 指定的连接凭据是否符合连接要求,默认指定 master 数据库。 - public static bool Proven(string address, string user, string pass) => Proven(address, "master", user, pass); - - /// 指定的连接凭据是否符合连接要求。 - public static bool Proven(string address, string store, string user, string pass) - { - var a = string.IsNullOrEmpty(address); - var s = string.IsNullOrEmpty(store); - var u = string.IsNullOrEmpty(user); - var p = string.IsNullOrEmpty(pass); - if (a) return false; - if (s) return false; - if (u && !p) return false; - return true; - } - - /// 创建参数。 - /// - /// - static SqlParameter Parameter(Parameter parameter) - { - if (parameter == null) throw new InvalidOperationException("参数无效。"); - return Parameter(parameter.Name, parameter.Type, parameter.Size, parameter.Value); - } - - /// 创建参数。 - public static SqlParameter Parameter(string name, ColumnType type, int size, object value) - { - var vname = TextUtility.Trim(name); - if (TextUtility.IsBlank(vname)) return null; - - var vtype = SqlDbType.BigInt; - switch (type) - { - case ColumnType.Bytes: - vtype = SqlDbType.Image; - break; - case ColumnType.Integer: - vtype = SqlDbType.BigInt; - break; - case ColumnType.Float: - vtype = SqlDbType.Float; - break; - case ColumnType.DateTime: - vtype = SqlDbType.DateTime; - break; - case ColumnType.VarChar: - case ColumnType.VarChar191: - case ColumnType.VarCharMax: - vtype = SqlDbType.VarChar; - break; - case ColumnType.NVarChar: - case ColumnType.NVarChar191: - case ColumnType.NVarCharMax: - vtype = SqlDbType.NVarChar; - break; - case ColumnType.Text: - vtype = SqlDbType.Text; - break; - case ColumnType.NText: - vtype = SqlDbType.NText; - break; - default: - throw new InvalidOperationException(TextUtility.Merge("类型 ", type.ToString(), " 不受支持。")); - } - - var vsize = size; - switch (type) - { - case ColumnType.VarChar: - vsize = NumberUtility.Restrict(vsize, 0, 8000); - break; - case ColumnType.NVarChar: - vsize = NumberUtility.Restrict(vsize, 0, 4000); - break; - case ColumnType.VarChar191: - case ColumnType.NVarChar191: - vsize = NumberUtility.Restrict(vsize, 0, 191); - break; - default: - vsize = 0; - break; - } - - var vvalue = value; - if (vvalue is string && vvalue != null && vsize > 0) - { - vvalue = TextUtility.Left((string)vvalue, vsize); - } - - var parameter = new SqlParameter(); - parameter.ParameterName = vname; - parameter.SqlDbType = vtype; - parameter.Value = vvalue; - if (vsize > 0) parameter.Size = vsize; - return parameter; - } - - /// 创建参数。 - public static SqlParameter Parameter(string name, SqlDbType type, int size, object value) - { - if (value is string && value != null && size > 0) - { - value = TextUtility.Left((string)value, (int)size); - } - - var p = new SqlParameter(); - p.ParameterName = name ?? ""; - p.SqlDbType = type; - p.Size = size; - p.Value = value; - return p; - } - - /// 创建参数。 - public static SqlParameter Parameter(string name, SqlDbType type, object value) - { - var p = new SqlParameter(); - p.ParameterName = name ?? ""; - p.SqlDbType = type; - p.Value = value; - return p; - } - - static string Declaration(ColumnAttribute column) - { - var type = TextUtility.Empty; - var vcolumn = column; - var length = Math.Max(0, vcolumn.Length); - switch (vcolumn.Type) - { - case ColumnType.Integer: - type = "bigint"; - break; - case ColumnType.Float: - type = "float"; - break; - case ColumnType.Bytes: - type = "image"; - break; - case ColumnType.DateTime: - type = "datetime"; - break; - case ColumnType.VarChar: - type = TextUtility.Merge("varchar(", Math.Min(8000, length).ToString(), ")"); - break; - case ColumnType.VarChar191: - type = TextUtility.Merge("varchar(191)"); - break; - case ColumnType.VarCharMax: - type = TextUtility.Merge("varchar(max)"); - break; - case ColumnType.Text: - type = TextUtility.Merge("text"); - break; - case ColumnType.NVarChar: - type = TextUtility.Merge("nvarchar(", Math.Min(4000, length).ToString(), ")"); - break; - case ColumnType.NVarChar191: - type = TextUtility.Merge("nvarchar(191)"); - break; - case ColumnType.NVarCharMax: - type = TextUtility.Merge("nvarchar(max)"); - break; - case ColumnType.NText: - type = TextUtility.Merge("ntext"); - break; - default: - return TextUtility.Empty; - } - return TextUtility.Merge("[", vcolumn.Field, "] ", type); - } - - static string XType(int xtype) - { - switch (xtype) - { - case 34: return "image"; - case 35: return "text"; - case 36: return "uniqueidentifier"; - case 48: return "tinyint"; - case 52: return "smallint"; - case 56: return "int"; - case 58: return "smalldatetime"; - case 59: return "real"; - case 60: return "money"; - case 61: return "datetime"; - case 62: return "float"; - case 98: return "sql_variant"; - case 99: return "ntext"; - case 104: return "bit"; - case 106: return "decimal"; - case 108: return "numeric"; - case 122: return "smallmoney"; - case 127: return "bigint"; - case 165: return "varbinary"; - case 167: return "varchar"; - case 173: return "binary"; - case 175: return "char"; - case 189: return "timestamp"; - case 231: return "nvarchar"; - case 239: return "nchar"; - case 241: return "xml"; - } - return null; - } - - #endregion - - } - -} - -#endif diff --git a/Apewer.Source/Source/Sqlite.cs b/Apewer.Source/Source/Sqlite.cs index e623691..edbbe19 100644 --- a/Apewer.Source/Source/Sqlite.cs +++ b/Apewer.Source/Source/Sqlite.cs @@ -14,418 +14,40 @@ namespace Apewer.Source { /// 用于快速连接 SQLite 数据库的辅助。 - public class Sqlite : IDbClient + public class Sqlite : DbClient { #region 基础 - private Timeout _timeout = null; - private string _connstring = ""; - private string _path = ""; - private string _pass = ""; - - private object _locker = new object(); - - /// 获取或设置日志记录。 - public Logger Logger { get; set; } - - /// 超时设定。 - public Timeout Timeout { get => _timeout; } + private string _connstr = null; + private string _path = null; + private string _pass = null; /// 创建连接实例。 /// 注意:
- 构造函数不会创建不存在的文件;
- 参数 path 为文件路径,指定为空时将使用 :memory: 作为路径连接内存。
- public Sqlite(string path = null, string pass = null, Timeout timeout = null) + public Sqlite(string path = null, string pass = null, Timeout timeout = null) : base(timeout) { - _timeout = timeout ?? Timeout.Default; _path = path.IsEmpty() ? Memory : path; _pass = pass; - if (pass.IsEmpty()) _connstring = $"data source='{_path}'; version=3; "; - else _connstring = $"data source='{_path}'; password={_pass}; version=3; "; - } - - #endregion - - #region 连接 - - private SQLiteConnection _db = null; - - /// 数据库已经连接。 - public bool Online { get => _db != null && _db.State == ConnectionState.Open; } - - /// 连接字符串。 - public string ConnectionString { get => _connstring; } - - /// 获取当前的 SQLiteConnection 对象。 - public IDbConnection Connection { get => _db; } - - /// 连接数据库,若未连接则尝试连接。 - /// 是否已连接。 - public string Connect() - { - if (_db == null) - { - _db = new SQLiteConnection(); - _db.ConnectionString = ConnectionString; - } - else - { - if (_db.State == ConnectionState.Open) return null; - } - try - { - _db.Open(); - switch (_db.State) - { - case ConnectionState.Open: return null; - default: return $"连接失败,当前处于 {_db.State} 状态。"; - } - } - catch (Exception ex) - { - Logger.Error(nameof(Sqlite), "Connection", ex, _db.ConnectionString); - Close(); - return ex.Message; - } - } - - /// 关闭连接,并释放对象所占用的系统资源。 - public void Close() - { - if (_db != null) - { - if (_transaction != null) - { - if (_autocommit) Commit(); - else Rollback(); - } - lock (_locker) - { - _db.Dispose(); - } - _db = null; - } - } - - /// 关闭连接,释放对象所占用的系统资源,并清除连接信息。 - public void Dispose() { Close(); } - - #endregion - - #region Transaction - - private IDbTransaction _transaction = null; - private bool _autocommit = false; - - /// 启动事务。 - public string Begin(bool commit = true) => Begin(commit, null); - - /// 启动事务。 - public string Begin(bool commit, Class isolation) - { - var connect = Connect(); - if (connect.NotEmpty()) return connect; - if (_transaction != null) return "存在已启动的事务,无法再次启动。"; - try - { - _transaction = isolation ? _db.BeginTransaction(isolation.Value) : _db.BeginTransaction(); - _autocommit = commit; - return null; - } - catch (Exception ex) - { - Logger.Error(nameof(Sqlite), "Begin", ex.Message()); - return ex.Message(); - } - } - - /// 提交事务。 - public string Commit() - { - if (_transaction == null) return "事务不存在。"; - try - { - _transaction.Commit(); - RuntimeUtility.Dispose(_transaction); - _transaction = null; - return null; - } - catch (Exception ex) - { - RuntimeUtility.Dispose(_transaction); - _transaction = null; - Logger.Error(nameof(Sqlite), "Commit", ex.Message()); - return ex.Message(); - } - } - - /// 从挂起状态回滚事务。 - public string Rollback() - { - if (_transaction == null) return "事务不存在。"; - try - { - _transaction.Rollback(); - RuntimeUtility.Dispose(_transaction); - _transaction = null; - return null; - } - catch (Exception ex) - { - RuntimeUtility.Dispose(_transaction); - _transaction = null; - Logger.Error(nameof(Sqlite), "Rollback", ex.Message()); - return ex.Message(); - } - } - - #endregion - - #region SQL - - /// 查询。 - public IQuery Query(string sql) => Query(sql, null); - - /// 查询。 - public IQuery Query(string sql, IEnumerable parameters) - { - if (string.IsNullOrEmpty(sql)) return Example.InvalidQueryStatement; - - var connected = Connect(); - if (connected.NotEmpty()) return new Query(false, connected); - - var query = new Query(); - try - { - using (var command = new SQLiteCommand()) - { - command.Connection = _db; - command.CommandTimeout = _timeout.Query; - command.CommandText = sql; - if (parameters != null) - { - foreach (var p in parameters) - { - if (p != null) command.Parameters.Add(p); - } - } - using (var dataset = new DataSet()) - { - using (var da = new SQLiteDataAdapter(command)) - { - var name = "table_" + Guid.NewGuid().ToString("n"); - da.Fill(dataset, name); - var table = dataset.Tables[name]; - return new Query(table); - } - } - } - } - catch (Exception ex) - { - Logger.Error(nameof(Sqlite), "Query", ex, sql); - return new Query(ex); - } - } - - /// 执行单条 Transact-SQL 语句。 - public IExecute Execute(string sql) => Execute(sql, null); - - /// 执行单条 Transact-SQL 语句,并加入参数。 - public IExecute Execute(string sql, IEnumerable parameters) - { - if (string.IsNullOrEmpty(sql)) return Example.InvalidExecuteStatement; - - var connected = Connect(); - if (connected.NotEmpty()) return new Execute(false, connected); - - lock (_locker) - { - var inTransaction = _transaction != null; - if (!inTransaction) Begin(); - try - { - using (var command = new SQLiteCommand()) - { - command.Connection = _db; - command.Transaction = (SQLiteTransaction)_transaction; - command.CommandTimeout = _timeout.Execute; - command.CommandText = sql; - if (parameters != null) - { - foreach (var p in parameters) - { - if (p != null) command.Parameters.Add(p); - } - } - var rows = command.ExecuteNonQuery(); - if (!inTransaction) Commit(); // todo 此处应该检查事务提交产生的错误。 - return new Execute(true, rows); - } - } - catch (Exception ex) - { - Logger.Error(nameof(Sqlite), "Execute", ex, sql); - if (!inTransaction) Rollback(); - return new Execute(ex); - } - } - } - - #endregion - - #region 属性。 - - /// 保存当前数据库到文件,若文件已存在则将重写文件。 - public bool Save(string path, string pass = null) - { - if (!StorageUtility.CreateFile(path, 0, true)) - { - Logger.Error(nameof(Sqlite), "Save", TextUtility.Merge("创建文件 ", path, " 失败。")); - return false; - } - - using (var destination = new Sqlite(path, pass)) return Save(destination); + if (pass.IsEmpty()) _connstr = $"data source='{_path}'; version=3; "; + else _connstr = $"data source='{_path}'; password={_pass}; version=3; "; } - /// 保存当前数据库到目标数据库。 - public bool Save(Sqlite destination) => string.IsNullOrEmpty(Backup(this, destination)); - - /// 加载文件到当前数据库。 - public bool Load(string path, string pass = null) - { - using (var source = new Sqlite(path, pass)) return Load(source); - } - - /// 加载源数据库到当前数据库。 - public bool Load(Sqlite source) => string.IsNullOrEmpty(Backup(source, this)); - #endregion - #region ORM - - /// 查询数据库中的所有表名。 - public string[] TableNames() - { - var list = new List(); - if (Connect().IsEmpty()) - { - var sql = "select name from sqlite_master where type='table' order by name; "; - var query = (Query)Query(sql); - for (int r = 0; r < query.Rows; r++) - { - var cell = query.Text(r, 0); - if (TextUtility.IsBlank(cell)) continue; - list.Add(cell); - } - query.Dispose(); - } - return list.ToArray(); - } - - /// 查询数据库中的所有视图名。 - public string[] ViewNames() - { - var list = new List(); - if (Connect().IsEmpty()) - { - var sql = "select name from sqlite_master where type='view' order by name; "; - var query = (Query)Query(sql); - for (int r = 0; r < query.Rows; r++) - { - var cell = query.Text(r, 0); - if (TextUtility.IsBlank(cell)) continue; - list.Add(cell); - } - query.Dispose(); - } - return list.ToArray(); - } + #region public - /// 查询表中的所有列名。 - public string[] ColumnNames(string table) - { - var list = new List(); - if (Connect().IsEmpty()) - { - var t = TextUtility.AntiInject(table); - var sql = TextUtility.Merge("pragma table_info('", TextUtility.AntiInject(t), "'); "); - using (var query = Query(sql) as Query) - { - for (int r = 0; r < query.Rows; r++) - { - var cell = query.Text(r, "name"); - if (TextUtility.IsBlank(cell)) continue; - list.Add(cell); - } - } - } - return list.ToArray(); - } - - /// 创建表,不修改已存在表。成功时返回空字符串,发生异常时返回异常信息。 - public string Initialize(Record model) => model == null ? "参数无效。" : Initialize(model.GetType()); - - /// 创建表,不修改已存在表。成功时返回空字符串,发生异常时返回异常信息。 - public string Initialize() where T : class, new() => Initialize(typeof(T)); - - /// 创建表,不修改已存在表。当现存表中缺少模型中属性对应的列时增加列。成功时返回空字符串,发生异常时返回异常信息。 - public string Initialize(Type model) - { - var structure = TableStructure.Parse(model); - if (structure == null) return "无法解析记录模型。"; + /// + public override string[] StoreNames() => throw new NotImplementedException(); - // 连接数据库。 - var connect = Connect(); - if (connect.NotEmpty()) return connect; + /// + public override string[] TableNames() => TextColumn("select name from sqlite_master where type='table' order by name"); - // 检查现存表。 - var exists = false; - var tables = TableNames(); - if (tables.Length > 0) - { - var lower = structure.Name.ToLower(); - foreach (var table in tables) - { - if (TextUtility.IsBlank(table)) continue; - if (table.ToLower() == lower) - { - exists = true; - break; - } - } - } - - if (exists) - { - return TextUtility.Merge("指定的表已经存在。"); - } - else - { - var sqlcolumns = new List(); - foreach (var column in structure.Columns) - { - var type = Declaration(column); - if (string.IsNullOrEmpty(type)) - { - return TextUtility.Merge("类型 ", column.Type.ToString(), " 不受支持。"); - } - - if (!column.Independent) - { - if (column.PrimaryKey) type = type + " primary key"; - } - - sqlcolumns.Add(type); - } - var sql = TextUtility.Merge("create table [", structure.Name, "](", TextUtility.Join(", ", sqlcolumns), "); "); - var execute = Execute(sql); - if (execute.Success) return TextUtility.Empty; - return execute.Message; - } - } + /// + public override string[] ColumnNames(string tableName) => TextColumn($"pragma table_info('{tableName.SafeName()}'); "); /// 插入记录。返回错误信息。 - public string Insert(object record, string table = null) + public override string Insert(object record, string table = null) { if (record == null) return "参数无效。"; SourceUtility.FixProperties(record); @@ -477,7 +99,7 @@ namespace Apewer.Source /// 更新记录,实体中的 Key 属性不被更新。返回错误信息。 /// 无法更新带有 Independent 特性的模型(缺少 Key 属性)。 - public string Update(IRecord record, string table = null) + public override string Update(IRecord record, string table = null) { if (record == null) return "参数无效。"; FixProperties(record); @@ -505,22 +127,9 @@ namespace Apewer.Source if (psc < 1) return "数据模型不包含字段。"; // 合成 SQL 语句。 - var sb = new StringBuilder(); - sb.Append("update ["); - sb.Append(table); - sb.Append("] set "); - for (var i = 0; i < psc; i++) - { - if (i > 0) sb.Append(", "); - sb.Append("["); - sb.Append(ps[i].ParameterName); - sb.Append("] = @"); - sb.Append(ps[i].ParameterName); - } - sb.Append(" where [_key] = '"); - sb.Append(record.Key.SafeKey()); - sb.Append("'; "); - var sql = sb.ToString(); + var cs = new List(); + foreach (var p in ps) cs.Add($"[{p.ParameterName}] = @{p.ParameterName}"); + var sql = $"update [{table}] set {string.Join(", ", cs.ToArray())} where [{structure.Key.Field}] = '{record.Key.SafeKey()}'"; // 执行。 var execute = Execute(sql, ps); @@ -528,93 +137,166 @@ namespace Apewer.Source return execute.Message; } - /// 获取按指定语句查询到的所有记录。 - public Result Query(Type model, string sql, IEnumerable parameters = null) => SourceUtility.Query(this, model, sql, parameters); + #endregion - /// 获取按指定语句查询到的所有记录。 - public Result Query(string sql, IEnumerable parameters = null) where T : class, new() + #region protected + + /// + protected override string Initialize(TableStructure structure) { - var query = Query(sql); - if (!query.Success) return new Result(query.Message); - var records = query.Fill(); - query.Dispose(); + // 检查现存表。 + var exists = false; + var tables = TableNames(); + if (tables.Length > 0) + { + var lower = structure.Name.ToLower(); + foreach (var table in tables) + { + if (TextUtility.IsBlank(table)) continue; + if (table.ToLower() == lower) + { + exists = true; + break; + } + } + } + + if (exists) + { + return TextUtility.Merge("指定的表已经存在。"); + } + else + { + var sqlcolumns = new List(); + foreach (var column in structure.Columns) + { + var type = Declaration(column); + if (string.IsNullOrEmpty(type)) + { + return TextUtility.Merge("类型 ", column.Type.ToString(), " 不受支持。"); + } - var result = new Result(records); - return result; + if (!column.Independent) + { + if (column.PrimaryKey) type = type + " primary key"; + } + + sqlcolumns.Add(type); + } + var sql = TextUtility.Merge("create table [", structure.Name, "](", TextUtility.Join(", ", sqlcolumns), "); "); + var execute = Execute(sql); + if (execute.Success) return TextUtility.Empty; + return execute.Message; + } } - /// 查询多条记录。 - public Result Query(Type model, long flag = 0) => SourceUtility.Query(this, model, (tn) => - { - if (flag == 0) return $"select * from [{tn}]; "; - return $"select * from [{tn}] where _flag={flag}; "; - }); + /// + protected override string NewConnectionString() => _connstr; - /// 查询多条记录。 - public Result Query(long flag = 0) where T : class, IRecord, new() => SourceUtility.Query(this, (tn) => - { - if (flag == 0) return $"select * from [{tn}]; "; - return $"select * from [{tn}] where _flag={flag}; "; - }); + /// + protected override IDataAdapter CreateDataAdapter(IDbCommand command) => new SQLiteDataAdapter((SQLiteCommand)command); + + /// + protected override IDbConnection CreateConnection() => new SQLiteConnection(); - /// 获取具有指定 Key 的记录。 - public Result Get(Type model, string key, long flag = 0) => SourceUtility.Get(this, model, key, (tn, sk) => + /// + protected override IDataParameter CreateParameter() => new SQLiteParameter(); + + /// + protected override string KeysSql(string tableName, string keyField, string flagField, long flagValue) { - if (flag == 0) return $"select * from [{tn}] where _key='{sk}' limit 1; "; - return $"select * from [{tn}] where _flag={flag} and _key='{sk}' limit 1; "; - }); + if (flagValue == 0) return $"select [{keyField}] from [{tableName}] where [{flagField}] = {flagValue}"; + return $"select [{keyField}] from [{tableName}]"; + } - /// 获取具有指定 Key 的记录。 - public Result Get(string key, long flag) where T : class, IRecord, new() => SourceUtility.Get(this, key, (tn, sk) => + /// + protected override string RecordSql(string tableName, string keyField, string keyValue, string flagField, long flagValue) { - if (flag == 0) return $"select * from [{tn}] where _key='{sk}' limit 1; "; - return $"select * from [{tn}] where _flag={flag} and _key='{sk}' limit 1; "; - }); + if (flagValue == 0) return $"select * from [{tableName}] where [{keyField}] = '{keyValue}' limit 1"; + else return $"select * from [{tableName}] where [{keyField}] = '{keyValue}' and [{flagField}] = {flagValue} limit 1"; + } - /// 获取指定类型的主键,按 Flag 属性筛选。 - public Result Keys(Type model, long flag = 0) => SourceUtility.Keys(this, model, (tn) => + /// + protected override string RecordsSql(string tableName, string flagField, long flagValue) { - if (flag == 0) return $"select _key from [{tn}] where _flag={flag}; "; - return $"select _key from [{tn}]; "; - }); + if (flagValue == 0) return $"select * from [{tableName}]"; + else return $"select * from [{tableName}] where [{flagField}] = {flagValue}"; + } + + #endregion - /// >获取指定类型的主键,按 Flag 属性筛选。 - public Result Keys(long flag = 0) where T : class, IRecord, new() => Keys(typeof(T), flag); + #region special + + /// 整理数据库,压缩未使用的空间。 + public const string Vacuum = "vacuum"; + + /// 内存数据库的地址。 + public const string Memory = ":memory:"; + + /// 查询数据库中的所有视图名。 + public string[] ViewNames() => TextColumn("select name from sqlite_master where type='view' order by name"); #endregion - #region static + #region mirror - /// 对文本转义,符合 SQL 安全性。可根据字段类型限制 UTF-8 字节数,默认为 0 时不限制字节数。 - public static string Escape(string text, int bytes = 0) + /// 保存当前数据库到文件,若文件已存在则将重写文件。 + public bool Save(string path, string pass = null) { - if (text.IsEmpty()) return ""; - - var t = text ?? ""; - t = t.Replace("\\", "\\\\"); - t = t.Replace("'", "\\'"); - t = t.Replace("\n", "\\n"); - t = t.Replace("\r", "\\r"); - t = t.Replace("\b", "\\b"); - t = t.Replace("\t", "\\t"); - t = t.Replace("\f", "\\f"); - - if (bytes > 5) + if (!StorageUtility.CreateFile(path, 0, true)) { - if (t.Bytes(Encoding.UTF8).Length > bytes) + Logger.Error(nameof(Sqlite), "Save", TextUtility.Merge("创建文件 ", path, " 失败。")); + return false; + } + + using (var destination = new Sqlite(path, pass)) return Save(destination); + } + + /// 保存当前数据库到目标数据库。 + public bool Save(Sqlite destination) => string.IsNullOrEmpty(Backup(this, destination)); + + /// 加载文件到当前数据库。 + public bool Load(string path, string pass = null) + { + using (var source = new Sqlite(path, pass)) return Load(source); + } + + /// 加载源数据库到当前数据库。 + public bool Load(Sqlite source) => string.IsNullOrEmpty(Backup(source, this)); + + /// 备份数据库,返回错误信息。 + static string Backup(Sqlite source, Sqlite destination) + { + if (source == null) return "备份失败:源无效。"; + if (destination == null) return "备份失败:目标无效。"; + var sConnect = source.Connect(); + if (sConnect.NotEmpty()) return sConnect; + var dConnect = source.Connect(); + if (dConnect.NotEmpty()) return dConnect; + + lock (source.Locker) + { + lock (destination.Locker) { - while (true) + try { - t = t.Substring(0, t.Length - 1); - if (t.Bytes(Encoding.UTF8).Length <= (bytes - 4)) break; + var src = (SQLiteConnection)source.Connection; + var dst = (SQLiteConnection)destination.Connection; + src.BackupDatabase(dst, "main", "main", -1, null, 0); + return ""; + } + catch (Exception ex) + { + return "SQLite Load Failed: " + ex.Message; } - t = t + " ..."; } } - - return t; } + #endregion + + #region type & parameter + private static string GetColumnTypeName(ColumnType type) { switch (type) @@ -764,39 +446,6 @@ namespace Apewer.Source return p; } - /// 备份数据库,返回错误信息。 - public static string Backup(Sqlite source, Sqlite destination) - { - if (source == null) return "备份失败:源无效。"; - if (destination == null) return "备份失败:目标无效。"; - var sConnect = source.Connect(); - if (sConnect.NotEmpty()) return sConnect; - var dConnect = source.Connect(); - if (dConnect.NotEmpty()) return dConnect; - - lock (source._locker) - { - lock (destination._locker) - { - try - { - source._db.BackupDatabase(destination._db, "main", "main", -1, null, 0); - return ""; - } - catch (Exception ex) - { - return "SQLite Load Failed: " + ex.Message; - } - } - } - } - - /// 整理数据库,压缩未使用的空间。 - public const string Vacuum = "vacuum"; - - /// 内存数据库的地址。 - public const string Memory = ":memory:"; - #endregion } diff --git a/Apewer.Web/AspNetBridge/ApiController.cs b/Apewer.Web/AspNetBridge/ApiController.cs new file mode 100644 index 0000000..d17b2c6 --- /dev/null +++ b/Apewer.Web/AspNetBridge/ApiController.cs @@ -0,0 +1,67 @@ +using Apewer.Web; +using System; +using System.Collections.Generic; +using System.IO; +using System.Text; + +namespace Apewer.AspNetBridge +{ + + /// + public abstract class ApiController + { + + /// + public ApiRequest Request { get; internal set; } + + /// + public ApiResponse Response { get; internal set; } + + /// + protected virtual IHttpActionResult Bytes(byte[] bytes, string contentType = "application/octet-stream", string attachmentName = null) + { + var har = new HttpActionResult(); + har.Bytes = bytes; + har.ContentType = contentType; + har.Attachment = attachmentName; + return har; + } + + /// + protected virtual IHttpActionResult Text(string text, string contentType = "text/plain") + { + var har = new HttpActionResult(); + har.Bytes = text.Bytes(); + har.ContentType = contentType; + return har; + } + + /// + protected virtual IHttpActionResult Json(T content) + { + var json = Apewer.Json.From(content, false, -1, true); + var text = json == null ? "" : json.ToString(); + return Bytes(text.Bytes(), "application/json"); + } + + /// + protected static string MapPath(string relativePath) + { + var root = RuntimeUtility.ApplicationPath; + var path = root; + if (relativePath.NotEmpty()) + { + var split = relativePath.Split('/'); + foreach (var seg in split) + { + if (seg.IsEmpty()) continue; + if (seg == "~") path = root; + path = Path.Combine(path, seg); + } + } + return path; + } + + } + +} diff --git a/Apewer.Web/AspNetBridge/Attributes.cs b/Apewer.Web/AspNetBridge/Attributes.cs new file mode 100644 index 0000000..4f6e1fa --- /dev/null +++ b/Apewer.Web/AspNetBridge/Attributes.cs @@ -0,0 +1,83 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Apewer.AspNetBridge +{ + + /// + [AttributeUsage(AttributeTargets.Class, Inherited = false)] + public sealed class RoutePrefixAttribute : Attribute + { + + string _path; + + /// + public string Path { get { return _path; } } + + /// + /// + public RoutePrefixAttribute(string path) { _path = path; } + + } + + /// + [AttributeUsage(AttributeTargets.Method, Inherited = false)] + public sealed class RouteAttribute : Attribute + { + + string _path; + + /// + public string Path { get { return _path; } } + + /// + public RouteAttribute(string path) { _path = path; } + + } + + /// + [AttributeUsage(AttributeTargets.Parameter, Inherited = false)] + public class FromBodyAttribute : Attribute { } + + /// + [AttributeUsage(AttributeTargets.Parameter, Inherited = false)] + public class FromUriAttribute : Attribute { } + + /// + [AttributeUsage(AttributeTargets.Method, Inherited = false)] + public sealed class HttpConnectAttribute : Attribute { } + + /// + [AttributeUsage(AttributeTargets.Method, Inherited = false)] + public sealed class HttpDeleteAttribute : Attribute { } + + /// + [AttributeUsage(AttributeTargets.Method, Inherited = false)] + public sealed class HttpGetAttribute : Attribute { } + + /// + [AttributeUsage(AttributeTargets.Method, Inherited = false)] + public sealed class HttpHeadAttribute : Attribute { } + + /// + [AttributeUsage(AttributeTargets.Method, Inherited = false)] + public sealed class HttpOptionsAttribute : Attribute { } + + /// + [AttributeUsage(AttributeTargets.Method, Inherited = false)] + public sealed class HttpPatchAttribute : Attribute { } + + /// + [AttributeUsage(AttributeTargets.Method, Inherited = false)] + public sealed class HttpPostAttribute : Attribute { } + + /// + [AttributeUsage(AttributeTargets.Method, Inherited = false)] + public sealed class HttpPutAttribute : Attribute { } + + /// + [AttributeUsage(AttributeTargets.Method, Inherited = false)] + public sealed class HttpTraceAttribute : Attribute { } + +} diff --git a/Apewer.Web/AspNetBridge/BridgeController.cs b/Apewer.Web/AspNetBridge/BridgeController.cs new file mode 100644 index 0000000..7b2e28a --- /dev/null +++ b/Apewer.Web/AspNetBridge/BridgeController.cs @@ -0,0 +1,211 @@ +using Apewer.Web; +using System; +using System.Collections.Generic; +using System.Reflection; +using System.Security.Cryptography; +using System.Text; + +using static Apewer.NumberUtility; +using static Apewer.Internals.ApiHelper; + +namespace Apewer.AspNetBridge +{ + + /// 桥接 ASP.NET 的控制器。 + public class BridgeController : Web.ApiController + { + + #region static routes + + static RouteItem[] _routes = null; + + /// 导出所有路由项。 + public static Json ExportRoutes() + { + var array = Json.NewArray(); + if (_routes != null) + { + foreach (var route in _routes) + { + var item = route.ToJson(); + array.AddItem(item); + } + } + return array; + } + + /// 初始化路由。 + /// 包含控制器的程序集。 + /// 包含返回 System.Void 的方法。 + public static void Initialize(IEnumerable assemblies, bool withVoid = false) => _routes = RouteItem.Parse(assemblies, withVoid); + + /// 初始化路由,不包含返回 System.Void 的方法。 + /// 包含控制器的程序集。 + public static void Initialize(params Assembly[] assemblies) => _routes = RouteItem.Parse(assemblies, false); + + #endregion + + #region events + + /// 发生异常时候的处理程序。 + public static OnException OnException { get; set; } + + /// 输出异常。 + /// + public static void Output(ApiRequest request, ApiResponse response, MethodInfo method, Exception exception) + { + if (request == null) throw new ArgumentNullException(nameof(request)); + if (response == null) throw new ArgumentNullException(nameof(response)); + if (method == null) throw new ArgumentNullException(nameof(method)); + if (exception == null) throw new ArgumentNullException(nameof(exception)); + + var sb = new StringBuilder(); + if (request.IP.NotEmpty()) + { + sb.Append(request.IP); + sb.Append(" "); + } + sb.Append(request.Method.ToString()); + if (request.Url != null) + { + sb.Append(" "); + sb.Append(request.Url.OriginalString); + } + if (method != null) + { + sb.Append("\r\n"); + sb.Append(method.DeclaringType.FullName); + sb.Append("."); + sb.Append(method.Name); + } + sb.Append("\r\n\r\n"); + sb.Append(new ApiExceptionModel(exception).ToString()); + + var model = new ApiTextModel(sb.ToString()); + model.Status = 500; + response.Model = model; + } + + #endregion + + #region instance + + /// + public BridgeController() : base((c) => ((BridgeController)c).Execute(), (c) => ((BridgeController)c).Default()) { } + + bool Execute() + { + if (_routes == null) Initialize(); + + var routes = _routes; + var route = RouteItem.Match(routes, Request.Url.AbsolutePath, Request.Method); + try + { + Execute(route); + } + catch (Exception ex) + { + Logger.Write("Route Error", ex.InnerException.GetType().FullName, ex.InnerException.Message); + var action = OnException; + if (action != null) action.Invoke(Request, Response, route.Method, ex.InnerException); + else Response.Model = new ApiStatusModel(500); + } + return false; + } + + void Default() { } + + void Execute(RouteItem route) + { + if (route == null) + { + Response.Model = new ApiStatusModel(404); + return; + } + + // 检查 HTTP 方法 + switch (Request.Method) + { + case Network.HttpMethod.GET: + if (!route.Get) + { + Response.Model = new ApiStatusModel(405); + return; + } + break; + case Network.HttpMethod.POST: + if (!route.Post) + { + Response.Model = new ApiStatusModel(405); + return; + } + break; + default: + Response.Model = new ApiStatusModel(405); + return; + } + + // 准备参数。 + var ps = ReadParameters(Request, route.Parameters); + + // 准备控制器。 + var c = Activator.CreateInstance(route.Controller) as ApiController; + c.Request = Request; + c.Response = Response; + + // 调用 API 方法。 + var r = route.Method.Invoke(c, ps); + if (r == null) return; + + // 识别返回类型。 + Response.Model = Model(r); + } + + static ApiModel Model(object value) + { + if (value == null) return null; + + if (value is ApiModel model) return model; + if (value is string text) return new ApiTextModel(text); + if (value is byte[] bytes) return new ApiBytesModel(bytes); + if (value is Json json) return new ApiJsonModel(json); + if (value is HttpActionResult har) return ToModel(har); + if (value is HttpResponseMessage hrm) return ToModel(hrm); + + return new ApiTextModel(value.ToString()); + } + + static ApiModel ToModel(HttpActionResult har) + { + if (har == null) return new ApiStatusModel(204); + var model = new ApiBytesModel(); + model.Status = har.Status; + model.Bytes = har.Bytes; + model.ContentType = har.ContentType; + model.Attachment = har.Attachment; + return model; + } + + static ApiModel ToModel(HttpResponseMessage hrm) + { + if (hrm == null) return new ApiStatusModel(204); + if (hrm.Content == null || hrm.Content.Stream == null) return new ApiStatusModel(204); + + var model = new ApiStreamModel(hrm.Content.Stream); + model.Status = (int)hrm.StatusCode; + if (hrm.Content.Headers != null) + { + model.Headers = new StringPairs(); + if (hrm.Content.Headers.ContentType != null) model.ContentType = hrm.Content.Headers.ContentType.MediaType; + if (hrm.Content.Headers.ContentDisposition != null) model.Attachment = hrm.Content.Headers.ContentDisposition.FileName; + model.Headers.Add("Content-Length", hrm.Content.Headers.ContentLength.ToString()); + } + model.Stream = hrm.Content.Stream; + return model; + } + + #endregion + + } + +} diff --git a/Apewer.Web/AspNetBridge/ContentDispositionHeaderValue.cs b/Apewer.Web/AspNetBridge/ContentDispositionHeaderValue.cs new file mode 100644 index 0000000..d555595 --- /dev/null +++ b/Apewer.Web/AspNetBridge/ContentDispositionHeaderValue.cs @@ -0,0 +1,20 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Apewer.AspNetBridge +{ + + /// + public class ContentDispositionHeaderValue + { + + /// + public string FileName { get; set; } + + /// + public ContentDispositionHeaderValue(string dispositionType) => FileName = dispositionType; + + } + +} diff --git a/Apewer.Web/AspNetBridge/HttpActionResult.cs b/Apewer.Web/AspNetBridge/HttpActionResult.cs new file mode 100644 index 0000000..46b2934 --- /dev/null +++ b/Apewer.Web/AspNetBridge/HttpActionResult.cs @@ -0,0 +1,39 @@ +using Apewer.Web; +using System; +using System.Collections.Generic; +using System.Text; + +namespace Apewer.AspNetBridge +{ + + /// + public interface IHttpActionResult { } + + /// + public class HttpActionResult : IHttpActionResult + { + + /// + public int Status { get; set; } + + /// + public string ContentType { get; set; } + + /// + public string Attachment { get; set; } + + /// + public byte[] Bytes { get; set; } + + /// + public StringPairs Cookies { get; set; } + + /// + public HttpActionResult() + { + Cookies = new StringPairs(); + } + + } + +} diff --git a/Apewer.Web/AspNetBridge/HttpContent.cs b/Apewer.Web/AspNetBridge/HttpContent.cs new file mode 100644 index 0000000..ecd2dda --- /dev/null +++ b/Apewer.Web/AspNetBridge/HttpContent.cs @@ -0,0 +1,26 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Text; + +namespace Apewer.AspNetBridge +{ + + /// + public abstract class HttpContent : IDisposable + { + + HttpContentHeaders _headers = new HttpContentHeaders(); + + /// + public Stream Stream { get; set; } + + /// + public HttpContentHeaders Headers { get => _headers; } + + /// + public void Dispose() => RuntimeUtility.Dispose(Stream); + + } + +} diff --git a/Apewer.Web/AspNetBridge/HttpContentHeaders.cs b/Apewer.Web/AspNetBridge/HttpContentHeaders.cs new file mode 100644 index 0000000..30516e1 --- /dev/null +++ b/Apewer.Web/AspNetBridge/HttpContentHeaders.cs @@ -0,0 +1,23 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Apewer.AspNetBridge +{ + + /// + public class HttpContentHeaders + { + + /// + public ContentDispositionHeaderValue ContentDisposition { get; set; } + + /// + public MediaTypeHeaderValue ContentType { get; set; } + + /// + public long ContentLength { get; set; } + + } + +} diff --git a/Apewer.Web/AspNetBridge/HttpResponseMessage.cs b/Apewer.Web/AspNetBridge/HttpResponseMessage.cs new file mode 100644 index 0000000..cca3da0 --- /dev/null +++ b/Apewer.Web/AspNetBridge/HttpResponseMessage.cs @@ -0,0 +1,49 @@ +using Apewer.Web; +using System; +using System.Collections.Generic; +using System.Net; +using System.Text; + +namespace Apewer.AspNetBridge +{ + + /// + public class HttpResponseMessage : IDisposable + { + + /// + public HttpContent Content { get; set; } + + /// + public HttpStatusCode StatusCode { get; set; } + + private string ReasonPhrase { get; set; } + + private bool IsSuccessStatusCode { get { var code = (int)StatusCode; return code >= 200 && code <= 299; } } + + /// + public HttpResponseMessage(HttpStatusCode statusCode = HttpStatusCode.OK) + { + var code = (int)StatusCode; + if (code < 0 || code > 999) throw new ArgumentOutOfRangeException("statusCode"); + StatusCode = statusCode; + } + + /// + public override string ToString() => ""; + + private bool ContainsNewLineCharacter(string value) + { + foreach (char c in value) + { + if (c == '\r' || c == '\n') return true; + } + return false; + } + + /// + public void Dispose() { } + + } + +} diff --git a/Apewer.Web/AspNetBridge/JsonResult.cs b/Apewer.Web/AspNetBridge/JsonResult.cs new file mode 100644 index 0000000..2049731 --- /dev/null +++ b/Apewer.Web/AspNetBridge/JsonResult.cs @@ -0,0 +1,23 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Apewer.AspNetBridge +{ + + /// + public class JsonResult : HttpActionResult + { + + /// + public JsonResult(object content) + { + var json = Json.From(content); + var text = json == null ? "" : json.ToString(); + Bytes = text.Bytes(); + ContentType = "application/json"; + } + + } + +} diff --git a/Apewer.Web/AspNetBridge/MediaTypeHeaderValue.cs b/Apewer.Web/AspNetBridge/MediaTypeHeaderValue.cs new file mode 100644 index 0000000..b571124 --- /dev/null +++ b/Apewer.Web/AspNetBridge/MediaTypeHeaderValue.cs @@ -0,0 +1,20 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Apewer.AspNetBridge +{ + + /// + public class MediaTypeHeaderValue + { + + /// + public string MediaType { get; set; } + + /// + public MediaTypeHeaderValue(string mediaType) => MediaType = mediaType; + + } + +} diff --git a/Apewer.Web/AspNetBridge/RouteItem.cs b/Apewer.Web/AspNetBridge/RouteItem.cs new file mode 100644 index 0000000..c7fab1c --- /dev/null +++ b/Apewer.Web/AspNetBridge/RouteItem.cs @@ -0,0 +1,196 @@ +using Apewer.Network; +using System; +using System.Collections.Generic; +using System.Reflection; +using System.Text; + +namespace Apewer.AspNetBridge +{ + + [Serializable] + internal class RouteItem : IToJson + { + + public string Path; + + public string Lower; + + public Type Controller; + + public MethodInfo Method; + + public string MethodName; + + public bool Connect; + public bool Delete; + public bool Get; + public bool Head; + public bool Options; + public bool Patch; + public bool Post; + public bool Put; + public bool Trace; + + public ParameterInfo[] Parameters; + + public Type Return; + + public Json ToJson() + { + var ps = Json.NewObject(); + foreach (var parameter in Parameters) ps.SetProperty(parameter.Name, parameter.ParameterType.FullName); + + var methods = Json.NewArray(); + if (Connect) methods.AddItem("connect"); + if (Delete) methods.AddItem("delete"); + if (Get) methods.AddItem("get"); + if (Head) methods.AddItem("head"); + if (Options) methods.AddItem("options"); + if (Patch) methods.AddItem("patch"); + if (Post) methods.AddItem("post"); + if (Put) methods.AddItem("put"); + if (Trace) methods.AddItem("trace"); + + var json = Json.NewObject(); + json.SetProperty("path", Path); + json.SetProperty("controller", Controller.FullName); + json.SetProperty("function", Method.Name); + json.SetProperty("return", Return.FullName); + json.SetProperty("parameters", ps); + json.SetProperty("methods", methods); + + return json; + } + + #region Enumerate + + internal static RouteItem[] Parse(IEnumerable assemblies, bool withVoid) + { + if (assemblies.IsEmpty()) assemblies = AppDomain.CurrentDomain.GetAssemblies(); + var baseType = typeof(ApiController); + var items = new ArrayBuilder(); + foreach (var assembly in assemblies) + { + var types = assembly.GetExportedTypes(); + foreach (var type in types) + { + if (type.IsNotPublic) continue; + if (!type.IsClass) continue; + if (!RuntimeUtility.IsInherits(type, baseType)) continue; + + if (type.Name == "AccountController") + { + } + + var pa = RuntimeUtility.GetAttribute(type, false); + var prefix = (pa == null || pa.Path.IsEmpty()) ? null : pa.Path.Split('/'); + + var methods = type.GetMethods(BindingFlags.Public | BindingFlags.Instance); + foreach (var method in methods) + { + if (method.IsConstructor) continue; + if (method.IsGenericMethod) continue; + if (!withVoid) + { + var returnType = method.ReturnType; + if (returnType == null || returnType.Equals(typeof(void))) continue; + } + + var route = RuntimeUtility.GetAttribute(method); + if (route == null || route.Path.IsEmpty()) continue; + + var path = Concat(prefix, route.Path.Split('/')); + + var item = new RouteItem(); + item.Controller = type; + item.Method = method; + item.MethodName = method.Name; + item.Parameters = method.GetParameters(); + item.Return = method.ReturnType; + item.Path = path; + item.Lower = path.Lower(); + + item.Get = RuntimeUtility.Contains(method); + item.Post = RuntimeUtility.Contains(method); + + item.Connect = RuntimeUtility.Contains(method); + item.Delete = RuntimeUtility.Contains(method); + item.Head = RuntimeUtility.Contains(method); + item.Options = RuntimeUtility.Contains(method); + item.Patch = RuntimeUtility.Contains(method); + item.Put = RuntimeUtility.Contains(method); + item.Trace = RuntimeUtility.Contains(method); + + items.Add(item); + } + } + } + return items.Export(); + } + + internal static RouteItem Match(RouteItem[] routes, string path, HttpMethod method) + { + if (routes == null) return null; + if (path.IsEmpty()) path = "/"; + + var length = routes.Length; + for (var i = 0; i < length; i++) + { + var route = routes[i]; + if (route == null) continue; + if (route.Path != path) continue; + + if (method == HttpMethod.GET && route.Get) return route; + if (method == HttpMethod.POST && route.Post) return route; + if (method == HttpMethod.CONNECT && route.Connect) return route; + if (method == HttpMethod.DELETE && route.Delete) return route; + if (method == HttpMethod.HEAD && route.Head) return route; + if (method == HttpMethod.OPTIONS && route.Options) return route; + if (method == HttpMethod.PATCH && route.Patch) return route; + if (method == HttpMethod.PUT && route.Put) return route; + if (method == HttpMethod.TRACE && route.Trace) return route; + } + + var lower = path.Lower(); + for (var i = 0; i < length; i++) + { + var route = routes[i]; + if (route == null) continue; + if (route.Lower != lower) continue; + + if (method == HttpMethod.GET && route.Get) return route; + if (method == HttpMethod.POST && route.Post) return route; + if (method == HttpMethod.CONNECT && route.Connect) return route; + if (method == HttpMethod.DELETE && route.Delete) return route; + if (method == HttpMethod.HEAD && route.Head) return route; + if (method == HttpMethod.OPTIONS && route.Options) return route; + if (method == HttpMethod.PATCH && route.Patch) return route; + if (method == HttpMethod.PUT && route.Put) return route; + if (method == HttpMethod.TRACE && route.Trace) return route; + } + + return null; + } + + static string Concat(string[] prefix, string[] path) + { + var all = new List(16); + + if (prefix != null) all.AddRange(prefix); + if (path != null) all.AddRange(path); + + var segs = new List(all.Count); + foreach (var seg in all) + { + if (seg.IsEmpty()) continue; + segs.Add(seg); + } + if (segs.Count < 1) return "/"; + return "/" + TextUtility.Join("/", segs.ToArray()); + } + + #endregion + + } + +} diff --git a/Apewer.Web/AspNetBridge/StreamContent.cs b/Apewer.Web/AspNetBridge/StreamContent.cs new file mode 100644 index 0000000..c73f684 --- /dev/null +++ b/Apewer.Web/AspNetBridge/StreamContent.cs @@ -0,0 +1,18 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Text; + +namespace Apewer.AspNetBridge +{ + + /// + public class StreamContent : HttpContent + { + + /// + public StreamContent(Stream stream) => Stream = stream; + + } + +} diff --git a/Apewer.Web/AspNetBridge/_Delegates.cs b/Apewer.Web/AspNetBridge/_Delegates.cs new file mode 100644 index 0000000..9ecaa54 --- /dev/null +++ b/Apewer.Web/AspNetBridge/_Delegates.cs @@ -0,0 +1,12 @@ +using Apewer.Web; +using System; +using System.Reflection; + +namespace Apewer.AspNetBridge +{ + + /// 输出异常。 + /// + public delegate void OnException(ApiRequest request, ApiResponse response, MethodInfo method, Exception exception); + +} diff --git a/Apewer.Web/Internals/ApiHelper.cs b/Apewer.Web/Internals/ApiHelper.cs index 9927f37..68339ca 100644 --- a/Apewer.Web/Internals/ApiHelper.cs +++ b/Apewer.Web/Internals/ApiHelper.cs @@ -2,6 +2,7 @@ using Apewer.Web; using System; using System.Collections.Generic; +using System.Reflection; using System.Text; namespace Apewer.Internals @@ -76,8 +77,16 @@ namespace Apewer.Internals internal static object[] ReadParameters(ApiRequest request, ApiFunction function) { if (request == null || function == null) return null; + return ReadParameters(request, function.Parameters); - var pis = function.Parameters; + } + + // 为带有形参的 Function 准备实参。 + internal static object[] ReadParameters(ApiRequest request, ParameterInfo[] parameters) + { + if (request == null || parameters == null || parameters.Length < 1) return null; + + var pis = parameters; if (pis == null) return null; var count = pis.Length; @@ -86,18 +95,84 @@ namespace Apewer.Internals // 当 Function 仅有一个参数时,尝试生成模型。 if (count == 1) { - if (function.ParamIsRecord) + var pin = pis[0].Name; + var pit = pis[0].ParameterType; + + // POST + if (request.Method == HttpMethod.POST) { - try + // string + if (pit.Equals(typeof(string))) return new object[] { request.Parameters.GetValue(pin, true) }; + + // json + if (pit.Equals(typeof(Json))) return new object[] { request.PostJson }; + +#if !NET20 + // dynamic + if (pit.Equals(typeof(object))) + { + try + { + // var expando = new System.Dynamic.ExpandoObject(); + // var dict = expando as IDictionary; + var expando = new ObjectSet(false, true); + var dict = expando.Origin(); + if (request.Form != null) + { + foreach (var kvp in request.Form) + { + if (dict.ContainsKey(kvp.Key)) continue; + dict.Add(kvp.Key, kvp.Value); + } + } + else if (request.PostJson) + { + var jprops = request.PostJson.GetProperties(); + foreach (var jprop in jprops) + { + var name = jprop.Name; + if (dict.ContainsKey(name)) continue; + var value = jprop.Value; + if (value != null) + { + + } + dict.Add(name, value); + } + } + return new object[] { expando }; + } + catch { } + } +#endif + + // class + if (pit.IsClass) { - var record = Activator.CreateInstance(pis[0].ParameterType); - Json.Object(record, request.Data, true, null, true); - return new object[] { record }; + try + { + var entity = Activator.CreateInstance(pit); + var setted = false; + if (!setted) setted = ReadParameter(request.Data, entity); + if (!setted) setted = ReadParameter(request.PostJson, entity); + return new object[] { entity }; + } + catch { } } - catch { } - return new object[] { null }; + + if (request.PostJson) return new object[] { request.PostJson }; + if (request.Form != null) return new object[] { request.Form }; + if (request.PostText.NotEmpty()) return new object[] { request.PostText }; + return new object[] { request.PostData }; + } + else + { + // string + if (pit.Equals(typeof(string))) return new object[] { request.Parameters.GetValue(pin, true) }; + + // json + if (pit.Equals(typeof(Json))) return new object[] { Json.From(request.Parameters.GetValue(pin, true)) }; } - // 未知类型的参数,继续使用通用方法获取。 } var ps = new object[count]; @@ -111,10 +186,31 @@ namespace Apewer.Internals return ps; } + static bool ReadParameter(Json json, object entity) + { + if (!json) return false; + if (json.IsObject) + { + var properties = json.GetProperties(); + if (properties.Length < 1) return false; + Json.Object(entity, json, true, null, true); + return true; + } + if (json.IsArray) + { + var items = json.GetItems(); + if (items.Length < 1) return false; + Json.Object(entity, json, true, null, true); + return true; + } + return false; + } + static object ReadParameter(string text, Type type) { if (type.Equals(typeof(object)) || type.Equals(typeof(string))) return text; - if (type.Equals(typeof(byte[]))) return TextUtility.FromBase64(text); + + if (type.Equals(typeof(bool))) return NumberUtility.Boolean(text); if (type.Equals(typeof(float))) return NumberUtility.Single(text); if (type.Equals(typeof(double))) return NumberUtility.Double(text); if (type.Equals(typeof(decimal))) return NumberUtility.Decimal(text); @@ -126,6 +222,10 @@ namespace Apewer.Internals if (type.Equals(typeof(uint))) return NumberUtility.UInt32(text); if (type.Equals(typeof(long))) return NumberUtility.Int64(text); if (type.Equals(typeof(ulong))) return NumberUtility.UInt64(text); + + if (type.Equals(typeof(byte[]))) return TextUtility.FromBase64(text); + if (type.Equals(typeof(Json))) return Json.From(text); + return type.IsValueType ? Activator.CreateInstance(type) : null; } @@ -252,6 +352,10 @@ namespace Apewer.Internals request.Data = data ?? Json.NewObject(); } } + + // 尝试解析 Form,需要 application/x-www-form-urlencoded + var contentType = headers.GetValue("content-type", true) ?? ""; + if (contentType.Contains("urlencoded")) request.Form = ApiUtility.Parameters(text); } } } @@ -328,6 +432,12 @@ namespace Apewer.Internals { merged.Add("Content-Security-Policy", "upgrade-insecure-requests"); } + + // 包含 API 的处理时间。 + if (options.WithDuration && response != null) + { + merged.Add("Duration", response.Duration.ToString() + "ms"); + } } if (response != null) { @@ -398,12 +508,12 @@ namespace Apewer.Internals return text; } - internal static void Output(ApiProvider provider, ApiOptions options, string type, byte[] bytes) + internal static void Output(ApiProvider provider, ApiOptions options, ApiResponse response, string type, byte[] bytes) { var preWrite = provider.PreWrite(); if (!string.IsNullOrEmpty(preWrite)) return; - var headers = PrepareHeaders(options, null); + var headers = PrepareHeaders(options, response); foreach (var header in headers) provider.SetHeader(header.Key, header.Value); provider.SetCache(0); @@ -412,6 +522,7 @@ namespace Apewer.Internals var length = bytes == null ? 0 : bytes.Length; provider.SetContentLength(length); if (length > 0) provider.ResponseBody().Write(bytes); + provider.Sent(); } internal static void Output(ApiProvider provider, ApiOptions options, ApiResponse response, ApiRequest request, HttpMethod method) @@ -420,9 +531,8 @@ namespace Apewer.Internals if (!string.IsNullOrEmpty(preWrite)) return; // 设置头。 - var headers = PrepareHeaders(options, null); + var headers = PrepareHeaders(options, response); foreach (var header in headers) provider.SetHeader(header.Key, header.Value); - var model = response.Model; if (model != null) { @@ -438,6 +548,7 @@ namespace Apewer.Internals provider.SetContentLength(json.Length); var stream = provider.ResponseBody(); if (stream != null && stream.CanWrite) stream.Write(json); + provider.Sent(); } #endregion diff --git a/Apewer.Web/Web/ApiProcessor.cs b/Apewer.Web/Web/ApiProcessor.cs index 92f6d0f..7c59dcf 100644 --- a/Apewer.Web/Web/ApiProcessor.cs +++ b/Apewer.Web/Web/ApiProcessor.cs @@ -115,7 +115,7 @@ namespace Apewer.Web { if (lowerPath.StartsWith("/favicon.ico")) { - Output(Provider, Options, null, null); + Output(Provider, Options, null, null, null); return "已取消对 favicon.ico 的请求。"; } } @@ -126,7 +126,7 @@ namespace Apewer.Web if (lowerPath.StartsWith("/robots.txt")) { const string text = "User-agent: *\nDisallow: / \n"; - Output(Provider, Options, "text/plain", TextUtility.Bytes(text)); + Output(Provider, Options, null, "text/plain", TextUtility.Bytes(text)); return "已取消对 robots.txt 的请求。"; } } diff --git a/Apewer.Web/Web/ApiProgram.cs b/Apewer.Web/Web/ApiProgram.cs index 0a88a60..149da2c 100644 --- a/Apewer.Web/Web/ApiProgram.cs +++ b/Apewer.Web/Web/ApiProgram.cs @@ -126,7 +126,9 @@ namespace Apewer.Web public partial class ApiProgram : IHttpHandler, IHttpModule { - #region IHttpHandler + #region IHttpHandler + + static bool _initialized = false; /// public bool IsReusable { get { return false; } } @@ -134,13 +136,17 @@ namespace Apewer.Web /// public void ProcessRequest(HttpContext context) { - _initializer?.Invoke(); + if (!_initialized) + { + _initialized = true; + _initializer?.Invoke(); + } _invoker.Invoke(new WebsiteProvider(context)); } - #endregion + #endregion - #region IHttpModule + #region IHttpModule /// public void Dispose() { } @@ -167,7 +173,7 @@ namespace Apewer.Web { } - #endregion + #endregion } diff --git a/Apewer.Web/Web/AspNetCoreProvider.cs b/Apewer.Web/Web/AspNetCoreProvider.cs index 8d338ad..20da70e 100644 --- a/Apewer.Web/Web/AspNetCoreProvider.cs +++ b/Apewer.Web/Web/AspNetCoreProvider.cs @@ -118,7 +118,7 @@ namespace Apewer.Web #region Response /// 设置 HTTP 状态。 - public override void SetStatus(string status) => response.StatusCode = Convert.ToInt32(status); + public override void SetStatus(int status, int subStatus = 0) => response.StatusCode = status; /// 设置响应的头。 public override string SetHeader(string name, string value) diff --git a/Apewer.Web/Web/HttpListenerProvider.cs b/Apewer.Web/Web/HttpListenerProvider.cs index 12c7f1d..f898f0e 100644 --- a/Apewer.Web/Web/HttpListenerProvider.cs +++ b/Apewer.Web/Web/HttpListenerProvider.cs @@ -84,7 +84,7 @@ namespace Apewer.Web #region Response /// 设置 HTTP 状态。 - public override void SetStatus(string status) => response.StatusCode = Convert.ToInt32(status); + public override void SetStatus(int status, int subStatus = 0) => response.StatusCode = status; /// 设置响应的头。 public override string SetHeader(string name, string value) diff --git a/Apewer.Web/Web/WebsiteProvider.cs b/Apewer.Web/Web/WebsiteProvider.cs index 6e0bac3..05c518e 100644 --- a/Apewer.Web/Web/WebsiteProvider.cs +++ b/Apewer.Web/Web/WebsiteProvider.cs @@ -37,7 +37,6 @@ namespace Apewer.Web /// 写入响应前的检查,返回错误信息。 public override string PreWrite() { - // 从 Response 头中移除 Server 和 X-Powered-By 属性。 #if NETFX var keys = new List(response.Headers.AllKeys); @@ -49,6 +48,9 @@ namespace Apewer.Web return null; } + /// + public override void Sent() => response.Flush(); + /// 停止并关闭响应流。 public override void End() => End(false); @@ -88,7 +90,11 @@ namespace Apewer.Web #region Response /// 设置 HTTP 状态。 - public override void SetStatus(string status) => response.Status = status; + public override void SetStatus(int status, int subStatus = 0) + { + response.StatusCode = status; + if (subStatus > 0) response.SubStatusCode = subStatus; + } /// 设置响应的头。 public override string SetHeader(string name, string value) diff --git a/Apewer.Web/WebConfig40.xml b/Apewer.Web/WebConfig40.xml index 6726e1e..b9b98f7 100644 --- a/Apewer.Web/WebConfig40.xml +++ b/Apewer.Web/WebConfig40.xml @@ -12,6 +12,9 @@ + + + diff --git a/Apewer.Web/WebConfig461.xml b/Apewer.Web/WebConfig461.xml index 16c6c0b..1c8df0b 100644 --- a/Apewer.Web/WebConfig461.xml +++ b/Apewer.Web/WebConfig461.xml @@ -12,6 +12,9 @@ + + + diff --git a/Apewer.Web/WebConfigStd.xml b/Apewer.Web/WebConfigStd.xml index 1649f27..c0604b3 100644 --- a/Apewer.Web/WebConfigStd.xml +++ b/Apewer.Web/WebConfigStd.xml @@ -16,6 +16,9 @@ + + + diff --git a/Apewer/Apewer.props b/Apewer/Apewer.props index 607d0a0..c787419 100644 --- a/Apewer/Apewer.props +++ b/Apewer/Apewer.props @@ -9,12 +9,13 @@ Apewer Apewer Libraries - 6.5.10 + 6.6.0 true + bin\$(Configuration)\$(TargetFramework)\$(AssemblyName).xml latest diff --git a/Apewer/Class.cs b/Apewer/Class.cs index 180435d..0797b45 100644 --- a/Apewer/Class.cs +++ b/Apewer/Class.cs @@ -1,130 +1,141 @@ using System; -/// 装箱类。 -public sealed class Class : IComparable, IComparable, IComparable> +namespace Apewer { - private bool _hashcode = false; - private bool _equals = false; + /// 装箱类。 + public sealed class Class : IComparable, IComparable, IComparable> + { - /// 装箱对象。 - public T Value { get; set; } + private bool _hashcode = false; + private bool _equals = false; - /// - public bool IsNull { get { return Value == null; } } + /// 装箱对象。 + public T Value { get; set; } - /// - public bool HasValue { get { return Value != null; } } + /// + public bool IsNull { get { return Value == null; } } - /// 创建默认值。 - public Class(T value = default, bool tryEquals = true, bool tryHashCode = true) - { - Value = value; - _hashcode = tryHashCode; - _equals = tryEquals; - } + /// + public bool HasValue { get { return Value != null; } } - #region Override + /// 创建默认值。 + public Class(T value = default, bool tryEquals = true, bool tryHashCode = true) + { + Value = value; + _hashcode = tryHashCode; + _equals = tryEquals; + } - /// - public override int GetHashCode() - { - if (_hashcode && Value != null) + #region Override + + /// + public override int GetHashCode() { - return Value.GetHashCode(); + if (_hashcode && Value != null) + { + return Value.GetHashCode(); + } + return base.GetHashCode(); } - return base.GetHashCode(); - } - /// - public override bool Equals(object obj) - { - if (_equals) + /// + public override bool Equals(object obj) { - var right = obj as Class; - if (right == null) return false; + if (_equals) + { + var right = obj as Class; + if (right == null) return false; + + if (Value == null && right.Value == null) return true; + if (Value == null && right.Value != null) return false; + if (Value != null && right.Value == null) return false; + return Value.Equals(right.Value); + } + return base.Equals(obj); + } - if (Value == null && right.Value == null) return true; - if (Value == null && right.Value != null) return false; - if (Value != null && right.Value == null) return false; - return Value.Equals(right.Value); + /// + public override string ToString() + { + if (Value == null) return ""; + return Value.ToString(); } - return base.Equals(obj); - } - /// - public override string ToString() - { - if (Value == null) return ""; - return Value.ToString(); - } + #endregion - #endregion + #region IComparable - #region IComparable + /// + /// + /// + public int CompareTo(object obj) + { + if (obj != null && obj is T) return CompareTo((T)obj); + if (obj != null && obj is Class) return CompareTo(obj as Class); - /// - /// - /// - public int CompareTo(object obj) - { - if (obj != null && obj is T) return CompareTo((T)obj); - if (obj != null && obj is Class) return CompareTo(obj as Class); + if (Value == null) throw new MissingMemberException(typeof(T).FullName, nameof(Value)); + if (!(Value is IComparable)) throw new NotSupportedException(); + return ((IComparable)Value).CompareTo(obj); + } - if (Value == null) throw new MissingMemberException(typeof(T).FullName, nameof(Value)); - if (!(Value is IComparable)) throw new NotSupportedException(); - return ((IComparable)Value).CompareTo(obj); - } + /// + /// + /// + public int CompareTo(T other) + { + if (Value == null) throw new MissingMemberException(typeof(T).FullName, nameof(Value)); + if (!(Value is IComparable)) throw new NotSupportedException(); + return ((IComparable)Value).CompareTo(other); + } - /// - /// - /// - public int CompareTo(T other) - { - if (Value == null) throw new MissingMemberException(typeof(T).FullName, nameof(Value)); - if (!(Value is IComparable)) throw new NotSupportedException(); - return ((IComparable)Value).CompareTo(other); - } + /// + /// + /// + public int CompareTo(Class other) + { + if (Value == null) throw new MissingMemberException(typeof(T).FullName, nameof(Value)); + if (other == null || !other.HasValue) return 1; - /// - /// - /// - public int CompareTo(Class other) - { - if (Value == null) throw new MissingMemberException(typeof(T).FullName, nameof(Value)); - if (other == null || !other.HasValue) return 1; + if (Value is IComparable) return ((IComparable)Value).CompareTo(other.Value); + if (Value is IComparable) return ((IComparable)Value).CompareTo(other.Value); - if (Value is IComparable) return ((IComparable)Value).CompareTo(other.Value); - if (Value is IComparable) return ((IComparable)Value).CompareTo(other.Value); + throw new NotSupportedException(); + } - throw new NotSupportedException(); - } + #endregion - #endregion + #region 运算符。 - #region 运算符。 + /// 到 Boolean 的隐式转换,判断 包含值。 + /// 当 T 是 Boolean 时,获取 Value。
当 T 是 String 时,判断 Value 不为 NULL 且不为空。
+ public static implicit operator bool(Class instance) + { + if (instance == null) return false; - /// 从 Class<T> 到 Boolean 的隐式转换,判断 Class<T> 包含值。 - /// 当 T 是 Boolean 时,获取 Value。
当 T 是 String 时,判断 Value 不为 NULL 且不为空。
- public static implicit operator bool(Class instance) - { - if (instance == null) return false; + var boolean = instance as Class; + if (boolean != null) return boolean.Value; - var boolean = instance as Class; - if (boolean != null) return boolean.Value; + var text = instance as Class; + if (text != null) return !string.IsNullOrEmpty(text.Value); - var text = instance as Class; - if (text != null) return !string.IsNullOrEmpty(text.Value); + return instance.HasValue; + } - return instance.HasValue; - } + /// 到 T 的隐式转换。 + public static implicit operator T(Class instance) + { + if (instance == null) return default(T); + + if (typeof(T).Equals(typeof(bool))) return instance.Value; + return instance.Value; + } - // /// 从 Class<T> 到 T 的隐式转换。 - // public static implicit operator T(Class instance) => instance == null ? default : instance.Value; + /// 从 T 到 的隐式转换。 + public static implicit operator Class(T value) => new Class(value); - // /// 从 T 到 Class<T> 的隐式转换。 - // public static implicit operator Class(T value) => new Class(value); + #endregion - #endregion + } -} +} \ No newline at end of file diff --git a/Apewer/Json.cs b/Apewer/Json.cs index a2afe6c..0596296 100644 --- a/Apewer/Json.cs +++ b/Apewer/Json.cs @@ -11,6 +11,7 @@ using System.IO; using System.Reflection; using System.Runtime.Serialization; using System.Text; +using static Apewer.NumberUtility; namespace Apewer { @@ -257,13 +258,15 @@ namespace Apewer if (_jvalue != null) { if (_jvalue.Value == null) return null; - if (_jvalue.Value is JToken) return new Json((JToken)_jvalue.Value); + if (_jvalue.Value is JValue jvalue) return jvalue.Value; + if (_jvalue.Value is JToken jtoken) return new Json(jtoken); else return _jvalue.Value; } if (_jproperty != null) { if (_jproperty.Value == null) return null; - if (_jproperty.Value is JToken) return new Json((JToken)_jproperty.Value); + if (_jproperty.Value is JValue jvalue) return jvalue.Value; + if (_jproperty.Value is JToken jtoken) return new Json(jtoken); else return _jproperty.Value; } return null; @@ -1185,26 +1188,26 @@ namespace Apewer if (recursively) continue; // 处理 Type 对象。 - if (value.GetType().Equals(typeof(Type)) && (previous.Length > 2)) - { - value = ((Type)value).FullName; - } + if (value.GetType().Equals(typeof(Type)) && (previous.Length > 2)) value = ((Type)value).FullName; // 处理 Assembly 对象。 - if (value.GetType().Equals(typeof(Assembly)) && (previous.Length > 2)) - { - value = ((Assembly)value).FullName; - } + if (value.GetType().Equals(typeof(Assembly)) && (previous.Length > 2)) value = ((Assembly)value).FullName; if (value == null) { json.AddItem(); } else if (value is DateTime) { json.AddItem(value.ToString()); } - else if (value is Boolean) { json.AddItem((Boolean)value); } - else if (value is Int32) { json.AddItem((Int32)value); } - else if (value is Int64) { json.AddItem((Int64)value); } - else if (value is Single) { json.AddItem((Single)value); } - else if (value is Double) { json.AddItem((Double)value); } - else if (value is Decimal) { json.AddItem((Decimal)value); } - else if (value is String) { json.AddItem((String)value); } + else if (value is bool) { json.AddItem((bool)value); } + else if (value is byte) { json.AddItem((byte)value); } + else if (value is sbyte) { json.AddItem((sbyte)value); } + else if (value is short) { json.AddItem((short)value); } + else if (value is ushort) { json.AddItem((ushort)value); } + else if (value is int) { json.AddItem((int)value); } + else if (value is uint) { json.AddItem((uint)value); } + else if (value is long) { json.AddItem((long)value); } + else if (value is ulong) { json.AddItem(value.ToString()); } + else if (value is float) { json.AddItem((float)value); } + else if (value is double) { json.AddItem((double)value); } + else if (value is decimal) { json.AddItem((decimal)value); } + else if (value is string) { json.AddItem((string)value); } else if (value is Json) { json.AddItem(value as Json); } else { @@ -1214,7 +1217,12 @@ namespace Apewer recursive.Add(previous); recursive.Add(value); - if (value is IDictionary) { json.AddItem(From(value as IDictionary, lower, recursive, depth, force)); } + if (value is IDictionary asExpando) + { + var expando = new Dictionary(asExpando); + json.AddItem(From(expando, lower, recursive, depth, force)); + } + else if (value is IDictionary) { json.AddItem(From(value as IDictionary, lower, recursive, depth, force)); } else if (value is IList) { json.AddItem(From(value as IList, lower, recursive, depth, force)); } else { json.AddItem(From(value, lower, recursive, depth, force)); } } @@ -1298,13 +1306,19 @@ namespace Apewer if (value == null) { json.AddItem(); } else if (value is DateTime) { json.SetProperty(field, value.ToString()); } - else if (value is Boolean) { json.SetProperty(field, (Boolean)value); } - else if (value is Int32) { json.SetProperty(field, (Int32)value); } - else if (value is Int64) { json.SetProperty(field, (Int64)value); } - else if (value is Single) { json.SetProperty(field, (Single)value); } - else if (value is Double) { json.SetProperty(field, (Double)value); } - else if (value is Decimal) { json.SetProperty(field, (Decimal)value); } - else if (value is String) { json.SetProperty(field, (String)value); } + else if (value is bool) { json.SetProperty(field, (bool)value); } + else if (value is byte) { json.SetProperty(field, (byte)value); } + else if (value is sbyte) { json.SetProperty(field, (sbyte)value); } + else if (value is short) { json.SetProperty(field, (short)value); } + else if (value is ushort) { json.SetProperty(field, (ushort)value); } + else if (value is int) { json.SetProperty(field, (int)value); } + else if (value is uint) { json.SetProperty(field, (uint)value); } + else if (value is long) { json.SetProperty(field, (long)value); } + else if (value is ulong) { json.SetProperty(field, value.ToString()); } + else if (value is float) { json.SetProperty(field, (float)value); } + else if (value is double) { json.SetProperty(field, (double)value); } + else if (value is decimal) { json.SetProperty(field, (decimal)value); } + else if (value is string) { json.SetProperty(field, (string)value); } else if (value is Json) { json.SetProperty(field, value as Json); } else { @@ -1347,8 +1361,9 @@ namespace Apewer if (entity is Json) { if (lower) Lower(entity as Json); return entity as Json; } else if (entity is String) { return From((String)entity); } - else if (entity is IDictionary) { return From(entity as IDictionary, (bool)lower); } - else if (entity is IList) { return From(entity as IList, (bool)lower); } + else if (entity is IDictionary asExpando) { return From(new Dictionary(asExpando), (bool)lower, depth, force); } + else if (entity is IDictionary) { return From(entity as IDictionary, (bool)lower, depth, force); } + else if (entity is IList) { return From(entity as IList, (bool)lower, depth, force); } var independent = RuntimeUtility.Contains(type); var checker = entity as IToJsonChecker; @@ -1522,14 +1537,8 @@ namespace Apewer { try { - if (entity is Array array) - { - array.SetValue(item, index); - } - else if (entity is IList list) - { - list.Add(entity); - } + if (entity is Array array) array.SetValue(item, index); + else if (entity is IList list) list.Add(entity); } catch (Exception ex) { @@ -1563,17 +1572,17 @@ namespace Apewer var ji = jis[index]; if (subtype.Equals(typeof(Json))) Add(array, ji, index); else if (subtype.Equals(typeof(string))) Add(array, (ji.TokenType == JTokenType.Null) ? null : ji.Text, index); - else if (subtype.Equals(typeof(byte))) Add(array, NumberUtility.Byte(ji.Text), index); - else if (subtype.Equals(typeof(short))) Add(array, NumberUtility.Int16(ji.Text), index); - else if (subtype.Equals(typeof(int))) Add(array, NumberUtility.Int32(ji.Text), index); - else if (subtype.Equals(typeof(long))) Add(array, NumberUtility.Int64(ji.Text), index); - else if (subtype.Equals(typeof(sbyte))) Add(array, NumberUtility.SByte(ji.Text), index); - else if (subtype.Equals(typeof(ushort))) Add(array, NumberUtility.UInt16(ji.Text), index); - else if (subtype.Equals(typeof(uint))) Add(array, NumberUtility.UInt32(ji.Text), index); - else if (subtype.Equals(typeof(ulong))) Add(array, NumberUtility.UInt64(ji.Text), index); - else if (subtype.Equals(typeof(float))) Add(array, NumberUtility.Single(ji.Text), index); - else if (subtype.Equals(typeof(double))) Add(array, NumberUtility.Double(ji.Text), index); - else if (subtype.Equals(typeof(decimal))) Add(array, NumberUtility.Decimal(ji.Text), index); + else if (subtype.Equals(typeof(byte))) Add(array, Byte(ji.Text), index); + else if (subtype.Equals(typeof(short))) Add(array, Int16(ji.Text), index); + else if (subtype.Equals(typeof(int))) Add(array, Int32(ji.Text), index); + else if (subtype.Equals(typeof(long))) Add(array, Int64(ji.Text), index); + else if (subtype.Equals(typeof(sbyte))) Add(array, SByte(ji.Text), index); + else if (subtype.Equals(typeof(ushort))) Add(array, UInt16(ji.Text), index); + else if (subtype.Equals(typeof(uint))) Add(array, UInt32(ji.Text), index); + else if (subtype.Equals(typeof(ulong))) Add(array, UInt64(ji.Text), index); + else if (subtype.Equals(typeof(float))) Add(array, Single(ji.Text), index); + else if (subtype.Equals(typeof(double))) Add(array, Double(ji.Text), index); + else if (subtype.Equals(typeof(decimal))) Add(array, Decimal(ji.Text), index); else { var serializable = force ? true : CanSerialize(subtype, false); @@ -1608,86 +1617,72 @@ namespace Apewer var pt = property.PropertyType; var ptname = property.PropertyType.FullName; - var parameter = new object[1] { null }; if (ptname == typeof(Json).FullName) { - if (value is Json) - { - parameter[0] = value; - setter.Invoke(entity, parameter); - } + if (value is Json) setter.Invoke(entity, new object[] { value }); } else { - switch (ptname) + if (pt.Equals(typeof(DateTime))) { - case "System.DateTime": - try - { - parameter[0] = DateTime.Parse(value.ToString()); - setter.Invoke(entity, parameter); - } - catch (Exception exception) - { - if (AllowException) throw exception; - parameter[0] = TextUtility.Empty; - setter.Invoke(entity, parameter); - } - break; - case "System.String": - if (value is Json) parameter[0] = ((Json)value).TokenType == JTokenType.Null ? null : ((Json)value).Text; - else parameter[0] = value.ToString(); - setter.Invoke(entity, parameter); - break; - case "System.Int32": - parameter[0] = NumberUtility.Int32(value.ToString()); - setter.Invoke(entity, parameter); - break; - case "System.Int64": - parameter[0] = NumberUtility.Int64(value.ToString()); - setter.Invoke(entity, parameter); - break; - case "System.Double": - parameter[0] = NumberUtility.Double(value.ToString()); - setter.Invoke(entity, parameter); - break; - case "System.Decimal": - parameter[0] = NumberUtility.Decimal(value.ToString()); - setter.Invoke(entity, parameter); - break; - default: - var serializable = force; - if (!serializable) serializable = CanSerialize(property.PropertyType, false); - if (serializable && (value is Json)) + try + { + setter.Invoke(entity, new object[] { DateTime.Parse(value.ToString()) }); + } + catch (Exception exception) + { + if (AllowException) throw exception; + setter.Invoke(entity, new object[] { "" }); + } + } + else if (pt.Equals(typeof(string))) + { + if (value is Json asJson) setter.Invoke(entity, new object[] { ((Json)value).TokenType == JTokenType.Null ? null : ((Json)value).Text }); + else setter.Invoke(entity, new object[] { value.ToString() }); + } + else if (pt.Equals(typeof(bool))) setter.Invoke(entity, new object[] { Boolean(value) }); + else if (pt.Equals(typeof(byte))) setter.Invoke(entity, new object[] { Byte(value) }); + else if (pt.Equals(typeof(sbyte))) setter.Invoke(entity, new object[] { SByte(value) }); + else if (pt.Equals(typeof(short))) setter.Invoke(entity, new object[] { Int16(value) }); + else if (pt.Equals(typeof(ushort))) setter.Invoke(entity, new object[] { UInt16(value) }); + else if (pt.Equals(typeof(int))) setter.Invoke(entity, new object[] { Int32(value) }); + else if (pt.Equals(typeof(uint))) setter.Invoke(entity, new object[] { UInt32(value) }); + else if (pt.Equals(typeof(long))) setter.Invoke(entity, new object[] { Int64(value) }); + else if (pt.Equals(typeof(ulong))) setter.Invoke(entity, new object[] { UInt64(value) }); + else if (pt.Equals(typeof(float))) setter.Invoke(entity, new object[] { Float(value) }); + else if (pt.Equals(typeof(double))) setter.Invoke(entity, new object[] { Double(value) }); + else if (pt.Equals(typeof(decimal))) setter.Invoke(entity, new object[] { Decimal(value) }); + else + { + var serializable = force; + if (!serializable) serializable = CanSerialize(property.PropertyType, false); + if (serializable && (value is Json)) + { + switch (((Json)value).TokenType) { - switch (((Json)value).TokenType) - { - case JTokenType.Object: - var subobject = Activator.CreateInstance(property.PropertyType); - Object(subobject, (Json)value, ignoreCase, ignoreCharacters, force); - parameter[0] = subobject; - setter.Invoke(entity, parameter); - break; - case JTokenType.Array: - object subarray; - if (pt.BaseType != null && pt.BaseType.Equals(typeof(Array))) - { - subarray = new object(); - var length = ((Json)value).GetItems().Length; - subarray = pt.InvokeMember("Set", BindingFlags.CreateInstance, null, subarray, new object[] { length }); - } - else - { - subarray = Activator.CreateInstance(property.PropertyType); - } - Array(subarray, (Json)value, ignoreCase, ignoreCharacters, force); - parameter[0] = subarray; - setter.Invoke(entity, parameter); - break; - } + case JTokenType.Object: + var subobject = Activator.CreateInstance(property.PropertyType); + Object(subobject, (Json)value, ignoreCase, ignoreCharacters, force); + setter.Invoke(entity, new object[] { subobject }); + break; + case JTokenType.Array: + object subarray; + if (pt.BaseType != null && pt.BaseType.Equals(typeof(Array))) + { + subarray = new object(); + var length = ((Json)value).GetItems().Length; + subarray = pt.InvokeMember("Set", BindingFlags.CreateInstance, null, subarray, new object[] { length }); + } + else + { + subarray = Activator.CreateInstance(property.PropertyType); + } + Array(subarray, (Json)value, ignoreCase, ignoreCharacters, force); + setter.Invoke(entity, new object[] { subarray }); + break; } - break; + } } } } @@ -1725,7 +1720,12 @@ namespace Apewer if (value is Json) return SetProperty(name, (Json)value); if (value is string) return SetProperty(name, (string)value); if (value is bool) return SetProperty(name, (bool)value); + if (value is byte) return SetProperty(name, (byte)value); + if (value is sbyte) return SetProperty(name, (sbyte)value); + if (value is short) return SetProperty(name, (short)value); + if (value is ushort) return SetProperty(name, (ushort)value); if (value is int) return SetProperty(name, (int)value); + if (value is uint) return SetProperty(name, (uint)value); if (value is long) return SetProperty(name, (long)value); if (value is float) return SetProperty(name, (float)value); if (value is double) return SetProperty(name, (double)value); @@ -2017,45 +2017,16 @@ namespace Apewer } /// 序列化指定对象为 JSON 字符串。 - /// 将要序列化的对象。 - /// 发生错误时的返回值。 - /// 使子对象缩进。 - public static string SerializeObject(object @object, bool indented = false, string onError = null) - { - try - { - return JsonConvert.SerializeObject(@object, indented ? Formatting.Indented : Formatting.None); - } - catch { return onError; } - } + /// + public static string SerializeObject(object @object, Type type = null, bool indented = false, string onError = null) => JsonConvert.SerializeObject(@object, type, indented ? Formatting.Indented : Formatting.None, null); /// 反序列化 JSON 字符串为指定的类型。 - /// 要反序列化的类型。 - /// 将要反序列化的 JSON 字符串。 - /// 发生错误时的返回值。 - public static T DeserializeObject(string json, T onError = default(T)) - { - try - { - return JsonConvert.DeserializeObject(json); - } - catch { return onError; } - } + /// + public static T DeserializeObject(string json) => (T)DeserializeObject(json, typeof(T)); - /// 反序列化 JSON 字符串为指定的匿名类型。 - /// 要反序列化的类型 - /// 将要反序列化的 JSON 字符串。 - /// 匿名对象。 - /// 发生错误时的返回值。 - /// 匿名对象。 - public static T DeserializeAnonymous(string json, T anonymousObject, T onError = default(T)) - { - try - { - return JsonConvert.DeserializeAnonymousType(json, anonymousObject); - } - catch { return onError; } - } + /// 反序列化 JSON 字符串为指定的类型。 + /// + public static object DeserializeObject(string json, Type type = null) => JsonConvert.DeserializeObject(json, type, (JsonSerializerSettings)null); #if !NET20 diff --git a/Apewer/NetworkUtility.cs b/Apewer/NetworkUtility.cs index cfa990e..8b2405a 100644 --- a/Apewer/NetworkUtility.cs +++ b/Apewer/NetworkUtility.cs @@ -379,6 +379,127 @@ namespace Apewer } } + /// 按文件扩展名获取 Content-Type 值。 + public static string Mime(string extension) + { + const string Default = "application/octet-stream"; + if (string.IsNullOrEmpty(extension)) return Default; + + var split = extension.Split('.'); + var lower = split.Length > 0 ? null : split[split.Length -1].Lower(); + switch (lower) + { + // text/plain; charset=utf-8 + case "css": return "text/css"; + case "htm": return "text/html"; + case "html": return "text/html"; + case "ini": return "text/ini"; + case "js": return "application/javascript"; + case "json": return "text/json"; + case "shtml": return "text/html"; + case "sh": return "text/plain"; + case "txt": return "text/plain"; + } + switch (lower) + { + case "jad": return "text/vnd.sun.j2me.app-descriptor"; + case "m3u8": return "text/vnd.apple.mpegurl"; // application/vnd.apple.mpegurl + case "xml": return "text/xml"; + case "htc": return "text/x-component"; + case "mml": return "text/mathml"; + case "wml": return "text/vnd.wap.wml"; + } + switch (lower) + { + case "3gp": return "video/3gpp"; + case "3gpp": return "video/3gpp"; + case "7z": return "application/x-7z-compressed"; + case "ai": return "application/postscript"; + case "asf": return "video/x-ms-asf"; + case "asx": return "video/x-ms-asf"; + case "atom": return "application/atom+xml"; + case "avi": return "video/x-msvideo"; + case "bmp": return "image/x-ms-bmp"; + case "cco": return "application/x-cocoa"; + case "crt": return "application/x-x509-ca-cert"; + case "der": return "application/x-x509-ca-cert"; + case "doc": return "application/msword"; + case "docx": return "application/vnd.openxmlformats-officedocument.wordprocessingml.document"; + case "ear": return "application/java-archive"; + case "eot": return "application/vnd.ms-fontobject"; + case "eps": return "application/postscript"; + case "flv": return "video/x-flv"; + case "gif": return "image/gif"; + case "hqx": return "application/mac-binhex40"; + case "ico": return "image/x-icon"; + case "jar": return "application/java-archive"; + case "jardiff": return "application/x-java-archive-diff"; + case "jng": return "image/x-jng"; + case "jnlp": return "application/x-java-jnlp-file"; + case "jpeg": return "image/jpeg"; + case "jpg": return "image/jpeg"; + case "kar": return "audio/midi"; + case "kml": return "application/vnd.google-earth.kml+xml"; + case "kmz": return "application/vnd.google-earth.kmz"; + case "m4a": return "audio/x-m4a"; + case "m4v": return "video/x-m4v"; + case "mid": return "audio/midi"; + case "midi": return "audio/midi"; + case "mng": return "video/x-mng"; + case "mov": return "video/quicktime"; + case "mp3": return "audio/mpeg"; + case "mp4": return "video/mp4"; + case "mpeg": return "video/mpeg"; + case "mpg": return "video/mpeg"; + case "odg": return "application/vnd.oasis.opendocument.graphics"; + case "odp": return "application/vnd.oasis.opendocument.presentation"; + case "ods": return "application/vnd.oasis.opendocument.spreadsheet"; + case "odt": return "application/vnd.oasis.opendocument.text"; + case "ogg": return "audio/ogg"; + case "pdb": return "application/x-pilot"; + case "pdf": return "application/pdf"; + case "pem": return "application/x-x509-ca-cert"; + case "pl": return "application/x-perl"; + case "pm": return "application/x-perl"; + case "png": return "image/png"; + case "ppt": return "application/vnd.ms-powerpoint"; + case "pptx": return "application/vnd.openxmlformats-officedocument.presentationml.presentation"; + case "prc": return "application/x-pilot"; + case "ps": return "application/postscript"; + case "ra": return "audio/x-realaudio"; + case "rar": return "application/x-rar-compressed"; + case "rpm": return "application/x-redhat-package-manager"; + case "rss": return "application/rss+xml"; + case "rtf": return "application/rtf"; + case "run": return "application/x-makeself"; + case "sea": return "application/x-sea"; + case "sit": return "application/x-stuffit"; + case "svg": return "image/svg+xml"; + case "svgz": return "image/svg+xml"; + case "swf": return "application/x-shockwave-flash"; + case "tcl": return "application/x-tcl"; + case "tif": return "image/tiff"; + case "tiff": return "image/tiff"; + case "tk": return "application/x-tcl"; + case "ts": return "video/mp2t"; + case "war": return "application/java-archive"; + case "wbmp": return "image/vnd.wap.wbmp"; + case "webm": return "video/webm"; + case "webp": return "image/webp"; + case "wmlc": return "application/vnd.wap.wmlc"; + case "wmv": return "video/x-ms-wmv"; + case "woff": return "font/woff"; + case "woff2": return "font/woff2"; + case "xhtml": return "application/xhtml+xml"; + case "xls": return "application/vnd.ms-excel"; + case "xlsx": return "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"; + case "xpi": return "application/x-xpinstall"; + case "xspf": return "application/xspf+xml"; + case "zip": return "application/zip"; + } + return Default; + } + #endregion #region Port diff --git a/Apewer/NumberUtility.cs b/Apewer/NumberUtility.cs index 6dc0d78..ad9e3e5 100644 --- a/Apewer/NumberUtility.cs +++ b/Apewer/NumberUtility.cs @@ -260,7 +260,7 @@ namespace Apewer { if (@object == null) return default(T); if (@object is T) return (T)@object; - var text = (@object is string) ? (string)@object : ""; + var text = (@object is string) ? (string)@object : @object.ToString(); var trim = Trim(text); if (trim == null) return default; diff --git a/Apewer/ObjectSet.cs b/Apewer/ObjectSet.cs index 682ea66..f01457c 100644 --- a/Apewer/ObjectSet.cs +++ b/Apewer/ObjectSet.cs @@ -16,7 +16,9 @@ namespace Apewer { /// - public ObjectSet(bool trimKey = false) : base(trimKey) { } + /// 使用 get 和 set 访问器时候修建 Key。 + /// 对动态属性的访问始终返回 TRUE 值。 + public ObjectSet(bool trimKey = false, bool always = false) : base(trimKey, always) { } } @@ -26,10 +28,9 @@ namespace Apewer { private Dictionary _origin; - private bool _trimkey = false; - private bool _locked = false; + internal bool _always = false; /// 获取或设置字典内容。 public T this[string key] { get { return Get(key); } set { Set(key, value); } } @@ -42,9 +43,12 @@ namespace Apewer internal bool TrimKey { get { return _trimkey; } set { _trimkey = value; } } /// 构造函数。 - public ObjectSet(bool trimKey = false) + /// 使用 get 和 set 访问器时候修建 Key。 + /// 对动态属性的访问始终返回 TRUE 值。 + public ObjectSet(bool trimKey = false, bool always = false) { _trimkey = trimKey; + _always = always; _origin = new Dictionary(); } @@ -105,6 +109,7 @@ namespace Apewer public partial class ObjectSet : DynamicObject { + /// public override IEnumerable GetDynamicMemberNames() { return new List(_origin.Keys).ToArray(); @@ -115,13 +120,33 @@ namespace Apewer { var contains = false; result = Get(binder.Name, ref contains); + if (_always) return true; return contains; } /// public override bool TrySetMember(SetMemberBinder binder, object value) { - return Set(binder.Name, (T)value); + var setted = Set(binder.Name, (T)value); + if (_always) return true; + return setted; + } + + internal static ExpandoObject Expando(ObjectSet os) + { + if (os == null) return null; + var eo = new ExpandoObject(); + var dict = eo as IDictionary; + foreach (var kvp in os._origin) dict.Add(kvp.Key, kvp.Value); + return eo; + } + + internal static ExpandoObject[] Expando(ObjectSet[] oss) + { + if (oss == null) return null; + var eos = new ExpandoObject[oss.Length]; + for (var i = 0; i < oss.Length; i++) eos[i] = Expando(oss[i]); + return eos; } } diff --git a/Apewer/RuntimeUtility.cs b/Apewer/RuntimeUtility.cs index a24795c..0b1a137 100644 --- a/Apewer/RuntimeUtility.cs +++ b/Apewer/RuntimeUtility.cs @@ -612,7 +612,7 @@ namespace Apewer private static Class _InIIS = null; /// 当前应用程序由 IIS 托管。 - public static bool InIIS() + private static bool InIIS() { if (_InIIS != null) return _InIIS.Value; diff --git a/Apewer/Source/IDbAdo.cs b/Apewer/Source/IDbAdo.cs index 1f2c55a..88469c7 100644 --- a/Apewer/Source/IDbAdo.cs +++ b/Apewer/Source/IDbAdo.cs @@ -21,31 +21,52 @@ namespace Apewer.Source /// 连接数据库,若未连接则尝试连接,返回错误信息。 string Connect(); + /// 更改已打开的数据库。 + string Change(string store); + + /// 关闭连接,并释放对象所占用的系统资源。 + void Close(); + #endregion #region ADO /// 查询。 - IQuery Query(string statement); - - /// 查询。 - IQuery Query(string statement, IEnumerable parameters); + IQuery Query(string sql, IEnumerable parameters = null); /// 执行。 - IExecute Execute(string statement); + IExecute Execute(string sql, IEnumerable parameters = null, bool autoTransaction = true); - /// 执行。 - IExecute Execute(string statement, IEnumerable parameters); + /// 创建参数。 + /// + IDataParameter Parameter(string name, object value); - // /// 获取当前的事务对象。 - // IDbTransaction Transaction { get; } + /// 创建参数。 + /// + IDataParameter Parameter(string name, object value, DbType type); #endregion #region Transaction + /// 获取已启动的事务。 + IDbTransaction Transaction { get; } + + /// + /// 使用默认的事务锁定行为启动事务。 + /// Chaos
无法覆盖隔离级别更高的事务中的挂起的更改。
+ /// ReadCommitted
在正在读取数据时保持共享锁,以避免脏读,但是在事务结束之前可以更改数据,从而导致不可重复的读取或幻像数据。
+ /// ReadUncommitted
可以进行脏读,意思是说,不发布共享锁,也不接受独占锁。
+ /// RepeatableRead
在查询中使用的所有数据上放置锁,以防止其他用户更新这些数据。 防止不可重复的读取,但是仍可以有幻像行。
+ /// Serializable
在 System.Data.DataSet 上放置范围锁,以防止在事务完成之前由其他用户更新行或向数据集中插入行。
+ /// Snapshot
通过在一个应用程序正在修改数据时存储另一个应用程序可以读取的相同数据版本来减少阻止。 表示您无法从一个事务中看到在其他事务中进行的更改,即便重新查询也是如此。
+ /// Unspecified = -1
正在使用与指定隔离级别不同的隔离级别,但是无法确定该级别。
+ ///
+ /// 在连接的生命周期结束时未结束事务,指定 TRUE 将自动提交,指定 FALSE 将自动回滚。 + string Begin(bool commit = false); + /// - /// 启动事务,可指定事务锁定行为。 + /// 使用指定的事务锁定行为启动事务。 /// Chaos
无法覆盖隔离级别更高的事务中的挂起的更改。
/// ReadCommitted
在正在读取数据时保持共享锁,以避免脏读,但是在事务结束之前可以更改数据,从而导致不可重复的读取或幻像数据。
/// ReadUncommitted
可以进行脏读,意思是说,不发布共享锁,也不接受独占锁。
@@ -56,7 +77,7 @@ namespace Apewer.Source ///
/// 在连接的生命周期结束时未结束事务,指定 TRUE 将自动提交,指定 FALSE 将自动回滚。 /// 指定事务锁定行为,不指定时将使用默认值。 - string Begin(bool commit = false, Class isolation = null); + string Begin(bool commit, IsolationLevel isolation); /// 提交事务。 /// 异常常见于事务已经提交或连接已断开。 diff --git a/Apewer/Source/IDbOrm.cs b/Apewer/Source/IDbOrm.cs index ea25d91..dd634b5 100644 --- a/Apewer/Source/IDbOrm.cs +++ b/Apewer/Source/IDbOrm.cs @@ -10,7 +10,7 @@ namespace Apewer.Source public interface IDbOrm { - #region Orm + #region object /// 初始化指定类型,以创建表或增加字段。 /// 要初始化的类型。 @@ -27,6 +27,21 @@ namespace Apewer.Source /// 错误信息。当成功时候返回空字符串。 public string Insert(object record, string table = null); + /// 使用指定语句查询,获取查询结果。 + /// 目标记录的类型。 + /// 要执行的 SQL 语句。 + /// 为 SQL 命令提供参数。 + public Result Query(Type model, string sql, IEnumerable parameters = null); + + /// 使用指定语句查询,获取查询结果。 + /// 要执行的 SQL 语句。 + /// 为 SQL 命令提供参数。 + public Result Query(string sql, IEnumerable parameters = null) where T : class, new(); + + #endregion + + #region record + /// 更新记录。 /// 要插入的记录实体。 /// 插入到指定表。当不指定时,由 record 类型决定。 @@ -46,32 +61,21 @@ namespace Apewer.Source /// 目标记录的类型。 /// 目标记录的主键。 /// 要求目标记录具有的 Flag 属性,当指定 0 时忽略此要求。 - public Result Get(Type model, string key, long flag = 0); + public Result Record(Type model, string key, long flag = 0); /// 获取具有指定 Key 的记录,并要求记录具有指定的 Flag 属性。 /// 目标记录的主键。 /// 要求目标记录具有的 Flag 属性,当指定 0 时忽略此要求。 - public Result Get(string key, long flag = 0) where T : class, IRecord, new(); - - /// 使用指定语句查询,获取查询结果。 - /// 目标记录的类型。 - /// 要执行的 SQL 语句。 - /// 为 SQL 命令提供参数。 - public Result Query(Type model, string sql, IEnumerable parameters = null); - - /// 使用指定语句查询,获取查询结果。 - /// 要执行的 SQL 语句。 - /// 为 SQL 命令提供参数。 - public Result Query(string sql, IEnumerable parameters = null) where T : class, new(); + public Result Record(string key, long flag = 0) where T : class, IRecord, new(); /// 查询所有记录。 /// 目标记录的类型。 /// 要求目标记录具有的 Flag 属性,当指定 0 时忽略此要求。 - public Result Query(Type model, long flag = 0); + public Result Records(Type model, long flag = 0); /// 查询所有记录。 /// 要求目标记录具有的 Flag 属性,当指定 0 时忽略此要求。 - public Result Query(long flag = 0) where T : class, IRecord, new(); + public Result Records(long flag = 0) where T : class, IRecord, new(); #endregion diff --git a/Apewer/Source/SourceUtility.cs b/Apewer/Source/SourceUtility.cs index fe960b6..9a3cfbd 100644 --- a/Apewer/Source/SourceUtility.cs +++ b/Apewer/Source/SourceUtility.cs @@ -495,6 +495,34 @@ namespace Apewer.Source return value; } + /// 查询。 + /// + public static IQuery Query(this IDbClient dbClient, string sql, IEnumerable> parameters) + { + if (dbClient == null) throw new ArgumentNullException(nameof(dbClient)); + return dbClient.Query(sql, Parameters(dbClient, parameters)); + } + + /// 执行 SQL 语句,并加入参数。 + /// + public static IExecute Execute(this IDbClient dbClient, string sql, IEnumerable> parameters, bool autoTransaction = true) + { + if (dbClient == null) throw new ArgumentNullException(nameof(dbClient)); + return dbClient.Execute(sql, Parameters(dbClient, parameters), autoTransaction); + } + + /// + static List Parameters(IDbClient dbClient, IEnumerable> parameters) + { + if (dbClient == null) throw new ArgumentNullException(nameof(dbClient)); + var ps = new List(); + if (parameters != null) + { + foreach (var parameter in parameters) ps.Add(dbClient.Parameter(parameter.Key, parameter.Value)); + } + return ps; + } + #endregion #region SQL @@ -529,6 +557,9 @@ namespace Apewer.Source return t; } + /// 限定名称文本,只允许包含字母、数字和下划线。 + public static string SafeName(this string name) => TextUtility.Restrict(name, "0123456789_ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"); + #endregion #region DataTable @@ -536,6 +567,7 @@ namespace Apewer.Source /// 将多个实体元素转换为 DataTable。 /// 实体元素的类型。 /// 实体元素。 + /// 设置 的名称。 /// public static DataTable DataTable(this IEnumerable items, string tableName = null) { @@ -584,6 +616,36 @@ namespace Apewer.Source return table; } + /// 转换 数组,每行记录为一个 ObjectSet 对象。 + /// 当参数 table 无效时返回 0 长度的 数组。 + internal static ObjectSet[] ObjectSet(this DataTable table) + { + if (table == null) return new ObjectSet[0]; + + var cc = table.Columns.Count; + var cns = new string[cc]; + for (var c = 0; c < cc; c++) cns[c] = table.Columns[c].ColumnName; + + var rc = table.Rows.Count; + var oss = new ObjectSet[rc]; + for (var r = 0; r < table.Rows.Count; r++) + { + var os = new ObjectSet(); + var osd = os.Origin; + for (var c = 0; c < cc; c++) + { + var cn = cns[c]; + if (string.IsNullOrEmpty(cn)) continue; + if (osd.ContainsKey(cn)) continue; + var v = table.Rows[r][c]; + if (v.IsNull()) v = null; + osd.Add(cn, v); + } + oss[r] = os; + } + return oss; + } + #endregion } diff --git a/Apewer/Web/ApiModel.cs b/Apewer/Web/ApiModel.cs index d5cd9f2..941d022 100644 --- a/Apewer/Web/ApiModel.cs +++ b/Apewer/Web/ApiModel.cs @@ -1,11 +1,14 @@ using System; +using System.Collections.Generic; using System.IO; +using System.Net; +using System.Text; namespace Apewer.Web { /// Response 模型。 - public abstract class ApiModel + public abstract class ApiModel : IToJson { #region @@ -44,48 +47,61 @@ namespace Apewer.Web /// 在 Response 头中添加用于设置文件名的属性。 protected void SetAttachment() { - if (Provider == null) return; + if (_provider == null) return; var name = Attachment; if (string.IsNullOrEmpty(name)) return; var encoded = TextUtility.EncodeUrl(name); - Provider.SetHeader("Content-Disposition", $"attachment; filename={encoded}"); + _provider.SetHeader("Content-Disposition", $"attachment; filename={encoded}"); } - /// 以指定参数输出。 - protected void Output(byte[] bytes) + private void OutputCommon() { - if (Provider == null) return; - try - { - SetAttachment(); - Provider.SetCache(Expires); - Provider.SetContentType(ContentType); + if (_provider == null) return; + + var status = Status > 0 ? Status : 200; + if (status != 200) _provider.SetStatus(status); - var length = bytes == null ? 0 : bytes.Length; - Provider.SetContentLength(length); - if (length > 0) Provider.ResponseBody().Write(bytes); + var headers = Headers; + if (headers != null) + { + foreach (var header in headers) + { + if (header.Key.IsEmpty()) continue; + if (header.Value.IsEmpty()) continue; + _provider.SetHeader(header.Key, header.Value); + } } - catch { } + + SetAttachment(); + _provider.SetCache(Expires); + _provider.SetContentType(ContentType); + } + + /// 以指定参数输出。 + protected void Output(byte[] bytes) + { + if (_provider == null) return; + OutputCommon(); + var length = bytes == null ? 0 : bytes.Length; + _provider.SetContentLength(length); + if (length > 0) _provider.ResponseBody().Write(bytes); } /// 以指定参数输出。 protected void Output(Stream stream, bool dispose) { - if (Provider == null) return; - try - { - SetAttachment(); - Provider.SetCache(Expires); - Provider.SetContentType(ContentType); - Provider.SetContentLength(stream.Length - stream.Position); - Provider.ResponseBody().Write(stream); - } - catch { } - if (dispose) RuntimeUtility.Dispose(stream); + if (_provider == null) return; + OutputCommon(); + _provider.SetContentLength(stream.Length - stream.Position); + _provider.ResponseBody().Write(stream); } #endregion + /// 状态。 + /// 默认值:200。 + public virtual int Status { get; set; } + /// 内容类型。 public virtual string ContentType { get; set; } @@ -95,6 +111,9 @@ namespace Apewer.Web /// 设置文件名,告知客户端此附件处理此响应。 public virtual string Attachment { get; set; } + /// 设置 Response 头。 + public virtual StringPairs Headers { get; set; } + /// 执行输出。 /// 此方法由 API 调用器发起调用,用户程序不应主动调用。 /// @@ -103,11 +122,21 @@ namespace Apewer.Web /// 创建对象实例,并设置默认属性。 public ApiModel() { + Status = 200; ContentType = "application/octet-stream"; Expires = 0; Attachment = null; } + /// + public Json ToJson() + { + var json = new Json(); + json.SetProperty("status", Status); + json.SetProperty("content-type", ContentType); + return json; + } + } /// 输出二进制的 Response 模型。 @@ -120,6 +149,13 @@ namespace Apewer.Web /// 输出字节数组。 public override void Output() => Output(Bytes); + /// 创建对象实例,并设置默认属性。 + public ApiBytesModel(byte[] bytes = null, string contentType = "application/octet-stream") + { + Bytes = bytes; + ContentType = contentType; + } + } /// 输出二进制的 Response 模型。 @@ -143,7 +179,11 @@ namespace Apewer.Web } /// 创建对象实例,并设置默认属性。 - public ApiStreamModel() => AutoDispose = true; + public ApiStreamModel(Stream stream = null, bool autoDispose = true) + { + Stream = stream; + AutoDispose = autoDispose; + } } @@ -151,8 +191,18 @@ namespace Apewer.Web public class ApiFileModel : ApiModel { + string _path; + + void SetPath(string path) + { + if (string.IsNullOrEmpty(path)) throw new FileNotFoundException("没有指定文件路径。"); + if (!File.Exists(path)) throw new FileNotFoundException($"文件 {path} 不存在。"); + _path = path; + } + /// 将要读取的文件所在路径,用于向 Body 写入。 - public string Path { get; set; } + /// + public string Path { get => _path; set => SetPath(value); } /// 输出指定路径的文件。 public override void Output() @@ -169,6 +219,15 @@ namespace Apewer.Web } catch { } } + + /// + /// + public ApiFileModel(string path, string name = null) + { + SetPath(path); + Attachment = name; + } + } /// 输出文本的 Response 模型。 @@ -182,7 +241,11 @@ namespace Apewer.Web public override void Output() => Output(TextUtility.Bytes(Text)); /// 创建对象实例,并设置默认属性。 - public ApiTextModel() => ContentType = "text/plain; charset=utf-8"; + public ApiTextModel(string text = null, string contentType = "text/plain") + { + ContentType = contentType; + Text = text; + } } @@ -210,10 +273,12 @@ namespace Apewer.Web } /// 创建对象实例,并设置默认属性。 - public ApiJsonModel() + public ApiJsonModel(Json json = null, bool camel = false, bool indented = true) { - ContentType = "text/plain; charset=utf-8"; - Indented = true; + ContentType = "application/json"; + Camel = camel; + Indented = indented; + Json = json; } } @@ -234,36 +299,98 @@ namespace Apewer.Web Provider.SetRedirect(Location); } + /// 重定向到指定的 URL。 + public ApiRedirectModel(string location = null) + { + Location = location; + } + } - /// 输出带有指定 Status 的 Response 模型。 - public class ApiStatusModel : ApiModel + /// 输出说明 Exception 的 Response 模型。 + public class ApiExceptionModel : ApiModel { - /// 状态。 - /// 默认值:200。 - public int Status { get; set; } = 200; - - /// 向 Body 写入的字节数组。 - public byte[] Bytes { get; set; } + /// 要输出的 Exception。 + public Exception Exception { get; set; } - /// 执行重定向。 + /// 解析 Exception 的内容并输出。 public override void Output() { - var status = Status > 0 ? Status : 200; - Provider.SetStatus(status.ToString()); - Output(Bytes); + Status = 500; + ContentType = "text/plain"; + Output(ToString().Bytes()); } /// - public ApiStatusModel() { } + public ApiExceptionModel(Exception exception = null) + { + Exception = exception; + } /// - public ApiStatusModel(int status) + public override string ToString() { - Status = status; + var ex = Exception; + var sb = new StringBuilder(); + if (ex == null) + { + sb.Append("Invalid Exception"); + } + else + { + try + { + sb.Append(Exception.GetType().FullName); + + var props = ex.GetType().GetProperties(); + foreach (var prop in props) + { + var getter = prop.GetGetMethod(); + if (getter == null) continue; + + var value = getter.Invoke(ex, null); + if (value == null) continue; + + sb.Append("\r\n\r\n"); + sb.Append(prop.Name); + sb.Append(" : "); + sb.Append(prop.PropertyType.FullName); + + sb.Append("\r\n"); + if (value is Json) sb.Append(((Json)value).ToString(true)); + else sb.Append(value.ToString() ?? ""); + } + + // sb.Append("\r\n\r\nToString\r\n"); + // sb.Append("\r\n"); + // sb.Append(ex.ToString()); + } + catch { } + } + sb.Append("\r\n"); + var text = sb.ToString(); + return text; } } + /// 输出带有指定 Status 的 Response 模型。 + public class ApiStatusModel : ApiModel + { + + /// 向 Body 写入的字节数组。 + public byte[] Bytes { get; set; } + + /// 执行重定向。 + public override void Output() => Output(Bytes); + + /// + public ApiStatusModel(int status = 200) => Status = status; + + /// + public ApiStatusModel(HttpStatusCode status) => Status = (int)status; + + } + } diff --git a/Apewer/Web/ApiOptions.cs b/Apewer/Web/ApiOptions.cs index 5bd93b0..ae62723 100644 --- a/Apewer/Web/ApiOptions.cs +++ b/Apewer/Web/ApiOptions.cs @@ -47,8 +47,8 @@ namespace Apewer.Web public bool UpgradeHttps { get; set; } = false; /// 在响应中包含 Access-Control 属性。 - /// 默认值:包含。 - public bool WithAccessControl { get; set; } = true; + /// 默认值:不包含。 + public bool WithAccessControl { get; set; } = false; /// 在响应中包含时间属性。 /// 默认值:不包含。 diff --git a/Apewer/Web/ApiProvider.cs b/Apewer/Web/ApiProvider.cs index a43b86c..880ddbf 100644 --- a/Apewer/Web/ApiProvider.cs +++ b/Apewer/Web/ApiProvider.cs @@ -25,6 +25,9 @@ namespace Apewer.Web /// 写入响应前的检查,可返回错误信息以终止输出。 public virtual string PreWrite() { return null; } + /// 发送完毕后执行。 + public virtual void Sent() { } + /// 结束本次请求和响应。 public virtual void End() { } @@ -64,7 +67,7 @@ namespace Apewer.Web #region Response /// 设置 HTTP 状态。 - public abstract void SetStatus(string status); + public abstract void SetStatus(int status, int subStatus = 0); /// 设置响应的头。 public abstract string SetHeader(string name, string value); diff --git a/Apewer/Web/ApiRequest.cs b/Apewer/Web/ApiRequest.cs index b3cdd26..27640e6 100644 --- a/Apewer/Web/ApiRequest.cs +++ b/Apewer/Web/ApiRequest.cs @@ -74,6 +74,9 @@ namespace Apewer.Web /// public Json PostJson { get; set; } + /// 已解码的 POST 参数,仅当内容类型为 application/x-www-form-urlencoded 时有效。 + public StringPairs Form { get; set; } + /// public Json Data { diff --git a/Apewer/Web/ApiUtility.cs b/Apewer/Web/ApiUtility.cs index e98172d..cf1e7fe 100644 --- a/Apewer/Web/ApiUtility.cs +++ b/Apewer/Web/ApiUtility.cs @@ -367,10 +367,9 @@ namespace Apewer.Web #region ApiResponse - internal static ApiModel Model(ApiResponse response, string type, ApiModel model) + internal static ApiModel Model(ApiResponse response, ApiModel model) { if (response == null && model == null) return null; - if (!string.IsNullOrEmpty(type)) model.ContentType = type; response.Model = model; return model; } diff --git a/Apewer/Web/StaticController.cs b/Apewer/Web/StaticController.cs index fcd15d6..748283c 100644 --- a/Apewer/Web/StaticController.cs +++ b/Apewer/Web/StaticController.cs @@ -113,7 +113,7 @@ namespace Apewer.Web } /// 从扩展名获取内容类型。 - protected virtual string ContentType(string extension) => Mime(extension); + protected virtual string ContentType(string extension) => NetworkUtility.Mime(extension); /// 从扩展名和文件路径获取过期时间。 /// 默认值:0,不缓存。 @@ -138,7 +138,7 @@ namespace Apewer.Web // 按扩展名获取 Content-Type。 var type = ContentType(ext); - if (string.IsNullOrEmpty(type)) type = Mime(ext); + if (string.IsNullOrEmpty(type)) type = NetworkUtility.Mime(ext); // Server Side Includes if (AllowSSI && ext == "html" || ext == "htm" || ext == "shtml") @@ -396,125 +396,6 @@ namespace Apewer.Web return false; } - static string Mime(string extension) - { - const string Default = "application/octet-stream"; - if (string.IsNullOrEmpty(extension)) return Default; - - var lower = extension.ToLower(); - switch (lower) - { - // text/plain; charset=utf-8 - case "css": return "text/css"; - case "htm": return "text/html"; - case "html": return "text/html"; - case "ini": return "text/ini"; - case "js": return "application/javascript"; - case "json": return "text/json"; - case "shtml": return "text/html"; - case "sh": return "text/plain"; - case "txt": return "text/plain"; - } - switch (lower) - { - case "jad": return "text/vnd.sun.j2me.app-descriptor"; - case "m3u8": return "text/vnd.apple.mpegurl"; // application/vnd.apple.mpegurl - case "xml": return "text/xml"; - case "htc": return "text/x-component"; - case "mml": return "text/mathml"; - case "wml": return "text/vnd.wap.wml"; - } - switch (lower) - { - case "3gp": return "video/3gpp"; - case "3gpp": return "video/3gpp"; - case "7z": return "application/x-7z-compressed"; - case "ai": return "application/postscript"; - case "asf": return "video/x-ms-asf"; - case "asx": return "video/x-ms-asf"; - case "atom": return "application/atom+xml"; - case "avi": return "video/x-msvideo"; - case "bmp": return "image/x-ms-bmp"; - case "cco": return "application/x-cocoa"; - case "crt": return "application/x-x509-ca-cert"; - case "der": return "application/x-x509-ca-cert"; - case "doc": return "application/msword"; - case "docx": return "application/vnd.openxmlformats-officedocument.wordprocessingml.document"; - case "ear": return "application/java-archive"; - case "eot": return "application/vnd.ms-fontobject"; - case "eps": return "application/postscript"; - case "flv": return "video/x-flv"; - case "gif": return "image/gif"; - case "hqx": return "application/mac-binhex40"; - case "ico": return "image/x-icon"; - case "jar": return "application/java-archive"; - case "jardiff": return "application/x-java-archive-diff"; - case "jng": return "image/x-jng"; - case "jnlp": return "application/x-java-jnlp-file"; - case "jpeg": return "image/jpeg"; - case "jpg": return "image/jpeg"; - case "kar": return "audio/midi"; - case "kml": return "application/vnd.google-earth.kml+xml"; - case "kmz": return "application/vnd.google-earth.kmz"; - case "m4a": return "audio/x-m4a"; - case "m4v": return "video/x-m4v"; - case "mid": return "audio/midi"; - case "midi": return "audio/midi"; - case "mng": return "video/x-mng"; - case "mov": return "video/quicktime"; - case "mp3": return "audio/mpeg"; - case "mp4": return "video/mp4"; - case "mpeg": return "video/mpeg"; - case "mpg": return "video/mpeg"; - case "odg": return "application/vnd.oasis.opendocument.graphics"; - case "odp": return "application/vnd.oasis.opendocument.presentation"; - case "ods": return "application/vnd.oasis.opendocument.spreadsheet"; - case "odt": return "application/vnd.oasis.opendocument.text"; - case "ogg": return "audio/ogg"; - case "pdb": return "application/x-pilot"; - case "pdf": return "application/pdf"; - case "pem": return "application/x-x509-ca-cert"; - case "pl": return "application/x-perl"; - case "pm": return "application/x-perl"; - case "png": return "image/png"; - case "ppt": return "application/vnd.ms-powerpoint"; - case "pptx": return "application/vnd.openxmlformats-officedocument.presentationml.presentation"; - case "prc": return "application/x-pilot"; - case "ps": return "application/postscript"; - case "ra": return "audio/x-realaudio"; - case "rar": return "application/x-rar-compressed"; - case "rpm": return "application/x-redhat-package-manager"; - case "rss": return "application/rss+xml"; - case "rtf": return "application/rtf"; - case "run": return "application/x-makeself"; - case "sea": return "application/x-sea"; - case "sit": return "application/x-stuffit"; - case "svg": return "image/svg+xml"; - case "svgz": return "image/svg+xml"; - case "swf": return "application/x-shockwave-flash"; - case "tcl": return "application/x-tcl"; - case "tif": return "image/tiff"; - case "tiff": return "image/tiff"; - case "tk": return "application/x-tcl"; - case "ts": return "video/mp2t"; - case "war": return "application/java-archive"; - case "wbmp": return "image/vnd.wap.wbmp"; - case "webm": return "video/webm"; - case "webp": return "image/webp"; - case "wmlc": return "application/vnd.wap.wmlc"; - case "wmv": return "video/x-ms-wmv"; - case "woff": return "font/woff"; - case "woff2": return "font/woff2"; - case "xhtml": return "application/xhtml+xml"; - case "xls": return "application/vnd.ms-excel"; - case "xlsx": return "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"; - case "xpi": return "application/x-xpinstall"; - case "xspf": return "application/xspf+xml"; - case "zip": return "application/zip"; - } - return Default; - } - #endregion } diff --git a/Apewer/_Delegates.cs b/Apewer/_Delegates.cs index 66d241e..2ed2c60 100644 --- a/Apewer/_Delegates.cs +++ b/Apewer/_Delegates.cs @@ -86,6 +86,27 @@ namespace Apewer /// 封装一个方法,该方法不具有参数且不返回值。 public delegate void Action(); + /// 封装一个方法,该方法不具有参数且不返回值。 + public delegate void Action(); + + /// 封装一个方法,该方法不具有参数且不返回值。 + public delegate void Action(); + + /// 封装一个方法,该方法不具有参数且不返回值。 + public delegate void Action(); + + /// 封装一个方法,该方法不具有参数且不返回值。 + public delegate void Action(); + + /// 封装一个方法,该方法不具有参数且不返回值。 + public delegate void Action(); + + /// 封装一个方法,该方法不具有参数且不返回值。 + public delegate void Action(); + + /// 封装一个方法,该方法不具有参数且不返回值。 + public delegate void Action(); + /// 表示当事件提供数据时将处理该事件的方法。 /// 事件生成的事件数据的类型。 /// 事件源。 diff --git a/Apewer/_Extensions.cs b/Apewer/_Extensions.cs index 93762ef..b3376d6 100644 --- a/Apewer/_Extensions.cs +++ b/Apewer/_Extensions.cs @@ -6,11 +6,16 @@ using System; using System.Collections; using System.Collections.Generic; using System.Collections.Specialized; +using System.Data; using System.Diagnostics; using System.IO; using System.Reflection; using System.Text; +#if !NET20 +using System.Dynamic; +#endif + /// 扩展方法。 public static class Extensions_Apewer { @@ -59,6 +64,23 @@ public static class Extensions_Apewer #endregion + #region Dynamic + +#if !NET20 + + /// 转换 对象。 + public static ExpandoObject Expando(this ObjectSet os) => ObjectSet.Expando(os); + + /// 转换 数组到 数组。 + public static ExpandoObject[] Expando(this ObjectSet[] oss) => ObjectSet.Expando(oss); + + /// 设置 的 Always 属性,对动态属性的访问始终返回 TRUE 值。 + public static void SetAlways(this ObjectSet os, bool always) => os._always = always; + +#endif + + #endregion + #region Number /// 判断此值为零。 @@ -476,6 +498,14 @@ public static class Extensions_Apewer /// 获取默认表中指定单元格的内容。从第 0 行开始。 public static string Text(this IQuery @this, int row, string column) => @this == null ? null : Query.Text(@this.Value(row, column)); + /// 转换 数组,每行记录为一个 ObjectSet 对象。 + /// 当参数 table 无效时返回 0 长度的 数组。 + public static ObjectSet[] ObjectSet(this DataTable @this) => SourceUtility.ObjectSet(@this); + + /// 转换 数组,每行记录为一个 ObjectSet 对象。 + /// 当参数 table 无效时返回 0 长度的 数组。 + public static ObjectSet[] ObjectSet(this IQuery @this) => @this == null ? null : SourceUtility.ObjectSet(@this.Table); + #endregion #region Web @@ -488,19 +518,19 @@ public static class Extensions_Apewer public static void Error(this ApiResponse @this, string message = "未知错误。") => ApiUtility.Error(@this, message); /// 输出字节数组。 - public static void Bytes(this ApiResponse @this, byte[] bytes, string type = "application/octet-stream") => ApiUtility.Model(@this, type, new ApiBytesModel() { Bytes = bytes }); + public static void Bytes(this ApiResponse @this, byte[] bytes, string type = "application/octet-stream") => ApiUtility.Model(@this, new ApiBytesModel(bytes, type)); /// 输出 UTF-8 文本。 - public static void Text(this ApiResponse @this, string text, string contentType = "text/plain; charset=utf-8") => ApiUtility.Model(@this, null, new ApiTextModel() { Text = text, ContentType = contentType }); + public static void Text(this ApiResponse @this, string text, string contentType = "text/plain") => ApiUtility.Model(@this, new ApiTextModel(text, contentType)); /// 输出 Json 文本。 - public static void Json(this ApiResponse @this, Json json, bool indented = true, bool camel = false) => ApiUtility.Model(@this, null, new ApiJsonModel() { Json = json, Indented = indented, Camel = camel }); + public static void Json(this ApiResponse @this, Json json, bool indented = true, bool camel = false) => ApiUtility.Model(@this, new ApiJsonModel(json, camel, indented)); /// 输出文件。 - public static void File(this ApiResponse @this, string path, string name = null) => ApiUtility.Model(@this, null, new ApiFileModel() { Path = path, Attachment = name }); + public static void File(this ApiResponse @this, string path, string name = null) => ApiUtility.Model(@this, new ApiFileModel(path, name)); /// 重定向。 - public static void Redirect(this ApiResponse @this, string location) => ApiUtility.Model(@this, null, new ApiRedirectModel() { Location = location }); + public static void Redirect(this ApiResponse @this, string location) => ApiUtility.Model(@this, new ApiRedirectModel() { Location = location }); /// 设置响应,当发生错误时设置响应。返回错误信息。 public static string Set(this ApiResponse @this, IList list, bool lower = true, int depth = -1, bool force = false) => ApiUtility.Respond(@this, list, lower, depth, force); diff --git a/ChangeLog.md b/ChangeLog.md index 373f6ee..0c2d0f7 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -1,6 +1,9 @@  ### 最新提交 +### 6.6.0 +- AspNetBridge:新命名空间,支持桥接 ASP.NET 的 API。 + ### 6.5.10 - Source:新增 Incremental 特性,用于标记自动增长字段; - Source:新增 PrimaryKey 特性,用于标记主键字段;