diff --git a/Apewer/Apewer.csproj b/Apewer/Apewer.csproj index dea55f6..583708e 100644 --- a/Apewer/Apewer.csproj +++ b/Apewer/Apewer.csproj @@ -2,41 +2,21 @@ - + + - true - bin\$(Configuration)\$(TargetFramework)\$(AssemblyName).xml - latest - CS0108,CS0162,CS0414,CS0612,CS0618,CS0649,CS1589,CS1570,CS1572,CS1573,CS3019,CS3021 Library netcoreapp3.1;netstandard2.1;netstandard2.0;net461;net40;net20 - - Elivo - Apewer Lab - Copyright Apewer Lab. All rights reserved. - - Apewer - Apewer Libraries - 6.3.3 - - - - Apewer - true - true + Apewer + Apewer Apewer - - - DEBUG;TRACE;$(DefineConstants);$(AdditionalConstants) - - $(AssemblyName) - .NET Standard 2.1 diff --git a/Apewer/Logger.cs b/Apewer/Logger.cs index cf55562..f5afb79 100644 --- a/Apewer/Logger.cs +++ b/Apewer/Logger.cs @@ -167,7 +167,7 @@ namespace Apewer internal static object ConsoleLocker = new object(); /// 获取用于保存日志文件的路径。 - public static Func FilePathGetter { get; set; } + public static Func FilePathGetter { get; set; } private static string MergeContent(params object[] content) => TextUtility.Join(" | ", content); @@ -283,7 +283,7 @@ namespace Apewer public static string GetFilePath(Logger logger = null) { var getter = FilePathGetter; - if (getter != null) try { return getter.Invoke(); } catch { } + if (getter != null) try { return getter.Invoke(logger); } catch { } // 找到 App_Data 目录。 var appDir = RuntimeUtility.ApplicationPath; diff --git a/Apewer/Models/StringPairs.cs b/Apewer/Models/StringPairs.cs index 08b1343..52cd3e0 100644 --- a/Apewer/Models/StringPairs.cs +++ b/Apewer/Models/StringPairs.cs @@ -12,6 +12,12 @@ namespace Apewer.Models public class StringPairs : List>, IToJson { + /// + public StringPairs() : base() { } + + /// + public StringPairs(int capacity) : base(capacity) { } + /// 添加项。返回错误信息。 public string Add(string key, string value) { diff --git a/Apewer/Source/Accessor.cs b/Apewer/Source/Accessor.cs deleted file mode 100644 index 1b49a88..0000000 --- a/Apewer/Source/Accessor.cs +++ /dev/null @@ -1,64 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Text; - -namespace Apewer.Source -{ - - /// - public abstract class Accessor where T : class, IDatabase, new() - { - - private T _source = null; - - private bool _disposed = false; - - /// - public Accessor() { } - - /// - protected virtual T Source - { - get - { - if (_source == null) _source = new T(); - return _source; - } - } - - /// - public virtual bool Connected - { - get - { - return (_source == null) ? false : _source.Online; - } - } - - /// - public virtual bool Disposed - { - get { return _disposed; } - } - - /// - public virtual void Close() - { - if (_source == null) return; - _source.Close(); - } - - /// - public virtual void Dispose() - { - if (_source != null) - { - _source.Dispose(); - _source = null; - } - _disposed = true; - } - - } - -} diff --git a/Apewer/Source/ColumnAttribute.cs b/Apewer/Source/ColumnAttribute.cs index da82bf6..b5d5cab 100644 --- a/Apewer/Source/ColumnAttribute.cs +++ b/Apewer/Source/ColumnAttribute.cs @@ -21,105 +21,71 @@ namespace Apewer.Source private bool _independent = false; - private bool _locked = false; - - /// 使用自动的列名称。当类型为 VarChar 或 NVarChar 时必须指定长度。 /// - public ColumnAttribute(ColumnType type = ColumnType.NVarChar, int length = 191) + private void Init(string field, ColumnType type, int length, bool underline) { - New(null, type, length, true); + _field = string.IsNullOrEmpty(field) ? "" : TableStructure.RestrictName(field, underline); + _type = type; + switch (type) + { + case ColumnType.VarChar: + case ColumnType.NVarChar: + if (length < 1) throw new ArgumentException("最大长度无效。"); + _length = length; + break; + case ColumnType.VarChar255: + case ColumnType.NVarChar255: + _length = 255; + break; + default: + _length = length; + break; + } } + /// 使用自动的列名称。当类型为 VarChar 或 NVarChar 时必须指定长度。 + /// + public ColumnAttribute(ColumnType type = ColumnType.NVarChar, int length = 191) => Init(null, type, length, true); + /// 使用指定的列名称。当类型为 VarChar 或 NVarChar 时必须指定长度。 /// - public ColumnAttribute(string field, ColumnType type = ColumnType.NVarChar, int length = 191) - { - New(field, type, length, false); - } + public ColumnAttribute(string field, ColumnType type = ColumnType.NVarChar, int length = 191) => Init(field, type, length, false); - internal ColumnAttribute(string field, ColumnType type, int length, bool underline) - { - New(field, type, length, underline); - } + internal ColumnAttribute(string field, ColumnType type, int length, bool underline) => Init(field, type, length, underline); /// 属性。 public PropertyInfo Property { - get { return _property; } - internal set - { - if (_locked) return; - _property = value; - } + get => _property; + internal set => _property = value; } /// 字段名。 public string Field { - get { return _field; } - set - { - if (_locked) return; - _field = value; - } + get => _field; + set => _field = value; } /// 指定字段的最大长度。 public int Length { - get { return _length; } - set - { - if (_locked) return; - _length = value; - } + get => _length; + set => _length = value; } /// 字段类型。 public ColumnType Type { - get { return _type; } - set - { - if (_locked) return; - _type = value; - } + get => _type; + set => _type = value; } /// Independent 特性。 public bool Independent { - get { return _independent; } - internal set { _independent = value; } - } - - /// - private void New(string field, ColumnType type, int length, bool underline) - { - _field = string.IsNullOrEmpty(field) ? "" : TableStructure.RestrictName(field, underline); - _type = type; - switch (type) - { - case ColumnType.VarChar: - case ColumnType.NVarChar: - if (length < 1) throw new ArgumentException("最大长度无效。"); - _length = length; - break; - case ColumnType.VarChar255: - case ColumnType.NVarChar255: - _length = 255; - break; - default: - _length = length; - break; - } - Lock(); - } - - /// 锁定属性,阻止修改。 - public void Lock() - { - _locked = true; + get => _independent; + internal set => _independent = value; } } diff --git a/Apewer/Source/Example.cs b/Apewer/Source/Example.cs index ee84585..cff80dd 100644 --- a/Apewer/Source/Example.cs +++ b/Apewer/Source/Example.cs @@ -24,22 +24,22 @@ namespace Apewer.Source } /// - public static IExecute InvalidExecuteConnection { get { return CreateExecuteError("连接无效。"); } } + public static IExecute InvalidExecuteConnection => CreateExecuteError("连接无效。"); /// - public static IExecute InvalidExecuteStatement { get { return CreateExecuteError("语句无效。"); } } + public static IExecute InvalidExecuteStatement => CreateExecuteError("语句无效。"); /// - public static IExecute InvalidExecuteParameters { get { return CreateExecuteError("参数无效。"); } } + public static IExecute InvalidExecuteParameters => CreateExecuteError("参数无效。"); /// - public static IQuery InvalidQueryConnection { get { return CreateQueryError("连接无效。"); } } + public static IQuery InvalidQueryConnection => CreateQueryError("连接无效。"); /// - public static IQuery InvalidQueryStatement { get { return CreateQueryError("语句无效。"); } } + public static IQuery InvalidQueryStatement => CreateQueryError("语句无效。"); /// - public static IQuery InvalidQueryParameters { get { return CreateQueryError("参数无效。"); } } + public static IQuery InvalidQueryParameters => CreateQueryError("参数无效。"); } diff --git a/Apewer/Source/IOrm.cs b/Apewer/Source/IOrm.cs new file mode 100644 index 0000000..fb89a8f --- /dev/null +++ b/Apewer/Source/IOrm.cs @@ -0,0 +1,46 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Apewer.Source +{ + + /// 数据库引擎支持 ORM 访问。 + public interface IOrm : IDatabase + { + + /// 初始化指定类型,以创建表或增加字段。 + /// 要初始化的类型。 + /// 错误信息。当成功时候返回空字符串。 + public string Initialize(Type model); + + /// 插入记录。 + /// 要插入的记录实体。 + /// 错误信息。当成功时候返回空字符串。 + public string Insert(Record record); + + /// 更新记录。 + /// 要插入的记录实体。 + /// 错误信息。当成功时候返回空字符串。 + public string Update(Record record); + + /// 获取指定类型的主键,按 Flag 属性筛选。 + /// 要查询的类型。 + /// 要求目标记录具有的 Flag 属性,当指定 0 时忽略此要求。 + public Result> Keys(Type model, long flag = 0); + + /// 获取具有指定 Key 的记录,并要求记录具有指定的 Flag 属性。 + /// 目标记录的主键。 + /// 要求目标记录具有的 Flag 属性,当指定 0 时忽略此要求。 + public Result Get(string key, long flag = 0) where T : Record; + + /// 使用指定语句查询,获取查询结果。 + public Result> Query(string sql) where T : Record; + + /// 查询所有记录。 + /// 要求目标记录具有的 Flag 属性,当指定 0 时忽略此要求。 + public Result> Query(long flag = 0) where T : Record; + + } + +} diff --git a/Apewer/Source/MySql.cs b/Apewer/Source/MySql.cs index 57a5b4f..0cdad6b 100644 --- a/Apewer/Source/MySql.cs +++ b/Apewer/Source/MySql.cs @@ -1,21 +1,28 @@ -#if NET40 || NET461 +#if (!EXTRA && (NET40 || NET461)) || (EXTRA && (NETSTD || NETCORE)) -/* 2021.02.20 */ +/* 2021.08.03 */ -using Externals.MySql.Data.MySqlClient; using System; using System.Collections.Generic; using System.Data; using System.Text; +#if !EXTRA +using Externals.MySql.Data.MySqlClient; +#endif + +#if EXTRA +using MySql.Data.MySqlClient; +#endif + namespace Apewer.Source { /// - public class MySql : IDatabase, IDisposable + public class MySql : IDatabase, IOrm, IDisposable { - #region fields & properties +#region fields & properties private const string EmptyString = TextUtility.EmptyString; @@ -63,9 +70,9 @@ namespace Apewer.Source Pass = pass; } - #endregion +#endregion - #region 日志。 +#region 日志。 /// 获取或设置日志记录。 public Logger Logger { get; set; } @@ -76,9 +83,9 @@ namespace Apewer.Source if (logger != null) logger.Error(this, "MySQL", action, ex.GetType().FullName, ex.Message, addtion); } - #endregion +#endregion - #region methods +#region methods private string CombineString() { @@ -237,34 +244,34 @@ namespace Apewer.Source /// public IExecute Execute(string sql) => Execute(sql, null as IEnumerable); - #endregion +#endregion - #region linq +#region ORM - private List QueryFirstColumn(string sql) + private List FirstColumn(string sql) { using (var query = Query(sql) as Query) return query.ReadColumn(); } /// - public List QueryAllTableNames() + public List TableNames() { var sql = TextUtility.Merge("select table_name from information_schema.tables where table_schema='", _store, "' and table_type='base table';"); - return QueryFirstColumn(sql); + return FirstColumn(sql); } /// - public List QueryAllViewNames() + public List ViewNames() { var sql = TextUtility.Merge("select table_name from information_schema.tables where table_schema='", _store, "' and table_type='view';"); - return QueryFirstColumn(sql); + return FirstColumn(sql); } /// - public List QueryAllColumnNames(string table) + public List ColumnNames(string table) { var sql = TextUtility.Merge("select column_name from information_schema.columns where table_schema='", _store, "' and table_name='", TextUtility.AntiInject(table), "';"); - return QueryFirstColumn(sql); + return FirstColumn(sql); } /// 获取用于创建表的语句。 @@ -272,7 +279,7 @@ namespace Apewer.Source { // 检查现存表。 var exists = false; - var tables = QueryAllTableNames(); + var tables = TableNames(); if (tables.Count > 0) { var lower = structure.Table.ToLower(); @@ -289,7 +296,7 @@ namespace Apewer.Source if (exists) { - var columns = QueryAllColumnNames(structure.Table); + var columns = ColumnNames(structure.Table); if (columns.Count > 0) { var lower = new List(columns.Count); @@ -366,7 +373,7 @@ namespace Apewer.Source } /// - private string CreateTable(Type model, out string sql) + private string Initialize(Type model, out string sql) { if (model == null) { @@ -399,36 +406,25 @@ namespace Apewer.Source } /// - public string CreateTable(Type model) - { - return CreateTable(model, out string sql); - } + public string Initialize(Type model) => Initialize(model, out string sql); /// - public string CreateTable() where T : Record - { - return CreateTable(typeof(T)); - } + public string Initialize() where T : Record => Initialize(typeof(T)); /// - public string CreateTable(Record model) - { - if (model == null) return "参数无效。"; - return CreateTable(model.GetType()); - } + public string Initialize(Record model) => (model == null) ? "参数无效。" : Initialize(model.GetType()); /// 插入记录。成功时候返回空字符串,发生异常时返回异常信息。 - public string Insert(Record entity, bool resetKey = false) + public string Insert(Record record) { - if (entity == null) return "参数无效。"; - entity.FixProperties(); - if (resetKey) entity.ResetKey(); + if (record == null) return "参数无效。"; + record.FixProperties(); var structure = null as TableStructure; - try { structure = TableStructure.ParseModel(entity); } + try { structure = TableStructure.ParseModel(record); } catch (Exception exception) { return exception.Message; } - var parameters = structure.CreateDataParameters(entity, CreateDataParameter); + var parameters = structure.CreateDataParameters(record, CreateDataParameter); var sql = GenerateInsertStatement(structure.Table, parameters); var execute = Execute(sql, parameters); if (execute.Success) return TextUtility.EmptyString; @@ -439,28 +435,27 @@ namespace Apewer.Source /// 更新记录,实体中的 Created 和 Key 属性不被更新。成功时返回空字符串,发生异常时返回异常信息。 /// 无法更新拥有 Independent 特性的模型。 /// - public string Update(Record entity) + public string Update(Record record) { - if (entity == null) return "参数无效。"; - entity.FixProperties(); - entity.Updated = ClockUtility.LucidNow; + if (record == null) return "参数无效。"; + record.FixProperties(); var structure = null as TableStructure; - try { structure = TableStructure.ParseModel(entity); } + try { structure = TableStructure.ParseModel(record); } catch (Exception exception) { return exception.Message; } // 检查 Independent 特性。 if (structure.Independent) return "无法更新拥有 Independent 特性的模型。"; - var parameters = structure.CreateDataParameters(entity, CreateDataParameter, "_created", "_key"); - var sql = GenerateUpdateStatement(structure, entity.Key, parameters); + var parameters = structure.CreateDataParameters(record, CreateDataParameter, "_created", "_key"); + var sql = GenerateUpdateStatement(structure, record.Key, parameters); var execute = Execute(sql, parameters); if (execute.Success) return TextUtility.EmptyString; return execute.Error; } /// - public Result> QueryRecords(string sql) where T : Record + public Result> Query(string sql) where T : Record { if (sql.IsEmpty()) return new Result>(new ArgumentException()); try @@ -480,7 +475,7 @@ namespace Apewer.Source } /// 获取记录。 - public Result QueryRecord(string key, long flag = 0) where T : Record + public Result Get(string key, long flag = 0) where T : Record { var k = TextUtility.SafeKey(key); if (k.IsEmpty()) new Result(new Exception("参数无效。")); @@ -490,7 +485,7 @@ namespace Apewer.Source var sqlflag = (flag == 0) ? TextUtility.EmptyString : TextUtility.Merge(" `_flag`=", flag.ToString(), " and"); var sqlkey = TextUtility.Merge(" `_key`='", k, "'"); var sql = TextUtility.Merge("select * from `", structure.Table, "` where ", sqlflag, sqlkey, " limit 1;"); - var result = QueryRecords(sql); + var result = Query(sql); var list = result.Entity; if (list != null && list.Count > 0) return new Result(list[0]); return new Result(null, "无结果。"); @@ -502,14 +497,14 @@ namespace Apewer.Source } /// 获取所有记录。Flag 为 0 时将忽略 Flag 条件。 - public Result> QueryRecords(long flag = 0) where T : Record + public Result> Query(long flag = 0) where T : Record { try { var structure = TableStructure.ParseModel(typeof(T)); var sqlflag = (flag == 0) ? TextUtility.EmptyString : TextUtility.Merge(" where `_flag`=", flag.ToString()); var sql = TextUtility.Merge("select * from `", structure.Table, "`", sqlflag, "; "); - return QueryRecords(sql); + return Query(sql); } catch (Exception exception) { @@ -520,7 +515,7 @@ namespace Apewer.Source /// 获取记录。 /// 要跳过的记录数,可用最小值为 0。 /// 要获取的记录数,可用最小值为 1。 - public Result> QueryRecords(int skip, int count) where T : Record + public Result> Query(int skip, int count) where T : Record { try { @@ -528,7 +523,7 @@ namespace Apewer.Source if (count < 1) return new Result>(new ArgumentOutOfRangeException(nameof(count))); var tableName = TableStructure.ParseTable(typeof(T)).Name; var sql = $"select * from `{tableName}` where _flag = 1 limit {skip}, {count}; "; - return QueryRecords(sql); + return Query(sql); } catch (Exception exception) { @@ -536,17 +531,17 @@ namespace Apewer.Source } } - /// 查询有效的 Key 值。 - public Result> QueryKeys(Type model, long flag = 0) + /// >获取指定类型的主键,按 Flag 属性筛选。 + public Result> Keys(Type model, long flag = 0) { if (model != null) { try { var type = model; - var ts = TableStructure.ParseModel(type); - var sqlflag = (flag == 0) ? TextUtility.EmptyString : TextUtility.Merge(" where `_flag`=", flag.ToString()); - var sql = TextUtility.Merge("select `_key` from `", ts.Table, "`", sqlflag, "; "); + var tn = TableStructure.ParseModel(type).Table; + var where = (flag == 0) ? TextUtility.EmptyString : TextUtility.Merge(" where `_flag`=", flag.ToString()); + var sql = $"select `_key` from `{tn}`{where}; "; using (var query = Query(sql) as Query) { var list = query.ReadColumn((r) => TextUtility.Trim(query.Text(r, 0))); @@ -561,27 +556,8 @@ namespace Apewer.Source return new Result>(new Exception("参数无效。")); } - /// 查询有效的 Key 值。 - public Result> QueryKeys(long flag = 0) where T : Record - { - try - { - var type = typeof(T); - - var ts = TableStructure.ParseModel(type); - var sqlflag = (flag == 0) ? TextUtility.EmptyString : TextUtility.Merge(" where _flag=", flag.ToString()); - var sql = TextUtility.Merge("select `_key` from `", ts.Table, "`", sqlflag, "; "); - using (var query = Query(sql) as Query) - { - var list = query.ReadColumn((r) => TextUtility.Trim(query.Text(r, 0))); - return new Result>(list); - } - } - catch (Exception ex) - { - return new Result>(ex); - } - } + /// >获取指定类型的主键,按 Flag 属性筛选。 + public Result> Keys(long flag = 0) where T : Record => Keys(typeof(T), flag); /// 对表添加列,返回错误信息。 /// 记录类型。 @@ -635,9 +611,9 @@ namespace Apewer.Source return error; } - #endregion +#endregion - #region static +#region static /// 对文本转义,符合 SQL 安全性。可根据字段类型限制 UTF-8 字节数,默认为 0 时不限制字节数。 public static string Escape(string text, int bytes = 0) @@ -996,7 +972,7 @@ namespace Apewer.Source return result; } - #endregion +#endregion } diff --git a/Apewer/Source/Record.cs b/Apewer/Source/Record.cs index dfbb189..2c630dc 100644 --- a/Apewer/Source/Record.cs +++ b/Apewer/Source/Record.cs @@ -64,11 +64,11 @@ namespace Apewer.Source if (record == null) return; if (record.Flag == 0) record.Flag = 1; - if (TextUtility.IsBlank(record.Key)) record.Key = TextUtility.NewGuid(); + if (string.IsNullOrEmpty(record.Key)) record.Key = TextUtility.NewGuid(); var now = ClockUtility.LucidNow; - if (TextUtility.IsBlank(record.Created)) record.Created = now; - if (TextUtility.IsBlank(record.Updated)) record.Updated = now; + if (string.IsNullOrEmpty(record.Created)) record.Created = now; + if (string.IsNullOrEmpty(record.Updated)) record.Updated = now; } /// 枚举带有 Table 特性的 派生类型。 diff --git a/Apewer/Source/SqlServer.cs b/Apewer/Source/SqlServer.cs index 09eebe8..ccd6b59 100644 --- a/Apewer/Source/SqlServer.cs +++ b/Apewer/Source/SqlServer.cs @@ -1,6 +1,6 @@ #if NET40 || NET461 -/* 2021.02.20 */ +/* 2021.08.03 */ using Apewer; using Apewer.Source; @@ -17,7 +17,7 @@ namespace Apewer.Source /// 用于快速连接 Microsoft SQL Server 数据库的辅助。 [Serializable] - public class SqlServer : IDatabase, IDisposable + public class SqlServer : IDatabase, IOrm, IDisposable { #region 变量定义。 @@ -322,7 +322,7 @@ namespace Apewer.Source } /// 查询数据库中的所有表名。 - public List QueryAllTableNames() + public List TableNames() { var list = new List(); if (Connect()) @@ -341,7 +341,7 @@ namespace Apewer.Source } /// 查询数据库实例中的所有数据库名。 - public List QueryAllStoreNames() + public List StoreNames() { var list = new List(); if (Connect()) @@ -364,7 +364,7 @@ namespace Apewer.Source } /// 查询表中的所有列名。 - public List QueryAllColumnNames(string tableName) + public List ColumnNames(string tableName) { var list = new List(); if (Connect()) @@ -384,16 +384,10 @@ namespace Apewer.Source } /// 创建表,当表不存在时创建表,当现存表中缺少模型中属性对应的列时增加列。成功时返回空字符串,发生异常时返回异常信息。 - public string CreateTable(Record model) - { - if (model == null) return "参数无效。"; - var type = model.GetType(); - var error = CreateTable(type); - return error; - } + public string Initialize(Record model) => model == null ? "参数无效。" : Initialize(model); /// 创建表,当表不存在时创建表,当现存表中缺少模型中属性对应的列时增加列。成功时返回空字符串,发生异常时返回异常信息。 - public string CreateTable(Type model) + public string Initialize(Type model) { var structure = null as TableStructure; try { structure = TableStructure.ParseModel(model); } @@ -404,7 +398,7 @@ namespace Apewer.Source // 检查现存表。 var exists = false; - var tables = QueryAllTableNames(); + var tables = TableNames(); if (tables.Count > 0) { var lower = structure.Table.ToLower(); @@ -422,7 +416,7 @@ namespace Apewer.Source if (exists) { // 获取已存在的列名。 - var columns = QueryAllColumnNames(structure.Table); + var columns = ColumnNames(structure.Table); if (columns.Count > 0) { var lower = new List(); @@ -478,18 +472,18 @@ namespace Apewer.Source } /// 插入记录。成功时候返回空字符串,发生异常时返回异常信息。 - public string Insert(Record entity) + public string Insert(Record record) { - if (entity == null) return "参数无效。"; - var type = entity.GetType(); + if (record == null) return "参数无效。"; + var type = record.GetType(); - entity.FixProperties(); + record.FixProperties(); var structure = null as TableStructure; - try { structure = TableStructure.ParseModel(entity); } + try { structure = TableStructure.ParseModel(record); } catch (Exception exception) { return exception.Message; } - var parameters = structure.CreateDataParameters(entity, CreateDataParameter); + var parameters = structure.CreateDataParameters(record, CreateDataParameter); var sql = GenerateInsertStatement(structure.Table, parameters); @@ -502,24 +496,24 @@ namespace Apewer.Source /// 更新记录,实体中的 Created 和 Key 属性不被更新。成功时返回空字符串,发生异常时返回异常信息。 /// 无法更新拥有 Independent 特性的模型。 /// - public string Update(Record entity) + public string Update(Record record) { - if (entity == null) return "参数无效。"; - var type = entity.GetType(); + if (record == null) return "参数无效。"; + var type = record.GetType(); - entity.FixProperties(); - entity.Updated = ClockUtility.LucidNow; + record.FixProperties(); + record.Updated = ClockUtility.LucidNow; var structure = null as TableStructure; - try { structure = TableStructure.ParseModel(entity); } + try { structure = TableStructure.ParseModel(record); } catch (Exception exception) { return exception.Message; } // 检查 Independent 特性。 if (structure.Independent) return "无法更新拥有 Independent 特性的模型。"; - var parameters = structure.CreateDataParameters(entity, CreateDataParameter, "_created", "_key"); + var parameters = structure.CreateDataParameters(record, CreateDataParameter, "_created", "_key"); - var sql = GenerateUpdateStatement(structure.Table, entity.Key, parameters); + var sql = GenerateUpdateStatement(structure.Table, record.Key, parameters); var execute = Execute(sql, parameters); if (execute.Success) return TextUtility.EmptyString; @@ -527,7 +521,7 @@ namespace Apewer.Source } /// 获取具有指定 Key 的记录。 - public Result QueryRecord(string key, long flag = 0) where T : Record + public Result Get(string key, long flag = 0) where T : Record { var k = TextUtility.SafeKey(key); if (TextUtility.IsBlank(k) == false) @@ -551,7 +545,7 @@ namespace Apewer.Source } /// 获取记录。 - public Result> QueryRecords(long flag = 0) where T : Record + public Result> Query(long flag = 0) where T : Record { try { @@ -588,7 +582,7 @@ namespace Apewer.Source } /// 查询有效的 Key 值。 - public Result> QueryKeys(Type model, long flag = 0) + public Result> Keys(Type model, long flag = 0) { if (model != null) { @@ -616,29 +610,7 @@ namespace Apewer.Source } /// 查询有效的 Key 值。 - public Result> QueryKeys(long flag = 0) where T : Record - { - try - { - var type = typeof(T); - - var ts = TableStructure.ParseModel(type); - var f = (flag == 0) ? TextUtility.EmptyString : TextUtility.Merge(" where _flag=", flag.ToString()); - var query = (Query)Query(TextUtility.Merge("select _key from [", ts.Table, "]", f, "; ")); - var list = new List(); - for (var r = 0; r < query.Rows; r++) - { - var cell = TextUtility.Trim(query.Text(r)); - if (cell.Length > 0) list.Add(cell); - } - query.Dispose(); - return new Result>(list); - } - catch (Exception ex) - { - return new Result>(ex); - } - } + public Result> Keys(long flag = 0) where T : Record => Keys(typeof(T), flag); #endregion @@ -826,10 +798,7 @@ namespace Apewer.Source } /// 指定的连接凭据是否符合连接要求,默认指定 master 数据库。 - public static bool Proven(string address, string user, string pass) - { - return Proven(address, "master", user, pass); - } + 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) @@ -934,7 +903,7 @@ namespace Apewer.Source /// 生成 INSERT INTO 语句。表名必须有效,无有效参数时将获取空结果。 /// /// - public static string GenerateInsertStatement(string table, IEnumerable parameters) + private static string GenerateInsertStatement(string table, IEnumerable parameters) { if (table == null) throw new ArgumentNullException(nameof(table)); var tableName = TextUtility.AntiInject(table, 255); @@ -949,7 +918,7 @@ namespace Apewer.Source /// 生成 UPDATE 语句,键字段名为“_key”。表名必须有效,键值必须有效,无有效参数时将获取空结果。 /// /// - public static string GenerateUpdateStatement(string table, string key, IEnumerable parameters) + private static string GenerateUpdateStatement(string table, string key, IEnumerable parameters) { if (table == null) throw new ArgumentNullException(nameof(table)); var t = TextUtility.AntiInject(table, 255); diff --git a/Apewer/Source/Sqlite.cs b/Apewer/Source/Sqlite.cs new file mode 100644 index 0000000..d3a9c88 --- /dev/null +++ b/Apewer/Source/Sqlite.cs @@ -0,0 +1,965 @@ +#if EXTRA + +/* 2021.08.03 */ + +using Apewer; +using Apewer.Source; +using System; +using System.Collections.Generic; +using System.Data; +using System.Data.SQLite; +using System.Text; +using System.Data.Common; +//using Mono.Data.Sqlite; + +namespace Apewer.Source +{ + + /// 用于快速连接 SQLite 数据库的辅助。 + public class Sqlite : IDatabase, IOrm, IDisposable + { + + #region 变量定义。 + + private SQLiteConnection _db = null; + + private Timeout _timeout = new Timeout(); + private string _connstring = ""; + private string _path = ""; + private string _pass = ""; + private byte[] _passdata = BinaryUtility.EmptyBytes; + + #endregion + + #region this + + private void VarInit(string path, Timeout timeout, string pass, byte[] passData) + { + _path = (path == null) ? "" : path; + _passdata = (passData == null) ? BinaryUtility.EmptyBytes : passData; + _pass = (pass == null) ? "" : pass; + _timeout = timeout; + } + + /// 连接内存。 + public Sqlite() => VarInit(Memory, new Timeout(), null, null); + + /// 连接指定文件。 + public Sqlite(string path) => VarInit(path, new Timeout(), null, null); + + /// 连接指定文件。 + private Sqlite(string path, byte[] passData) => VarInit(path, new Timeout(), null, passData); + + /// 连接指定文件。 + public Sqlite(string path, string pass) => VarInit(path, new Timeout(), pass, null); + + /// 连接指定文件。 + public Sqlite(string path, Timeout timeout) => VarInit(path, timeout, null, null); + + /// 连接指定文件。 + private Sqlite(string path, Timeout timeout, byte[] pass) => VarInit(path, timeout, null, pass); + + /// 连接指定文件。 + public Sqlite(string path, Timeout timeout, string pass) => VarInit(path, timeout, pass, null); + + #endregion + + #region 日志。 + + /// 获取或设置日志记录。 + public Logger Logger { get; set; } + + private void LogError(string action, Exception ex, string addtion) + { + var logger = Logger; + if (logger != null) logger.Error(this, "SQLite", action, ex.GetType().FullName, ex.Message, addtion); + } + + private void LogError(string action, string message) + { + var logger = Logger; + if (logger != null) logger.Error(this, "SQLite", action, message); + } + + #endregion + + #region 实现接口。 + + /// 数据库是否已经连接。 + public bool Online { get => _db != null && _db.State == ConnectionState.Open; } + + /// 连接数据库,若未连接则尝试连接。 + /// 是否已连接。 + public bool Connect() + { + if (_db == null) + { + _db = new SQLiteConnection(); + _db.ConnectionString = ConnectionString; + //if (string.IsNullOrEmpty(_connstring) && string.IsNullOrEmpty(_pass) && (_passdata.Length > 0)) + //{ + // _db.SetPassword(_pass); + //} + } + else + { + if (_db.State == ConnectionState.Open) return true; + } + try + { + _db.Open(); + switch (_db.State) + { + case ConnectionState.Open: return true; + default: return false; + } + } + catch (Exception ex) + { + LogError("Connection", ex, _db.ConnectionString); + Close(); + return false; + } + } + + /// 关闭连接,并释放对象所占用的系统资源。 + public void Close() + { + if (_db != null) + { + //_db.Close(); + _db.Dispose(); + _db = null; + } + } + + /// 关闭连接,释放对象所占用的系统资源,并清除连接信息。 + public void Dispose() { Close(); } + + /// 查询。 + public IQuery Query(string sql) => Query(sql, null); + + /// 查询。 + public IQuery Query(string sql, IEnumerable parameters) + { + if (string.IsNullOrEmpty(sql)) return Example.InvalidQueryStatement; + + const string table = "result"; + + var connected = Connect(); + if (!connected) return Example.InvalidQueryConnection; + + var query = new Query(); + try + { + 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(sql, _db)) + { + da.Fill(dataset, table); + query.Table = dataset.Tables[table]; + } + } + command.Dispose(); + query.Success = true; + } + catch (Exception ex) + { + LogError("Query", ex, sql); + query.Success = false; + query.Exception = ex; + } + return query; + } + + /// 执行单条 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) return Example.InvalidExecuteConnection; + + var transaction = _db.BeginTransaction(); + var execute = new Execute(); + try + { + var command = new SQLiteCommand(); + command.Connection = _db; + command.Transaction = transaction; + command.CommandTimeout = Timeout.Execute; + command.CommandText = sql; + if (parameters != null) + { + foreach (var p in parameters) + { + if (p != null) command.Parameters.Add(p); + } + } + execute.Rows += command.ExecuteNonQuery(); + transaction.Commit(); + command.Dispose(); + execute.Success = true; + } + catch (Exception ex) + { + try { transaction.Rollback(); } catch { } + LogError("Execute", ex, sql); + execute.Success = false; + execute.Exception = ex; + } + try { transaction.Dispose(); } catch { } + return execute; + } + + #endregion + + #region 属性。 + + /// 获取当前的 SQLiteConnection 对象。 + public IDbConnection Connection { get => _db; } + + /// 获取或设置超时。 + public Timeout Timeout { get => _timeout; set => _timeout = value; } + + /// 获取或设置连接字符串,连接字符串非空时将忽略 Path 属性。数据库在线时无法设置。 + public string ConnectionString + { + get + { + if (string.IsNullOrEmpty(_connstring)) + { + var temp = new StringBuilder(); + temp.Append("data source='", _path, "'; version=3; "); + if (!string.IsNullOrEmpty(_pass)) temp.Append("password=", _pass, "; "); + return temp.ToString(); + } + else return _connstring; + } + set + { + if (Online) return; + _connstring = string.IsNullOrEmpty(value) ? "" : value; + } + } + + /// 获取或设置数据库路径(文件或内存)。数据库在线时无法设置。 + public string Path + { + get { return _path; } + set + { + if (Online) return; + _path = string.IsNullOrEmpty(value) ? "" : value; + } + } + + /// 获取或设置数据库密码。数据库在线时无法设置。 + public string Password + { + get { return _pass; } + set + { + if (Online) return; + _pass = string.IsNullOrEmpty(value) ? "" : value; + } + } + + /// 获取或设置数据库密码。数据库在线时无法设置。 + private byte[] PasswordData + { + get { return _passdata; } + set + { + if (Online) return; + _passdata = (value == null) ? BinaryUtility.EmptyBytes : value; + } + } + + /// 保存当前数据库到文件,若文件已存在则将重写文件。 + public bool Save(string path) + { + if (!StorageUtility.CreateFile(path, 0, true)) + { + LogError("Save", TextUtility.Merge("创建文件 ", path, " 失败。")); + return false; + } + + var temp = new Sqlite(path); + var result = Save(temp); + temp.Close(); + return result; + } + + /// 保存当前数据库到文件,若文件已存在则将重写文件。 + public bool Save(string path, string password) + { + if (!StorageUtility.CreateFile(path, 0, true)) + { + LogError("Save", TextUtility.Merge("创建文件 ", path, " 失败。")); + return false; + } + + var temp = new Sqlite(path, password); + var result = Save(temp); + temp.Close(); + return result; + } + + /// 保存当前数据库到目标数据库。 + public bool Save(Sqlite destination) + { + var error = Backup(this, destination); + return string.IsNullOrEmpty(error); + } + + /// 加载文件到当前数据库。 + public bool Load(string path) + { + var temp = new Sqlite(path); + var result = Load(temp); + temp.Close(); + return result; + } + + /// 加载文件到当前数据库。 + public bool Load(string path, params byte[] pass) + { + var temp = new Sqlite(path, pass); + var result = Load(temp); + temp.Close(); + return result; + } + + /// 加载文件到当前数据库。 + public bool Load(string path, string pass) + { + var temp = new Sqlite(path, pass); + var result = Load(temp); + temp.Close(); + return result; + } + + /// 加载源数据库到当前数据库。 + public bool Load(Sqlite source) + { + var error = Backup(source, this); + return string.IsNullOrEmpty(error); + } + + #endregion + + #region ORM。 + + /// 查询数据库中的所有表名。 + public List TableNames() + { + var list = new List(); + if (Connect()) + { + 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; + } + + /// 查询数据库中的所有视图名。 + public List ViewNames() + { + var list = new List(); + if (Connect()) + { + 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; + } + + /// 查询表中的所有列名。 + public List ColumnNames(string table) + { + var list = new List(); + if (Connect()) + { + 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; + } + + /// 创建表,不修改已存在表。成功时返回空字符串,发生异常时返回异常信息。 + public string Initialize(Record model) => model == null ? "参数无效。" : Initialize(model.GetType()); + + /// 创建表,不修改已存在表。成功时返回空字符串,发生异常时返回异常信息。 + public string Initialize() where T : Record => Initialize(typeof(T)); + + /// 创建表,不修改已存在表。当现存表中缺少模型中属性对应的列时增加列。成功时返回空字符串,发生异常时返回异常信息。 + public string Initialize(Type model) + { + var structure = null as TableStructure; + try { structure = TableStructure.ParseModel(model); } + catch (Exception exception) { return exception.Message; } + + // 连接数据库。 + if (!Connect()) return "连接数据库失败。"; + + // 检查现存表。 + var exists = false; + var tables = TableNames(); + if (tables.Count > 0) + { + var lower = structure.Table.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.Values) + { + var type = GetColumnDeclaration(column); + if (type == TextUtility.EmptyString) return TextUtility.Merge("类型 ", column.Type.ToString(), " 不受支持。"); + sqlcolumns.Add(type); + } + var sql = TextUtility.Merge("create table [", structure.Table, "](", TextUtility.Join(", ", sqlcolumns), "); "); + var execute = Execute(sql); + if (execute.Success) return TextUtility.EmptyString; + return execute.Error; + } + } + + /// 插入记录。成功时候返回空字符串,发生异常时返回异常信息。 + public string Insert(Record record) + { + if (record == null) return "参数无效。"; + record.FixProperties(); + + var structure = null as TableStructure; + try { structure = TableStructure.ParseModel(record); } + catch (Exception exception) { return exception.Message; } + + var parameters = structure.CreateDataParameters(record, CreateDataParameter); + + var sql = GenerateInsertStatement(structure.Table, (IEnumerable)parameters); + + var execute = Execute(sql, parameters); + if (execute.Success && execute.Rows > 0) return TextUtility.EmptyString; + return execute.Error; + } + + /// 更新记录,实体中的 Created 和 Key 属性不被更新。成功时返回空字符串,发生异常时返回异常信息。 + public string Update(Record record) + { + if (record == null) return "参数无效。"; + record.FixProperties(); + record.Updated = ClockUtility.LucidNow; + + var structure = null as TableStructure; + try { structure = TableStructure.ParseModel(record); } + catch (Exception exception) { return exception.Message; } + + var parameters = structure.CreateDataParameters(record, CreateDataParameter, "_created", "_key"); + + var sql = GenerateUpdateStatement(structure.Table, record.Key, parameters); + + var execute = Execute(sql, parameters); + if (execute.Success && execute.Rows > 0) return TextUtility.EmptyString; + return execute.Error; + } + + /// 获取具有指定 Key 的记录。 + public Result Get(string key) where T : Record => Get(key, 0); + + /// 获取具有指定 Key 的记录。 + public Result Get(string key, long flag) where T : Record + { + var k = TextUtility.SafeKey(key); + if (TextUtility.IsBlank(k) == false) + { + try + { + var ts = TableStructure.ParseModel(typeof(T)); + var f = (flag == 0) ? TextUtility.EmptyString : TextUtility.Merge(" _flag=", flag.ToString(), " and"); + var query = (Query)Query(TextUtility.Merge("select * from [", ts.Table, "] where", f, " _key='", k, "' limit 1; ")); + var list = query.Fill(); + query.Dispose(); + if (list.Count > 0) return new Result(list[0]); + } + catch (Exception ex) + { + return new Result(ex); + } + } + return new Result(new Exception("参数无效。")); + } + + /// 查询多条记录。 + public Result> Query() where T : Record => Query(0); + + /// 查询多条记录。 + public Result> Query(long flag) where T : Record + { + try + { + var ts = TableStructure.ParseModel(typeof(T)); + var f = (flag == 0) ? TextUtility.EmptyString : TextUtility.Merge(" where _flag=", flag.ToString()); + var query = Query(TextUtility.Merge("select * from [", ts.Table, "]", f, "; ")) as Query; + var list = query.Fill(); + query.Dispose(); + return new Result>(list); + } + catch (Exception ex) + { + return new Result>(ex); + } + } + + /// 获取按指定语句查询到的所有记录。 + public Result> Query(string sql) where T : Record + { + using (var query = Query(sql) as Query) + { + if (query.Exception == null) return new Result>(query.Fill()); + else return new Result>(query.Exception); + } + } + + /// 查询所有有效的 Key 值。 + public Result> Keys() where T : Record => Keys(0); + + /// 查询所有有效的 Key 值。 + public Result> Keys(long flag) where T : Record => Keys(typeof(T), flag); + + /// 查询所有有效的 Key 值。 + public Result> Keys(Type model) => Keys(model, 0); + + /// 查询所有有效的 Key 值。 + public Result> Keys(Type model, long flag) + { + if (model != null) + { + try + { + var list = new List(); + var ts = TableStructure.ParseModel((Type)model); + var f = (flag == 0) ? TextUtility.EmptyString : TextUtility.Merge(" where _flag=", flag.ToString()); + var query = (Query)Query(TextUtility.Merge("select _key from [", ts.Table, "]", f, "; ")); + for (var r = 0; r < query.Rows; r++) + { + var cell = TextUtility.Trim(query.Text(r)); + if (cell.Length > 0) list.Add(cell); + } + query.Dispose(); + return new Result>(list); + } + catch (Exception ex) + { + return new Result>(ex); + } + } + return new Result>(new Exception("参数无效。")); + } + + #endregion + + #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.GetBytes(Encoding.UTF8).Length > bytes) + { + while (true) + { + t = t.Substring(0, t.Length - 1); + if (t.GetBytes(Encoding.UTF8).Length <= (bytes - 4)) break; + } + t = t + " ..."; + } + } + + return t; + } + + private static string GetColumnTypeName(ColumnType type) + { + switch (type) + { + case ColumnType.Binary: + return "blob"; + case ColumnType.Integer: + return "integer"; + case ColumnType.Float: + return "float"; + case ColumnType.VarChar: + case ColumnType.VarChar255: + case ColumnType.VarCharMax: + return "varchar"; + case ColumnType.Text: + return "text"; + case ColumnType.NVarChar: + case ColumnType.NVarChar255: + case ColumnType.NVarCharMax: + return "nvarchar"; + case ColumnType.NText: + return "ntext"; + default: + return null; + } + } + + private static string GetColumnDeclaration(ColumnAttribute column) + { + var type = TextUtility.EmptyString; + var length = NumberUtility.RestrictValue(column.Length, 0, 255).ToString(); + switch (column.Type) + { + case ColumnType.Binary: + type = "blob"; + break; + case ColumnType.Integer: + type = "integer"; + break; + case ColumnType.Float: + type = "real"; + break; + case ColumnType.VarChar: + case ColumnType.VarChar255: + type = TextUtility.Merge("varchar(", length, ")"); + break; + case ColumnType.VarCharMax: + type = TextUtility.Merge("varchar(255)"); + break; + case ColumnType.Text: + type = TextUtility.Merge("text"); + break; + case ColumnType.NVarChar: + case ColumnType.NVarChar255: + type = TextUtility.Merge("nvarchar(", length, ")"); + break; + case ColumnType.NVarCharMax: + type = TextUtility.Merge("nvarchar(255)"); + break; + case ColumnType.NText: + type = TextUtility.Merge("ntext"); + break; + default: + return TextUtility.EmptyString; + } + return TextUtility.Merge("[", (object)column.Field, "] ", type); + } + + /// 创建参数。 + /// + /// + public static SQLiteParameter CreateDataParameter(Parameter parameter) + { + if (parameter == null) throw new InvalidOperationException("参数无效。"); + return CreateDataParameter(parameter.Name, parameter.Type, parameter.Size, parameter.Value); + } + + /// 创建参数。 + public static SQLiteParameter CreateDataParameter(string name, ColumnType type, int size, object value) + { + var n = TextUtility.Trim(name); + if (TextUtility.IsBlank(n)) return null; + + var t = GetColumnTypeName(type); + + var s = size; + switch (type) + { + case ColumnType.VarChar: + s = NumberUtility.RestrictValue(s, 0, 8000); + break; + case ColumnType.NVarChar: + s = NumberUtility.RestrictValue(s, 0, 4000); + break; + case ColumnType.VarChar255: + case ColumnType.VarCharMax: + case ColumnType.NVarChar255: + case ColumnType.NVarCharMax: + s = NumberUtility.RestrictValue(s, 0, 255); + break; + default: + s = 0; + break; + } + + var v = value; + if (v is string && v != null && s > 0) + { + v = TextUtility.RestrictLength((string)v, s); + } + + var p = new SQLiteParameter(); + p.ParameterName = n; + p.TypeName = t; + p.Value = v; + if (s > 0) p.Size = s; + return p; + } + + /// 创建参数。 + public static SQLiteParameter CreateDataParameter(string name, string type, int size, object value) + { + var v = value; + if (value is string && value != null && size > 0) + { + v = TextUtility.RestrictLength((string)value, size); + } + + var p = new SQLiteParameter(); + p.ParameterName = name; + p.TypeName = type; + p.Value = v; + p.Size = size; + return p; + } + + /// 创建参数。 + public static SQLiteParameter CreateDataParameter(string name, string type, object value) + { + var p = new SQLiteParameter(); + p.ParameterName = name; + p.TypeName = type; + p.Value = value; + return p; + } + + /// 备份数据库,返回错误信息。 + public static string Backup(Sqlite source, Sqlite destination) + { + if (source == null) return "SQLite Backup Failed: Invalid Source"; + if (destination == null) return "SQLite Backup Failed: Invalid Destination"; + if (!source.Connect()) return "SQLite Backup Failed: Invalid Source Connection"; + if (!destination.Connect()) return "SQLite Backup Failed: Invalid Destination Connection"; + + try + { + source._db.BackupDatabase(destination._db, "main", "main", -1, null, 0); + return ""; + } + catch (Exception ex) + { + return "SQLite Load Failed: " + ex.Message; + } + } + + /// 创建参数。 + public static IDbDataParameter CreateParameter(string field, DbType type, int size, object value) + { + var p = new SQLiteParameter(); + p.ParameterName = field; + p.DbType = type; + p.Size = size; + p.Value = value; + return p; + } + + /// 创建参数。 + public static IDbDataParameter CreateParameter(string field, DbType type, object value) + { + var p = new SQLiteParameter(); + p.ParameterName = field; + p.DbType = type; + p.Value = value; + return p; + } + + /// 整理数据库,压缩未使用的空间。 + public const string Vacuum = "vacuum"; + + /// 内存数据库的地址。 + public const string Memory = ":memory:"; + + #endregion + + #region ORM + + private static string GetParameterName(string parameter) + { + var name = TextUtility.AntiInject(parameter, 255); + if (name.StartsWith("@") && name.Length > 1) + { + name = name.Substring(1, name.Length - 1); + } + return name; + } + + private static string GetParameterName(IDataParameter parameter) + { + var name = TextUtility.EmptyString; + if (parameter != null) + { + name = GetParameterName(parameter.ParameterName); + } + return name; + } + + private static List GetParametersNames(IEnumerable parameters) + { + var columns = new List(); + if (parameters != null) + { + foreach (var parameter in parameters) + { + var name = GetParameterName(parameter); + var isblank = TextUtility.IsBlank(name); + if (isblank) continue; + columns.Add(name); + } + } + return columns; + } + + private static string GenerateInsertStatement(string table, List columns) + { + var r = TextUtility.EmptyString; + var t = TextUtility.AntiInject(table, 255); + if (columns != null && !TextUtility.IsBlank(t)) + { + var count = 0; + var names = new List(); + var values = new List(); + foreach (var column in columns) + { + //names.Add(TextGenerator.Merge("[", column, "]")); + names.Add(TextUtility.Merge(column)); + values.Add("@" + column); + count += 1; + } + var sb = new StringBuilder(); + if (count > 0) + { + sb.Append("insert into [", t, "](", TextUtility.Join(", ", names), ") "); + sb.Append("values(", TextUtility.Join(", ", values), "); "); + } + r = sb.ToString(); + } + return r; + } + + /// 生成 INSERT INTO 语句。表名必须有效,无有效参数时将获取空结果。 + /// + /// + public static string GenerateInsertStatement(string table, IEnumerable parameters) + { + if (table == null) throw new ArgumentNullException(nameof(table)); + var t = TextUtility.AntiInject(table, 255); + if (TextUtility.IsBlank(t)) throw new ArgumentException("表名无效。", nameof(table)); + + var cs = GetParametersNames(parameters); + if (cs.Count < 1) return TextUtility.EmptyString; + + return GenerateInsertStatement(t, cs); + } + + private static string GenerateUpdateStatement(string table, string key, List columns) + { + var result = TextUtility.EmptyString; + var t = TextUtility.AntiInject(table, 255); + var k = TextUtility.AntiInject(key, 255); + if (columns != null && !TextUtility.IsBlank(t) && !TextUtility.IsBlank(k)) + { + var items = new List(); + foreach (var column in columns) + { + items.Add(TextUtility.Merge("[", column, "]=@", column)); + } + if (items.Count > 0) + { + result = TextUtility.Merge("update [", t, "] set ", TextUtility.Join(", ", items), " where [_key]='", k, "'; "); + } + } + return result; + } + + /// 生成 UPDATE 语句,键字段名为“_key”。表名必须有效,键值必须有效,无有效参数时将获取空结果。 + /// + /// + public static string GenerateUpdateStatement(string table, string key, IEnumerable parameters) + { + if (table == null) throw new ArgumentNullException("argTable"); + var t = TextUtility.AntiInject(table, 255); + if (TextUtility.IsBlank(t)) throw new ArgumentException("表名无效。", nameof(table)); + + if (key == null) throw new ArgumentNullException("argKey"); + var k = TextUtility.AntiInject(key, 255); + if (TextUtility.IsBlank(k)) throw new ArgumentException("键值无效。", nameof(key)); + + var columns = GetParametersNames(parameters); + if (columns.Count < 1) return TextUtility.EmptyString; + + return GenerateUpdateStatement(t, k, columns); + } + + #endregion + + } + +} + +#endif diff --git a/Apewer/Source/TableAttribute.cs b/Apewer/Source/TableAttribute.cs index 0bbbf63..7b5124d 100644 --- a/Apewer/Source/TableAttribute.cs +++ b/Apewer/Source/TableAttribute.cs @@ -15,8 +15,6 @@ namespace Apewer.Source private string _name; private bool _independent = false; - private bool _locked = false; - /// public TableAttribute(string name = null) { @@ -32,36 +30,19 @@ namespace Apewer.Source /// 表名。 public string Name { - get { return _name; } - set - { - if (_locked) return; - _name = TableStructure.RestrictName(value, false); - } + get => _name; + set => _name = TableStructure.RestrictName(value, false); } /// public bool Independent { - get { return _independent; } - internal set - { - if (_locked) return; - _independent = value; - } + get => _independent; + internal set => _independent = value; } /// - public override int GetHashCode() - { - return _name.GetHashCode(); - } - - /// 锁定属性,阻止修改。 - public void Lock() - { - _locked = true; - } + public override int GetHashCode() => _name.GetHashCode(); } diff --git a/Apewer/Source/TableStructure.cs b/Apewer/Source/TableStructure.cs index 956ce1b..a89b28c 100644 --- a/Apewer/Source/TableStructure.cs +++ b/Apewer/Source/TableStructure.cs @@ -14,8 +14,6 @@ namespace Apewer.Source public sealed class TableStructure { - private bool _locked = false; - private string _tablename = Constant.EmptyString; private bool _independent = false; @@ -26,38 +24,22 @@ namespace Apewer.Source /// 不依赖 Record 公共属性。 public bool Independent { - get { return _independent; } - private set { _independent = value; } + get => _independent; + private set => _independent = value; } /// 表名称。 public string Table { - get { return _tablename; } - private set { _tablename = value ?? ""; } + get => _tablename; + private set => _tablename = value ?? ""; } /// 列信息。 public Dictionary Columns { - get - { - if (_locked) - { - var copy = new Dictionary(_columns.Count); - foreach (var c in _columns) copy.Add(c.Key, c.Value); - return copy; - } - return _columns; - } - private set { _columns = value; } - } - - /// 锁定属性,阻止修改。 - public void Lock() - { - _locked = true; - foreach (var c in _columns) c.Value.Lock(); + get => _columns; + private set => _columns = value; } #region cache @@ -157,9 +139,6 @@ namespace Apewer.Source ts.Independent = ta.Independent; ts.Columns = columns; - // 锁定属性。 - ts.Lock(); - // 加入缓存。 if (useCache) { @@ -216,9 +195,6 @@ namespace Apewer.Source ta.Independent = RuntimeUtility.ContainsAttribute(type, true); - // 锁定属性。 - ta.Lock(); - // 加入缓存。 if (useCache) { @@ -291,9 +267,6 @@ namespace Apewer.Source ca.Property = property; - // 锁定属性。 - ca.Lock(); - return ca; } diff --git a/Apewer/Web/ApiMime.cs b/Apewer/Web/ApiMime.cs deleted file mode 100644 index f9e1ae3..0000000 --- a/Apewer/Web/ApiMime.cs +++ /dev/null @@ -1,34 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Text; - -namespace Apewer.Web -{ - - /// - [Serializable] - public sealed class ApiMime - { - - /// - public string Extension { get; set; } - - /// - public string ContentType { get; set; } - - /// 缓存过期时间,单位为秒,默认值 0 为不缓存(立即过期)。 - public int Expires { get; set; } - - /// - public ApiMime() { } - - internal ApiMime(string ext, string type, int expires) - { - Extension = ext; - ContentType = type; - Expires = expires; - } - - } - -} diff --git a/Apewer/Web/ApiOptions.cs b/Apewer/Web/ApiOptions.cs index 5cee1c5..d563047 100644 --- a/Apewer/Web/ApiOptions.cs +++ b/Apewer/Web/ApiOptions.cs @@ -9,31 +9,35 @@ namespace Apewer.Web public static class ApiOptions { - /// 允许 Invoker 解析 favicon.ico 请求。 + /// 允许解析 favicon.ico 请求。 /// 默认值:不允许,响应空。 public static bool AllowFavIcon { get; set; } = false; - /// 允许 Invoker 解析 robots.txt 请求。 + /// 允许解析 robots.txt 请求。 /// 默认值:不允许,拒绝搜索引擎收录根目录。 public static bool AllowRobots { get; set; } = false; - /// 允许 Invoker 枚举输出 Applications 或 Functions。 + /// 允许响应标头中包含 X-Content-Type-Options: nosiff。 + /// 默认值:不包含。当设置默认控制器时自动启用此属性。 + public static bool AllowContentTypeOptions { get; set; } = false; + + /// 允许枚举输出 Applications 或 Functions。 /// 默认值:不允许,不输出列表。 public static bool AllowEnumerate { get; set; } = false; - /// 允许 Invoker 输出 Exception 对象的属性。 + /// 允许输出 Exception 对象的属性。 /// 默认值:不允许输出。 public static bool AllowException { get; set; } = false; - /// 允许 Invoker 输出的 Json 对象缩进。 + /// 允许输出的 Json 对象缩进。 /// 默认值:不缩进。 public static bool JsonIndent { get; set; } = false; - /// 允许 Invoker 输出 Application 列表时包含模块名称。 + /// 允许输出 Application 列表时包含模块名称。 /// 默认值:不包含。 public static bool WithModuleName { get; set; } = false; - /// 允许 Invoker 输出 Application 列表时包含类型名称。 + /// 允许输出 Application 列表时包含类型名称。 /// 默认值:不包含。 public static bool WithTypeName { get; set; } = false; diff --git a/Apewer/Web/ApiProcessor.cs b/Apewer/Web/ApiProcessor.cs index 89a51c6..a88b8ba 100644 --- a/Apewer/Web/ApiProcessor.cs +++ b/Apewer/Web/ApiProcessor.cs @@ -140,18 +140,13 @@ namespace Apewer.Web // 检查执行的前提条件。 string PreCheck() { - // Context + // 检查条件。 if (!HaveContext) return "Context 无效。"; - - // Entries if (Entries == null) return "Entries 无效。"; - - // Method Method = GetMethod(); if (Method == HttpMethod.NULL) return "Methods 无效。"; - // AccessControl - // 在此之后可以输出 + // 响应标头。 if (ApiOptions.WithAccessControl) { AddHeader("Access-Control-Allow-Headers", "Content-Type"); @@ -161,6 +156,12 @@ namespace Apewer.Web var maxage = ApiOptions.AccessControlMaxAge; if (maxage > 0) AddHeader("Access-Control-Max-Age", maxage.ToString()); } + if (ApiOptions.AllowContentTypeOptions || ApiOptions.Default != null) + { + AddHeader("X-Content-Type-Options", "nosniff"); + } + + // 在此之后可以输出 // URL Url = GetURL(); @@ -419,6 +420,11 @@ namespace Apewer.Web SetCacheControl(apiResponse.Expires); Output(apiResponse.BinaryStream, type); } + else if (!string.IsNullOrEmpty(apiResponse.BinaryPath)) + { + SetCacheControl(apiResponse.Expires); + Output(StorageUtility.ReadFile(apiResponse.BinaryPath), type); + } return null; } diff --git a/Apewer/Web/ApiResponse.cs b/Apewer/Web/ApiResponse.cs index 556019d..9efbfeb 100644 --- a/Apewer/Web/ApiResponse.cs +++ b/Apewer/Web/ApiResponse.cs @@ -68,6 +68,8 @@ namespace Apewer.Web internal byte[] BinaryBytes; + internal string BinaryPath; + internal string BinaryType; #endregion diff --git a/Apewer/Web/StaticController.cs b/Apewer/Web/StaticController.cs index 71301d8..a9de0ef 100644 --- a/Apewer/Web/StaticController.cs +++ b/Apewer/Web/StaticController.cs @@ -113,11 +113,12 @@ namespace Apewer.Web var html = ReadWithSSI(path); var bytes = html.ToBinary(); Response.Binary(bytes, type); - return; } - - var stream = StorageUtility.OpenFile(path, true); - Response.Binary(stream, type); + else + { + var bytes = StorageUtility.OpenFile(path, true); + Response.Binary(bytes, type); + } } void ExecuteDirectory(string path) @@ -305,6 +306,27 @@ namespace Apewer.Web { var lower = extension.ToLower(); switch (lower) + { + case "css": return "text/css; charset=utf-8"; + case "htm": return "text/html; charset=utf-8"; + case "html": return "text/html; charset=utf-8"; + case "js": return "application/javascript; charset=utf-8"; + // case "json": return "application/json"; + case "json": return "text/json; charset=utf-8"; + case "shtml": return "text/html; charset=utf-8"; + } + switch (lower) + { + // case "m3u8": return "application/vnd.apple.mpegurl"; + case "m3u8": return "text/vnd.apple.mpegurl"; + case "txt": return "text/plain"; + case "xml": return "text/xml"; + case "htc": return "text/x-component"; + case "jad": return "text/vnd.sun.j2me.app-descriptor"; + case "mml": return "text/mathml"; + case "wml": return "text/vnd.wap.wml"; + } + switch (lower) { case "3gp": return "video/3gpp"; case "3gpp": return "video/3gpp"; @@ -318,7 +340,6 @@ namespace Apewer.Web case "bmp": return "image/x-ms-bmp"; case "cco": return "application/x-cocoa"; case "crt": return "application/x-x509-ca-cert"; - case "css": return "text/css"; case "deb": return "application/octet-stream"; case "der": return "application/x-x509-ca-cert"; case "dll": return "application/octet-stream"; @@ -332,32 +353,22 @@ namespace Apewer.Web case "flv": return "video/x-flv"; case "gif": return "image/gif"; case "hqx": return "application/mac-binhex40"; - case "htc": return "text/x-component"; - case "htm": return "text/html"; - case "html": return "text/html"; case "ico": return "image/x-icon"; case "img": return "application/octet-stream"; case "iso": return "application/octet-stream"; - case "jad": return "text/vnd.sun.j2me.app-descriptor"; 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 "js": return "application/javascript"; - // case "json": return "application/json"; - case "json": return "text/json"; case "kar": return "audio/midi"; case "kml": return "application/vnd.google-earth.kml+xml"; case "kmz": return "application/vnd.google-earth.kmz"; - // case "m3u8": return "application/vnd.apple.mpegurl"; - case "m3u8": return "text/vnd.apple.mpegurl"; case "m4a": return "audio/x-m4a"; case "m4v": return "video/x-m4v"; case "mid": return "audio/midi"; case "midi": return "audio/midi"; - case "mml": return "text/mathml"; case "mng": return "video/x-mng"; case "mov": return "video/quicktime"; case "mp3": return "audio/mpeg"; @@ -389,7 +400,6 @@ namespace Apewer.Web case "rtf": return "application/rtf"; case "run": return "application/x-makeself"; case "sea": return "application/x-sea"; - case "shtml": return "text/html"; case "sit": return "application/x-stuffit"; case "svg": return "image/svg+xml"; case "svgz": return "image/svg+xml"; @@ -399,12 +409,10 @@ namespace Apewer.Web case "tiff": return "image/tiff"; case "tk": return "application/x-tcl"; case "ts": return "video/mp2t"; - case "txt": return "text/plain"; case "war": return "application/java-archive"; case "wbmp": return "image/vnd.wap.wbmp"; case "webm": return "video/webm"; case "webp": return "image/webp"; - case "wml": return "text/vnd.wap.wml"; case "wmlc": return "application/vnd.wap.wmlc"; case "wmv": return "video/x-ms-wmv"; case "woff": return "font/woff"; @@ -412,7 +420,6 @@ namespace Apewer.Web case "xhtml": return "application/xhtml+xml"; case "xls": return "application/vnd.ms-excel"; case "xlsx": return "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"; - case "xml": return "text/xml"; case "xpi": return "application/x-xpinstall"; case "xspf": return "application/xspf+xml"; case "zip": return "application/zip"; diff --git a/Apewer/Web/WebUtility.cs b/Apewer/Web/WebUtility.cs index c541b3b..4a96707 100644 --- a/Apewer/Web/WebUtility.cs +++ b/Apewer/Web/WebUtility.cs @@ -547,6 +547,7 @@ namespace Apewer.Web response.Type = ApiFormat.Binary; response.BinaryStream = null; response.BinaryBytes = content; + response.BinaryPath = null; response.BinaryType = type ?? "application/octet-stream"; } @@ -557,6 +558,18 @@ namespace Apewer.Web response.Type = ApiFormat.Binary; response.BinaryStream = content; response.BinaryBytes = null; + response.BinaryPath = null; + response.BinaryType = type ?? "application/octet-stream"; + } + + /// 输出二进制。 + public static void SetBinary(ApiResponse response, string path, string type = "application/octet-stream") + { + if (response == null) return; + response.Type = ApiFormat.Binary; + response.BinaryStream = null; + response.BinaryBytes = null; + response.BinaryPath = path; response.BinaryType = type ?? "application/octet-stream"; } diff --git a/Apewer/_Common.props b/Apewer/_Common.props new file mode 100644 index 0000000..92549ca --- /dev/null +++ b/Apewer/_Common.props @@ -0,0 +1,28 @@ + + + + + true + bin\$(Configuration)\$(TargetFramework)\$(AssemblyName).xml + latest + CS0108,CS0162,CS0414,CS0612,CS0618,CS0649,CS1589,CS1570,CS1572,CS1573,CS3019,CS3021 + + + + + Apewer Libraries + 6.3.4 + + + + + true + true + + + + + DEBUG;TRACE;$(DefineConstants);$(AdditionalConstants) + + + \ No newline at end of file diff --git a/Apewer/_Extensions.cs b/Apewer/_Extensions.cs index d98b9e7..80ec493 100644 --- a/Apewer/_Extensions.cs +++ b/Apewer/_Extensions.cs @@ -549,6 +549,9 @@ public static class Extensions /// 输出二进制。 public static void Binary(this ApiResponse @this, Stream content, string type = "application/octet-stream") => WebUtility.SetBinary(@this, content, type); + /// 输出二进制。 + public static void Binary(this ApiResponse @this, string path, string type = "application/octet-stream") => WebUtility.SetBinary(@this, path, type); + /// 输出文件。 public static void File(this ApiResponse @this, Stream stream, string name, string type = "application/octet-stream") => WebUtility.SetFile(@this, stream, name, type); diff --git a/Apewer/_ChangeLog.md b/ChangeLog.md similarity index 95% rename from Apewer/_ChangeLog.md rename to ChangeLog.md index fd15662..8dada48 100644 --- a/Apewer/_ChangeLog.md +++ b/ChangeLog.md @@ -1,6 +1,16 @@  ### 最新提交 +### 6.3.4 +- Logger:支持指定文件路径; +- Models:StringPairs 支持通过构造函数设置初始容量; +- Source:去除 Attribute 的 Lock 方法,不再锁定属性; +- Source:增加 ORM 接口,统一方法; +- Web:增加 HTTP 头,应对 Edge 浏览器的检查; +- Web:新增 Response.Binary 重载,支持指定文件路径; +- Web:ApiOptions 支持设置 X-Content-Type-Options; +- Web:Staitc 控制器对部分纯文本文件的 Content-Type 加入“utf-8”。 + ### 6.3.3 - 全局:恢复 .NET Standard 2.0 的输出,用于支持 .NET Framework 4.6.1; - StorageUtility:优化了 ReadFile 的内存占用;