Compare commits

...

52 Commits

Author SHA1 Message Date
Falcon
eb5881873d 修改版本号2.14.2发布 2025-03-06 14:38:36 +08:00
Falcon
2fec21a3ea TimedTask使用进准的计时器方式而非轮训方式。 2025-03-06 14:38:16 +08:00
Falcon
7350a60bb2 ToJson方法防止中文编码 2025-03-06 14:37:23 +08:00
Falcon
7aaaacb309 优化代码 2025-03-05 11:24:06 +08:00
Falcon
d64b49f8a1 发布插件2.14.2 2025-03-04 13:57:58 +08:00
Falcon
5081ddb227 修改名字空间 2025-03-04 13:57:26 +08:00
Falcon
d93af99a15 增加异常定义。完成初步测试 2025-02-24 16:55:32 +08:00
Falcon
f3096745e3 将日判断和星期判断分开 2025-02-24 16:22:13 +08:00
Falcon
fb3544ec89 增加后台任务开始后和每次执行完成后任务退出机制 2025-02-24 11:08:58 +08:00
Falcon
3560ab0e54 后台任务发生异常时增加默认异常处理,并实现了异常退出机制 2025-02-24 10:56:38 +08:00
Falcon
cdfedb4c6b 去掉重复的IsYearAdjust检查 2025-02-24 10:32:55 +08:00
Falcon
4f19f502eb 增加说明文档 2025-02-20 17:24:20 +08:00
Falcon
222342969c CronSchedule改为必须实现。 2025-02-20 17:24:11 +08:00
Falcon
ace506c577 整理代码格式 2025-02-20 16:48:31 +08:00
Falcon
6092239b29 修正目标框架 2025-02-20 16:38:21 +08:00
Falcon
9f8b5071b7 修正TimedTask错误 2025-02-20 16:33:32 +08:00
Falcon
6e68a8ad8c 修正cron解析错误 2025-02-20 16:33:11 +08:00
Falcon
690feef066 优化调整 2025-02-20 09:39:04 +08:00
Falcon
e6101c0f6e 修改注册TimedTask的注册方法。发布新版本 2025-02-19 16:56:12 +08:00
Falcon
4c362574e6 初步完成TimedTask,等待测试 2025-02-19 16:45:17 +08:00
Falcon
4bb1064352 初步TimedTask完成 2025-02-19 15:57:34 +08:00
Falcon
3805d2b645 完成CronExpression并测试 2025-02-19 15:57:11 +08:00
Falcon
a0a4f98f61 增加Object.Between方法和测试 2025-02-14 15:49:25 +08:00
Falcon
a7617c35e1 升级2.13.0
新增支持从xml文件中获取Type和属性的summary。
2025-02-12 15:54:57 +08:00
Falcon
0e8b885784 更新支持字段加密 2025-02-11 17:03:49 +08:00
Falcon
a0773bbdd6 升级SqlSugarCore 5.1.4.175,发布版本2.11.0 2025-02-08 14:09:27 +08:00
Falcon
2b5aae7385 发布2.10.0 2025-02-08 14:07:36 +08:00
Falcon
d133bf934d 优化Object的InNotIn方法。增加String的In和NotIn方法。 2025-02-08 14:07:06 +08:00
Falcon
d527ec9411 优化,清除无用using 2025-02-08 10:42:34 +08:00
Falcon
aec7cc3994 更新版本号,发布组件 2025-01-16 17:15:15 +08:00
Falcon
224ab2d621 增加In方法,判断对象是否在目标数组中 2025-01-16 17:14:43 +08:00
Falcon
633f20e46a 发布2.8.0 2024-12-03 17:38:01 +08:00
Falcon
b079a7d390 为主键增加KeyAttribute特性 2024-12-03 17:36:56 +08:00
Falcon
d3679de856 修改版本号,发布组件 2024-12-03 14:16:13 +08:00
Falcon
4d55eba3a8 扩展跨域请求,组件可以注册自己的跨域请求处理策略。 2024-12-03 14:15:49 +08:00
Falcon
cd93e01074 增加String.SplitToStringList方法。 2024-12-03 11:23:59 +08:00
Falcon
66d22c5c08 nugetplugin测试 2024-11-20 10:56:44 +08:00
Falcon
b581dce350 发布v2.6.0 2024-10-25 11:37:50 +08:00
Falcon
075ebb1fec 新增禁止删除列和禁止自动更新表结构的服务。只需要在列模型上增加DisableDeleteColummAttribute和DisableUpdateAttribute特性即可。 2024-10-25 10:46:15 +08:00
Falcon
0b6d923df7 修改版本v2.5.0发布。 2024-10-24 11:25:43 +08:00
Falcon
8a1fc6b420 添加代理类测试 2024-10-24 11:25:04 +08:00
Falcon
d32e95cdff 代理类分为普通代理和泛型代理 2024-10-24 11:24:38 +08:00
Falcon
59cf1928f8 增加对象代理 2024-10-23 17:28:51 +08:00
Falcon
bba31988a4 修改版本号 2024-10-22 10:14:47 +08:00
Falcon
e4a54ddd34 增加string.TrimString扩展方法,可以去除头尾给定字符串 2024-10-22 10:14:23 +08:00
Falcon
eb45aa189a 新增NugetPluginTest测试 2024-08-27 10:52:27 +08:00
Falcon
1f46efcce1 消除空格 2024-08-21 15:44:52 +08:00
Falcon
121b2ca127 新增IGetDataTimeNow接口,返回当前系统时间 2024-08-21 15:36:07 +08:00
Falcon
0540eadce8 tag v2.2 2024-08-19 16:26:57 +08:00
Falcon
8e2231df90 支持多租户数据库 2024-08-19 16:03:11 +08:00
Falcon
a879a5d585 升级RabbitMQ.Client客户端 2024-07-25 15:52:49 +08:00
Falcon
6c52267815 增加record测试 2024-07-25 11:46:05 +08:00
54 changed files with 2115 additions and 32 deletions

View File

@ -0,0 +1,19 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<IsPackable>false</IsPackable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.1.0" />
<PackageReference Include="NUnit" Version="3.13.3" />
<PackageReference Include="NUnit3TestAdapter" Version="4.2.1" />
<PackageReference Include="NUnit.Analyzers" Version="3.3.0" />
<PackageReference Include="coverlet.collector" Version="3.1.2" />
</ItemGroup>
</Project>

31
CSharpTest/UnitTest1.cs Normal file
View File

@ -0,0 +1,31 @@
using System.Text.Json;
namespace CSharpTest
{
public record Person(string name,int age);
public class RecordTests
{
[SetUp]
public void Setup() {
}
[Test]
public void Test1() {
var p1 = new Person("ÕÅÈý",40);
var p2 = p1 with { age = 50 };
Console.WriteLine(p1 == p2);
Console.WriteLine(p1.Equals(p2));
Console.WriteLine(p1.ToString());
Console.WriteLine(JsonSerializer.Serialize(p1));
Console.WriteLine(JsonSerializer.Serialize(p2));
var p3 = new Person("ÕÅÈý",40);
var p4 = new Person("ÕÅÈý",40);
Console.WriteLine(p3 == p4);
Assert.IsTrue(p3 == p4);
}
}
}

1
CSharpTest/Usings.cs Normal file
View File

@ -0,0 +1 @@
global using NUnit.Framework;

View File

