Browse Source

Apewer-6.1.0:WebAPI 新增功能;TextUtility 新增支持 MD。

dev
Elivo 4 years ago
parent
commit
ac8b7934d7
  1. 26
      Apewer/Apewer.csproj
  2. 3
      Apewer/DateTimeUtility.cs
  3. 20
      Apewer/Externals/MarkdownSharp-1.13/Demo.cs
  4. 84
      Apewer/Externals/MarkdownSharp-1.13/Info.cs
  5. 1656
      Apewer/Externals/MarkdownSharp-1.13/Markdown.cs
  6. 45
      Apewer/Externals/MarkdownSharp-1.13/MarkdownOptions.cs
  7. 28
      Apewer/Externals/MarkdownSharp-1.13/Token.cs
  8. 3
      Apewer/Internals/Constant.cs
  9. 45
      Apewer/Json.cs
  10. 2
      Apewer/Models/StringPairs.cs
  11. 4
      Apewer/NetworkUtility.cs
  12. 54
      Apewer/Source/ColumnAttribute.cs
  13. 128
      Apewer/Source/MySql.cs
  14. 6
      Apewer/Source/Record.cs
  15. 22
      Apewer/Source/TableAttribute.cs
  16. 148
      Apewer/Source/TableStructure.cs
  17. 16
      Apewer/TextUtility.cs
  18. 176
      Apewer/Web/ApiInternals.cs
  19. 195
      Apewer/Web/ApiInvoker.cs
  20. 3
      Apewer/Web/ApiRequest.cs
  21. 9
      Apewer/Web/ApiResponse.cs
  22. 108
      Apewer/Web/WebUtility.cs
  23. 14
      Apewer/_ChangeLog.md
  24. 12
      Apewer/_Class.cs
  25. 7
      Apewer/_Extensions.cs

26
Apewer/Apewer.csproj

@ -2,15 +2,15 @@
<!--<Import Sdk="Microsoft.NET.Sdk" Project="Sdk.props" />--> <!--<Import Sdk="Microsoft.NET.Sdk" Project="Sdk.props" />-->
<!-- Info --> <!-- Info: Package -->
<PropertyGroup> <PropertyGroup>
<AssemblyName>Apewer</AssemblyName> <AssemblyName>Apewer</AssemblyName>
<PackageId>Apewer</PackageId> <PackageId>Apewer</PackageId>
<Title>Apewer</Title> <Title>Apewer</Title>
<Version>6.0.14</Version> <Version>6.1.0</Version>
</PropertyGroup> </PropertyGroup>
<!-- Info --> <!-- Info: Copyright -->
<PropertyGroup> <PropertyGroup>
<Authors>Elivo</Authors> <Authors>Elivo</Authors>
<Company>Apewer Lab</Company> <Company>Apewer Lab</Company>
@ -21,7 +21,7 @@
<Product>Apewer Libraries</Product> <Product>Apewer Libraries</Product>
</PropertyGroup> </PropertyGroup>
<!-- Info --> <!-- Info: Build -->
<PropertyGroup> <PropertyGroup>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks> <AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<DocumentationFile>bin\$(Configuration)\$(TargetFramework)\$(AssemblyName).xml</DocumentationFile> <DocumentationFile>bin\$(Configuration)\$(TargetFramework)\$(AssemblyName).xml</DocumentationFile>
@ -30,19 +30,19 @@
<OutputType>Library</OutputType> <OutputType>Library</OutputType>
</PropertyGroup> </PropertyGroup>
<!-- NuGet --> <!-- Info: NuGet -->
<PropertyGroup Condition="'$(Configuration)'=='Release'"> <PropertyGroup Condition="'$(Configuration)'=='Release'">
<GeneratePackageOnBuild>true</GeneratePackageOnBuild> <GeneratePackageOnBuild>true</GeneratePackageOnBuild>
<IsPackable>true</IsPackable> <IsPackable>true</IsPackable>
</PropertyGroup> </PropertyGroup>
<!-- For .NET Standard 2.1 --> <!-- .NET Standard 2.1 -->
<PropertyGroup Condition="'$(TargetFramework)'=='netstandard2.1'"> <PropertyGroup Condition="'$(TargetFramework)'=='netstandard2.1'">
<AssemblyTitle>$(AssemblyName) - .NET Standard 2.1</AssemblyTitle> <AssemblyTitle>$(AssemblyName) - .NET Standard 2.1</AssemblyTitle>
<DefineConstants>NETSTD;HAVE_ASYNC;HAVE_BIG_INTEGER;$(AdditionalConstants)</DefineConstants> <DefineConstants>NETSTD;HAVE_ASYNC;HAVE_BIG_INTEGER;$(AdditionalConstants)</DefineConstants>
</PropertyGroup> </PropertyGroup>
<!-- For .NET Framework 2.0 --> <!-- .NET Framework 2.0 -->
<PropertyGroup Condition="'$(TargetFramework)'=='net20'"> <PropertyGroup Condition="'$(TargetFramework)'=='net20'">
<AssemblyTitle>$(AssemblyName) - .NET Framework 2.0</AssemblyTitle> <AssemblyTitle>$(AssemblyName) - .NET Framework 2.0</AssemblyTitle>
<DefineConstants>NETFX;NET20;$(AdditionalConstants)</DefineConstants> <DefineConstants>NETFX;NET20;$(AdditionalConstants)</DefineConstants>
@ -50,7 +50,6 @@
<ItemGroup Condition="'$(TargetFramework)'=='net20'"> <ItemGroup Condition="'$(TargetFramework)'=='net20'">
<Reference Include="CustomMarshalers" /> <Reference Include="CustomMarshalers" />
<Reference Include="System.Configuration" /> <Reference Include="System.Configuration" />
<Reference Include="System.Configuration.Install" />
<Reference Include="System.Design" /> <Reference Include="System.Design" />
<Reference Include="System.Management" /> <Reference Include="System.Management" />
<Reference Include="System.Security" /> <Reference Include="System.Security" />
@ -59,7 +58,7 @@
<Reference Include="System.Windows.Forms" /> <Reference Include="System.Windows.Forms" />
</ItemGroup> </ItemGroup>
<!-- For .NET Framework 4.0 --> <!-- .NET Framework 4.0 -->
<PropertyGroup Condition="'$(TargetFramework)'=='net40'"> <PropertyGroup Condition="'$(TargetFramework)'=='net40'">
<AssemblyTitle>$(AssemblyName) - .NET Framework 4.0</AssemblyTitle> <AssemblyTitle>$(AssemblyName) - .NET Framework 4.0</AssemblyTitle>
<DefineConstants>NETFX;NET40;HAVE_BIG_INTEGER;$(AdditionalConstants)</DefineConstants> <DefineConstants>NETFX;NET40;HAVE_BIG_INTEGER;$(AdditionalConstants)</DefineConstants>
@ -70,7 +69,6 @@
<Reference Include="PresentationCore" /> <Reference Include="PresentationCore" />
<Reference Include="System.Core" /> <Reference Include="System.Core" />
<Reference Include="System.Configuration" /> <Reference Include="System.Configuration" />
<Reference Include="System.Configuration.Install" />
<Reference Include="System.Design" /> <Reference Include="System.Design" />
<Reference Include="System.Drawing" /> <Reference Include="System.Drawing" />
<Reference Include="System.Management" /> <Reference Include="System.Management" />
@ -83,7 +81,7 @@
<Reference Include="WindowsBase" /> <Reference Include="WindowsBase" />
</ItemGroup> </ItemGroup>
<!-- For .NET Framework 4.6.1 --> <!-- .NET Framework 4.6.1 -->
<PropertyGroup Condition="'$(TargetFramework)'=='net461'"> <PropertyGroup Condition="'$(TargetFramework)'=='net461'">
<AssemblyTitle>$(AssemblyName) - .NET Framework 4.6.1</AssemblyTitle> <AssemblyTitle>$(AssemblyName) - .NET Framework 4.6.1</AssemblyTitle>
<DefineConstants>NETFX;NET461;NET45;HAVE_ASYNC;HAVE_BIG_INTEGER;$(AdditionalConstants)</DefineConstants> <DefineConstants>NETFX;NET461;NET45;HAVE_ASYNC;HAVE_BIG_INTEGER;$(AdditionalConstants)</DefineConstants>
@ -94,7 +92,6 @@
<Reference Include="PresentationCore" /> <Reference Include="PresentationCore" />
<Reference Include="System.Core" /> <Reference Include="System.Core" />
<Reference Include="System.Configuration" /> <Reference Include="System.Configuration" />
<Reference Include="System.Configuration.Install" />
<Reference Include="System.Design" /> <Reference Include="System.Design" />
<Reference Include="System.Drawing" /> <Reference Include="System.Drawing" />
<Reference Include="System.Management" /> <Reference Include="System.Management" />
@ -107,7 +104,7 @@
<Reference Include="WindowsBase" /> <Reference Include="WindowsBase" />
</ItemGroup> </ItemGroup>
<!-- For .NET Core 3.1 --> <!-- .NET Core 3.1 -->
<PropertyGroup Condition="'$(TargetFramework)'=='netcoreapp3.1'"> <PropertyGroup Condition="'$(TargetFramework)'=='netcoreapp3.1'">
<AssemblyTitle>$(AssemblyName) - .NET Core 3.1</AssemblyTitle> <AssemblyTitle>$(AssemblyName) - .NET Core 3.1</AssemblyTitle>
<DefineConstants>NETCORE;HAVE_ASYNC;HAVE_BIG_INTEGER;$(AdditionalConstants)</DefineConstants> <DefineConstants>NETCORE;HAVE_ASYNC;HAVE_BIG_INTEGER;$(AdditionalConstants)</DefineConstants>
@ -115,9 +112,6 @@
<!--<UseWindowsForms>true</UseWindowsForms>--> <!--<UseWindowsForms>true</UseWindowsForms>-->
</PropertyGroup> </PropertyGroup>
<ItemGroup Condition="'$(TargetFramework)'=='netcoreapp3.1'"> <ItemGroup Condition="'$(TargetFramework)'=='netcoreapp3.1'">
<!-- Condition="'$(DisableImplicitFrameworkReferences)' != 'true' And '$(TargetFrameworkIdentifier)' == '.NETCoreApp' And '$(_TargetFrameworkVersionWithoutV)' &gt;= '3.0'" -->
<!-- IsImplicitlyDefined="true" -->
<!-- Condition="('$(UseWPF)' == 'true') And ('$(UseWindowsForms)' == 'true')" -->
<FrameworkReference Include="Microsoft.AspNetCore.App" /> <FrameworkReference Include="Microsoft.AspNetCore.App" />
<FrameworkReference Include="Microsoft.WindowsDesktop.App" /> <FrameworkReference Include="Microsoft.WindowsDesktop.App" />
</ItemGroup> </ItemGroup>

