├── models ├── .upgrade.v1.xml ├── .processor.xml ├── user.model.xml ├── organization.model.xml └── .connection.xml ├── libs ├── Third │ └── Kdbndp.dll ├── AppConfig │ ├── Settings │ │ ├── CompressionSetting.cs │ │ ├── CookieSetting.cs │ │ ├── FileUploadSetting.cs │ │ ├── VersionControlSetting.cs │ │ ├── CorsSetting.cs │ │ ├── RouterSetting.cs │ │ └── SessionSetting.cs │ ├── CodeM.FastApi.Config.csproj │ └── ApplicationConfig.cs ├── Cache │ ├── ExpirationType.cs │ ├── CodeM.FastApi.Cache.csproj │ ├── CacheManager.cs │ ├── ICache.cs │ └── LocalCache.cs ├── ControllerContext │ ├── Wrappers │ │ ├── CookieOptionsExt.cs │ │ ├── SessionWrapper.cs │ │ └── CookieWrapper.cs │ ├── Params │ │ ├── RouteParams.cs │ │ ├── PostForms.cs │ │ ├── HeaderParams.cs │ │ └── QueryParams.cs │ ├── CodeM.FastApi.Context.csproj │ └── ControllerContext.cs ├── Common │ ├── CodeM.FastApi.Common.csproj │ ├── FastApiException.cs │ ├── WebUtils.cs │ └── FastApiUtils.cs ├── DbUpgrade │ ├── CodeM.FastApi.DbUpgrade.csproj │ ├── UpgradeLoader.cs │ └── UpgradeManager.cs ├── Schedule │ ├── CodeM.FastApi.Schedule.csproj │ ├── ScheduleSetting.cs │ ├── ScheduleManager.cs │ └── ScheduleParser.cs ├── Logger │ ├── CodeM.FastApi.Log.csproj │ ├── File │ │ ├── FileLoggerProvider.cs │ │ ├── FileLogger.cs │ │ └── FileWriter.cs │ ├── FileLoggerFactoryExtensions.cs │ ├── ControllerContextLogExtension.cs │ └── Logger.cs ├── JsonResponseWriter │ ├── CodeM.FastApi.Context.JsonResponse.csproj │ └── JsonResponse.cs └── Router │ ├── CodeM.FastApi.Router.csproj │ └── MethodInvoker.cs ├── schedule.xml ├── Services ├── HelloService.cs └── BaseService.cs ├── router.xml ├── appsettings.Production.json ├── appsettings.Development.json ├── Schedules └── DemoJob.cs ├── .gitignore ├── Controllers ├── BaseController.cs └── HelloController.cs ├── Properties └── launchSettings.json ├── System ├── Middlewares │ └── CorsMiddleware.cs ├── Core │ ├── CurrentContext.cs │ ├── CacheLoader.cs │ └── Application.cs └── Utils │ └── CorsUtils.cs ├── LICENSE ├── appsettings.json ├── README.md ├── docs ├── structure.md ├── version.md ├── middleware.md ├── env.md ├── schedule.md ├── session.md ├── config.md ├── logging.md └── router.md ├── fastapi.csproj ├── Program.cs ├── fastapi.sln └── Startup.cs /models/.upgrade.v1.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /models/.processor.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /libs/Third/Kdbndp.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/softwaiter/fastapi.net/master/libs/Third/Kdbndp.dll -------------------------------------------------------------------------------- /schedule.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /libs/AppConfig/Settings/CompressionSetting.cs: -------------------------------------------------------------------------------- 1 | namespace CodeM.FastApi.Config.Settings 2 | { 3 | public class CompressionSetting 4 | { 5 | public bool Enable { get; set; } = true; 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /libs/AppConfig/Settings/CookieSetting.cs: -------------------------------------------------------------------------------- 1 | namespace CodeM.FastApi.Config.Settings 2 | { 3 | public class CookieSetting 4 | { 5 | 6 | public string Keys { get; set; } = string.Empty; 7 | 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /libs/Cache/ExpirationType.cs: -------------------------------------------------------------------------------- 1 | namespace CodeM.FastApi.Cache 2 | { 3 | public enum ExpirationType 4 | { 5 | Absolute = 0, 6 | RelativeToNow = 1, 7 | Sliding = 2 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /libs/AppConfig/Settings/FileUploadSetting.cs: -------------------------------------------------------------------------------- 1 | namespace CodeM.FastApi.Config.Settings 2 | { 3 | public class FileUploadSetting 4 | { 5 | public long MaxBodySize { get; set; } = 1024 * 1024 * 5; // 默认5M 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /Services/HelloService.cs: -------------------------------------------------------------------------------- 1 | namespace CodeM.FastApi.Services 2 | { 3 | public class HelloService : BaseService 4 | { 5 | public string GetHi() 6 | { 7 | return "你好,世界!"; 8 | } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /libs/ControllerContext/Wrappers/CookieOptionsExt.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Http; 2 | 3 | namespace CodeM.FastApi.Context.Wrappers 4 | { 5 | public class CookieOptionsExt : CookieOptions 6 | { 7 | 8 | public bool Encrypt { get; set; } = false; 9 | 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /router.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /appsettings.Production.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Warning", 5 | "Microsoft": "Error", 6 | "Microsoft.Hosting.Lifetime": "Error" 7 | }, 8 | "File": { 9 | "LogLevel": { 10 | "Default": "Warning" 11 | } 12 | } 13 | } 14 | } -------------------------------------------------------------------------------- /appsettings.Development.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft": "Warning", 6 | "Microsoft.Hosting.Lifetime": "Warning" 7 | }, 8 | "File": { 9 | "LogLevel": { 10 | "Default": "None" 11 | } 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /Schedules/DemoJob.cs: -------------------------------------------------------------------------------- 1 | using Quartz; 2 | using System; 3 | using System.Threading.Tasks; 4 | 5 | namespace CodeM.FastApi.Schedules 6 | { 7 | public class DemoJob : IJob 8 | { 9 | public Task Execute(IJobExecutionContext context) 10 | { 11 | Console.WriteLine("Hello,现在时间是 " + DateTime.Now); 12 | return Task.CompletedTask; 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /libs/Common/CodeM.FastApi.Common.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net8.0 5 | CodeM.FastApi.Common 6 | CodeM.FastApi.Common 7 | softwaiter 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /libs/DbUpgrade/CodeM.FastApi.DbUpgrade.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net8.0 5 | true 6 | softwaiter 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /Services/BaseService.cs: -------------------------------------------------------------------------------- 1 | using CodeM.FastApi.System.Core; 2 | 3 | namespace CodeM.FastApi.Services 4 | { 5 | public class BaseService 6 | { 7 | public T Service(bool singleton = true) 8 | { 9 | return Application.Instance().Service(singleton); 10 | } 11 | 12 | public Application App 13 | { 14 | get 15 | { 16 | return Application.Instance(); 17 | } 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /libs/Common/FastApiException.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace CodeM.FastApi.Common 4 | { 5 | public class FastApiException : Exception 6 | { 7 | public FastApiException() 8 | : base() 9 | { 10 | } 11 | 12 | public FastApiException(string message) 13 | : base(message) 14 | { 15 | } 16 | 17 | public FastApiException(string message, Exception innerException) 18 | : base(message, innerException) 19 | { 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | 4 | # User-specific files 5 | *.suo 6 | *.user 7 | *.userosscache 8 | *.sln.docstates 9 | 10 | # User-specific files (MonoDevelop/Xamarin Studio) 11 | *.userprefs 12 | 13 | # Build results 14 | [Dd]ebug/ 15 | [Dd]ebugPublic/ 16 | [Rr]elease/ 17 | [Rr]eleases/ 18 | x64/ 19 | x86/ 20 | bld/ 21 | [Bb]in/ 22 | [Oo]bj/ 23 | [Ll]og/ 24 | 25 | # Visual Studio 2015 cache/options directory 26 | .vs/ 27 | # Uncomment if you have tasks that create the 28 | 29 | fastapi.db 30 | 31 | PublishProfiles/ 32 | 33 | logs/ -------------------------------------------------------------------------------- /libs/Cache/CodeM.FastApi.Cache.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net8.0 5 | true 6 | softwaiter 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /libs/AppConfig/CodeM.FastApi.Config.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net8.0 5 | CodeM.FastApi.Config 6 | CodeM.FastApi.Config 7 | true 8 | softwaiter 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /libs/AppConfig/Settings/VersionControlSetting.cs: -------------------------------------------------------------------------------- 1 | namespace CodeM.FastApi.Config.Settings 2 | { 3 | public class VersionControlSetting 4 | { 5 | //是否启用版本控制,默认false 6 | public bool Enable 7 | { 8 | get; 9 | set; 10 | } = false; 11 | 12 | public string Default 13 | { 14 | get; 15 | set; 16 | } = "v1"; 17 | 18 | public string[] AllowedVersions 19 | { 20 | get; 21 | set; 22 | } 23 | 24 | public string Param 25 | { 26 | get; 27 | set; 28 | } = "version"; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /libs/Schedule/CodeM.FastApi.Schedule.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net8.0 5 | true 6 | softwaiter 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /libs/Logger/CodeM.FastApi.Log.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net8.0 5 | true 6 | softwaiter 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /libs/JsonResponseWriter/CodeM.FastApi.Context.JsonResponse.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net8.0 5 | CodeM.FastApi.Context.JsonResponse 6 | CodeM.FastApi.Context 7 | true 8 | softwaiter 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /libs/ControllerContext/Params/RouteParams.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Routing; 2 | 3 | namespace CodeM.FastApi.Context.Params 4 | { 5 | public class RouteParams 6 | { 7 | private RouteData mData; 8 | 9 | public RouteParams(RouteData data) 10 | { 11 | mData = data; 12 | } 13 | 14 | public string this[string key] 15 | { 16 | get 17 | { 18 | if (mData != null) 19 | { 20 | object value; 21 | if (mData.Values.TryGetValue(key, out value)) 22 | { 23 | return value.ToString(); 24 | } 25 | } 26 | return null; 27 | } 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /Controllers/BaseController.cs: -------------------------------------------------------------------------------- 1 | using CodeM.Common.Ioc; 2 | using CodeM.FastApi.System.Core; 3 | 4 | namespace CodeM.FastApi.Controllers 5 | { 6 | public class BaseController 7 | { 8 | public T Service(bool singleton = true) 9 | { 10 | string serviceFullName = typeof(T).FullName; 11 | if (singleton) 12 | { 13 | return (T)Wukong.GetSingleObject(serviceFullName); 14 | } 15 | else 16 | { 17 | return (T)Wukong.GetObject(serviceFullName); 18 | } 19 | } 20 | 21 | public Application App 22 | { 23 | get 24 | { 25 | return Application.Instance(); 26 | } 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "iisSettings": { 3 | "windowsAuthentication": false, 4 | "anonymousAuthentication": true, 5 | "iisExpress": { 6 | "applicationUrl": "http://localhost:5000", 7 | "sslPort": 0 8 | } 9 | }, 10 | "profiles": { 11 | "IIS Express": { 12 | "commandName": "IISExpress", 13 | "launchBrowser": true, 14 | "environmentVariables": { 15 | "ASPNETCORE_ENVIRONMENT": "Development" 16 | } 17 | }, 18 | "fastapi": { 19 | "commandName": "Project", 20 | "launchBrowser": true, 21 | "applicationUrl": "http://localhost:5000", 22 | "environmentVariables": { 23 | "ASPNETCORE_ENVIRONMENT": "Development" 24 | } 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /System/Middlewares/CorsMiddleware.cs: -------------------------------------------------------------------------------- 1 | using CodeM.FastApi.Config; 2 | using CodeM.FastApi.System.Utils; 3 | using Microsoft.AspNetCore.Http; 4 | using System.Threading.Tasks; 5 | 6 | namespace CodeM.FastApi.System.Middlewares 7 | { 8 | public class CorsMiddleware 9 | { 10 | private readonly RequestDelegate _next; 11 | private ApplicationConfig mConfig; 12 | 13 | public CorsMiddleware(RequestDelegate next, ApplicationConfig config) 14 | { 15 | _next = next; 16 | mConfig = config; 17 | } 18 | 19 | public Task Invoke(HttpContext context) 20 | { 21 | CorsUtils.SetCorsHeaders(context); 22 | 23 | if ("OPTIONS".Equals(context.Request.Method.ToUpper())) 24 | { 25 | return Task.CompletedTask; 26 | } 27 | 28 | return _next(context); 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /libs/ControllerContext/CodeM.FastApi.Context.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net8.0 5 | true 6 | softwaiter 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /libs/AppConfig/Settings/CorsSetting.cs: -------------------------------------------------------------------------------- 1 | namespace CodeM.FastApi.Config.Settings 2 | { 3 | public class CorsSetting 4 | { 5 | public class CorsSettingOptions 6 | { 7 | public string[] AllowSites 8 | { 9 | get; 10 | set; 11 | } 12 | 13 | public string[] AllowMethods 14 | { 15 | get; 16 | set; 17 | } 18 | 19 | public bool SupportsCredentials 20 | { 21 | get; 22 | set; 23 | } = false; 24 | } 25 | 26 | //是否启用跨域设置,默认false 27 | public bool Enable 28 | { 29 | get; 30 | set; 31 | } = false; 32 | 33 | public CorsSettingOptions Options 34 | { 35 | get; 36 | set; 37 | } = new CorsSettingOptions(); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /Controllers/HelloController.cs: -------------------------------------------------------------------------------- 1 | using CodeM.FastApi.Context; 2 | using CodeM.FastApi.Services; 3 | using System.Threading.Tasks; 4 | 5 | namespace CodeM.FastApi.Controllers 6 | { 7 | public class HelloController : BaseController 8 | { 9 | 10 | public async Task Handle(ControllerContext cc) 11 | { 12 | HelloService helloService = Service(); 13 | string hi = helloService.GetHi(); 14 | await cc.JsonAsync(hi); 15 | } 16 | 17 | /// 18 | /// 可通过传入版本号v2参数,测试版本控制功能 19 | /// 20 | /// 21 | /// 22 | public async Task Handle_v2(ControllerContext cc) 23 | { 24 | HelloService helloService = Service(); 25 | string hi = helloService.GetHi(); 26 | await cc.JsonAsync(hi + "这是v2版本返回的。"); 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /libs/DbUpgrade/UpgradeLoader.cs: -------------------------------------------------------------------------------- 1 | using CodeM.Common.Tools; 2 | using System.Collections.Generic; 3 | 4 | namespace CodeM.FastApi.DbUpgrade 5 | { 6 | internal class UpgradeLoader 7 | { 8 | public static List Load(string filename) 9 | { 10 | List data = new List(); 11 | 12 | Xmtool.Xml().Iterate(filename, (nodeInfo) => 13 | { 14 | if (!nodeInfo.IsEndNode) 15 | { 16 | if (nodeInfo.Path == "/sqls/sql/@text") 17 | { 18 | data.Add(nodeInfo.Text.Trim()); 19 | } 20 | else if (nodeInfo.Path == "/sqls/sql/@cdata") 21 | { 22 | data.Add(nodeInfo.CData.Trim()); 23 | } 24 | } 25 | return true; 26 | }); 27 | 28 | return data; 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /libs/AppConfig/ApplicationConfig.cs: -------------------------------------------------------------------------------- 1 | using CodeM.FastApi.Config.Settings; 2 | using System.Collections.Generic; 3 | 4 | namespace CodeM.FastApi.Config 5 | { 6 | public class ApplicationConfig 7 | { 8 | public CompressionSetting Compression { get; set; } = new CompressionSetting(); 9 | 10 | public RouterSetting Router { get; set; } = new RouterSetting(); 11 | 12 | public List Middlewares { get; set; } = new List(); 13 | 14 | public CookieSetting Cookie { get; set; } = new CookieSetting(); 15 | 16 | public CorsSetting Cors { get; set; } = new CorsSetting(); 17 | 18 | public SessionSetting Session { get; set; } = new SessionSetting(); 19 | 20 | public FileUploadSetting FileUpload { get; set; } = new FileUploadSetting(); 21 | 22 | public VersionControlSetting VersionControl { get; set; } = new VersionControlSetting(); 23 | 24 | public dynamic Settings { get; set; } = null; 25 | 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /libs/Logger/File/FileLoggerProvider.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.Configuration; 2 | using Microsoft.Extensions.Logging; 3 | using System; 4 | using System.Collections.Concurrent; 5 | 6 | namespace CodeM.FastApi.Log.File 7 | { 8 | [ProviderAlias("File")] 9 | public class FileLoggerProvider : ILoggerProvider, IDisposable 10 | { 11 | private static ConcurrentDictionary mLoggers = new ConcurrentDictionary(); 12 | 13 | private IConfigurationSection mOptions; 14 | 15 | public FileLoggerProvider(IConfigurationSection options) 16 | { 17 | mOptions = options; 18 | } 19 | 20 | public ILogger CreateLogger(string categoryName) 21 | { 22 | return mLoggers.GetOrAdd(categoryName, (key) => 23 | { 24 | return new FileLogger(mOptions, key); 25 | }); 26 | } 27 | 28 | public void Dispose() 29 | { 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /libs/Schedule/ScheduleSetting.cs: -------------------------------------------------------------------------------- 1 | namespace CodeM.FastApi.Schedule 2 | { 3 | public class ScheduleSetting 4 | { 5 | public string Id { get; set; } 6 | 7 | /// 8 | /// 重复次数,默认0,不限制 9 | /// 10 | public int Repeat { get; set; } = 0; 11 | 12 | /// 13 | /// 轮询周期,默认单位秒,支持ms、s、m、h、d 14 | /// 15 | public string Interval { get; set; } 16 | 17 | /// 18 | /// Cron表达式 19 | /// 20 | public string Cron { get; set; } 21 | 22 | /// 23 | /// 定时任务业务逻辑类 24 | /// 25 | public string Class { get; set; } 26 | 27 | /// 28 | /// 是否启动定时任务 29 | /// 30 | public bool Disable { get; set; } = false; 31 | 32 | /// 33 | /// 定时任务启动的环境,多个用逗号分隔 34 | /// 35 | public string Environment { get; set; } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /libs/Router/CodeM.FastApi.Router.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net8.0 5 | CodeM.FastApi.Router 6 | CodeM.FastApi.Router 7 | true 8 | softwaiter 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 softwaiter 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /models/user.model.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /libs/Logger/FileLoggerFactoryExtensions.cs: -------------------------------------------------------------------------------- 1 | using CodeM.FastApi.Log.File; 2 | using Microsoft.Extensions.Configuration; 3 | using Microsoft.Extensions.DependencyInjection; 4 | using Microsoft.Extensions.DependencyInjection.Extensions; 5 | 6 | namespace Microsoft.Extensions.Logging 7 | { 8 | public static class FileLoggerFactoryExtensions 9 | { 10 | private static FileLoggerProvider sFileLoggerProvider; 11 | private static object sFileLoggerProviderLock = new object(); 12 | 13 | public static ILoggingBuilder AddFile(this ILoggingBuilder builder, IConfigurationSection options) 14 | { 15 | builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton((spes) => 16 | { 17 | if (sFileLoggerProvider == null) 18 | { 19 | lock (sFileLoggerProviderLock) 20 | { 21 | if (sFileLoggerProvider == null) 22 | { 23 | sFileLoggerProvider = new FileLoggerProvider(options); 24 | } 25 | } 26 | } 27 | return sFileLoggerProvider; 28 | })); 29 | return builder; 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "AllowedHosts": "*", 3 | 4 | "Compression": { 5 | "Enable": true 6 | }, 7 | 8 | "Middlewares": [], 9 | 10 | "Router": { 11 | "MaxConcurrentTotal": 65535, 12 | "MaxIdlePerRouter": 10, 13 | "MaxConcurrentPerRouter": 100, 14 | "MaxInvokePerInstance": 10000 15 | }, 16 | 17 | "Cookie": { 18 | "Keys": "fastapi" 19 | }, 20 | 21 | "Session": { 22 | "Enable": true, 23 | "Timeout": "20m", 24 | "Cookie": { 25 | "Name": "fastapi.sid", 26 | "SameSite": "Lax", 27 | "HttpOnly": true, 28 | "Secure": "None" 29 | }, 30 | "Redis": { 31 | "Enable": false 32 | } 33 | }, 34 | 35 | "FileUpload": { 36 | "MaxBodySize": 5242880 37 | }, 38 | 39 | "Cors": { 40 | "Enable": true, 41 | "Options": { 42 | "AllowMethods": [ "*" ], 43 | "AllowSites": [ "http://localhost:4000", "http://localhost:9527" ], 44 | "SupportsCredentials": true 45 | } 46 | }, 47 | 48 | "Cache": { 49 | "Default": { 50 | "Type": "Local", 51 | "Default": true 52 | } 53 | }, 54 | 55 | "VersionControl": { 56 | "Enable": true, 57 | "Default": "v1", 58 | "AllowedVersions": [], 59 | "Param": "version" 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /System/Core/CurrentContext.cs: -------------------------------------------------------------------------------- 1 | using CodeM.FastApi.Context; 2 | using Microsoft.AspNetCore.Builder; 3 | using Microsoft.AspNetCore.Http; 4 | using Microsoft.Extensions.DependencyInjection; 5 | 6 | namespace CodeM.FastApi.System.Core 7 | { 8 | public static class CurrentContext 9 | { 10 | private static IHttpContextAccessor _accessor; 11 | 12 | public static ControllerContext Context 13 | { 14 | get 15 | { 16 | HttpContext context = _accessor.HttpContext; 17 | return ControllerContext.FromHttpContext(context, 18 | Application.Instance().Config()); 19 | } 20 | } 21 | 22 | internal static void Configure(IHttpContextAccessor accessor) 23 | { 24 | _accessor = accessor; 25 | } 26 | } 27 | 28 | public static class CurrentContextExtensions 29 | { 30 | public static void AddHttpContextAccessor(this IServiceCollection services) 31 | { 32 | services.AddSingleton(); 33 | } 34 | 35 | public static IApplicationBuilder UseCurrentContext(this IApplicationBuilder app) 36 | { 37 | var httpContextAccessor = app.ApplicationServices.GetRequiredService(); 38 | CurrentContext.Configure(httpContextAccessor); 39 | return app; 40 | } 41 | } 42 | 43 | } 44 | -------------------------------------------------------------------------------- /models/organization.model.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /libs/ControllerContext/Wrappers/SessionWrapper.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Http; 2 | 3 | namespace CodeM.FastApi.Context.Wrappers 4 | { 5 | public class SessionWrapper 6 | { 7 | private HttpContext mContext; 8 | public SessionWrapper(HttpContext context) 9 | { 10 | mContext = context; 11 | } 12 | 13 | public string Id 14 | { 15 | get 16 | { 17 | return mContext.Session.Id; 18 | } 19 | } 20 | 21 | public void SetString(string key, string value) 22 | { 23 | mContext.Session.SetString(key, value); 24 | } 25 | 26 | public string GetString(string key) 27 | { 28 | return mContext.Session.GetString(key); 29 | } 30 | 31 | public void SetInt32(string key, int value) 32 | { 33 | mContext.Session.SetInt32(key, value); 34 | } 35 | 36 | public int? GetInt32(string key) 37 | { 38 | return mContext.Session.GetInt32(key); 39 | } 40 | 41 | public void SetBoolean(string key, bool value) 42 | { 43 | SetInt32(key, value ? 1 : 0); 44 | } 45 | 46 | public bool GetBoolean(string key) 47 | { 48 | return GetInt32(key) == 1; 49 | } 50 | 51 | public void Clear() 52 | { 53 | mContext.Session.Clear(); 54 | } 55 | 56 | public void Remove(string key) 57 | { 58 | mContext.Session.Remove(key); 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 |
3 |

4 |

fastapi.net

5 |

6 | 全面、快速、智能、轻量级、跨平台的API开发框架。 7 |

8 |
9 |
10 | 11 | 12 | 13 | # 🎉 特性 14 | 15 | - :fire: 简单易用的ORM模型定义,全面支持SQLite、MySQL、SqlServer等常用数据库 16 | - :tea: App、Tools、Cache等丰富的内置对象,开箱即用 17 | - :rocket: ORM模型即接口,增、删、改、查API接口智能生成 18 | - 👏 灵活可扩展的插件机制 19 | - :nut_and_bolt: 支持框架级、路由级多种中间件 20 | - :vertical_traffic_light: 内置跨域、限流等安全配置,XSS等安全操作库 21 | - :clock3: 像写普通代码一样实现定时任务,框架内配置,灵活调度 22 | - :100: 其他更多特性,等你发现哦 23 | 24 | 25 | 26 | # :twisted_rightwards_arrows: 分支 27 | 28 | **master**:master分支已升级到最新的.Net8.0 SDK,Bug修改和所有新功能将在此分支进行。
29 | **netcore3.1**:netcore3.1是最早基于.NetCore3.1 SDK开发的版本,后续Bug修改将继续在此分支更新。 30 | 31 | 32 | 33 | # :memo:文档 34 | 35 | - [目录结构](docs/structure.md) 36 | - [运行环境](docs/env.md) 37 | - [配置(Config)](docs/config.md) 38 | - [中间件(Middleware)](docs/middleware.md) 39 | - [会话(Session)](docs/session.md) 40 | - [路由(Router)](docs/router.md) 41 | - [控制器(Controller)](docs/controller.md) 42 | - [版本控制(Version)](docs/version.md) 43 | - [数据库操作(ORM)](https://github.com/softwaiter/netcoreORM/blob/master/README.md) 44 | - [定时任务](docs/schedule.md) 45 | - [日志](docs/logging.md) 46 | - [常用工具库](https://github.com/softwaiter/netcoreTools/blob/master/README.md) 47 | 48 | # 🎈 协议 49 | 50 | fastapi.net 使用 [MIT 协议](https://github.com/softwaiter/fastapi.net/blob/master/LICENSE) 51 | 52 | -------------------------------------------------------------------------------- /System/Core/CacheLoader.cs: -------------------------------------------------------------------------------- 1 | using CodeM.Common.Ioc; 2 | using CodeM.Common.Tools.DynamicObject; 3 | using CodeM.FastApi.Cache; 4 | using CodeM.FastApi.Config; 5 | using System; 6 | 7 | namespace CodeM.FastApi.System.Core 8 | { 9 | internal class CacheLoader 10 | { 11 | public static void Load(ApplicationConfig config) 12 | { 13 | if (config.Settings.Has("Cache")) 14 | { 15 | if (config.Settings.Cache is DynamicObjectExt) 16 | { 17 | dynamic cacheSettings = config.Settings.Cache; 18 | foreach (string key in cacheSettings.Keys) 19 | { 20 | dynamic cacheItem = cacheSettings[key]; 21 | if (!cacheItem.Has("Type")) 22 | { 23 | throw new Exception("Cache配置缺少Type属性。"); 24 | } 25 | 26 | bool isDefault = false; 27 | if (cacheItem.Has("Default")) 28 | { 29 | isDefault = cacheItem.Default; 30 | } 31 | 32 | string cacheClassName = string.Concat("CodeM.FastApi.Cache.", cacheItem.Type, "Cache"); 33 | dynamic options = cacheItem["Options"]; 34 | ICache cacheInst = Wukong.GetObject(cacheClassName, new object[] { options }); 35 | CacheManager.Add(key, cacheInst, isDefault); 36 | } 37 | } 38 | } 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /libs/Common/WebUtils.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Http; 2 | using System; 3 | 4 | namespace CodeM.FastApi.Common 5 | { 6 | public class WebUtils 7 | { 8 | public static string GetClientIp(HttpRequest request) 9 | { 10 | string unknown = ""; 11 | string ip = request.Headers["x-forwarded-for"]; 12 | 13 | if (ip == null || string.IsNullOrWhiteSpace(ip) || 14 | unknown.Equals(ip, StringComparison.OrdinalIgnoreCase)) 15 | { 16 | ip = request.Headers["Proxy-Client-IP"]; 17 | } 18 | 19 | if (ip == null || string.IsNullOrWhiteSpace(ip) || 20 | unknown.Equals(ip, StringComparison.OrdinalIgnoreCase)) 21 | { 22 | ip = request.Headers["WL-Proxy-Client-IP"]; 23 | } 24 | 25 | if (ip == null || string.IsNullOrWhiteSpace(ip) || 26 | unknown.Equals(ip, StringComparison.OrdinalIgnoreCase)) 27 | { 28 | ip = request.Headers["HTTP_CLIENT_IP"]; 29 | } 30 | 31 | if (ip == null || string.IsNullOrWhiteSpace(ip) || 32 | unknown.Equals(ip, StringComparison.OrdinalIgnoreCase)) 33 | { 34 | ip = request.Headers["HTTP_X_FORWARDED_FOR"]; 35 | } 36 | 37 | if (ip == null || string.IsNullOrWhiteSpace(ip) || 38 | unknown.Equals(ip, StringComparison.OrdinalIgnoreCase)) 39 | { 40 | ip = request.HttpContext.Connection.RemoteIpAddress.MapToIPv4().ToString(); 41 | } 42 | 43 | return ip; 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /libs/Cache/CacheManager.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Concurrent; 2 | 3 | namespace CodeM.FastApi.Cache 4 | { 5 | public class CacheManager 6 | { 7 | private static ConcurrentDictionary sCaches = new ConcurrentDictionary(); 8 | private static string sDefaultCacheName = null; 9 | 10 | public static void Add(string cacheName, ICache cache, bool isDefault = false) 11 | { 12 | sCaches.AddOrUpdate(cacheName, cache, (key, value) => 13 | { 14 | return cache; 15 | }); 16 | 17 | if (isDefault) 18 | { 19 | sDefaultCacheName = cacheName; 20 | } 21 | } 22 | 23 | public static ICache Remove(string cacheName) 24 | { 25 | ICache result; 26 | if (sCaches.TryRemove(cacheName, out result)) 27 | { 28 | return result; 29 | } 30 | return null; 31 | } 32 | 33 | public static void Clear() 34 | { 35 | sCaches.Clear(); 36 | } 37 | 38 | public static ICache Cache(string cacheName = null) 39 | { 40 | if (sCaches.Count > 0) 41 | { 42 | if (cacheName == null) 43 | { 44 | cacheName = sDefaultCacheName; 45 | } 46 | 47 | ICache result; 48 | if (sCaches.TryGetValue(cacheName, out result)) 49 | { 50 | return result; 51 | } 52 | } 53 | return null; 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /docs/structure.md: -------------------------------------------------------------------------------- 1 | # 目录结构 2 | 3 | 本章我们将对框架的整体目录结构做一个简单的介绍,让大家了解框架的目录组成和相关约定规范,方便大家在后面的使用过程中能够更加高效和准确。 4 | 5 | ``` 6 | fastapi.net-project 7 | |-- appsettings.Development.json(开发环境配置文件,可选) 8 | |-- appsettings.json(默认配置文件) 9 | |-- appsettings.Production.json(生产环境配置文件,可选) 10 | |-- Program.cs(框架启动入口文件,必需) 11 | |-- Startup.cs(框架加载初始化文件,必需) 12 | |-- router.xml(路由定义文件,必需) 13 | |-- schedule.xml(可选) 14 | |-- Controllers(控制器目录) 15 | | |-- BaseController.cs(控制器基类,非必要勿动) 16 | | |-- DemoController.cs(用户自定义业务控制器) 17 | |-- Services(业务逻辑目录) 18 | | |-- BaseService.cs(业务逻辑基类,非必要勿动) 19 | | |-- DemoService.cs(用户自定义业务逻辑处理类) 20 | |-- models(数据库模型目录) 21 | | |-- .connection.xml(用户自定义数据连接配置) 22 | | |-- .upgrade.xml(用户自定义数据升级文件,可选) 23 | | |-- demo.model.xml(用户自定义业务数据模型) 24 | |-- Schedules(可选) 25 | |-- DemoJob.cs(用户自定义定时任务) 26 | |-- System(系统级代码,必需,非必要勿修改) 27 | |-- Core 28 | |-- App.cs(系统插件装配和初始化) 29 | |-- CacheLoader.cs(加载系统缓存配置并初始化) 30 | |-- Middlewares 31 | |-- CorsMiddleware.cs(跨域处理中间件) 32 | ``` 33 | 34 | 如上,由框架约定的目录,用户可根据实际业务需要自定义内容: 35 | 36 | - `router.xml` - 用于配置URL路由规则,具体参见[路由(Router)](router.md)。 37 | - `schedule.xml` - 用于配置定时任务及其调度规则,可选,具体参见[定时任务](schedule.md)。 38 | - `appsettings.json`、`appsettings.*.json` - 框架配置文件,具体参见[配置(Config)](config.md)。 39 | - `Controlles/**` - 控制器实现类,解析用户输入,返回相应处理结果,具体参见[控制器(Controller)](controller.md)。 40 | - `Services/**` - 用于放置业务逻辑实现类,可选,建议使用,具体参见[服务(Service)](service.md)。 41 | - `models/.connection.xml` - 数据库连接配置文件,具体参见[数据库操作(ORM)](database.md)。 42 | - `models/.upgrade.xml` - 数据库版本升级文件,具体参见[数据库操作(ORM)](database.md)。 43 | - `models/*.model.xml` - 数据实体模型定义文件,具体参见[数据库操作(ORM)](database.md)。 44 | - `Schedules/**` - 用于放置定时任务实现类,可选,具体参见[定时任务](schedule.md)。 45 | 46 | -------------------------------------------------------------------------------- /libs/JsonResponseWriter/JsonResponse.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Http; 2 | using System; 3 | using System.Text.Encodings.Web; 4 | using System.Text.Json; 5 | using System.Threading.Tasks; 6 | 7 | namespace CodeM.FastApi.Context 8 | { 9 | public static class JsonResponse 10 | { 11 | private static JsonSerializerOptions sJsonSerializerOptions = new JsonSerializerOptions 12 | { 13 | Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping 14 | }; 15 | 16 | private static void CheckContentType(HttpResponse response) 17 | { 18 | if (!response.HasStarted) 19 | { 20 | response.ContentType = "application/json"; 21 | } 22 | } 23 | 24 | public static async Task JsonAsync(this ControllerContext cc, object _data = null) 25 | { 26 | CheckContentType(cc.Response); 27 | 28 | string result = JsonSerializer.Serialize(new 29 | { 30 | code = _data is Exception ? -1 : 0, //0,成功;-1:失败 31 | data = _data is Exception ? null : _data, 32 | error = _data is Exception ? (_data as Exception).Message : null 33 | }, sJsonSerializerOptions); 34 | await cc.Response.WriteAsync(result); 35 | } 36 | 37 | public static async Task JsonAsync(this ControllerContext cc, int _code, object _data = null, string _error = null) 38 | { 39 | CheckContentType(cc.Response); 40 | 41 | string result = JsonSerializer.Serialize(new 42 | { 43 | code = _code, 44 | data = _data, 45 | error = _error 46 | }, sJsonSerializerOptions); 47 | await cc.Response.WriteAsync(result); 48 | } 49 | 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /models/.connection.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | sqlite 4 | fastapi.db 5 | 6 | 7 | 8 | 9 | true 10 | 11 | 20 | 30 | 41 | 50 | 59 | -------------------------------------------------------------------------------- /docs/version.md: -------------------------------------------------------------------------------- 1 | # 版本控制(Version) 2 | 3 | 4 | 5 | ## 哪些场景需要版本控制 6 | 7 | 版本控制功能是一个使用比较低频的功能。一般用户群体较小、生命周期较短的项目型系统不会用到,开发者也不用关心如何实现;只有符合如下特征的系统需要提前考虑版本控制:一是生命周期较长的产品型系统;二是升级之后老版本不会下线,需要和新版本同时运行一段时间的系统;三是系统本身就需要持续维护不同的版本服务不同的用户群。 8 | 9 | 10 | 11 | ## 如何配置版本控制 12 | 13 | ```json 14 | "VersionControl": { 15 | "Enable": true, // 是否开启版本控制 16 | "Default": "v1", // 默认版本号,默认v1 17 | "AllowedVersions": [], // 允许的版本号,默认为空,允许任意版本号 18 | "Param": "version" // 版本参数名,请求时通过Header参数或Query参数传递皆可 19 | } 20 | ``` 21 | 22 | 23 | 24 | ## 框架处理逻辑 25 | 26 | 1. 不传版本号,将使用默认版本号处理。 27 | 2. 设置了允许版本号内容,传递的版本号不在允许的版本号中, 将使用默认版本号处理。 28 | 3. 未设置允许版本号内容,传递的版本号不在允许的版本号中, 报处理方法未找到异常。 29 | 30 | 31 | 32 | ## 不同版本控制器实现方法 33 | 34 | 在路由定义中,指定的API处理方法将作为默认版本号的处理方法;其他版本的处理方法根据默认版本号的处理方法名加上版本号进行选择。 35 | 36 | 假设有如下路由定义: 37 | 38 | ```xml 39 | 40 | ``` 41 | 42 | 控制器实现如下: 43 | 44 | ```c# 45 | namespace CodeM.FastApi.Controllers 46 | { 47 | public class HelloController : BaseController 48 | { 49 | 50 | public async Task Handle(ControllerContext cc) 51 | { 52 | // TODO 53 | await cc.JsonAsync("这是v1版本的返回结果。"); 54 | } 55 | 56 | /// 57 | /// 可通过传入版本号v2参数,测试版本控制功能 58 | /// 59 | /// 60 | /// 61 | public async Task Handle_v2(ControllerContext cc) 62 | { 63 | // TODO 64 | await cc.JsonAsync("这是v2版本的返回结果。"); 65 | } 66 | } 67 | } 68 | ``` 69 | 70 | 当用户使用不同的版本参数访问API时,会根据参数变化返回不同结果: 71 | 72 | 1. 通过`/hello`路由访问,返回`这是v1版本的返回结果。`; 73 | 2. 通过`/hello?version=v2`路由访问,返回`这是v2版本的返回结果。`; 74 | 3. 通过`/hello?version=v1`路由访问,返回`这是v1版本的返回结果。`; 75 | 4. 通过`/hello?version=v3`路由访问,抛出Handle_v3处理方法未找到异常。 76 | 77 | 78 | 79 | ## 为什么不使用新路由代替版本控制 80 | 81 | 使用新路由会给开发者带来如下麻烦: 82 | 83 | 1. 每个新路由都需要进行路由的定义,而版本控制不会; 84 | 2. 前端同一功能因为版本不同需要调用不同的路由,工作量大幅增加; 85 | 3. 当多个API出现版本控制需求时,使用新路由很难统一控制;使用版本控制统一控制参数传递很简单。 -------------------------------------------------------------------------------- /docs/middleware.md: -------------------------------------------------------------------------------- 1 | # 中间件(Middleware) 2 | 3 | 中间件(Middleware)是一个可以处理 HTTP 请求或响应的软件管道。中间件可以在HTTP请求进入真正的业务逻辑处理之前对HTTP请求进行必要的身份验证、IP黑名单、URL过滤等等;也可以在业务逻辑处理完成之后对返回的结果进行结构格式化、日志记录等操作。 4 | 5 | 中间件是有顺序的,HTTP请求到来时,会依据中间件的注册顺序依次通过;返回处理结果时,会按照注册的相反顺序依次通过。整个流程如下图所示: 6 | 7 | ![]( http://res.dayuan.tech/images/middleware.png ) 8 | 9 | 10 | 11 | ## 中间件类型 12 | 13 | 按照作用范围,中间件分为框架中间件和路由中间件两种。 14 | 15 | - 框架中间件 - 对所有的进入框架的HTTP请求起作用。 16 | - 路由中间件 - 只对设置了该中间件的路由起作用。 17 | 18 | 19 | 20 | ## 编写中间件 21 | 22 | 编写中间件没有严格的规范,只需要遵守简单的规则;HTPP请求进入时的拦截方法必须命名为Requset,返回时的拦截方法必须命名为Response;方法有一个唯一参数,类型为[ControllerContext](objects.md)。 23 | 24 | 先看一个简单示例: 25 | 26 | ```c# 27 | using CodeM.FastApi.Context; 28 | using System; 29 | 30 | namespace CodeM.FastApi.System.Middlewares 31 | { 32 | public class DemoMiddleware 33 | { 34 | public bool Request(ControllerContext cc) 35 | { 36 | Console.WriteLine("请求进来喽!"); 37 | return true; 38 | } 39 | 40 | public void Response(ControllerContext cc) 41 | { 42 | Console.WriteLine("请求返回啦!"); 43 | } 44 | } 45 | } 46 | ``` 47 | 48 | 可以看到,Request是一个返回bool值的方法,当返回true时,HTPP请求会继续向下进入接下来的中间件或业务逻辑处理模块;返回false的话,会直接向上返回,不会经过业务逻辑处理模块。 49 | 50 | 51 | 52 | ## 使用中间件 53 | 54 | 中间件编写完成后,我们还需要手动挂载,支持以下方式: 55 | 56 | #### 在框架中使用中间件 57 | 58 | 框架中间件需要在appsettings.json中进行配置,方式如下: 59 | 60 | ```json 61 | { 62 | "AllowedHosts": "*", 63 | 64 | "Compression": { 65 | "Enable": true 66 | }, 67 | 68 | // 中间件配置使用类全名称,多个之间用逗号分隔 69 | "Middlewares": [ "CodeM.FastApi.System.Middlewares.DemoMiddleware" ] 70 | } 71 | ``` 72 | 73 | 74 | 75 | #### 在路由中使用中间件 76 | 77 | 路由中间件需要在router.xml中进行配置,方式如下: 78 | 79 | ```xml 80 | 81 | 82 | 83 | 84 | 85 | ``` 86 | 87 | -------------------------------------------------------------------------------- /libs/AppConfig/Settings/RouterSetting.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace CodeM.FastApi.Config.Settings 4 | { 5 | public class RouterSetting 6 | { 7 | 8 | private int mMaxConcurrentTotal = 65535; 9 | //全部路由最大并发处理器数量 10 | public int MaxConcurrentTotal 11 | { 12 | get 13 | { 14 | return mMaxConcurrentTotal; 15 | } 16 | set 17 | { 18 | if (value < 0) 19 | { 20 | throw new Exception(string.Concat("Router配置项MaxConcurrentTotal必须大于等于0:", value)); 21 | } 22 | mMaxConcurrentTotal = value; 23 | } 24 | } 25 | 26 | private int mMaxIdlePerRouter = 10; 27 | //每个路由最大空闲处理器数量 28 | public int MaxIdlePerRouter 29 | { 30 | get 31 | { 32 | return mMaxIdlePerRouter; 33 | } 34 | set 35 | { 36 | if (value < 0) 37 | { 38 | throw new Exception(string.Concat("Router配置项MaxIdlePerRouter必须大于等于0:", value)); 39 | } 40 | mMaxIdlePerRouter = value; 41 | } 42 | } 43 | 44 | private int mMaxConcurrentPerRouter = 100; 45 | //每个路由最大并发处理器数量 46 | public int MaxConcurrentPerRouter 47 | { 48 | get 49 | { 50 | return mMaxConcurrentPerRouter; 51 | } 52 | set 53 | { 54 | if (value < 0) 55 | { 56 | throw new Exception(string.Concat("Router配置项MaxConcurrentPerRouter必须大于等于0:", value)); 57 | } 58 | mMaxConcurrentPerRouter = value; 59 | } 60 | } 61 | 62 | private int mMaxInvokePerInstance = 10000; 63 | //每个路由处理器实例最大使用次数 64 | public int MaxInvokePerInstance 65 | { 66 | get 67 | { 68 | return mMaxInvokePerInstance; 69 | } 70 | set 71 | { 72 | if (value < 0) 73 | { 74 | throw new Exception(string.Concat("Router配置项MaxInvokePerInstance必须大于等于0:", value)); 75 | } 76 | mMaxInvokePerInstance = value; 77 | } 78 | } 79 | 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /docs/env.md: -------------------------------------------------------------------------------- 1 | # 运行环境 2 | 一个Web应用在生命周期内至少会经过开发、测试、生产几个不同的环境,比较完善的过程管理甚至还会包含预发布环境、验收环境等。同一系统在不同的运行环境中会自动加载不同的配置,以适应不同运行环境的差异化。 3 | 4 | 5 | 6 | ## 指定运行环境 7 | 8 | 框架常用的指定运行环境的方法有3种: 9 | 10 | 1. 在开发调试过程中,可以在`launchSettings.json`中进行如下配置: 11 | 12 | ```json 13 | { 14 | "iisSettings": { 15 | "windowsAuthentication": false, 16 | "anonymousAuthentication": true, 17 | "iisExpress": { 18 | "applicationUrl": "http://localhost:5000", 19 | "sslPort": 0 20 | } 21 | }, 22 | "profiles": { 23 | "IIS Express": { 24 | "commandName": "IISExpress", 25 | "launchBrowser": true, 26 | "environmentVariables": { 27 | "ASPNETCORE_ENVIRONMENT": "Development" // 指定使用开发环境 28 | } 29 | }, 30 | "fastapi": { 31 | "commandName": "Project", 32 | "launchBrowser": true, 33 | "applicationUrl": "http://localhost:5000", 34 | "environmentVariables": { 35 | "ASPNETCORE_ENVIRONMENT": "Development" // 指定使用开发环境配置 36 | } 37 | } 38 | } 39 | } 40 | ``` 41 | 42 | 2. 通过`ASPNETCORE_ENVIRONMENT`环境变量指定运行环境更加方便,比如在生产环境所在的服务器中可以执行如下命令: 43 | 44 | ```shell 45 | //Windows服务器,指定使用开发环境配置 46 | setx ASPNETCORE_ENVIRONMENT "Development" 47 | ``` 48 | ```shell 49 | //MacOS/Linux服务器,指定使用开发环境配置 50 | export ASPNETCORE_ENVIRONMENT=development 51 | ``` 52 | 3. 通过命令行运行时指定`env`参数: 53 | ```shell 54 | //指定使用开发环境配置 55 | dotnet fastapi.dll env=Development 56 | ``` 57 | 58 | 59 | ## 应用内获取运行环境 60 | 61 | 1. 获取当前运行环境的名称,可以使用系统提供的获取环境变量的方法: 62 | 63 | ```c# 64 | string envName = FastApiUtils.GetEnvironmentName(); 65 | ``` 66 | 67 | 2. 判断当前是否某个特定的运行环境,可以使用公共库提供的方法: 68 | 69 | ```c# 70 | if (FastApiUtils.IsDev()) //是否开发调试环境 71 | { 72 | //TODO 73 | } 74 | 75 | if (FastApiUtils.IsProd()) //是否生产环境 76 | { 77 | //TODO 78 | } 79 | 80 | if (FastApiUtils.IsEnv("Test")) //是否测试环境,可判断任何自定义环境名称 81 | { 82 | //TODO 83 | } 84 | ``` 85 | 86 | 87 | 88 | ## 运行环境相关配置 89 | 90 | 不同的运行环境会对应不同的配置,具体请阅读 [配置(Config)](config.md)。 91 | 92 | 93 | 94 | ## 自定义环境 95 | 96 | 常规开发流程可能不仅仅只有以上几种运行环境,fastapi.net 支持自定义环境来适应自己的开发流程。 97 | 98 | 比如,要为开发流程增加集成测试环境 TEST。将 `ASPNETCORE_ENVIRONMENT` 设置成 `test`,启动时会加载 `appsettings.test.json`,此时,使用环境判断方法`FastApiUtils.IsEnv("test")`将返回`true`。 -------------------------------------------------------------------------------- /libs/Logger/ControllerContextLogExtension.cs: -------------------------------------------------------------------------------- 1 | using CodeM.FastApi.Log; 2 | using System; 3 | 4 | namespace CodeM.FastApi.Context 5 | { 6 | public static class ControllerContextLogExtension 7 | { 8 | 9 | public static void Trace(this ControllerContext cc, string message, params object[] args) 10 | { 11 | string callerName = Logger.GetCallerName(1); 12 | Logger.Instance().Trace(callerName, message, args); 13 | } 14 | 15 | public static void Debug(this ControllerContext cc, string message, params object[] args) 16 | { 17 | string callerName = Logger.GetCallerName(1); 18 | Logger.Instance().Debug(callerName, message, args); 19 | } 20 | 21 | public static void Info(this ControllerContext cc, string message, params object[] args) 22 | { 23 | string callerName = Logger.GetCallerName(1); 24 | Logger.Instance().Info(callerName, message, args); 25 | } 26 | 27 | public static void Warn(this ControllerContext cc, string message, params object[] args) 28 | { 29 | string callerName = Logger.GetCallerName(1); 30 | Logger.Instance().Warn(callerName, message, args); 31 | } 32 | 33 | public static void Warn(this ControllerContext cc, Exception exp) 34 | { 35 | string callerName = Logger.GetCallerName(1); 36 | Logger.Instance().Warn(callerName, exp); 37 | } 38 | 39 | public static void Error(this ControllerContext cc, string message, params object[] args) 40 | { 41 | string callerName = Logger.GetCallerName(1); 42 | Logger.Instance().Error(callerName, message, args); 43 | } 44 | 45 | public static void Error(this ControllerContext cc, Exception exp) 46 | { 47 | string callerName = Logger.GetCallerName(1); 48 | Logger.Instance().Error(callerName, exp); 49 | } 50 | 51 | /// 52 | /// 致命错误 53 | /// 54 | /// 55 | /// 56 | public static void Fatal(this ControllerContext cc, string message, params object[] args) 57 | { 58 | string callerName = Logger.GetCallerName(1); 59 | Logger.Instance().Fatal(callerName, message, args); 60 | } 61 | 62 | public static void Fatal(this ControllerContext cc, Exception exp) 63 | { 64 | string callerName = Logger.GetCallerName(1); 65 | Logger.Instance().Fatal(callerName, exp); 66 | } 67 | 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /libs/ControllerContext/Params/PostForms.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Http; 2 | using Microsoft.Extensions.Primitives; 3 | 4 | namespace CodeM.FastApi.Context.Params 5 | { 6 | public class PostForms 7 | { 8 | IFormCollection mForm; 9 | 10 | public PostForms(IFormCollection form) 11 | { 12 | mForm = form; 13 | } 14 | 15 | public string this[string key] 16 | { 17 | get 18 | { 19 | if (mForm != null) 20 | { 21 | StringValues result; 22 | if (mForm.TryGetValue(key, out result)) 23 | { 24 | return string.Join(",", result); 25 | } 26 | } 27 | return null; 28 | } 29 | } 30 | 31 | public string this[int index] 32 | { 33 | get 34 | { 35 | if (mForm != null) 36 | { 37 | if (index >= 0 && index < mForm.Count) 38 | { 39 | string[] keys = new string[mForm.Count]; 40 | mForm.Keys.CopyTo(keys, 0); 41 | string key = keys[index]; 42 | StringValues result; 43 | if (mForm.TryGetValue(key, out result)) 44 | { 45 | return string.Join(",", result); 46 | } 47 | } 48 | } 49 | return null; 50 | } 51 | } 52 | 53 | public bool ContainsKey(string key) 54 | { 55 | if (mForm != null) 56 | { 57 | return mForm.ContainsKey(key); 58 | } 59 | return false; 60 | } 61 | 62 | public string Get(string key, string defaultValue) 63 | { 64 | if (ContainsKey(key)) 65 | { 66 | return this[key]; 67 | } 68 | return defaultValue; 69 | } 70 | 71 | public string Get(int index, string defaultValue) 72 | { 73 | if (mForm != null) 74 | { 75 | if (index >= 0 && index < mForm.Count) 76 | { 77 | return this[index]; 78 | } 79 | } 80 | return defaultValue; 81 | } 82 | 83 | public int Count 84 | { 85 | get 86 | { 87 | if (mForm != null) 88 | { 89 | return mForm.Count; 90 | } 91 | return 0; 92 | } 93 | } 94 | 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /libs/Logger/File/FileLogger.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.Configuration; 2 | using Microsoft.Extensions.Logging; 3 | using System; 4 | 5 | namespace CodeM.FastApi.Log.File 6 | { 7 | public class FileLogger : ILogger, IDisposable 8 | { 9 | public FileLogger(IConfigurationSection options, string categoryName) 10 | { 11 | FileWriter.Init(options); 12 | this.CategoryName = categoryName; 13 | } 14 | 15 | public IDisposable BeginScope(TState state) 16 | { 17 | return this; 18 | } 19 | 20 | public string CategoryName { get; set; } 21 | 22 | public bool IsEnabled(LogLevel logLevel) 23 | { 24 | return logLevel != LogLevel.None; 25 | } 26 | 27 | public void Log(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func formatter) 28 | { 29 | if (!IsEnabled(logLevel)) 30 | { 31 | return; 32 | } 33 | 34 | if (formatter == null) 35 | { 36 | throw new ArgumentNullException(nameof(formatter)); 37 | } 38 | 39 | var result = $"{ GetLogLevelString(logLevel) }: {this.CategoryName}[{eventId}] - {DateTime.Now}{Environment.NewLine}"; 40 | 41 | var message = formatter(state, null); 42 | if (!string.IsNullOrWhiteSpace(message)) 43 | { 44 | result += (" " + message).Replace(Environment.NewLine, Environment.NewLine + " "); 45 | } 46 | 47 | if (exception != null) 48 | { 49 | if (!string.IsNullOrWhiteSpace(message)) 50 | { 51 | result += Environment.NewLine; 52 | } 53 | result += (" " + exception).Replace(Environment.NewLine, Environment.NewLine + " "); 54 | } 55 | 56 | FileWriter.Write(result); 57 | } 58 | private string GetLogLevelString(LogLevel logLevel) 59 | { 60 | switch (logLevel) 61 | { 62 | case LogLevel.Trace: 63 | return "trce"; 64 | case LogLevel.Debug: 65 | return "dbug"; 66 | case LogLevel.Information: 67 | return "info"; 68 | case LogLevel.Warning: 69 | return "warn"; 70 | case LogLevel.Error: 71 | return "fail"; 72 | case LogLevel.Critical: 73 | return "crit"; 74 | default: 75 | throw new ArgumentOutOfRangeException(nameof(logLevel)); 76 | } 77 | } 78 | 79 | public void Dispose() 80 | { 81 | } 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /libs/ControllerContext/Params/HeaderParams.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Http; 2 | using Microsoft.Extensions.Primitives; 3 | 4 | namespace CodeM.FastApi.Context.Params 5 | { 6 | public class HeaderParams 7 | { 8 | 9 | IHeaderDictionary mData; 10 | 11 | public HeaderParams(IHeaderDictionary data) 12 | { 13 | mData = data; 14 | } 15 | 16 | public string this[string key] 17 | { 18 | get 19 | { 20 | if (mData != null) 21 | { 22 | StringValues result; 23 | if (mData.TryGetValue(key, out result)) 24 | { 25 | string values = string.Join(",", result); 26 | return values; 27 | } 28 | } 29 | return null; 30 | } 31 | } 32 | 33 | public string this[int index] 34 | { 35 | get 36 | { 37 | if (mData != null) 38 | { 39 | if (index >= 0 && index < mData.Keys.Count) 40 | { 41 | string[] keys = new string[mData.Keys.Count]; 42 | mData.Keys.CopyTo(keys, 0); 43 | string key = keys[index]; 44 | StringValues result; 45 | if (mData.TryGetValue(key, out result)) 46 | { 47 | string values = string.Join(",", result); 48 | return values; 49 | } 50 | } 51 | } 52 | return null; 53 | } 54 | } 55 | 56 | public bool ContainsKey(string key) 57 | { 58 | if (mData != null) 59 | { 60 | return mData.ContainsKey(key); 61 | } 62 | return false; 63 | } 64 | 65 | public string Get(string key, string defaultValue) 66 | { 67 | if (ContainsKey(key)) 68 | { 69 | return this[key]; 70 | } 71 | return defaultValue; 72 | } 73 | 74 | public string Get(int index, string defaultValue) 75 | { 76 | if (mData != null) 77 | { 78 | if (index >= 0 && index < mData.Keys.Count) 79 | { 80 | return this[index]; 81 | } 82 | } 83 | return defaultValue; 84 | } 85 | 86 | public int Count 87 | { 88 | get 89 | { 90 | if (mData != null) 91 | { 92 | return mData.Count; 93 | } 94 | return 0; 95 | } 96 | } 97 | 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /System/Utils/CorsUtils.cs: -------------------------------------------------------------------------------- 1 | using CodeM.FastApi.Config.Settings; 2 | using CodeM.FastApi.System.Core; 3 | using Microsoft.AspNetCore.Http; 4 | using Microsoft.Extensions.Primitives; 5 | using System; 6 | using System.Linq; 7 | 8 | namespace CodeM.FastApi.System.Utils 9 | { 10 | public class CorsUtils 11 | { 12 | public static string GetAllowOrigin() 13 | { 14 | CorsSetting corsSetting = Application.Instance().Config().Cors; 15 | 16 | string allowOrigin = "*"; 17 | if (corsSetting.Options.AllowSites != null && 18 | !corsSetting.Options.AllowSites.Contains("*")) 19 | { 20 | StringValues origins; 21 | if (CurrentContext.Context.Request.Headers.TryGetValue("Origin", out origins)) 22 | { 23 | foreach (string item in origins) 24 | { 25 | if (corsSetting.Options.AllowSites.Contains(item, 26 | StringComparer.OrdinalIgnoreCase)) 27 | { 28 | allowOrigin = item; 29 | break; 30 | } 31 | } 32 | } 33 | else 34 | { 35 | return corsSetting.Options.AllowSites[0]; 36 | } 37 | } 38 | return allowOrigin; 39 | } 40 | 41 | public static string GetAllowMethods() 42 | { 43 | CorsSetting corsSetting = Application.Instance().Config().Cors; 44 | 45 | string allowMethods = "*"; 46 | if (corsSetting.Options.AllowMethods != null) 47 | { 48 | allowMethods = string.Join(",", corsSetting.Options.AllowMethods).ToUpper(); 49 | } 50 | if (allowMethods.Contains("*")) 51 | { 52 | allowMethods = "GET,POST,HEAD,PATCH,PUT,DELETE,OPTIONS,TRACE,CONNECT"; 53 | } 54 | return allowMethods; 55 | } 56 | 57 | public static string GetAllowHeaders() 58 | { 59 | return CurrentContext.Context.Request.Headers["Access-Control-Request-Headers"]; 60 | } 61 | 62 | public static string GetAllowCredentials() 63 | { 64 | CorsSetting corsSetting = Application.Instance().Config().Cors; 65 | return corsSetting.Options.SupportsCredentials ? "true" : "false"; 66 | } 67 | 68 | public static void SetCorsHeaders(HttpContext context) 69 | { 70 | SetCorsHeaders(context.Response); 71 | } 72 | 73 | public static void SetCorsHeaders(HttpResponse response) 74 | { 75 | response.Headers.Add("Access-Control-Allow-Origin", GetAllowOrigin()); 76 | response.Headers.Add("Access-Control-Allow-Methods", GetAllowMethods()); 77 | response.Headers.Add("Access-Control-Allow-Headers", GetAllowHeaders()); 78 | response.Headers.Add("Access-Control-Allow-Credentials", GetAllowCredentials()); 79 | } 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /docs/schedule.md: -------------------------------------------------------------------------------- 1 | # 定时任务 2 | 3 | 虽然我们通过框架开发的 HTTP Server 是请求响应模型的,但是仍然还会有许多场景需要执行一些定时任务,例如: 4 | 5 | 1. 定时上报应用状态。 6 | 2. 定时从远程接口更新本地缓存。 7 | 3. 定时进行文件切割、临时文件删除。 8 | 9 | 框架提供了一套机制来让定时任务的编写和维护更加优雅。 10 | 11 | 12 | 13 | ## 编写定时任务 14 | 15 | 所有的定时任务都统一存放在 `Schedules` 目录下,每一个文件都是一个独立的定时任务,可以配置定时任务的属性和要执行的方法。 16 | 17 | 定时任务的实现基于开源的Quartz作业调度框架,所有的定时任务实现类都需要实现IJob接口。 18 | 19 | 一个简单的例子,我们定义一个定时打印时间的任务,就可以在Schedules目录下创建一个DemoJob.cs类文件 20 | 21 | ```c# 22 | public class DemoJob : IJob 23 | { 24 | public Task Execute(IJobExecutionContext context) 25 | { 26 | Console.WriteLine("Hello,现在时间是 " + DateTime.Now); 27 | return Task.CompletedTask; 28 | } 29 | } 30 | ``` 31 | 32 | 上面的代码实现了定时打印时间的任务,但此时任务还不会运行,必须在schedule.xml中进行任务配置才能正常启动定时任务。 33 | 34 | 35 | 36 | ## 配置定时任务 37 | 38 | 定时任务的配置全部在schedule.xml文件中进行定义。 39 | 定时任务可以指定 interval 或者 cron 两种不同的定时方式。 40 | #### interval 41 | 42 | ```xml 43 | 44 | 45 | 46 | ``` 47 | 48 | id:定时任务唯一编码。 49 | 50 | interval:定时任务的间隔时间,支持的单位:ms(毫秒)、s(秒)、m(分钟)、h(小时)、d(天)。 51 | 52 | repeat:定时任务执行的次数,默认为0(无限次)。 53 | 54 | class:定时任务的实现类全名称。 55 | 56 | 57 | 58 | #### cron 59 | 60 | 通过cron参数来配置定时任务的执行时机,定时任务将会按照 cron 表达式在特定的时间点执行。 61 | 62 | ```xml 63 | 64 | 65 | 66 | ``` 67 | 68 | id:定时任务唯一编码。 69 | 70 | cron:cron表达式,具体cron表达式格式可百度搜索学习。 71 | 72 | class:定时任务的实现类全名称。 73 | 74 | 75 | 76 | #### 其他参数 77 | 78 | disable: 配置该参数为 true 时,定时任务不会被启动;后续可通过代码进行手动启动。 79 | 80 | env: 仅在指定的环境下才启动该定时任务,多个环境逗号分隔。 81 | 82 | 83 | 84 | ## 手动执行定时任务 85 | 多有的定时任务在启动后都可以通过代码的方式进行控制;需要注意的是,env运行环境不匹配的定时任务,无法启动和控制。 86 | 87 | 系统提供了Run、Shutdown、ResumeAll、PauseAll、StartJob、StopJob等方法进行定时任务的启动、停止等控制。 88 | 89 | ###### bool Run() 90 | 91 | 描述:启动加载后的所有disable为false并且匹配当前env运行环境的定时任务。 92 | 93 | 参数:无。 94 | 95 | 返回:成功返回true;否则,返回false。 96 | 97 | 98 | 99 | ###### bool Shutdown() 100 | 101 | 描述:终止运行所有的定时任务,并清理释放相关资源;终止后无法重新启动启动。 102 | 103 | 参数:无。 104 | 105 | 返回:成功返回true;否则,返回false。 106 | 107 | 108 | 109 | ###### bool ResumeAll() 110 | 111 | 描述:恢复所有暂定运行的定时任务。 112 | 113 | 参数:无。 114 | 115 | 返回:成功返回true;否则,返回false。 116 | 117 | 118 | 119 | ###### bool PauseAll() 120 | 121 | 描述:暂停所有运行中的定时任务;暂停后,可通过ResumeAll恢复运行。 122 | 123 | 参数:无。 124 | 125 | 返回:成功返回true;否则,返回false。 126 | 127 | 128 | 129 | ###### bool StartJob(string jobId) 130 | 131 | 描述:启动运行指定Id的定时任务。 132 | 133 | 参数: 134 | 135 | ​ jobId:要启动的定时任务的Id。 136 | 137 | 返回:成功返回true;否则,返回false。 138 | 139 | 140 | 141 | ###### bool StopJob(string jobId) 142 | 143 | 描述:停止运行指定Id的定时任务。 144 | 145 | 参数: 146 | 147 | ​ jobId:要启动的定时任务的Id。 148 | 149 | 返回:成功返回true;否则,返回false。 150 | 151 | 152 | 153 | 实际开发中,可以通过App全局对象获取系统的定时任务管理实例。 154 | ```c# 155 | //启动Id为demo的定时任务 156 | Application.Instance().Schedule().StartJob("demo"); 157 | 158 | //停止Id为demo的定时任务 159 | Application.Instance().Schedule().StopJob("demo"); 160 | ``` 161 | 162 | -------------------------------------------------------------------------------- /libs/ControllerContext/Wrappers/CookieWrapper.cs: -------------------------------------------------------------------------------- 1 | using CodeM.Common.Tools; 2 | using Microsoft.AspNetCore.Http; 3 | 4 | namespace CodeM.FastApi.Context.Wrappers 5 | { 6 | public class CookieWrapper 7 | { 8 | private HttpContext mContext; 9 | private string mKeys = null; 10 | 11 | public CookieWrapper(HttpContext context, string keys = null) 12 | { 13 | mContext = context; 14 | mKeys = keys; 15 | } 16 | 17 | public void Set(string key, string value, CookieOptionsExt options = null) 18 | { 19 | if (mContext != null && mContext.Response != null && 20 | mContext.Response.Cookies != null) 21 | { 22 | if (options != null) 23 | { 24 | if (options.Encrypt && !string.IsNullOrWhiteSpace(mKeys)) 25 | { 26 | string[] keyItems = mKeys.Split(","); 27 | if (keyItems.Length > 0) 28 | { 29 | value = Xmtool.Crypto().AESEncode(value, keyItems[0].Trim()); 30 | } 31 | } 32 | mContext.Response.Cookies.Append(key, value, options); 33 | } 34 | else 35 | { 36 | mContext.Response.Cookies.Append(key, value); 37 | } 38 | } 39 | } 40 | 41 | public string Get(string key, CookieOptionsExt options = null) 42 | { 43 | if (mContext != null && mContext.Request != null && 44 | mContext.Request.Cookies != null && 45 | mContext.Request.Cookies.ContainsKey(key)) 46 | { 47 | string value; 48 | if (mContext.Request.Cookies.TryGetValue(key, out value)) 49 | { 50 | if (options != null) 51 | { 52 | if (options.Encrypt && !string.IsNullOrWhiteSpace(mKeys)) 53 | { 54 | string[] keyItems = mKeys.Split(","); 55 | foreach (string keyItem in keyItems) 56 | { 57 | try 58 | { 59 | value = Xmtool.Crypto().AESDecode(value, keyItem.Trim()); 60 | break; 61 | } 62 | catch 63 | { 64 | ; 65 | } 66 | } 67 | } 68 | } 69 | return value; 70 | } 71 | } 72 | return null; 73 | } 74 | 75 | public void Delete(string key) 76 | { 77 | if (mContext != null && mContext.Response != null && 78 | mContext.Response.Cookies != null) 79 | { 80 | mContext.Response.Cookies.Delete(key); 81 | } 82 | } 83 | 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /libs/Common/FastApiUtils.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Concurrent; 3 | using System.Collections.Generic; 4 | using System.Reflection; 5 | 6 | namespace CodeM.FastApi.Common 7 | { 8 | public static class FastApiUtils 9 | { 10 | private static string sEnvironmentName = null; 11 | public static void SetEnvironmentName(string envName) 12 | { 13 | sEnvironmentName = envName; 14 | } 15 | 16 | public static string GetEnvironmentName() 17 | { 18 | if (string.IsNullOrWhiteSpace(sEnvironmentName)) 19 | { 20 | return Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT"); 21 | } 22 | return sEnvironmentName; 23 | } 24 | 25 | private static bool sIsDevelopmentChecked = false; 26 | private static bool sIsDevelopment = false; 27 | public static bool IsDev() 28 | { 29 | if (!sIsDevelopmentChecked) 30 | { 31 | string env = GetEnvironmentName(); 32 | sIsDevelopment = "Development".Equals(env, StringComparison.OrdinalIgnoreCase); 33 | sIsDevelopmentChecked = true; 34 | } 35 | return sIsDevelopment; 36 | } 37 | 38 | private static bool sIsProductionChecked = false; 39 | private static bool sIsProduction = false; 40 | public static bool IsProd() 41 | { 42 | if (!sIsProductionChecked) 43 | { 44 | string env = GetEnvironmentName(); 45 | sIsProduction = "Production".Equals(env, StringComparison.OrdinalIgnoreCase); 46 | sIsProductionChecked = true; 47 | } 48 | return sIsProduction; 49 | } 50 | 51 | private readonly static ConcurrentDictionary sEnvs = new ConcurrentDictionary(); 52 | public static bool IsEnv(string envName) 53 | { 54 | string env = GetEnvironmentName(); 55 | string key = env != null ? env.ToLower() : ""; 56 | if (!sEnvs.TryGetValue(key, out bool bRet)) 57 | { 58 | bRet = key.Equals(envName, StringComparison.OrdinalIgnoreCase); 59 | sEnvs.TryAdd(key, bRet); 60 | } 61 | return bRet; 62 | } 63 | 64 | private readonly static Dictionary sTypeMethods = new Dictionary(); 65 | public static bool IsMethodExists(Type _typ, string method) 66 | { 67 | string key = string.Concat(_typ.FullName, "`", method); 68 | if (!sTypeMethods.ContainsKey(key)) 69 | { 70 | MethodInfo mi = _typ.GetMethod(method, 71 | BindingFlags.Instance | BindingFlags.Public | 72 | BindingFlags.Static | BindingFlags.IgnoreCase); 73 | sTypeMethods[key] = mi != null; 74 | } 75 | return sTypeMethods[key]; 76 | } 77 | 78 | public static bool IsMethodExists(object obj, string method) 79 | { 80 | Type _typ = obj.GetType(); 81 | return IsMethodExists(_typ, method); 82 | } 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /fastapi.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net8.0 5 | 6 | a6af7ceb-db91-4c17-b2f2-e4009bc1e53a 7 | 快速、轻量级、跨平台、自动化API框架 8 | CodeM.FastApi 9 | true 10 | softwaiter 11 | False 12 | True 13 | True 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | libs\Third\Kdbndp.dll 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | -------------------------------------------------------------------------------- /System/Core/Application.cs: -------------------------------------------------------------------------------- 1 | using CodeM.Common.Ioc; 2 | using CodeM.Common.Orm; 3 | using CodeM.FastApi.Cache; 4 | using CodeM.FastApi.Config; 5 | using CodeM.FastApi.DbUpgrade; 6 | using CodeM.FastApi.Log; 7 | using CodeM.FastApi.Schedule; 8 | using System; 9 | using System.IO; 10 | using System.Text.RegularExpressions; 11 | 12 | namespace CodeM.FastApi.System.Core 13 | { 14 | public class Application 15 | { 16 | private static ApplicationConfig sAppConfig = null; 17 | private static ScheduleManager sScheduleManager = null; 18 | private static Regex sThirdDot = new Regex("\\.[^\\.]*\\.[^\\.]*\\.[^\\.]*$"); 19 | 20 | private static Application sSingleInst = new Application(); 21 | 22 | public static void Init(ApplicationConfig cfg, string scheduleFile) 23 | { 24 | InitOrm(AppDomain.CurrentDomain.BaseDirectory); 25 | CacheLoader.Load(cfg); 26 | 27 | sAppConfig = cfg; 28 | 29 | Console.WriteLine("加载定时任务配置文件......"); 30 | sScheduleManager = new ScheduleManager(scheduleFile); 31 | } 32 | 33 | private static void InitOrm(string contentRootPath) 34 | { 35 | //ORM模型库初始化 36 | Derd.ModelPath = Path.Combine(contentRootPath, "models"); 37 | Console.WriteLine("加载ORM模型定义文件:" + Derd.ModelPath); 38 | Derd.Load(); 39 | 40 | Exception exp; 41 | if (!Derd.TryCreateTables(false, true, out exp)) 42 | { 43 | throw exp; 44 | } 45 | 46 | //ORM版本控制 47 | UpgradeManager.EnableVersionControl(); 48 | Console.WriteLine("执行ORM模型升级操作......"); 49 | UpgradeManager.Upgrade(Path.Combine(contentRootPath, "models")); 50 | } 51 | 52 | private Application() 53 | { 54 | } 55 | 56 | public static Application Instance() 57 | { 58 | return sSingleInst; 59 | } 60 | 61 | public string ContentRootPath 62 | { 63 | get 64 | { 65 | return AppDomain.CurrentDomain.BaseDirectory; 66 | } 67 | } 68 | 69 | private string mAddress = string.Empty; 70 | public string Address 71 | { 72 | get 73 | { 74 | return mAddress; 75 | } 76 | internal set 77 | { 78 | mAddress = value; 79 | } 80 | } 81 | 82 | public ApplicationConfig Config() 83 | { 84 | return sAppConfig; 85 | } 86 | 87 | public ScheduleManager Schedule() 88 | { 89 | return sScheduleManager; 90 | } 91 | 92 | public ICache Cache(string cacheName = null) 93 | { 94 | return CacheManager.Cache(cacheName); 95 | } 96 | 97 | public Logger Log() 98 | { 99 | return Logger.Instance(); 100 | } 101 | 102 | public T Service(bool singleton = true) 103 | { 104 | string serviceFullName = typeof(T).FullName; 105 | if (singleton) 106 | { 107 | return (T)Wukong.GetSingleObject(serviceFullName); 108 | } 109 | else 110 | { 111 | return (T)Wukong.GetObject(serviceFullName); 112 | } 113 | } 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /docs/session.md: -------------------------------------------------------------------------------- 1 | # 会话(Session) 2 | 3 | 大家都知道,在Web应用中,接口服务和浏览器页面之间的通信通常是基于HTTP协议的,而HTTP协议是无状态的,也就是各个请求之间是相互独立的。但一个真正的Web应用是解决某种具体业务需求的,而业务需求之间一定是有各种各样的依赖关系,需要通过某种状态进行关联的。如:一个人浏览电商网站,将某件商品放入购物车,进入购物车进行结算操作;在这里,将商品放入购物车,对购物车商品进行结算是两个独立的接口服务,但它们必须保持某种关系,整个购买操作才能正确完成,这种关系包括是谁选择了哪件商品放入了购物车,是谁进入购物车做了结算操作;而Session就是为了解决这种问题,Session是一种身份标识,通过这种身份,我们就可以在不同的接口服务中按照相同的身份进行业务关联和状态保持。 4 | 5 | 6 | 7 | ## 如何启用Session 8 | 9 | 在框架中,Session默认是启用状态;如果想修改Session启用状态,可以通过appsettings.json文件进行配置。 10 | 11 | ```json 12 | "Session": { 13 | "Enable": false //是否启用Session,默认为true,启用。 14 | } 15 | ``` 16 | 17 | 18 | 19 | ## Session配置说明 20 | 21 | ```json 22 | "Session": { 23 | "Enable": true, 24 | "Timeout": "20m", 25 | "Cookie": { 26 | "Name": "fastapi.sid", 27 | "SameSite": "None", 28 | "Secure": "None", 29 | "HttpOnly": true, 30 | "MaxAge": "20m" 31 | } 32 | } 33 | ``` 34 | 35 | Enable:是否启用Session,默认为true。 36 | 37 | Timeout:session的空闲过期时间,即session没有使用的情况下能保持多长时间;默认20m,单位:ms-毫秒,s-秒,m-分钟,h-小时,d-天。 38 | 39 | Cookie:浏览器端存储session会话cookie的相关设置。 40 | 41 | ​ Name:Cookie的名称,默认fastapi.sid。 42 | 43 | ​ SameSite:跨站请求时Cookie策略(None,Lax,Strict),默认None。 44 | 45 | ​ Secure:是否只在HTTPS安全协议下传输(SameAsRequest,Always,None),默认None。 46 | 47 | ​ HttpOnly:是否禁止通过Javascript读取cookie,默认true。 48 | 49 | ​ MaxAge:Cookie有效期,默认未设置(有效期至浏览器关闭);可根据需要设置相对过期时间,单位:ms-毫秒,s-秒,m-分钟,h-小时,d-天。 50 | 51 | 52 | 53 | ## Redis分布式Session 54 | 55 | 为了框架在分布式部署时对Session一致性的支持,用户可以选择使用Redis对Session进行存储,这样不同的服务节点可以保证使用相同的Session数据,避免Session不同引起业务混乱;默认Session是存在进程内存中,只有当前服务本身能进行访问;且进程重启后,Session数据将丢失;而Redis分布式Session能解决这些问题。 56 | 57 | ```json 58 | "Session": { 59 | "Redis": { 60 | "Enable": false, 61 | "Host": "127.0.0.1", 62 | "Port": 6379, 63 | "Database": 0, 64 | "Password": "root", 65 | "Retry": 3, 66 | "Timeout": 5000, 67 | "Ssl": false, 68 | "SslHost": "", 69 | "SslProtocols": "" 70 | } 71 | } 72 | ``` 73 | 74 | Enable:是否使用Redis存储Session,默认false。 75 | 76 | Host:Redis服务的主机地址,默认127.0.0.1。 77 | 78 | Port:Redis服务的端口号,默认6379。 79 | 80 | Database:使用的Redis数据库,默认0。 81 | 82 | Password:Redis服务的访问口令,默认空。 83 | 84 | Retry:Redis服务连接的重试次数,默认3。 85 | 86 | Timeout:Redis服务连接的超时时间,单位毫秒,默认5000。 87 | 88 | Ssl:是否使用Ssl加密连接,默认false。 89 | 90 | SslHost:Redis服务的Ssl访问主机地址,默认空。 91 | 92 | SslProtocols:Ssl的安全协议版本,默认空。 93 | 94 | ​ None - 允许操作系统选择要使用的最佳协议,并将其用于阻止不安全的协议。 应使用此字段,除非应用有特定原因不得使用此字段。 95 | ​ Ssl2 - 指定 SSL 2.0 协议。 SSL 2.0 已由 TLS 协议取代,之所以仍然提供这个方法,只是为了向后兼容。 96 | ​ Ssl3 - 指定 SSL 3.0 协议。 SSL 3.0 已由 TLS 协议取代,之所以仍然提供这个方法,只是为了向后兼容。 97 | ​ Tls - 指定 TLS 1.0 安全协议。 提供 TLS 1.0 只是为了实现向后兼容性。 TLS 协议在 IETF RFC 2246 中定义。 98 | ​ Tls11 - 指定 TLS 1.1 安全协议。 TLS 协议在 IETF RFC 4346 中定义。 99 | ​ Tls12 - 指定 TLS 1.2 安全协议。 TLS 协议在 IETF RFC 5246 中定义。 100 | ​ Tls13 - 指定 TLS 1.3 安全协议。 此 TLS 协议在 IETF RFC 8446 定义。 101 | 102 | 103 | 104 | ## 代码中使用Session 105 | 106 | 在框架层面,用户可以全程获得ControllerContext对象,该对象下的Session对象 提供了方法可以方便的操作Session: 107 | 108 | ```c# 109 | public async Task Handle(ControllerContext cc) 110 | { 111 | cc.Session.SetString("userid", "wangxm"); //存储一条session信息 112 | await cc.JsonAsync("Hello World."); 113 | } 114 | ``` 115 | 116 | - ControllerContext.Session.SetString(string key, string value) - 存储一条字符串Session信息。 117 | - ControllerContext.Session.GetString(string key) - 获取Session指定key的Session信息,返回类型string。 118 | - ControllerContext.Session.SetInt32(string key, Int32 value) - 存储一条32位整型的Session信息。 119 | - ControllerContext.Session.GetInt32(string key) - 获取Session指定key的Session信息,返回了类型Int32?,允许null值。 120 | - ControllerContext.Session.SetBoolean(string key, bool value) - 存储一条布尔型Session信息。 121 | - ControllerContext.Session.GetBoolean(string key) - 获取Session指定key的Session信息,返回类型Boolean。 -------------------------------------------------------------------------------- /libs/Cache/ICache.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | 3 | namespace CodeM.FastApi.Cache 4 | { 5 | public interface ICache 6 | { 7 | void SetString(string key, string value); 8 | 9 | Task SetStringAsync(string key, string value); 10 | 11 | void SetString(string key, string value, long seconds, ExpirationType type = ExpirationType.RelativeToNow); 12 | 13 | Task SetStringAsync(string key, string value, long seconds, ExpirationType type = ExpirationType.RelativeToNow); 14 | 15 | string GetString(string key); 16 | 17 | Task GetStringAsync(string key); 18 | 19 | void SetInt32(string key, int value); 20 | 21 | Task SetInt32Async(string key, int value); 22 | 23 | void SetInt32(string key, int value, long seconds, ExpirationType type = ExpirationType.RelativeToNow); 24 | 25 | Task SetInt32Async(string key, int value, long seconds, ExpirationType type = ExpirationType.RelativeToNow); 26 | 27 | int? GetInt32(string key); 28 | 29 | Task GetInt32Async(string key); 30 | 31 | void SetInt64(string key, long value); 32 | 33 | Task SetInt64Async(string key, long value); 34 | 35 | void SetInt64(string key, long value, long seconds, ExpirationType type = ExpirationType.RelativeToNow); 36 | 37 | Task SetInt64Async(string key, long value, long seconds, ExpirationType type = ExpirationType.RelativeToNow); 38 | 39 | long? GetInt64(string key); 40 | 41 | Task GetInt64Async(string key); 42 | 43 | void SetDouble(string key, double value); 44 | 45 | Task SetDoubleAsync(string key, double value); 46 | 47 | void SetDouble(string key, double value, long seconds, ExpirationType type = ExpirationType.RelativeToNow); 48 | 49 | Task SetDoubleAsync(string key, double value, long seconds, ExpirationType type = ExpirationType.RelativeToNow); 50 | 51 | double? GetDouble(string key); 52 | 53 | Task GetDoubleAsync(string key); 54 | 55 | void SetBoolean(string key, bool value); 56 | 57 | Task SetBooleanAsync(string key, bool value); 58 | 59 | void SetBoolean(string key, bool value, long seconds, ExpirationType type = ExpirationType.RelativeToNow); 60 | 61 | Task SetBooleanAsync(string key, bool value, long seconds, ExpirationType type = ExpirationType.RelativeToNow); 62 | 63 | bool GetBoolean(string key); 64 | 65 | Task GetBooleanAsync(string key); 66 | 67 | void Set(string key, byte[] value); 68 | 69 | Task SetAsync(string key, byte[] value); 70 | 71 | void Set(string key, byte[] value, long seconds, ExpirationType type = ExpirationType.RelativeToNow); 72 | 73 | Task SetAsync(string key, byte[] value, long seconds, ExpirationType type = ExpirationType.RelativeToNow); 74 | 75 | byte[] Get(string key); 76 | 77 | Task GetAsync(string key); 78 | 79 | bool ContainsKey(string key); 80 | 81 | bool TryGetValue(string key, out T result); 82 | 83 | void Remove(string key); 84 | 85 | Task RemoveAsync(string key); 86 | 87 | void MultiSet(string key, params object[] values); 88 | 89 | void MultiSet2(string key, long seconds, ExpirationType type, params object[] values); 90 | 91 | dynamic MultiGet(string key, params string[] names); 92 | 93 | Task MultiSetAsync(string key, params object[] values); 94 | 95 | Task MultiSet2Async(string key, long seconds, ExpirationType type = ExpirationType.RelativeToNow, params object[] values); 96 | 97 | Task MultiGetAsync(string key, params string[] names); 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /docs/config.md: -------------------------------------------------------------------------------- 1 | # Config 配置 2 | 3 | 框架提供了强大且可扩展的配置功能,支持根据运行环境维护不同的配置;运行时,框架会自动合并默认配置和运行环境的配置,合并后的配置可直接从 `Application.Instance().Config()` 获取。 4 | 5 | ## 多环境配置 6 | 7 | 框架支持根据环境来加载配置,用户可以定义多个环境的配置文件,实现一次构建多次部署,具体环境请查看[[运行环境配置]](env.md) 8 | 9 | ```json 10 | appsettings.json 11 | appsettings.Development.json 12 | appsettings.Production.json 13 | ``` 14 | 15 | `appsettings.json` 为默认的配置文件,所有环境都会加载这个配置文件,一般所有环境都需要且内容相同的配置会放在这个配置文件中。 16 | 17 | 当指定env时,框架会首先加载默认配置文件`appsettings.json`,然后加载对应运行环境的配置文件,具体运行环境配置文件中的配置项会覆盖默认配置文件中的同名配置项。如Production环境会加载appsettings.json配置文件和appsettings.Production.json配置文件,appsettings.Production.json会覆盖appsettings.json的同名配置。 18 | 19 | ## 配置写法 20 | 21 | 配置文件内容是一个JSON格式的对象,可以覆盖框架的一些配置,应用也可以将自己业务的配置放到这里方便管理。 22 | 23 | ```json 24 | // 配置允许访问的Host主机地址 25 | { 26 | "AllowedHosts": "127.0.0.1" 27 | } 28 | ``` 29 | 30 | ## 配置内容 31 | 32 | 配置内容主要包含框架配置和用户自定义的业务配置,如下是框架自身的主要配置: 33 | 34 | ```json 35 | { 36 | "AllowedHosts": "*", //允许访问的主机地址,默认为*,即无限制。 37 | 38 | "Compression": { 39 | "Enable": true //请求返回是否启用压缩。 40 | }, 41 | 42 | "Middlewares": [], //框架中间件,多个用逗号分隔。 43 | 44 | "Router": { 45 | "MaxConcurrentTotal": 65535, //框架的最大并发数。 46 | "MaxIdlePerRouter": 10, //每个路由池中保留的最大空闲实例数。 47 | "MaxConcurrentPerRouter": 100, //每个路由的最大并发数。 48 | "MaxInvokePerInstance": 10000 //每个路由实例处理请求的次数上限,超过将销毁实例。 49 | }, 50 | 51 | "Cookie": { 52 | "Keys": "fastapi" //读写cookie的密钥,多个用逗号分隔。 53 | //加密时使用第一个,解密时按照逗号分隔依次尝试。 54 | }, 55 | 56 | "Session": { 57 | "Enable": true, //是否启用session会话机制。 58 | "Timeout": "20m", //session超时时间。 59 | "Cookie": { 60 | "Name": "fastapi.sid", //session对应的cookie名称。 61 | "SameSite": "None", //session对应cookie的SameSite配置。 62 | "HttpOnly": true, //session对应cookie的HttpOnly配置。 63 | "Secure": "None" //session对应cookie的Secure配置。 64 | }, 65 | "Redis": { 66 | "Enable": false //是否使用redis存储session数据。 67 | } 68 | }, 69 | 70 | "Cors": { 71 | "Enable": true, //是否启用跨域安全检查。 72 | "Options": { 73 | "AllowMethods": [ "*" ], //允许的跨域请求方法。 74 | "AllowSites": [ "http://localhost" ], //允许跨域请求的地址列表。 75 | "SupportsCredentials": true //请求是否携带cookie、session等信息。 76 | } 77 | }, 78 | 79 | "Cache": { 80 | "Default": { 81 | "Type": "Local", //缓存类型,框架默认支持Local、Redis两种,可自定义扩展。 82 | "Default": true //是否默认缓存,多个缓存只能有一个默认缓存,最后配置的有效。 83 | } 84 | } 85 | } 86 | ``` 87 | 88 | 89 | 90 | ## 如何使用 91 | 92 | 配置配置内容大部分是针对框架的属性,配置后,框架会自动感知并进行设置;但有些用户自定义的业务配置,用户会希望在代码中进行读取,即使是框架的属性配置,用户也有可能需要在代码中进行读取作为业务判断的一种逻辑;因此,框架提供了多种方式读取配置的内容。 93 | 94 | ##### 通过全局对象获取配置 95 | 96 | 此种方式允许用户在任何位置进行使用,不受代码位置显示。 97 | 98 | ```c# 99 | if (Application.Instance().Compression.Enable) //判断框架是否开启了结果压缩机制 100 | { 101 | //TODO 102 | } 103 | ``` 104 | 105 | ##### 通过控制器的上下文对象获取配置 106 | 107 | 在控制器的上下文对象ControllerContext中,我们可以通过Config属性获取配置内容。 108 | 109 | ```c# 110 | public async Task GetList(ControllerContext cc) 111 | { 112 | if (cc.Config.Compression.Enable) //判断框架是否开启了结果压缩机制 113 | { 114 | //TODO 115 | } 116 | } 117 | ``` 118 | 119 | ##### 自定义配置的获取 120 | 121 | 框架的配置都有对应的配置属性进行读取,对于用户自定义的配置,全部放在Settings动态对象中,用户可以通过该对象进行获取,事实上,框架的配置属性也全部定义在该对象内。 122 | 123 | ```c# 124 | if (Application.Instance().Settings.Compression.Enable) 125 | { 126 | //TODO 127 | } 128 | ``` 129 | 130 | 131 | 132 | 用户自定义配置: 133 | 134 | ```jSon 135 | { 136 | "AllowedHosts": "*", //允许访问的主机地址,默认为*,即无限制。 137 | ... 138 | "WechatSecret": "owejksfdpi23" //用户自定义业务配置。 139 | } 140 | ``` 141 | 142 | 读取方式: 143 | 144 | ```c# 145 | string wechatSecret = Application.Instance().Settings.WechatSecret; 146 | ``` 147 | 148 | -------------------------------------------------------------------------------- /docs/logging.md: -------------------------------------------------------------------------------- 1 | # 日志 2 | 3 | 日志对于 Web 开发的重要性毋庸置疑,它对于监控应用的运行状态、问题排查等都有非常重要的意义。 4 | 5 | 框架基于微软日志工厂进行封装,同时增加日志写入文件能力。 6 | 7 | 主要特性: 8 | 9 | - 开发和生产智能区分 10 | - 日志分级 11 | - 自动分割日志 12 | - 高性能 13 | 14 | 15 | 16 | ## 开发和生产智能区分 17 | 18 | 基于开发环境和生产环境的不同需要,框架直接硬编码对两种环境进行了区分。 19 | 20 | 1. 开发环境默认仅支持控制台、终端 2种日志同时输出。 21 | 22 | 2. 开发环境如需输出到日志文件,可通过在appsettings.json中配置: 23 | 24 | ```json 25 | "Logging": { 26 | "File": { 27 | "LogLevel": { 28 | "Default": "Debug" 29 | } 30 | } 31 | } 32 | ``` 33 | 34 | 3. 生产环境仅支持文件日志的输出,且输出日志级别为Warning,如需在生产环境打印跟踪调试信息,可在appsettings.json中调整日志文件的输出级别: 35 | 36 | ```json 37 | "Logging": { 38 | "File": { 39 | "LogLevel": { 40 | "Default": "Trace" 41 | } 42 | } 43 | } 44 | ``` 45 | 46 | 47 | 48 | ## 日志分级 49 | 50 | 框架使用微软日志工厂基本能力封装而成,沿用了日志工厂的分级方式,分为None、Trace、Debug、Information、Warning、Error、Fatal 7个级别。 51 | 52 | - None - 不打印 53 | - Trace - 跟踪日志,级别最低,一般不会使用。 54 | - Debug - 开发调试日志,通常用于开发过程打印运行信息。 55 | - Information - 系统运行情况日志,粒度相较于Debug更粗,记录系统重要信息。 56 | - Warning - 警告信息,指示有潜在错误或可能有风险的信息。 57 | - Error - 错误信息,但不影响系统继续运行。 58 | - Fatal - 致命错误信息,会影响系统运行,甚至终止运行。 59 | 60 | 61 | 62 | ## 日志文件路径 63 | 64 | 框架默认的日志文件保存路径为当前工作目录的logs子目录下。 65 | 66 | 日志文件默认命名为fastapi.log,实际写入名称会根据日志文件分割方式增加不同的后缀。如按天分割:fastapi_yyyy-MM-dd.log。 67 | 68 | 如果想自定义日志路径,在appsettings.json中进行如下配置: 69 | 70 | ```json 71 | "Logging": { 72 | "File": { 73 | "Options": { 74 | "FileName": "D:\\fastapi.log" 75 | } 76 | } 77 | } 78 | ``` 79 | 80 | 81 | 82 | ## 日志文件格式 83 | 84 | 日志文件采用文本文件格式保存,具体格式如下: 85 | 86 | 级别: 日志写入位置 - 记录时间 87 | 88 | ​ 日志内容 89 | 90 | 如: 91 | 92 | ``` 93 | info: CodeM.FastApi.Program.InitApp[0] - 2020/9/21 15:01:43 94 | Hello World. 95 | ``` 96 | 97 | 98 | 99 | ## 日志文件分割 100 | 101 | 为避免日志文件一直写入,框架支持按照多种方式对日志文件进行自动分割。 102 | 103 | - Date - 按日期进行分割,一天的日志写入一个文件。 104 | - Hour - 按小时进行分割,一小时的日志写入一个文件。 105 | - Size - 按文件大小进行分割,日志内容每达到一定大小就写入一个文件。 106 | - None - 不分割。 107 | 108 | 109 | 110 | 框架默认分割设置为None,不进行分割,用户可在appsettings.json中进行自定义: 111 | 112 | ```json 113 | "Logging": { 114 | "File": { 115 | "Options": { 116 | "SplitType": "Size", 117 | "MaxFileSize": 1048576 //单位为字节;如果不设置,默认为2097152,即2M;其他分割类型无需设置该配置项。 118 | } 119 | } 120 | } 121 | ``` 122 | 123 | 124 | 125 | ## 日志文件滚动个数 126 | 127 | 使用了日志分割配置后,框架会根据分割规则将日志分割为若干个小文件,随着日志的增加,小文件会越来越多,为了避免小文件太多占用过多的硬盘空间,可以通过配置日志文件滚动个数,只保存最新的几个日志文件,框架默认为10,即保存离当前时间点最近的10个日志文件。 128 | 129 | 如需更改,可在appsettings.json中配置: 130 | 131 | ```json 132 | "Logging": { 133 | "File": { 134 | "Options": { 135 | "MaxFileBackups": 20 136 | } 137 | } 138 | } 139 | ``` 140 | 141 | 142 | 143 | ## 日志文件编码 144 | 145 | 日志文件默认采用UTF8编码,如果需要其他格式的编码,可以在appsettings.json中修改: 146 | 147 | ```json 148 | "Logging": { 149 | "File": { 150 | "Options": { 151 | "Encoding": "gb2312" 152 | } 153 | } 154 | } 155 | ``` 156 | 157 | 158 | 159 | ## 如何打印日志 160 | 161 | 在框架层面,用户可以全程获得ControllerContext对象,该对象提供方法可以方便的打印日志: 162 | 163 | ```c# 164 | public async Task Handle(ControllerContext cc) 165 | { 166 | cc.Debug("Hello World."); //打印一条调试日志 167 | await cc.JsonAsync("Hello World."); 168 | } 169 | ``` 170 | 171 | - ControllerContext.Trace - 打印跟踪日志 172 | - ControllerContext.Debug - 打印调试日志 173 | - ControllerContext.Info - 打印运行信息 174 | - ControllerContext.Warn - 打印警告日志 175 | - ControllerContext.Error - 打印错误日志 176 | - ControllerContext.Fatal - 打印致命错误日志 177 | 178 | 179 | 180 | 在没有ControllerContext上下文对象的地方,可以直接使用App全局对象进行打印: 181 | 182 | ```c# 183 | using CodeM.FastApi.System.Core; 184 | 185 | public void GetUser(string userid) 186 | { 187 | // do something 188 | Application.Instance().Log().Debug("查询指定用户信息。"); 189 | } 190 | ``` 191 | 192 | 193 | 194 | ## 高性能 195 | 196 | 通常 Web 访问是高频访问,每次打印日志都写磁盘会造成频繁磁盘 IO,为了提高性能,我们采用的文件日志写入策略是: 197 | 198 | > 日志同步写入内存,异步每隔一段时间(默认 1 秒)刷盘 -------------------------------------------------------------------------------- /libs/ControllerContext/Params/QueryParams.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Http; 2 | using Microsoft.Extensions.Primitives; 3 | using System; 4 | 5 | namespace CodeM.FastApi.Context.Params 6 | { 7 | public class QueryParams 8 | { 9 | private IQueryCollection mData; 10 | 11 | public QueryParams(IQueryCollection data) 12 | { 13 | mData = data; 14 | } 15 | 16 | public string this[string key] 17 | { 18 | get 19 | { 20 | if (mData != null) 21 | { 22 | StringValues result; 23 | if (mData.TryGetValue(key, out result)) 24 | { 25 | return result[0]; 26 | } 27 | } 28 | return null; 29 | } 30 | } 31 | 32 | public string this[int index] 33 | { 34 | get 35 | { 36 | if (mData != null) 37 | { 38 | if (index >= 0 && index < mData.Keys.Count) 39 | { 40 | string[] keys = new string[mData.Keys.Count]; 41 | mData.Keys.CopyTo(keys, 0); 42 | string key = keys[index]; 43 | StringValues result; 44 | if (mData.TryGetValue(key, out result)) 45 | { 46 | return result[0]; 47 | } 48 | } 49 | } 50 | return null; 51 | } 52 | } 53 | 54 | public StringValues AllValues(string key) 55 | { 56 | StringValues result = new StringValues(); 57 | if (mData != null) 58 | { 59 | mData.TryGetValue(key, out result); 60 | } 61 | return result; 62 | } 63 | 64 | public StringValues AllValues(int index) 65 | { 66 | StringValues result = new StringValues(); 67 | if (mData != null) 68 | { 69 | if (index >= 0 && index < mData.Keys.Count) 70 | { 71 | string[] keys = new string[mData.Keys.Count]; 72 | mData.Keys.CopyTo(keys, 0); 73 | string key = keys[index]; 74 | mData.TryGetValue(key, out result); 75 | } 76 | } 77 | return result; 78 | } 79 | 80 | public bool ContainsKey(string key) 81 | { 82 | if (mData != null) 83 | { 84 | return mData.ContainsKey(key); 85 | } 86 | return false; 87 | } 88 | 89 | public string Get(string key, string defaultValue) 90 | { 91 | if (ContainsKey(key)) 92 | { 93 | return this[key]; 94 | } 95 | return defaultValue; 96 | } 97 | 98 | public T Get(string key, T defaultValue, bool throwError = false) 99 | { 100 | object value = Get(key, null); 101 | if (value != null) 102 | { 103 | try 104 | { 105 | object typValue = Convert.ChangeType(value, typeof(T)); 106 | return (T)typValue; 107 | } 108 | catch 109 | { 110 | if (throwError) 111 | { 112 | throw; 113 | } 114 | } 115 | } 116 | return defaultValue; 117 | } 118 | 119 | public string Get(int index, string defaultValue) 120 | { 121 | if (mData != null) 122 | { 123 | if (index >= 0 && index < mData.Keys.Count) 124 | { 125 | return this[index]; 126 | } 127 | } 128 | return defaultValue; 129 | } 130 | 131 | public T Get(int index, T defaultValue, bool throwError = false) 132 | { 133 | object value = Get(index, null); 134 | if (value != null) 135 | { 136 | try 137 | { 138 | object typValue = Convert.ChangeType(value, typeof(T)); 139 | return (T)typValue; 140 | } 141 | catch 142 | { 143 | if (throwError) 144 | { 145 | throw; 146 | } 147 | } 148 | } 149 | return defaultValue; 150 | } 151 | 152 | public int Count 153 | { 154 | get 155 | { 156 | if (mData != null) 157 | { 158 | return mData.Count; 159 | } 160 | return 0; 161 | } 162 | } 163 | } 164 | } 165 | -------------------------------------------------------------------------------- /Program.cs: -------------------------------------------------------------------------------- 1 | using CodeM.FastApi.Common; 2 | using CodeM.FastApi.Log; 3 | using Microsoft.AspNetCore.Hosting; 4 | using Microsoft.Extensions.Configuration; 5 | using Microsoft.Extensions.DependencyInjection; 6 | using Microsoft.Extensions.Hosting; 7 | using Microsoft.Extensions.Logging; 8 | using System; 9 | using System.Diagnostics; 10 | using System.Threading; 11 | 12 | namespace CodeM.FastApi 13 | { 14 | public class Program 15 | { 16 | private static string mEnv = null; 17 | private static string mPort = null; 18 | 19 | public static void Main(string[] args) 20 | { 21 | try 22 | { 23 | foreach (string arg in args) 24 | { 25 | string[] paramValues = arg.Split("="); 26 | if (paramValues.Length == 2) 27 | { 28 | if ("env".Equals(paramValues[0].Trim(), StringComparison.OrdinalIgnoreCase)) 29 | { 30 | mEnv = paramValues[1].Trim(); 31 | } 32 | else if ("port".Equals(paramValues[0].Trim(), StringComparison.OrdinalIgnoreCase)) 33 | { 34 | mPort = paramValues[1].Trim(); 35 | } 36 | } 37 | } 38 | 39 | string frameworkName = " _____ ___ _____ _____ ___ _____ _ __ _ _____ _____ \n" + 40 | "| ___| / | / ___/ |_ _| / | | _ \\ | | | \\ | | | ____| |_ _| \n" + 41 | "| |__ / /| | | |___ | | / /| | | |_| | | | | \\| | | |__ | | \n" + 42 | "| __| / /_| | \\___ \\ | | / /_| | | ___/ | | | |\\ | | __| | | \n" + 43 | "| | / / | | ___| | | | / / | | | | | | _ | | \\ | | |___ | | \n" + 44 | "|_| /_/ |_| /_____/ |_| /_/ |_| |_| |_| |_| |_| \\_| |_____| |_| \n" + 45 | " \n" + 46 | "==================================================================================="; 47 | Console.ForegroundColor = ConsoleColor.Green; 48 | Console.WriteLine(frameworkName); 49 | 50 | CreateHostBuilder(args).Build().Run(); 51 | } 52 | catch (Exception exp) 53 | { 54 | if (Logger.Inited) 55 | { 56 | Logger.Instance().Fatal(exp); 57 | Thread.Sleep(1000); 58 | } 59 | 60 | Process.GetCurrentProcess().Kill(); 61 | } 62 | finally 63 | { 64 | Console.ForegroundColor = ConsoleColor.White; 65 | } 66 | } 67 | 68 | public static IHostBuilder CreateHostBuilder(string[] args) => 69 | Host.CreateDefaultBuilder(args) 70 | .ConfigureLogging((hostingContext, logging) => 71 | { 72 | Console.WriteLine("开始启动......"); 73 | Console.WriteLine(string.Format("发现内容目录:{0}", 74 | AppDomain.CurrentDomain.BaseDirectory)); 75 | 76 | logging.ClearProviders(); 77 | if (hostingContext.HostingEnvironment.IsDevelopment()) 78 | { 79 | logging.AddDebug(); 80 | logging.AddConsole(); 81 | } 82 | 83 | IConfigurationSection options = hostingContext.Configuration 84 | .GetSection("Logging").GetSection("File").GetSection("Options"); 85 | logging.AddFile(options); 86 | 87 | ILoggerFactory factory = logging.Services.BuildServiceProvider().GetService(); 88 | Logger.Init(factory); 89 | }) 90 | .ConfigureWebHostDefaults(webBuilder => 91 | { 92 | if (!string.IsNullOrWhiteSpace(mEnv)) 93 | { 94 | webBuilder = webBuilder.UseEnvironment(mEnv); 95 | } 96 | 97 | if (!string.IsNullOrWhiteSpace(mPort)) 98 | { 99 | webBuilder = webBuilder.UseUrls(string.Concat("http://*:", mPort)); 100 | } 101 | 102 | webBuilder.ConfigureKestrel(options => 103 | { 104 | options.Limits.MaxRequestBodySize = null; 105 | }); 106 | 107 | webBuilder.UseStartup(); 108 | 109 | FastApiUtils.SetEnvironmentName(webBuilder.GetSetting("ENVIRONMENT")); 110 | }); 111 | } 112 | 113 | } 114 | -------------------------------------------------------------------------------- /fastapi.sln: -------------------------------------------------------------------------------- 1 | 2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 16 4 | VisualStudioVersion = 16.0.30330.147 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "fastapi", "fastapi.csproj", "{B7FF69D4-C73C-4304-BDA2-3D9EB3B991D7}" 7 | EndProject 8 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CodeM.FastApi.Router", "libs\Router\CodeM.FastApi.Router.csproj", "{38C64B18-96FF-4550-B812-0CE65F8A5B5F}" 9 | EndProject 10 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CodeM.FastApi.Context.JsonResponse", "libs\JsonResponseWriter\CodeM.FastApi.Context.JsonResponse.csproj", "{4F69774A-0C31-4732-B1AA-B10C5DECC4FF}" 11 | EndProject 12 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CodeM.FastApi.Context", "libs\ControllerContext\CodeM.FastApi.Context.csproj", "{E993785D-7145-4AFD-9135-F5099F398197}" 13 | EndProject 14 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CodeM.FastApi.Config", "libs\AppConfig\CodeM.FastApi.Config.csproj", "{C7A8FCEE-5D0B-46AA-923A-917AD3480D60}" 15 | EndProject 16 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CodeM.FastApi.Common", "libs\Common\CodeM.FastApi.Common.csproj", "{FEB3F9DE-B5A6-406B-83F9-AA6ADB5F1E29}" 17 | EndProject 18 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CodeM.FastApi.Log", "libs\Logger\CodeM.FastApi.Log.csproj", "{7C4C8975-7A29-461C-87DA-59E58AF1EAD3}" 19 | EndProject 20 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CodeM.FastApi.DbUpgrade", "libs\DbUpgrade\CodeM.FastApi.DbUpgrade.csproj", "{20E6EC9E-53E7-416F-A29D-197A70813B2D}" 21 | EndProject 22 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CodeM.FastApi.Schedule", "libs\Schedule\CodeM.FastApi.Schedule.csproj", "{77D6F14C-9764-42D1-A09B-30EA481A23A0}" 23 | EndProject 24 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CodeM.FastApi.Cache", "libs\Cache\CodeM.FastApi.Cache.csproj", "{5EEAAF55-FBDF-4238-ABBF-A026DE617885}" 25 | EndProject 26 | Global 27 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 28 | Debug|Any CPU = Debug|Any CPU 29 | Release|Any CPU = Release|Any CPU 30 | EndGlobalSection 31 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 32 | {B7FF69D4-C73C-4304-BDA2-3D9EB3B991D7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 33 | {B7FF69D4-C73C-4304-BDA2-3D9EB3B991D7}.Debug|Any CPU.Build.0 = Debug|Any CPU 34 | {B7FF69D4-C73C-4304-BDA2-3D9EB3B991D7}.Release|Any CPU.ActiveCfg = Release|Any CPU 35 | {B7FF69D4-C73C-4304-BDA2-3D9EB3B991D7}.Release|Any CPU.Build.0 = Release|Any CPU 36 | {38C64B18-96FF-4550-B812-0CE65F8A5B5F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 37 | {38C64B18-96FF-4550-B812-0CE65F8A5B5F}.Debug|Any CPU.Build.0 = Debug|Any CPU 38 | {38C64B18-96FF-4550-B812-0CE65F8A5B5F}.Release|Any CPU.ActiveCfg = Release|Any CPU 39 | {38C64B18-96FF-4550-B812-0CE65F8A5B5F}.Release|Any CPU.Build.0 = Release|Any CPU 40 | {4F69774A-0C31-4732-B1AA-B10C5DECC4FF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 41 | {4F69774A-0C31-4732-B1AA-B10C5DECC4FF}.Debug|Any CPU.Build.0 = Debug|Any CPU 42 | {4F69774A-0C31-4732-B1AA-B10C5DECC4FF}.Release|Any CPU.ActiveCfg = Release|Any CPU 43 | {4F69774A-0C31-4732-B1AA-B10C5DECC4FF}.Release|Any CPU.Build.0 = Release|Any CPU 44 | {E993785D-7145-4AFD-9135-F5099F398197}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 45 | {E993785D-7145-4AFD-9135-F5099F398197}.Debug|Any CPU.Build.0 = Debug|Any CPU 46 | {E993785D-7145-4AFD-9135-F5099F398197}.Release|Any CPU.ActiveCfg = Release|Any CPU 47 | {E993785D-7145-4AFD-9135-F5099F398197}.Release|Any CPU.Build.0 = Release|Any CPU 48 | {C7A8FCEE-5D0B-46AA-923A-917AD3480D60}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 49 | {C7A8FCEE-5D0B-46AA-923A-917AD3480D60}.Debug|Any CPU.Build.0 = Debug|Any CPU 50 | {C7A8FCEE-5D0B-46AA-923A-917AD3480D60}.Release|Any CPU.ActiveCfg = Release|Any CPU 51 | {C7A8FCEE-5D0B-46AA-923A-917AD3480D60}.Release|Any CPU.Build.0 = Release|Any CPU 52 | {FEB3F9DE-B5A6-406B-83F9-AA6ADB5F1E29}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 53 | {FEB3F9DE-B5A6-406B-83F9-AA6ADB5F1E29}.Debug|Any CPU.Build.0 = Debug|Any CPU 54 | {FEB3F9DE-B5A6-406B-83F9-AA6ADB5F1E29}.Release|Any CPU.ActiveCfg = Release|Any CPU 55 | {FEB3F9DE-B5A6-406B-83F9-AA6ADB5F1E29}.Release|Any CPU.Build.0 = Release|Any CPU 56 | {7C4C8975-7A29-461C-87DA-59E58AF1EAD3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 57 | {7C4C8975-7A29-461C-87DA-59E58AF1EAD3}.Debug|Any CPU.Build.0 = Debug|Any CPU 58 | {7C4C8975-7A29-461C-87DA-59E58AF1EAD3}.Release|Any CPU.ActiveCfg = Release|Any CPU 59 | {7C4C8975-7A29-461C-87DA-59E58AF1EAD3}.Release|Any CPU.Build.0 = Release|Any CPU 60 | {20E6EC9E-53E7-416F-A29D-197A70813B2D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 61 | {20E6EC9E-53E7-416F-A29D-197A70813B2D}.Debug|Any CPU.Build.0 = Debug|Any CPU 62 | {20E6EC9E-53E7-416F-A29D-197A70813B2D}.Release|Any CPU.ActiveCfg = Release|Any CPU 63 | {20E6EC9E-53E7-416F-A29D-197A70813B2D}.Release|Any CPU.Build.0 = Release|Any CPU 64 | {77D6F14C-9764-42D1-A09B-30EA481A23A0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 65 | {77D6F14C-9764-42D1-A09B-30EA481A23A0}.Debug|Any CPU.Build.0 = Debug|Any CPU 66 | {77D6F14C-9764-42D1-A09B-30EA481A23A0}.Release|Any CPU.ActiveCfg = Release|Any CPU 67 | {77D6F14C-9764-42D1-A09B-30EA481A23A0}.Release|Any CPU.Build.0 = Release|Any CPU 68 | {5EEAAF55-FBDF-4238-ABBF-A026DE617885}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 69 | {5EEAAF55-FBDF-4238-ABBF-A026DE617885}.Debug|Any CPU.Build.0 = Debug|Any CPU 70 | {5EEAAF55-FBDF-4238-ABBF-A026DE617885}.Release|Any CPU.ActiveCfg = Release|Any CPU 71 | {5EEAAF55-FBDF-4238-ABBF-A026DE617885}.Release|Any CPU.Build.0 = Release|Any CPU 72 | EndGlobalSection 73 | GlobalSection(SolutionProperties) = preSolution 74 | HideSolutionNode = FALSE 75 | EndGlobalSection 76 | GlobalSection(ExtensibilityGlobals) = postSolution 77 | SolutionGuid = {7B5CD19F-8E26-4B72-94E7-497F3696CB46} 78 | EndGlobalSection 79 | EndGlobal 80 | -------------------------------------------------------------------------------- /libs/Schedule/ScheduleManager.cs: -------------------------------------------------------------------------------- 1 | using CodeM.Common.Ioc; 2 | using CodeM.Common.Tools; 3 | using CodeM.FastApi.Common; 4 | using Quartz; 5 | using Quartz.Impl; 6 | using System; 7 | using System.Collections.Generic; 8 | 9 | namespace CodeM.FastApi.Schedule 10 | { 11 | public class ScheduleManager 12 | { 13 | private static List sSettings; 14 | 15 | private static IScheduler sScheduler; 16 | 17 | public ScheduleManager(string file) 18 | { 19 | Load(file); 20 | BuildScheduler(); 21 | } 22 | 23 | private void Load(string file) 24 | { 25 | List < ScheduleSetting > settings = ScheduleParser.Parse(file); 26 | sSettings = settings; 27 | } 28 | 29 | private void BuildScheduler() 30 | { 31 | //创建计划任务抽象工厂 32 | ISchedulerFactory factory = new StdSchedulerFactory(); 33 | // 创建计划任务 34 | sScheduler = factory.GetScheduler().GetAwaiter().GetResult(); 35 | 36 | AttachJobs(); 37 | } 38 | 39 | private void AttachJobs() 40 | { 41 | sSettings.ForEach(setting => 42 | { 43 | AttachJob(setting); 44 | }); 45 | } 46 | 47 | private bool IsMatchEnvironment(string env) 48 | { 49 | if (!string.IsNullOrWhiteSpace(env)) 50 | { 51 | string sCurEnv = FastApiUtils.GetEnvironmentName(); 52 | return env.ToLower().Contains(sCurEnv.ToLower()); 53 | } 54 | return true; 55 | } 56 | 57 | private bool AttachJob(ScheduleSetting setting, bool ignoreDisable = false) 58 | { 59 | if (IsMatchEnvironment(setting.Environment)) 60 | { 61 | object jobInst = Wukong.GetSingleObject(setting.Class); 62 | Type _typ = jobInst.GetType(); 63 | 64 | IJobDetail job = JobBuilder.Create(_typ) 65 | .WithIdentity(JobKey.Create(setting.Id)) 66 | .Build(); 67 | 68 | ITrigger trigger; 69 | if (!string.IsNullOrWhiteSpace(setting.Interval)) 70 | { 71 | trigger = TriggerBuilder.Create() 72 | .WithIdentity(new TriggerKey(setting.Id)) 73 | .WithDailyTimeIntervalSchedule(builder => 74 | { 75 | TimeSpan ts = Xmtool.DateTime().GetTimeSpanFromString(setting.Interval).Value; 76 | builder = builder.WithIntervalInSeconds((int)ts.TotalSeconds); 77 | if (setting.Repeat > 0) 78 | { 79 | builder = builder.WithRepeatCount(setting.Repeat); 80 | } 81 | }) 82 | .Build(); 83 | } 84 | else 85 | { 86 | trigger = TriggerBuilder.Create() 87 | .WithIdentity(new TriggerKey(setting.Id)) 88 | .WithCronSchedule(setting.Cron) 89 | .Build(); 90 | } 91 | 92 | if (!setting.Disable || ignoreDisable) 93 | { 94 | sScheduler.ScheduleJob(job, trigger); 95 | return true; 96 | } 97 | } 98 | return false; 99 | } 100 | 101 | private ScheduleSetting GetScheduleSetting(string jobId) 102 | { 103 | return sSettings.Find(setting => 104 | { 105 | return string.Compare(setting.Id, jobId, true) == 0; 106 | }); 107 | } 108 | 109 | public bool Run() 110 | { 111 | if (!IsShutdown() && !IsRunning()) 112 | { 113 | sScheduler.Start(); 114 | return true; 115 | } 116 | return false; 117 | } 118 | 119 | public bool Shutdown() 120 | { 121 | if (!IsShutdown()) 122 | { 123 | sScheduler.Shutdown(); 124 | return true; 125 | } 126 | return false; 127 | } 128 | 129 | public bool IsRunning() 130 | { 131 | return sScheduler.IsStarted && 132 | !sScheduler.InStandbyMode && 133 | !sScheduler.IsShutdown; 134 | } 135 | 136 | public bool IsShutdown() 137 | { 138 | return sScheduler.IsShutdown; 139 | } 140 | 141 | public bool ResumeAll() 142 | { 143 | if (!IsShutdown() && sScheduler.InStandbyMode) 144 | { 145 | sScheduler.Start(); 146 | return true; 147 | } 148 | return false; 149 | } 150 | 151 | public bool PauseAll() 152 | { 153 | if (!IsShutdown() && !sScheduler.InStandbyMode) 154 | { 155 | sScheduler.Standby(); 156 | return true; 157 | } 158 | return false; 159 | } 160 | 161 | public bool StartJob(string jobId) 162 | { 163 | ScheduleSetting setting = GetScheduleSetting(jobId); 164 | if (IsMatchEnvironment(setting.Environment)) 165 | { 166 | return AttachJob(setting, true); 167 | } 168 | return false; 169 | } 170 | 171 | public bool StopJob(string jobId) 172 | { 173 | bool bRet = sScheduler.UnscheduleJob(new TriggerKey(jobId)).GetAwaiter().GetResult(); 174 | return bRet; 175 | } 176 | } 177 | } 178 | -------------------------------------------------------------------------------- /libs/Schedule/ScheduleParser.cs: -------------------------------------------------------------------------------- 1 | using CodeM.Common.Ioc; 2 | using CodeM.Common.Tools; 3 | using CodeM.Common.Tools.Xml; 4 | using Quartz; 5 | using System; 6 | using System.Collections; 7 | using System.Collections.Generic; 8 | using System.Text.RegularExpressions; 9 | 10 | namespace CodeM.FastApi.Schedule 11 | { 12 | public class ScheduleParser 13 | { 14 | public static List Parse(string file) 15 | { 16 | Hashtable ids = new Hashtable(); 17 | List result = new List(); 18 | 19 | Regex reInt = new Regex("^[1-9][0-9]*$"); 20 | Regex reBool = new Regex("^(true|false)$", RegexOptions.IgnoreCase); 21 | 22 | Xmtool.Xml().Iterate(file, (XmlNodeInfo nodeInfo) => 23 | { 24 | if (!nodeInfo.IsEndNode) 25 | { 26 | if (nodeInfo.Path == "/schedules/job") 27 | { 28 | string idStr = nodeInfo.GetAttribute("id"); 29 | if (idStr == null) 30 | { 31 | throw new Exception("缺少id属性。 " + file + " - Line " + nodeInfo.Line); 32 | } 33 | if (string.IsNullOrWhiteSpace(idStr)) 34 | { 35 | throw new Exception("id属性不能为空。 " + file + " - Line " + nodeInfo.Line); 36 | } 37 | 38 | if (ids.ContainsKey(idStr.Trim())) 39 | { 40 | throw new Exception("id重复。 " + file + " - Line " + nodeInfo.Line); 41 | } 42 | 43 | string intervalStr = nodeInfo.GetAttribute("interval"); 44 | string cronStr = nodeInfo.GetAttribute("cron"); 45 | 46 | if (!string.IsNullOrWhiteSpace(intervalStr) && 47 | !string.IsNullOrWhiteSpace(cronStr)) 48 | { 49 | throw new Exception("interval属性和cron属性不能同时存在。 " + file + " - Line " + nodeInfo.Line); 50 | } 51 | 52 | if (!string.IsNullOrWhiteSpace(intervalStr)) 53 | { 54 | try 55 | { 56 | Xmtool.DateTime().CheckStringTimeSpan(intervalStr.Trim()); 57 | } 58 | catch (Exception exp) 59 | { 60 | throw new Exception("格式非法。 " + file + " - Line " + nodeInfo.Line, exp); 61 | } 62 | } 63 | 64 | int repeat = 0; 65 | string repeatStr = nodeInfo.GetAttribute("repeat"); 66 | if (repeatStr != null) 67 | { 68 | if (!string.IsNullOrWhiteSpace(cronStr)) 69 | { 70 | throw new Exception("repeat属性只能跟随interval属性出现。 " + file + " - Line " + nodeInfo.Line); 71 | } 72 | 73 | if (string.IsNullOrWhiteSpace(repeatStr)) 74 | { 75 | throw new Exception("repeat属性不能为空。 " + file + " - Line " + nodeInfo.Line); 76 | } 77 | 78 | if (!reInt.IsMatch(repeatStr)) 79 | { 80 | throw new Exception("repeat属性必须是有效正整数。 " + file + " - Line " + nodeInfo.Line); 81 | } 82 | 83 | repeat = int.Parse(repeatStr); 84 | } 85 | 86 | string classStr = nodeInfo.GetAttribute("class"); 87 | if (string.IsNullOrWhiteSpace(classStr)) 88 | { 89 | throw new Exception("class属性不能为空。 " + file + " - Line " + nodeInfo.Line); 90 | } 91 | else 92 | { 93 | object jobInst = Wukong.GetSingleObject(classStr.Trim()); 94 | if (!(jobInst is IJob)) 95 | { 96 | throw new Exception("class指定类型必须实现IJob接口。 " + file + " - Line " + nodeInfo.Line); 97 | } 98 | } 99 | 100 | bool disable = false; 101 | string disableStr = nodeInfo.GetAttribute("disable"); 102 | if (disableStr != null) 103 | { 104 | if (string.IsNullOrWhiteSpace(disableStr)) 105 | { 106 | throw new Exception("disable属性不能为空。 " + file + " - Line " + nodeInfo.Line); 107 | } 108 | 109 | if (!reBool.IsMatch(disableStr)) 110 | { 111 | throw new Exception("disable属性必须是布尔型。 " + file + " - Line " + nodeInfo.Line); 112 | } 113 | 114 | disable = bool.Parse(disableStr.Trim()); 115 | } 116 | 117 | string envStr = nodeInfo.GetAttribute("env"); 118 | 119 | ScheduleSetting setting = new ScheduleSetting(); 120 | setting.Id = idStr.Trim(); 121 | setting.Interval = intervalStr != null ? intervalStr.Trim() : null; 122 | setting.Cron = cronStr != null ? cronStr.Trim() : null; 123 | setting.Repeat = repeat; 124 | setting.Class = classStr.Trim(); 125 | setting.Disable = disable; 126 | setting.Environment = envStr != null ? envStr.ToLower() : null; 127 | result.Add(setting); 128 | 129 | ids.Add(idStr.Trim(), 1); 130 | } 131 | } 132 | return true; 133 | }); 134 | 135 | return result; 136 | } 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /libs/Logger/Logger.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.Logging; 2 | using System; 3 | using System.Diagnostics; 4 | using System.Linq; 5 | using System.Reflection; 6 | 7 | namespace CodeM.FastApi.Log 8 | { 9 | public class Logger 10 | { 11 | private static Logger sLogger = new Logger(); 12 | 13 | private static ILoggerFactory sFactory; 14 | 15 | private static string[] sFilterMethos = new string[] { "MoveNext", "Start", "InvokeMethod" }; 16 | public static string GetCallerName(int skipNum = 0) 17 | { 18 | string result = string.Empty; 19 | 20 | int skip = 1 + Math.Max(0, skipNum); 21 | StackTrace st = new StackTrace(); 22 | StackFrame[] sfs = st.GetFrames(); 23 | for (int i = 0; i < sfs.Length; i++) 24 | { 25 | StackFrame sf = sfs[i]; 26 | if (StackFrame.OFFSET_UNKNOWN != sf.GetILOffset()) 27 | { 28 | MethodBase mb = sf.GetMethod(); 29 | if (!sFilterMethos.Contains(mb.Name)) 30 | { 31 | if (skip == 0) 32 | { 33 | result = mb.DeclaringType.FullName + "." + mb.Name; 34 | break; 35 | } 36 | skip--; 37 | } 38 | } 39 | } 40 | 41 | return result; 42 | } 43 | 44 | public static void Init(ILoggerFactory factory) 45 | { 46 | sFactory = factory; 47 | } 48 | 49 | public static bool Inited 50 | { 51 | get 52 | { 53 | return sFactory != null && sLogger != null; 54 | } 55 | } 56 | 57 | public static Logger Instance() 58 | { 59 | return sLogger; 60 | } 61 | 62 | private static void _CheckInit() 63 | { 64 | if (sFactory == null) 65 | { 66 | throw new Exception("Please use Init method to initialize."); 67 | } 68 | } 69 | 70 | private static ILogger _GetLogger(string categoryName) 71 | { 72 | return sFactory.CreateLogger(categoryName); 73 | } 74 | 75 | private Logger() 76 | { 77 | } 78 | 79 | public void Trace(string tag, string message, params object[] args) 80 | { 81 | _CheckInit(); 82 | ILogger logger = _GetLogger(tag); 83 | logger.LogTrace(message, args); 84 | } 85 | 86 | public void Trace(string message, params object[] args) 87 | { 88 | string callerName = GetCallerName(1); 89 | Trace(callerName, message, args); 90 | } 91 | 92 | public void Debug(string tag, string message, params object[] args) 93 | { 94 | _CheckInit(); 95 | ILogger logger = _GetLogger(tag); 96 | logger.LogDebug(message, args); 97 | } 98 | 99 | public void Debug(string message, params object[] args) 100 | { 101 | string callerName = GetCallerName(1); 102 | Debug(callerName, message, args); 103 | } 104 | 105 | public void Info(string tag, string message, params object[] args) 106 | { 107 | _CheckInit(); 108 | ILogger logger = _GetLogger(tag); 109 | logger.LogInformation(message, args); 110 | } 111 | 112 | public void Info(string message, params object[] args) 113 | { 114 | string callerName = GetCallerName(1); 115 | Info(callerName, message, args); 116 | } 117 | 118 | public void Warn(string tag, string message, params object[] args) 119 | { 120 | _CheckInit(); 121 | ILogger logger = _GetLogger(tag); 122 | logger.LogWarning(message, args); 123 | } 124 | 125 | public void Warn(string message, params object[] args) 126 | { 127 | string callerName = GetCallerName(1); 128 | Warn(callerName, message, args); 129 | } 130 | 131 | public void Warn(string tag, Exception exp) 132 | { 133 | _CheckInit(); 134 | ILogger logger = _GetLogger(tag); 135 | logger.LogWarning(exp, string.Empty); 136 | } 137 | 138 | public void Warn(Exception exp) 139 | { 140 | string callerName = GetCallerName(1); 141 | Warn(callerName, exp); 142 | } 143 | 144 | public void Error(string tag, string message, params object[] args) 145 | { 146 | _CheckInit(); 147 | ILogger logger = _GetLogger(tag); 148 | logger.LogError(message, args); 149 | } 150 | 151 | public void Error(string message, params object[] args) 152 | { 153 | string callerName = GetCallerName(1); 154 | Error(callerName, message, args); 155 | } 156 | 157 | public void Error(string tag, Exception exp) 158 | { 159 | _CheckInit(); 160 | ILogger logger = _GetLogger(tag); 161 | logger.LogError(exp, string.Empty); 162 | } 163 | 164 | public void Error(Exception exp) 165 | { 166 | string callerName = GetCallerName(1); 167 | Error(callerName, exp); 168 | } 169 | 170 | /// 171 | /// 致命错误 172 | /// 173 | /// 174 | /// 175 | /// 176 | public void Fatal(string tag, string message, params object[] args) 177 | { 178 | _CheckInit(); 179 | ILogger logger = _GetLogger(tag); 180 | logger.LogCritical(message, args); 181 | } 182 | 183 | /// 184 | /// 致命错误 185 | /// 186 | /// 187 | /// 188 | public void Fatal(string message, params object[] args) 189 | { 190 | string callerName = GetCallerName(1); 191 | Fatal(callerName, message, args); 192 | } 193 | 194 | public void Fatal(string tag, Exception exp) 195 | { 196 | _CheckInit(); 197 | ILogger logger = _GetLogger(tag); 198 | logger.LogCritical(exp, string.Empty); 199 | } 200 | 201 | public void Fatal(Exception exp) 202 | { 203 | string callerName = GetCallerName(1); 204 | Fatal(callerName, exp); 205 | } 206 | } 207 | } 208 | -------------------------------------------------------------------------------- /libs/ControllerContext/ControllerContext.cs: -------------------------------------------------------------------------------- 1 | using CodeM.Common.Tools; 2 | using CodeM.Common.Tools.DynamicObject; 3 | using CodeM.FastApi.Common; 4 | using CodeM.FastApi.Config; 5 | using CodeM.FastApi.Context.Params; 6 | using CodeM.FastApi.Context.Wrappers; 7 | using Microsoft.AspNetCore.Http; 8 | using Microsoft.AspNetCore.Routing; 9 | using System.IO; 10 | using System.Threading.Tasks; 11 | 12 | namespace CodeM.FastApi.Context 13 | { 14 | public class ControllerContext 15 | { 16 | private HttpContext mContext; 17 | 18 | private HeaderParams mHeaders = null; 19 | private RouteParams mRouteParams = null; 20 | private QueryParams mQueryParams = null; 21 | private string mPostContent = null; 22 | private PostForms mPostForms = null; 23 | private dynamic mPostJson = null; 24 | private CookieWrapper mCookies = null; 25 | 26 | public static ControllerContext FromHttpContext(HttpContext context, ApplicationConfig config) 27 | { 28 | return new ControllerContext(context, config); 29 | } 30 | 31 | public ControllerContext(HttpContext context, ApplicationConfig config) 32 | { 33 | mContext = context; 34 | 35 | if (config != null) 36 | { 37 | Config = config; 38 | } 39 | } 40 | 41 | private SessionWrapper mSession; 42 | public SessionWrapper Session 43 | { 44 | get 45 | { 46 | if (mSession == null) 47 | { 48 | if (Config.Session.Enable) 49 | { 50 | mSession = new SessionWrapper(mContext); 51 | } 52 | } 53 | return mSession; 54 | } 55 | } 56 | 57 | public HttpRequest Request 58 | { 59 | get 60 | { 61 | return mContext.Request; 62 | } 63 | } 64 | 65 | public HttpResponse Response 66 | { 67 | get 68 | { 69 | return mContext.Response; 70 | } 71 | } 72 | 73 | public int State 74 | { 75 | get 76 | { 77 | return mContext.Response.StatusCode; 78 | } 79 | set 80 | { 81 | mContext.Response.StatusCode = value; 82 | } 83 | } 84 | 85 | public HeaderParams Headers 86 | { 87 | get 88 | { 89 | if (mHeaders == null && mContext != null) 90 | { 91 | mHeaders = new HeaderParams(mContext.Request.Headers); 92 | } 93 | return mHeaders; 94 | } 95 | } 96 | 97 | public RouteParams RouteParams 98 | { 99 | get 100 | { 101 | if (mRouteParams == null && mContext != null) 102 | { 103 | mRouteParams = new RouteParams(mContext.GetRouteData()); 104 | } 105 | return mRouteParams; 106 | } 107 | } 108 | 109 | public QueryParams QueryParams 110 | { 111 | get 112 | { 113 | if (mQueryParams == null && mContext != null) 114 | { 115 | mQueryParams = new QueryParams(mContext.Request.Query); 116 | } 117 | return mQueryParams; 118 | } 119 | } 120 | 121 | public string PostContent 122 | { 123 | get 124 | { 125 | if (mPostContent == null && mContext != null && mContext.Request != null) 126 | { 127 | if (mContext.Request.Body != null && mContext.Request.ContentLength > 0) 128 | { 129 | mContext.Request.Body.Seek(0, SeekOrigin.Begin); 130 | 131 | StreamReader sr = new StreamReader(mContext.Request.Body); 132 | Task getContentTask = sr.ReadToEndAsync(); 133 | Task.WaitAll(getContentTask); 134 | mPostContent = getContentTask.Result; 135 | 136 | mContext.Request.Body.Seek(0, SeekOrigin.Begin); 137 | } 138 | } 139 | return mPostContent; 140 | } 141 | } 142 | 143 | public PostForms PostForms 144 | { 145 | get 146 | { 147 | if (mPostForms == null && mContext != null) 148 | { 149 | if (mContext.Request != null && mContext.Request.HasFormContentType) 150 | { 151 | mPostForms = new PostForms(mContext.Request.Form); 152 | } 153 | } 154 | return mPostForms; 155 | } 156 | } 157 | 158 | public dynamic PostJson 159 | { 160 | get 161 | { 162 | if (mPostJson == null && mContext != null) 163 | { 164 | if (mContext.Request != null && ("" + mContext.Request.ContentType).ToLower().Contains("json")) 165 | { 166 | if (!string.IsNullOrEmpty(PostContent)) 167 | { 168 | mPostJson = Xmtool.Json.ConfigParser().Parse(PostContent); 169 | } 170 | } 171 | } 172 | return mPostJson; 173 | } 174 | } 175 | 176 | public CookieWrapper Cookies 177 | { 178 | get 179 | { 180 | if (mCookies == null && mContext != null) 181 | { 182 | mCookies = new CookieWrapper(mContext, Config.Cookie.Keys); 183 | } 184 | return mCookies; 185 | } 186 | } 187 | 188 | public ApplicationConfig Config 189 | { 190 | get; 191 | } 192 | 193 | public string Ip 194 | { 195 | get 196 | { 197 | string result = WebUtils.GetClientIp(mContext.Request); 198 | return result; 199 | } 200 | } 201 | 202 | private static string sDataItemKey = "_$_userdata_$_"; 203 | private object mLock = new object(); 204 | public dynamic Data 205 | { 206 | get 207 | { 208 | if (!mContext.Items.ContainsKey(sDataItemKey)) 209 | { 210 | lock (mLock) 211 | { 212 | if (!mContext.Items.ContainsKey(sDataItemKey)) 213 | { 214 | mContext.Items[sDataItemKey] = new DynamicObjectExt(); 215 | } 216 | } 217 | } 218 | return mContext.Items[sDataItemKey]; 219 | } 220 | } 221 | 222 | } 223 | } 224 | -------------------------------------------------------------------------------- /libs/AppConfig/Settings/SessionSetting.cs: -------------------------------------------------------------------------------- 1 | using CodeM.Common.Tools; 2 | using Microsoft.AspNetCore.Http; 3 | using System; 4 | using System.Text; 5 | 6 | namespace CodeM.FastApi.Config.Settings 7 | { 8 | public class SessionSetting 9 | { 10 | public class SessionSettingCookie 11 | { 12 | private string mName = "fastapi.sid"; 13 | public string Name 14 | { 15 | get 16 | { 17 | return mName; 18 | } 19 | set 20 | { 21 | if (string.IsNullOrWhiteSpace(value)) 22 | { 23 | throw new Exception("Session的Cookie名称不能为空。"); 24 | } 25 | mName = value; 26 | } 27 | } 28 | 29 | public bool HttpOnly { get; set; } = true; 30 | 31 | public SameSiteMode SameSite { get; set; } = SameSiteMode.None; 32 | 33 | public CookieSecurePolicy Secure { get; set; } = CookieSecurePolicy.None; 34 | 35 | private string mMaxAge = null; 36 | /// 37 | /// cookie有效期,默认单位:秒,可随值指定单位,ms-毫秒,s-秒,m-分钟,h-小时,d-天 38 | /// 39 | public string MaxAge 40 | { 41 | get 42 | { 43 | return mMaxAge; 44 | } 45 | set 46 | { 47 | Xmtool.DateTime().CheckStringTimeSpan(value); 48 | mMaxAge = value; 49 | } 50 | } 51 | 52 | public TimeSpan? MaxAgeTimeSpan 53 | { 54 | get 55 | { 56 | if (!string.IsNullOrWhiteSpace(mMaxAge)) 57 | { 58 | return Xmtool.DateTime().GetTimeSpanFromString(mMaxAge); 59 | } 60 | return null; 61 | } 62 | } 63 | } 64 | 65 | public class SessionSettingRedis 66 | { 67 | public bool Enable { get; set; } = false; 68 | 69 | private string mHost = "127.0.0.1"; 70 | public string Host 71 | { 72 | get 73 | { 74 | return mHost; 75 | } 76 | set 77 | { 78 | if (string.IsNullOrWhiteSpace(value)) 79 | { 80 | throw new Exception("Session的Redis配置项Host不能为空。"); 81 | } 82 | mHost = value; 83 | } 84 | } 85 | 86 | public int Port { get; set; } = 6379; 87 | 88 | public int Database = 0; 89 | 90 | public string Password = null; 91 | 92 | public int Retry = 3; 93 | 94 | public int Timeout = 5000; 95 | 96 | public bool Ssl = false; 97 | 98 | private string mSslHost = null; 99 | public string SslHost 100 | { 101 | get 102 | { 103 | return mSslHost; 104 | } 105 | set 106 | { 107 | if (string.IsNullOrWhiteSpace(value)) 108 | { 109 | throw new Exception("Session的Redis配置项SslHost不能为空。"); 110 | } 111 | mSslHost = value; 112 | } 113 | } 114 | 115 | private string mSslProtocols = null; 116 | public string SslProtocols 117 | { 118 | get 119 | { 120 | return mSslProtocols; 121 | } 122 | set 123 | { 124 | if (string.IsNullOrWhiteSpace(value)) 125 | { 126 | throw new Exception("Session的Redis配置项SslProtocols不能为空。"); 127 | } 128 | mSslProtocols = value; 129 | } 130 | } 131 | 132 | private string mInstanceName = "fastapi-"; 133 | public string InstanceName 134 | { 135 | get 136 | { 137 | return mInstanceName; 138 | } 139 | set 140 | { 141 | if (string.IsNullOrWhiteSpace(value)) 142 | { 143 | throw new Exception("Session的Redis配置项InstanceName不能为空。"); 144 | } 145 | mInstanceName = value; 146 | } 147 | } 148 | 149 | public override string ToString() 150 | { 151 | StringBuilder sb = new StringBuilder(); 152 | sb.Append(Host).Append(":").Append(Port) 153 | .Append(",connectRetry=").Append(Retry) 154 | .Append(",connectTimeout=").Append(Timeout) 155 | .Append(", defaultDatabase=").Append(Database); 156 | if (Password != null) 157 | { 158 | sb.Append(",password=").Append(Password); 159 | } 160 | if (Ssl) 161 | { 162 | sb.Append(",ssl=").Append(Ssl); 163 | if (!string.IsNullOrWhiteSpace(SslHost)) 164 | { 165 | sb.Append(",sslHost=").Append(SslHost); 166 | } 167 | if (!string.IsNullOrWhiteSpace(SslProtocols)) 168 | { 169 | sb.Append(",sslProtocols=").Append(SslProtocols); 170 | } 171 | } 172 | return sb.ToString(); 173 | } 174 | } 175 | 176 | public bool Enable { get; set; } = true; 177 | 178 | private string mTimeout = "20m"; 179 | /// 180 | /// session超时时间,默认单位:秒,可随值指定单位,ms-毫秒,s-秒,m-分钟,h-小时,d-天 181 | /// 182 | public string Timeout 183 | { 184 | get 185 | { 186 | return mTimeout; 187 | } 188 | set 189 | { 190 | Xmtool.DateTime().CheckStringTimeSpan(mTimeout); 191 | mTimeout = value; 192 | } 193 | } 194 | 195 | public TimeSpan TimeoutTimeSpan 196 | { 197 | get 198 | { 199 | TimeSpan? ts = Xmtool.DateTime().GetTimeSpanFromString(mTimeout, false); 200 | return ts != null ? ts.Value : new TimeSpan(); 201 | } 202 | } 203 | 204 | public SessionSettingCookie Cookie 205 | { 206 | get; 207 | set; 208 | } = new SessionSettingCookie(); 209 | 210 | public SessionSettingRedis Redis 211 | { 212 | get; 213 | set; 214 | } = new SessionSettingRedis(); 215 | 216 | } 217 | } 218 | -------------------------------------------------------------------------------- /Startup.cs: -------------------------------------------------------------------------------- 1 | using CodeM.Common.Tools; 2 | using CodeM.FastApi.Config; 3 | using CodeM.FastApi.Log; 4 | using CodeM.FastApi.Router; 5 | using CodeM.FastApi.System.Core; 6 | using CodeM.FastApi.System.Middlewares; 7 | using Microsoft.AspNetCore.Builder; 8 | using Microsoft.AspNetCore.Hosting; 9 | using Microsoft.AspNetCore.Hosting.Server; 10 | using Microsoft.AspNetCore.Hosting.Server.Features; 11 | using Microsoft.AspNetCore.Http; 12 | using Microsoft.AspNetCore.Http.Features; 13 | using Microsoft.AspNetCore.ResponseCompression; 14 | using Microsoft.Extensions.Configuration; 15 | using Microsoft.Extensions.DependencyInjection; 16 | using Microsoft.Extensions.Hosting; 17 | using System; 18 | using System.Collections.Generic; 19 | using System.Diagnostics; 20 | using System.IO; 21 | using System.Threading; 22 | 23 | namespace CodeM.FastApi 24 | { 25 | public class Startup 26 | { 27 | public Startup(IWebHostEnvironment env) 28 | { 29 | Init(env); 30 | } 31 | 32 | private void Init(IWebHostEnvironment env) 33 | { 34 | Console.WriteLine("解析框架全局配置文件......"); 35 | IConfigurationBuilder builder = new ConfigurationBuilder() 36 | .SetBasePath(AppDomain.CurrentDomain.BaseDirectory) 37 | .AddJsonFile("appsettings.json", true, true) 38 | .AddJsonFile(string.Concat("appsettings.", env.EnvironmentName, ".json"), true, true); 39 | builder.Build().Bind(AppConfig); 40 | 41 | string settingFile = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "appsettings.json"); 42 | string envSettingFile = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, string.Concat("appsettings.", env.EnvironmentName, ".json")); 43 | AppConfig.Settings = Xmtool.Json.ConfigParser() 44 | .AddJsonFile(settingFile) 45 | .AddJsonFile(envSettingFile) 46 | .Parse(); 47 | } 48 | 49 | internal ApplicationConfig AppConfig { get; set; } = new ApplicationConfig(); 50 | 51 | public void ConfigureServices(IServiceCollection services) 52 | { 53 | Console.WriteLine("初始化框架配置信息......"); 54 | 55 | services.AddSingleton(); 56 | 57 | if (AppConfig.Compression.Enable) 58 | { 59 | services.AddResponseCompression(options => 60 | { 61 | options.EnableForHttps = true; 62 | options.Providers.Add(); 63 | }); 64 | } 65 | 66 | if (AppConfig.Session.Enable) 67 | { 68 | if (AppConfig.Session.Redis.Enable) 69 | { 70 | services.AddStackExchangeRedisCache(options => 71 | { 72 | options.Configuration = AppConfig.Session.Redis.ToString(); 73 | options.InstanceName = AppConfig.Session.Redis.InstanceName; 74 | }); 75 | } 76 | else 77 | { 78 | services.AddDistributedMemoryCache(); 79 | } 80 | 81 | services.AddSession(options => 82 | { 83 | options.IdleTimeout = AppConfig.Session.TimeoutTimeSpan; 84 | 85 | options.Cookie.Name = AppConfig.Session.Cookie.Name; 86 | options.Cookie.HttpOnly = AppConfig.Session.Cookie.HttpOnly; 87 | options.Cookie.SameSite = AppConfig.Session.Cookie.SameSite; 88 | options.Cookie.SecurePolicy = AppConfig.Session.Cookie.Secure; 89 | options.Cookie.MaxAge = AppConfig.Session.Cookie.MaxAgeTimeSpan; 90 | options.Cookie.IsEssential = true; 91 | }); 92 | 93 | services.Configure(options => 94 | { 95 | options.CheckConsentNeeded = context => false; 96 | options.MinimumSameSitePolicy = SameSiteMode.None; 97 | }); 98 | } 99 | 100 | services.Configure(options => 101 | { 102 | options.MultipartBodyLengthLimit = AppConfig.FileUpload.MaxBodySize; 103 | }); 104 | 105 | string scheduleFile = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "schedule.xml"); 106 | Application.Init(AppConfig, scheduleFile); 107 | } 108 | 109 | public void Configure(IApplicationBuilder app, IWebHostEnvironment env, 110 | IServer server, IHostApplicationLifetime lifetime) 111 | { 112 | try 113 | { 114 | if (env.IsDevelopment()) 115 | { 116 | app.UseDeveloperExceptionPage(); 117 | } 118 | 119 | app.Use(next => context => 120 | { 121 | context.Request.EnableBuffering(); 122 | return next(context); 123 | }); 124 | 125 | app.UseCurrentContext(); 126 | 127 | if (AppConfig.Compression.Enable) 128 | { 129 | app.UseResponseCompression(); 130 | } 131 | 132 | if (AppConfig.Session.Enable) 133 | { 134 | app.UseSession(); 135 | } 136 | 137 | if (AppConfig.Cors.Enable) 138 | { 139 | app.UseMiddleware(AppConfig); 140 | } 141 | 142 | Console.WriteLine("挂载API路由接口......"); 143 | string routerFile = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "router.xml"); 144 | RouterManager.Current.Init(AppConfig, routerFile); 145 | RouterManager.Current.MountRouters(app); 146 | 147 | Console.WriteLine("启动定时任务调度程序......"); 148 | lifetime.ApplicationStarted.Register(() => 149 | { 150 | string listenAddress = string.Empty; 151 | IServerAddressesFeature saf = server.Features.Get(); 152 | if (saf.Addresses.Count > 0) 153 | { 154 | IEnumerator e = saf.Addresses.GetEnumerator(); 155 | if (e.MoveNext()) 156 | { 157 | listenAddress = e.Current.Trim(); 158 | } 159 | Application.Instance().Address = listenAddress; 160 | } 161 | 162 | Console.WriteLine(string.Format("启动成功,监听地址:[{0}],环境变量:[{1}]", 163 | listenAddress, env.EnvironmentName)); 164 | Console.WriteLine("==================================================================================="); 165 | Console.ForegroundColor = ConsoleColor.White; 166 | }); 167 | lifetime.ApplicationStopping.Register(() => 168 | { 169 | Application.Instance().Schedule().Shutdown(); 170 | }); 171 | Application.Instance().Schedule().Run(); 172 | } 173 | catch (Exception exp) 174 | { 175 | Console.ForegroundColor = ConsoleColor.White; 176 | 177 | Logger.Instance().Fatal(exp); 178 | Thread.Sleep(1000); 179 | 180 | Process.GetCurrentProcess().Kill(); 181 | } 182 | } 183 | } 184 | } 185 | -------------------------------------------------------------------------------- /libs/DbUpgrade/UpgradeManager.cs: -------------------------------------------------------------------------------- 1 | using CodeM.Common.Orm; 2 | using CodeM.Common.Tools; 3 | using System; 4 | using System.Collections.Generic; 5 | using System.IO; 6 | using System.Text.RegularExpressions; 7 | 8 | namespace CodeM.FastApi.DbUpgrade 9 | { 10 | internal class UpgradeInfo 11 | { 12 | public UpgradeInfo(string modelPath, string file, int version) 13 | { 14 | this.ModelPath = modelPath; 15 | this.File = file; 16 | this.Version = version; 17 | } 18 | 19 | public string ModelPath { get; set; } 20 | 21 | public string File { get; set; } 22 | 23 | public int Version { get; set; } 24 | } 25 | 26 | public class UpgradeManager 27 | { 28 | public static void EnableVersionControl() 29 | { 30 | if (Derd.GetVersion() < 0) 31 | { 32 | Derd.EnableVersionControl(); 33 | } 34 | } 35 | 36 | private static string SpecialHandle(string modelPath, string command) 37 | { 38 | string datetimeNow = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss"); 39 | 40 | ConnectionSetting cs = Derd.GetConnectionSetting(modelPath); 41 | if ("oracle".Equals(cs.Dialect, StringComparison.OrdinalIgnoreCase)) 42 | { 43 | Regex re = new Regex("^\\s*INSERT\\s*INTO\\s*(\\w*)\\s*\\((.*)\\)\\s*VALUES\\s*\\(", RegexOptions.IgnoreCase | RegexOptions.Multiline); 44 | Match match = re.Match(command); 45 | if (match.Success) 46 | { 47 | string prefix = command.Substring(0, match.Groups[1].Index); 48 | string table = command.Substring(match.Groups[1].Index, match.Groups[1].Length); 49 | string fields = command.Substring(match.Groups[2].Index, match.Groups[2].Length); 50 | string suffix = command.Substring(match.Groups[2].Index + match.Groups[2].Length); 51 | 52 | string newTable = string.Concat("\"", table, "\""); 53 | 54 | string[] fieldItems = fields.Replace(" ", "").Split(","); 55 | string newFields = string.Concat("\"", string.Join("\", \"", fieldItems), "\""); 56 | 57 | command = string.Concat(prefix, newTable, " (", newFields, suffix).Trim(); 58 | } 59 | 60 | string orclDatetimeNow = string.Concat("TO_TIMESTAMP('", datetimeNow, "', 'yyyy-mm-dd HH24:mi:ss')"); 61 | command = command.Replace("{{$nowtime$}}", orclDatetimeNow); 62 | 63 | command = command.Replace("'9999-12-31'", "TO_DATE('9999-12-31', 'yyyy-MM-dd')"); 64 | 65 | if (command.EndsWith(";")) 66 | { 67 | command = command.Substring(0, command.Length - 1); 68 | } 69 | } 70 | else if ("dm".Equals(cs.Dialect, StringComparison.OrdinalIgnoreCase)) 71 | { 72 | Regex re = new Regex("^\\s*INSERT\\s*INTO\\s*(\\w*)\\s*\\((.*)\\)\\s*VALUES\\s*\\(", RegexOptions.IgnoreCase | RegexOptions.Multiline); 73 | Match match = re.Match(command); 74 | if (match.Success) 75 | { 76 | string prefix = command.Substring(0, match.Groups[1].Index); 77 | string table = command.Substring(match.Groups[1].Index, match.Groups[1].Length); 78 | string fields = command.Substring(match.Groups[2].Index, match.Groups[2].Length); 79 | string suffix = command.Substring(match.Groups[2].Index + match.Groups[2].Length); 80 | 81 | string newTable = string.Concat("\"", table, "\""); 82 | 83 | string[] fieldItems = fields.Replace(" ", "").Split(","); 84 | string newFields = string.Concat("\"", string.Join("\", \"", fieldItems), "\""); 85 | 86 | command = string.Concat(prefix, newTable, " (", newFields, suffix).Trim(); 87 | } 88 | 89 | command = command.Replace("{{$nowtime$}}", string.Concat("'", datetimeNow, "'")); 90 | } 91 | else 92 | { 93 | command = command.Replace("{{$nowtime$}}", string.Concat("'", datetimeNow, "'")); 94 | } 95 | return command; 96 | } 97 | 98 | private static void ExecuteUpgradeFile(UpgradeInfo info) 99 | { 100 | int lastVersion = Derd.GetVersion(info.ModelPath); 101 | if (info.Version > lastVersion) 102 | { 103 | List commands = UpgradeLoader.Load(info.File); 104 | 105 | int trans = Derd.GetTransaction(info.ModelPath); 106 | try 107 | { 108 | foreach (string cmd in commands) 109 | { 110 | string sql = SpecialHandle(info.ModelPath, cmd); 111 | Derd.ExecSql(sql, trans); 112 | } 113 | 114 | Derd.SetVersion(info.ModelPath, info.Version, trans); 115 | Derd.CommitTransaction(trans); 116 | } 117 | catch 118 | { 119 | Derd.RollbackTransaction(trans); 120 | throw; 121 | } 122 | } 123 | } 124 | 125 | public static void Upgrade(string modelPath) 126 | { 127 | int currentVersion = Derd.GetVersion(); 128 | if (currentVersion < 0) 129 | { 130 | throw new Exception("版本控制未开启,请先使用EnableVersionControl方法开启版本控制。"); 131 | } 132 | if (!Derd.IsVersionControlEnabled()) 133 | { 134 | throw new Exception("版本控制未开启,请先使用EnableVersionControl方法开启版本控制。"); 135 | } 136 | 137 | IEnumerable upgradeFiles = Directory.EnumerateFiles(modelPath, ".upgrade.v*.xml", SearchOption.AllDirectories); 138 | IEnumerator e = upgradeFiles.GetEnumerator(); 139 | 140 | List sortedUpgradeData = new List(); 141 | while (e.MoveNext()) 142 | { 143 | string upgradeFile = e.Current; 144 | FileInfo fi = new FileInfo(upgradeFile); 145 | string filename = fi.Name; 146 | string version = filename.Substring(10, filename.Length - 14); 147 | if (Xmtool.Regex().IsPositiveInteger(version)) 148 | { 149 | string currPath = fi.DirectoryName.ToLower().Replace(modelPath.ToLower(), ""); 150 | if (string.IsNullOrWhiteSpace(currPath)) 151 | { 152 | currPath = "/"; 153 | } 154 | else 155 | { 156 | currPath = currPath.Replace(Path.DirectorySeparatorChar, '/'); 157 | } 158 | sortedUpgradeData.Add(new UpgradeInfo(currPath, upgradeFile, int.Parse(version))); 159 | } 160 | } 161 | 162 | sortedUpgradeData.Sort((left, right) => 163 | { 164 | if (left.File.Length < right.File.Length) 165 | { 166 | return -1; 167 | } 168 | else if (left.File.Length > right.File.Length) 169 | { 170 | return 1; 171 | } 172 | else 173 | { 174 | if (left.Version < right.Version) 175 | { 176 | return -1; 177 | } 178 | else if (left.Version > right.Version) 179 | { 180 | return 1; 181 | } 182 | } 183 | 184 | return 0; 185 | }); 186 | 187 | foreach (var upgradeData in sortedUpgradeData) 188 | { 189 | ExecuteUpgradeFile(upgradeData); 190 | } 191 | } 192 | 193 | } 194 | } -------------------------------------------------------------------------------- /libs/Router/MethodInvoker.cs: -------------------------------------------------------------------------------- 1 | using CodeM.Common.Ioc; 2 | using CodeM.FastApi.Common; 3 | using CodeM.FastApi.Context; 4 | using System; 5 | using System.Collections.Concurrent; 6 | using System.Reflection; 7 | using System.Threading; 8 | using System.Threading.Tasks; 9 | 10 | namespace CodeM.FastApi.Router 11 | { 12 | internal class MethodInvoker 13 | { 14 | readonly ConcurrentDictionary> mHandlers = new ConcurrentDictionary>(); 15 | readonly ReaderWriterLockSlim handlerLocker = new ReaderWriterLockSlim(LockRecursionPolicy.SupportsRecursion); 16 | 17 | readonly ConcurrentDictionary mHandlerCounters = new ConcurrentDictionary(); 18 | readonly ConcurrentDictionary mHandlerExecNumbers = new ConcurrentDictionary(); 19 | 20 | private ConcurrentStack GetHandlerStack(string handlerFullName) 21 | { 22 | string key = handlerFullName.ToLower(); 23 | if (!mHandlers.TryGetValue(key, out ConcurrentStack result)) 24 | { 25 | handlerLocker.EnterWriteLock(); 26 | try 27 | { 28 | if (!mHandlers.TryGetValue(key, out result)) 29 | { 30 | result = new ConcurrentStack(); 31 | mHandlers.TryAdd(key, result); 32 | } 33 | } 34 | finally 35 | { 36 | handlerLocker.ExitWriteLock(); 37 | } 38 | } 39 | 40 | return result; 41 | } 42 | 43 | private bool IncHandlerCount(string handlerFullName, int maxConcurrent) 44 | { 45 | bool result = true; 46 | 47 | if (maxConcurrent > 0) 48 | { 49 | string key = handlerFullName.ToLower(); 50 | mHandlerCounters.AddOrUpdate(key, 1, (key, value) => 51 | { 52 | if (value < maxConcurrent) 53 | { 54 | return value + 1; 55 | } 56 | else 57 | { 58 | result = false; 59 | return value; 60 | } 61 | }); 62 | } 63 | else 64 | { 65 | result = false; 66 | } 67 | 68 | return result; 69 | } 70 | 71 | private bool DecHandlerCount(string handlerFullName) 72 | { 73 | bool result = true; 74 | 75 | string key = handlerFullName.ToLower(); 76 | mHandlerCounters.AddOrUpdate(key, 0, (key, value) => 77 | { 78 | if (value > 0) 79 | { 80 | return value - 1; 81 | } 82 | else 83 | { 84 | return 0; 85 | } 86 | }); 87 | 88 | return result; 89 | } 90 | 91 | private object GetHandler(string handlerFullName, int maxConcurrent) 92 | { 93 | object result = null; 94 | 95 | int pos = handlerFullName.LastIndexOf("."); 96 | string handlerClass = handlerFullName.Substring(0, pos); 97 | 98 | ConcurrentStack stack = GetHandlerStack(handlerFullName); 99 | if (stack != null && !stack.IsEmpty) 100 | { 101 | stack.TryPop(out result); 102 | } 103 | 104 | if (result == null && IncHandlerCount(handlerFullName, maxConcurrent)) 105 | { 106 | try 107 | { 108 | result = Wukong.GetObject(handlerClass); 109 | if (result == null) 110 | { 111 | throw new Exception(); 112 | } 113 | } 114 | catch 115 | { 116 | DecHandlerCount(handlerFullName); 117 | throw new Exception(string.Concat("Instantiation exception(", handlerFullName, ")")); 118 | } 119 | } 120 | 121 | return result; 122 | } 123 | 124 | private void IncHandlerExecNum(object handler) 125 | { 126 | mHandlerExecNumbers.AddOrUpdate("handler_" + handler.GetHashCode(), 1, (key, value) => 127 | { 128 | return value + 1; 129 | }); 130 | } 131 | 132 | private bool IsNotExpired(object handler, int maxInvokePerInstance) 133 | { 134 | bool result = true; 135 | 136 | mHandlerExecNumbers.TryGetValue("handler_" + handler.GetHashCode(), out int execNum); 137 | if (execNum >= maxInvokePerInstance) 138 | { 139 | result = false; 140 | } 141 | 142 | return result; 143 | } 144 | 145 | private void ReleaseHandler(string handlerFullName, object handler, 146 | int maxIdle, int maxInvokePerInstance) 147 | { 148 | ConcurrentStack stack = GetHandlerStack(handlerFullName); 149 | if (stack != null && stack.Count < maxIdle && IsNotExpired(handler, maxInvokePerInstance)) 150 | { 151 | stack.Push(handler); 152 | } 153 | else 154 | { 155 | mHandlerExecNumbers.TryRemove("handler_" + handler.GetHashCode(), out int execNum); 156 | DecHandlerCount(handlerFullName); 157 | } 158 | } 159 | 160 | public object Invoke(string handlerFullName, ControllerContext cc, 161 | int maxConcurrent, int maxIdle, int maxInvokePerInstance, bool ignoreMethodNotExists = false) 162 | { 163 | int pos = handlerFullName.LastIndexOf("."); 164 | string handlerMethod = handlerFullName.Substring(pos + 1); 165 | 166 | dynamic result = null; 167 | 168 | object handlerInst = GetHandler(handlerFullName, maxConcurrent); 169 | if (handlerInst != null) 170 | { 171 | try 172 | { 173 | Type handlerType = handlerInst.GetType(); 174 | 175 | if (!FastApiUtils.IsMethodExists(handlerType, handlerMethod)) 176 | { 177 | if (ignoreMethodNotExists) 178 | { 179 | return null; 180 | } 181 | else 182 | { 183 | throw new Exception(String.Concat("Router processor not found: ", handlerFullName)); 184 | } 185 | } 186 | 187 | result = handlerType.InvokeMember(handlerMethod, 188 | BindingFlags.IgnoreCase | BindingFlags.Public | 189 | BindingFlags.Instance | BindingFlags.InvokeMethod, 190 | null, handlerInst, new object[] { cc }); 191 | 192 | if (result != null) 193 | { 194 | Type _resultTyp = result.GetType(); 195 | if (_resultTyp.IsGenericType) 196 | { 197 | if (_resultTyp.GetGenericTypeDefinition() == typeof(Task<>)) 198 | { 199 | if (result.Exception != null) 200 | { 201 | if (FastApiUtils.IsDev()) 202 | { 203 | throw result.Exception; 204 | } 205 | } 206 | else 207 | { 208 | Type[] _typs = _resultTyp.GetGenericArguments(); 209 | if (_typs.Length == 1 && 210 | string.Compare(_typs[0].Name, "VoidTaskResult", true) != 0) 211 | { 212 | result = result.Result; 213 | } 214 | else 215 | { 216 | result = null; 217 | } 218 | } 219 | } 220 | } 221 | } 222 | 223 | IncHandlerExecNum(handlerInst); 224 | } 225 | finally 226 | { 227 | ReleaseHandler(handlerFullName, handlerInst, maxIdle, maxInvokePerInstance); 228 | } 229 | } 230 | else 231 | { 232 | throw new Exception(string.Concat("Router busy(", cc.Request.Method, " ", cc.Request.Path, "): ", maxConcurrent)); 233 | } 234 | 235 | return result; 236 | } 237 | } 238 | } -------------------------------------------------------------------------------- /libs/Cache/LocalCache.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.Caching.Memory; 2 | using System; 3 | using System.Threading.Tasks; 4 | 5 | namespace CodeM.FastApi.Cache 6 | { 7 | public class LocalCache : CacheBase 8 | { 9 | private MemoryCache mLocalCache; 10 | 11 | public LocalCache(dynamic options) 12 | { 13 | MemoryCacheOptions mco = new MemoryCacheOptions(); 14 | mLocalCache = new MemoryCache(mco); 15 | } 16 | 17 | private MemoryCacheEntryOptions CreateCacheOptions(long seconds, ExpirationType type = ExpirationType.RelativeToNow) 18 | { 19 | MemoryCacheEntryOptions options = new MemoryCacheEntryOptions(); 20 | switch (type) 21 | { 22 | case ExpirationType.Absolute: 23 | options.AbsoluteExpiration = DateTimeOffset.Now.AddSeconds(seconds); 24 | break; 25 | case ExpirationType.RelativeToNow: 26 | options.AbsoluteExpirationRelativeToNow = TimeSpan.FromSeconds(seconds); 27 | break; 28 | case ExpirationType.Sliding: 29 | options.SlidingExpiration = TimeSpan.FromSeconds(seconds); 30 | break; 31 | } 32 | return options; 33 | } 34 | 35 | public override void Set(string key, byte[] value) 36 | { 37 | mLocalCache.Set(key, value); 38 | } 39 | 40 | public override async Task SetAsync(string key, byte[] value) 41 | { 42 | await Task.Run(() => 43 | { 44 | Set(key, value); 45 | }); 46 | } 47 | 48 | public override void Set(string key, byte[] value, 49 | long seconds, ExpirationType type = ExpirationType.RelativeToNow) 50 | { 51 | MemoryCacheEntryOptions op = CreateCacheOptions(seconds, type); 52 | mLocalCache.Set(key, value, op); 53 | } 54 | 55 | public override async Task SetAsync(string key, byte[] value, 56 | long seconds, ExpirationType type = ExpirationType.RelativeToNow) 57 | { 58 | await Task.Run(() => 59 | { 60 | Set(key, value, seconds, type); 61 | }); 62 | } 63 | 64 | public override byte[] Get(string key) 65 | { 66 | byte[] buff = mLocalCache.Get(key); 67 | return buff; 68 | } 69 | 70 | public override async Task GetAsync(string key) 71 | { 72 | return await Task.Run(() => 73 | { 74 | return Get(key); 75 | }); 76 | } 77 | 78 | public override void SetBoolean(string key, bool value) 79 | { 80 | mLocalCache.Set(key, value); 81 | } 82 | 83 | public override async Task SetBooleanAsync(string key, bool value) 84 | { 85 | await Task.Run(() => 86 | { 87 | SetBoolean(key, value); 88 | }); 89 | } 90 | 91 | public override void SetBoolean(string key, bool value, 92 | long seconds, ExpirationType type = ExpirationType.RelativeToNow) 93 | { 94 | MemoryCacheEntryOptions op = CreateCacheOptions(seconds, type); 95 | mLocalCache.Set(key, value, op); 96 | } 97 | 98 | public override async Task SetBooleanAsync(string key, bool value, 99 | long seconds, ExpirationType type = ExpirationType.RelativeToNow) 100 | { 101 | await Task.Run(() => 102 | { 103 | SetBoolean(key, value, seconds, type); 104 | }); 105 | } 106 | 107 | public override bool GetBoolean(string key) 108 | { 109 | return mLocalCache.Get(key); 110 | } 111 | 112 | public override async Task GetBooleanAsync(string key) 113 | { 114 | return await Task.Run(() => 115 | { 116 | return GetBoolean(key); 117 | }); 118 | } 119 | 120 | public override void SetInt32(string key, int value) 121 | { 122 | mLocalCache.Set(key, value); 123 | } 124 | 125 | public override async Task SetInt32Async(string key, int value) 126 | { 127 | await Task.Run(() => 128 | { 129 | SetInt32(key, value); 130 | }); 131 | } 132 | 133 | public override void SetInt32(string key, int value, 134 | long seconds, ExpirationType type = ExpirationType.RelativeToNow) 135 | { 136 | MemoryCacheEntryOptions op = CreateCacheOptions(seconds, type); 137 | mLocalCache.Set(key, value, op); 138 | } 139 | 140 | public override async Task SetInt32Async(string key, int value, 141 | long seconds, ExpirationType type = ExpirationType.RelativeToNow) 142 | { 143 | await Task.Run(() => 144 | { 145 | SetInt32(key, value, seconds, type); 146 | }); 147 | } 148 | 149 | public override int? GetInt32(string key) 150 | { 151 | return mLocalCache.Get(key); 152 | } 153 | 154 | public override async Task GetInt32Async(string key) 155 | { 156 | return await Task.Run(() => 157 | { 158 | return GetInt32(key); 159 | }); 160 | } 161 | 162 | public override void SetInt64(string key, long value) 163 | { 164 | mLocalCache.Set(key, value); 165 | } 166 | 167 | public override async Task SetInt64Async(string key, long value) 168 | { 169 | await Task.Run(() => 170 | { 171 | SetInt64(key, value); 172 | }); 173 | } 174 | 175 | public override void SetInt64(string key, long value, long seconds, ExpirationType type) 176 | { 177 | MemoryCacheEntryOptions op = CreateCacheOptions(seconds, type); 178 | mLocalCache.Set(key, value, op); 179 | } 180 | 181 | public override async Task SetInt64Async(string key, long value, long seconds, ExpirationType type) 182 | { 183 | await Task.Run(() => 184 | { 185 | SetInt64(key, value, seconds, type); 186 | }); 187 | } 188 | 189 | public override long? GetInt64(string key) 190 | { 191 | return mLocalCache.Get(key); 192 | } 193 | 194 | public override async Task GetInt64Async(string key) 195 | { 196 | return await Task.Run(() => 197 | { 198 | return GetInt64(key); 199 | }); 200 | } 201 | 202 | public override void SetDouble(string key, double value) 203 | { 204 | mLocalCache.Set(key, value); 205 | } 206 | 207 | public override async Task SetDoubleAsync(string key, double value) 208 | { 209 | await Task.Run(() => 210 | { 211 | SetDouble(key, value); 212 | }); 213 | } 214 | 215 | public override void SetDouble(string key, double value, long seconds, ExpirationType type) 216 | { 217 | MemoryCacheEntryOptions op = CreateCacheOptions(seconds, type); 218 | mLocalCache.Set(key, value, op); 219 | } 220 | 221 | public override async Task SetDoubleAsync(string key, double value, long seconds, ExpirationType type) 222 | { 223 | await Task.Run(() => 224 | { 225 | SetDouble(key, value, seconds, type); 226 | }); 227 | } 228 | 229 | public override double? GetDouble(string key) 230 | { 231 | return mLocalCache.Get(key); 232 | } 233 | 234 | public override async Task GetDoubleAsync(string key) 235 | { 236 | return await Task.Run(() => 237 | { 238 | return GetDouble(key); 239 | }); 240 | } 241 | 242 | public override void SetString(string key, string value) 243 | { 244 | if (value == null) 245 | { 246 | throw new ArgumentNullException("value", "Value cannot be null"); 247 | } 248 | mLocalCache.Set(key, value); 249 | } 250 | 251 | public override async Task SetStringAsync(string key, string value) 252 | { 253 | await Task.Run(() => 254 | { 255 | SetString(key, value); 256 | }); 257 | } 258 | 259 | public override void SetString(string key, string value, 260 | long seconds, ExpirationType type = ExpirationType.RelativeToNow) 261 | { 262 | if (value == null) 263 | { 264 | throw new ArgumentNullException("value", "Value cannot be null"); 265 | } 266 | 267 | MemoryCacheEntryOptions op = CreateCacheOptions(seconds, type); 268 | mLocalCache.Set(key, value, op); 269 | } 270 | 271 | public override async Task SetStringAsync(string key, string value, 272 | long seconds, ExpirationType type = ExpirationType.RelativeToNow) 273 | { 274 | await Task.Run(() => 275 | { 276 | SetString(key, value, seconds, type); 277 | }); 278 | } 279 | 280 | public override string GetString(string key) 281 | { 282 | return mLocalCache.Get(key); 283 | } 284 | 285 | public override async Task GetStringAsync(string key) 286 | { 287 | return await Task.Run(() => 288 | { 289 | return GetString(key); 290 | }); 291 | } 292 | 293 | public override bool ContainsKey(string key) 294 | { 295 | object value; 296 | return TryGetValue(key, out value); 297 | } 298 | 299 | public override bool TryGetValue(string key, out T result) 300 | { 301 | result = default(T); 302 | object value; 303 | if (mLocalCache.TryGetValue(key, out value)) 304 | { 305 | result = (T)value; 306 | return true; 307 | } 308 | return false; 309 | } 310 | 311 | public override void Remove(string key) 312 | { 313 | mLocalCache.Remove(key); 314 | } 315 | 316 | public override async Task RemoveAsync(string key) 317 | { 318 | await Task.Run(() => 319 | { 320 | Remove(key); 321 | }); 322 | } 323 | } 324 | } 325 | -------------------------------------------------------------------------------- /docs/router.md: -------------------------------------------------------------------------------- 1 | # 路由(Router) 2 | 3 | Router主要用来指定接口路径和接口具体处理逻辑的Controller的映射关系,框架约定了在router.xml文件中统一定义所有路由的规则。 4 | 5 | 通过统一的配置,我们可以避免路由规则逻辑散落在多个地方,从而出现未知的冲突,集中在一起我们可以更方便的来查看全局的路由规则。 6 | 7 | 8 | 9 | ## 如何定义Router 10 | 11 | router.xml里面定义接口规则 12 | 13 | ```xml 14 | 15 | 16 | 17 | 18 | ``` 19 | 20 | controller目录下实现Controller(框架建议在controller目录下实现所有Controller,并不强制) 21 | 22 | ```c# 23 | // controller/User.cs 24 | using CodeM.FastApi.Context; 25 | using System.Threading.Tasks; 26 | 27 | namespace CodeM.FastApi.Controller 28 | { 29 | public class User 30 | { 31 | public async Task Handle(ControllerContext cc) 32 | { 33 | await cc.JsonAsync("Hello World."); 34 | } 35 | } 36 | } 37 | ``` 38 | 39 | 这样就完成了一个最简单的Router定义,当用户执行GET /user,User.cs里的的Handle方法就会执行。 40 | 41 | 42 | 43 | ## Router详细定义说明 44 | 45 | 下面是路由的完整定义,参数可以根据场景的不同,自由选择。 46 | 47 | ```xml 48 | 49 | ``` 50 | 51 | 路由定义可选参数主要包括10个部分: 52 | 53 | * path - 接口路径,即通常所说的URL路径(必填)。 54 | * method - 接口请求的类型,即HTTP对应的method方法,现支持GET、POST、PUT、DELETE,默认GET(可选)。 55 | * handler - 指定路由具体执行业务逻辑的代码实现,需要填写完整代码路径,即命名空间+类名+方法名(可选)。 56 | * resource - 框架支持restful风格的路由定义,通过指定一个类,快速定义一组CRUD路由,需要指定类全名称(可选)。 57 | * model - 框架内置ORM能力,通过指定一个 model定义,快速定义一组针对指定model的CRUD路由(可选)。 58 | * action - 配合model定义路由的接口行为,取值范围CURLD,对应增、改、删、列表、详情,可自由组合,默认CURLD。 59 | * batchAction - 配合model定义路由的接口批操作行为,取值范围CUR,对应批量增、批量改、批量删,可自由组合,默认空。 60 | * middlewares - 路由定义支持中间件设置,通过设置中间件可以在具体业务逻辑执行前或者执行后进行额外的处理(可选)。 61 | * maxConcurrent - 路由请求最大并发数,超过最大数,直接返回状态繁忙,默认100(可选)。 62 | * maxIdle - 指定路由空闲状态时,存活的最大实例数,默认10(可选)。 63 | * maxInvokePerInstance - 指定路由实例最大使用次数,一旦达到规定数量,将销毁当前实例,并创建新的实例作为代替,默认10000(可选)。 64 | * include - 支持包含子文件,避免所有路由定义在一个文件中繁杂,不容易管理。 65 | 66 | ### 注意 67 | 68 | * router.xml必须在根目录下。 69 | * router定义中,middleware中间件支持多个串联执行。 70 | * handler、resource、model属性必须设置一个,不能同时使用。 71 | * 使用resource、model后,不能再设置method属性。 72 | * include指向的子文件是以路由定义文件所在目录为基准目录。 73 | 74 | 下面是一些路由的定义方式: 75 | 76 | ```xml 77 | 78 | 79 | 80 | ``` 81 | 82 | 83 | 84 | ### RESTful 风格的 URL 定义 85 | 86 | 如果想通过RESTful的方式来定义路由,我们提供了两种方法可以轻松实现。 87 | 88 | #### 1. 通过resource属性,指定一个类快速在一个路径上生成CRUD功能。 89 | 90 | ```xml 91 | 92 | ``` 93 | 94 | 上面的配置就在/user路径上部署一组CRUD功能,对应的CRUD处理逻辑在resource指向的类中提供,用户只需要在类中实现对应的函数就可以了。 95 | 96 | | Method | Path | Router Name | Description | 97 | | ------ | ---------- | ----------- | ----------- | 98 | | POST | /user | Create | 增 | 99 | | DELETE | /user/{id} | Delete | 删 | 100 | | PUT | /user/{id} | Update | 改 | 101 | | GET | /user | List | 查列表 | 102 | | GET | /user/{id} | Detail | 查详情 | 103 | 104 | ```c# 105 | // controller/User.cs 106 | using CodeM.FastApi.Context; 107 | using System.Threading.Tasks; 108 | 109 | namespace CodeM.FastApi.Controller 110 | { 111 | public class User 112 | { 113 | public async Task Create(ControllerContext cc) 114 | { 115 | await cc.JsonAsync("增"); 116 | } 117 | 118 | public async Task Delete(ControllerContext cc) 119 | { 120 | await cc.JsonAsync("删"); 121 | } 122 | 123 | public async Task Update(ControllerContext cc) 124 | { 125 | await cc.JsonAsync("改"); 126 | } 127 | 128 | public async Task List(ControllerContext cc) 129 | { 130 | await cc.JsonAsync("查列表"); 131 | } 132 | 133 | public async Task Detail(ControllerContext cc) 134 | { 135 | await cc.JsonAsync("查详情"); 136 | } 137 | } 138 | } 139 | ``` 140 | 141 | 如果我们不需要其中的某几个方法,可以不用在User类里实现,这样对应的URL路径也不会注册到Router。 142 | 143 | 144 | 145 | #### 2. 通过model属性,指定一个数据模型,自动生成针对该模型定义的CRUD功能。 146 | 147 | 框架自带了一套轻量级开源ORM库,ORM库使用方法和Model定义规则可参见[使用说明](http://www.github.com/softwaiter/netcoreORM) 148 | 149 | 通过针对指定Model配置一条路由,就可以即刻获得针对该Model的增、删、改、查的接口。 150 | 151 | 152 | 153 | 路由定义: 154 | 155 | ```xml 156 | 157 | 158 | 159 | 160 | ``` 161 | 162 | 如上定义了一条/user的路由,该条路由绑定了用户模型User;针对该条路由定义框架会在运行时生成如下几条路由: 163 | 164 | | 方法 | 路由 | 功能 | 参数 | 165 | | ------ | ---------- | ------------ | ------------------------------------------------------------ | 166 | | POST | /user | 新建用户 | -- | 167 | | POST | /user | 批量新建用户 | 只支持json传参,格式如下:
{
"_items": [
{
"Name":"wangxm"
},
{
"Name":"wangxm2"
}
]
} | 168 | | GET | /user | 查询用户列表 | pagesize: 分页大小,默认为50;最大200。

pageindex: 第几页,默认1。

gettotal: 是否返回总数,默认false。

sort: 排序方式,格式为排序“排序属性_排序方式”,排序方式分为ASC和DESC两种。
如按照用户名年龄降序排列:/user?sort=Age_desc;多个排序条件增加同名参数即可,如:/user?sort=Age_desc&sort=Name_asc

where: 查询条件,使用表达式进行定义,支持的操作符包括(、)、AND、OR、>=、<=、<>、>、<、=、~=、~!=;~=表示数据库的Like,~!=表示数据库的Not Like。
如查找用户名以王开头的用户:/user?where=Name~=王%

source: 返回的模型属性,默认返回所有属性;多个属性用,分隔。 | 169 | | GET | /user/{id} | 查询用户详情 | -- | 170 | | DELETE | /user/{id} | 删除用户 | -- | 171 | | DELETE | /user | 批量删除用户 | model主键:要删除的model模型的主键值,多个主键值可分别设置;如:?id=1&id=2&id=3 | 172 | | PUT | /user/{id} | 修改用户详情 | -- | 173 | | PUT | /user | 批量修改用户 | model主键:要删除的model模型的主键值,多个主键值可分别设置;如:?id=1&id=2&id=3 | 174 | 175 | 通过配置[action](#link_action)属性,可以定义路由支持的单个对象操作能力,假如只需要用户模型User的列表查询能力,可进行如下定义: 176 | 177 | ```xml 178 | 179 | 180 | 181 | 182 | ``` 183 | 184 | 通过配置[batchAction](#link_batchAction)属性,可以定义路由支持的批量操作能力,假如只需要用户模型User的批量删除能力,可以进行如下定义: 185 | 186 | ```xml 187 | 188 | 189 | 190 | 191 | ``` 192 | 193 | 194 | 195 | 196 | ## router实战 197 | 198 | 下面通过更多实际的例子,来说明router的用法。 199 | 200 | ### 参数获取 201 | 202 | #### Query String方式 203 | 204 | ```xml 205 | 206 | 207 | ``` 208 | 209 | ```c# 210 | using CodeM.FastApi.Context; 211 | using System.Threading.Tasks; 212 | 213 | namespace CodeM.FastApi.Controller 214 | { 215 | public class User 216 | { 217 | public async Task Handle(ControllerContext cc) 218 | { 219 | string id = cc.QueryParams["id"]; 220 | await cc.JsonAsync("User id is: " + id); 221 | } 222 | } 223 | } 224 | ``` 225 | 226 | ```shell 227 | curl http://127.0.0.1:5000/user?id=1 228 | ``` 229 | 230 | 231 | 232 | #### 路由参数方式 233 | 234 | ```xml 235 | 236 | 237 | ``` 238 | 239 | ```c# 240 | using CodeM.FastApi.Context; 241 | using System.Threading.Tasks; 242 | 243 | namespace CodeM.FastApi.Controller 244 | { 245 | public class User 246 | { 247 | public async Task Handle(ControllerContext cc) 248 | { 249 | string id = cc.RouteParams["id"]; 250 | await cc.JsonAsync("User id is: " + id); 251 | } 252 | } 253 | } 254 | ``` 255 | 256 | ```shell 257 | curl http://127.0.0.1:5000/user/1 258 | ``` 259 | 260 | 261 | 262 | 在路由定义中使用路由参数时,可以对路由参数进行约束,支持类型约束、范围约束和正则表达式约束3中形式。 263 | 264 | ##### 类型约束 265 | 266 | | 约束 | 格式 | 说明 | 267 | | -------- | ------------- | ------------------ | 268 | | int | {id:int} | 只允许int32整数 | 269 | | alpha | {id:alpha} | 只能包含大小写字母 | 270 | | bool | {id:bool} | 只允许布尔类型 | 271 | | datetime | {id:datetime} | 只允许日期格式 | 272 | | decimal | {id:decimal} | 只允许decimal类型 | 273 | | double | {id:double} | 只允许double类型 | 274 | | float | {id:float} | 只允许float类型 | 275 | | guid | {id:guid} | 只允许guid类型 | 276 | 277 | ##### 范围约束 278 | 279 | | 约束 | 格式 | 说明 | 280 | | ---------------- | ------------------ | ------------------ | 281 | | length(length) | {id:length(12)} | 字符串长度限制 | 282 | | maxlength(value) | {id:maxlength(8)} | 字符串最大长度限制 | 283 | | minlength(value) | {id:minlength(4)} | 字符串最小长度限制 | 284 | | range(min,max) | {id:range(18,120)} | 数值范围限制 | 285 | | min(value) | {id:min(18)} | 最小数值限制 | 286 | | max(value) | {id:max(120)} | 最大数值限制 | 287 | 288 | ##### 正则表达式约束 289 | 290 | | 约束 | 格式 | 说明 | 291 | | ----------------- | ---------------------------------------------------------- | -------------- | 292 | | regex(expression) | {id:regex(^[^@]{{1,}}@[^@\.]{{1,}}\.(com\|cn\|net\|org)$)} | 正则表达式约束 | 293 | 294 | 约束规则可在同一个路由定义中组合使用,如下: 295 | 296 | ```xml 297 | 298 | 299 | ``` 300 | 301 | 302 | 303 | #### 表单参数方式 304 | 305 | ```xml 306 | 307 | 308 | ``` 309 | 310 | ```c# 311 | using CodeM.FastApi.Context; 312 | using System.Threading.Tasks; 313 | 314 | namespace CodeM.FastApi.Controller 315 | { 316 | public class User 317 | { 318 | public async Task Create(ControllerContext cc) 319 | { 320 | await cc.JsonAsync("User name is " + cc.PostForms["name"] + ", age is " + cc.PostForms["age"]); 321 | } 322 | } 323 | } 324 | ``` 325 | 326 | ```shell 327 | curl -d "name=wangxm&age=18" http://127.0.0.1:5000/user 328 | ``` 329 | 330 | 331 | 332 | #### Post参数内容 333 | 334 | Post提交除了form-data参数形式,还可能是json、text、xml等其他形式,这时可使用如下方式获得参数内容: 335 | 336 | ```xml 337 | 338 | 339 | ``` 340 | 341 | ```c# 342 | using CodeM.FastApi.Context; 343 | using System.Threading.Tasks; 344 | 345 | namespace CodeM.FastApi.Controller 346 | { 347 | public class User 348 | { 349 | public async Task Create(ControllerContext cc) 350 | { 351 | await cc.JsonAsync("User data is " + cc.PostContent); 352 | } 353 | } 354 | } 355 | ``` 356 | 357 | ```shell 358 | curl -H "Content-Type: application/json" -X POST --data '{"name":"wangxm","age":18}' http://127.0.0.1:5000/user 359 | ``` 360 | 361 | ##### 注:通过Post方式提交的任何参数都可通过PostContent得到,但PostContent仅提供字符串格式的结果;如需其他格式,如json,需要用户根据PostContent内容自行转换。 362 | 363 | 364 | 365 | ### 太多路由映射? 366 | 367 | 框架不建议将路由定义分散在太多地方,尽量定义在根目录的router.xml中,便于问题的排查和发现。 368 | 369 | 若确实有需求,可以拆分成子文件,在子文件内进行定义,最后通过include属性在router.xml中进行整合。 370 | 371 | ```xml 372 | 373 | 374 | 375 | ``` 376 | 377 | ```xml 378 | 379 | 380 | ``` 381 | 382 | ```xml 383 | 384 | 385 | ``` 386 | 387 | ##### 注:include属性指向的目录是以当前路由文件所在目录为参照的相对路径。 -------------------------------------------------------------------------------- /libs/Logger/File/FileWriter.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.Configuration; 2 | using System; 3 | using System.Collections.Concurrent; 4 | using System.IO; 5 | using System.Text; 6 | using System.Threading; 7 | 8 | namespace CodeM.FastApi.Log.File 9 | { 10 | /// 11 | /// 日志文件分割类型 12 | /// 13 | public enum SplitType 14 | { 15 | Date = 0, //按照日期为界限进行分割,同一天的日志内容写入同一个日志文件 16 | Hour = 1, //按照小时为界限进行分割,同一小时的日志内容写入同一个日志文件 17 | Size = 2, //按照MaxFileSize设置的文件大小为界限,日志内容每到达MaxFileSize大小就写入一个日志文件 18 | None = 3 //不分割,所有日志内容写入同一个日志文件 19 | } 20 | 21 | public class FileWriter 22 | { 23 | /// 24 | /// 日志文件名称 25 | /// 26 | public static string FileName { get; set; } = string.Concat("logs", Path.DirectorySeparatorChar, "fastapi.log"); 27 | 28 | private static string FilePath { get; set; } 29 | private static string FileShortName { get; set; } 30 | private static string FileExtension { get; set; } 31 | private static string FileFullName { get; set; } 32 | 33 | public static Encoding FileEncoding { get; set; } = Encoding.UTF8; 34 | 35 | /// 36 | /// 日志方式分割类型 37 | /// 38 | public static SplitType SplitType { get; set; } = SplitType.None; 39 | 40 | /// 41 | /// 日志最大备份文件数(),默认保留10个最近的日志文件;如果设置为0,则保留所有日志文件(注意空间占用问题) 42 | /// 43 | public static int MaxFileBackups { get; set; } = 10; 44 | 45 | /// 46 | /// 日志文件最大容量,单位byte,默认2M 47 | /// 48 | public static int MaxFileSize { get; set; } = 2 * 1024 * 1024; 49 | 50 | private static ConcurrentQueue sLogs = new ConcurrentQueue(); 51 | 52 | private static bool sInited = false; 53 | 54 | public static void Init(IConfigurationSection options) 55 | { 56 | if (options != null) 57 | { 58 | if (!sInited) 59 | { 60 | string fileName = options.GetValue("FileName", null); 61 | if (!string.IsNullOrEmpty(fileName)) 62 | { 63 | FileName = fileName; 64 | } 65 | 66 | string splitType = options.GetValue("SplitType", null); 67 | if (!string.IsNullOrEmpty(splitType)) 68 | { 69 | SplitType stResult; 70 | if (Enum.TryParse(splitType, out stResult)) 71 | { 72 | SplitType = stResult; 73 | } 74 | } 75 | 76 | int? maxFileSize = options.GetValue("MaxFileSize", null); 77 | if (maxFileSize != null) 78 | { 79 | MaxFileSize = (int)maxFileSize; 80 | } 81 | 82 | int? maxFileBackups = options.GetValue("MaxFileBackups", null); 83 | if (maxFileBackups != null) 84 | { 85 | MaxFileBackups = (int)maxFileBackups; 86 | } 87 | 88 | string encoding = options.GetValue("Encoding", null); 89 | if (!string.IsNullOrEmpty(encoding)) 90 | { 91 | FileEncoding = Encoding.GetEncoding(encoding); 92 | } 93 | 94 | sInited = true; 95 | } 96 | } 97 | } 98 | 99 | private static bool InitFileInfo() 100 | { 101 | if (sInited) 102 | { 103 | FileFullName = Path.Combine(Environment.CurrentDirectory, FileName); 104 | 105 | FileInfo fi = new FileInfo(FileFullName); 106 | if (!fi.Directory.Exists) 107 | { 108 | fi.Directory.Create(); 109 | } 110 | 111 | FilePath = fi.Directory.FullName; 112 | FileShortName = fi.Name; 113 | FileExtension = fi.Extension; 114 | 115 | return true; 116 | } 117 | return false; 118 | } 119 | 120 | private static void SplitLogFile() 121 | { 122 | if (!System.IO.File.Exists(FileFullName)) 123 | { 124 | return; 125 | } 126 | 127 | switch (SplitType) 128 | { 129 | case SplitType.Date: 130 | DateTime nowDay = DateTime.Now; 131 | DateTime nextDay = nowDay.AddDays(1); 132 | DateTime nextDay2 = new DateTime(nextDay.Year, nextDay.Month, nextDay.Day, 0, 0, 0); 133 | TimeSpan diffDay = nextDay2 - nowDay; 134 | if (Math.Abs(diffDay.TotalSeconds) < 2) 135 | { 136 | DateTime deleteDay = nowDay.AddHours(-MaxFileBackups); 137 | string deleteFile = Path.Combine(FilePath, 138 | string.Concat(FileShortName.Substring(0, FileShortName.Length - FileExtension.Length), "_", deleteDay.ToString("yyyy-MM-dd"), FileExtension)); 139 | System.IO.File.Delete(deleteFile); 140 | 141 | string destFile = Path.Combine(FilePath, 142 | string.Concat(FileShortName.Substring(0, FileShortName.Length - FileExtension.Length), "_", nowDay.ToString("yyyy-MM-dd"), FileExtension)); 143 | System.IO.File.Move(FileFullName, destFile, true); 144 | Thread.Sleep(5000); 145 | } 146 | break; 147 | case SplitType.Hour: 148 | DateTime nowHour = DateTime.Now; 149 | DateTime nextHour = nowHour.AddHours(1); 150 | DateTime nextHour2 = new DateTime(nextHour.Year, nextHour.Month, nextHour.Day, nextHour.Hour, 0, 0); 151 | TimeSpan diffHour = nextHour2 - nowHour; 152 | if (Math.Abs(diffHour.TotalSeconds) < 2) 153 | { 154 | DateTime deleteHour = nowHour.AddHours(-MaxFileBackups); 155 | string deleteFile = Path.Combine(FilePath, 156 | string.Concat(FileShortName.Substring(0, FileShortName.Length - FileExtension.Length), "_", deleteHour.ToString("yyyy-MM-dd-HH"), FileExtension)); 157 | System.IO.File.Delete(deleteFile); 158 | 159 | string destFile = Path.Combine(FilePath, 160 | string.Concat(FileShortName.Substring(0, FileShortName.Length - FileExtension.Length), "_", nowHour.ToString("yyyy-MM-dd-HH"), FileExtension)); 161 | System.IO.File.Move(FileFullName, destFile, true); 162 | Thread.Sleep(5000); 163 | } 164 | break; 165 | case SplitType.Size: 166 | FileInfo fi = new FileInfo(FileFullName); 167 | if (fi.Length >= MaxFileSize) 168 | { 169 | if (MaxFileBackups > 0) 170 | { 171 | MoveSplitFileByOrderIfNeed(MaxFileBackups); 172 | 173 | string fileSuffix = ("" + MaxFileBackups).PadLeft(("" + MaxFileBackups).Length, '0'); 174 | string destFile = Path.Combine(FilePath, 175 | string.Concat(FileShortName.Substring(0, FileShortName.Length - FileExtension.Length), "_", fileSuffix, FileExtension)); 176 | System.IO.File.Move(FileFullName, destFile, true); 177 | } 178 | else 179 | { 180 | string destFile = Path.Combine(FilePath, 181 | string.Concat(FileShortName.Substring(0, FileShortName.Length - FileExtension.Length), "_", DateTime.Now.ToString("yyyyMMddHHmmss"), FileExtension)); 182 | System.IO.File.Move(FileFullName, destFile, true); 183 | } 184 | } 185 | break; 186 | default: 187 | break; 188 | } 189 | } 190 | 191 | /// 192 | /// 新的分割文件产生时,将已有文件顺序向前覆盖,淘汰最早的一个文件,SplitType=Size时启用 193 | /// 194 | /// 195 | private static void MoveSplitFileByOrderIfNeed(int fileIndex) 196 | { 197 | if (fileIndex > 1) 198 | { 199 | string fileSuffix = ("" + fileIndex).PadLeft(("" + MaxFileBackups).Length, '0'); 200 | string filename = Path.Combine(FilePath, 201 | string.Concat(FileShortName.Substring(0, FileShortName.Length - FileExtension.Length), "_", fileSuffix, FileExtension)); 202 | if (System.IO.File.Exists(filename)) 203 | { 204 | MoveSplitFileByOrderIfNeed(fileIndex - 1); 205 | 206 | string fileSuffix2 = ("" + (fileIndex - 1)).PadLeft(("" + MaxFileBackups).Length, '0'); 207 | string destFile = Path.Combine(FilePath, 208 | string.Concat(FileShortName.Substring(0, FileShortName.Length - FileExtension.Length), "_", fileSuffix2, FileExtension)); 209 | System.IO.File.Move(filename, destFile, true); 210 | } 211 | } 212 | } 213 | 214 | private static void WriteHandler() 215 | { 216 | if (InitFileInfo()) 217 | { 218 | string log; 219 | StringBuilder sbBuff = new StringBuilder(); 220 | int emptyLoop = 0; 221 | while (true) 222 | { 223 | if (sLogs.Count > 0) 224 | { 225 | emptyLoop = 0; 226 | 227 | try 228 | { 229 | SplitLogFile(); 230 | 231 | while (sLogs.TryDequeue(out log)) 232 | { 233 | if (!string.IsNullOrEmpty(log)) 234 | { 235 | sbBuff.AppendLine(log); 236 | if (sbBuff.Length > 10240) 237 | { 238 | break; 239 | } 240 | } 241 | } 242 | } 243 | catch 244 | { 245 | } 246 | finally 247 | { 248 | if (sbBuff.Length > 0) 249 | { 250 | System.IO.File.AppendAllText(FileFullName, sbBuff.ToString(), FileEncoding); 251 | sbBuff.Length = 0; 252 | } 253 | } 254 | } 255 | else 256 | { 257 | emptyLoop++; 258 | if (emptyLoop >= 60) 259 | { 260 | break; 261 | } 262 | } 263 | 264 | Thread.Sleep(1000); 265 | } 266 | } 267 | } 268 | 269 | private static Thread sWriteThread; 270 | private static object sThreadStartLock = new object(); 271 | 272 | public static void Write(string log) 273 | { 274 | sLogs.Enqueue(log); 275 | 276 | if (sWriteThread == null || !sWriteThread.IsAlive) 277 | { 278 | lock (sThreadStartLock) 279 | { 280 | if (sWriteThread == null || !sWriteThread.IsAlive) 281 | { 282 | if (sWriteThread == null || 283 | sWriteThread.ThreadState == ThreadState.Stopped || 284 | sWriteThread.ThreadState == ThreadState.Aborted) 285 | { 286 | sWriteThread = new Thread(WriteHandler); 287 | } 288 | sWriteThread.IsBackground = true; 289 | sWriteThread.Start(); 290 | } 291 | } 292 | } 293 | } 294 | } 295 | } 296 | --------------------------------------------------------------------------------