@ -0,0 +1,223 @@
using Falcon.SugarApi.TimedBackgroundTask;
using Microsoft.AspNetCore.Mvc.TagHelpers;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using System;
namespace Falcon.SugarApi.Test
{
[TestClass()]
public class CronExpressionTests
{
[TestMethod]
public void CronExpressionTest() {
var now20250213102512 = new DateTime(2025,2,13,10,25,12);
var cron = new CronExpression("0 30 3 * * 1-5"); // 工作日凌晨3:30:00
var nextTime = cron.GetNextOccurrence(now20250213102512);
Assert.IsTrue(DateToString(nextTime) == "20250214033000");
cron = new CronExpression("");//下一秒触发
nextTime = cron.GetNextOccurrence(now20250213102512);
Assert.IsTrue(DateToString(nextTime) == "20250213102513");
nextTime = cron.GetNextOccurrence(nextTime);
Assert.IsTrue(DateToString(nextTime) == "20250213102514",nextTime.ToString());
nextTime = cron.GetNextOccurrence(nextTime);
Assert.IsTrue(DateToString(nextTime) == "20250213102515",nextTime.ToString());
nextTime = cron.GetNextOccurrence(nextTime);
Assert.IsTrue(DateToString(nextTime) == "20250213102516",nextTime.ToString());
cron = new CronExpression("10");//每分钟10秒时触发
nextTime = cron.GetNextOccurrence(now20250213102512);
Assert.IsTrue(DateToString(nextTime) == "20250213102610");
cron = new CronExpression("10 25");//每个小时的25分10秒触发
nextTime = cron.GetNextOccurrence(now20250213102512);
Assert.IsTrue(DateToString(nextTime) == "20250213112510");
cron = new CronExpression("10 25");//每个小时的25分10秒触发
nextTime = cron.GetNextOccurrence(now20250213102512);
Assert.IsTrue(DateToString(nextTime) == "20250213112510");
cron = new CronExpression("10/10 25");//每个小时的25分10秒触发,只有每10秒触发一次
nextTime = cron.GetNextOccurrence(now20250213102512);
Assert.IsTrue(DateToString(nextTime) == "20250213102520");
nextTime = cron.GetNextOccurrence(nextTime);
Assert.IsTrue(DateToString(nextTime) == "20250213102530");
cron = new CronExpression("0 0 3-5");//每天凌晨3点到5点每小时触发一次
nextTime = cron.GetNextOccurrence(now20250213102512);
Assert.IsTrue(DateToString(nextTime) == "20250214030000");
nextTime = cron.GetNextOccurrence(nextTime);
Assert.IsTrue(DateToString(nextTime) == "20250214040000");
nextTime = cron.GetNextOccurrence(nextTime);
Assert.IsTrue(DateToString(nextTime) == "20250214050000");
nextTime = cron.GetNextOccurrence(nextTime);
Assert.IsTrue(DateToString(nextTime) == "20250215030000");
cron = new CronExpression("0 0 3-5 * * 1-5");//每个工作日凌晨3点到5点每小时触发一次
nextTime = cron.GetNextOccurrence(now20250213102512);
Assert.IsTrue(DateToString(nextTime) == "20250214030000");
nextTime = cron.GetNextOccurrence(nextTime);
Assert.IsTrue(DateToString(nextTime) == "20250214040000");
nextTime = cron.GetNextOccurrence(nextTime);
Assert.IsTrue(DateToString(nextTime) == "20250214050000");
nextTime = cron.GetNextOccurrence(nextTime);
Assert.IsTrue(DateToString(nextTime) == "20250217030000",DateToString(nextTime));
cron = new CronExpression("0 0/30 3-5 * * 1-5");//每个工作日凌晨3点到5点每小时触发两次平均每半小时一次
nextTime = cron.GetNextOccurrence(now20250213102512);
Assert.IsTrue(DateToString(nextTime) == "20250214030000",nextTime.ToString());
nextTime = cron.GetNextOccurrence(nextTime);
Assert.IsTrue(DateToString(nextTime) == "20250214033000");
nextTime = cron.GetNextOccurrence(nextTime);
Assert.IsTrue(DateToString(nextTime) == "20250214040000");
nextTime = cron.GetNextOccurrence(nextTime);
Assert.IsTrue(DateToString(nextTime) == "20250214043000");
nextTime = cron.GetNextOccurrence(nextTime);
Assert.IsTrue(DateToString(nextTime) == "20250214050000");
nextTime = cron.GetNextOccurrence(nextTime);
Assert.IsTrue(DateToString(nextTime) == "20250214053000");
nextTime = cron.GetNextOccurrence(nextTime);
Assert.IsTrue(DateToString(nextTime) == "20250217030000");
}
private string? DateToString(DateTime? date) => date?.ToString("yyyyMMddHHmmss");
//-----------------------------
// 基础语法解析测试
//-----------------------------
[TestMethod]
public void TestBasicWildcard() {
var cron = new CronExpression("* * * * * * *");
Assert.AreEqual(60,cron.Seconds.Count,"秒字段应包含0-59");
Assert.AreEqual(12,cron.Months.Count,"月字段应包含1-12");
}
[TestMethod]
public void TestFixedValue() {
var cron = new CronExpression("5 10 3 15 6 2 2026");
Assert.IsTrue(cron.Seconds.Contains(5));
Assert.IsTrue(cron.Minutes.Contains(10));
Assert.IsTrue(cron.Hours.Contains(3));
Assert.IsTrue(cron.DaysOfMonth.Contains(15));
Assert.IsTrue(cron.Months.Contains(6));
Assert.IsTrue(cron.DaysOfWeek.Contains(2));
Assert.IsTrue(cron.Years.Contains(2026));
}
//-----------------------------
// 复杂语法测试
//-----------------------------
[TestMethod]
public void TestRangeAndStep() {
//var cron = new CronExpression("0-30/15 10,20,30 * 1-5 */3 * *");
//CollectionAssert.AreEqual(new[] { 0,15,30 },cron.Seconds);
//CollectionAssert.AreEqual(new[] { 10,20,30 },cron.Minutes);
//CollectionAssert.AreEqual(new[] { 1,2,3,4,5 },cron.DaysOfMonth);
//CollectionAssert.AreEqual(new[] { 1,4,7,10 },cron.Months);
}
//-----------------------------
// 字段省略测试
//-----------------------------
[TestMethod]
public void TestPartialExpression() {
var cron = new CronExpression("30 15 4");
Assert.AreEqual(1,cron.Seconds.Count);
Assert.AreEqual(1,cron.Minutes.Count);
Assert.AreEqual(1,cron.Hours.Count);
Assert.AreEqual(31,cron.DaysOfMonth.Count); // 默认1-31
Assert.AreEqual(12,cron.Months.Count); // 默认1-12
}
//-----------------------------
// 边界值测试
//-----------------------------
[TestMethod]
public void TestBoundaryValues() {
// 最大最小值测试
var cron = new CronExpression("59 59 23 31 12 6 2026");
Assert.IsTrue(cron.Seconds.Contains(59));
Assert.IsTrue(cron.DaysOfMonth.Contains(31));
Assert.IsTrue(cron.Months.Contains(12));
}
//-----------------------------
// 错误处理测试
//-----------------------------
[TestMethod]
public void TestInvalidCharacter() {
try {
var _ = new CronExpression("5a * * * * *");
}
catch(CronAdjustNullException) { }
catch(CronException) { }
}
[TestMethod]
public void TestOutOfRangeValue() {
try {
var _ = new CronExpression("70 * * * * *");
}
catch(CronAdjustNullException) { }
catch(CronException) { }
}
//-----------------------------
// 下次执行时间计算测试
//-----------------------------
[TestMethod]
public void TestNextOccurrence_Basic() {
var baseTime = new DateTime(2025,10,1,12,30,15);
var cron = new CronExpression("30 * * * * * *");
// 下一个30秒
var expected = new DateTime(2025,10,1,12,30,30);
var actual = cron.GetNextOccurrence(baseTime);
Assert.AreEqual(expected,actual);
}
[TestMethod]
public void TestNextOccurrence_MonthRollover() {
var baseTime = new DateTime(2024,12,31,23,59,59);
var cron = new CronExpression("* * * 1 * * *"); // 每月1号
var expected = new DateTime(2025,1,1,0,0,0);
var actual = cron.GetNextOccurrence(baseTime);
Assert.AreEqual(expected,actual);
}
//-----------------------------
// 特殊日期处理测试
//-----------------------------
[TestMethod]
public void TestFebruaryHandling() {
// 测试闰年2月29日
var cron = new CronExpression("0 0 0 29 2 * 2028");
Assert.IsTrue(cron.DaysOfMonth.Contains(29));
var nextTime = cron.GetNextOccurrence(new DateTime(2028,2,28));
Assert.AreEqual(new DateTime(2028,2,29),nextTime.Date);
}
//-----------------------------
// 日期与星期冲突测试
//-----------------------------
[TestMethod]
public void TestDayOfWeekConflict() {
// 每月15号且为星期一2023-10-15是星期日
var cron = new CronExpression("* * * 15 * 1 *");
var baseTime = new DateTime(2023,10,1);
var nextTime = cron.GetNextOccurrence(baseTime);
Assert.AreEqual(2025,nextTime.Year,nextTime.ToString());
Assert.AreEqual(9,nextTime.Month,nextTime.ToString());
Assert.AreEqual(15,nextTime.Day,nextTime.ToString());
Assert.AreEqual(DayOfWeek.Monday,nextTime.DayOfWeek);
}
}
}

View File

@ -0,0 +1,41 @@
using Microsoft.VisualStudio.TestTools.UnitTesting;
using System;
namespace Falcon.SugarApi.Test
{
[TestClass]
public class IGetDataTimeNowTest
{
[TestMethod]
public void nowTest() {
var year = DateTime.Now.Year;
Console.WriteLine($"base year {year}");
var ic1 = new c1();
Console.WriteLine($"c1 year {ic1.now.Year}");
var ic2 = new c2();
Console.WriteLine($"c2 year {ic2.now.Year}");
var ic3 = new c3();
Console.WriteLine($"c3 year {ic3.now.Year}");
Assert.IsTrue(ic1.now.Year == year);
Assert.IsTrue(ic3.now.Year == year);
Assert.IsTrue(ic2.now.Year == year + 1);
}
}
public class c1
{
public c1() {
this.now = this is IGetDataTimeNow gdn ? gdn.GetDataTimeNow() : DateTime.Now;
}
public DateTime now { get; set; }
}
public class c2:c1, IGetDataTimeNow
{
public DateTime GetDataTimeNow() {
return DateTime.Now.AddYears(1);
}
}
public class c3:c1 { }
}

View File

@ -125,6 +125,38 @@ namespace Falcon.SugarApi.Test
Assert.IsTrue(fullCount > nFullCount);
}
/// <summary>
/// 测试In方法
/// </summary>
[TestMethod]
public void ObjectInTest() {
string obj = "abc";
Assert.IsTrue(obj.In("eee","abc","1231"));
Assert.IsFalse(obj.In("2313","eeee"));
var o1 = new object();
Assert.IsTrue(o1.In(o1));
Assert.IsFalse(o1.In(new object(),new object()));
var o2 = new ObjWithComparable { val = 1 };
var o22 = new ObjWithComparable { val = 1 };
var o23 = new ObjWithComparable { val = 1 };
Assert.IsTrue(o2.In(o22,o23));
}
/// <summary>
/// 应用动态比较的方式比较对象是否在另外两个对象区间。
/// </summary>
[TestMethod]
public void ObjectBetweenTest() {
Assert.IsTrue(1.Between(0,2));
Assert.IsFalse(3.Between(0,1));
var now = DateTime.Now;
Assert.IsTrue(now.Between(now.AddSeconds(-1),now.AddSeconds(1)));
Assert.IsFalse(now.Between(now.AddSeconds(1),now.AddSeconds(2)));
}
}
/// <summary>
/// 扩展属性测试类
@ -157,4 +189,13 @@ namespace Falcon.SugarApi.Test
{
public string ItemA { get; set; } = "itema";
}
public class ObjWithComparable:IComparable<ObjWithComparable>
{
public int val { get; set; }
public int CompareTo(ObjWithComparable? other) {
return this.val - other.val;
}
}
}

View File

@ -0,0 +1,68 @@
using Falcon.SugarApi.Proxy;
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace Falcon.SugarApi.Test
{
/// <summary>
/// 代理类测试
/// </summary>
[TestClass]
public class ProxyTest
{
/// <summary>
/// 基础功能测试
/// </summary>
[TestMethod]
public void BaseTest() {
var obj = new TestObject { Name = "tom" };
//普通代理测试
var pobj = new ProxyObj(obj);
pobj.SetAction("Name","123");
Assert.AreEqual(obj.Name,"ProxyObj run:123");
Assert.IsTrue(pobj.GetFunc("Name").ToString() == "ProxyObj run:123");
pobj.Invoke("SetName","jack");
Assert.IsTrue(obj.Name == "jack");
//泛型代理测试
var pobj1 = new ProxyObj1(obj);
pobj1.SetAction("Name","123");
Assert.AreEqual(obj.Name,"ProxyObj1 run:123");
Assert.IsTrue(pobj1.GetFunc("Name").ToString() == "ProxyObj1 run:123");
pobj1.Invoke("SetName","jack1");
Assert.IsTrue(obj.Name == "jack1");
}
class TestObject
{
public string Name { get; set; }
public void SetName(string newName) {
this.Name = newName;
}
}
class ProxyObj:ProxyBase
{
public ProxyObj(object target) : base(target) {
}
public override void SetAction(string prop,object? value) {
value = "ProxyObj run:" + value;
base.SetAction(prop,value);
}
}
class ProxyObj1:Proxy<TestObject>
{
public ProxyObj1(TestObject target) : base(target) {
}
public override void SetAction(string prop,object? value) {
value = "ProxyObj1 run:" + value;
base.SetAction(prop,value);
}
}
}
}