3
Apewer/DateTimeUtility.cs

@ -276,6 +276,9 @@ namespace Apewer
return new Nullable<DateTime>(entity); return new Nullable<DateTime>(entity);
} }
/// <summary>创建新的零值 DateTime 对象。</summary>
public static DateTime Zero() => new DateTime(0L, DateTimeKind.Utc);
#endregion #endregion
} }

20
Apewer/Externals/MarkdownSharp-1.13/Demo.cs

@ -0,0 +1,20 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace MarkdownSharp
{
internal class Demo
{
public static string ToHtml(string markdown)
{
var processor = new Markdown();
var html = processor.Transform(markdown);
return html;
}
}
}

84
Apewer/Externals/MarkdownSharp-1.13/Info.cs

@ -0,0 +1,84 @@
/*
* MarkdownSharp
* -------------
* a C# Markdown processors
*
* Markdown is a text-to-HTML conversion tool for web writers
* Copyright (c) 2004 John Gruber
* https://daringfireball.net/projects/markdown/
*
* Markdown.NET
* Copyright (c) 2004-2009 Milan Negovan
* https://www.aspnetresources.com
* https://aspnetresources.com/blog/markdown_announced.aspx
*
* MarkdownSharp
* Copyright (c) 2009-2011 Jeff Atwood
* https://stackoverflow.com
* https://blog.codinghorror.com
* https://github.com/StackExchange/MarkdownSharp
*
* History: Milan ported the Markdown processor to C#. He granted license to me so I can open source it
* and let the community contribute to and improve MarkdownSharp.
*
*/
#region Copyright and license
/*
Copyright (c) 2009 - 2010 Jeff Atwood
http://www.opensource.org/licenses/mit-license.php
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
Copyright (c) 2003-2004 John Gruber
<http://daringfireball.net/>
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are
met:
* Redistributions of source code must retain the above copyright notice,
this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright
notice, this list of conditions and the following disclaimer in the
documentation and/or other materials provided with the distribution.
* Neither the name "Markdown" nor the names of its contributors may
be used to endorse or promote products derived from this software
without specific prior written permission.
This software is provided by the copyright holders and contributors "as
is" and any express or implied warranties, including, but not limited
to, the implied warranties of merchantability and fitness for a
particular purpose are disclaimed. In no event shall the copyright owner
or contributors be liable for any direct, indirect, incidental, special,
exemplary, or consequential damages (including, but not limited to,
procurement of substitute goods or services; loss of use, data, or
profits; or business interruption) however caused and on any theory of
liability, whether in contract, strict liability, or tort (including
negligence or otherwise) arising in any way out of the use of this
software, even if advised of the possibility of such damage.
*/
#endregion

1656
Apewer/Externals/MarkdownSharp-1.13/Markdown.cs

File diff suppressed because it is too large

45
Apewer/Externals/MarkdownSharp-1.13/MarkdownOptions.cs

@ -0,0 +1,45 @@
namespace MarkdownSharp
{
/// <summary>
/// Options for configuring MarkdownSharp.
/// </summary>
internal class MarkdownOptions
{
/// <summary>
/// when true, (most) bare plain URLs are auto-hyperlinked
/// WARNING: this is a significant deviation from the markdown spec
/// </summary>
public bool AutoHyperlink { get; set; }
/// <summary>
/// when true, RETURN becomes a literal newline
/// WARNING: this is a significant deviation from the markdown spec
/// </summary>
public bool AutoNewlines { get; set; }
/// <summary>
/// use ">" for HTML output, or " />" for XHTML output
/// </summary>
public string EmptyElementSuffix { get; set; }
/// <summary>
/// when false, email addresses will never be auto-linked
/// WARNING: this is a significant deviation from the markdown spec
/// </summary>
public bool LinkEmails { get; set; }
/// <summary>
/// when true, bold and italic require non-word characters on either side
/// WARNING: this is a significant deviation from the markdown spec
/// </summary>
public bool StrictBoldItalic { get; set; }
/// <summary>
/// when true, asterisks may be used for intraword emphasis
/// this does nothing if StrictBoldItalic is false
/// </summary>
public bool AsteriskIntraWordEmphasis { get; set; }
}
}

28
Apewer/Externals/MarkdownSharp-1.13/Token.cs

@ -0,0 +1,28 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace MarkdownSharp
{
internal enum TokenType
{
Text,
Tag
}
internal struct Token
{
public Token(TokenType type, string value)
{
Type = type;
Value = value;
}
public TokenType Type;
public string Value;
}
}

3
Apewer/Internals/Constant.cs

@ -38,6 +38,9 @@ namespace Apewer.Internals
/// <summary>所有 GUID 中的字符,包含字母、数字和连字符。</summary> /// <summary>所有 GUID 中的字符,包含字母、数字和连字符。</summary>
public const string GuidCollection = "0123456789ABCDEFabcdef-"; public const string GuidCollection = "0123456789ABCDEFabcdef-";
/// <summary>所有主键字符。</summary>
public const string KeyCollection = "0123456789abcdefghijklmnopqrstuvwxyz";
/// <summary>所有十六进制字符。</summary> /// <summary>所有十六进制字符。</summary>
public const string HexCollection = "0123456789abcdef"; public const string HexCollection = "0123456789abcdef";

45
Apewer/Json.cs

@ -16,6 +16,9 @@ namespace Apewer
/// <summary>Json。</summary> /// <summary>Json。</summary>
[Serializable] [Serializable]
public class Json public class Json
#if !NET20
: DynamicObject
#endif
{ {
#region 配置。 #region 配置。
@ -1698,6 +1701,48 @@ namespace Apewer
#endregion #endregion
#region Dynamic
#if !NET20
/// <summary></summary>
public override bool TryGetMember(GetMemberBinder binder, out object result)
{
var contains = false;
if (IsObject)
{
var property = GetProperty(binder.Name);
contains = property != null;
result = contains ? property.Value : null;
return contains;
}
result = null;
return contains;
}
/// <summary></summary>
public override bool TrySetMember(SetMemberBinder binder, object value)
{
var name = binder.Name;
if (value == null) return SetProperty(name);
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 int) return SetProperty(name, (int)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);
if (value is decimal) return SetProperty(name, (decimal)value);
return false;
}
#endif
#endregion
#region Statics #region Statics
#region 创建实例。 #region 创建实例。

2
Apewer/Models/StringPairs.cs

@ -15,7 +15,7 @@ namespace Apewer.Models
public string Add(string key, string value) public string Add(string key, string value)
{ {
if (string.IsNullOrEmpty(key)) return "参数 Name 无效。"; if (string.IsNullOrEmpty(key)) return "参数 Name 无效。";
if (string.IsNullOrEmpty(value)) return "参数 Value 无效。"; // if (string.IsNullOrEmpty(value)) return "参数 Value 无效。";
var kvp = new KeyValuePair<string, string>(key, value); var kvp = new KeyValuePair<string, string>(key, value);
Add(kvp); Add(kvp);

4
Apewer/NetworkUtility.cs

@ -58,7 +58,7 @@ namespace Apewer
} }
} }
catch { } catch { }
return new DateTime(1, 0, 0, 0, 0, 0, 0, DateTimeKind.Utc); return DateTimeUtility.Zero();
} }
/// <summary>从 NTP 服务器获取 UTC 时间。</summary> /// <summary>从 NTP 服务器获取 UTC 时间。</summary>
@ -90,7 +90,7 @@ namespace Apewer
return utc; return utc;
} }
catch { } catch { }
return new DateTime(1, 0, 0, 0, 0, 0, 0, DateTimeKind.Utc); return DateTimeUtility.Zero();
} }
private static uint SwapEndian(ulong x) private static uint SwapEndian(ulong x)

