From 95b73314ebb64d464f8c17f0697c1cf1a2b8340a Mon Sep 17 00:00:00 2001 From: Elivo Date: Thu, 24 Oct 2024 14:52:54 +0800 Subject: [PATCH] Apewer-6.8.0 --- Apewer.Web/AspNetBridge/BridgeController.cs | 1 + Apewer.Web/Web/AspNetCoreProvider.cs | 18 +- Apewer.Windows/Internals/Interop/Constant.cs | 4 + Apewer.Windows/Internals/Interop/Kernel32.cs | 16 +- Apewer.Windows/WindowsUtility.cs | 157 +++++- Apewer/Apewer.props | 2 +- Apewer/Class.cs | 81 +-- Apewer/ClockUtility.cs | 177 ++++--- Apewer/CollectionUtility.cs | 67 ++- Apewer/DateTimePart.cs | 35 ++ Apewer/Json.cs | 224 ++++---- Apewer/Network/HttpBody.cs | 52 ++ Apewer/Network/HttpHeader.cs | 47 ++ Apewer/Network/HttpHeaders.cs | 369 +++++++++++++ Apewer/Network/HttpMethod.cs | 18 +- Apewer/RuntimeUtility.cs | 28 +- Apewer/Web/ActionResult.cs | 24 + Apewer/Web/ApiAction.cs | 240 +++++++++ Apewer/Web/ApiActionJsonFormat.cs | 33 ++ Apewer/Web/ApiApplication.cs | 177 ++++++- Apewer/Web/ApiCatch.cs | 15 +- Apewer/Web/ApiContext.cs | 8 +- Apewer/Web/ApiEntries.cs | 333 +++++------- Apewer/Web/ApiEntry.cs | 200 -------- Apewer/Web/ApiFunction.cs | 170 ++++-- Apewer/Web/ApiInvoker.cs | 16 +- Apewer/Web/ApiMiddleware.cs | 18 - Apewer/Web/ApiModel.cs | 233 +++++---- Apewer/Web/ApiOptions.cs | 16 + Apewer/Web/ApiParameter.cs | 77 +++ Apewer/Web/ApiProcessor.cs | 512 +++++++++++-------- Apewer/Web/ApiProvider.cs | 11 +- Apewer/Web/ApiRequest.cs | 5 +- Apewer/Web/ApiResponse.cs | 13 +- Apewer/Web/ApiServiceDescriptor.cs | 92 ---- Apewer/Web/ApiServiceLifetime.cs | 23 - Apewer/Web/ApiUtility.cs | 169 +++--- Apewer/Web/BytesResult.cs | 65 +++ Apewer/Web/CacheControl.cs | 44 ++ Apewer/Web/FromBodyAttribute.cs | 12 + Apewer/Web/FromFormAttribute.cs | 12 + Apewer/Web/FromHeaderAttribute.cs | 12 + Apewer/Web/FromQueryAttribute.cs | 12 + Apewer/Web/HeadResult.cs | 92 ++++ Apewer/Web/HttpConnectAttribute.cs | 12 + Apewer/Web/HttpDeleteAttribute.cs | 12 + Apewer/Web/HttpGetAttribute.cs | 12 + Apewer/Web/HttpHeadAttribute.cs | 12 + Apewer/Web/HttpListenerProvider.cs | 10 +- Apewer/Web/HttpOptionsAttribute.cs | 12 + Apewer/Web/HttpPatchAttribute.cs | 12 + Apewer/Web/HttpPostAttribute.cs | 12 + Apewer/Web/HttpPutAttribute.cs | 12 + Apewer/Web/HttpTraceAttribute.cs | 12 + Apewer/Web/IActionResult.cs | 17 + Apewer/Web/IApiModel.cs | 17 + Apewer/Web/IHttpActionResult.cs | 11 + Apewer/Web/MiniConnection.cs | 8 +- Apewer/Web/MiniProvider.cs | 11 +- Apewer/Web/MiniRequest.cs | 9 +- Apewer/Web/RouteAttribute.cs | 29 ++ Apewer/Web/RoutePrefixAttribute.cs | 24 + Apewer/Web/StreamResult.cs | 120 +++++ Apewer/Web/TextResult.cs | 20 + Apewer/Web/WebsiteProvider.cs | 8 +- Apewer/Web/_Attributes.cs | 95 ---- Apewer/_Extensions.cs | 35 +- ChangeLog.md | 7 + 68 files changed, 3049 insertions(+), 1410 deletions(-) create mode 100644 Apewer/DateTimePart.cs create mode 100644 Apewer/Network/HttpBody.cs create mode 100644 Apewer/Network/HttpHeader.cs create mode 100644 Apewer/Network/HttpHeaders.cs create mode 100644 Apewer/Web/ActionResult.cs create mode 100644 Apewer/Web/ApiAction.cs create mode 100644 Apewer/Web/ApiActionJsonFormat.cs delete mode 100644 Apewer/Web/ApiEntry.cs delete mode 100644 Apewer/Web/ApiMiddleware.cs create mode 100644 Apewer/Web/ApiParameter.cs delete mode 100644 Apewer/Web/ApiServiceDescriptor.cs delete mode 100644 Apewer/Web/ApiServiceLifetime.cs create mode 100644 Apewer/Web/BytesResult.cs create mode 100644 Apewer/Web/CacheControl.cs create mode 100644 Apewer/Web/FromBodyAttribute.cs create mode 100644 Apewer/Web/FromFormAttribute.cs create mode 100644 Apewer/Web/FromHeaderAttribute.cs create mode 100644 Apewer/Web/FromQueryAttribute.cs create mode 100644 Apewer/Web/HeadResult.cs create mode 100644 Apewer/Web/HttpConnectAttribute.cs create mode 100644 Apewer/Web/HttpDeleteAttribute.cs create mode 100644 Apewer/Web/HttpGetAttribute.cs create mode 100644 Apewer/Web/HttpHeadAttribute.cs create mode 100644 Apewer/Web/HttpOptionsAttribute.cs create mode 100644 Apewer/Web/HttpPatchAttribute.cs create mode 100644 Apewer/Web/HttpPostAttribute.cs create mode 100644 Apewer/Web/HttpPutAttribute.cs create mode 100644 Apewer/Web/HttpTraceAttribute.cs create mode 100644 Apewer/Web/IActionResult.cs create mode 100644 Apewer/Web/IApiModel.cs create mode 100644 Apewer/Web/IHttpActionResult.cs create mode 100644 Apewer/Web/RouteAttribute.cs create mode 100644 Apewer/Web/RoutePrefixAttribute.cs create mode 100644 Apewer/Web/StreamResult.cs create mode 100644 Apewer/Web/TextResult.cs delete mode 100644 Apewer/Web/_Attributes.cs diff --git a/Apewer.Web/AspNetBridge/BridgeController.cs b/Apewer.Web/AspNetBridge/BridgeController.cs index 47ffa21..5370bb8 100644 --- a/Apewer.Web/AspNetBridge/BridgeController.cs +++ b/Apewer.Web/AspNetBridge/BridgeController.cs @@ -157,6 +157,7 @@ namespace Apewer.AspNetBridge } // 准备参数。 + var pis = CollectionUtility.Vacuum(route.Parameters.Map(x => ApiParameter.Parse(x))); var ps = ReadParameters(Request, route.Parameters); // 准备控制器。 diff --git a/Apewer.Web/Web/AspNetCoreProvider.cs b/Apewer.Web/Web/AspNetCoreProvider.cs index 99157b7..ae77ab8 100644 --- a/Apewer.Web/Web/AspNetCoreProvider.cs +++ b/Apewer.Web/Web/AspNetCoreProvider.cs @@ -1,5 +1,6 @@ #if NETCORE +using Apewer.Network; using Microsoft.AspNetCore.Http; using System; using System.Collections.Generic; @@ -10,13 +11,16 @@ namespace Apewer.Web { /// 用于网站的服务程序。 - public class AspNetCoreProvider : ApiProvider + public class AspNetCoreProvider : ApiProvider { private HttpContext context; private HttpRequest request; private HttpResponse response; + /// HttpContext + public override HttpContext Context { get => context; } + /// 创建服务程序实例。 /// public AspNetCoreProvider(HttpContext context) @@ -39,7 +43,7 @@ namespace Apewer.Web public override Uri GetUrl() { var https = request.IsHttps; - var port = context.Connection.LocalPort; + var port = Context.Connection.LocalPort; var query = request.QueryString == null ? null : request.QueryString.Value; var sb = new StringBuilder(); @@ -66,11 +70,11 @@ namespace Apewer.Web public override string GetReferrer() => null; /// 获取请求的头。 - public override StringPairs GetHeaders() + public override HttpHeaders GetHeaders() { var headers = request.Headers; - var sp = new StringPairs(); - if (headers == null) return sp; + var result = new HttpHeaders(); + if (headers == null) return result; foreach (var key in headers.Keys) { if (string.IsNullOrEmpty(key)) continue; @@ -78,11 +82,11 @@ namespace Apewer.Web { var value = headers[key]; if (string.IsNullOrEmpty(value)) continue; - sp.Add(key, value); + result.Add(key, value); } catch { } } - return sp; + return result; } /// 获取请求的内容类型。 diff --git a/Apewer.Windows/Internals/Interop/Constant.cs b/Apewer.Windows/Internals/Interop/Constant.cs index 476298e..8875856 100644 --- a/Apewer.Windows/Internals/Interop/Constant.cs +++ b/Apewer.Windows/Internals/Interop/Constant.cs @@ -89,6 +89,10 @@ namespace Apewer.Internals.Interop /// public const int PROCESS_QUERY_INFORMATION = 0x400; + public const int PROCESS_VM_READ = 0x0010; + + public const int PROCESS_VM_WRITE = 0x0020; + /// public const int SC_MOVE = 0xF010; diff --git a/Apewer.Windows/Internals/Interop/Kernel32.cs b/Apewer.Windows/Internals/Interop/Kernel32.cs index 3b2ad7b..ea776b6 100644 --- a/Apewer.Windows/Internals/Interop/Kernel32.cs +++ b/Apewer.Windows/Internals/Interop/Kernel32.cs @@ -13,7 +13,7 @@ namespace Apewer.Internals.Interop public static extern int CloseHandle(int hObject); [DllImport("kernel32.dll", SetLastError = true)] - [return: MarshalAs(UnmanagedType.Bool)] + // [return: MarshalAs(UnmanagedType.Bool)] public static extern bool CloseHandle(IntPtr hObject); [DllImport("kernel32.dll")] @@ -64,11 +64,12 @@ namespace Apewer.Internals.Interop [DllImport("kernel32.dll", CharSet = CharSet.Auto)] public static extern int OpenProcess(int dwDesiredAccess, int bInheritHandle, int dwProcessId); - [DllImport("kernel32.dll", CharSet = CharSet.Auto)] - public static extern int OpenProcess(int dwDesiredAccess, bool bInheritHandle, int dwProcessId); + [DllImport("kernel32.dll", EntryPoint = "OpenProcess")] + public static extern IntPtr OpenProcess(int dwDesiredAccess, bool bInheritHandle, int dwProcessId); - [DllImport("kernel32.dll")] - public static extern bool ReadProcessMemory(IntPtr hProcess, IntPtr lpBaseAddress, IntPtr lpBuffer, int nSize, IntPtr lpNumberOfBytesRead); + // BOOL ReadProcessMemory([in] HANDLE hProcess, [in] LPCVOID lpBaseAddress, [out] LPVOID lpBuffer, [in] SIZE_T nSize, [out] SIZE_T *lpNumberOfBytesRead); + [DllImport("kernel32.dll ")] + public static extern bool ReadProcessMemory(IntPtr hProcess, int lpBaseAddress, byte[] lpBuffer, int nSize, out int lpNumberOfBytesRead); /// Copies the contents of a source memory block to a destination memory block, and supports overlapping source and destination memory blocks. /// A pointer to the destination memory block to copy the bytes to. @@ -84,8 +85,9 @@ namespace Apewer.Internals.Interop [DllImport("kernel32.dll")] public static extern bool UnmapViewOfFile(IntPtr lpBaseAddress); - [DllImportAttribute("kernel32.dll")] - public static extern bool WriteProcessMemory(IntPtr hProcess, IntPtr lpBaseAddress, int[] lpBuffer, int nSize, IntPtr lpNumberOfBytesWritten); + // BOOL WriteProcessMemory([in] HANDLE hProcess, [in] LPVOID lpBaseAddress, [in] LPCVOID lpBuffer, [in] SIZE_T nSize, [out] SIZE_T *lpNumberOfBytesWritten); + [DllImport("kernel32.dll")] + public static extern bool WriteProcessMemory(IntPtr hProcess, IntPtr lpBaseAddress, byte[] lpBuffer, int nSize, out int lpNumberOfBytesWritten); } diff --git a/Apewer.Windows/WindowsUtility.cs b/Apewer.Windows/WindowsUtility.cs index 52833e2..b0bf8bf 100644 --- a/Apewer.Windows/WindowsUtility.cs +++ b/Apewer.Windows/WindowsUtility.cs @@ -25,9 +25,16 @@ namespace Apewer { /// Windows 实用工具。 - public class WindowsUtility + public static class WindowsUtility { + #region 句柄 + + /// 关闭句柄。 + public static bool CloseHandle(IntPtr handle) => Kernel32.CloseHandle(handle); + + #endregion + #region 进程。 #if NETFX @@ -238,45 +245,143 @@ namespace Apewer return 0; } - /// 读取内存中的值。 - /// 进程 ID。 - /// 地址。 - /// 抛出发生的异常。 - public static int ReadMemoryInt32(int address, int pid, bool @throw = true) + /// 打开现有的本地进程对象。 + /// 要打开的本地进程的标识符。 + /// 指定进程的打开句柄。 + /// + /// + public static IntPtr OpenProcess(int processId) + { + if (processId == 0) throw new ArgumentNullException(nameof(processId)); + var handle = Kernel32.OpenProcess(PROCESS_VM_READ | PROCESS_VM_WRITE, false, processId); + if (handle == IntPtr.Zero) throw new SystemException($"打开进程 {processId} 失败。"); + return handle; + } + + /// 打开现有的本地进程对象。 + /// 要打开的本地进程的标识符。 + /// 使用句柄。 + /// 指定进程的打开句柄。 + /// + /// + public static void OpenProcess(int processId, Action callback) { + if (processId == 0) throw new ArgumentNullException(nameof(processId)); + if (callback == null) throw new ArgumentNullException(nameof(callback)); + + var handle = IntPtr.Zero; try { - var buffer = new byte[4]; - var pinned = Marshal.UnsafeAddrOfPinnedArrayElement(buffer, 0); - var process = new IntPtr(OpenProcess(0x1F0FFF, false, pid)); - ReadProcessMemory(process, (IntPtr)address, pinned, 4, IntPtr.Zero); - CloseHandle(process); - return Marshal.ReadInt32(pinned); + handle = OpenProcess(processId); + callback.Invoke(handle); } - catch (Exception ex) + finally { - if (@throw) throw ex; - return 0; + if (handle != IntPtr.Zero) CloseHandle(handle); } } - /// 将值写入指定内存地址中。 - /// 地址。 - /// 进程 ID。 - /// Int32 值。 - /// 抛出发生的异常。 - public static void WriteMemoryInt32(int address, int pid, int value, bool @throw = true) + /// 打开现有的本地进程对象。 + /// 要打开的本地进程的标识符。 + /// 使用句柄。 + /// 指定进程的打开句柄。 + /// + /// + public static T OpenProcess(int processId, Func callback) { + if (processId == 0) throw new ArgumentNullException(nameof(processId)); + if (callback == null) throw new ArgumentNullException(nameof(callback)); + + var handle = IntPtr.Zero; try { - var hProcess = OpenProcess(0x1F0FFF, false, pid); // 0x1F0FFF 最高权限 - WriteProcessMemory(new IntPtr(hProcess), (IntPtr)address, new[] { value }, 4, IntPtr.Zero); - CloseHandle(hProcess); + handle = OpenProcess(processId); + return callback.Invoke(handle); } - catch (Exception ex) + finally + { + if (handle != IntPtr.Zero) CloseHandle(handle); + } + } + + /// 读取指定进程的内存。 + /// 进程。 + /// 要读取的内存地址。 + /// 要读取的字节数。 + /// 读取到的数据。 + /// + /// + /// + public static byte[] ReadMemory(this Process process, IntPtr address, int length) + { + if (process == null) throw new ArgumentNullException(nameof(process)); + if (address == IntPtr.Zero) throw new ArgumentNullException(nameof(address)); + if (length < 1) throw new ArgumentNullException(nameof(length)); + + return OpenProcess(process.Id, (processHandle) => ReadMemory(processHandle, address, length)); + } + + /// 读取指定进程的内存。 + /// 进程句柄。 + /// 要读取的内存地址。 + /// 要读取的字节数。 + /// 读取到的数据。 + /// + /// + /// + public static byte[] ReadMemory(IntPtr processHandle, IntPtr address, int length) + { + if (processHandle == IntPtr.Zero) throw new ArgumentNullException(nameof(processHandle)); + if (address == IntPtr.Zero) throw new ArgumentNullException(nameof(address)); + if (length < 1) throw new ArgumentOutOfRangeException(nameof(length)); ; + + var buffer = new byte[length]; + if (ReadProcessMemory(processHandle, address.ToInt32(), buffer, length, out var read)) { - if (@throw) throw ex; + if (read == length) return buffer; + + // 收缩数组 + var array = new byte[read]; + if (read > 0) Buffer.BlockCopy(buffer, 0, array, 0, read); + return array; } + + throw new SystemException($"从内存地址读取数据失败。"); + } + + /// 在进程的指定地址写入数据。 + /// 进程。 + /// 要读取的内存地址。 + /// 要写入的数据。 + /// 写入的字节数。 + /// + /// + public static int WriteMemory(this Process process, IntPtr address, byte[] data) + { + if (process == null) throw new ArgumentNullException(nameof(process)); + if (address == IntPtr.Zero) throw new ArgumentNullException(nameof(address)); + if (data == null) throw new ArgumentNullException(nameof(data)); + if (data.Length < 1) return 0; + + return OpenProcess(process.Id, processHandle => WriteMemory(processHandle, address, data)); + } + + /// 在进程的指定地址写入数据。 + /// 进程句柄。 + /// 要读取的内存地址。 + /// 要写入的数据。 + /// 写入的字节数。 + /// + /// + public static int WriteMemory(IntPtr processHandle, IntPtr address, byte[] data) + { + if (processHandle == IntPtr.Zero) throw new ArgumentNullException(nameof(processHandle)); + if (address == IntPtr.Zero) throw new ArgumentNullException(nameof(address)); + if (data == null) throw new ArgumentNullException(nameof(data)); + if (data.Length < 1) return 0; + + if (WriteProcessMemory(processHandle, address, data, data.Length, out var written)) return written; + throw new SystemException($"向内存地址写入数据失败。"); } #endregion diff --git a/Apewer/Apewer.props b/Apewer/Apewer.props index 67efd36..f805432 100644 --- a/Apewer/Apewer.props +++ b/Apewer/Apewer.props @@ -9,7 +9,7 @@ Apewer Apewer Libraries - 6.7.6 + 6.8.0 diff --git a/Apewer/Class.cs b/Apewer/Class.cs index b3ea296..7e29505 100644 --- a/Apewer/Class.cs +++ b/Apewer/Class.cs @@ -4,73 +4,88 @@ namespace Apewer { /// 装箱类。 - public sealed class Class : IComparable, IComparable, IComparable> + public sealed class Class // : IComparable, IComparable, IComparable> { - private bool _hashcode = false; - private bool _equals = false; - - /// 装箱对象。 + /// 值。 public T Value { get; set; } - /// 创建默认值。 - public Class(T value = default, bool tryEquals = true, bool tryHashCode = true) - { - Value = value; - _hashcode = tryHashCode; - _equals = tryEquals; - } + /// 创建装箱实例,值为默认值。 + public Class() { } + + /// 创建装箱实例,值为指定值。 + public Class(T value) => Value = value; #region Override /// public override int GetHashCode() { - if (_hashcode && Value != null) - { - return Value.GetHashCode(); - } + if (Value != null) return Value.GetHashCode(); return base.GetHashCode(); } /// public override bool Equals(object obj) { - if (_equals) + if (ReferenceEquals(this, obj)) return true; + + var right = obj as Class; + if (Value.IsNull()) + { + if (right == null) return true; + if (right.Value.IsNull()) return true; + return false; + } + else { - var right = obj as Class; if (right == null) return false; - - if (Value == null && right.Value == null) return true; - if (Value == null && right.Value != null) return false; - if (Value != null && right.Value == null) return false; + if (right.Value.IsNull()) return false; + if (ReferenceEquals(Value, right.Value)) return true; return Value.Equals(right.Value); } - return base.Equals(obj); } /// public override string ToString() { - if (Value == null) return ""; + if (Value == null) return null; return Value.ToString(); } #endregion - #region IComparable +#if ClassCompare + + #region Compare /// - /// /// public int CompareTo(object obj) { - if (obj != null && obj is T) return CompareTo((T)obj); - if (obj != null && obj is Class) return CompareTo(obj as Class); + if (typeof(IComparable).IsAssignableFrom(typeof(T))) + { + var comparable = Value as IComparable; + if (comparable.IsNull()) + { + if (obj is Class bro) + { + if (bro == null) return 0; + if (bro.Value.IsNull()) return 0; + return -1; + } + + if (obj.IsNull()) return 0; + return -1; + } + else + { + if (obj != null && obj is Class bro) return comparable.CompareTo(bro.Value); + return comparable.CompareTo(obj); + } + } - if (Value == null) throw new MissingMemberException(typeof(T).FullName, nameof(Value)); - if (!(Value is IComparable)) throw new NotSupportedException(); - return ((IComparable)Value).CompareTo(obj); + throw new NotImplementedException($"类型 {typeof(T).Name} 没有实现 {nameof(IComparable)} 接口。"); } /// @@ -98,6 +113,8 @@ namespace Apewer #endregion +#endif + #region 运算符。 /// 到 Boolean 的隐式转换,判断 包含值。 @@ -112,7 +129,7 @@ namespace Apewer var text = instance as Class; if (text != null) return !string.IsNullOrEmpty(text.Value); - return instance.NotNull(); + return instance != null; } /// 到 T 的隐式转换。 diff --git a/Apewer/ClockUtility.cs b/Apewer/ClockUtility.cs index e4fb138..a3b4a4a 100644 --- a/Apewer/ClockUtility.cs +++ b/Apewer/ClockUtility.cs @@ -1,7 +1,4 @@ -using Apewer.Internals; -using System; -using System.Collections.Generic; -using System.Diagnostics; +using System; using System.Globalization; using System.Text; @@ -22,11 +19,10 @@ namespace Apewer if (value is DateTime dt) return dt; if (value.IsNull()) return null; - DateTime result; try { var text = value.ToString(); - var parsed = System.DateTime.TryParse(text, out result); + var parsed = System.DateTime.TryParse(text, out var result); return parsed ? new Class(result) : null; } catch @@ -46,9 +42,6 @@ namespace Apewer /// 创建新的零值 DateTime 对象。 public static DateTime Zero { get => _zero; } - /// 获取一个 DateTime 对象,该对象设置为 1970-01-01 00:00:00.000,表示为本地时间。 - public static DateTime Origin { get => _origin; } - /// 获取一个 DateTime 对象,该对象设置为 1970-01-01 00:00:00.000,表示为协调通用时间 (UTC)。 public static DateTime UtcOrigin { get => _utc_origin; } @@ -76,6 +69,26 @@ namespace Apewer #endregion + #region Round + + /// 对齐时间,小于 的部分将被舍弃。 + public static DateTime Round(this DateTime dateTime, DateTimePart datePart) + { + switch (datePart) + { + case DateTimePart.Year: return new DateTime(dateTime.Year, 1, 1, 0, 0, 0, 0, dateTime.Kind); + case DateTimePart.Month: return new DateTime(dateTime.Year, dateTime.Month, 1, 0, 0, 0, 0, dateTime.Kind); + case DateTimePart.Day: return new DateTime(dateTime.Year, dateTime.Month, dateTime.Day, 0, 0, 0, 0, dateTime.Kind); + case DateTimePart.Hour: return new DateTime(dateTime.Year, dateTime.Month, dateTime.Day, dateTime.Hour, 0, 0, 0, dateTime.Kind); + case DateTimePart.Minute: return new DateTime(dateTime.Year, dateTime.Month, dateTime.Day, dateTime.Hour, dateTime.Minute, 0, 0, dateTime.Kind); + case DateTimePart.Second: return new DateTime(dateTime.Year, dateTime.Month, dateTime.Day, dateTime.Hour, dateTime.Minute, dateTime.Second, 0, dateTime.Kind); + case DateTimePart.Millisecond: return dateTime; + default: throw new ArgumentException($"指定的 {nameof(DateTimePart)} 不受支持。"); + } + } + + #endregion + #region Common /// 判断指定年份是闰年。 @@ -88,9 +101,10 @@ namespace Apewer } /// 判断指定年份是闰年。 - public static bool IsLeapYear(DateTime datetime) => IsLeapYear(SafeDateTime(datetime).Year); + public static bool IsLeapYear(DateTime dateTime) => IsLeapYear(dateTime.Year); /// 获取指定年月的天数。 + /// public static int MonthDays(int year, int month) { switch (month) @@ -98,41 +112,7 @@ namespace Apewer case 1: case 3: case 5: case 7: case 8: case 10: case 12: return 31; case 4: case 6: case 9: case 11: return 30; case 2: return IsLeapYear(year) ? 29 : 28; - default: return 0; - } - } - - /// 尝试获取安全的 DateTime 对象。 - public static DateTime SafeDateTime(object datetime) - { - if (datetime is DateTime) - { - var span = (DateTime)datetime - Origin; - return Origin.AddMilliseconds(span.TotalMilliseconds); - } - else - { - if (datetime == null) return Zero; - var type = datetime.GetType(); - if (type.Equals(typeof(string))) - { - var s = datetime as string; - if (string.IsNullOrEmpty(s)) return Zero; - DateTime value; - var success = TryParse(s, out value); - return success ? value : Zero; - } - else - { - if (datetime == null) return Zero; - try - { - var s = datetime.ToString(); - if (string.IsNullOrEmpty(s)) return Zero; - return SafeDateTime(s); - } - catch { return Zero; } - } + default: throw new ArgumentOutOfRangeException(nameof(month)); } } @@ -140,35 +120,92 @@ namespace Apewer #region Stamp + /// 自定义从 DateTime 转为时间戳的方法。 + public static Func CustomToStamp { get; set; } + + /// 自定义从时间戳转为 DateTime 的方法。 + public static Func CustomFromStamp { get; set; } + /// 获取当前本地时间的毫秒时间戳。 - public static long NowStamp { get => Stamp(Now); } + public static long NowStamp { get => ToStamp(Now); } /// 获取当前 UTC 的毫秒时间戳。 - public static long UtcStamp { get => Stamp(UtcNow); } + public static long UtcStamp { get => ToStamp(UtcNow); } - /// 获取毫秒时间戳。 - public static long Stamp(DateTime datetime, bool byMilliseconds = true) + /// 获取毫秒时间戳。当指定了 时将优先使用自定义的方法。 + /// 默认不判断参数的时区,与 相同。 + public static long ToStamp(DateTime dateTime) { - var span = datetime - Origin; - var value = byMilliseconds ? span.TotalMilliseconds : span.TotalSeconds; - var stamp = Convert.ToInt64(value); + var converter = CustomToStamp; + if (converter != null) return converter.Invoke(dateTime); + + var span = dateTime - _origin; + var value = span.TotalMilliseconds; + var stamp = Convert.ToInt64(Math.Floor(value)); return stamp; } - /// 从毫秒时间戳获取 DateTime 对象。发生异常且不允许异常时将返回 1970-01-01 00:00:00.000。 + /// 获取 UTC 毫秒时间戳 + public static long ToUtcStamp(DateTime dateTime) + { + if (dateTime.Kind == DateTimeKind.Local) dateTime = dateTime.ToUniversalTime(); + + var span = dateTime - _utc_origin; + var value = span.TotalMilliseconds; + var stamp = Convert.ToInt64(Math.Floor(value)); + return stamp; + } + + /// 解析毫秒时间戳,获取 DateTime 对象。当指定了 时将优先使用自定义的方法。 + /// 默认不判断系统时区,返回的结果是 /// - public static DateTime FromStamp(long stamp, DateTimeKind kind = DateTimeKind.Unspecified, bool throwException = true) + public static DateTime FromStamp(long stamp) { - try - { - var origin = NewOrigin(kind); - var datetime = origin.AddMilliseconds(Convert.ToDouble(stamp)); - return datetime; - } - catch + var converter = CustomFromStamp; + if (converter != null) return converter.Invoke(stamp); + return FromStamp(stamp, DateTimeKind.Unspecified, DateTimeKind.Unspecified); + } + + /// 从毫秒时间戳获取 DateTime 对象。 + /// + public static DateTime FromStamp(long stamp, DateTimeKind stampKind, DateTimeKind dateTimeKind) + { + switch (dateTimeKind) { - if (throwException) throw new ArgumentOutOfRangeException(); - return Origin; + case DateTimeKind.Unspecified: + switch (stampKind) + { + case DateTimeKind.Unspecified: + case DateTimeKind.Utc: + case DateTimeKind.Local: + return _origin.AddMilliseconds(stamp); + default: + throw new ArgumentOutOfRangeException(nameof(stampKind)); + } + case DateTimeKind.Utc: + switch (stampKind) + { + case DateTimeKind.Unspecified: + case DateTimeKind.Utc: + return _utc_origin.AddMilliseconds(stamp); + case DateTimeKind.Local: + return NewOrigin(DateTimeKind.Local).AddMilliseconds(stamp).ToUniversalTime(); + default: + throw new ArgumentOutOfRangeException(nameof(stampKind)); + } + case DateTimeKind.Local: + switch (stampKind) + { + case DateTimeKind.Unspecified: + case DateTimeKind.Utc: + return _utc_origin.AddMilliseconds(stamp).ToLocalTime(); + case DateTimeKind.Local: + return NewOrigin(DateTimeKind.Local).AddMilliseconds(stamp); + default: + throw new ArgumentOutOfRangeException(nameof(stampKind)); + } + default: + throw new ArgumentOutOfRangeException(nameof(dateTimeKind)); } } @@ -229,29 +266,27 @@ namespace Apewer public static string CompactDate { get { return Compact(Now, true, false, false, false); } } /// 转换 DateTime 对象到易于阅读的文本。 - public static string Lucid(DateTime datetime, bool date = true, bool time = true, bool seconds = true, bool milliseconds = true) + public static string Lucid(DateTime dateTime, bool date = true, bool time = true, bool seconds = true, bool milliseconds = true) { - var safe = SafeDateTime(datetime); var sb = new StringBuilder(); - if (date) sb.Append(FormatDate(safe, true)); + if (date) sb.Append(FormatDate(dateTime, true)); if (time) { if (date) sb.Append(" "); - sb.Append(FormatTime(safe, true, seconds, milliseconds)); + sb.Append(FormatTime(dateTime, true, seconds, milliseconds)); } var lucid = sb.ToString(); return lucid; } /// 转换 DateTime 对象到紧凑的文本。 - public static string Compact(DateTime datetime, bool date = true, bool time = true, bool seconds = true, bool milliseconds = true) + public static string Compact(DateTime dateTime, bool date = true, bool time = true, bool seconds = true, bool milliseconds = true) { - var safe = SafeDateTime(datetime); var sb = new StringBuilder(); - if (date) sb.Append(FormatDate(safe, false)); + if (date) sb.Append(FormatDate(dateTime, false)); if (time) { - sb.Append(FormatTime(safe, false, seconds, milliseconds)); + sb.Append(FormatTime(dateTime, false, seconds, milliseconds)); } var lucid = sb.ToString(); return lucid; diff --git a/Apewer/CollectionUtility.cs b/Apewer/CollectionUtility.cs index 5d5ec57..4daff30 100644 --- a/Apewer/CollectionUtility.cs +++ b/Apewer/CollectionUtility.cs @@ -311,7 +311,7 @@ namespace Apewer /// 对元素去重,且去除 NULL 值。 public static T[] Distinct(IEnumerable items) { - if (items == null) throw new ArgumentNullException(nameof(items)); + if (items != null) throw new ArgumentNullException(nameof(items)); var count = Count(items); var added = 0; var array = new T[count]; @@ -346,6 +346,9 @@ namespace Apewer return array; } + /// 清理集合,去除 NULL 值。 + public static T[] Vacuum(this IEnumerable items) => FindAll(items, x => x != null); + /// 获取可枚举集合的部分元素。 /// 集合元素的类型。 /// 原集合。 @@ -650,10 +653,68 @@ namespace Apewer #region Find /// 根据条件筛选,将符合条件的元素组成新数组。 - public static T[] FindAll(this T[] array, Predicate match) => System.Array.FindAll(array, match); + public static T[] FindAll(this IEnumerable collection, Predicate match) + { + if (collection == null) new ArgumentNullException(nameof(collection)); + if (match == null) throw new ArgumentNullException(nameof(match)); + + if (collection is T[] array) + { + return System.Array.FindAll(array, match); + } + else if (collection is IList list) + { + var count = list.Count; + var result = new List(count); + for (var i = 0; i < count; i++) + { + var matched = match.Invoke(list[i]); + if (matched) result.Add(list[i]); + } + return result.ToArray(); + } + else + { + var result = new List(); + foreach (var item in collection) + { + var matched = match.Invoke(item); + if (matched) result.Add(item); + } + return result.ToArray(); + } + } /// 根据条件筛选,找到第一个符合条件的元素。 - public static T Find(this T[] array, Predicate match) => System.Array.Find(array, match); + public static T Find(this IEnumerable collection, Predicate match) + { + if (collection == null) throw new ArgumentNullException(nameof(collection)); + if (match == null) throw new ArgumentNullException(nameof(match)); + + if (collection is T[] array) + { + return System.Array.Find(array, match); + } + else if (collection is IList list) + { + var count = list.Count; + for (var i = 0; i < count; i++) + { + var matched = match.Invoke(list[i]); + if (matched) return list[i]; + } + } + else + { + foreach (var item in collection) + { + var matched = match.Invoke(item); + if (matched) return item; + } + } + + return default; + } #endregion diff --git a/Apewer/DateTimePart.cs b/Apewer/DateTimePart.cs new file mode 100644 index 0000000..0239ad7 --- /dev/null +++ b/Apewer/DateTimePart.cs @@ -0,0 +1,35 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Apewer +{ + + /// 表示 DateTime 的部分。 + public enum DateTimePart + { + + /// + Year, + + /// + Month, + + /// + Day, + + /// + Hour, + + /// + Minute, + + /// + Second, + + /// + Millisecond + + } + +} diff --git a/Apewer/Json.cs b/Apewer/Json.cs index ab66893..59299c9 100644 --- a/Apewer/Json.cs +++ b/Apewer/Json.cs @@ -1,6 +1,4 @@ -using Apewer; -using Apewer.Internals; -using Newtonsoft.Json; +using Newtonsoft.Json; using Newtonsoft.Json.Linq; using System; using System.Collections; @@ -11,8 +9,6 @@ using System.Dynamic; using System.IO; #endif using System.Reflection; -using System.Runtime.Serialization; -using System.Text; using static Apewer.NumberUtility; using static Apewer.RuntimeUtility; @@ -1739,35 +1735,80 @@ namespace Apewer if (json._jtoken == null) return null; if (json.TokenType != JTokenType.Object) return null; - var entity = Activator.CreateInstance(typeof(T)); - Object(entity, json, ignoreCase, ignoreCharacters, force); - return (T)entity; + var entity = Object(typeof(T), json, ignoreCase, ignoreCharacters, force); + return entity == null ? default : (T)entity; } /// 将 Json 填充到数组列表,失败时返回 NULL 值。 - internal static T[] Array(Json json, bool ignoreCase = true, string ignoreCharacters = null, bool force = false) + internal static TItem[] Array(Json json, bool ignoreCase = true, string ignoreCharacters = null, bool force = false) { if (json == null) return null; if (json._jtoken == null) return null; if (json.TokenType != JTokenType.Array) return null; - var list = new List(); - Array(list, json, ignoreCase, ignoreCharacters, force); - return list.ToArray(); + var instance = Array(typeof(TItem), json, ignoreCase, ignoreCharacters, force); + var array = (TItem[])instance; + return array; } - /// - public static void Object(object entity, Json json, bool ignoreCase, string ignoreCharacters, bool force) + static ConstructorInfo GetDeserializeConstructor(Type type) + { + var types = new Type[] { typeof(Json) }; + var independent = type.GetConstructor(BindingFlags.Public | BindingFlags.Instance, null, types, null); + if (independent == null) independent = type.GetConstructor(BindingFlags.NonPublic | BindingFlags.Instance, null, types, null); + return independent; + } + + /// 创建指定类型的新实例,将 Json 的内容填充到新实例中。 + public static object Object(Type type, Json json, bool ignoreCase, string ignoreCharacters, bool force) { - if (entity == null || json == null) return; - if (json.TokenType != JTokenType.Object) return; + // 检查参数。 + if (type == null) throw new ArgumentNullException(nameof(type)); + if (json == null || json._jtoken == null) return null; + + // 类型自己实现反序列化。 + var independent = GetDeserializeConstructor(type); + if (independent != null) return independent.Invoke(new object[] { json }); + + // 是数组。 + if (type.IsArray) + { + var itemType = GetTypeOfArrayItem(type); + return Array(itemType, json, ignoreCase, ignoreCharacters, force); + } + + // 是列表 + if (typeof(IList).IsAssignableFrom(type)) + { + var genericTypes = type.GetGenericArguments(); + if (genericTypes != null && genericTypes.Length == 1) + { + var genericType = genericTypes[0]; + if (genericType != null) + { + var array = Array(genericType, json, ignoreCase, ignoreCharacters, force); + var list = ArrayToList(array, type); + return list; + } + } + } + + // 必须是有效的 Json 实例。 + if (json.TokenType != JTokenType.Object && json.TokenType != JTokenType.Array) return null; + + // 必须有无参数的构造函数。 + var constructor = type.GetConstructor(System.Type.EmptyTypes); + if (constructor == null) return null; + + // 创建实例。 + var entity = constructor.Invoke(null); var jps = json.GetProperties(); - if (jps.Length < 1) return; + if (jps.Length < 1) return entity; var etype = entity.GetType(); var eps = etype.GetProperties(); - if (eps.Length < 1) return; + if (eps.Length < 1) return entity; foreach (var ep in eps) { @@ -1801,6 +1842,8 @@ namespace Apewer Property(entity, ep, value, ignoreCase, ignoreCharacters, force); } } + + return entity; } static void Add(object entity, object item, int index) @@ -1816,64 +1859,54 @@ namespace Apewer } } - internal static void Array(object array, Json json, bool ignoreCase, string ignoreCharacters, bool force) + /// + internal static object Array(Type itemType, Json json, bool ignoreCase, string ignoreCharacters, bool force) { - if (array == null) return; - if (json == null) return; - if (json.TokenType != JTokenType.Array) return; + if (itemType == null) throw new ArgumentNullException(nameof(itemType)); + if (json == null) return null; - var type = array.GetType(); - var subtype = null as Type; - if (array is Array) - { - var arrayType = array.GetType(); - subtype = RuntimeUtility.GetTypeOfArrayItem(arrayType); - } - else - { - var subtypes = type.GetGenericArguments(); - if (subtypes.Length < 1) return; - subtype = subtypes[0]; - } - - var jis = json.GetItems(); - for (var index = 0; index < jis.Length; index++) - { - var ji = jis[index]; - if (subtype.Equals(typeof(Json))) Add(array, ji, index); - else if (subtype.Equals(typeof(string))) Add(array, (ji.TokenType == JTokenType.Null) ? null : ji.Text, index); - else if (subtype.Equals(typeof(byte))) Add(array, Byte(ji.Text), index); - else if (subtype.Equals(typeof(short))) Add(array, Int16(ji.Text), index); - else if (subtype.Equals(typeof(int))) Add(array, Int32(ji.Text), index); - else if (subtype.Equals(typeof(long))) Add(array, Int64(ji.Text), index); - else if (subtype.Equals(typeof(sbyte))) Add(array, SByte(ji.Text), index); - else if (subtype.Equals(typeof(ushort))) Add(array, UInt16(ji.Text), index); - else if (subtype.Equals(typeof(uint))) Add(array, UInt32(ji.Text), index); - else if (subtype.Equals(typeof(ulong))) Add(array, UInt64(ji.Text), index); - else if (subtype.Equals(typeof(float))) Add(array, Single(ji.Text), index); - else if (subtype.Equals(typeof(double))) Add(array, Double(ji.Text), index); - else if (subtype.Equals(typeof(decimal))) Add(array, Decimal(ji.Text), index); + // 必须是有效的 Json 实例。 + if (json.TokenType != JTokenType.Array) return null; + + // 加入列表。 + var items = json.GetItems(); + var list = new List(items.Length); + for (var index = 0; index < items.Length; index++) + { + var item = items[index]; + if (item == null) list.Add(null); + else if (itemType.Equals(typeof(Json))) list.Add(item); + else if (itemType.Equals(typeof(string))) list.Add(item ? item.Text : null); + else if (itemType.Equals(typeof(byte))) list.Add(Byte(item.Text)); + else if (itemType.Equals(typeof(short))) list.Add(Int16(item.Text)); + else if (itemType.Equals(typeof(int))) list.Add(Int32(item.Text)); + else if (itemType.Equals(typeof(long))) list.Add(Int64(item.Text)); + else if (itemType.Equals(typeof(sbyte))) list.Add(SByte(item.Text)); + else if (itemType.Equals(typeof(ushort))) list.Add(UInt16(item.Text)); + else if (itemType.Equals(typeof(uint))) list.Add(UInt32(item.Text)); + else if (itemType.Equals(typeof(ulong))) list.Add(UInt64(item.Text)); + else if (itemType.Equals(typeof(float))) list.Add(Single(item.Text)); + else if (itemType.Equals(typeof(double))) list.Add(Double(item.Text)); + else if (itemType.Equals(typeof(decimal))) list.Add(Decimal(item.Text)); else { - var serializable = (force || _forceall) ? true : CanSerialize(subtype, false); - if (serializable && (ji is Json)) + var serializable = (force || _forceall) ? true : CanSerialize(itemType, false); + if (serializable) { - switch (ji.TokenType) - { - case JTokenType.Object: - var subobject = Activator.CreateInstance(subtype); - Object(subobject, ji, ignoreCase, ignoreCharacters, force); - Add(array, subobject, index); - break; - case JTokenType.Array: - var subarray = Activator.CreateInstance(subtype); - Array(subarray, ji, ignoreCase, ignoreCharacters, force); - Add(array, subarray, index); - break; - } + var itemInstance = Object(itemType, item, ignoreCase, ignoreCharacters, force); + list.Add(itemInstance); + } + else + { + list.Add(null); } } } + + // 输出数组。 + var array = System.Array.CreateInstance(itemType, list.Count); + for (var i = 0; i < list.Count; i++) array.SetValue(list[i], i); + return array; } private static void Property(object entity, PropertyInfo property, object value, bool ignoreCase, string ignoreCharacters, bool force) @@ -1930,36 +1963,51 @@ namespace Apewer { var serializable = (force || _forceall); if (!serializable) serializable = CanSerialize(property.PropertyType, false); - if (serializable && (value is Json)) + if (serializable && value is Json json) { - switch (((Json)value).TokenType) + if (pt.IsArray) { - case JTokenType.Object: - var subobject = Activator.CreateInstance(property.PropertyType); - Object(subobject, (Json)value, ignoreCase, ignoreCharacters, force); - setter.Invoke(entity, new object[] { subobject }); - break; - case JTokenType.Array: - object subarray; - if (pt.BaseType != null && pt.BaseType.Equals(typeof(Array))) - { - subarray = new object(); - var length = ((Json)value).GetItems().Length; - subarray = pt.InvokeMember("Set", BindingFlags.CreateInstance, null, subarray, new object[] { length }); - } - else + var array = Array(pt.GetElementType(), json, ignoreCase, ignoreCharacters, force); + setter.Invoke(entity, array); + } + else if (typeof(IList).IsAssignableFrom(pt)) + { + var genericTypes = pt.GetGenericArguments(); + if (genericTypes != null && genericTypes.Length == 1) + { + var genericType = genericTypes[0]; + if (genericType != null) { - subarray = Activator.CreateInstance(property.PropertyType); + var array = Array(genericType, json, ignoreCase, ignoreCharacters, force); + var list = ArrayToList(array, pt); + setter.Invoke(entity, list); } - Array(subarray, (Json)value, ignoreCase, ignoreCharacters, force); - setter.Invoke(entity, new object[] { subarray }); - break; + } + } + else + { + var @object = Object(pt, json, ignoreCase, ignoreCharacters, force); + setter.Invoke(entity, @object); } } } } } + static IList ArrayToList(object array, Type listType) + { + if (listType == null) throw new ArgumentNullException(nameof(listType)); + + if (array != null && array is Array a) + { + var list = Activator.CreateInstance(listType) as IList; + for (var i = 0; i < a.Length; i++) list.Add(a.GetValue(i)); + return list; + } + + return null; + } + #endregion #endregion diff --git a/Apewer/Network/HttpBody.cs b/Apewer/Network/HttpBody.cs new file mode 100644 index 0000000..17ccecb --- /dev/null +++ b/Apewer/Network/HttpBody.cs @@ -0,0 +1,52 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Text; + +namespace Apewer.Network +{ + + /// HTTP 正文。 + public abstract class HttpBody { } + + /// HTTP 报文结构。 + public sealed class HttpBytesMessage : HttpBody + { + + /// 主体。 + public byte[] Bytes { get; set; } + + } + + /// HTTP 报文结构。 + public class HttpStreamMessage : HttpBody where T : Stream + { + + /// 自动释放 Stream 对象。 + public bool AutoDispose { get; set; } + + /// 主体。 + public Stream Stream { get; set; } + + /// 主体的长度。 + public long Length { get; set; } + + + } + + /// HTTP 报文结构。 + public sealed class HttpStreamMessage : HttpBody + { + + /// 自动释放 Stream 对象。 + public bool AutoDispose { get; set; } + + /// 主体。 + public Stream Stream { get; set; } + + /// 主体的长度。 + public long Length { get; set; } + + } + +} diff --git a/Apewer/Network/HttpHeader.cs b/Apewer/Network/HttpHeader.cs new file mode 100644 index 0000000..75c7cbb --- /dev/null +++ b/Apewer/Network/HttpHeader.cs @@ -0,0 +1,47 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Apewer.Network +{ + + /// HTTP 头。 + [Serializable] + public sealed class HttpHeader + { + + string _name = null; + string _value = null; + + /// 名称。 + public string Name { get => _name; set => _name = value?.Trim(); } + + /// 值。 + public string Value { get => _value; set => _value = value?.Trim(); } + + /// 创建 HTTP 头的实例。 + public HttpHeader() { } + + /// 创建 HTTP 头的实例。 + /// + public HttpHeader(KeyValuePair keyValuePair) + { + if (keyValuePair.Key.IsEmpty()) throw new ArgumentNullException("Key 无效。"); + + Name = keyValuePair.Key; + Value = keyValuePair.Value; + } + + /// 创建 HTTP 头的实例。 + /// + public HttpHeader(string name, string value) + { + if (name.IsEmpty()) throw new ArgumentNullException(nameof(name)); + + Name = name; + Value = value; + } + + } + +} diff --git a/Apewer/Network/HttpHeaders.cs b/Apewer/Network/HttpHeaders.cs new file mode 100644 index 0000000..3d1f9b0 --- /dev/null +++ b/Apewer/Network/HttpHeaders.cs @@ -0,0 +1,369 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Collections.Specialized; + +namespace Apewer.Network +{ + + /// HTTP 头的集合。 + [Serializable] + public sealed class HttpHeaders : IEnumerable, ICollection, IToJson + { + + #region IEnumerable + + List _list = new List(); + long _version = 0L; + + /// 获取枚举器。 + public IEnumerator GetEnumerator() => new Enumerator(this); + + /// 获取枚举器。 + IEnumerator IEnumerable.GetEnumerator() => new Enumerator(this); + + /// + public sealed class Enumerator : IEnumerator + { + + HttpHeaders _headeres = null; + long _version = 0L; + bool _disposed = false; + + HttpHeader _current = null; + int _index = 0; + + const string ObjectName = nameof(Enumerator); + const string OriginChanged = "原集合已变更,无法继续遍历。"; + + /// + public HttpHeader Current + { + get + { + if (_disposed) throw new ObjectDisposedException(ObjectName); + if (_version != _headeres._version) throw new InvalidOperationException(OriginChanged); + return _current; + } + } + + /// + object IEnumerator.Current { get => Current; } + + /// + public void Dispose() { _disposed = true; } + + /// + public bool MoveNext() + { + if (_disposed) throw new ObjectDisposedException(ObjectName); + if (_version != _headeres._version) throw new InvalidOperationException(OriginChanged); + + if (_index < _headeres._list.Count) + { + _current = _headeres._list[_index]; + _index++; + return true; + } + + return false; + } + + /// + public void Reset() + { + _current = null; + _index = 0; + } + + /// + public Enumerator(HttpHeaders headers) + { + if (headers == null) throw new ArgumentNullException(nameof(headers)); + _headeres = headers; + _version = headers._version; + Reset(); + } + + } + + #endregion + + #region ICollection + + /// 元素数量。 + public int Count { get => _list.Count; } + + /// 当前集合是只读。 + public bool IsReadOnly { get => false; } + + /// 添加一项。 + /// + public HttpHeader Add(string name, string value) + { + if (name.IsEmpty()) throw new ArgumentNullException(nameof(name)); + var header = new HttpHeader(name, value); + _list.Add(header); + _version++; + return header; + } + + /// 添加一项。 + /// + public void Add(HttpHeader header) + { + if (header == null) throw new ArgumentNullException(nameof(header)); + _list.Add(header); + _version++; + } + + /// 移除所有元素。 + public void Clear() => _list.Clear(); + + /// 判断是否包含指定的元素。 + public bool Contains(HttpHeader item) => _list.Contains(item); + + /// 复制所有元素到数组。 + /// 目标数组。 + /// 数组的位置。 + /// + /// + /// + public void CopyTo(HttpHeader[] array, int arrayIndex) => _list.CopyTo(array, arrayIndex); + + /// 移除指定的元素 + public bool Remove(HttpHeader item) => _list.Remove(item); + + #endregion + + #region IList + + /// + /// + public HttpHeader this[int index] { get => _list[index]; set => _list[index] = value; } + + /// + /// + public string this[string name] { get => GetValue(name); set => SetValue(name, value); } + + /// 搜索指定对象在当前集合中的位置索引。 + public int IndexOf(HttpHeader item) => _list.IndexOf(item); + + /// 插入元素到指定位置。 + /// + public void Insert(int index, HttpHeader item) => _list.Insert(index, item); + + /// 移除指定位置的元素。 + /// + public void RemoveAt(int index) => _list.RemoveAt(index); + + #endregion + + #region constructor + + /// + public HttpHeaders() { } + + /// + /// + public HttpHeaders(params HttpHeader[] headers) : this(headers as IEnumerable) { } + + /// + /// + public HttpHeaders(IEnumerable headers) + { + if (headers == null) throw new ArgumentNullException(nameof(headers)); + + foreach (var header in headers) + { + if (header == null) continue; + if (header.Name.IsEmpty()) continue; + Add(header); + } + } + + /// + /// + public HttpHeaders(IEnumerable> headers) + { + if (headers == null) return; + + foreach (var header in headers) + { + if (header.Key.IsEmpty()) continue; + Add(header.Key, header.Value); + } + } + + /// + /// + public HttpHeaders(NameValueCollection headers) + { + if (headers == null) return; + + var keys = headers.AllKeys; + foreach (var key in keys) + { + if (key.IsEmpty()) continue; + var value = headers[key]; + Add(key, value); + } + } + + #endregion + + #region operation + + /// 获取匹配 Name 的 Value。不存在 Name 时返回 NULL 值。 + /// + public string GetValue(string name) + { + if (name.IsEmpty()) throw new ArgumentNullException(nameof(name)); + + // 精准匹配。 + var count = _list.Count; + for (var i = 0; i < count; i++) + { + var item = _list[i]; + if (string.IsNullOrEmpty(item.Name)) continue; + if (item.Name == name) + { + if (string.IsNullOrEmpty(item.Value)) continue; + return item.Value; + } + } + + // 忽略大小写。 + var lower = TextUtility.Lower(name); + for (var i = 0; i < count; i++) + { + var item = _list[i]; + if (string.IsNullOrEmpty(item.Name)) continue; + if (TextUtility.Lower(item.Name) == lower) + { + if (string.IsNullOrEmpty(item.Value)) continue; + return item.Value; + } + } + + return null; + } + + /// 获取匹配 Name 的 Value。不存在 Name 时返回 NULL 值。 + /// + public string[] GetValues(string name) + { + if (name.IsEmpty()) throw new ArgumentNullException(nameof(name)); + + // 忽略大小写。 + // name = TextUtility.Lower(name); + + var count = _list.Count; + var values = new List(_list.Count); + for (var i = 0; i < count; i++) + { + var item = _list[i]; + if (string.IsNullOrEmpty(item.Name)) continue; + if (string.Equals(item.Name, name, StringComparison.CurrentCultureIgnoreCase)) + { + var value = item.Value.ToTrim(); + if (string.IsNullOrEmpty(value)) continue; + values.Add(value); + } + } + + return values.ToArray(); + } + + /// 设置 Name 的 Value。不存在 Name 时添加新元素。 + /// + public HttpHeader SetValue(string name, string value) + { + if (name.IsEmpty()) throw new ArgumentNullException(nameof(name)); + + // 尝试精准匹配。 + var count = _list.Count; + for (var i = 0; i < count; i++) + { + var item = _list[i]; + if (string.IsNullOrEmpty(item.Name)) continue; + if (item.Name == name) + { + item.Value = value; + _version++; + return item; + } + } + + // 尝试模糊匹配。 + var lower = TextUtility.Lower(name); + for (var i = 0; i < count; i++) + { + var item = _list[i]; + if (TextUtility.Lower(item.Name) == lower) + { + item.Value = value; + _version++; + return item; + } + } + + // 添加新项。 + return Add(name, value); + } + + /// 每个元素组成为新数组。 + public HttpHeader[] ToArray() => _list.ToArray(); + + #endregion + + #region Json + + internal HttpHeaders(Json json) + { + if (!json) return; + + if (json.IsObject) + { + var properties = json.GetProperties(); + foreach (var property in properties) + { + if (property == null) continue; + if (property.Name.IsEmpty()) continue; + Add(property.Name, property.Value?.ToString()); + } + } + + if (json.IsArray) + { + var array = json.Array(); + if (array != null) + { + for (var i = 0; i < array.Length; i++) + { + if (array[i] == null) continue; + if (array[i].Name.IsEmpty()) continue; + _list.Add(array[i]); + } + } + } + } + + /// + public Json ToJson() + { + var array = Json.NewArray(); + var count = 0; + for (var i = 0; i < count; i++) + { + var item = Json.From(_list[i]); + array.AddItem(item); + } + return array; + } + + #endregion + + } + +} diff --git a/Apewer/Network/HttpMethod.cs b/Apewer/Network/HttpMethod.cs index 5c7c9e4..ae08438 100644 --- a/Apewer/Network/HttpMethod.cs +++ b/Apewer/Network/HttpMethod.cs @@ -11,31 +11,31 @@ namespace Apewer.Network /// NULL, - /// The CONNECT method establishes a tunnel to the server identified by the target resource. + /// CONNECT 方法建立一个到由目标资源标识的服务器的隧道。 CONNECT, - /// The DELETE method deletes the specified resource. + /// DELETE 方法删除指定的资源。 DELETE, - /// The GET method requests a representation of the specified resource. Requests using GET should only retrieve data. + /// GET 方法请求一个指定资源的表示形式,使用 GET 的请求应该只被用于获取数据。 GET, - /// The HEAD method asks for a response identical to that of a GET request, but without the response body. + /// HEAD 方法请求一个与 GET 请求的响应相同的响应,但没有响应体。 HEAD, - /// The OPTIONS method is used to describe the communication options for the target resource. + /// OPTIONS 方法用于描述目标资源的通信选项。 OPTIONS, - /// The PATCH method is used to apply partial modifications to a resource. + /// PATCH 方法用于对资源应用部分修改。 PATCH, - /// The POST method is used to submit an entity to the specified resource, often causing a change in state or side effects on the server. + /// POST 方法用于将实体提交到指定的资源,通常导致在服务器上的状态变化或副作用。 POST, - /// The PUT method replaces all current representations of the target resource with the request payload. + /// PUT 方法用有效载荷请求替换目标资源的所有当前表示。 PUT, - /// The TRACE method performs a message loop-back test along the path to the target resource. + /// TRACE 方法沿着到目标资源的路径执行一个消息环回测试。 TRACE } diff --git a/Apewer/RuntimeUtility.cs b/Apewer/RuntimeUtility.cs index eb24203..bd837c5 100644 --- a/Apewer/RuntimeUtility.cs +++ b/Apewer/RuntimeUtility.cs @@ -229,32 +229,14 @@ namespace Apewer /// /// /// - public static object InvokeMethod(object instance, MethodInfo method, object parameter) + public static object Invoke(this MethodInfo method, object instance, params object[] parameters) { if (instance == null || method == null) return null; - { - var pis = method.GetParameters(); - if (pis == null || pis.Length < 1) return method.Invoke(instance, null); - } - return method.Invoke(instance, new object[] { parameter }); - } - /// 调用方法。 - /// - /// - /// - /// - /// - /// - /// - public static object InvokeMethod(object instance, MethodInfo method, params object[] parameters) - { - if (instance == null || method == null) return null; - { - var pis = method.GetParameters(); - if (pis == null || pis.Length < 1) return method.Invoke(instance, null); - } - if (parameters == null) return method.Invoke(instance, new object[] { null }); + var pis = method.GetParameters(); + if (pis == null || pis.Length < 1) return method.Invoke(instance, null); + + if (parameters == null) return method.Invoke(instance, null); return method.Invoke(instance, parameters); } diff --git a/Apewer/Web/ActionResult.cs b/Apewer/Web/ActionResult.cs new file mode 100644 index 0000000..3c9d74c --- /dev/null +++ b/Apewer/Web/ActionResult.cs @@ -0,0 +1,24 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Apewer.Web +{ + + /// API 结果。 + public abstract class ActionResult : IActionResult, IHttpActionResult, IDisposable + { + + /// 释放系统资源。 + public virtual void Dispose() { } + + // /// 释放系统资源。 + // /// TRUE:释放托管资源和非托管资源。FALSE:释放非托管资源。 + // protected virtual void Dispose(bool disposing) { } + + /// 执行结果。 + public abstract void ExecuteResult(ApiContext context); + + } + +} diff --git a/Apewer/Web/ApiAction.cs b/Apewer/Web/ApiAction.cs new file mode 100644 index 0000000..26db784 --- /dev/null +++ b/Apewer/Web/ApiAction.cs @@ -0,0 +1,240 @@ +using Apewer.Network; +using System; +using System.Collections.Generic; +using System.Reflection; +using System.Text; + +namespace Apewer.Web +{ + + /// API 行为。 + public sealed class ApiAction : IToJson + { + + #region fields + + Type _type = null; + MethodInfo _method = null; + + string _path = null; + HttpMethod[] _methods = null; + ApiParameter[] _parameters = null; + + #endregion + + #region propeties + + /// 控制器的反射类型。 + public Type Type { get => _type; } + + /// API 行为的反射方法。 + public MethodInfo MethodInfo { get => _method; } + + /// URL 路径。 + public string Path { get => _path; } + + /// HTTP 方法。 + public HttpMethod[] Methods + { + get + { + var result = new HttpMethod[_methods.Length]; + if (_methods.Length > 0) _methods.CopyTo(result, 0); + return result; + } + } + + /// 参数。 + public ApiParameter[] Parameters + { + get + { + var result = new ApiParameter[_parameters.Length]; + if (_parameters.Length > 0) _parameters.CopyTo(result, 0); + return result; + } + } + + /// 生成 JSON 实例。 + public Json ToJson() => ToJson(null); + + /// 生成 JSON 实例。 + public Json ToJson(ApiActionJsonFormat format) + { + if (format == null) format = ApiActionJsonFormat.Default ?? new ApiActionJsonFormat(); + + var methods = new Json(); + foreach (var method in _methods) + { + methods.AddItem(method.ToString().Lower()); + } + + var json = new Json(); + json.SetProperty("path", _path); + json.SetProperty("methods", methods); + + if (format.WithReflection) + { + var reflection = new Json(); + reflection.SetProperty("type", _type.FullName); + reflection.SetProperty("method", _method.Name); + json.SetProperty("reflection", reflection); + } + + if (format.WithParameters && _parameters.Length > 0) + { + var parameters = Json.NewArray(); + foreach (var parameter in _parameters) + { + parameters.AddItem(parameter.ToJson(format.WithReflection)); + } + json.SetProperty("parameters", parameters); + } + + return json; + } + + /// 生成字符串。 + public override string ToString() => _path; + + #endregion + + #region parse + + const string Separator = "/"; + + /// 创建 API 行为描述实例。 + /// 控制器的反射类型。 + /// API 行为的反射方法。 + /// URL 路径。 + /// HTTP 方法。 + /// 参数。 + /// + /// + ApiAction(Type type, MethodInfo method, string path, HttpMethod[] methods, ApiParameter[] parameters) + { + if (type == null) throw new ArgumentNullException(nameof(type)); + if (method == null) throw new ArgumentNullException(nameof(method)); + if (method.IsAbstract) throw new ArgumentException($"参数 {nameof(method)} 是抽象的。"); + if (path.IsEmpty()) throw new ArgumentNullException(nameof(path)); + if (methods.IsEmpty()) throw new ArgumentNullException(nameof(methods)); + + var split = path.Split('/'); + var segs = split.Trim(); + path = segs.Length < 1 ? Separator : (Separator + string.Join(Separator, segs)); + + _type = type; + _method = method; + _path = path; + _methods = methods; + _parameters = parameters; + } + + /// 解析控制器类型,获取 API 活动。 + /// + public static ApiAction[] Parse(Type type) + { + if (type == null) throw new ArgumentNullException(nameof(type)); + + if (type.FullName == "Front.Debug.Controller") + { + } + + // 检查类型的属性。 + if (!type.IsClass) return new ApiAction[0]; + if (type.IsAbstract) return new ApiAction[0]; + if (type.IsGenericType) return new ApiAction[0]; + if (type.GetGenericArguments().NotEmpty()) return new ApiAction[0]; + if (!RuntimeUtility.CanNew(type)) return new ApiAction[0]; + + // 判断基类。 + if (!typeof(ApiController).IsAssignableFrom(type)) return new ApiAction[0]; + + // 读取 URL 前缀。 + var prefixAttribute = RuntimeUtility.GetAttribute(type, false); + var prefixPath = (prefixAttribute == null || prefixAttribute.Path.IsEmpty()) ? null : prefixAttribute.Path.Split('/').Trim(); + // if (prefixPath.IsEmpty()) + // { + // // 读取 API 特性。 + // var api = RuntimeUtility.GetAttribute(type); + // if (api != null) + // { + // var apiName = api.Name.ToTrim(); + // if (apiName.Lower().EndsWith("controller")) apiName = apiName.Substring(0, apiName.Length - 10); + // if (apiName.NotEmpty()) prefixPath = new string[] { apiName }; + // } + // } + + // 读取方法。 + var methods = type.GetMethods(BindingFlags.Public | BindingFlags.Instance); + var actions = new List(methods.Length); + foreach (var method in methods) + { + // 不支持构造函数和泛型。 + if (method.IsConstructor) continue; + if (method.IsGenericMethod) continue; + + // 抽象类无法创建实例。 + if (method.IsAbstract) continue; + if (!method.DeclaringType.Equals(type)) continue; + + // 必须有 Route 特性 + var route = RuntimeUtility.GetAttribute(method); + if (route == null) continue; + + // 确定路径 + var path = route.Path; + if (path.IsEmpty()) + { + if (prefixPath.IsEmpty()) continue; + path = method.Name; + } + path = ConcatPath(prefixPath, path.Split('/').Trim()); + + // 必须有 HTTP 方法 + var httpMethods = new List(9); + if (RuntimeUtility.Contains(method)) httpMethods.Add(HttpMethod.CONNECT); + if (RuntimeUtility.Contains(method)) httpMethods.Add(HttpMethod.DELETE); + if (RuntimeUtility.Contains(method)) httpMethods.Add(HttpMethod.GET); + if (RuntimeUtility.Contains(method)) httpMethods.Add(HttpMethod.HEAD); + if (RuntimeUtility.Contains(method)) httpMethods.Add(HttpMethod.OPTIONS); + if (RuntimeUtility.Contains(method)) httpMethods.Add(HttpMethod.PATCH); + if (RuntimeUtility.Contains(method)) httpMethods.Add(HttpMethod.POST); + if (RuntimeUtility.Contains(method)) httpMethods.Add(HttpMethod.PUT); + if (RuntimeUtility.Contains(method)) httpMethods.Add(HttpMethod.TRACE); + if (httpMethods.Count < 1) continue; + + // 参数 + var parameters = new List(); + foreach (var pi in method.GetParameters()) + { + var parameter = ApiParameter.Parse(pi); + if (parameter == null) continue; + parameters.Add(parameter); + } + + var action = new ApiAction(type, method, path, httpMethods.ToArray(), parameters.ToArray()); + actions.Add(action); + } + + return actions.ToArray(); + } + + static string ConcatPath(string[] prefix, string[] path) + { + var list = new List(); + if (prefix != null) list.AddRange(prefix); + if (path != null) list.AddRange(path); + + var segs = list.Trim(); + if (segs.Length < 1) return Separator; + + var result = Separator + string.Join(Separator, segs); + return result; + } + + #endregion + + } + +} diff --git a/Apewer/Web/ApiActionJsonFormat.cs b/Apewer/Web/ApiActionJsonFormat.cs new file mode 100644 index 0000000..d4b82b1 --- /dev/null +++ b/Apewer/Web/ApiActionJsonFormat.cs @@ -0,0 +1,33 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Apewer.Web +{ + + /// 类型 的实例转为 时的格式。 + [Serializable] + public sealed class ApiActionJsonFormat + { + + /// 默认格式。 + public static ApiActionJsonFormat Default { get; set; } + + /// 包含参数。 + /// TRUE(默认值) + public bool WithParameters { get; set; } + + /// 包含反射信息。 + /// TRUE(默认值) + public bool WithReflection { get; set; } + + /// 创建 的实例。 + public ApiActionJsonFormat() + { + WithParameters = true; + WithReflection = true; + } + + } + +} diff --git a/Apewer/Web/ApiApplication.cs b/Apewer/Web/ApiApplication.cs index a598c8e..605eb4e 100644 --- a/Apewer/Web/ApiApplication.cs +++ b/Apewer/Web/ApiApplication.cs @@ -5,47 +5,176 @@ using System.Reflection; namespace Apewer.Web { - internal sealed class ApiApplication + /// + public sealed class ApiApplication : IToJson { - internal Dictionary Functions = null; - internal List Items = null; + #region fields - internal Type Type; - internal string Module; + Type _type = null; + string _module = null; - // 主特性和主要属性。 - internal ApiAttribute Attribute; - internal string Name; - internal string Lower; - internal string Caption; - internal string Description; + // ApiAttribute + string _name = null; + string _lower = null; + string _caption = null; + string _description = null; - // 附加特性。 - internal bool Independent; - internal bool Hidden; + // invoke & enumerate + bool _independent = false; + bool _hidden = false; - internal ApiFunction Get(string name) + // functions + Dictionary _dict = new Dictionary(); + List _list = new List(); + + #endregion + + #region properties + + /// + public Type Type { get => _type; } + + /// + public string Module { get => _module; } + + /// + public string Name { get => _name; } + + /// + public string Caption { get => _caption; } + + /// + public string Description { get => _description; } + + /// + public bool Independent { get => _independent; } + + /// + public bool Hidden { get => _hidden; } + + /// + public ApiFunction[] Functions { get => _list.ToArray(); } + + #endregion + + /// + public ApiApplication(Type type, ApiAttribute api) + { + // type + _type = type; + + // api + if (api == null) + { + _name = type?.Name; + } + else + { + _name = string.IsNullOrEmpty(api.Name) ? type?.Name : api.Name; + _caption = api.Caption; + _description = api.Description; + } + + if (type != null) + { + // caption + if (string.IsNullOrEmpty(_caption)) + { + var captions = type.GetCustomAttributes(typeof(CaptionAttribute), true); + if (captions.Length > 0) + { + var caption = (CaptionAttribute)captions[0]; + _caption = caption.Title; + if (string.IsNullOrEmpty(_description)) + { + _description = caption.Description; + } + } + } + + // hidden + if (type.Contains(false)) _hidden = true; + + // independent + if (type.Contains(false)) _independent = true; + + // Module + var assemblyName = type.Assembly.GetName(); + _module = TextUtility.Join("-", assemblyName.Name, assemblyName.Version.ToString()); + + // functions + var funcs = new Dictionary(); + var methods = type.GetMethods(BindingFlags.Instance | BindingFlags.Public); + foreach (var method in methods) + { + var func = ApiFunction.Parse(this, method); + if (func == null) continue; + + var funcKey = func.Name.Lower(); + if (funcs.ContainsKey(funcKey)) continue; + funcs.Add(funcKey, func); + } + _dict = funcs; + + _list.AddRange(funcs.Values); + _list.Sort(new Comparison((a, b) => a.Name.CompareTo(b.Name))); + } + } + + internal ApiFunction GetFunction(string name) { if (string.IsNullOrEmpty(name)) return null; - var lower = name.ToLower(); - ApiFunction func; - var exist = Functions.TryGetValue(lower, out func); - return func; + if (_dict.TryGetValue(name.ToLower(), out var func)) return func; + return null; } + /// + public Json ToJson() => ToJson(new ApiOptions()); + internal Json ToJson(ApiOptions options) { if (Hidden) return null; + var json = Json.NewObject(); - json.SetProperty("name", Name); - if (!string.IsNullOrEmpty(Caption)) json.SetProperty("caption", Caption); - if (!string.IsNullOrEmpty(Description)) json.SetProperty("description", Description); - if (options.WithTypeName) json.SetProperty("type", Type.FullName); - if (options.WithModuleName) json.SetProperty("mudule", Module); + json.SetProperty("name", _name); + if (!string.IsNullOrEmpty(_caption)) json.SetProperty("caption", _caption); + if (!string.IsNullOrEmpty(_description)) json.SetProperty("description", _description); + if (options != null) + { + if (options.WithTypeName) json.SetProperty("type", _type.FullName); + if (options.WithModuleName) json.SetProperty("mudule", _module); + if (options.AllowEnumerate) json.SetProperty("functions", Json.From(_list)); + } return json; } + /// 解析类型,获取 实例。 + /// 要解析的类型。 + /// 要求此类型拥有 Api 特性。 + /// 解析成功返回实例,解析失败返回 NULL 值。 + public static ApiApplication Parse(Type type, bool requireAttribute) + { + if (type == null) return null; + + // 检查类型的属性。 + if (!type.IsClass) return null; + if (type.IsAbstract) return null; + if (type.IsGenericType) return null; + if (type.GetGenericArguments().NotEmpty()) return null; + if (!RuntimeUtility.CanNew(type)) return null; + + // 判断基类。 + if (!typeof(ApiController).IsAssignableFrom(type)) return null; + + // 检查类型的特性。 + var apis = type.GetCustomAttributes(typeof(ApiAttribute), false); + var api = apis.Length > 0 ? (ApiAttribute)apis[0] : null; + if (requireAttribute && api == null) return null; + + return new ApiApplication(type, api); + } + } } diff --git a/Apewer/Web/ApiCatch.cs b/Apewer/Web/ApiCatch.cs index f5a632b..d214ff5 100644 --- a/Apewer/Web/ApiCatch.cs +++ b/Apewer/Web/ApiCatch.cs @@ -9,23 +9,18 @@ namespace Apewer.Web public sealed class ApiCatch { - ApiController _controller = null; - ApiOptions _options = null; + ApiContext _context = null; Exception _exception = null; - /// 调度程序调用的控制器。 - public ApiController Controller { get => _controller; } - - /// 调度程序使用的 API 选项。 - public ApiOptions Options { get => _options; } + /// 上下文。 + public ApiContext Context { get => _context; } /// 已捕获的异常。 public Exception Exception { get => _exception; } - internal ApiCatch(ApiController controller, ApiOptions options, Exception exception) + internal ApiCatch(ApiContext context, Exception exception) { - _controller = controller; - _options = options; + _context = context; _exception = exception; } diff --git a/Apewer/Web/ApiContext.cs b/Apewer/Web/ApiContext.cs index 86ff90e..424b875 100644 --- a/Apewer/Web/ApiContext.cs +++ b/Apewer/Web/ApiContext.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Generic; using System.Diagnostics; +using System.Reflection; using System.Text; namespace Apewer.Web @@ -39,8 +40,8 @@ namespace Apewer.Web #region 执行过程中产生的内容 - /// API 入口。 - public ApiEntry Entry { get; internal set; } + /// API 行为。 + public ApiAction ApiAction { get; internal set; } /// API 请求。 public ApiRequest Request { get; internal set; } @@ -51,6 +52,9 @@ namespace Apewer.Web /// API 控制器实例。 public ApiController Controller { get; internal set; } + /// 执行的方法。 + public MethodInfo MethodInfo { get; internal set; } + #endregion internal ApiContext(ApiInvoker invoker, ApiProvider provider, ApiEntries entries) diff --git a/Apewer/Web/ApiEntries.cs b/Apewer/Web/ApiEntries.cs index 28624ba..94a3edd 100644 --- a/Apewer/Web/ApiEntries.cs +++ b/Apewer/Web/ApiEntries.cs @@ -7,272 +7,181 @@ namespace Apewer.Web { /// 入口集合。 - public sealed class ApiEntries + public sealed class ApiEntries : IToJson { - #region 实例。 + #region instance object locker = new object(); - Dictionary Applications = null; - List Items = null; + SortedDictionary _apps = new SortedDictionary(); + SortedDictionary _actions = new SortedDictionary(); - internal ApiApplication Get(string name) + internal ApiApplication GetApplication(string name) { if (string.IsNullOrEmpty(name)) return null; - if (Applications == null) return null; - var lower = name.ToLower(); - ApiApplication app; - var found = false; - lock (locker) { found = Applications.TryGetValue(lower, out app); } - return found ? app : null; - } - - internal List Enumerate() => Items; - /// 清空当前实例。 - public void Clear() - { + var key = name.ToLower(); lock (locker) { - Applications = new Dictionary(); - Items = new List(); + if (_apps.TryGetValue(key, out var value)) return value; } + return null; } - /// 追加指定的集合,指定 replace 参数将替换当前实例中的同名的入口。 - public void Append(ApiEntries entries, bool replace = false) + internal ApiAction GetAction(string path) { - if (entries == null) return; + if (string.IsNullOrEmpty(path)) return null; + + var key = path.ToLower(); lock (locker) { - var dict = Applications ?? new Dictionary(); - foreach (var app in entries.Applications) - { - var key = app.Key; - if (dict.ContainsKey(key)) - { - if (replace) dict[key] = app.Value; - continue; - } - dict.Add(app.Key, app.Value); - } - var list = new List(dict.Values); - list.Sort(new Comparison((a, b) => a.Lower.CompareTo(b.Lower))); - Applications = dict; - Items = list; + if (_actions.TryGetValue(key, out var value)) return value; } + return null; } - #endregion + /// + public ApiApplication[] Applications { get => _apps.Values.Map(x => x); } - #region 静态方法。 + /// + public ApiAction[] Actions { get => _actions.Values.Map(x => x); } - /// 从指定的程序集获取入口。 - public static ApiEntries From(Assembly assembly) - { - if (assembly == null) return null; - var types = RuntimeUtility.GetTypes(assembly, false); - var dict = new Dictionary(); - foreach (var type in types) - { - var app = Application(type, true); - if (app == null) continue; - var lower = app.Lower; - if (dict.ContainsKey(lower)) continue; - dict.Add(lower, app); - - var funcs = new Dictionary(); - var methods = type.GetMethods(BindingFlags.Instance | BindingFlags.Public); - foreach (var method in methods) - { - var func = Function(app, method); - if (func == null) continue; - if (funcs.ContainsKey(func.Lower)) continue; - funcs.Add(func.Lower, func); - } - app.Functions = funcs; - app.Items = new List(funcs.Values); - app.Items.Sort(new Comparison((a, b) => a.Name.CompareTo(b.Name))); - } - var entries = new ApiEntries(); - entries.Applications = dict; - var list = new List(dict.Values); - list.Sort(new Comparison((a, b) => a.Lower.CompareTo(b.Lower))); - entries.Items = list; - return entries; - } + /// + public ApiEntries() { } - /// 从多个程序集中获取入口。 - public static ApiEntries From(IEnumerable assemblies, bool replace = false) + /// + public ApiEntries(IEnumerable applications, IEnumerable actions, bool replace = false) : this() { - if (assemblies == null) return null; - var entries = new ApiEntries(); - foreach (var assembly in assemblies) entries.Append(From(assembly), replace); - return entries; + Add(applications, replace); + Add(actions, replace); } - /// 从当前程序中获取入口。 - public static ApiEntries Calling() => From(Assembly.GetCallingAssembly()); + /// + public ApiEntries(IEnumerable applications, bool replace = false) : this(applications, null, replace) { } - /// 从当前 AppDomain 中获取入口。 - public static ApiEntries AppDomain(bool replace = false) => From(System.AppDomain.CurrentDomain.GetAssemblies(), replace); + /// + public ApiEntries(ApiAction[] actions, bool replace = false) : this(null, actions, replace) { } - static ApiApplication Application(Type type, bool requireAttribute) + /// 添加入口。 + public void Add(IEnumerable applications, bool replace = false) { - if (type == null) return null; - - // 检查类型的属性。 - if (!type.IsClass) return null; - if (type.IsAbstract) return null; - if (type.IsGenericType) return null; - if (type.GetGenericArguments().NotEmpty()) return null; - if (!RuntimeUtility.CanNew(type)) return null; - - // 检查类型的特性。 - var apis = type.GetCustomAttributes(typeof(ApiAttribute), false); - var api = apis.Length > 0 ? (ApiAttribute)apis[0] : null; - if (requireAttribute && api == null) return null; - - // 检查基类。 - if (!RuntimeUtility.IsInherits(type, typeof(ApiController))) return null; - - // Entry - var entry = new ApiApplication(); - entry.Type = type; - entry.Attribute = api; - - // Attribute - if (api != null) + if (applications == null) return; + lock (locker) { - entry.Name = string.IsNullOrEmpty(api.Name) ? type.Name : api.Name; - entry.Lower = entry.Name.ToLower(); - var name = api.Name; - if (string.IsNullOrEmpty(name)) name = type.Name; + foreach (var app in applications) + { + if (app == null) continue; - entry.Caption = api.Caption; - entry.Description = api.Description; - } - else - { - entry.Name = type.Name; - entry.Lower = entry.Name.ToLower(); - entry.Caption = null; - entry.Description = null; - entry.Hidden = true; - } + var appKey = app.Name.Lower(); + if (appKey.IsEmpty()) continue; - // Caption - if (string.IsNullOrEmpty(entry.Caption)) - { - var captions = type.GetCustomAttributes(typeof(CaptionAttribute), true); - if (captions.Length > 0) - { - var caption = (CaptionAttribute)captions[0]; - entry.Caption = caption.Title; - entry.Description = caption.Description; + if (_apps.ContainsKey(appKey)) + { + if (replace) _apps[appKey] = app; + } + else + { + _apps.Add(appKey, app); + } } } - - // Hidden - if (type.Contains(false)) entry.Hidden = true; - - // Independent - if (type.Contains(false)) entry.Independent = true; - - // Module - var assemblyName = type.Assembly.GetName(); - entry.Module = TextUtility.Join("-", assemblyName.Name, assemblyName.Version.ToString()); - - return entry; } - static ApiFunction Function(ApiApplication application, MethodInfo method) + /// 添加入口。 + public void Add(IEnumerable actions, bool replace = false) { - if (application == null) return null; - if (method == null) return null; - - // 滤除构造函数、抽象方法、泛型和非本类定义方法。 - if (method.IsConstructor) return null; - if (method.IsAbstract) return null; - if (method.GetGenericArguments().NotEmpty()) return null; - - // 滤除 get 和 set 访问器。 - var methodName = method.Name; - if (methodName.StartsWith("get_") || methodName.StartsWith("set_")) return null; - - // 定义者。 - var declaring = method.DeclaringType; - if (declaring.Equals(typeof(object))) return null; - - // 检查 ApiAttribute 特性。 - var apis = method.GetCustomAttributes(typeof(ApiAttribute), false); - var api = apis.Length > 0 ? (ApiAttribute)apis[0] : null; - - // Entry - var entry = new ApiFunction(); - entry.Application = application; - entry.Method = method; - - // 返回值。 - var returnable = method.ReturnType; - if (returnable.Equals(typeof(void))) returnable = null; - entry.Returnable = returnable; - - // 参数。 - var pis = method.GetParameters(); - if (pis != null && pis.Length > 0) + if (actions == null) return; + lock (locker) { - var pisc = pis.Length; - for (var i = 0; i < pisc; i++) + foreach (var action in actions) { - var pi = pis[i]; - if (pi.IsIn) return null; - if (pi.IsOut) return null; - } - entry.Parameters = pis; + if (action == null) continue; - if (pisc == 1) - { - var pi = pis[0]; - var pt = pi.ParameterType; - if (RuntimeUtility.IsInherits(pt, typeof(Source.Record))) entry.ParamIsRecord = true; + var actionKey = action.Path.Lower(); + if (actionKey.IsEmpty()) continue; + + if (_actions.ContainsKey(actionKey)) + { + if (replace) _actions[actionKey] = action; + } + else + { + _actions.Add(actionKey, action); + } } } + } - if (api != null) + /// 追加指定的集合,指定 replace 参数将替换当前实例中的同名的入口。 + public void Add(ApiEntries entries, bool replace = false) + { + if (entries == null) return; + Add(entries.Applications, replace); + Add(entries.Actions, replace); + } + + /// 清空当前实例。 + public void Clear() + { + lock (locker) { - entry.Name = string.IsNullOrEmpty(api.Name) ? method.Name : api.Name; - entry.Lower = entry.Name.ToLower(); - entry.Caption = api.Caption; - entry.Description = api.Description; + _apps.Clear(); + _actions.Clear(); } - else + } + + /// 生成 Json 实例。 + public Json ToJson() + { + lock (locker) { - entry.Name = method.Name; - entry.Lower = entry.Name.ToLower(); - entry.Caption = null; - entry.Description = null; + var obj = new + { + applications = Applications, + actions = Actions + }; + return Json.From(obj); } - entry.Name = method.Name; - entry.Lower = entry.Name.ToLower(); + } + + #endregion + + #region static + + /// 从指定的程序集获取入口。 + public static ApiEntries From(Assembly assembly, bool replace = false) + { + if (assembly == null) return null; - // Caption - var captions = method.GetCustomAttributes(typeof(CaptionAttribute), true); - if (captions.Length > 0) + var apps = new List(); + var actions = new List(); + var types = assembly.GetExportedTypes(); + foreach (var type in types) { - var caption = (CaptionAttribute)captions[0]; - entry.Caption = caption.Title; - entry.Description = caption.Description; + apps.Add(ApiApplication.Parse(type, true)); + actions.AddRange(ApiAction.Parse(type)); } - // Hidden - entry.Hidden = application.Hidden; - if (!entry.Hidden && method.Contains(false)) entry.Hidden = true; + var entries = new ApiEntries(apps, actions, replace); + return entries; + } - return entry; + /// 从多个程序集中获取入口。 + public static ApiEntries From(IEnumerable assemblies, bool replace = false) + { + if (assemblies == null) return null; + var entries = new ApiEntries(); + foreach (var assembly in assemblies) entries.Add(From(assembly), replace); + return entries; } + /// 从当前程序中获取入口。 + public static ApiEntries Calling(bool replace = false) => From(Assembly.GetCallingAssembly(), replace); + + /// 从当前 AppDomain 中获取入口。 + public static ApiEntries AppDomain(bool replace = false) => From(System.AppDomain.CurrentDomain.GetAssemblies(), replace); + #endregion } diff --git a/Apewer/Web/ApiEntry.cs b/Apewer/Web/ApiEntry.cs deleted file mode 100644 index e065588..0000000 --- a/Apewer/Web/ApiEntry.cs +++ /dev/null @@ -1,200 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Reflection; -using System.Text; - -namespace Apewer.Web -{ - - /// API 入口。 - [Serializable] - public sealed class ApiEntry : IToJson - { - - #region Reflection - - internal Assembly _assembly = null; - internal Module _module = null; - internal Type _type = null; - internal MethodInfo _method = null; - - /// 定义当前入口的程序集。 - public Assembly Assembly { get => _assembly; } - - /// 定义当前入口的模块。 - public Module Module { get => _module; } - - /// 定义当前入口的类型。 - public Type Type { get => _type; } - - /// 定义当前入口的方法。 - public MethodInfo Method { get => _method; } - - #endregion - - #region Define - - internal CaptionAttribute _caption = null; - internal HiddenAttribute _hidden = null; - - internal RoutePrefixAttribute _prefix = null; - internal RouteAttribute _route = null; - internal string _path = null; - internal string _lower = null; - - /// 当前入口的路由前缀。 - public RoutePrefixAttribute RoutePrefix { get => _prefix; } - - /// 当前入口的路由。 - public RouteAttribute Route { get => _route; } - - /// 当前入口的标题。 - public CaptionAttribute Caption { get => _caption; } - - /// 当前入口的路由路径。 - public string Path { get => _path; } - - #endregion - - #region HTTP Method - - internal bool _restricted = false; - internal HttpConnectAttribute _connect = null; - internal HttpDeleteAttribute _delete = null; - internal HttpGetAttribute _get = null; - internal HttpHeadAttribute _head = null; - internal HttpOptionsAttribute _options = null; - internal HttpPatchAttribute _patch = null; - internal HttpPostAttribute _post = null; - internal HttpPutAttribute _put = null; - internal HttpTraceAttribute _trace = null; - - /// 当前入口拥有 特性。 - public HttpConnectAttribute Connect { get => _connect; } - - /// 当前入口拥有 特性。 - public HttpDeleteAttribute Delete { get => _delete; } - - /// 当前入口拥有 特性。 - public HttpGetAttribute Get { get => _get; } - - /// 当前入口拥有 特性。 - public HttpHeadAttribute Head { get => _head; } - - /// 当前入口拥有 特性。 - public HttpOptionsAttribute Options { get => _options; } - - /// 当前入口拥有 特性。 - public HttpPatchAttribute Patch { get => _patch; } - - /// 当前入口拥有 特性。 - public HttpPostAttribute Post { get => _post; } - - /// 当前入口拥有 特性。 - public HttpPutAttribute Put { get => _put; } - - /// 当前入口拥有 特性。 - public HttpTraceAttribute Trace { get => _trace; } - - #endregion - - private ApiEntry() { } - - internal static ApiEntry[] Parse(Type type) - { - return null; - } - - internal static ApiEntry Parse(Type type, MethodInfo method, RoutePrefixAttribute prefix, bool typeIndependent) - { - if (type == null) return null; - if (method == null) return null; - if (!method.IsPublic) return null; - if (method.IsGenericMethod) return null; - if (method.IsStatic) return null; - - var route = RuntimeUtility.GetAttribute(method, false); - var path = route == null ? null : Concat(prefix, route); - var lower = path == null ? null : path.ToLower(); - - var entry = new ApiEntry(); - - entry._assembly = type.Assembly; - entry._module = type.Module; - entry._type = type; - entry._method = method; - - entry._caption = RuntimeUtility.GetAttribute(method, true); - entry._hidden = RuntimeUtility.GetAttribute(method, true); - - entry._prefix = prefix; - entry._route = route; - entry._path = path; - entry._lower = lower; - - // 允许的 HTTP 方法,指定任何方法即表示需要限定方法。 - entry._connect = RuntimeUtility.GetAttribute(method, false); - entry._delete = RuntimeUtility.GetAttribute(method, false); - entry._get = RuntimeUtility.GetAttribute(method, false); - entry._head = RuntimeUtility.GetAttribute(method, false); - entry._options = RuntimeUtility.GetAttribute(method, false); - entry._patch = RuntimeUtility.GetAttribute(method, false); - entry._post = RuntimeUtility.GetAttribute(method, false); - entry._put = RuntimeUtility.GetAttribute(method, false); - entry._trace = RuntimeUtility.GetAttribute(method, false); - if (entry._get != null) entry._restricted = true; - else if (entry._post != null) entry._restricted = true; - else if (entry._options != null) entry._restricted = true; - else if (entry._connect != null) entry._restricted = true; - else if (entry._delete != null) entry._restricted = true; - else if (entry._head != null) entry._restricted = true; - else if (entry._patch != null) entry._restricted = true; - else if (entry._put != null) entry._restricted = true; - else if (entry._trace != null) entry._restricted = true; - - return entry; - } - - static string Concat(RoutePrefixAttribute prefix, RouteAttribute route) - { - var segs = new List(16); - - if (prefix != null && !string.IsNullOrEmpty(prefix.Path)) - { - var split = prefix.Path.Split('/'); - var count = split.Length; - for (var i = 0; i < count; i++) - { - var seg = split[i]; - if (string.IsNullOrEmpty(seg)) continue; - segs.Add(seg); - } - } - - if (route != null && !string.IsNullOrEmpty(route.Path)) - { - var split = route.Path.Split('/'); - var count = split.Length; - for (var i = 0; i < count; i++) - { - var seg = split[i]; - if (string.IsNullOrEmpty(seg)) continue; - segs.Add(seg); - } - } - - if (segs.Count < 1) return "/"; - return "/" + TextUtility.Join("/", segs.ToArray()); - } - - /// 生成包含当前实例属性的 Json 对象。 - /// Json 对象。 - public Json ToJson() - { - var json = Json.NewObject(); - return json; - } - - } - -} diff --git a/Apewer/Web/ApiFunction.cs b/Apewer/Web/ApiFunction.cs index bfd5849..885ca94 100644 --- a/Apewer/Web/ApiFunction.cs +++ b/Apewer/Web/ApiFunction.cs @@ -5,61 +5,157 @@ using System.Reflection; namespace Apewer.Web { - internal sealed class ApiFunction + /// + public sealed class ApiFunction : IToJson { - internal ApiApplication Application; - internal MethodInfo Method; - internal Type Returnable; - internal ParameterInfo[] Parameters; - internal bool ParamIsRecord = false; + #region fields - // 主特性和主要属性。 - // internal ApiAttribute Attribute; - internal string Name = null; - internal string Lower = null; + ApiApplication _application = null; + MethodInfo _method = null; + Type _return = null; + ApiParameter[] _parameters = null; - // 附加特性。 - internal bool Hidden; - internal string Caption; - internal string Description; + string _name = null; + string _lower = null; + string _caption = null; + string _description = null; - private Class psJson = null; + bool _hidden = false; - internal Json ToJson(ApiOptions options) + #endregion + + #region properties + + /// + public ApiApplication Application { get => _application; } + + /// 方法。 + public MethodInfo Method { get => _method; } + + /// 参数。 + public ApiParameter[] Parameters { get => _parameters.Map(x => x); } + + /// 返回的类型。 + public Type ReturnType { get => _return; } + + /// + public string Name { get => _name; } + + /// + public string Caption { get => _caption; } + + /// + public string Description { get => _description; } + + /// + public bool Hidden { get => _hidden; } + + #endregion + + /// + public Json ToJson() => ToJson(true); + + /// + public Json ToJson(ApiOptions options) => ToJson((options ?? new ApiOptions()).WithParameters); + + /// + public Json ToJson(bool withParameters) { if (Hidden) return null; + var json = Json.NewObject(); json.SetProperty("name", Name); if (!string.IsNullOrEmpty(Caption)) json.SetProperty("caption", Caption); if (!string.IsNullOrEmpty(Description)) json.SetProperty("description", Description); - if (options.WithParameters) + if (withParameters) json.SetProperty("parameters", Json.From(_parameters)); + return json; + } + + ApiFunction(MethodInfo method, ApiApplication application, ApiParameter[] parameters) + { + // 检查 ApiAttribute 特性。 + var apis = method.GetCustomAttributes(typeof(ApiAttribute), false); + var api = apis.Length > 0 ? (ApiAttribute)apis[0] : null; + + // Entry + _application = application; + _method = method; + + // 返回值。 + _return = method.ReturnType; + if (_return.Equals(typeof(void))) _return = null; + + // api + if (api == null) { - if (psJson == null) + _name = method?.Name; + _lower = _name?.Lower(); + } + else + { + _name = string.IsNullOrEmpty(api.Name) ? method?.Name : api.Name; + _lower = _name?.Lower(); + _caption = api.Caption; + _description = api.Description; + } + + // caption + if (string.IsNullOrEmpty(_caption)) + { + var captions = method.GetCustomAttributes(typeof(CaptionAttribute), true); + if (captions.Length > 0) { - if (Parameters == null || Parameters.Length < 0) - { - psJson = new Class(); - } - else + var caption = (CaptionAttribute)captions[0]; + _caption = caption.Title; + if (string.IsNullOrEmpty(_description)) { - var ps = Json.NewArray(); - var psList = new List(); - psList.AddRange(Parameters); - psList.Sort(new Comparison((a, b) => a.Name.CompareTo(b.Name))); - foreach (var pi in psList) - { - var p = Json.NewObject(); - p.SetProperty("name", pi.Name); - p.SetProperty("type", pi.ParameterType.Name); - ps.AddItem(p); - } - psJson = new Class(ps); + _description = caption.Description; } } - if (psJson) json.SetProperty("parameters", psJson.Value); } - return json; + + // hidden + if (method.Contains(false)) _hidden = true; + + // 参数。 + _parameters = parameters; + } + + /// + public static ApiFunction Parse(ApiApplication application, MethodInfo method) + { + if (application == null) return null; + if (method == null) return null; + + // 滤除构造函数、抽象方法、泛型和非本类定义方法。 + if (method.IsConstructor) return null; + if (method.IsAbstract) return null; + if (method.GetGenericArguments().NotEmpty()) return null; + + // 滤除 get 和 set 访问器。 + var methodName = method.Name; + if (methodName.StartsWith("get_") || methodName.StartsWith("set_")) return null; + switch (methodName) + { + case "Dispose": + return null; + } + + // 定义者。 + var declaring = method.DeclaringType; + if (declaring.Equals(typeof(object))) return null; + + // 参数,所有参数必须是 In 方向。 + var ps = new List(); + foreach (var pi in method.GetParameters()) + { + var p = ApiParameter.Parse(pi); + if (p == null) return null; + ps.Add(p); + } + + return new ApiFunction(method, application, ps.ToArray()); } } diff --git a/Apewer/Web/ApiInvoker.cs b/Apewer/Web/ApiInvoker.cs index 50c1cdf..b357223 100644 --- a/Apewer/Web/ApiInvoker.cs +++ b/Apewer/Web/ApiInvoker.cs @@ -51,23 +51,27 @@ namespace Apewer.Web } /// 发起调用。 - public string Invoke(ApiProvider provider) + /// + public void Invoke(ApiProvider provider) { + if (provider == null) throw new ArgumentNullException(nameof(provider)); + var entries = Entries; if (entries == null) entries = ApiEntries.AppDomain(); Entries = entries; - return Invoke(provider, entries); + Invoke(provider, entries); } /// 发起调用。 - public string Invoke(ApiProvider provider, ApiEntries entries) + /// + public void Invoke(ApiProvider provider, ApiEntries entries) { - if (provider == null) return "未指定有效的服务程序。"; - if (entries == null) return "未指定有效的入口。"; + if (provider == null) throw new ArgumentNullException(nameof(provider)); + if (entries == null) throw new ArgumentNullException(nameof(entries)); var context = new ApiContext(this, provider, entries); var processor = new ApiProcessor(context); - return processor.Run(); + processor.Run(); } } diff --git a/Apewer/Web/ApiMiddleware.cs b/Apewer/Web/ApiMiddleware.cs deleted file mode 100644 index f79fc27..0000000 --- a/Apewer/Web/ApiMiddleware.cs +++ /dev/null @@ -1,18 +0,0 @@ -#if Middleware - -using System; -using System.Collections.Generic; -using System.Text; - -namespace Apewer.Web -{ - - /// 中间件。 - public abstract class ApiMiddleware - { - - } - -} - -#endif diff --git a/Apewer/Web/ApiModel.cs b/Apewer/Web/ApiModel.cs index 40b7813..ae3e16a 100644 --- a/Apewer/Web/ApiModel.cs +++ b/Apewer/Web/ApiModel.cs @@ -8,19 +8,15 @@ namespace Apewer.Web { /// Response 模型。 - public abstract class ApiModel : IToJson + public abstract class ApiModel : IApiModel { - #region + #region Headers - private int _expires = 0; + int _expires = 0; + StringPairs _headers = new StringPairs(); - internal ApiRequest _request; - internal ApiResponse _response; - internal ApiOptions _options; - internal ApiProvider _provider; - - static int SafeExpires(int seconds) + int SafeExpires(int seconds) { var s = seconds; if (s < 0) s = 0; @@ -28,38 +24,37 @@ namespace Apewer.Web return s; } - #endregion + /// 响应缓存的过期时间,以秒为单位。 + public virtual int Expires { get => _expires; set => _expires = SafeExpires(value); } - #region 内部属性和方法。 + /// 状态。 + /// 默认值:200。 + public virtual int Status { get; set; } - /// 处理当前模型的 API 请求。 - protected ApiRequest Request { get => _request; } + /// 设置 Response 头。 + public virtual StringPairs Headers { get => _headers; set => _headers = value ?? new StringPairs(); } - /// 处理当前模型的 API 响应。 - protected ApiResponse Response { get => Response; } + /// 内容类型。当 Headers 中包含 Content-Type 时此属性将被忽略。 + public virtual string ContentType { get; set; } - /// 处理当前模型的 API 选项。 - protected ApiOptions Options { get => _options; } + /// 设置文件名,告知客户端此附件处理此响应。 + public virtual string Attachment { get; set; } - /// 处理当前模型的服务程序实例。 - protected ApiProvider Provider { get => _provider; } + #endregion - /// 在 Response 头中添加用于设置文件名的属性。 - protected void SetAttachment() - { - if (_provider == null) return; - var name = Attachment; - if (string.IsNullOrEmpty(name)) return; - var encoded = TextUtility.EncodeUrl(name); - _provider.SetHeader("Content-Disposition", $"attachment; filename={encoded}"); - } + #region Output - private List WriteHeader() - { - if (_provider == null) return null; + /// 执行输出。 + /// 此方法由 API 调用器发起调用,用户程序不应主动调用。 + /// + public abstract void Output(ApiContext context); + /// 向 HTTP 写入头。 + /// 已写入的头。 + List WriteHeaders(ApiContext context) + { var status = Status > 0 ? Status : 200; - if (status != 200) _provider.SetStatus(status); + if (status != 200) context.Provider.SetStatus(status); var headers = Headers; var added = new List(32); @@ -69,69 +64,82 @@ namespace Apewer.Web { if (header.Key.IsEmpty()) continue; if (header.Value.IsEmpty()) continue; - _provider.SetHeader(header.Key, header.Value); + context.Provider.SetHeader(header.Key, header.Value); added.Add(header.Key.Lower()); } } - SetAttachment(); - _provider.SetCache(Expires); - if (!added.Contains("content-type")) _provider.SetContentType(ContentType); + SetAttachment(context); + context.Provider.SetCache(Expires); + if (!added.Contains("content-type")) context.Provider.SetContentType(ContentType); return added; } - /// 以指定参数输出。 - protected void Output(byte[] bytes) + /// 在 Response 头中添加用于设置文件名的属性。 + void SetAttachment(ApiContext context) { - if (_provider == null) return; - if (_provider.PreWrite().NotEmpty()) return; - var added = WriteHeader(); + var name = Attachment; + if (string.IsNullOrEmpty(name)) return; + var encoded = TextUtility.EncodeUrl(name); + context.Provider.SetHeader("Content-Disposition", $"attachment; filename={encoded}"); + } + /// 输出头和响应体,响应体是字节数组。 + /// + protected void Output(ApiContext context, byte[] bytes) + { + if (context == null) throw new ArgumentNullException(nameof(context)); + + // 写入头 + if (context.Provider == null) return; + if (context.Provider.PreWrite().NotEmpty()) return; + var added = WriteHeaders(context); + + // 写入头 var length = bytes == null ? 0 : bytes.Length; - if (!added.Contains("content-length")) _provider.SetContentLength(length); - if (length > 0) _provider.ResponseBody().Write(bytes); - _provider.Sent(); + if (!added.Contains("content-length")) context.Provider.SetContentLength(length); + + // 写入主体 + if (length > 0) context.Provider.ResponseBody().Write(bytes); + + // 发送 + context.Provider.Sent(); } /// 以指定参数输出。 - protected void Output(Stream stream, bool dispose) + /// + protected void Output(ApiContext context, Stream stream) { - if (_provider == null) return; - if (_provider.PreWrite().NotEmpty()) return; - var added = WriteHeader(); + if (context == null) throw new ArgumentNullException(nameof(context)); - if (!added.Contains("content-length")) + // 写入头 + if (context.Provider == null) return; + if (context.Provider.PreWrite().NotEmpty()) return; + var added = WriteHeaders(context); + + if (stream == null) { - var length = stream.Length - stream.Position; - _provider.SetContentLength(length); + context.Provider.SetContentLength(0); + context.Provider.Sent(); } - _provider.ResponseBody().Write(stream); - _provider.Sent(); - if (dispose) RuntimeUtility.Dispose(stream); - } - - #endregion - - /// 状态。 - /// 默认值:200。 - public virtual int Status { get; set; } - - /// 内容类型。当 Headers 中包含 Content-Type 时此属性将被忽略。 - public virtual string ContentType { get; set; } - - /// 响应缓存的过期时间,以秒为单位。 - public virtual int Expires { get => _expires; set => _expires = SafeExpires(value); } + else + { + // 写入头 + if (!added.Contains("content-length")) + { + var length = stream.Length - stream.Position; + context.Provider.SetContentLength(length); + } - /// 设置文件名,告知客户端此附件处理此响应。 - public virtual string Attachment { get; set; } + // 写入主体 + context.Provider.ResponseBody().Write(stream); - /// 设置 Response 头。 - public virtual StringPairs Headers { get; set; } + // 发送 + context.Provider.Sent(); + } + } - /// 执行输出。 - /// 此方法由 API 调用器发起调用,用户程序不应主动调用。 - /// - public abstract void Output(); + #endregion /// 创建对象实例,并设置默认属性。 public ApiModel() @@ -143,15 +151,6 @@ namespace Apewer.Web Headers = new StringPairs(); } - /// - public Json ToJson() - { - var json = new Json(); - json.SetProperty("status", Status); - json.SetProperty("content-type", ContentType); - return json; - } - } /// 输出二进制的 Response 模型。 @@ -162,7 +161,7 @@ namespace Apewer.Web public byte[] Bytes { get; set; } /// 输出字节数组。 - public override void Output() => Output(Bytes); + public override void Output(ApiContext context) => Output(context, Bytes); /// 创建对象实例,并设置默认属性。 public ApiBytesModel(byte[] bytes = null, string contentType = "application/octet-stream") @@ -185,7 +184,17 @@ namespace Apewer.Web public bool AutoDispose { get; set; } /// 输出流。 - public override void Output() => Output(Stream, AutoDispose); + public override void Output(ApiContext context) + { + if (AutoDispose) + { + using (var stream = Stream) Output(context, Stream); + } + else + { + Output(context, Stream); + } + } /// 当指定 AutoDispose 属性时释放流。 public void Dispose() @@ -220,21 +229,17 @@ namespace Apewer.Web public string Path { get => _path; set => SetPath(value); } /// 输出指定路径的文件。 - public override void Output() + public override void Output(ApiContext context) { - try - { - if (!File.Exists(Path)) return; + if (!File.Exists(Path)) return; - var info = new FileInfo(Path); - if (string.IsNullOrEmpty(Attachment)) Attachment = info.Name; + var info = new FileInfo(Path); + if (string.IsNullOrEmpty(Attachment)) Attachment = info.Name; - using (var stream = new FileStream(Path, FileMode.Open, FileAccess.Read, FileShare.Read)) - { - Output(stream, false); - } + using (var stream = new FileStream(Path, FileMode.Open, FileAccess.Read, FileShare.Read)) + { + Output(context, stream); } - catch { } } /// @@ -254,7 +259,7 @@ namespace Apewer.Web public string Text { get; set; } /// 输出文本。 - public override void Output() => Output(TextUtility.Bytes(Text)); + public override void Output(ApiContext context) => Output(context, TextUtility.Bytes(Text)); /// 创建对象实例,并设置默认属性。 public ApiTextModel(string text = null, string contentType = "text/plain") @@ -281,15 +286,15 @@ namespace Apewer.Web public bool Camel { get; set; } /// 输出文本。 - public override void Output() + public override void Output(ApiContext context) { var json = (Json != null && Json.Available) ? Json : Json.NewObject(); if (Camel) Json.Camel(json); - Output(TextUtility.Bytes(json.ToString(Indented))); + Output(context, TextUtility.Bytes(json.ToString(Indented))); } /// 创建对象实例,并设置默认属性。 - public ApiJsonModel(Json json = null, bool camel = false, bool indented = true) + public ApiJsonModel(Json json = null, bool indented = false, bool camel = true) { ContentType = "application/json"; Camel = camel; @@ -304,20 +309,21 @@ namespace Apewer.Web { /// 将要重定向的位置。 - public string Location { get; set; } + public string Location { get; private set; } /// 执行重定向。 - public override void Output() + public override void Output(ApiContext context) { var location = Location; if (string.IsNullOrEmpty(location)) return; - if (Provider == null) return; - Provider.SetRedirect(Location); + context.Provider.SetRedirect(Location); } /// 重定向到指定的 URL。 - public ApiRedirectModel(string location = null) + /// + public ApiRedirectModel(string location) { + if (location.IsEmpty()) throw new ArgumentNullException(nameof(location)); Location = location; } @@ -331,23 +337,24 @@ namespace Apewer.Web public Exception Exception { get; set; } /// 解析 Exception 的内容并输出。 - public override void Output() + public override void Output(ApiContext context) { Status = 500; ContentType = "text/plain"; - Output(ToString().Bytes()); + Output(context, Format(Exception).Bytes()); } /// - public ApiExceptionModel(Exception exception = null) + /// + public ApiExceptionModel(Exception exception) { + if (exception == null) throw new ArgumentNullException(nameof(exception)); Exception = exception; } /// - public override string ToString() + static string Format(Exception ex) { - var ex = Exception; var sb = new StringBuilder(); if (ex == null) { @@ -357,7 +364,7 @@ namespace Apewer.Web { try { - sb.Append(Exception.GetType().FullName); + sb.Append(ex.GetType().FullName); var props = ex.GetType().GetProperties(); foreach (var prop in props) @@ -399,7 +406,7 @@ namespace Apewer.Web public byte[] Bytes { get; set; } /// 执行重定向。 - public override void Output() => Output(Bytes); + public override void Output(ApiContext context) => Output(context, Bytes); /// public ApiStatusModel(int status = 200) => Status = status; diff --git a/Apewer/Web/ApiOptions.cs b/Apewer/Web/ApiOptions.cs index d1a3811..6208d50 100644 --- a/Apewer/Web/ApiOptions.cs +++ b/Apewer/Web/ApiOptions.cs @@ -38,6 +38,10 @@ namespace Apewer.Web // /// // public bool AllowSynchronousIO { get; set; } = true; + /// 默认的结果渲染器。 + /// 默认值:NULL + public Action DefaultRenderer { get; set; } + /// 允许输出的 Json 对象缩进。 /// 默认值:不缩进。 public bool JsonIndent { get; set; } = false; @@ -49,10 +53,22 @@ namespace Apewer.Web /// 输出前的检查。 public ApiPreOutput PreOutput { get; set; } + /// 默认的结果渲染器。 + /// 默认值: + public Action TextRenderer { get; set; } = ApiUtility.Error; + /// 在响应头中设置 Content-Security-Policy,要求浏览器升级资源链接,使用 HTTPS。 /// 默认值:不要求。在 HTTPS 页面中,不自动升级 HTTP 资源。 public bool UpgradeHttps { get; set; } = false; + /// 使用反射。 + /// 默认值:使用 + public bool UseReflection { get; set; } = true; + + /// 使用路由。 + /// 默认值:使用 + public bool UseRoute { get; set; } = true; + /// 在响应中包含 Access-Control 属性。 /// 默认值:不包含。 public bool WithAccessControl { get; set; } = false; diff --git a/Apewer/Web/ApiParameter.cs b/Apewer/Web/ApiParameter.cs new file mode 100644 index 0000000..8944391 --- /dev/null +++ b/Apewer/Web/ApiParameter.cs @@ -0,0 +1,77 @@ +using System; +using System.Collections.Generic; +using System.Reflection; +using System.Text; + +namespace Apewer.Web +{ + + /// API 行为的参数。 + public sealed class ApiParameter : IToJson + { + + ParameterInfo _parameter = null; + bool _body = false; + bool _query = false; + + /// + public ParameterInfo ParameterInfo { get => _parameter; } + + /// + public string Name { get => _parameter.Name; } + + /// + public Type Type { get => _parameter.ParameterType; } + + /// + public bool FromBody { get => _body; } + + /// + public bool FromQuery { get => _query; } + + /// + public override string ToString() => _parameter.Name; + + /// + public Json ToJson() + { + var format = ApiActionJsonFormat.Default ?? new ApiActionJsonFormat(); + var withReflection = format.WithReflection; + return ToJson(withReflection); + } + + /// + public Json ToJson(bool withReflection) + { + var json = Json.NewObject(); + json.SetProperty("name", _parameter.Name); + if (withReflection) + { + json.SetProperty("type", _parameter.ParameterType.Name); + } + if (_body) json.SetProperty("fromBody", _body); + if (_query) json.SetProperty("fromQuery", _query); + return json; + } + + /// + /// + ApiParameter(ParameterInfo parameterInfo) + { + _parameter = parameterInfo ?? throw new ArgumentNullException(nameof(parameterInfo)); + _query = RuntimeUtility.Contains(parameterInfo); + _body = RuntimeUtility.Contains(parameterInfo); + } + + /// + public static ApiParameter Parse(ParameterInfo parameter) + { + if (parameter == null) return null; + if (parameter.IsOut) return null; + if (parameter.IsRetval) return null; + return new ApiParameter(parameter); + } + + } + +} diff --git a/Apewer/Web/ApiProcessor.cs b/Apewer/Web/ApiProcessor.cs index 01f6034..97567a3 100644 --- a/Apewer/Web/ApiProcessor.cs +++ b/Apewer/Web/ApiProcessor.cs @@ -2,7 +2,7 @@ using Apewer.Source; using System; using System.Net; - +using System.Reflection; using static Apewer.Web.ApiUtility; namespace Apewer.Web @@ -11,67 +11,64 @@ namespace Apewer.Web internal class ApiProcessor { - // in private ApiContext _context = null; - // temp - // private Uri _url = null; - // private HttpMethod _method = HttpMethod.NULL; - - // out - // private ApiEntry _entry = null; - // private ApiRequest _request = null; - // private ApiResponse _response = null; - // private ApiController _controller = null; + internal ApiProcessor(ApiContext context) => _context = context ?? throw new ArgumentNullException(nameof(context)); - internal ApiProcessor(ApiContext context) - { - if (context == null) throw new ArgumentNullException(nameof(context)); - _context = context; - } + #region prepare /// 执行处理程序,返回错误信息。 - public string Run() + public void Run() { - var error = Flow(); - return error; - } + var url = null as Uri; + var method = HttpMethod.NULL; + var response = null as ApiResponse; - string Flow() - { try { // 检查执行的前提条件,获取 Method 和 URL。 - Uri url = null; - HttpMethod method = HttpMethod.NULL; var check = Check(ref method, ref url); - if (!string.IsNullOrEmpty(check)) return check; + if (!string.IsNullOrEmpty(check)) + { + Logger.Internals.Error(typeof(ApiInvoker), check); + return; + } // 准备请求模型。 var request = GetRequest(_context.Provider, _context.Options, method, url); _context.Request = request; // 准备响应模型。 - var response = new ApiResponse(); + response = new ApiResponse(); response.Random = request.Random; response.Application = request.Application; response.Function = request.Function; _context.Response = response; // 调用 API。 - var invoke = Invoke(); - if (!string.IsNullOrEmpty(invoke)) return invoke; - - // 输出。 - response.Duration = Duration(_context.Beginning); - Output(_context.Provider, _context.Options, response, request, method); - return null; + Invoke(); } catch (Exception ex) { var message = ex.Message(); Logger.Internals.Error(typeof(ApiInvoker), message); - return message; + } + finally + { + // 输出。 + if (response != null) + { + try + { + response.Duration = Duration(_context.Beginning); + Output(_context.Provider, _context.Options, response, null, method); + } + catch { } + finally + { + RuntimeUtility.Dispose(response.Model); + } + } } } @@ -79,18 +76,10 @@ namespace Apewer.Web { var span = DateTime.Now - beginning; var ms = span.TotalMilliseconds; - if (ms > 0D) - { - var s = span.TotalMilliseconds / 1000D; - if (s > 10D) return Math.Round(s, 1).ToString() + "s"; - if (s > 1D) return Math.Round(s, 2).ToString() + "s"; - if (ms > 10D) return Math.Round(ms, 0).ToString() + "ms"; - return Math.Round(ms, 1).ToString() + "ms"; - } - else - { - return null; - } + if (ms < 1000) return Math.Round(ms, 0).ToString() + "ms"; + if (ms < 10000) return Math.Round(ms / 1000, 2).ToString() + "s"; + if (ms < 60000) return Math.Round(ms / 1000, 1).ToString() + "s"; + return Math.Round(ms / 1000, 0).ToString() + "s"; } string Check(ref HttpMethod method, ref Uri url) @@ -133,18 +122,171 @@ namespace Apewer.Web } // 寻找入口。 - string Invoke() + void Invoke() { - var appName = _context.Request.Application; - var application = _context.Entries.Get(appName); - Invoke(application); + // 路由 + if (_context.Options.UseRoute) + { + var path = _context?.Request?.Url?.AbsolutePath; + var action = _context.Entries.GetAction(path); + if (action != null) + { + Invoke(action); + _context.Response.Duration = Duration(_context.Beginning); + return; + } + } - var duration = Duration(_context.Beginning); - _context.Response.Duration = duration; + // 反射 + if (_context.Options.UseReflection) + { + var appName = _context.Request.Application; + var application = _context.Entries.GetApplication(appName); + Invoke(application); + _context.Response.Duration = Duration(_context.Beginning); + return; + } - return null; + // 未匹配到 + _context.Response.Duration = Duration(_context.Beginning); + _context.Response.Model = new ApiStatusModel(404); + } + + #endregion + + #region common + + // 创建控制器实例 + static ApiController CreateController(Type type, ApiRequest request, ApiResponse response, ApiOptions options) + { + var controller = (ApiController)Activator.CreateInstance(type); + ApiUtility.SetProperties(controller, request, response, options); + return controller; + } + + static void Invoke(ApiContext context, MethodInfo method, ApiParameter[] parameters) + { + context.MethodInfo = method; + + // 调用。 + var parametersValue = ReadParameters(context.Request, parameters); + var controller = context.Controller; + var returnValue = method.Invoke(controller, parametersValue); + + // 程序要求停止输出。 + var response = context.Response; + if (response.StopReturn) return; + + // 已经有了返回模型。 + if (response.Model != null) return; + + // 没有返回类型。 + var returnType = method.ReturnType; + if (returnType == null) return; + + // 已明确字符串类型。 + if (returnType.Equals(typeof(string))) + { + var textValue = returnValue as string; + var textRenderer = context.Options.TextRenderer; + if (textRenderer != null) + { + textRenderer.Invoke(context, textValue); + return; + } + + // 默认视为提示错误 + if (!string.IsNullOrEmpty(textValue)) response.Error(textValue); + return; + } + + // 已明确 Exception 类型,视为提示错误。 + if (returnValue is Exception) + { + ApiUtility.Exception(response, returnValue as Exception); + return; + } + + // 已明确 Json 类型。 + if (returnValue is Json json) + { + response.Data = json; + return; + } + + // 已明确 Model 类型。 + if (returnValue is IApiModel model) + { + response.Model = model; + return; + } + + // 已明确 Result 类型。 + if (returnValue is IActionResult result) + { + response.Model = result; + return; + } + + // 类型未知,尝试 ToJson 方法。 + if (returnValue is IToJson toJson) + { + response.Data = toJson.ToJson(); + return; + } + + // 未知返回类型,尝试使用默认渲染器。 + var defaultRenderer = context.Options.DefaultRenderer; + if (defaultRenderer != null) defaultRenderer.Invoke(context, returnValue); + } + + #endregion + + #region route + + // 执行 Action。 + void Invoke(ApiAction action) + { + var controller = null as ApiController; + try + { + // 准备控制器。 + controller = CreateController(action.Type, _context.Request, _context.Response, _context.Options); + + // 准备参数。 + var parameters = action.Parameters; + var values = ReadParameters(_context.Request, parameters); + + // 调用。 + _context.Controller = controller; + Invoke(_context, action.MethodInfo, action.Parameters); + } + catch (Exception ex) + { + if (ex.InnerException != null) ex = ex.InnerException; + ApiUtility.Exception(_context.Response, ex, _context.Options.WithException); + + var catcher = _context.Invoker.Catcher; + if (catcher != null) + { + try + { + var apiCatch = new ApiCatch(_context, ex); + catcher.Invoke(apiCatch); + } + catch { } + } + } + finally + { + RuntimeUtility.Dispose(controller); + } } + #endregion + + #region reflection + // 创建控制器。 void Invoke(ApiApplication application) { @@ -153,9 +295,6 @@ namespace Apewer.Web var request = _context.Request; var response = _context.Response; - var function = null as ApiFunction; - var controller = null as ApiController; - // Application 无效,尝试默认控制器和枚举。 if (application == null) { @@ -163,40 +302,54 @@ namespace Apewer.Web if (@default == null) { // 没有指定默认控制器,尝试枚举。 - response.Error("Invalid Application"); - if (options.AllowEnumerate) response.Data = Enumerate(entries.Enumerate(), options); + response.Status = "notfound"; + response.Message = "Not Found"; + if (options.AllowEnumerate) response.Data = Enumerate(entries.Applications, options); return; } else { // 创建默认控制器。 - try { controller = CreateController(@default, request, response, options); } - catch (Exception ex) { ApiUtility.Exception(response, ex.InnerException); } + var controller = null as ApiController; + try + { + controller = CreateController(@default, request, response, options); + Invoke(controller, application, null, options, request, response); + } + catch (Exception ex) + { + ApiUtility.Exception(response, ex.InnerException ?? ex); + } + finally + { + RuntimeUtility.Dispose(controller); + } } } else { // 创建控制器时候会填充 Controller.Request 属性,可能导致 Request.Function 被篡改,所以在创建之前获取 Function。 - function = application.Get(request.Function); - try { controller = CreateController(application.Type, request, response, options); } - catch (Exception ex) { ApiUtility.Exception(response, ex.InnerException); } + var function = application.GetFunction(request.Function); + var controller = null as ApiController; + try + { + controller = CreateController(application.Type, request, response, options); + Invoke(controller, application, function, options, request, response); + } + catch (Exception ex) + { + ApiUtility.Exception(response, ex.InnerException ?? ex); + } + finally + { + RuntimeUtility.Dispose(controller); + } } - if (controller == null) response.Error("创建控制器实例失败。"); - else Invoke(controller, application, function, options, request, response); - RuntimeUtility.Dispose(controller); } // 调用 Function。 void Invoke(ApiController controller, ApiApplication application, ApiFunction function, ApiOptions options, ApiRequest request, ApiResponse response) { - // 没有 ApiApplication,使用了 Options 中指定的默认控制器。 - if (application == null) - { - application = new ApiApplication(); - application.Independent = true; - application.Hidden = true; - } - try { // 控制器初始化。 @@ -208,67 +361,8 @@ namespace Apewer.Web if (function != null) { // 调用 API,获取返回值。 - var result = function.Method.Invoke(controller, ReadParameters(request, function)); - if (response.StopReturn) return; - - // 检查返回值。 - if (result == null || function.Returnable == null) return; - var returnable = function.Returnable; - - // 已明确字符串类型,视为提示错误。 - if (returnable.Equals(typeof(string))) - { - var error = result as string; - if (!string.IsNullOrEmpty(error)) response.Error(error); - return; - } - - // 已明确 Exception 类型,视为提示错误。 - if (result is Exception) - { - ApiUtility.Exception(response, result as Exception); - return; - } - - // 已明确 Json 类型。 - if (result is Json) - { - response.Data = result as Json; - return; - } - - // 已明确 Model 类型。 - if (result is ApiModel) - { - response.Model = result as ApiModel; - return; - } - - // 类型未知,尝试 ToJson 方法。 - var tojson = result as IToJson; - if (tojson != null) - { - response.Data = tojson.ToJson(); - return; - } - - // 类型未知,尝试 Record 模型。 - var record = result as IRecord; - if (record != null) - { - response.Data = Json.From(record); - return; - } - - // 未知类型,尝试 Json 类型。 - var json = result as Json; - if (json != null) - { - response.Data = json; - return; - } - - // 未知返回类型,无法明确输出格式,忽略。 + _context.Controller = controller; + Invoke(_context, function.Method, function.Parameters); } else { @@ -281,44 +375,37 @@ namespace Apewer.Web } // 没有执行任何 Function,尝试枚举。 + response.Status = "notfound"; if (application.Hidden) { - response.Error("Invalid Application"); + response.Message = "Not Found"; } else { - response.Error("Invalid Function"); - if (options.AllowEnumerate) response.Data = Enumerate(application.Items, options); + response.Message = "Not Found"; + if (options.AllowEnumerate) response.Data = Enumerate(application.Functions, options); } } } - catch (Exception exception) + catch (Exception ex) { - var ex = exception.InnerException; + if (ex.InnerException != null) ex = ex.InnerException; + ApiUtility.Exception(_context.Response, ex, _context.Options.WithException); var catcher = _context.Invoker.Catcher; if (catcher != null) { - ApiUtility.Exception(response, ex, false); try { - var apiCatch = new ApiCatch(controller, options, ex); + var apiCatch = new ApiCatch(_context, ex); catcher.Invoke(apiCatch); } catch { } - return; } - - ApiUtility.Exception(response, ex); } } - static ApiController CreateController(Type type, ApiRequest request, ApiResponse response, ApiOptions options) - { - var controller = (ApiController)Activator.CreateInstance(type); - ApiUtility.SetProperties(controller, request, response, options); - return controller; - } + #endregion #region static @@ -332,7 +419,7 @@ namespace Apewer.Web // 基本信息。 var ip = provider.GetClientIP(); - var headers = provider.GetHeaders() ?? new StringPairs(); + var headers = provider.GetHeaders() ?? new HttpHeaders(); request.Headers = headers; request.IP = ip; request.Url = url; @@ -352,58 +439,62 @@ namespace Apewer.Web var page = null as string; // 解析 POST 请求。 - if (request.Method == HttpMethod.POST) + switch (request.Method) { - var preRead = provider.PreRead(); - if (string.IsNullOrEmpty(preRead)) - { - var post = null as byte[]; - var length = 0L; - var max = options.MaxRequestBody; - if (max == 0) post = new byte[0]; - else if (max < 0) post = provider.RequestBody().Read(); - else - { - length = provider.GetContentLength(); - if (length <= max) post = provider.RequestBody().Read(); - } - - length = post == null ? 0 : post.Length; - if (length > 1) + case HttpMethod.PATCH: + case HttpMethod.POST: + case HttpMethod.PUT: + var preRead = provider.PreRead(); + if (string.IsNullOrEmpty(preRead)) { - request.PostData = post; - if (length < 104857600) + var post = null as byte[]; + var length = 0L; + var max = options.MaxRequestBody; + if (max == 0) post = new byte[0]; + else if (max < 0) post = provider.RequestBody().Read(); + else { - var text = TextUtility.FromBytes(post); - request.PostText = text; + length = provider.GetContentLength(); + if (length <= max) post = provider.RequestBody().Read(); + } - // 尝试解析 Json,首尾必须是“{}”或“[]”。 - var first = post[0]; - var last = post[length - 1]; - if ((first == 123 && last == 125) || (first == 91 && last == 93)) + length = post == null ? 0 : post.Length; + if (length > 1) + { + request.PostData = post; + if (length < 104857600) { - var json = Json.From(text); - if (json != null && json.IsObject) + var text = TextUtility.FromBytes(post); + request.PostText = text; + + // 尝试解析 Json,首尾必须是“{}”或“[]”。 + var first = post[0]; + var last = post[length - 1]; + if ((first == 123 && last == 125) || (first == 91 && last == 93)) { - application = json["application"]; - function = json["function"]; - random = json["random"]; - ticket = json["ticket"]; - session = json["session"]; - page = json["page"]; - - var data = json.GetProperty("data"); - request.PostJson = json; - request.Data = data ?? Json.NewObject(); + var json = Json.From(text); + if (json != null && json.IsObject) + { + application = json["application"]; + function = json["function"]; + random = json["random"]; + ticket = json["ticket"]; + session = json["session"]; + page = json["page"]; + + var data = json.GetProperty("data"); + request.PostJson = json; + request.Data = data ?? Json.NewObject(); + } } - } - // 尝试解析 Form,需要 application/x-www-form-urlencoded - var contentType = headers.GetValue("content-type", true) ?? ""; - if (contentType.Contains("urlencoded")) request.Form = ApiUtility.Parameters(text); + // 尝试解析 Form,需要 application/x-www-form-urlencoded + var contentType = headers.GetValue("Content-Type") ?? ""; + if (contentType.Contains("urlencoded")) request.Form = ApiUtility.Parameters(text); + } } } - } + break; } // 解析 URL 参数。 @@ -501,7 +592,7 @@ namespace Apewer.Web { foreach (var header in headers) { - var key = TextUtility.Trim(header.Key); + var key = TextUtility.Trim(header.Name); if (string.IsNullOrEmpty(key)) continue; var value = header.Value; if (string.IsNullOrEmpty(value)) continue; @@ -512,8 +603,6 @@ namespace Apewer.Web return merged; } - - internal void Output(ApiProvider provider, ApiOptions options, ApiResponse response, string type, byte[] bytes) { var preWrite = provider.PreWrite(); @@ -587,15 +676,36 @@ namespace Apewer.Web // 设置头。 var headers = PrepareHeaders(options, response, request); foreach (var header in headers) provider.SetHeader(header.Key, header.Value); - var model = response.Model; + + // 自定义模型 + var model = response.Model as IApiModel; + var result = response.Model as IActionResult; if (model != null) { - ApiUtility.Initialize(model, request, response, options, provider); - try { model.Output(); } - catch (Exception ex) { Logger.Internals.Exception(model, ex); } + try + { + model.Output(_context); + } + catch (Exception ex) + { + Logger.Internals.Exception(model, ex); + } RuntimeUtility.Dispose(model); return; } + else if (result != null) + { + try + { + result.ExecuteResult(_context); + } + catch (Exception ex) + { + Logger.Internals.Exception(result, ex); + } + RuntimeUtility.Dispose(result); + return; + } var text = ApiUtility.ToJson(response, options); var bytes = TextUtility.Bytes(text); diff --git a/Apewer/Web/ApiProvider.cs b/Apewer/Web/ApiProvider.cs index 452277b..c58e5b1 100644 --- a/Apewer/Web/ApiProvider.cs +++ b/Apewer/Web/ApiProvider.cs @@ -51,7 +51,7 @@ namespace Apewer.Web public abstract string GetReferrer(); /// 获取请求的头。 - public abstract StringPairs GetHeaders(); + public abstract HttpHeaders GetHeaders(); /// 获取请求的内容类型。 public abstract string GetContentType(); @@ -91,4 +91,13 @@ namespace Apewer.Web } + /// API 服务程序。 + public abstract class ApiProvider : ApiProvider + { + + /// HttpContext + public abstract TContext Context { get; } + + } + } diff --git a/Apewer/Web/ApiRequest.cs b/Apewer/Web/ApiRequest.cs index e12e151..8fcbc3c 100644 --- a/Apewer/Web/ApiRequest.cs +++ b/Apewer/Web/ApiRequest.cs @@ -14,6 +14,9 @@ namespace Apewer.Web private Json _data = null; internal string[] _segmentals = null; + /// 自定义标签。 + public object Tag { get; set; } + #region http /// 客户端 IP 地址。 @@ -35,7 +38,7 @@ namespace Apewer.Web public StringPairs Parameters { get; set; } = new StringPairs(); /// HTTP 头。 - public StringPairs Headers { get; set; } = new StringPairs(); + public HttpHeaders Headers { get; set; } = new HttpHeaders(); /// Cookies。 public CookieCollection Cookies { get; set; } = new CookieCollection(); diff --git a/Apewer/Web/ApiResponse.cs b/Apewer/Web/ApiResponse.cs index 7c8af58..2bd0bdd 100644 --- a/Apewer/Web/ApiResponse.cs +++ b/Apewer/Web/ApiResponse.cs @@ -1,4 +1,5 @@ using Apewer.Models; +using Apewer.Network; using System; using System.IO; using System.Net; @@ -11,9 +12,12 @@ namespace Apewer.Web public sealed class ApiResponse { + /// 自定义标签。 + public object Tag { get; set; } + #region internal - private ApiModel _model = null; + private object _model = null; private Json _data = Json.NewObject(); internal bool StopReturn = false; @@ -35,20 +39,19 @@ namespace Apewer.Web #region user /// 头。 - public StringPairs Headers { get; set; } = new StringPairs(); + public HttpHeaders Headers { get; set; } = new HttpHeaders(); /// Cookies。 public CookieCollection Cookies { get; set; } = new CookieCollection(); /// 获取或设置输出模型。 - public ApiModel Model + public object Model { get { return _model; } set { - var old = _model; + RuntimeUtility.Dispose(_model); _model = value; - RuntimeUtility.Dispose(old); } } diff --git a/Apewer/Web/ApiServiceDescriptor.cs b/Apewer/Web/ApiServiceDescriptor.cs deleted file mode 100644 index d092d6d..0000000 --- a/Apewer/Web/ApiServiceDescriptor.cs +++ /dev/null @@ -1,92 +0,0 @@ -#if Middleware - -using System; -using System.Collections.Generic; -using System.Text; - -namespace Apewer.Web -{ - - /// API 服务描述。 - public sealed class ApiServiceDescriptor : IToJson - { - - /// 订阅器的生命周期。 - public ApiServiceLifetime Lifetime { get; private set; } - - /// 服务的类型。 - public Type ServiceType { get; private set; } - - /// 实现服务的类型。 - public Type ImplementationType { get; private set; } - - public object ImplementationInstance - { - get; - } - - public Func ImplementationFactory - { - get; - } - - /// 创建订阅器的实例。 - /// API 调用器。 - /// 订阅器的生命周期。 - /// 服务的类型。 - /// 实现服务的类型。 - /// - /// - internal ApiServiceDescriptor(ApiServiceLifetime lifetime, Type service, Type implementation) - { - if (invoker == null) throw new ArgumentNullException(nameof(invoker)); - if (service == null) throw new ArgumentNullException(nameof(service)); - if (implementation == null) throw new ArgumentNullException(nameof(implementation)); - - if (!service.IsAssignableFrom(implementation)) throw new ArgumentException($"类型 {implementation.Name} 未实现服务。"); - if (!implementation.IsClass) throw new ArgumentException($"实现服务的类型 {implementation.Name} 不是引用类型。"); - if (implementation.IsAbstract) throw new ArgumentException($"实现服务的类型 {implementation.Name} 是抽象类型,无法实例化。"); - - Lifetime = lifetime; - ServiceType = service; - ImplementationType = implementation; - } - - /// 创建订阅器的实例。 - /// API 调用器。 - /// 订阅器的生命周期。 - /// 服务的类型。 - /// 实现服务的方法。 - /// - /// - internal ApiServiceDescriptor(ApiServiceLifetime lifetime, Type service, Func生成 JSON 实例。 - public Json ToJson() - { - var json = new Json(); - json["Lifetime"] = Lifetime.ToString(); - json["Service"] = ServiceType.FullName; - json["Implementation"] = Implementation.FullName; - return json; - } - - } - -} - -#endif diff --git a/Apewer/Web/ApiServiceLifetime.cs b/Apewer/Web/ApiServiceLifetime.cs deleted file mode 100644 index 358e699..0000000 --- a/Apewer/Web/ApiServiceLifetime.cs +++ /dev/null @@ -1,23 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Text; - -namespace Apewer.Web -{ - - /// 服务的生命周期。 - public enum ApiServiceLifetime - { - - /// 对所有请求使用同一个实例。 - Singleton, - - /// 对每个请求使用同一个实例。 - Scoped, - - /// 对每个请求里的每次声明创建新实例。 - Transient - - } - -} diff --git a/Apewer/Web/ApiUtility.cs b/Apewer/Web/ApiUtility.cs index 41de0e8..0e56a23 100644 --- a/Apewer/Web/ApiUtility.cs +++ b/Apewer/Web/ApiUtility.cs @@ -117,7 +117,7 @@ namespace Apewer.Web } /// 获取 User Agent。 - public static string UserAgent(StringPairs headers) => headers == null ? null : headers.GetValue("user-agent"); + public static string UserAgent(HttpHeaders headers) => headers == null ? null : headers.GetValue("user-agent"); // 从 Uri 对象中解析路径片段。 private static string[] Segmentals(Uri url) @@ -162,11 +162,11 @@ namespace Apewer.Web } /// 获取 X-Forwarded-For,不存在时返回 NULL 值。 - public static string[] GetForwardedIP(StringPairs headers) + public static string[] GetForwardedIP(HttpHeaders headers) { if (headers != null) { - var value = headers.GetValue("x-forwarded-for", true); + var value = headers.GetValue("x-forwarded-for"); if (!string.IsNullOrEmpty(value)) { var fips = new List(); @@ -349,12 +349,12 @@ namespace Apewer.Web } } - internal static CookieCollection ParseCookies(StringPairs headers) + internal static CookieCollection ParseCookies(HttpHeaders headers) { var cookies = new CookieCollection(); if (headers == null) return cookies; - var hvs = headers.GetValues("cookie", true, false); + var hvs = headers.GetValues("Cookie"); foreach (var hv in hvs) { if (string.IsNullOrEmpty(hv)) continue; @@ -534,6 +534,25 @@ namespace Apewer.Web #endregion + #region ApiContext + + /// 作为纯文本输出。 + /// Content-Type: text/plain + public static void Text(ApiContext context, string text) + { + if (context == null) return; + Model(context.Response, new ApiTextModel(text, "text/plain")); + } + + /// 设置 status 为 error,并设置 message 的内容。 + public static void Error(ApiContext context, string text) + { + if (context == null) return; + Error(context.Response, text); + } + + #endregion + #region ApiRequest /// 获取 URL 路径段,不存在的段为 NULL 值。可要求解码。 @@ -707,22 +726,24 @@ namespace Apewer.Web if (response == null) return; response.Model = null; response.Status = "exception"; - if (exception == null) return; - try + if (exception != null) { - if (setData) + try { - var json = ToJson(exception); - response.Message = json["message"]; - response.Data = json; - } - else - { - response.Message = exception.Message(); + if (setData) + { + var json = ToJson(exception); + response.Message = json["message"]; + response.Data = json; + } + else + { + response.Message = exception.Message(); + } } + catch { } } - catch { } } private static Json ToJson(Exception exception, bool withInner = true) @@ -810,23 +831,24 @@ namespace Apewer.Web #endregion - #region ApiModel + #region ApiResult - /// 初始化 ApiMode 的属性。 - public static void Initialize(ApiModel model, ApiRequest request, ApiResponse response, ApiOptions options, ApiProvider provider) + /// 对 HTTP 结果设置文件名。 + /// 结果。 + /// 文件名(未编码)。 + public static void SetAttachemnt(this HeadResult result, string name) { - if (model == null) return; - model._request = request; - model._response = response; - model._options = options; - model._provider = provider; + if (result == null) throw new ArgumentNullException(nameof(result)); + if (name.IsEmpty()) throw new ArgumentNullException(nameof(name)); + + var encoded = TextUtility.EncodeUrl(name); + result.Headers.Add("Content-Disposition", $"attachment; filename={encoded}"); } #endregion #region ApiFunction Parameters - internal static object[] ReadParameters(ApiRequest request, ApiFunction function) { if (request == null || function == null) return null; @@ -840,31 +862,41 @@ namespace Apewer.Web public static object[] ReadParameters(ApiRequest request, ParameterInfo[] parameters) { if (request == null || parameters == null || parameters.Length < 1) return null; + var apiParameters = parameters.Map(x => ApiParameter.Parse(x) ?? throw new Exception($"参数【{x.Name}】无效。")); + return ReadParameters(request, apiParameters); + } - var pis = parameters; - if (pis == null) return null; + /// 为带有形参的 Function 准备实参。 + /// API 请求模型。 + /// Function 的参数信息。 + /// 实参。 + public static object[] ReadParameters(ApiRequest request, ApiParameter[] parameters) + { + if (request == null || parameters == null || parameters.Length < 1) return null; + + if (parameters == null) return null; - var count = pis.Length; + var count = parameters.Length; if (count < 1) return null; // 当 Function 仅有一个参数时,尝试生成模型。 - if (count == 1) + if (count == 1 && parameters[0] != null) { - var pin = pis[0].Name; - var pit = pis[0].ParameterType; + var parameterName = parameters[0].Name; + var parameterType = parameters[0].Type; // POST if (request.Method == HttpMethod.POST) { // string - if (pit.Equals(typeof(string))) return new object[] { request.Parameters.GetValue(pin, true) }; + if (parameterType.Equals(typeof(string))) return new object[] { request.Parameters.GetValue(parameterName, true) }; // json - if (pit.Equals(typeof(Json))) return new object[] { request.PostJson }; + if (parameterType.Equals(typeof(Json))) return new object[] { request.PostJson }; #if !NET20 // dynamic - if (pit.Equals(typeof(object))) + if (parameterType.Equals(typeof(object))) { try { @@ -902,15 +934,13 @@ namespace Apewer.Web #endif // class - if (pit.IsClass) + if (parameterType.IsClass) { try { - var entity = Activator.CreateInstance(pit); - var setted = false; - if (!setted) setted = ReadParameter(request.Data, entity); - if (!setted) setted = ReadParameter(request.PostJson, entity); - return new object[] { entity }; + var entity = ReadParameter(request.Data, parameterType); + if (entity == null) entity = ReadParameter(request.PostJson, parameterType); + if (entity != null) return new object[] { entity.Value }; } catch { } } @@ -923,42 +953,51 @@ namespace Apewer.Web else { // string - if (pit.Equals(typeof(string))) return new object[] { request.Parameters.GetValue(pin, true) }; + if (parameterType.Equals(typeof(string))) return new object[] { request.Parameters.GetValue(parameterName, true) }; // json - if (pit.Equals(typeof(Json))) return new object[] { Json.From(request.Parameters.GetValue(pin, true)) }; + if (parameterType.Equals(typeof(Json))) return new object[] { Json.From(request.Parameters.GetValue(parameterName, true)) }; } } - var ps = new object[count]; + var values = new object[count]; for (var i = 0; i < count; i++) { - var name = pis[i].Name; - var type = pis[i].ParameterType; - var text = Parameter(request, name); - ps[i] = ReadParameter(text, type); + if (parameters[i] != null) + { + var name = parameters[i].Name; + var type = parameters[i].Type; + var text = Parameter(request, name); + values[i] = ReadParameter(text, type); + } } - return ps; + return values; } - static bool ReadParameter(Json json, object entity) + static Class ReadParameter(Json json, Type type) { - if (!json) return false; - if (json.IsObject) - { - var properties = json.GetProperties(); - if (properties.Length < 1) return false; - Json.Object(entity, json, true, null, true); - return true; - } - if (json.IsArray) + if (json) { - var items = json.GetItems(); - if (items.Length < 1) return false; - Json.Object(entity, json, true, null, true); - return true; + if (json.IsObject) + { + var properties = json.GetProperties(); + if (properties.Length > 0) + { + var entity = Json.Object(type, json, true, null, true); + return new Class(entity); + } + } + if (json.IsArray) + { + var items = json.GetItems(); + if (items.Length > 0) + { + var entity = Json.Object(type, json, true, null, true); + return new Class(entity); + } + } } - return false; + return null; } static object ReadParameter(string text, Type type) @@ -1004,7 +1043,7 @@ namespace Apewer.Web } var json = Json.NewObject(); json.SetProperty("count", count); - json.SetProperty("list", list); + json.SetProperty("applications", list); return json; } @@ -1024,7 +1063,7 @@ namespace Apewer.Web } var json = Json.NewObject(); json.SetProperty("count", count); - json.SetProperty("list", list); + json.SetProperty("functions", list); return json; } diff --git a/Apewer/Web/BytesResult.cs b/Apewer/Web/BytesResult.cs new file mode 100644 index 0000000..52378eb --- /dev/null +++ b/Apewer/Web/BytesResult.cs @@ -0,0 +1,65 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Apewer.Web +{ + + /// 表示 API 行为结果,主体为字节数组。 + public class BytesResult : HeadResult + { + + #region content + + /// 主体。 + public virtual byte[] Bytes { get; set; } + + /// 创建结果实例。 + public BytesResult(byte[] bytes, string contentType = "application/octet-stream") : this(200, bytes, contentType) { } + + /// 创建结果实例。 + public BytesResult(int status, byte[] bytes, string contentType = "application/octet-stream") : base(status) + { + Headers.Add("Content-Type", contentType); + Bytes = bytes; + } + + #endregion + + #region execute + + /// 写入主体。 + /// API 上下文。 + /// 主体的内容,字节数应该和 Content-Length 一致。 + protected virtual void WriteBody(ApiContext context, byte[] bodyData) + { + if (bodyData != null && bodyData.Length > 0) + { + var stream = context.Provider.ResponseBody(); + stream.Write(bodyData, 0, bodyData.Length); + } + } + + /// 写入 HTTP 头和主体。 + public override void ExecuteResult(ApiContext context) + { + if (Bytes == null) + { + WriteHead(context, 0); + } + else + { + WriteHead(context, Bytes.Length); + if (Bytes.Length > 0L) + { + var stream = context.Provider.ResponseBody(); + stream.Write(Bytes); + } + } + } + + #endregion + + } + +} diff --git a/Apewer/Web/CacheControl.cs b/Apewer/Web/CacheControl.cs new file mode 100644 index 0000000..17fa652 --- /dev/null +++ b/Apewer/Web/CacheControl.cs @@ -0,0 +1,44 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Apewer.Web +{ + + /// 缓存控制的值。 + public static class CacheControl + { + + /// 不缓存。 + public const string Disabled = "no-cache, no-store, must-revalidate"; + + /// 1 天的秒数。 + public const int Day = 86400; + + /// 30 天的秒数。 + public const int Month = 259200; + + /// 365 天的秒数。 + public const int Year = 31536000; + + /// 构建缓存指令,此指令允许所有缓存。 + /// 浏览器的缓存秒数。 + /// 代理服务器的缓存秒数。 + public static string Public(int maxAge, int sMaxAge) + { + if (maxAge < 0) throw new ArgumentOutOfRangeException(nameof(maxAge)); + if (sMaxAge < 0) throw new ArgumentOutOfRangeException(nameof(sMaxAge)); + return $"public, max-age={maxAge}, s-maxage={sMaxAge}, must-revalidate"; + } + + /// 构建缓存指令,此指令仅允许浏览器缓存。 + /// 浏览器的缓存秒数。 + public static string Private(int maxAge) + { + if (maxAge < 0) throw new ArgumentOutOfRangeException(nameof(maxAge)); + return $"private, max-age={maxAge}, must-revalidate"; + } + + } + +} diff --git a/Apewer/Web/FromBodyAttribute.cs b/Apewer/Web/FromBodyAttribute.cs new file mode 100644 index 0000000..70ba568 --- /dev/null +++ b/Apewer/Web/FromBodyAttribute.cs @@ -0,0 +1,12 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Apewer.Web +{ + + /// + [AttributeUsage(AttributeTargets.Parameter, AllowMultiple = false, Inherited = false)] + public sealed class FromBodyAttribute : Attribute { } + +} diff --git a/Apewer/Web/FromFormAttribute.cs b/Apewer/Web/FromFormAttribute.cs new file mode 100644 index 0000000..ea6c3be --- /dev/null +++ b/Apewer/Web/FromFormAttribute.cs @@ -0,0 +1,12 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Apewer.Web +{ + + /// + [AttributeUsage(AttributeTargets.Parameter, AllowMultiple = false, Inherited = false)] + public sealed class FromFormAttribute : Attribute { } + +} diff --git a/Apewer/Web/FromHeaderAttribute.cs b/Apewer/Web/FromHeaderAttribute.cs new file mode 100644 index 0000000..8069744 --- /dev/null +++ b/Apewer/Web/FromHeaderAttribute.cs @@ -0,0 +1,12 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Apewer.Web +{ + + /// + [AttributeUsage(AttributeTargets.Parameter, AllowMultiple = false, Inherited = false)] + public sealed class FromHeaderAttribute : Attribute { } + +} diff --git a/Apewer/Web/FromQueryAttribute.cs b/Apewer/Web/FromQueryAttribute.cs new file mode 100644 index 0000000..2a098bd --- /dev/null +++ b/Apewer/Web/FromQueryAttribute.cs @@ -0,0 +1,12 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Apewer.Web +{ + + /// + [AttributeUsage(AttributeTargets.Parameter, AllowMultiple = false, Inherited = false)] + public sealed class FromQueryAttribute : Attribute { } + +} diff --git a/Apewer/Web/HeadResult.cs b/Apewer/Web/HeadResult.cs new file mode 100644 index 0000000..49592ab --- /dev/null +++ b/Apewer/Web/HeadResult.cs @@ -0,0 +1,92 @@ +using Apewer.Network; +using System; + +namespace Apewer.Web +{ + + /// 表示 API 行为结果,仅包含头。 + public class HeadResult : ActionResult, IActionResult, IHttpActionResult + { + + #region content + + int _status = 200; + HttpHeaders _headers = new HttpHeaders(); + + /// 由 RFC 7231 定义的状态码。 + /// 1xx (Informational)
2xx (Successful)
3xx (Redirection)
4xx (Client Error)
5xx (Server Error)
+ public virtual int StatusCode + { + get { return _status; } + set { _status = value; } + } + + /// 头部。 + public virtual HttpHeaders Headers + { + get { return _headers; } + set { _headers = value ?? new HttpHeaders(); } + } + + /// 创建结果实例。 + public HeadResult() : this(200) { } + + /// 创建结果实例。 + public HeadResult(int status) + { + StatusCode = status; + } + + #endregion + + #region execute + + /// 写入 HTTP 头。 + /// API 上下文。 + /// 内容长度。指定为负数时不写入 HTTP 头。 + protected virtual void WriteHead(ApiContext context, long contentLength) + { + context.Provider.SetStatus(StatusCode); + + const string ContentType = "Content-Type"; + const string ContentLength = "Content-Length"; + foreach (var header in _headers) + { + if (header.Name.IsEmpty()) continue; + if (header.Value.IsEmpty()) continue; + + // Content-Length + if (ContentLength.Equals(header.Name, StringComparison.CurrentCultureIgnoreCase)) + { + continue; + } + + // Content-Type + if (ContentType.Equals(header.Name, StringComparison.CurrentCultureIgnoreCase)) + { + context.Provider.SetContentType(header.Value); + continue; + } + + // default + context.Provider.SetHeader(header.Name, header.Value); + } + + // Content-Length + if (contentLength >= 0L) + { + context.Provider.SetContentLength(contentLength); + } + } + + /// 写入 HTTP 头,其中不包含 Content-Length。 + public override void ExecuteResult(ApiContext context) + { + WriteHead(context, 0L); + } + + #endregion + + } + +} diff --git a/Apewer/Web/HttpConnectAttribute.cs b/Apewer/Web/HttpConnectAttribute.cs new file mode 100644 index 0000000..0188586 --- /dev/null +++ b/Apewer/Web/HttpConnectAttribute.cs @@ -0,0 +1,12 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Apewer.Web +{ + + /// + [AttributeUsage(AttributeTargets.Method, AllowMultiple = false, Inherited = false)] + public sealed class HttpConnectAttribute : Attribute { } + +} diff --git a/Apewer/Web/HttpDeleteAttribute.cs b/Apewer/Web/HttpDeleteAttribute.cs new file mode 100644 index 0000000..aa87da9 --- /dev/null +++ b/Apewer/Web/HttpDeleteAttribute.cs @@ -0,0 +1,12 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Apewer.Web +{ + + /// + [AttributeUsage(AttributeTargets.Method, AllowMultiple = false, Inherited = false)] + public sealed class HttpDeleteAttribute : Attribute { } + +} diff --git a/Apewer/Web/HttpGetAttribute.cs b/Apewer/Web/HttpGetAttribute.cs new file mode 100644 index 0000000..af24867 --- /dev/null +++ b/Apewer/Web/HttpGetAttribute.cs @@ -0,0 +1,12 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Apewer.Web +{ + + /// + [AttributeUsage(AttributeTargets.Method, AllowMultiple = false, Inherited = false)] + public sealed class HttpGetAttribute : Attribute { } + +} diff --git a/Apewer/Web/HttpHeadAttribute.cs b/Apewer/Web/HttpHeadAttribute.cs new file mode 100644 index 0000000..e7a99c0 --- /dev/null +++ b/Apewer/Web/HttpHeadAttribute.cs @@ -0,0 +1,12 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Apewer.Web +{ + + /// + [AttributeUsage(AttributeTargets.Method, AllowMultiple = false, Inherited = false)] + public sealed class HttpHeadAttribute : Attribute { } + +} diff --git a/Apewer/Web/HttpListenerProvider.cs b/Apewer/Web/HttpListenerProvider.cs index c53a620..9530734 100644 --- a/Apewer/Web/HttpListenerProvider.cs +++ b/Apewer/Web/HttpListenerProvider.cs @@ -1,4 +1,5 @@ -using System; +using Apewer.Network; +using System; using System.Collections.Generic; using System.IO; using System.Net; @@ -7,13 +8,16 @@ namespace Apewer.Web { /// 用于网站的服务程序。 - public class HttpListenerProvider : ApiProvider + public class HttpListenerProvider : ApiProvider { private HttpListenerContext context; private HttpListenerRequest request; private HttpListenerResponse response; + /// HttpContext + public override HttpListenerContext Context { get => context; } + /// 创建服务程序实例。 /// public HttpListenerProvider(HttpListenerContext context) @@ -59,7 +63,7 @@ namespace Apewer.Web public override string GetReferrer() => request.UrlReferrer == null ? null : request.UrlReferrer.OriginalString; /// 获取请求的头。 - public override StringPairs GetHeaders() => StringPairs.From(request.Headers); + public override HttpHeaders GetHeaders() => new HttpHeaders(request.Headers); /// 获取请求的内容类型。 public override string GetContentType() => request.ContentType; diff --git a/Apewer/Web/HttpOptionsAttribute.cs b/Apewer/Web/HttpOptionsAttribute.cs new file mode 100644 index 0000000..6903961 --- /dev/null +++ b/Apewer/Web/HttpOptionsAttribute.cs @@ -0,0 +1,12 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Apewer.Web +{ + + /// + [AttributeUsage(AttributeTargets.Method, AllowMultiple = false, Inherited = false)] + public sealed class HttpOptionsAttribute : Attribute { } + +} diff --git a/Apewer/Web/HttpPatchAttribute.cs b/Apewer/Web/HttpPatchAttribute.cs new file mode 100644 index 0000000..9fad85e --- /dev/null +++ b/Apewer/Web/HttpPatchAttribute.cs @@ -0,0 +1,12 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Apewer.Web +{ + + /// + [AttributeUsage(AttributeTargets.Method, AllowMultiple = false, Inherited = false)] + public sealed class HttpPatchAttribute : Attribute { } + +} diff --git a/Apewer/Web/HttpPostAttribute.cs b/Apewer/Web/HttpPostAttribute.cs new file mode 100644 index 0000000..c6306bf --- /dev/null +++ b/Apewer/Web/HttpPostAttribute.cs @@ -0,0 +1,12 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Apewer.Web +{ + + /// + [AttributeUsage(AttributeTargets.Method, AllowMultiple = false, Inherited = false)] + public sealed class HttpPostAttribute : Attribute { } + +} diff --git a/Apewer/Web/HttpPutAttribute.cs b/Apewer/Web/HttpPutAttribute.cs new file mode 100644 index 0000000..a03c9f5 --- /dev/null +++ b/Apewer/Web/HttpPutAttribute.cs @@ -0,0 +1,12 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Apewer.Web +{ + + /// + [AttributeUsage(AttributeTargets.Method, AllowMultiple = false, Inherited = false)] + public sealed class HttpPutAttribute : Attribute { } + +} diff --git a/Apewer/Web/HttpTraceAttribute.cs b/Apewer/Web/HttpTraceAttribute.cs new file mode 100644 index 0000000..a3302de --- /dev/null +++ b/Apewer/Web/HttpTraceAttribute.cs @@ -0,0 +1,12 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Apewer.Web +{ + + /// + [AttributeUsage(AttributeTargets.Method, AllowMultiple = false, Inherited = false)] + public sealed class HttpTraceAttribute : Attribute { } + +} diff --git a/Apewer/Web/IActionResult.cs b/Apewer/Web/IActionResult.cs new file mode 100644 index 0000000..34c4076 --- /dev/null +++ b/Apewer/Web/IActionResult.cs @@ -0,0 +1,17 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Apewer.Web +{ + + /// 定义如何输出 API 结果。 + public interface IActionResult + { + + /// 执行结果。 + void ExecuteResult(ApiContext context); + + } + +} diff --git a/Apewer/Web/IApiModel.cs b/Apewer/Web/IApiModel.cs new file mode 100644 index 0000000..c72cbf0 --- /dev/null +++ b/Apewer/Web/IApiModel.cs @@ -0,0 +1,17 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Apewer.Web +{ + + /// 定义 API 响应模型。 + public interface IApiModel + { + + /// 输出。 + void Output(ApiContext context); + + } + +} diff --git a/Apewer/Web/IHttpActionResult.cs b/Apewer/Web/IHttpActionResult.cs new file mode 100644 index 0000000..ef76549 --- /dev/null +++ b/Apewer/Web/IHttpActionResult.cs @@ -0,0 +1,11 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Apewer.Web +{ + + /// 定义如何输出 API 结果。 + public interface IHttpActionResult : IActionResult { } + +} diff --git a/Apewer/Web/MiniConnection.cs b/Apewer/Web/MiniConnection.cs index 1ec8124..fffb0a6 100644 --- a/Apewer/Web/MiniConnection.cs +++ b/Apewer/Web/MiniConnection.cs @@ -387,12 +387,12 @@ namespace Apewer.Web } // 保持连接。 - _context.Request.KeepAlive = _context.Request.Http11 && TextUtility.Lower(_context.Request.Headers.GetValue("connection", true)) == "keep-alive"; + _context.Request.KeepAlive = _context.Request.Http11 && TextUtility.Lower(_context.Request.Headers.GetValue("Connection")) == "keep-alive"; // 启用压缩。 if (_server.Compression) { - var headerValue = _context.Request.Headers.GetValue("accept-encoding", true); + var headerValue = _context.Request.Headers.GetValue("Accept-Encoding"); if (!string.IsNullOrEmpty(headerValue)) { var split = headerValue.ToLower().Split(','); @@ -408,7 +408,7 @@ namespace Apewer.Web } // URL - var host = _context.Request.Headers.GetValue("host", true); + var host = _context.Request.Headers.GetValue("Host"); var port = 0; var local = LocalEndPoint; if (local != null) @@ -449,7 +449,7 @@ namespace Apewer.Web var headers = _context.Request.Headers; var length = -1L; - var value = headers.GetValue("Content-Length", true); + var value = headers.GetValue("Content-Length"); if (!string.IsNullOrEmpty(value)) { var num = value.Int64(); diff --git a/Apewer/Web/MiniProvider.cs b/Apewer/Web/MiniProvider.cs index 883bfc0..c270373 100644 --- a/Apewer/Web/MiniProvider.cs +++ b/Apewer/Web/MiniProvider.cs @@ -8,7 +8,7 @@ namespace Apewer.Web { /// - public sealed class MiniProvider : ApiProvider + public sealed class MiniProvider : ApiProvider { MiniConnection connection; @@ -16,6 +16,9 @@ namespace Apewer.Web MiniRequest request; MiniResponse response; + /// HttpContext + public override MiniContext Context { get => context; } + /// 创建服务程序实例。 /// public MiniProvider(MiniContext context) @@ -45,16 +48,16 @@ namespace Apewer.Web public override long GetContentLength() => request.ContentLength; /// - public override string GetContentType() => request.Headers.GetValue("Content-Type", true); + public override string GetContentType() => request.Headers.GetValue("Content-Type"); /// - public override StringPairs GetHeaders() => request.Headers; + public override HttpHeaders GetHeaders() => request.Headers; /// public override HttpMethod GetMethod() => NetworkUtility.ParseHttpMethod(request.Method); /// - public override string GetReferrer() => request.Headers.GetValue("Referrer", true); + public override string GetReferrer() => request.Headers.GetValue("Referrer"); /// public override Uri GetUrl() => request.Url; diff --git a/Apewer/Web/MiniRequest.cs b/Apewer/Web/MiniRequest.cs index 698038b..e37fa8b 100644 --- a/Apewer/Web/MiniRequest.cs +++ b/Apewer/Web/MiniRequest.cs @@ -1,4 +1,5 @@ -using System; +using Apewer.Network; +using System; using System.Collections.Generic; using System.Collections.Specialized; using System.Globalization; @@ -28,10 +29,10 @@ namespace Apewer.Web #region headers - StringPairs _headers = new StringPairs(); + HttpHeaders _headers = new HttpHeaders(); /// 头部。 - public StringPairs Headers { get => _headers; } + public HttpHeaders Headers { get => _headers; } /// 统一资源定位。 public Uri Url { get; internal set; } @@ -52,7 +53,7 @@ namespace Apewer.Web public bool Gzip { get; internal set; } /// 内容长度,单位:字节。 - public long ContentLength { get => _headers.GetValue("Content-Length", true).Int64(); } + public long ContentLength { get => _headers.GetValue("Content-Length").Int64(); } #endregion diff --git a/Apewer/Web/RouteAttribute.cs b/Apewer/Web/RouteAttribute.cs new file mode 100644 index 0000000..f190113 --- /dev/null +++ b/Apewer/Web/RouteAttribute.cs @@ -0,0 +1,29 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Apewer.Web +{ + + /// + [AttributeUsage(AttributeTargets.Method, AllowMultiple = true, Inherited = false)] + public sealed class RouteAttribute : Attribute + { + + string _path; + + /// + public string Path { get => _path; } + + /// + public RouteAttribute() { } + + /// + public RouteAttribute(string path) + { + _path = path; + } + + } + +} diff --git a/Apewer/Web/RoutePrefixAttribute.cs b/Apewer/Web/RoutePrefixAttribute.cs new file mode 100644 index 0000000..a11efa1 --- /dev/null +++ b/Apewer/Web/RoutePrefixAttribute.cs @@ -0,0 +1,24 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Apewer.Web +{ + + /// + [AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = false)] + public sealed class RoutePrefixAttribute : Attribute + { + + string _path; + + /// + public string Path { get { return _path; } } + + /// + /// + public RoutePrefixAttribute(string path) { _path = path; } + + } + +} diff --git a/Apewer/Web/StreamResult.cs b/Apewer/Web/StreamResult.cs new file mode 100644 index 0000000..3ee8121 --- /dev/null +++ b/Apewer/Web/StreamResult.cs @@ -0,0 +1,120 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Text; + +namespace Apewer.Web +{ + + /// 表示 API 行为结果,主体为流。 + public sealed class StreamResult : HeadResult, IDisposable + { + + #region content + + /// 主体。 + public Stream Stream { get; set; } + + /// Content-Length 的值,用于限制输出的最大长度。指定为 -1 时不限长度,输出到流的末尾。 + /// Default = -1 + public long Length { get; set; } + + /// 输出后自动释放流。 + public bool AutoDispose { get; set; } + + /// 创建结果实例。 + /// 要输出的流。 + /// 内容类型。 + /// 输出后自动释放流。 + public StreamResult(Stream stream, string contentType = "application/octet-stream", bool autoDispose = true) : this(200, stream, contentType, -1, autoDispose) { } + + /// 创建结果实例。 + /// HTTP 状态码。 + /// 要输出的流。 + /// 内容类型。 + /// Content-Length 的值。 + /// 输出后自动释放流。 + public StreamResult(int status, Stream stream, string contentType = "application/octet-stream", long length = -1, bool autoDispose = true) : base(status) + { + Headers.Add("Content-Type", contentType); + Stream = stream; + Length = length; + AutoDispose = autoDispose; + } + + #endregion + + #region execute + + /// 释放系统资源。 + public override void Dispose() + { + if (Stream != null) + { + RuntimeUtility.Dispose(Stream); + Stream = null; + } + } + + /// 写入 HTTP 头和主体。 + public override void ExecuteResult(ApiContext context) + { + try + { + if (Stream == null || Length == 0L) + { + WriteHead(context, 0); + } + else + { + WriteHead(context, Length); + + var writed = 0L; + var capacity = 4096; + var buffer = new byte[capacity]; + var body = context.Provider.ResponseBody(); + + // 限制长度。 + if (Length > 0L) + { + var remains = Length; + while (true) + { + var limit = Math.Min((int)remains, capacity); + var read = Stream.Read(buffer, 0, limit); + if (read < 1) break; + + body.Write(buffer, 0, read); + writed += read; + remains -= read; + if (remains < 1L) break; + } + } + + // 不限制长度,输出到流的末尾。 + else + { + while (true) + { + var read = Stream.Read(buffer, 0, capacity); + if (read < 1) break; + + body.Write(buffer, 0, read); + writed += read; + } + } + + context.Provider.End(); + } + } + finally + { + if (AutoDispose) Dispose(); + } + } + + #endregion + + } + +} diff --git a/Apewer/Web/TextResult.cs b/Apewer/Web/TextResult.cs new file mode 100644 index 0000000..2bbad31 --- /dev/null +++ b/Apewer/Web/TextResult.cs @@ -0,0 +1,20 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Apewer.Web +{ + + /// 表示 API 行为结果,主体为文本。 + public class TextResult : BytesResult + { + + /// 创建结果实例。 + public TextResult(string text, string contentType = "text/plain") : base(text.Bytes(), contentType) { } + + /// 创建结果实例。 + public TextResult(int status, string text, string contentType = "text/plain") : base(status, text.Bytes(), contentType) { } + + } + +} diff --git a/Apewer/Web/WebsiteProvider.cs b/Apewer/Web/WebsiteProvider.cs index 69aca6f..33fdaa8 100644 --- a/Apewer/Web/WebsiteProvider.cs +++ b/Apewer/Web/WebsiteProvider.cs @@ -1,5 +1,6 @@ #if NETFX +using Apewer.Network; using System; using System.Collections.Generic; using System.IO; @@ -9,13 +10,16 @@ namespace Apewer.Web { /// 用于网站的服务程序。 - public class WebsiteProvider : ApiProvider + public class WebsiteProvider : ApiProvider { private HttpContext context; private HttpRequest request; private HttpResponse response; + /// HttpContext + public override HttpContext Context { get => context; } + /// 创建服务程序实例。 /// public WebsiteProvider(HttpContext context) @@ -65,7 +69,7 @@ namespace Apewer.Web public override string GetReferrer() => request.UrlReferrer == null ? null : request.UrlReferrer.OriginalString; /// 获取请求的头。 - public override StringPairs GetHeaders() => StringPairs.From(request.Headers); + public override HttpHeaders GetHeaders() => new HttpHeaders(request.Headers); /// 获取请求的内容类型。 public override string GetContentType() => request.ContentType; diff --git a/Apewer/Web/_Attributes.cs b/Apewer/Web/_Attributes.cs deleted file mode 100644 index 5f5348d..0000000 --- a/Apewer/Web/_Attributes.cs +++ /dev/null @@ -1,95 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Text; - -namespace Apewer.Web -{ - - #region 路由 - - /// - [AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = false)] - public sealed class RoutePrefixAttribute : Attribute - { - - string _path; - - /// - public string Path { get { return _path; } } - - /// - /// - public RoutePrefixAttribute(string path) { _path = path; } - - } - - /// - [AttributeUsage(AttributeTargets.Method, AllowMultiple = true, Inherited = false)] - public sealed class RouteAttribute : Attribute - { - - string _path; - - /// - public string Path { get { return _path; } } - - /// - public RouteAttribute(string path) { _path = path; } - - } - - #endregion - - #region 方法 - - /// - [AttributeUsage(AttributeTargets.Method, AllowMultiple = false, Inherited = false)] - public sealed class HttpConnectAttribute : Attribute { } - - /// - [AttributeUsage(AttributeTargets.Method, AllowMultiple = false, Inherited = false)] - public sealed class HttpDeleteAttribute : Attribute { } - - /// - [AttributeUsage(AttributeTargets.Method, AllowMultiple = false, Inherited = false)] - public sealed class HttpGetAttribute : Attribute { } - - /// - [AttributeUsage(AttributeTargets.Method, AllowMultiple = false, Inherited = false)] - public sealed class HttpHeadAttribute : Attribute { } - - /// - [AttributeUsage(AttributeTargets.Method, AllowMultiple = false, Inherited = false)] - public sealed class HttpOptionsAttribute : Attribute { } - - /// - [AttributeUsage(AttributeTargets.Method, AllowMultiple = false, Inherited = false)] - public sealed class HttpPatchAttribute : Attribute { } - - /// - [AttributeUsage(AttributeTargets.Method, AllowMultiple = false, Inherited = false)] - public sealed class HttpPostAttribute : Attribute { } - - /// - [AttributeUsage(AttributeTargets.Method, AllowMultiple = false, Inherited = false)] - public sealed class HttpPutAttribute : Attribute { } - - /// - [AttributeUsage(AttributeTargets.Method, AllowMultiple = false, Inherited = false)] - public sealed class HttpTraceAttribute : Attribute { } - - #endregion - - #region 参数 - - /// - [AttributeUsage(AttributeTargets.Parameter, AllowMultiple = false, Inherited = false)] - public sealed class FromBodyAttribute : Attribute { } - - /// - [AttributeUsage(AttributeTargets.Parameter, AllowMultiple = false, Inherited = false)] - public sealed class FromUriAttribute : Attribute { } - - #endregion - -} diff --git a/Apewer/_Extensions.cs b/Apewer/_Extensions.cs index ee57e71..05c683b 100644 --- a/Apewer/_Extensions.cs +++ b/Apewer/_Extensions.cs @@ -20,10 +20,10 @@ using System.Dynamic; public static class Extensions { - /// 是 NULL 值。 + /// 是 NULL 值,或是 DBNULL 值。 public static bool IsNull(this object @this) => @this == null || @this.Equals(DBNull.Value); - /// 不是 NULL 值。 + /// 不是 NULL 值,且不是 DBNULL 值。 public static bool NotNull(this object @this) => @this != null && !@this.Equals(DBNull.Value); /// 是默认值。 @@ -43,16 +43,6 @@ public static class Extensions /// 调用 Set 方法。 public static void Set(this PropertyInfo @this, object instance, T value) => RuntimeUtility.InvokeSet(instance, @this, value); - /// 调用方法。 - /// - /// - /// - /// - /// - /// - /// - public static object Invoke(this MethodInfo @this, object instace, params object[] parameters) => RuntimeUtility.InvokeMethod(instace, @this, parameters); - /// 判断静态属性。 public static bool IsStatic(this PropertyInfo @this) => RuntimeUtility.IsStatic(@this); @@ -239,8 +229,9 @@ public static class Extensions #region DateTime - /// 获取毫秒时间戳。 - public static long Stamp(this DateTime @this, bool byMilliseconds = true) => ClockUtility.Stamp(@this, byMilliseconds); + /// 获取毫秒时间戳。当指定了 时将优先使用自定义的方法。 + /// 默认不判断参数的时区,与 相同。 + public static long Stamp(this DateTime dt) => ClockUtility.ToStamp(dt); /// 转换为易于阅读的文本。 /// 格式:1970-01-01 00:00:00.000 @@ -252,9 +243,10 @@ public static class Extensions /// 当前 DateTime 为闰年。 public static bool LeapYear(this DateTime @this) => ClockUtility.IsLeapYear(@this); - /// 从毫秒时间戳获取 DateTime 对象。发生异常且不允许异常时将返回 1970-01-01 00:00:00.000。 + /// 解析毫秒时间戳,获取 DateTime 对象。当指定了 时将优先使用自定义的方法。 + /// 默认不判断系统时区,返回的结果是 /// - public static DateTime DateTime(this long stamp, DateTimeKind kind = DateTimeKind.Unspecified, bool throwException = true) => ClockUtility.FromStamp(stamp, kind, throwException); + public static DateTime DateTime(this long stamp) => ClockUtility.FromStamp(stamp); #region Nullable @@ -265,8 +257,9 @@ public static class Extensions /// 转换为紧凑的文本。 public static string Compact(this DateTime? @this, bool date = true, bool time = true, bool seconds = true, bool milliseconds = true) => @this == null ? default : ClockUtility.Compact(@this.Value, date, time, seconds, milliseconds); - /// 获取毫秒时间戳。 - public static long Stamp(this DateTime? @this, bool byMilliseconds = true) => @this == null ? default : ClockUtility.Stamp(@this.Value, byMilliseconds); + /// 获取毫秒时间戳。当指定了 时将优先使用自定义的方法。 + /// 默认不判断参数的时区,与 相同。 + public static long Stamp(this DateTime? @this) => @this == null ? default : ClockUtility.ToStamp(@this.Value); /// 转换为易于阅读的文本。 /// 格式:1970-01-01 00:00:00.000 @@ -276,7 +269,7 @@ public static class Extensions public static string Compact(this Class @this, bool date = true, bool time = true, bool seconds = true, bool milliseconds = true) => @this == null ? default : ClockUtility.Compact(@this.Value, date, time, seconds, milliseconds); /// 获取毫秒时间戳。 - public static long Stamp(this Class @this, bool byMilliseconds = true) => @this == null ? default : ClockUtility.Stamp(@this.Value, byMilliseconds); + public static long Stamp(this Class @this) => @this == null ? default : ClockUtility.ToStamp(@this.Value); #endregion @@ -594,13 +587,13 @@ public static class Extensions public static void Text(this ApiResponse @this, string text, string contentType = "text/plain") => ApiUtility.Model(@this, new ApiTextModel(text, contentType)); /// 输出 Json 文本。 - public static void Json(this ApiResponse @this, Json json, bool indented = true, bool camel = false) => ApiUtility.Model(@this, new ApiJsonModel(json, camel, indented)); + public static void Json(this ApiResponse @this, Json json, bool indented = false, bool camel = true) => ApiUtility.Model(@this, new ApiJsonModel(json, indented, camel)); /// 输出文件。 public static void File(this ApiResponse @this, string path) => ApiUtility.Model(@this, new ApiFileModel(path)); /// 重定向。 - public static void Redirect(this ApiResponse @this, string location) => ApiUtility.Model(@this, new ApiRedirectModel() { Location = location }); + public static void Redirect(this ApiResponse @this, string location) => ApiUtility.Model(@this, new ApiRedirectModel(location)); #endregion diff --git a/ChangeLog.md b/ChangeLog.md index c780d3e..886d95d 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -1,6 +1,13 @@  ### 最新提交 +### 6.8.0 +- 新特性 + - ClockUtility:增加 CustomToStamp 和 CustomFromStamp,支持自定义时间戳的转换方法; + - Json:识别实例类型的 .ctor(Json) 构造函数,由类型自己实现反序列化; + - Web:兼容微软 API,支持 Route 特性,支持返回 ActionResult; + - WindowsUtility:增加读取进程内存的方法。 + ### 6.7.6 - 新特性 - CollectionUtility:增加数组的 Push 和 Unshift 方法;