View File

@ -112,5 +112,40 @@ namespace Falcon.SugarApi.Test
Assert.IsTrue(result.Length == 1);
Assert.IsTrue(result[0] == "abc");
}
/// <summary>
/// 去除头尾字符
/// </summary>
[TestMethod]
public void TestTrimString() {
var str = @"abcabcffe222256256";
//去除头部的abc字符
var r = str.TrimString("abc");
Assert.AreEqual(r , "ffe222256256");
//去除尾部256字符
r = str.TrimString("","256");
Assert.AreEqual(r,"abcabcffe222");
//去除头部abc字符和尾部256字符
r = str.TrimString("abc","256");
Assert.AreEqual(r,"ffe222");
}
/// <summary>
/// SplitToStringList测试
/// </summary>
[TestMethod]
public void SplitToStringListTest() {
var str = "abc,123,ggg";
var l = str.SplitToStringList();
Assert.IsTrue(l.Count == 3);
Assert.IsTrue(l[0] == "abc");
Assert.IsTrue(l[1] == "123");
Assert.IsTrue(l[2] == "ggg");
str = "";
l = str.SplitToStringList();
Assert.IsTrue(l.Count == 0);
}
}
}

View File

@ -19,6 +19,10 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Falcon.SugarApi.Windows", "
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "RabbitMqTest", "RabbitMqTest\RabbitMqTest.csproj", "{7F308FE9-B5C5-45BA-B882-15B4B983C39C}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CSharpTest", "CSharpTest\CSharpTest.csproj", "{FA6A7A47-7EEE-4264-9C89-C2E717B1CB78}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "NugetPluginTest", "NugetPluginTest\NugetPluginTest.csproj", "{3570FDA6-C408-4A61-852E-3B4F1DCAD1AD}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@ -45,6 +49,14 @@ Global
{7F308FE9-B5C5-45BA-B882-15B4B983C39C}.Debug|Any CPU.Build.0 = Debug|Any CPU
{7F308FE9-B5C5-45BA-B882-15B4B983C39C}.Release|Any CPU.ActiveCfg = Release|Any CPU
{7F308FE9-B5C5-45BA-B882-15B4B983C39C}.Release|Any CPU.Build.0 = Release|Any CPU
{FA6A7A47-7EEE-4264-9C89-C2E717B1CB78}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{FA6A7A47-7EEE-4264-9C89-C2E717B1CB78}.Debug|Any CPU.Build.0 = Debug|Any CPU
{FA6A7A47-7EEE-4264-9C89-C2E717B1CB78}.Release|Any CPU.ActiveCfg = Release|Any CPU
{FA6A7A47-7EEE-4264-9C89-C2E717B1CB78}.Release|Any CPU.Build.0 = Release|Any CPU
{3570FDA6-C408-4A61-852E-3B4F1DCAD1AD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{3570FDA6-C408-4A61-852E-3B4F1DCAD1AD}.Debug|Any CPU.Build.0 = Debug|Any CPU
{3570FDA6-C408-4A61-852E-3B4F1DCAD1AD}.Release|Any CPU.ActiveCfg = Release|Any CPU
{3570FDA6-C408-4A61-852E-3B4F1DCAD1AD}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE

View File

@ -0,0 +1,10 @@
using System;
namespace Falcon.SugarApi.DatabaseDefinitions
{
/// <summary>
/// 禁止删除表格的列
/// </summary>
[AttributeUsage(AttributeTargets.Class,AllowMultiple = false,Inherited = true)]
public class DisableDeleteColummAttribute:Attribute { }
}

View File

@ -0,0 +1,10 @@
using System;
namespace Falcon.SugarApi.DatabaseDefinitions
{
/// <summary>
/// 禁止更新表结构
/// </summary>
[AttributeUsage(AttributeTargets.Class,AllowMultiple = false,Inherited = true)]
public class DisableUpdateAttribute:Attribute { }
}

View File

@ -1,5 +1,6 @@
using SqlSugar;
using System;
using System.ComponentModel.DataAnnotations;
namespace Falcon.SugarApi.DatabaseDefinitions
{
@ -10,6 +11,7 @@ namespace Falcon.SugarApi.DatabaseDefinitions
/// <summary>
/// 主键
/// </summary>
[Key]
[SugarColumn(IsPrimaryKey = true,ColumnDescription = "主键")]
public Guid? Id { get; set; } = Guid.NewGuid();
}

View File

@ -1,4 +1,5 @@
using SqlSugar;
using System.ComponentModel.DataAnnotations;
namespace Falcon.SugarApi.DatabaseDefinitions
{
@ -10,6 +11,7 @@ namespace Falcon.SugarApi.DatabaseDefinitions
/// <summary>
/// 主键 默认GUID 382c74c3-721d-4f34-80e5-57657b6cbc27
/// </summary>
[Key]
[SugarColumn(IsPrimaryKey = true,ColumnDescription = "主键")]
public int? Id { get; set; }
}

View File

@ -0,0 +1,18 @@
using SqlSugar;
using System;
namespace Falcon.SugarApi.DatabaseDefinitions.EntityServices
{
/// <summary>
/// 设置禁止删除列服务
/// </summary>
public class DisableDeleteColummService:IEntityTableServices
{
/// <inheritdoc/>
public void SetupTable(Type t,EntityInfo e) {
if(t.TryGetAttribute<DisableDeleteColummAttribute>(out var _)) {
e.IsDisabledDelete = true;
}
}
}
}

View File

@ -0,0 +1,18 @@
using SqlSugar;
using System;
namespace Falcon.SugarApi.DatabaseDefinitions.EntityServices
{
/// <summary>
/// 设置禁止升级表架构服务
/// </summary>
public class DisableUpdateAllService:IEntityTableServices
{
/// <inheritdoc/>
public void SetupTable(Type t,EntityInfo e) {
if(t.TryGetAttribute<DisableUpdateAttribute>(out var _)) {
e.IsDisabledUpdateAll = true;
}
}
}
}

View File

@ -12,6 +12,7 @@ namespace Falcon.SugarApi.DatabaseDefinitions
/// 主键 默认GUID 382c74c3-721d-4f34-80e5-57657b6cbc27
/// </summary>
[SugarColumn(IsPrimaryKey = true,ColumnDescription = "主键")]
[Key]
[MaxLength(36)]
public string? Id { get; set; }
}

View File

@ -9,6 +9,12 @@ namespace Falcon.SugarApi.DatabaseDefinitions
/// </summary>
public abstract class SugarBasicTable:EntityGuidId
{
/// <summary>
/// 构造数据表基类
/// </summary>
public SugarBasicTable() {
this.CreateTime = this is IGetDataTimeNow gtn ? gtn.GetDataTimeNow() : DateTime.Now;
}
/// <summary>
/// 创建时间
/// </summary>
@ -24,7 +30,7 @@ namespace Falcon.SugarApi.DatabaseDefinitions
/// <returns>本条数据</returns>
public virtual SugarBasicTable SetNew(DateTime? now = null) {
this.Id = Guid.NewGuid();
this.CreateTime = now ?? DateTime.Now;
this.CreateTime = now ?? (this is IGetDataTimeNow gtn ? gtn.GetDataTimeNow() : DateTime.Now);
return this;
}
}

View File

@ -9,6 +9,12 @@ namespace Falcon.SugarApi.DatabaseDefinitions
/// </summary>
public abstract class SugarBasicTable2:EntityStringId
{
/// <summary>
/// 构造数据表基类
/// </summary>
public SugarBasicTable2() {
this.CreateTime = this is IGetDataTimeNow gtn ? gtn.GetDataTimeNow() : DateTime.Now;
}
/// <summary>
/// 创建时间
/// </summary>

View File

@ -9,6 +9,12 @@ namespace Falcon.SugarApi.DatabaseDefinitions
/// </summary>
public abstract class SugarBasicTable3:EntityStringId
{
/// <summary>
/// 构造数据表基类
/// </summary>
public SugarBasicTable3() {
this.CreateTime = this is IGetDataTimeNow gtn ? gtn.GetDataTimeNow() : DateTime.Now;
}
/// <summary>
/// 记录状态
/// </summary>
@ -18,6 +24,6 @@ namespace Falcon.SugarApi.DatabaseDefinitions
/// 创建时间
/// </summary>
[Required, SugarColumn(ColumnDescription = "创建时间")]
public DateTime CreateTime { get; set; } = DateTime.Now;
public DateTime CreateTime { get; set; }
}
}

View File

@ -1,5 +1,4 @@
using Microsoft.Extensions.Logging;
using SqlSugar;
using SqlSugar;
using System;
namespace Falcon.SugarApi.DatabaseManager

View File

@ -23,6 +23,10 @@ namespace Falcon.SugarApi.DatabaseManager
/// 链接字符串
/// </summary>
public string ConnectionString { get; set; }
/// <summary>
/// 是否支持字段加密
/// </summary>
public bool IsEnableEncrypy { get; set; } = true;
}
}

View File