54
Apewer/Source/ColumnAttribute.cs

@ -21,6 +21,8 @@ namespace Apewer.Source
private bool _independent = false; private bool _independent = false;
private bool _locked = false;
/// <summary>使用自动的列名称。当类型为 VarChar 或 NVarChar 时必须指定长度。</summary> /// <summary>使用自动的列名称。当类型为 VarChar 或 NVarChar 时必须指定长度。</summary>
/// <exception cref="System.ArgumentException"></exception> /// <exception cref="System.ArgumentException"></exception>
public ColumnAttribute(ColumnType type = ColumnType.NVarChar255, int length = 0) public ColumnAttribute(ColumnType type = ColumnType.NVarChar255, int length = 0)
@ -41,19 +43,55 @@ namespace Apewer.Source
} }
/// <summary>属性。</summary> /// <summary>属性。</summary>
public PropertyInfo Property { get { return _property; } internal set { _property = value; } } public PropertyInfo Property
{
get { return _property; }
internal set
{
if (_locked) return;
_property = value;
}
}
/// <summary>字段名。</summary> /// <summary>字段名。</summary>
public string Field { get { return _field; } set { _field = value; } } public string Field
{
get { return _field; }
set
{
if (_locked) return;
_field = value;
}
}
/// <summary>指定字段的最大长度。</summary> /// <summary>指定字段的最大长度。</summary>
public int Length { get { return _length; } set { _length = value; } } public int Length
{
get { return _length; }
set
{
if (_locked) return;
_length = value;
}
}
/// <summary>字段类型。</summary> /// <summary>字段类型。</summary>
public ColumnType Type { get { return _type; } set { _type = value; } } public ColumnType Type
{
get { return _type; }
set
{
if (_locked) return;
_type = value;
}
}
/// <summary>Independent 特性。</summary> /// <summary>Independent 特性。</summary>
public bool Independent { get { return _independent; } internal set { _independent = value; } } public bool Independent
{
get { return _independent; }
internal set { _independent = value; }
}
/// <exception cref="System.ArgumentException"></exception> /// <exception cref="System.ArgumentException"></exception>
private void New(string field, ColumnType type, int length, bool underline) private void New(string field, ColumnType type, int length, bool underline)
@ -77,6 +115,12 @@ namespace Apewer.Source
} }
} }
/// <summary>锁定属性,阻止修改。</summary>
public void Lock()
{
_locked = true;
}
} }
} }

128
Apewer/Source/MySql.cs

@ -1,7 +1,5 @@
#if NET40 || NET461 #if NET40 || NET461
/* 2020.9.4.0 */
using Apewer; using Apewer;
using Apewer.Source; using Apewer.Source;
using Externals.MySql.Data.MySqlClient; using Externals.MySql.Data.MySqlClient;
@ -415,12 +413,12 @@ namespace Apewer.Source
return CreateTable(model.GetType()); return CreateTable(model.GetType());
} }
/// <summary></summary>
/// <summary>插入记录。成功时候返回空字符串,发生异常时返回异常信息。</summary> /// <summary>插入记录。成功时候返回空字符串,发生异常时返回异常信息。</summary>
public string Insert(Record entity) public string Insert(Record entity, bool resetKey = false)
{ {
if (entity == null) return "参数无效。"; if (entity == null) return "参数无效。";
entity.FixProperties(); entity.FixProperties();
if (resetKey) Record.ResetKey(entity);
var structure = null as TableStructure; var structure = null as TableStructure;
try { structure = TableStructure.ParseModel(entity); } try { structure = TableStructure.ParseModel(entity); }
@ -477,7 +475,7 @@ namespace Apewer.Source
} }
} }
/// <summary></summary> /// <summary>获取记录。</summary>
public Result<T> QueryRecord<T>(string key, long flag = 0) where T : Record public Result<T> QueryRecord<T>(string key, long flag = 0) where T : Record
{ {
var k = TextUtility.RestrictGuid(key); var k = TextUtility.RestrictGuid(key);
@ -515,6 +513,25 @@ namespace Apewer.Source
} }
} }
/// <summary>获取记录。</summary>
/// <param name="skip">要跳过的记录数,可用最小值为 0。</param>
/// <param name="count">要获取的记录数,可用最小值为 1。</param>
public Result<List<T>> QueryRecords<T>(int skip, int count) where T : Record
{
try
{
if (skip < 0) return new Result<List<T>>(new ArgumentOutOfRangeException(nameof(skip)));
if (count < 1) return new Result<List<T>>(new ArgumentOutOfRangeException(nameof(count)));
var tableName = TableStructure.ParseTable(typeof(T)).Name;
var sql = $"select * from `{tableName}` where _flag = 1 limit {skip}, {count}; ";
return QueryRecords<T>(sql);
}
catch (Exception exception)
{
return new Result<List<T>>(exception);
}
}
/// <summary>查询有效的 Key 值。</summary> /// <summary>查询有效的 Key 值。</summary>
public Result<List<string>> QueryKeys(Type model, long flag = 0) public Result<List<string>> QueryKeys(Type model, long flag = 0)
{ {
@ -562,10 +579,111 @@ namespace Apewer.Source
} }
} }
/// <summary>对表添加列,返回错误信息。</summary>
/// <typeparam name="T">记录类型。</typeparam>
/// <param name="column">列名称。</param>
/// <param name="type">字段类型。</param>
/// <param name="length">字段长度,仅对 VarChar 和 NVarChar 类型有效。</param>
/// <returns></returns>
public string AddColumn<T>(string column, ColumnType type, int length = 0) where T : Record
{
var columnName = SafeColumn(column);
if (columnName.IsEmpty()) return "列名无效。";
var tableName = TableStructure.ParseTable(typeof(T)).Name;
var columeType = "";
switch (type)
{
case ColumnType.Integer:
columeType = "bigint";
break;
case ColumnType.Float:
columeType = "double";
break;
case ColumnType.Binary:
columeType = "longblob";
break;
case ColumnType.DateTime:
columeType = "datetime";
break;
case ColumnType.VarChar:
case ColumnType.NVarChar:
columeType = $"varchar({length})";
break;
case ColumnType.VarChar255:
case ColumnType.NVarChar255:
columeType = "varchar(255)";
break;
case ColumnType.VarCharMax:
case ColumnType.NVarCharMax:
columeType = "varchar(max)";
break;
case ColumnType.Text:
columeType = "longtext";
break;
}
if (columeType.IsEmpty()) return "类型不支持。";
var sql = $"alter table `{tableName}` add {columnName} {columeType}; ";
var execute = Execute(sql) as Execute;
var error = execute.Error;
return error;
}
#endregion #endregion
#region static #region static
/// <summary>对文本转义,符合 SQL 安全性。可根据字段类型限制 UTF-8 字节数,默认为 0 时不限制字节数。</summary>
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;
}
/// <summary></summary>
public static string SafeTable(string table)
{
const string chars = "0123456789_-ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
var safety = TextUtility.RestrictCharacters(table, chars).SafeLower();
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;
}
/// <summary></summary>
public static string SafeColumn(string column) => SafeTable(column);
/// <summary></summary> /// <summary></summary>
/// <exception cref="ArgumentNullException"></exception> /// <exception cref="ArgumentNullException"></exception>
/// <exception cref="InvalidOperationException"></exception> /// <exception cref="InvalidOperationException"></exception>

6
Apewer/Source/Record.cs

@ -46,13 +46,15 @@ namespace Apewer.Source
[Column("_key", ColumnType.NVarChar, 128)] [Column("_key", ColumnType.NVarChar, 128)]
public virtual string Key { get { return _key; } set { _key = TextModifier.Compact(value, 128); } } public virtual string Key { get { return _key; } set { _key = TextModifier.Compact(value, 128); } }
internal static void SetNewKey(Record record) /// <summary>重置主键。</summary>
internal static void ResetKey(Record record)
{ {
if (record == null) return; if (record == null) return;
record.Key = GenerateKey(); record.Key = GenerateKey();
} }
internal static string GenerateKey() => Guid.NewGuid().ToString().ToLower().Replace("-", ""); /// <summary>生成新主键。</summary>
public static string GenerateKey() => Guid.NewGuid().ToString().ToLower().Replace("-", "");
internal static void FixProperties(Record record) internal static void FixProperties(Record record)
{ {

22
Apewer/Source/TableAttribute.cs

@ -15,6 +15,8 @@ namespace Apewer.Source
private string _name; private string _name;
private bool _independent = false; private bool _independent = false;
private bool _locked = false;
/// <summary></summary> /// <summary></summary>
public TableAttribute(string name = null) public TableAttribute(string name = null)
{ {
@ -27,18 +29,26 @@ namespace Apewer.Source
_name = TableStructure.RestrictName(name, underline); _name = TableStructure.RestrictName(name, underline);
} }
/// <summary></summary> /// <summary>表名。</summary>
public string Name public string Name
{ {
get { return _name; } get { return _name; }
set { _name = TableStructure.RestrictName(value, false); } set
{
if (_locked) return;
_name = TableStructure.RestrictName(value, false);
}
} }
/// <summary></summary> /// <summary></summary>
public bool Independent public bool Independent
{ {
get { return _independent; } get { return _independent; }
internal set { _independent = value; } internal set
{
if (_locked) return;
_independent = value;
}
} }
/// <summary></summary> /// <summary></summary>
@ -47,6 +57,12 @@ namespace Apewer.Source
return _name.GetHashCode(); return _name.GetHashCode();
} }
/// <summary>锁定属性,阻止修改。</summary>
public void Lock()
{
_locked = true;
}
} }
} }

148
Apewer/Source/TableStructure.cs

@ -14,6 +14,8 @@ namespace Apewer.Source
public sealed class TableStructure public sealed class TableStructure
{ {
private bool _locked = false;
private string _tablename = Constant.EmptyString; private string _tablename = Constant.EmptyString;
private bool _independent = false; private bool _independent = false;
@ -21,36 +23,95 @@ namespace Apewer.Source
internal TableStructure() { } internal TableStructure() { }
/// <summary></summary> /// <summary>不依赖 Record 公共属性。</summary>
public bool Independent { get { return _independent; } private set { _independent = value; } } public bool Independent
{
get { return _independent; }
private set { _independent = value; }
}
/// <summary></summary> /// <summary>表名称。</summary>
public string Table { get { return _tablename; } set { _tablename = value ?? ""; } } public string Table
{
get { return _tablename; }
private set { _tablename = value ?? ""; }
}
/// <summary></summary> /// <summary>列信息。</summary>
public Dictionary<string, ColumnAttribute> Columns { get { return _columns; } set { _columns = value; } } public Dictionary<string, ColumnAttribute> Columns
{
get
{
if (_locked)
{
var copy = new Dictionary<string, ColumnAttribute>(_columns.Count);
foreach (var c in _columns) copy.Add(c.Key, c.Value);
return copy;
}
return _columns;
}
private set { _columns = value; }
}
/// <summary>锁定属性,阻止修改。</summary>
public void Lock()
{
_locked = true;
foreach (var c in _columns) c.Value.Lock();
}
#region cache
private static Dictionary<string, TableStructure> _tsc = new Dictionary<string, TableStructure>();
private static Dictionary<string, TableAttribute> _tac = new Dictionary<string, TableAttribute>();
#endregion
#region static #region static
/// <summary></summary> /// <summary></summary>
/// <exception cref="System.Exception"></exception> /// <exception cref="System.Exception"></exception>
/// <exception cref="System.ArgumentNullException"></exception> /// <exception cref="System.ArgumentNullException"></exception>
public static TableStructure ParseModel(object entity) public static TableStructure ParseModel(object entity, bool useCache = true)
{ {
if (entity == null) throw new ArgumentNullException("参数无效"); if (entity == null) throw new ArgumentNullException("参数无效");
var type = entity.GetType(); var type = entity.GetType();
var result = ParseModel(type); var result = ParseModel(type, useCache);
return result; return result;
} }
/// <summary></summary> /// <summary></summary>
/// <exception cref="System.Exception"></exception> /// <exception cref="System.Exception"></exception>
/// <exception cref="System.ArgumentNullException"></exception> /// <exception cref="System.ArgumentNullException"></exception>
public static TableStructure ParseModel(Type model) public static TableStructure ParseModel<T>(bool useCache = true) where T : IRecord
{
return ParseModel(typeof(T), useCache);
}
/// <summary></summary>
/// <exception cref="System.Exception"></exception>
/// <exception cref="System.ArgumentNullException"></exception>
public static TableStructure ParseModel(Type model, bool useCache = true)
{ {
var type = model; var type = model;
if (type == null) throw new ArgumentNullException("参数无效"); if (type == null) throw new ArgumentNullException("参数无效");
// 使用缓存。
var cacheKey = type.FullName;
if (useCache)
{
var hint = null as TableStructure;
lock (_tsc)
{
if (_tsc.ContainsKey(cacheKey))
{
hint = _tsc[cacheKey];
}
}
if (hint != null) return hint;
}
// 检查基类。 // 检查基类。
// if (type.BaseType.FullName.Equals(typeof(DatabaseRecord).FullName) == false) return "基类不受支持。"; // if (type.BaseType.FullName.Equals(typeof(DatabaseRecord).FullName) == false) return "基类不受支持。";
@ -91,18 +152,57 @@ namespace Apewer.Source
columns = SortColumns(columns); columns = SortColumns(columns);
// 返回结果。 // 返回结果。
var result = new TableStructure(); var ts = new TableStructure();
result.Table = ta.Name; ts.Table = ta.Name;
result.Independent = ta.Independent; ts.Independent = ta.Independent;
result.Columns = columns; ts.Columns = columns;
return result;
// 锁定属性。
ts.Lock();
// 加入缓存。
if (useCache)
{
lock (_tsc)
{
if (!_tsc.ContainsKey(cacheKey))
{
_tsc.Add(cacheKey, ts);
}
}
}
return ts;
}
/// <summary></summary>
/// <param name="type"></param>
/// <exception cref="Exception"></exception>"
public static TableAttribute ParseTable<T>(bool useCache = true) where T : IRecord
{
return ParseTable(typeof(T), useCache);
} }
/// <summary></summary> /// <summary></summary>
/// <param name="type"></param> /// <param name="type"></param>
/// <exception cref="Exception"></exception>" /// <exception cref="Exception"></exception>"
public static TableAttribute ParseTable(Type type) public static TableAttribute ParseTable(Type type, bool useCache = true)
{ {
// 使用缓存。
var cacheKey = type.FullName;
if (useCache)
{
var hint = null as TableAttribute;
lock (_tac)
{
if (_tac.ContainsKey(cacheKey))
{
hint = _tac[cacheKey];
}
}
if (hint != null) return hint;
}
var tas = type.GetCustomAttributes(typeof(TableAttribute), false); var tas = type.GetCustomAttributes(typeof(TableAttribute), false);
if (tas.LongLength < 1L) throw new Exception(TextGenerator.Merge("类 ", type.FullName, " 不包含 ", typeof(TableAttribute).FullName, "。")); if (tas.LongLength < 1L) throw new Exception(TextGenerator.Merge("类 ", type.FullName, " 不包含 ", typeof(TableAttribute).FullName, "。"));
if (tas.LongLength > 1L) throw new Exception(TextGenerator.Merge("类 ", type.FullName, " 包含多个 ", typeof(TableAttribute).FullName, "。")); if (tas.LongLength > 1L) throw new Exception(TextGenerator.Merge("类 ", type.FullName, " 包含多个 ", typeof(TableAttribute).FullName, "。"));
@ -116,6 +216,21 @@ namespace Apewer.Source
ta.Independent = ClassUtility.ContainsAttribute<IndependentAttribute>(type, true); ta.Independent = ClassUtility.ContainsAttribute<IndependentAttribute>(type, true);
// 锁定属性。
ta.Lock();
// 加入缓存。
if (useCache)
{
lock (_tac)
{
if (!_tac.ContainsKey(cacheKey))
{
_tac.Add(cacheKey, ta);
}
}
}
return ta; return ta;
} }
@ -176,6 +291,9 @@ namespace Apewer.Source
ca.Property = property; ca.Property = property;
// 锁定属性。
ca.Lock();
return ca; return ca;
} }