@ -2,6 +2,7 @@
using Microsoft.Extensions.Logging;
using SqlSugar;
using System;
using System.Linq;
namespace Falcon.SugarApi.DatabaseManager
{
@ -24,12 +25,25 @@ namespace Falcon.SugarApi.DatabaseManager
this.Logger = service.GetService(typeof(ILogger<>).MakeGenericType(GetType())) as ILogger;
ConfigureExternalServices(this.CurrentConnectionConfig);
}
/// <summary>
/// 使用服务提供程序和配置集合初始化数据库上下文
/// </summary>
/// <param name="service">服务提供程序</param>
/// <param name="configs">数据库上下文配置集合</param>
public DbContextBase(IServiceProvider service,params ConnectionConfig[] configs) : base(configs.ToList()) {
this.Logger = service.GetService(typeof(ILogger<>).MakeGenericType(GetType())) as ILogger;
foreach(var c in configs) {
ConfigureExternalServices(this.GetConnection(c.ConfigId).CurrentConnectionConfig);
}
}
private static void ConfigureExternalServices(ConnectionConfig config) {
config.ConfigureExternalServices ??= new ConfigureExternalServices();
var ces = config.ConfigureExternalServices;
ces.EntityNameService = (t,e) => {
new TableNameTableService().SetupTable(t,e);
new DisableDeleteColummService().SetupTable(t,e);
new DisableUpdateAllService().SetupTable(t,e);
};
ces.EntityService = (p,c) => {
new SetupKeyColumnServices().SetupColumn(p,c);

View File

@ -0,0 +1,39 @@
using System.Reflection;
namespace Falcon.SugarApi.DatabaseManager
{
/// <summary>
/// 数据库上下文字段加密支持
/// </summary>
public static class DbContextBaseEncryptExtend
{
/// <summary>
/// 设置数据库上下文支持字段加密
/// </summary>
/// <param name="dbContext">数据库上下文</param>
public static void SetEnableEncrypt(this DbContextBase dbContext) {
dbContext.Aop.DataExecuting = (value,entity) => {
var attr = entity.GetAttribute<EnableEncryptAttribute>();
var enProvider = attr.EncryptType.Assembly.CreateInstance(attr.EncryptType.FullName) as IEncrypt;
if(attr == null) {
return;
}
var newVal = enProvider.Encrypt(value.ToString());
entity.SetValue(newVal);
};
dbContext.Aop.DataExecuted = (value,entity) => {
foreach(var col in entity.EntityColumnInfos) {
var pName = col.PropertyName;
var attr = col.PropertyInfo.GetCustomAttribute<EnableEncryptAttribute>();
var enProvider = attr.EncryptType.Assembly.CreateInstance(attr.EncryptType.FullName) as IEncrypt;
if(attr == null) {
continue;
}
var newVal = enProvider.Decrypt(entity.GetValue(pName).ToString());
entity.SetValue(pName,newVal);
}
};
}
}
}

View File

@ -0,0 +1,28 @@
using System;
namespace Falcon.SugarApi.DatabaseManager
{
/// <summary>
/// 允许对字段进行加密存储
/// </summary>
[AttributeUsage(AttributeTargets.Property,AllowMultiple = false,Inherited = false)]
public class EnableEncryptAttribute:Attribute
{
/// <summary>
/// 用于加密的类型
/// </summary>
public Type EncryptType { get; set; }
/// <summary>
/// 通过提供加密解密接口实现字段加密
/// </summary>
/// <param name="encryptType">实现加密解密的类型</param>
/// <exception cref="ArgumentException">提供的类型不符合要求</exception>
public EnableEncryptAttribute(Type encryptType) {
if(!typeof(IEncrypt).IsAssignableFrom(encryptType)) {
throw new ArgumentException("encryptType must implement IEncrypt");
}
this.EncryptType = encryptType;
}
}
}

View File

@ -0,0 +1,21 @@
namespace Falcon.SugarApi.DatabaseManager
{
/// <summary>
/// 字段加密解密方法
/// </summary>
public interface IEncrypt
{
/// <summary>
/// 加密。当存储字段值进入数据库时使用该方法进行加密
/// </summary>
/// <param name="str">字段明文值</param>
/// <returns>字段密文</returns>
string Encrypt(string str);
/// <summary>
/// 解密。当读取字段内容时对数据库中的加密值进行解密
/// </summary>
/// <param name="str">字段密文</param>
/// <returns>字段明文值</returns>
string Decrypt(string str);
}
}

View File

@ -145,9 +145,32 @@ namespace Falcon.SugarApi
/// <param name="start">开始时间</param>
/// <param name="end">结束时间</param>
/// <returns>True是False否</returns>
public static bool Between(this DateTime? dt,DateTime start,DateTime? end = null) {
end ??= DateTime.Now;
return dt > start && dt <= end;
//public static bool Between(this DateTime? dt,DateTime start,DateTime? end = null) {
// end ??= DateTime.Now;
// return dt > start && dt <= end;
//}
/// <summary>
/// 时间是否处于某个时间段内
/// </summary>
/// <param name="dt">要判断的时间</param>
/// <param name="start">开始时间</param>
/// <param name="end">结束时间</param>
/// <returns>True是False否</returns>
public static bool Between(this DateTime? dt,DateTime? start,DateTime? end) {
if(dt==null) {
return false;
}
if(start == null && end == null) {
throw new Exception("给定的时间范围无效");
}
if(start == null) {
return dt < end;
}
if(end == null) {
return dt >= start;
}
return dt >= start && dt <= end;
}
}

View File

@ -8,7 +8,7 @@
<DocumentationFile>bin\$(Configuration)\$(TargetFramework)\$(AssemblyName).xml</DocumentationFile>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
<Version>2.1.0</Version>
<Version>2.14.3</Version>
</PropertyGroup>
<ItemGroup>
@ -16,7 +16,7 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="SqlSugarCore" Version="5.1.4.59" />
<PackageReference Include="SqlSugarCore" Version="5.1.4.175" />
<PackageReference Include="SugarRedis" Version="1.3.0" />
<PackageReference Include="Swashbuckle.AspNetCore.SwaggerGen" Version="6.4.0" />
<PackageReference Include="System.Runtime.Caching" Version="6.0.0" />

View File

@ -0,0 +1,29 @@
namespace Falcon.SugarApi.FalconCors
{
/// <summary>
/// 跨域请求cors参数
/// </summary>
public class CorsOptions
{
/// <summary>
/// 策略名
/// </summary>
public string? PolicyName { get; set; }
/// <summary>
/// 允许通过的方法。
/// </summary>
public string? Methods { get; set; }
/// <summary>
/// 允许通过的请求头
/// </summary>
public string? Headers { get; set; }
/// <summary>
/// 运行通过的源
/// </summary>
public string? Origins { get; set; }
/// <summary>
/// 允许携带凭据
/// </summary>
public bool? AllowCredentials { get; set; }
}
}

View File

@ -0,0 +1,35 @@
using Microsoft.AspNetCore.Cors.Infrastructure;
namespace Falcon.SugarApi.FalconCors
{
/// <summary>
/// CorsPolicy装饰器
/// </summary>
public class CorsPolicyWallpaper:CorsPolicy
{
/// <summary>
/// 通过一个CorsOptions构建CorsPolicy
/// </summary>
/// <param name="options">跨域请求参数</param>
public CorsPolicyWallpaper(CorsOptions? options) {
if(options == null) {
return;
}
if(options.AllowCredentials != null) {
this.SupportsCredentials = options.AllowCredentials.Value;
}
if(options.Headers.IsNotNullOrEmpty()) {
this.Headers.Clear();
options.Headers.SplitToStringList().ForEach(a => this.Headers.Add(a));
}
if(options.Methods.IsNotNullOrEmpty()) {
this.Methods.Clear();
options.Methods.SplitToStringList().ForEach(a => this.Methods.Add(a));
}
if(options.Origins.IsNotNullOrEmpty()) {
this.Origins.Clear();
options.Origins.SplitToStringList().ForEach(a => this.Origins.Add(a));
}
}
}
}

View File

@ -0,0 +1,75 @@
using Falcon.SugarApi.ApiClient;
using Microsoft.AspNetCore.Cors.Infrastructure;
using Microsoft.Extensions.DependencyInjection;
using Npgsql.Replication.PgOutput.Messages;
using System;
namespace Falcon.SugarApi.FalconCors
{
/// <summary>
/// 服务集合方法扩展
/// </summary>
public static class IServiceCollectionExtend
{
/// <summary>
/// 通过cors方式允许所有跨域源通过。Configure中通过app.UseCors("CorsPolicy")运行策略或在控制器中通过EnableCors应用。
/// </summary>
/// <param name="services">服务集合</param>
/// <param name="policyName">策略名称默认CorsPolicy</param>
/// <returns>服务集合</returns>
public static IServiceCollection AllowAny(this IServiceCollection services,string policyName = "CorsPolicy") =>
services.AddCors(options => options.AddPolicy(policyName,builder => {
builder
.AllowAnyMethod()
//.WithMethods("GET","POST","OPTIONS")
.AllowAnyHeader()
.SetIsOriginAllowed(_ => true) // =AllowAnyOrigin()
.AllowCredentials();
}));
/// <summary>
/// 通过cors方式允许所有跨域源通过。Configure中通过app.UseCors("CorsPolicy")运行策略或在控制器中通过EnableCors应用。
/// </summary>
/// <param name="services">服务集合</param>
/// <param name="policyName">策略名称默认CorsPolicy</param>
/// <param name="policyBuilder">策略创建器</param>
/// <returns>服务集合</returns>
public static IServiceCollection AddCorsByAllowAny(
this IServiceCollection services,
string policyName,
Action<CorsPolicyBuilder>? policyBuilder = null) {
if(policyName.IsNullOrEmpty()) {
throw new ArgumentNullException(nameof(policyName));
}
services.AddCors(options => options.AddPolicy(policyName,builder => {
builder
.AllowAnyMethod() //.WithMethods("GET","POST","OPTIONS")
.AllowAnyHeader()
.SetIsOriginAllowed(_ => true) // =AllowAnyOrigin()
.AllowCredentials();
policyBuilder?.Invoke(builder);
}));
return services;
}
/// <summary>
/// 添加跨域配置。
/// </summary>
/// <param name="services">服务集合</param>
/// <param name="options">跨域配置选项</param>
/// <returns>服务集合</returns>
/// <exception cref="ArgumentNullException"></exception>
public static IServiceCollection AddCoreWithOptions(this IServiceCollection services,CorsOptions options) {
if(options == null) {
throw new ArgumentNullException(nameof(options));
}
if(options.PolicyName.IsNullOrEmpty()) {
throw new ArgumentNullException("options.PolicyName");
}
var policy = new CorsOptions();
services.AddCors(op => op.AddPolicy(options.PolicyName,new CorsPolicyWallpaper(options)));
return services;
}
}
}

View File

@ -0,0 +1,7 @@
## cors源认证
组件可以通过注册自己的cors源认证方法来实现访问的控制。
插件可以通过IServiceCollection.AddCoreWithOptions方法注册自己跨域请求策略。
然后在控制器中使用EnableCorsAttribute特性标记特性使用该策略。
如果不使用自己的策略apiservice平台允许所有跨域请求。

View File

@ -0,0 +1,16 @@
using System;
namespace Falcon.SugarApi
{
/// <summary>
/// 获取当前时间接口
/// </summary>
public interface IGetDataTimeNow
{
/// <summary>
/// 获取当前时间
/// </summary>
/// <returns></returns>
DateTime GetDataTimeNow();
}
}

View File

@ -5,7 +5,9 @@ using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Reflection;
using System.Text.Encodings.Web;
using System.Text.Json;
using System.Text.Unicode;
namespace Falcon.SugarApi
{
@ -142,6 +144,7 @@ namespace Falcon.SugarApi
/// <returns>转换后字符串</returns>
public static string ToJson<T>(this T obj,Action<JsonSerializerOptions>? OptionBuilder = null) {
var option = new JsonSerializerOptions();
option.Encoder = JavaScriptEncoder.Create(UnicodeRanges.All);
OptionBuilder?.Invoke(option);
var ser = new JsonSerialize.JsonSerializeFactory().CreateJsonSerialize(option);
return ser.Serialize(obj);
@ -208,6 +211,77 @@ namespace Falcon.SugarApi
targetBuilder?.Invoke(obj);
return obj as T;
}
/// <summary>
/// 判断对象是否在目标对象数组中。如果对象实现IComparable则使用IComparable比较对象否则使用Equals方法比较对象
/// </summary>
/// <typeparam name="T">对象类型</typeparam>
/// <param name="obj">当前对象</param>
/// <param name="values">目标对象数组</param>
/// <returns>在数组中返回True否则False</returns>
public static bool In<T>(this T obj,params T[] values) {
if(obj == null) {
return values.Any(a => a == null);
}
foreach(var i in values) {
if(i is IComparable<T> ict) {
if(ict.CompareTo(obj) == 0) {
return true;
}
continue;
}
if(i is IComparable ic) {
if(ic.CompareTo(obj) == 0) {
return true;
}
continue;
}
if(obj.Equals(i)) {
return true;
}
}
return false;
}
/// <summary>
/// 判断对象是否不在目标对象数组中。如果对象实现IComparable则使用IComparable比较对象否则使用Equals方法比较对象
/// </summary>
/// <typeparam name="T">对象类型</typeparam>
/// <param name="obj">当前对象</param>
/// <param name="values">目标对象数组</param>
/// <returns>不在数组中返回True否则False</returns>
public static bool NotIn<T>(this T obj,params T[] values) => !obj.In(values);
/// <summary>
/// 比较值是否在给定的上下限之间。包含下限但不包含上限。
/// </summary>
/// <typeparam name="T">值类型</typeparam>
/// <param name="obj">比较对象</param>
/// <param name="min">最小值</param>
/// <param name="max">最大值</param>
/// <returns>是否包含在最小值和最大值之间</returns>
/// <exception cref="Exception">给定的参数无法进行比较</exception>
public static bool Between<T>(this T? obj,T? min,T? max) {
if(obj == null) {
return false;
}
if(min == null && max == null) {
throw new Exception("min和max不能都为null");
}
dynamic? c = obj, i = min, x = max;
try {
if(i == null) {
return c < x;
}
if(x == null) {
return c >= i;
}
return c >= i && c <= x;
}
catch(Exception ex) {
throw new Exception("给定的参数无法进行比较",ex);
}
}
}
/// <summary>

View File

@ -0,0 +1,22 @@
using System;
namespace Falcon.SugarApi.Proxy
{
/// <summary>
/// 方法没有找到的异常
/// </summary>
public class MethodNotFoundException:Exception
{
/// <summary>
/// 构造属性没有找到的异常
/// </summary>
/// <param name="methodName">属性名称</param>
public MethodNotFoundException(string methodName) {
MethodName = methodName;
}
/// <summary>
/// 属性名称
/// </summary>
public string MethodName { get; }
}
}

View File

@ -0,0 +1,22 @@
using System;
namespace Falcon.SugarApi.Proxy
{
/// <summary>
/// 属性没有找到的异常
/// </summary>
public class PropNotFoundException:Exception
{
/// <summary>
/// 构造属性没有找到的异常
/// </summary>
/// <param name="propName">属性名称</param>
public PropNotFoundException(string propName) {
PropName = propName;
}
/// <summary>
/// 属性名称
/// </summary>
public string PropName { get; }
}
}

View File

@ -0,0 +1,53 @@
using System;
namespace Falcon.SugarApi.Proxy
{
/// <summary>
/// 对象代理接口
/// </summary>
/// <typeparam name="T">代理的对象类型</typeparam>
public abstract class Proxy<T>:ProxyBase where T:class
{
/// <summary>
/// 通过传入代理对象生成代理
/// </summary>
/// <param name="target">要代理的对象</param>
/// <exception cref="ArgumentNullException">代理对象为空</exception>
public Proxy(T target):base(target) {}
/// <summary>
/// 代理对象
/// </summary>
public new T? Target => base.Target as T;
/// <summary>
/// 获取对象属性的代理
/// </summary>
/// <param name="prop">属性名称</param>
/// <returns></returns>
/// <exception cref="PropNotFoundException">没有找到属性</exception>
public override object? GetFunc(string prop) {
var p = typeof(T).GetProperty(prop) ?? throw new PropNotFoundException(prop);
return p.GetValue(this.Target);
}
/// <summary>
/// 设置对象属性的代理
/// </summary>
/// <param name="prop">属性名称</param>
/// <param name="value">属性值</param>
/// <exception cref="PropNotFoundException">没有找到属性</exception>
public override void SetAction(string prop,object? value) {
var p = typeof(T).GetProperty(prop) ?? throw new PropNotFoundException(prop);
p.SetValue(this.Target,value);
}
/// <summary>
/// 执行代理对象方法
/// </summary>
/// <param name="name">方法名称</param>
/// <param name="args">传入的参数</param>
/// <returns>方法返回值</returns>
/// <exception cref="MethodNotFoundException">没有找到方法</exception>
public override object? Invoke(string name,params object[] args) {
var m = typeof(T).GetMethod(name) ?? throw new MethodNotFoundException(name);
return m.Invoke(this.Target,args);
}
}
}

View File

@ -0,0 +1,53 @@
namespace Falcon.SugarApi.Proxy
{
/// <summary>
/// 对象代理接口
/// </summary>
public abstract class ProxyBase
{
/// <summary>
/// 对象代理接口
/// </summary>
/// <param name="target">代理对象</param>
public ProxyBase(object target) {
Target = target;
}
/// <summary>
/// 代理对象
/// </summary>
public virtual object Target { get; }
/// <summary>
/// 获取对象属性的代理
/// </summary>
/// <param name="prop">属性名称</param>
/// <returns></returns>
/// <exception cref="PropNotFoundException">没有找到属性</exception>
public virtual object? GetFunc(string prop) {
var p = this.Target.GetType().GetProperty(prop) ?? throw new PropNotFoundException(prop);
return p.GetValue(this.Target);
}
/// <summary>
/// 设置对象属性的代理
/// </summary>
/// <param name="prop">属性名称</param>
/// <param name="value">属性值</param>
/// <exception cref="PropNotFoundException">没有找到属性</exception>
public virtual void SetAction(string prop,object? value) {
var p = this.Target.GetType().GetProperty(prop) ?? throw new PropNotFoundException(prop);
p.SetValue(this.Target,value);
}
/// <summary>
/// 执行代理对象方法
/// </summary>
/// <param name="name">方法名称</param>
/// <param name="args">传入的参数</param>
/// <returns>方法返回值</returns>
/// <exception cref="MethodNotFoundException">没有找到方法</exception>
public virtual object? Invoke(string name,params object[] args) {
var m = this.Target.GetType().GetMethod(name) ?? throw new MethodNotFoundException(name);
return m.Invoke(this.Target,args);
}
}
}

View File

@ -0,0 +1,79 @@
## 对象代理
通过代理对象,可以在获取、设置或运行对象方法时执行代理操作。
被代理的类型
~~~
public class TestObj
{
public string Name { get; set; }
public string func(string str) {
return str + " run";
}
public void Action() {
this.Name = "123";
}
}
~~~
要实现代理首先应继承Proxy泛型类泛型类型参数为要代理的对象类型。
~~~c#
public class TestObjProxy:Proxy<TestObj>
{
public TestObjProxy(TestObj target) : base(target) {
}
public override object? GetFunc(string prop) {
if(prop == "Name") {
Console.WriteLine($"GetFunc{this.Target.Name}");
return base.GetFunc(prop);
}
return "propName error";
}
public override void SetAction(string prop,object? value) {
if(prop == "Name") {
Console.WriteLine($"SetAction{this.Target.Name}");
base.SetAction(prop,value);
}
}
public override object? Invoke(string name,params object[] args) {
if(name == "func") {
Console.WriteLine($"run invoke:{name}");
return base.Invoke(name,args);
}
if(name== "Action") {
Console.WriteLine($"run invoke:{name}");
base.Invoke(name,args);
return null;
}
if(name== "ToString") {
Console.WriteLine($"run invoke:{name}");
base.Invoke(name,args);
}
return base.Invoke(name,args);
}
}
~~~
实例化代理类型,同时传入要代理的对象。
~~~c#
var obj = new TestObj();
var proxy = new TestObjProxy(obj);
~~~
然后通过代理获取或设置属性或者运行方法。
~~~c#
proxy.SetAction("Name","Tom");
var r = proxy.GetFunc("Name");
Console.WriteLine(r);
r = proxy.Invoke("func","paraaaaa");
Console.WriteLine(r);
proxy.Invoke("Action");
Console.WriteLine(proxy.GetFunc("Name"));
Console.WriteLine(proxy.Invoke("ToString"));
~~~

View File

@ -40,6 +40,24 @@ namespace Falcon.SugarApi
public static string[] SplitStr(this string? str,params char[] splitChars)
=> str.IsNullOrEmpty() ? Array.Empty<string>() : str.Split(splitChars.Length == 0 ? new char[] { ',','',';','','.','。' } : splitChars,StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
/// <summary>
/// 使用提供的分隔符分割字符串,返回分割后的字符串列表
/// </summary>
/// <param name="str">要分割的字符串</param>
/// <param name="splitChars">分割字符</param>
/// <returns>分割后的字符串列表</returns>
public static List<string> SplitToStringList(this string? str,params char[] splitChars) {
var list = new List<string>();
if(str.IsNullOrEmpty()) {
return list;
}
if(splitChars.Length == 0) {
splitChars = new char[] { ',','',';','','.','。' };
}
list.AddRange(str.Split(splitChars));
return list;
}
/// <summary>
/// 将字符串按格式转换为时间格式
/// </summary>
@ -148,5 +166,60 @@ namespace Falcon.SugarApi
i += length;
}
}
/// <summary>
/// 从字符串开头去除startStr字符串并从末尾去除endStr字符串如果有多个会全部去除。
/// </summary>
/// <param name="str">原始字符串</param>
/// <param name="startStr">开始处要去除的字符串,不需要则传入空字符串</param>
/// <param name="endStr">末尾处要去除的字符串,不需要则传入空字符串</param>
/// <returns>结果字符串</returns>
public static string TrimString(this string str,string startStr = "",string endStr = "") {
if(str == "") {
return str;
}
if(startStr != "") {
while(true) {
var p = str.IndexOf(startStr);
if(p < 0) {
break;
}
str = str.Substring(p + startStr.Length);
}
}
if(endStr != "") {
while(true) {
var p = str.LastIndexOf(endStr);
if(p < 0) {
break;
}
str = str.Substring(0,p);
}
}
return str;
}
/// <summary>
/// 判断字符串是否属于某个集合范围
/// </summary>
/// <param name="str">当前字符串</param>
/// <param name="strings">字符串范围</param>
/// <returns>属于True否则False</returns>
public static bool In(this string str,params string[] strings) {
foreach(var s in strings) {
if(str == s) {
return true;
}
}
return false;
}
/// <summary>
/// 和In方法相反判断字符串是否不属于某个集合范围
/// </summary>
/// <param name="str">当前字符串</param>
/// <param name="strings">字符串范围</param>
/// <returns>属于True否则False</returns>
public static bool NotIn(this string str,params string[] strings) => !str.In(strings);
}
}

View File

@ -0,0 +1,17 @@
using System;
namespace Falcon.SugarApi.TimedTask
{
/// <summary>
/// Cron表达式无法匹配到任何值
/// </summary>
public class CronAdjustNullException:Exception
{
/// <summary>
/// 通过提供的字段实例化表达式无法匹配到任何值的异常
/// </summary>
/// <param name="message">错误信息</param>
public CronAdjustNullException(string? message) : base($"Cron表达式无法匹配到任何值:{message}") {
}
}
}

View File

@ -0,0 +1,17 @@
using System;
namespace Falcon.SugarApi.TimedTask
{
/// <summary>
/// Cron表达式错误
/// </summary>
public class CronException:Exception
{
/// <summary>
/// Cron表达式错误
/// </summary>
/// <param name="message">错误信息</param>
public CronException(string? message) : base($"提供的Cron表达式错误:{message}") {
}
}
}

View File

@ -0,0 +1,285 @@
using System;
using System.Collections.Generic;
using System.Linq;
namespace Falcon.SugarApi.TimedTask
{
/// <summary>
/// Cron表达式
/// </summary>
public class CronExpression
{
/// <summary>
/// 可选的秒范围枚举
/// </summary>
public List<int> Seconds { get; }
/// <summary>
/// 可选的分范围枚举
/// </summary>
public List<int> Minutes { get; }
/// <summary>
/// 可选的时范围枚举
/// </summary>
public List<int> Hours { get; }
/// <summary>
/// 可选的天范围枚举
/// </summary>
public List<int> DaysOfMonth { get; }
/// <summary>
/// 可选的月范围枚举
/// </summary>
public List<int> Months { get; }
/// <summary>
/// 可选的星期范围枚举
/// </summary>
public List<int> DaysOfWeek { get; }
/// <summary>
/// 可选的年范围枚举
/// </summary>
public List<int> Years { get; set; }
/// <summary>
/// 解析配置
/// </summary>
public CronOptions Options { get; set; } = new CronOptions();
/// <summary>
/// 通过提供cron表达式构造对象
/// <para>表达式以空格分隔,按顺序每一段分别为“秒,分,时,日,月,星期,年”</para>
/// <para>取值范围秒分时取值范围0-59日取值范围1-31月取值范围1-12星期取值范围0-6年取值范围两年内</para>
/// <para>取值*表示可以匹配任意值,比如*表示每一秒,* *表示所有分秒。</para>
/// <para>取值-表示取值范围比如1-5 3匹配3分1秒到3分5秒所有时间。</para>
/// <para>取值/表示周期取值比如0/10表示0秒开始每10秒匹配一次。</para>
/// <para>取值可以用分割表示取值枚举比如0 10,20,30 5 表示匹配每天早上5点10分、5点20分和5点30分。</para>
/// <para>表达式右侧的*可以省略。比如0 15 4表示每天上午4点15分和0 15 4 * * * *相同</para>
/// <para>星期取值范围0-60表示星期天1表示星期一以此类推。</para>
/// <para>日取值范围1-31不用考虑大月小月和二月的特殊情况方法会自动处理这些特殊日期</para>
/// <para>关于日期和星期匹配问题。一般的cron标准中日期和星期只要满足一个即可匹配但本方法不同我认为既然其他所有部分(比如分和秒)都是必须同时满足,那么日期和星期也是必须同时满足。</para>
/// <para>目前方法不支持*-/,分隔符的混合使用,等待后续改进</para>
/// </summary>
/// <param name="cronExpression">cron表达式</param>
/// <exception cref="ArgumentException"></exception>
public CronExpression(string cronExpression) {
var fields = cronExpression.Split(new[] { ' ' },StringSplitOptions.RemoveEmptyEntries);
Seconds = fields.Length > 0 ? GetRange(fields[0],0,59) : Enumerable.Range(0,60).ToList();
if(Seconds.Count == 0) {
throw new CronAdjustNullException("Seconds");
}
Minutes = fields.Length > 1 ? GetRange(fields[1],0,59) : Enumerable.Range(0,60).ToList();
if(Minutes.Count == 0) {
throw new CronAdjustNullException("Minutes");
}
Hours = fields.Length > 2 ? GetRange(fields[2],0,23) : Enumerable.Range(0,24).ToList();
if(Hours.Count == 0) {
throw new CronAdjustNullException("Hours");
}
DaysOfMonth = fields.Length > 3 ? GetRange(fields[3],1,31) : Enumerable.Range(1,31).ToList();
if(DaysOfMonth.Count == 0) {
throw new CronAdjustNullException("DaysOfMonth");
}
Months = fields.Length > 4 ? GetRange(fields[4],1,12) : Enumerable.Range(1,12).ToList();
if(Months.Count == 0) {
throw new CronAdjustNullException("Months");
}
DaysOfWeek = fields.Length > 5 ? GetRange(fields[5],0,6) : Enumerable.Range(0,7).ToList();
if(DaysOfWeek.Count == 0) {
throw new CronAdjustNullException("DaysOfWeek");
}
var nowYear = DateTime.Now.Year;
var endYear = Options.MaxYear;
Years = fields.Length > 6 ? GetRange(fields[6],nowYear,endYear) : Enumerable.Range(nowYear,2).ToList();
if(Years.Count == 0) {
throw new CronAdjustNullException("Years");
}
}
/// <summary>
/// 通过提供的时间获取下一次时间
/// </summary>
/// <param name="afterTime">上一次的时间</param>
/// <returns>下一次到达时间</returns>
public DateTime GetNextOccurrence(DateTime afterTime) {
var ct = new CronResult(afterTime.AddSeconds(1));
while(!ct.IsAllAdjust) {
if(!ct.IsYearAdjust) {
AdjustYear(ct);
}
if(!ct.IsMonthAdjust) {
AdjustMonth(ct);
if(!ct.IsYearAdjust) {
continue;
}
}
if(!ct.IsDayAdjust) {
AdjustDay(ct);
if(!ct.IsYearAdjust || !ct.IsMonthAdjust || !ct.IsDayAdjust) {
continue;
}
}
if(!ct.IsDayOfWeekAdjust) {
AdjustDayOfWeek(ct);
if(!ct.IsYearAdjust || !ct.IsMonthAdjust || !ct.IsDayAdjust || !ct.IsDayOfWeekAdjust) {
continue;
}
}
if(!ct.IsHourAdjust) {
AdjustHour(ct);
if(!ct.IsYearAdjust || !ct.IsMonthAdjust || !ct.IsDayAdjust || !ct.IsDayOfWeekAdjust) {
continue;
}
}
if(!ct.IsMinuteAdjust) {
AdjustMinute(ct);
if(!ct.IsYearAdjust || !ct.IsMonthAdjust || !ct.IsDayAdjust || !ct.IsDayOfWeekAdjust || !ct.IsHourAdjust) {
continue;
}
}
if(!ct.IsSecondAdjust) {
AdjustSecond(ct);
}
}
return ct.AdjustTime;
}
private void AdjustYear(CronResult date) {
var next = Years.Where(a => a >= date.AdjustTime.Year);
if(next.Any()) {
date.SetAdjustTime(date.AdjustTime.AddYears(next.First() - date.AdjustTime.Year),TimePartEnum.Year);
date.IsYearAdjust = true;
}
else {
date.IsNullVal = true;
date.IsYearAdjust = true;
date.IsMonthAdjust = true;
date.IsDayAdjust = true;
date.IsHourAdjust = true;
date.IsMinuteAdjust = true;
date.IsSecondAdjust = true;
date.AdjustTime = DateTime.MaxValue;
}
}
private void AdjustMonth(CronResult date) {
var next = Months.Where(m => m >= date.AdjustTime.Month);
if(next.Any()) {
date.SetAdjustTime(date.AdjustTime.AddMonths(next.First() - date.AdjustTime.Month),TimePartEnum.YearMonth);
date.IsMonthAdjust = true;
}
else {
date.SetAdjustTime(date.AdjustTime.AddYears(1),TimePartEnum.YearMonth);
}
}
private void AdjustDay(CronResult date) {
var dt = date.AdjustTime;
int year = dt.Year;
int month = dt.Month;
int day = dt.Day;
int maxDayInMonth = DateTime.DaysInMonth(year,month);
var next = DaysOfMonth.Where(a => a >= day && a <= maxDayInMonth);
if(next.Any()) {
date.SetAdjustTime(dt.AddDays(next.First() - day),TimePartEnum.YearDay);
date.IsDayAdjust = true;
return;
}
date.SetAdjustTime(dt.AddMonths(1),TimePartEnum.YearMonth);
}
private void AdjustDayOfWeek(CronResult date) {
var dw = (int)date.AdjustTime.DayOfWeek;
var next = DaysOfWeek.Where(a => a >= dw);
if(next.Any()) {
date.SetAdjustTime(date.AdjustTime.AddDays(next.First() - dw),TimePartEnum.YearDay);
date.IsDayOfWeekAdjust = true;
}
else {
date.SetAdjustTime(date.AdjustTime.AddDays(7 - dw + DaysOfWeek.First()),TimePartEnum.YearDay);
date.IsDayOfWeekAdjust = true;
}
}
private void AdjustHour(CronResult date) {
var next = Hours.Where(m => m >= date.AdjustTime.Hour);
if(next.Any()) {
date.SetAdjustTime(date.AdjustTime.AddHours(next.First() - date.AdjustTime.Hour),TimePartEnum.YearHour);
date.IsHourAdjust = true;
return;
}
date.SetAdjustTime(date.AdjustTime.AddDays(1),TimePartEnum.YearHour);
}
private void AdjustMinute(CronResult date) {
var next = Minutes.Where(m => m >= date.AdjustTime.Minute);
if(next.Any()) {
date.SetAdjustTime(date.AdjustTime.AddMinutes(next.First() - date.AdjustTime.Minute),TimePartEnum.YearMinute);
date.IsMinuteAdjust = true;
return;
}
date.SetAdjustTime(date.AdjustTime.AddHours(1),TimePartEnum.YearMinute);
}
private void AdjustSecond(CronResult date) {
var next = Seconds.Where(m => m >= date.AdjustTime.Second);
if(next.Any()) {
date.SetAdjustTime(date.AdjustTime.AddSeconds(next.First() - date.AdjustTime.Second),TimePartEnum.YearSecond);
date.IsSecondAdjust = true;
return;
}
date.SetAdjustTime(date.AdjustTime.AddMinutes(1),TimePartEnum.YearSecond);
}
private static List<int> GetRange(string exp,int min,int max) {
var charList = "0123456789*-/,".ToArray();
var expChars = exp.ToCharArray().Distinct();
var outRangeChars = expChars.Where(a => a.NotIn(charList)).ToArray();
if(outRangeChars.Any()) {
throw new CronException($"非法字符{new string(outRangeChars)} 在表达式 {exp} 中");
}
var list = new List<int>();
if(exp == "*") {
return Enumerable.Range(min,max - min + 1).ToList();
}
if(int.TryParse(exp,out int iexp) && iexp.Between(min,max)) {
list.Add(iexp);
return list;
}
if(exp.Contains('-')) {
var g = exp.Split('-',StringSplitOptions.RemoveEmptyEntries);
var i = int.Parse(g[0]);
var x = int.Parse(g[1]);
i = Math.Max(i,min);
x = Math.Min(x,max);
while(i <= x) {
list.Add(i++);
}
return list;
}
if(exp.Contains('/')) {
var g = exp.Split('/',StringSplitOptions.RemoveEmptyEntries);
var f = Math.Max(min,int.Parse(g[0]));
var s = int.Parse(g[1]);
while(f <= max) {
list.Add(f);
f += s;
}
return list;
}
if(exp.Contains(',')) {
foreach(var ie in exp.Split(',')) {
if(int.TryParse(ie,out int iei) && iei.Between(min,max)) {
list.Add(iei);
}
else {
throw new CronException("给定的exp表达式错误逗号分割的每一项都必须是数字");
}
}
return list;
}
return list;
}
}
}