16
Apewer/TextUtility.cs

@ -354,17 +354,22 @@ namespace Apewer
if (string.IsNullOrEmpty(text)) return Constant.EmptyString; if (string.IsNullOrEmpty(text)) return Constant.EmptyString;
var input = ToLower(text); var input = ToLower(text);
var max = input.Length; var max = maxLength;
if (maxLength > 0 && maxLength < input.Length) max = maxLength; if (max < 1 || max > input.Length) max = input.Length;
var sb = new StringBuilder(); var sb = new StringBuilder();
for (var i = 0; i < max; i++) var total = input.Length;
var length = 0;
for (var i = 0; i < total; i++)
{ {
var c = input[i]; var c = input[i];
if (Constant.HexCollection.IndexOf(c) < 0) continue; if (Constant.KeyCollection.IndexOf(c) < 0) continue;
sb.Append(c); sb.Append(c);
length += 1;
if (length >= max) break;
} }
var result = sb.ToString(); var result = sb.ToString();
if (result.Length > max) result = result.Substring(0, max);
return result; return result;
} }
@ -412,6 +417,9 @@ namespace Apewer
return match.Success; return match.Success;
} }
/// <summary>渲染 Markdown 文本为 HTML 文本。</summary>
public static string RenderMarkdown(string markdown) => MarkdownSharp.Demo.ToHtml(markdown);
/// <summary>合并用于启动进程的参数。</summary> /// <summary>合并用于启动进程的参数。</summary>
public static string MergeProcessArgument(params object[] args) public static string MergeProcessArgument(params object[] args)
{ {

176
Apewer/Web/ApiInternals.cs

@ -1,14 +1,16 @@
#if NETFX || NETCORE #if NETFX || NETCORE
using Apewer; using Apewer;
using Apewer.Network; using Apewer.Models;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Collections.Specialized; using System.Collections.Specialized;
using System.IO; using System.IO;
using System.Reflection; using System.Reflection;
#if NETFX
using System.Web; using System.Web;
using Apewer.Models; #endif
#if NETCORE #if NETCORE
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
@ -45,6 +47,7 @@ namespace Apewer.Web
if (deduplicates.Contains(assembly)) continue; if (deduplicates.Contains(assembly)) continue;
deduplicates.Add(assembly); deduplicates.Add(assembly);
} }
_assemblies = deduplicates;
var aes = new ObjectSet<ApiApplication>(); var aes = new ObjectSet<ApiApplication>();
foreach (var assembly in deduplicates) foreach (var assembly in deduplicates)
@ -201,9 +204,9 @@ namespace Apewer.Web
internal static StringPairs ParseUrlParameters internal static StringPairs ParseUrlParameters
#if NETFX #if NETFX
(System.Web.HttpRequest request) => WebUtility.ParseParameters(request.Url.Query); (HttpRequest request) => WebUtility.ParseParameters(request.Url.Query);
#else #else
(Microsoft.AspNetCore.Http.HttpRequest request) (HttpRequest request)
{ {
var list = new StringPairs(); var list = new StringPairs();
foreach (var key in request.Query.Keys) foreach (var key in request.Query.Keys)
@ -217,11 +220,7 @@ namespace Apewer.Web
} }
#endif #endif
#if NETFX internal static ApiRequest GetRequest(HttpRequest request)
internal static ApiRequest GetRequest(System.Web.HttpRequest request)
#else
internal static ApiRequest GetRequest(Microsoft.AspNetCore.Http.HttpRequest request)
#endif
{ {
if (request == null) return null; if (request == null) return null;
if (string.IsNullOrEmpty(request.Path)) return null; if (string.IsNullOrEmpty(request.Path)) return null;
@ -232,7 +231,7 @@ namespace Apewer.Web
apiRequest.Parameters = ParseUrlParameters(request); apiRequest.Parameters = ParseUrlParameters(request);
apiRequest.UserAgent = WebUtility.GetUserAgent(request); apiRequest.UserAgent = WebUtility.GetUserAgent(request);
#if NETFX #if NETFX
apiRequest.Referrer = request.UrlReferrer; apiRequest.Referrer = request.UrlReferrer;
#endif #endif
// 获取 Http Method。 // 获取 Http Method。
@ -258,8 +257,11 @@ namespace Apewer.Web
} }
} }
// Cookies。
apiRequest.Cookies = WebUtility.GetCookies(request);
// 解析 POST 请求。 // 解析 POST 请求。
if (apiRequest.Method == HttpMethod.POST) if (apiRequest.Method == Apewer.Network.HttpMethod.POST)
{ {
#if NETFX #if NETFX
var post = BinaryUtility.Read(request.InputStream); var post = BinaryUtility.Read(request.InputStream);
@ -293,6 +295,9 @@ namespace Apewer.Web
if (string.IsNullOrEmpty(session)) session = WebUtility.GetParameter(apiRequest.Parameters, "session"); if (string.IsNullOrEmpty(session)) session = WebUtility.GetParameter(apiRequest.Parameters, "session");
if (string.IsNullOrEmpty(page)) page = WebUtility.GetParameter(apiRequest.Parameters, "page"); if (string.IsNullOrEmpty(page)) page = WebUtility.GetParameter(apiRequest.Parameters, "page");
// 从 Cookie 中获取 Ticket。
if (string.IsNullOrEmpty(ticket)) ticket = apiRequest.Cookies.GetValue("ticket");
// 最后检查 URL 路径。 // 最后检查 URL 路径。
var paths = request.Path.ToString().Split('/'); var paths = request.Path.ToString().Split('/');
if (string.IsNullOrEmpty(application) && paths.Length >= 2) application = TextUtility.DecodeUrl(paths[1]); if (string.IsNullOrEmpty(application) && paths.Length >= 2) application = TextUtility.DecodeUrl(paths[1]);
@ -353,12 +358,12 @@ namespace Apewer.Web
} }
/// <summary>输出 UTF-8 文本。</summary> /// <summary>输出 UTF-8 文本。</summary>
internal static void RespondText(ApiResponse response, string content, string type = "text/plain") internal static void RespondText(ApiResponse response, string content, string type = "text/plain; charset=utf-8")
{ {
if (response == null) return; if (response == null) return;
response.Type = ApiFormat.Text; response.Type = ApiFormat.Text;
response.TextString = content; response.TextString = content;
response.TextType = type ?? "text/plain"; response.TextType = type ?? "text/plain; charset=utf-8";
} }
/// <summary>输出字节数组。</summary> /// <summary>输出字节数组。</summary>
@ -466,9 +471,7 @@ namespace Apewer.Web
#region HttpResponse #region HttpResponse
public static void AddHeader public static void AddHeader(HttpResponse response, string name, string value)
#if NETFX
(System.Web.HttpResponse response, string name, string value)
{ {
if (response == null || string.IsNullOrEmpty(name) || string.IsNullOrEmpty(value)) return; if (response == null || string.IsNullOrEmpty(name) || string.IsNullOrEmpty(value)) return;
#if NET20 #if NET20
@ -477,33 +480,14 @@ namespace Apewer.Web
try { response.Headers.Add(name, value); } catch { } try { response.Headers.Add(name, value); } catch { }
#endif #endif
} }
#else
(Microsoft.AspNetCore.Http.HttpResponse response, string name, string value)
{
if (response == null || string.IsNullOrEmpty(name) || string.IsNullOrEmpty(value)) return;
try { response.Headers.Add(name, value); } catch { }
}
#endif
public static void AddHeaders public static void AddHeaders(HttpResponse response, NameValueCollection collection)
#if NETFX
(System.Web.HttpResponse response, NameValueCollection collection)
{ {
if (response == null || collection == null) return; if (response == null || collection == null) return;
foreach (var key in collection.AllKeys) AddHeader(response, key, collection[key]); foreach (var key in collection.AllKeys) AddHeader(response, key, collection[key]);
} }
#else
(Microsoft.AspNetCore.Http.HttpResponse response, NameValueCollection collection)
{
if (response == null) return;
if (collection == null) return;
foreach (var key in collection.AllKeys) AddHeader(response, key, collection[key]);
}
#endif
public static void AddHeaders public static void AddHeaders(HttpResponse response, StringPairs headers)
#if NETFX
(System.Web.HttpResponse response, StringPairs headers)
{ {
if (response == null || headers == null) return; if (response == null || headers == null) return;
foreach (var key in headers.GetAllKeys()) foreach (var key in headers.GetAllKeys())
@ -512,73 +496,121 @@ namespace Apewer.Web
foreach (var value in values) AddHeader(response, key, value); foreach (var value in values) AddHeader(response, key, value);
} }
} }
#else
(Microsoft.AspNetCore.Http.HttpResponse response, StringPairs headers) #if NETFX
private static FieldInfo CacheControlField = null;
private static void SetCacheControlField(HttpResponse response, string value)
{ {
if (response == null || headers == null) return; if (CacheControlField == null)
foreach (var key in headers.GetAllKeys())
{ {
var values = headers.GetValues(key); var type = typeof(HttpResponse);
foreach (var value in values) AddHeader(response, key, value); var field = type.GetField("_cacheControl", BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.DeclaredOnly);
if (field == null) return;
CacheControlField = field;
} }
if (CacheControlField != null) CacheControlField.SetValue(response, value);
} }
#endif #endif
public static void SetTextPlain /// <summary>设置缓存时间,单位为秒,最大为 2592000 秒(30 天)。</summary>
#if NETFX public static void SetCacheControl
(System.Web.HttpResponse response, string value = null)
(HttpResponse response, int seconds = 0)
{ {
if (response == null) return; if (response == null) return;
if (string.IsNullOrEmpty(value)) var s = seconds;
if (s < 0) s = 0;
if (s > 2592000) s = 2592000;
#if NETFX
if (s > 0)
{ {
response.ContentType = "text/plain"; SetCacheControlField(response, $"public, max-age={s}, s-maxage={s}");
response.Charset = "utf-8";
} }
else else
{ {
response.ContentType = value; var minutes = s < 60 ? 0 : (s / 60);
try { response.CacheControl = "no-cache"; } catch { }
try { response.Expires = minutes; } catch { }
AddHeader(response, "Pragma", "no-cache");
} }
}
#else #else
(Microsoft.AspNetCore.Http.HttpResponse response, string value = null) if (s > 0)
{
AddHeader(response, "Cache-Control", $"public, max-age={s}, s-maxage={s}");
}
else
{
var minutes = s < 60 ? 0 : (s / 60);
AddHeader(response, "Cache-Control", "no-cache, no-store, must-revalidate");
AddHeader(response, "Expires", minutes.ToString());
AddHeader(response, "Pragma", "no-cache");
}
#endif
}
public static void SetTextPlain(HttpResponse response, string value = null)
{ {
if (response == null) return; if (response == null) return;
response.ContentType = string.IsNullOrEmpty(value) ? "text/plain; charset=utf-8" : value; const string plain = "text/plain; charset=utf-8";
// response.ContentType = "text/plain";
// response.Charset = "utf-8";
response.ContentType = string.IsNullOrEmpty(value) ? plain : value;
} }
#endif
public static void SetContentLength public static void SetContentLength(HttpResponse response, long value)
#if NETFX
(System.Web.HttpResponse response, long value)
{ {
if (response == null) return; if (response == null) return;
if (value < 0L) return; if (value < 0L) return;
#if NETFX
response.AddHeader("Content-Length", value.ToString()); response.AddHeader("Content-Length", value.ToString());
}
#else #else
(Microsoft.AspNetCore.Http.HttpResponse response, long value)
{
if (response == null) return;
if (value < 0L) return;
response.ContentLength = value; response.ContentLength = value;
}
#endif #endif
}
public static Stream GetStream public static Stream GetStream(HttpResponse response)
#if NETFX
(System.Web.HttpResponse response)
{ {
if (response == null) return null; if (response == null) return null;
#if NETFX
return response.OutputStream; return response.OutputStream;
}
#else #else
(Microsoft.AspNetCore.Http.HttpResponse response)
{
if (response == null) return null;
return response.Body; return response.Body;
}
#endif #endif
}
/// <summary>设置响应的 Cookies。</summary>
public static void SetCookies(HttpResponse response, IEnumerable<KeyValuePair<string, string>> cookies)
{
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie
// https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Headers/Set-Cookie
// <cookie-name> 可以是除了控制字符 (CTLs)、空格 (spaces) 或制表符 (tab)之外的任何 US-ASCII 字符。
// 同时不能包含以下分隔字符: ( ) < > @ , ; : \ " / [ ] ? = { }.
// <cookie-value> 是可选的,如果存在的话,那么需要包含在双引号里面。
// 支持除了控制字符(CTLs)、空格(whitespace)、双引号(double quotes)、逗号(comma)、分号(semicolon)以及反斜线(backslash)之外的任意 US-ASCII 字符。
// 关于编码:许多应用会对 cookie 值按照URL编码(URL encoding)规则进行编码,但是按照 RFC 规范,这不是必须的。不过满足规范中对于 <cookie-value> 所允许使用的字符的要求是有用的。
if (cookies == null) return;
var ps = new List<string>();
foreach (var kvp in cookies)
{
var k = kvp.Key;
var v = kvp.Value;
if (k.IsEmpty()) continue;
ps.Add(TextUtility.Merge(k, "="), v);
}
if (ps.Count < 1) return;
var value = TextUtility.Merge("; ", ps);
AddHeader(response, "Set-Cookie", value);
}
#endregion #endregion

195
Apewer/Web/ApiInvoker.cs

@ -233,7 +233,7 @@ namespace Apewer.Web
if (request == null || response == null) return; if (request == null || response == null) return;
// 创建应用程序实例。 // 创建应用程序实例。
var target = null as ApiController; var controller = null as ApiController;
try try
{ {
if (application == null) if (application == null)
@ -246,7 +246,7 @@ namespace Apewer.Web
//var instance = assembly.CreateInstance(argApplication.FullName, false); //var instance = assembly.CreateInstance(argApplication.FullName, false);
//var handler = Activator.CreateInstance(null, argApplication.FullName); //var handler = Activator.CreateInstance(null, argApplication.FullName);
//target = (Application)handler.Unwrap(); //target = (Application)handler.Unwrap();
target = (ApiController)Activator.CreateInstance(application.Type); controller = (ApiController)Activator.CreateInstance(application.Type);
} }
} }
catch (Exception exception) catch (Exception exception)
@ -258,13 +258,13 @@ namespace Apewer.Web
// 调用功能。 // 调用功能。
try try
{ {
target.Request = request; controller.Request = request;
target.Response = response; controller.Response = response;
target.Context = Context; controller.Context = Context;
if (target.AfterInitialized != null) target.AfterInitialized.Invoke(); if (controller.AfterInitialized != null) controller.AfterInitialized.Invoke();
var allowed = target.AllowFunction; var allowed = controller.AllowFunction;
if (application.Independent) allowed = false; if (application.Independent) allowed = false;
if (allowed) if (allowed)
@ -275,8 +275,8 @@ namespace Apewer.Web
} }
else else
{ {
var result = function.Method.Invoke(target, null); var result = function.Method.Invoke(controller, null);
target.Dispose(); controller.Dispose();
if (response.Status.IsBlank()) response.Status = "ok"; if (response.Status.IsBlank()) response.Status = "ok";
if (function.Returnable && result != null) if (function.Returnable && result != null)
{ {
@ -288,102 +288,124 @@ namespace Apewer.Web
} }
} }
} }
} }
catch (Exception exception) catch (Exception exception)
{ {
response.Error(exception.InnerException); response.Error(exception.InnerException);
} }
try { target.Dispose(); } catch { }
try { controller.Dispose(); } catch { }
} }
private string Output() private void OutputMain()
{ {
if (Context == null) return "Invoker 的 Context 无效。"; var response = Context.Response;
if (ApiResponse == null) return "Invoker 的 ApiResponse 无效。";
try // 设置自定义头。
ApiInternals.AddHeaders(response, ApiResponse.Headers);
var setCookies = WebUtility.SetCookies(response, ApiResponse.Cookies);
if (setCookies.NotEmpty())
{ {
var response = Context.Response; ApiResponse.Error("系统错误。");
var body = ApiInternals.GetStream(response); ApiResponse.Data.Reset(Json.NewObject());
ApiInternals.AddHeaders(response, ApiResponse.Headers); ApiResponse.Data["cookies"] = setCookies;
switch (ApiResponse.Type) }
{
case ApiFormat.Json: // 过期。
{ ApiInternals.SetCacheControl(response, ApiResponse.Expires);
var text = ApiInternals.ExportJson(ApiResponse, ApiOptions.JsonIndent, ApiOptions.AllowException);
var data = TextUtility.ToBinary(text); // 判断内容类型输出。
ApiInternals.SetTextPlain(response); var body = ApiInternals.GetStream(response);
ApiInternals.SetContentLength(response, data.LongLength); switch (ApiResponse.Type)
body.Write(data); {
} case ApiFormat.Json:
break; {
case ApiFormat.Text: var text = ApiInternals.ExportJson(ApiResponse, ApiOptions.JsonIndent, ApiOptions.AllowException);
var data = TextUtility.ToBinary(text);
ApiInternals.SetTextPlain(response);
ApiInternals.SetContentLength(response, data.LongLength);
body.Write(data);
}
break;
case ApiFormat.Text:
{
var data = TextUtility.ToBinary(ApiResponse.TextString);
var type = ApiResponse.TextType;
ApiInternals.SetTextPlain(response, type);
ApiInternals.SetContentLength(response, data.LongLength);
body.Write(data);
}
break;
case ApiFormat.Binary:
{
Context.Response.ContentType = ApiResponse.BinaryType ?? "application/octet-stream";
if (ApiResponse.BinaryBytes != null)
{ {
var data = TextUtility.ToBinary(ApiResponse.TextString); var data = ApiResponse.BinaryBytes;
var type = ApiResponse.TextType; var length = data.LongLength;
ApiInternals.SetTextPlain(response, type); ApiInternals.SetContentLength(response, length);
ApiInternals.SetContentLength(response, data.LongLength); if (length > 0L) body.Write(data);
body.Write(data);
} }
break; else if (ApiResponse.BinaryStream != null && ApiResponse.BinaryStream.CanRead)
case ApiFormat.Binary:
{ {
Context.Response.ContentType = ApiResponse.BinaryType ?? "application/octet-stream"; var source = ApiResponse.BinaryStream;
if (ApiResponse.BinaryBytes != null) try
{ {
var data = ApiResponse.BinaryBytes; var length = source.Length - source.Position;
var length = data.LongLength;
ApiInternals.SetContentLength(response, length); ApiInternals.SetContentLength(response, length);
if (length > 0L) body.Write(data); if (length > 0) body.Write(source);
}
else if (ApiResponse.BinaryStream != null && ApiResponse.BinaryStream.CanRead)
{
var source = ApiResponse.BinaryStream;
try
{
var length = source.Length - source.Position;
ApiInternals.SetContentLength(response, length);
if (length > 0) body.Write(source);
}
catch { }
KernelUtility.Dispose(source);
} }
catch { }
KernelUtility.Dispose(source);
} }
break; }
case ApiFormat.File: break;
{ case ApiFormat.File:
var type = ApiResponse.FileType ?? "application/octet-stream"; {
var name = TextUtility.EncodeUrl(ApiResponse.FileName); var type = ApiResponse.FileType ?? "application/octet-stream";
var disposition = TextUtility.Merge("attachment; filename=", name); var name = TextUtility.EncodeUrl(ApiResponse.FileName);
var disposition = TextUtility.Merge("attachment; filename=", name);
response.ContentType = type; response.ContentType = type;
ApiInternals.AddHeader(response, "Content-Disposition", disposition); ApiInternals.AddHeader(response, "Content-Disposition", disposition);
var source = ApiResponse.FileStream; var source = ApiResponse.FileStream;
try try
{
if (source != null && source.CanRead)
{ {
if (source != null && source.CanRead) var length = source.Length - source.Position;
{ ApiInternals.SetContentLength(response, length);
var length = source.Length - source.Position; if (length > 0) body.Write(source);
ApiInternals.SetContentLength(response, length);
if (length > 0) body.Write(source);
}
} }
catch { }
KernelUtility.Dispose(source);
}
break;
case ApiFormat.Redirect:
{
WebUtility.RedirectResponse(response, ApiResponse.RedirectUrl);
} }
break; catch { }
} KernelUtility.Dispose(source);
// Context.Response.Flush(); }
// Context.Response.End(); break;
case ApiFormat.Redirect:
{
WebUtility.RedirectResponse(response, ApiResponse.RedirectUrl);
}
break;
}
// Context.Response.Flush();
// Context.Response.End();
}
private string Output()
{
if (Context == null) return "Invoker 的 Context 无效。";
if (ApiResponse == null) return "Invoker 的 ApiResponse 无效。";
#if DEBUG
OutputMain();
#else
try
{
OutputMain();
} }
catch (Exception ex) { return ex.Message; } catch (Exception ex) { return ex.Message; }
#endif
return null; return null;
} }
@ -406,6 +428,14 @@ namespace Apewer.Web
invoker.Run(); invoker.Run();
} }
/// <summary>获取用于执行 API 的程序集。</summary>
public static Assembly[] GetAssemblies()
{
var list = ApiInternals.Assemblies;
if (list == null) return new Assembly[0];
return list.ToArray();
}
#if NETFX #if NETFX
/// <summary>解析请求,获取指定程序集中定义的入口,并执行。指定程序集为 NULL 值时,将自动从应用程序域获取所有 WebAPI 入口。</summary> /// <summary>解析请求,获取指定程序集中定义的入口,并执行。指定程序集为 NULL 值时,将自动从应用程序域获取所有 WebAPI 入口。</summary>
@ -467,8 +497,7 @@ namespace Apewer.Web
/// <summary>设置 Kestrel 的 API 入口。</summary> /// <summary>设置 Kestrel 的 API 入口。</summary>
public static void SetKestrelEntries(Assembly assembly, bool sort = true) public static void SetKestrelEntries(Assembly assembly, bool sort = true)
{ {
var assemblies = new Assembly[] { assembly }; var assemblies = new Assembly[] { assembly ?? Assembly.GetCallingAssembly() };
if (assemblies[0] == null) assemblies[0] = Assembly.GetCallingAssembly();
KestrelEntries = ApiInternals.GetEntries(assemblies, sort); KestrelEntries = ApiInternals.GetEntries(assemblies, sort);
} }