View File

@ -0,0 +1,13 @@
namespace Falcon.SugarApi.TimedTask
{
/// <summary>
/// Cron表达式解析选项
/// </summary>
public class CronOptions
{
/// <summary>
/// 最大支持的年份
/// </summary>
public int MaxYear { get; set; } = 2099;
}
}

View File

@ -0,0 +1,97 @@
using System;
namespace Falcon.SugarApi.TimedTask
{
public class CronResult
{
public DateTime AdjustTime { get; set; }
public bool IsYearAdjust { get; set; } = false;
public bool IsMonthAdjust { get; set; } = false;
public bool IsDayAdjust { get; set; } = false;
public bool IsDayOfWeekAdjust { get; set; } = false;
public bool IsHourAdjust { get; set; } = false;
public bool IsMinuteAdjust { get; set; } = false;
public bool IsSecondAdjust { get; set; } = false;
public bool IsAllAdjust
=> (IsYearAdjust && IsMonthAdjust && IsDayAdjust && IsDayOfWeekAdjust
&& IsHourAdjust && IsMinuteAdjust && IsSecondAdjust) || IsNullVal;
/// <summary>
/// 没有匹配到值
/// </summary>
public bool IsNullVal { get; set; } = false;
public CronResult(DateTime last) => this.AdjustTime = last;
public void SetAdjustTime(DateTime dt,TimePartEnum part) {
if(!part.HasFlag(TimePartEnum.Year)) {
return;
}
if(dt.Year != this.AdjustTime.Year) {
this.AdjustTime = new DateTime(dt.Year,1,1,0,0,0);
IsYearAdjust = false;
IsMonthAdjust = false;
IsDayAdjust = false;
IsDayOfWeekAdjust = false;
IsHourAdjust = false;
IsMinuteAdjust = false;
IsSecondAdjust = false;
return;
}
if(!part.HasFlag(TimePartEnum.Month)) {
return;
}
if(dt.Month != this.AdjustTime.Month) {
this.AdjustTime = new DateTime(dt.Year,dt.Month,1,0,0,0);
IsMonthAdjust = false;
IsDayAdjust = false;
IsDayOfWeekAdjust = false;
IsHourAdjust = false;
IsMinuteAdjust = false;
IsSecondAdjust = false;
return;
}
if(!part.HasFlag(TimePartEnum.Day)) {
return;
}
if(dt.Day != this.AdjustTime.Day) {
this.AdjustTime = new DateTime(dt.Year,dt.Month,dt.Day,0,0,0);
IsDayAdjust = false;
IsDayOfWeekAdjust = false;
IsHourAdjust = false;
IsMinuteAdjust = false;
IsSecondAdjust = false;
return;
}
if(!part.HasFlag(TimePartEnum.Hour)) {
return;
}
if(dt.Hour != this.AdjustTime.Hour) {
this.AdjustTime = new DateTime(dt.Year,dt.Month,dt.Day,dt.Hour,0,0);
IsHourAdjust = false;
IsMinuteAdjust = false;
IsSecondAdjust = false;
return;
}
if(!part.HasFlag(TimePartEnum.Minute)) {
return;
}
if(dt.Minute != this.AdjustTime.Minute) {
this.AdjustTime = new DateTime(dt.Year,dt.Month,dt.Day,dt.Hour,dt.Minute,0);
IsMinuteAdjust = false;
IsSecondAdjust = false;
return;
}
if(!part.HasFlag(TimePartEnum.Second)) {
return;
}
if(dt.Second != this.AdjustTime.Second) {
this.AdjustTime = dt;
IsSecondAdjust = false;
return;
}
}
}
}