3
Apewer/Web/ApiRequest.cs

@ -46,6 +46,9 @@ namespace Apewer.Web
/// <summary>HTTP 头。</summary> /// <summary>HTTP 头。</summary>
public StringPairs Headers { get; set; } = new StringPairs(); public StringPairs Headers { get; set; } = new StringPairs();
/// <summary>Cookies。</summary>
public StringPairs Cookies { get; set; } = new StringPairs();
#endregion #endregion
#region API 请求。 #region API 请求。

9
Apewer/Web/ApiResponse.cs

@ -15,9 +15,12 @@ namespace Apewer.Web
private Json _data = Json.NewObject(); private Json _data = Json.NewObject();
/// <summary></summary> /// <summary>HTTP 头。</summary>
public StringPairs Headers { get; set; } = new StringPairs(); public StringPairs Headers { get; set; } = new StringPairs();
/// <summary>Cookies。</summary>
public StringPairs Cookies { get; set; } = new StringPairs();
#if NETFX #if NETFX
internal System.Web.HttpContext Context { get; set; } internal System.Web.HttpContext Context { get; set; }
#endif #endif
@ -59,6 +62,10 @@ namespace Apewer.Web
internal ApiFormat Type = ApiFormat.Json; internal ApiFormat Type = ApiFormat.Json;
internal Exception Exception { get; set; } internal Exception Exception { get; set; }
/// <summary>设置缓存过期时间,单位为秒。默认值:0,立即过期,不缓存。</summary>
/// <remarks>在 .NET Framework 中,此设置可能无效。</remarks>
public int Expires { get; set; }
#endregion #endregion
#region 输出纯文本。 #region 输出纯文本。

108
Apewer/Web/WebUtility.cs