View File

@ -0,0 +1,22 @@
using Microsoft.Extensions.DependencyInjection;
namespace Falcon.SugarApi.TimedTask
{
/// <summary>
/// 服务集合扩展方法
/// </summary>
public static class IServiceCollectionExtend
{
/// <summary>
/// 向HostService中注册TimedBackgroundTask服务
/// </summary>
/// <typeparam name="T">TimedBackgroundTask的具体类型</typeparam>
/// <param name="services">服务集合</param>
/// <returns>服务集合</returns>
public static IServiceCollection AddTimedTask<T>(this IServiceCollection services)
where T : TimedTask {
services.AddHostedService<T>();
return services;
}
}
}

View File

@ -0,0 +1,53 @@
## 后台任务模块 TimedTask
### 设计目标
1. 后台独立运行的任务模块。插件通过继承TimedTask类实现业务类并通过IServiceCollection.AddTimedTask进行注册。
1. 业务类需要重写Run方法实现业务逻辑。
1. 业务类通过重写CronSchedule属性实现任务调度该属性提供cron表达式。
1. 业务类注册后首先调用一次业务实现然后按照cron表达式定义的时间调度执行业务逻辑。
1. 本次业务逻辑结束后才会调度下一次,不会存在重复执行的情况。
### 业务实现
~~~c#
//通过继承TaskBase实现后台任务
public class RunTestUseLogTask:TaskBase
{
public RunTestUseLogTask(IServiceProvider service) : base(service) {
}
//cron表达式。在每分钟的10秒时运行
public override string CronSchedule => "10";
//测试用业务逻辑 任务开始2秒后和5秒后记录日志。
public override async Task<bool> Run(CancellationToken cancellationToken) {
await Task.Run(() => {
Thread.Sleep(2 * 1000);
});
this.Logger.LogInformation($"RunTestUseLogTask run 1 at {DateTime.Now:yyyy-MM-dd HH:mm:ss}");
await Task.Run(() => {
Thread.Sleep(3 * 1000);
});
this.Logger.LogInformation($"RunTestUseLogTask run 2 at {DateTime.Now:yyyy-MM-dd HH:mm:ss}");
return await Task.FromResult(true);
}
}
~~~
### 后台任务注册
~~~c#
public class ServicePlugin:IServicePlugin
{
/// <inheritdoc/>
public IServiceCollection AddServices(IServiceCollection services,IConfiguration configuration) {
//其他模块注册
//注册任务
services.AddTimedTask<RunTestUseLogTask>();
//其他模块注册
//返回服务集合
return services;
}
}
~~~

View File