@ -154,8 +154,9 @@ namespace Apewer.Web
var d = GetDirectIP(request); var d = GetDirectIP(request);
var f = GetForwardedIP(request); var f = GetForwardedIP(request);
// return string.IsNullOrEmpty(f) ? "NULL" : $"{f},NULL"; // 去除端口。
// return string.IsNullOrEmpty(f) ? $"{d}" : $"{f},{d}"; if (d.NotEmpty()) d = d.Split(':')[0];
if (f.NotEmpty()) f = f.Split(':')[0];
// 没有代理,返回 Direct IP。 // 没有代理,返回 Direct IP。
if (string.IsNullOrEmpty(f)) return d; if (string.IsNullOrEmpty(f)) return d;
@ -319,8 +320,13 @@ namespace Apewer.Web
foreach (var key in request.Headers.Keys) foreach (var key in request.Headers.Keys)
#endif #endif
{ {
var value = request.Headers[key].ToString(); try
sp.Add(new KeyValuePair<string, string>(key, value)); {
var values = request.Headers[key];
var text = values.ToString();
sp.Add(new KeyValuePair<string, string>(key, text));
}
catch { }
} }
} }
return sp; return sp;
@ -377,6 +383,20 @@ namespace Apewer.Web
#endif #endif
} }
/// <summary>设置响应。</summary>
public static string Respond(ApiResponse response, Json data, bool lower = true)
{
if (response == null) return "Response 对象无效。";
if (data != null)
{
if (lower) data = Json.ToLower(data);
response.Data.Reset(data);
}
return null;
}
/// <summary>设置响应,当发生错误时设置响应。返回错误信息。</summary> /// <summary>设置响应,当发生错误时设置响应。返回错误信息。</summary>
public static string Respond(ApiResponse response, IList list, bool lower = true, int depth = -1, bool force = false) public static string Respond(ApiResponse response, IList list, bool lower = true, int depth = -1, bool force = false)
{ {
@ -631,6 +651,86 @@ namespace Apewer.Web
#endregion #endregion
#region Cookies
/// <summary>获取 Cookies。</summary>
public static StringPairs GetCookies(HttpRequest request)
{
var sp = new StringPairs();
if (request != null && request.Cookies != null)
{
#if NETFX
foreach (var key in request.Cookies.AllKeys)
{
try
{
var cookie = request.Cookies[key];
var value = cookie.Value ?? "";
sp.Add(new KeyValuePair<string, string>(key, value));
}
catch { }
}
#else
foreach (var key in request.Cookies.Keys)
{
try
{
var value = request.Cookies[key] ?? "";
sp.Add(new KeyValuePair<string, string>(key, value));
}
catch { }
}
#endif
}
return sp;
}
/// <summary>设置响应的 Cookies。</summary>
public static string SetCookies(HttpResponse response, IEnumerable<KeyValuePair<string, string>> cookies)
{
// experimentation_subject_id
if (cookies == null) return "参数 Response 无效。";
var count = 0;
foreach (var kvp in cookies)
{
count++;
var error = SetCookie(response, kvp.Key, kvp.Value);
if (error.NotEmpty()) return error;
}
return null;
}
/// <summary>设置响应的 Cookie。</summary>
public static string SetCookie(HttpResponse response, string key, string value)
{
if (response == null) return null;
var k = key.SafeTrim();
var v = value.SafeTrim();
if (k.IsEmpty()) return "参数 Key 无效。";
try
{
#if NETFX
var cookie = new HttpCookie(k, v);
var now = DateTime.Now;
cookie.Expires = v.IsEmpty() ? now.AddDays(-1) : now.AddYears(1);
response.SetCookie(cookie);
#else
// var options = new CookieOptions();
// response.Cookies.Append(key, value, options);
response.Cookies.Append(key, value);
#endif
return null;
}
catch (Exception ex)
{
return ex.Message;
}
}
#endregion
#endif #endif
} }

14
Apewer/_ChangeLog.md

@ -5,6 +5,20 @@
### 最新提交 ### 最新提交
### 6.1.0
- DateTime:修正了超出范围值的问题;
- TextUtility:新增 RenderMarkdown 方法,支持将 Markdown 文本转换为 HTML 文本;
- TextUtility:放宽了 SafeKey 限制十六进制字符的限制,现在允许所有字母和数字;
- Source:解析 Record 结构加入缓存,以提高性能;
- MySql:新增 Escape 方法;
- MySql:新增 QueryRecords 重载,支持分页查询;
- MySql:新增 AddColumn 方法,支持增加单列;
- MySql:新增 SafeTable 方法和 SafeColumn 方法,提供对表明和列名的检查;
- WebAPI:支持设置 Response 的缓存过期时间(在 .NET Framework 中可能无效);
- WebAPI:添加 Response.Respond(Json) 方法;
- WebAPI:支持读取或设置 Cookies,且 ApiRequest 从 Cookies 中读取 Ticket;
- WebAPI:Response.Text 现在默认在 Content-Type 中指定 UTF-8。
### 6.0.14 ### 6.0.14
- 修正了 Json.Parse 的 NULL 引用问题; - 修正了 Json.Parse 的 NULL 引用问题;
- 修正了生成 MySQL 的 Insert 语句字段报错问题。 - 修正了生成 MySQL 的 Insert 语句字段报错问题。

12
Apewer/_Class.cs

@ -8,16 +8,16 @@ public class Class<T> : IComparable, IComparable<T>, IComparable<Class<T>>
private bool _equals = false; private bool _equals = false;
/// <summary>装箱对象。</summary> /// <summary>装箱对象。</summary>
public T Value { get; set; } public virtual T Value { get; set; }
/// <summary></summary> /// <summary></summary>
public bool IsNull public virtual bool IsNull
{ {
get { return Value == null; } get { return Value == null; }
} }
/// <summary></summary> /// <summary></summary>
public bool HasValue public virtual bool HasValue
{ {
get { return Value != null; } get { return Value != null; }
} }
@ -63,7 +63,7 @@ public class Class<T> : IComparable, IComparable<T>, IComparable<Class<T>>
/// <summary></summary> /// <summary></summary>
/// <exception cref="MissingMemberException"></exception> /// <exception cref="MissingMemberException"></exception>
/// <exception cref="NotSupportedException"></exception> /// <exception cref="NotSupportedException"></exception>
public int CompareTo(object obj) public virtual int CompareTo(object obj)
{ {
if (obj != null && obj is T) return CompareTo((T)obj); if (obj != null && obj is T) return CompareTo((T)obj);
if (obj != null && obj is Class<T>) return CompareTo(obj as Class<T>); if (obj != null && obj is Class<T>) return CompareTo(obj as Class<T>);
@ -76,7 +76,7 @@ public class Class<T> : IComparable, IComparable<T>, IComparable<Class<T>>
/// <summary></summary> /// <summary></summary>
/// <exception cref="MissingMemberException"></exception> /// <exception cref="MissingMemberException"></exception>
/// <exception cref="NotSupportedException"></exception> /// <exception cref="NotSupportedException"></exception>
public int CompareTo(T other) public virtual int CompareTo(T other)
{ {
if (Value == null) throw new MissingMemberException(typeof(T).FullName, nameof(Value)); if (Value == null) throw new MissingMemberException(typeof(T).FullName, nameof(Value));
if (!(Value is IComparable)) throw new NotSupportedException(); if (!(Value is IComparable)) throw new NotSupportedException();
@ -86,7 +86,7 @@ public class Class<T> : IComparable, IComparable<T>, IComparable<Class<T>>
/// <summary></summary> /// <summary></summary>
/// <exception cref="MissingMemberException"></exception> /// <exception cref="MissingMemberException"></exception>
/// <exception cref="NotSupportedException"></exception> /// <exception cref="NotSupportedException"></exception>
public int CompareTo(Class<T> other) public virtual int CompareTo(Class<T> other)
{ {
if (Value == null) throw new MissingMemberException(typeof(T).FullName, nameof(Value)); if (Value == null) throw new MissingMemberException(typeof(T).FullName, nameof(Value));
if (other == null || !other.HasValue) return 1; if (other == null || !other.HasValue) return 1;

7
Apewer/_Extensions.cs

@ -482,7 +482,7 @@ public static class Extensions
#region Source #region Source
/// <summary>为记录的 Key 属性设置新值。</summary> /// <summary>为记录的 Key 属性设置新值。</summary>
public static void SetNewKey(this Record @this) => Record.SetNewKey(@this); public static void ResetKey(this Record @this) => Record.ResetKey(@this);
/// <summary>修补基本属性。</summary> /// <summary>修补基本属性。</summary>
public static void FixProperties(this Record @this) => Record.FixProperties(@this); public static void FixProperties(this Record @this) => Record.FixProperties(@this);
@ -534,7 +534,7 @@ public static class Extensions
public static void Error(this ApiResponse @this, Exception exception) => ApiInternals.RespondError(@this, exception); public static void Error(this ApiResponse @this, Exception exception) => ApiInternals.RespondError(@this, exception);
/// <summary>输出 UTF-8 文本。</summary> /// <summary>输出 UTF-8 文本。</summary>
public static void Text(this ApiResponse @this, string content, string type = "text/plain") => ApiInternals.RespondText(@this, content, type); public static void Text(this ApiResponse @this, string content, string type = "text/plain; charset=utf-8") => ApiInternals.RespondText(@this, content, type);
/// <summary>输出字节数组。</summary> /// <summary>输出字节数组。</summary>
public static void Binary(this ApiResponse @this, byte[] content, string type = "application/octet-stream") => ApiInternals.RespondBinary(@this, content, type); public static void Binary(this ApiResponse @this, byte[] content, string type = "application/octet-stream") => ApiInternals.RespondBinary(@this, content, type);
@ -571,6 +571,9 @@ public static class Extensions
/// <summary>设置响应,当发生错误时设置响应。返回错误信息。</summary> /// <summary>设置响应,当发生错误时设置响应。返回错误信息。</summary>
public static string Respond(this ApiResponse @this, Record record, bool lower = true) => WebUtility.Respond(@this, record, lower); public static string Respond(this ApiResponse @this, Record record, bool lower = true) => WebUtility.Respond(@this, record, lower);
/// <summary>设置响应,当发生错误时设置响应。返回错误信息。</summary>
public static string Respond(this ApiResponse @this, Json data, bool lower = true) => WebUtility.Respond(@this, data, lower);
#endif #endif
#endregion #endregion

Loading…
Cancel
Save