@ -0,0 +1,56 @@
using System;
namespace Falcon.SugarApi.TimedTask
{
/// <summary>
/// 时间结构枚举
/// </summary>
[Flags]
public enum TimePartEnum
{
/// <summary>
/// 年
/// </summary>
Year = 1,
/// <summary>
/// 月
/// </summary>
Month = 2,
/// <summary>
/// 日
/// </summary>
Day = 4,
/// <summary>
/// 小时
/// </summary>
Hour = 8,
/// <summary>
/// 分
/// </summary>
Minute = 16,
/// <summary>
/// 秒
/// </summary>
Second = 32,
/// <summary>
/// 年和月
/// </summary>
YearMonth = Year + Month,
/// <summary>
/// 年月日
/// </summary>
YearDay = YearMonth + Day,
/// <summary>
/// 年月日时
/// </summary>
YearHour = YearDay + Hour,
/// <summary>
/// 年月日时分
/// </summary>
YearMinute = YearHour + Minute,
/// <summary>
/// 年月入时分秒
/// </summary>
YearSecond = YearMinute + Second,
}
}

View File

@ -0,0 +1,124 @@
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using System;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
namespace Falcon.SugarApi.TimedTask
{
/// <summary>
/// 由定时器驱动的后台任务
/// </summary>
public abstract class TimedTask:BackgroundService
{
/// <summary>
/// 日志记录器
/// </summary>
protected readonly ILogger? Logger;
/// <summary>
/// 运行一次任务
/// </summary>
/// <param name="cancellationToken">退出信号</param>
/// <returns>如果继续执行返回True否则False</returns>
public abstract Task<bool> Run(CancellationToken cancellationToken);
/// <summary>
/// 执行计划的Cron串
/// </summary>
protected abstract string CronSchedule { get; }
/// <summary>
/// 获取下次执行任务的计划
/// </summary>
protected CronExpression Schedule { get; private set; }
/// <summary>
/// 下次执行时间
/// </summary>
public DateTime NextTickTime { get; set; } = DateTime.MinValue;
/// <summary>
/// 后台任务开始。返回True任务会继续执行False则任务退出。
/// </summary>
protected virtual bool OnStart(TimedTask t,CancellationToken stoppingToken) {
return true;
}
/// <summary>
/// 后台任务停止
/// </summary>
protected virtual void OnStop(TimedTask t,CancellationToken stoppingToken) { }
/// <summary>
/// 完成一次执行。返回True任务会继续执行False则任务退出。
/// </summary>
protected virtual bool OnCompleted(TimedTask t,CancellationToken stoppingToken) {
return true;
}
/// <summary>
/// 执行中发生未处理异常。返回True任务会继续执行False则任务退出。
/// </summary>
protected virtual bool OnException(TimedTask t,Exception ex,CancellationToken stoppingToken) {
StringBuilder sb = new();
sb.AppendLine($"运行后台任务发生未处理的异常,后台任务会继续执行,异常信息可能会重复出现!");
sb.AppendLine(ex.ToString());
this.Logger?.LogError(sb.ToString());
return true;
}
/// <summary>
/// 系统服务
/// </summary>
public IServiceProvider Service { get; set; }
/// <summary>
/// 构造Timed后台任务
/// </summary>
/// <param name="service">服务提供器</param>
public TimedTask(IServiceProvider service) {
this.Service = service;
this.Logger = service.GetService(typeof(ILogger<>).MakeGenericType(GetType())) as ILogger;
this.Schedule = new CronExpression(this.CronSchedule);
}
/// <summary>
/// 由系统调度执行任务
/// </summary>
/// <param name="stoppingToken">退出信号</param>
/// <returns>无返回</returns>
protected override async Task ExecuteAsync(CancellationToken stoppingToken) {
if(!this.OnStart(this,stoppingToken)) {
return;
}
try {
while(!stoppingToken.IsCancellationRequested) {
DateTime nextRunTime = Schedule.GetNextOccurrence(DateTime.Now);
TimeSpan delay = nextRunTime - DateTime.Now;
if(delay > TimeSpan.Zero) {
await Task.Delay(delay,stoppingToken);
}
if(stoppingToken.IsCancellationRequested) {
break;
}
// 执行任务
try {
var goOn = await this.Run(stoppingToken);
var comResult = this.OnCompleted(this,stoppingToken);
if(!goOn || !comResult) {
break;
}
}
catch(Exception ex) {
if(!this.OnException(this,ex,stoppingToken)) {
break;
}
}
//Schedule = new CronExpression(this.CronSchedule);
}
}
finally {
this.OnStop(this,stoppingToken);
}
}
}
}

View File

@ -1,6 +1,9 @@
using System;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Xml.Linq;
namespace Falcon.SugarApi
{
@ -33,5 +36,64 @@ namespace Falcon.SugarApi
p = info.GetCustomAttribute<T>();
return p != null;
}
/// <summary>
/// 获取类型的summary说明
/// </summary>
/// <param name="type">类型</param>
/// <returns>summary值没有返回空字符串</returns>
/// <exception cref="ArgumentNullException"></exception>
public static string? GetTypeSummary(this Type type) {
_ = type ?? throw new ArgumentNullException(nameof(type));
var assemblyName = type.Assembly.FullName;
if(assemblyName == null) {
return "";
}
var dllLocal = type.Assembly.Location;
var xmlLocal = $"{dllLocal.Substring(0,dllLocal.Length - 3)}xml";
if(!File.Exists(xmlLocal)) {
return "";
}
var xdoc = XDocument.Load(xmlLocal);
if(xdoc == null) {
return "";
}
var members = xdoc.Descendants("member");
var find = members.Where(a => a.Attribute("name")?.Value == $"T:{type.FullName}");
if(find.Any()) {
return find.First().Element("summary")?.Value?.Trim() ?? "";
}
return "";
}
/// <summary>
/// 获取属性的summary说明
/// </summary>
/// <param name="property">属性</param>
/// <returns>summary值没有返回空字符串</returns>
/// <exception cref="ArgumentNullException"></exception>
public static string GetPropertySummary(this PropertyInfo property) {
_ = property ?? throw new ArgumentNullException(nameof(property));
var type = property.ReflectedType;
if(type == null) {
return "";
}
var dllLocal = type.Assembly.Location;
var xmlLocal = $"{dllLocal.Substring(0,dllLocal.Length - 3)}xml";
if(!File.Exists(xmlLocal)) {
return "";
}
var xdoc = XDocument.Load(xmlLocal);
if(xdoc == null) {
return "";
}
var members = xdoc.Descendants("member");
var find = members.Where(a => a.Attribute("name")?.Value == $"P:{type.FullName}.{property.Name}");
if(find.Any()) {
return find.First().Element("summary")?.Value?.Trim() ?? "";
}
return "";
}
}
}

View File

@ -0,0 +1,29 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>net6</TargetFrameworks>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<IsPackable>false</IsPackable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Build" Version="17.3.2" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.1.0" />
<PackageReference Include="MSTest.TestAdapter" Version="2.2.8" />
<PackageReference Include="MSTest.TestFramework" Version="2.2.8" />
<PackageReference Include="RabbitMQ.Client" Version="6.8.1" />
</ItemGroup>
<ItemGroup>
<Folder Include="Nuget\" />
</ItemGroup>
<ItemGroup>
<None Update="Falcon.SugarApi.2.2.0.nupkg">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
</ItemGroup>
</Project>

View File

@ -0,0 +1,3 @@
## 测试插件以nuget包的形式提供
包括插件安装,卸载,升级操作

89
NugetPluginTest/Test1.cs Normal file
View File

@ -0,0 +1,89 @@
using Microsoft.Build.Evaluation;
using Microsoft.VisualStudio.TestPlatform.PlatformAbstractions.Interfaces;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using System;
using System.Collections.Generic;
using System.IO.Compression;
using System.Linq;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading.Tasks;
namespace NugetPluginTest
{
[TestClass]
public class Test1
{
public string BasePath { get => AppDomain.CurrentDomain.BaseDirectory; }
public string NugetName { get => Path.Combine(BasePath,"Falcon.SugarApi.2.2.0.nupkg"); }
[TestMethod("nuget包解压测试")]
public void ExtractNugetTest() {
var extractPath = Path.Combine(BasePath,"ent");
if(Directory.Exists(extractPath)) {
Directory.Delete(extractPath,true);
}
Directory.CreateDirectory(extractPath);
using var packageStream = new FileStream(NugetName,FileMode.Open,FileAccess.Read);
using var archive = new ZipArchive(packageStream,ZipArchiveMode.Read);
archive.ExtractToDirectory(extractPath);
Assert.IsTrue(Directory.GetFiles(extractPath).Length > 0);
}
[TestMethod("获取系统运行框架版本")]
public void GetRuntimeInformation() {
var info = RuntimeInformation.FrameworkDescription;
Console.WriteLine(info);
}
/// <summary>
/// 获取项目的Farmwork版本environment.version
/// </summary>
[TestMethod("获取项目运行框架版本")]
public void GetTargetFramework() {
try {
File.Copy("NugetPluginTest.dll","NugetPluginTest.dll1");
var project = new Project("NugetPluginTest.dll1");
string targetFramework = project.GetPropertyValue("TargetFramework");
}
catch(Exception ex) {
Console.WriteLine(ex.ToString());
}
}
/// <summary>
/// 获取项目的Farmwork版本environment.version
/// </summary>
[TestMethod("获取项目运行框架版本 Environment")]
public void GetTargetFramework2() {
Console.WriteLine(Environment.Version.ToString());
}
/// <summary>
/// 获取项目的Farmwork版本
/// </summary>
[TestMethod("获取项目运行框架版本,条件编译")]
public void GetTargetFramework1() {
#if NET6
Console.WriteLine("NET6");
#endif
#if NET6_0
Console.WriteLine("NET6_0");
#endif
#if NET5_0_OR_GREATER
Console.WriteLine("NET5_0_OR_GREATER");
#endif
#if NET6_0_OR_GREATER
Console.WriteLine("NET6_0_OR_GREATER");
#endif
#if NETCOREAPP
Console.WriteLine("NETCOREAPP");
#endif
}
}
}

View File

@ -10,7 +10,7 @@
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.1.0" />
<PackageReference Include="MSTest.TestAdapter" Version="2.2.8" />
<PackageReference Include="MSTest.TestFramework" Version="2.2.8" />
<PackageReference Include="RabbitMQ.Client" Version="6.4.0" />
<PackageReference Include="RabbitMQ.Client" Version="6.8.1" />
</ItemGroup>
</Project>