深度优先

这个家伙好懒,除了文章什么都没留下

0%

https://www.cnblogs.com/lwqlun/p/13551149.html

前言#

笔者最近在开发和维护一个.NET Core项目,其中使用几个非常有意思的.NET Core相关的扩展,在此总结整理一下。

EF Core性能调优#

如果你的项目中使用了EF Core, 且正在处于性能调优阶段,那么了解EF Core生成的SQL语句是非常关键的。那么除了使用第三方工具,如何查看EF Core生成的SQL语句呢?这里笔者将给出一个基于.NET Core内置日志组件的实现方式。

创建一个实例项目#

我们首先建一个控制台程序,在主程序中我们编写了一个最简单的EF查询。

1
2
3
4
5
6
7
8
9
10
11
12
class Program {
static void Main (string[] args) {

var dbOptionBuilder = new DbContextOptionsBuilder<MyDbContext>();
dbOptionBuilder
.UseMySql("server=localhost;port=3306;database=EFCoreSampleDB;userid=root;pwd=a@12345");

using (var dbContext = new MyDbContext(dbOptionBuilder.Options)) {
var query = dbContext.Users.ToList();
}
}
}

这里为了演示,我们提前创建了一个MySql数据库,并在项目中创建了一个对应的EF Core上下文。当前上下文中只有一个User实体,该实体只有2个属性UserIdUserName

1
2
3
4
5
6
7
8
public class MyDbContext : DbContext {

public MyDbContext (DbContextOptions<MyDbContext> options) : base (options) {

}

public DbSet<User> Users { get; set; }
}
1
2
3
4
5
6
public class User
{
[Key]
public Guid UserId { get; set;}
public string UserName { get; set;}
}

如何生成的SQL语句输出到控制台?#

.NET Core中提供了非常完善的日志接口。这里为了和.NET Core的日志接口集成,我们需要实现2个接口,一个是日志提供器接口ILoggerProvider, 一个是日志接口ILogger

EFLoggerProvider.cs

1
2
3
4
public class EFLoggerProvider : ILoggerProvider {
public ILogger CreateLogger (string categoryName) => new EFLogger (categoryName);
public void Dispose () { }
}

EFLoggerProvider的代码非常的简单,就是直接返回一个我们后续创建的EFLogger对象。

EFLogger.cs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class EFLogger : ILogger {
private readonly string categoryName;

public EFLogger (string categoryName) => this.categoryName = categoryName;

public bool IsEnabled (LogLevel logLevel) => true;

public void Log<TState> (LogLevel logLevel,
EventId eventId,
TState state,
Exception exception,
Func<TState, Exception, string> formatter) {
var logContent = formatter (state, exception);
Console.WriteLine ();
Console.WriteLine (logContent);
}
}

public IDisposable BeginScope<TState> (TState state) => null;
}

这里我们主要使用了内置的formatter格式化了日志信息。

最后我们还需要将自定义的日志处理类和EF Core集成起来。这里我们需要复写上下文类的OnConfiguring方法。在其中通过UseLoggerFactory方法,将我们自定义的日志处理类和EF Core的日志系统关联起来。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class MyDbContext : DbContext {

public MyDbContext (DbContextOptions<MyDbContext> options) : base (options) {

}

protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) {

var loggerFactory = new LoggerFactory ();
loggerFactory.AddProvider(new EFLoggerProvider());
optionsBuilder.UseLoggerFactory(loggerFactory);

base.OnConfiguring(optionsBuilder);
}

public DbSet<User> Users { get; set; }
}

下面我们启动项目,看一下效果。这里日志信息正确的显示出来了。

PS: 如果项目中使用了通用主机或者ASP.NET Core, 你也可以在服务配置部分,通过DbContextOptions参数配置。

1
2
3
4
services.AddDbContext<MyDbContext>(options =>
options.UseSqlServer(Configuration.GetConnectionString("MyDb"))
.UseLoggerFactory(new LoggerFactory()));

如何去除无关日志?#

在前面的步骤中,我们成功的输出了查询语句,但是有一个问题是我们只想查看输出的SQL语句,其他的信息我们都不想要,那么能不能去除掉这些无关日志呢?答案是肯定的。

我们可以在Log方法中,通过分类名称,只输出Microsoft.EntityFrameworkCore.Database.Command分类下的日志,该日志即生成的SQL语句部分。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public void Log<TState> (LogLevel logLevel,
EventId eventId,
TState state,
Exception exception,
Func<TState, Exception, string> formatter)
{

if (categoryName == "Microsoft.EntityFrameworkCore.Database.Command" &&
logLevel == LogLevel.Information) {
var logContent = formatter (state, exception);

Console.WriteLine ();
Console.ForegroundColor = ConsoleColor.Green;
Console.WriteLine (logContent);
Console.ResetColor ();
}
}

这里我们也做了一些其他的操作,通过修改控制台输出文本的颜色,高亮了生成的SQL语句。重新启动项目之后,效果如下。

如何显示敏感数据?#

这里看似我们已经完成了EF Core的语句输出,但是在实际使用中,你还会遇到另外一个问题。

下面我们修改一下我们的主程序,我们尝试插入一条User信息。

1
2
3
4
5
6
7
8
9
10
11
12
class Program {
static void Main (string[] args) {

var dbOptionBuilder = new DbContextOptionsBuilder<MyDbContext> ();
dbOptionBuilder.UseMySql ("server=localhost;port=3306;database=EFCoreSampleDB;userid=root;pwd=a@12345");

using (var dbContext = new MyDbContext (dbOptionBuilder.Options)) {
dbContext.Users.Add(new User { UserId = Guid.NewGuid(), UserName = "Lamond Lu"});
dbContext.SaveChanges();
}
}
}

重新运行程序,你会得到一下结果。

这里你可能会问为什么不显示@p0, @p1参数的值。这里是原因是为了保护敏感数据,EF Core默认关闭的敏感数据的显示配置,如果你想要查看敏感数据,你需要通过DbContextOptionsBuilder对象的EnableSensitiveDataLogging方法修改敏感数据日志配置。

1
2
3
4
5
6
7
8
protected override void OnConfiguring (DbContextOptionsBuilder optionsBuilder) {
var loggerFactory = new LoggerFactory ();
loggerFactory.AddProvider (new EFLoggerProvider ());
optionsBuilder.EnableSensitiveDataLogging (true);
optionsBuilder.UseLoggerFactory (loggerFactory);

base.OnConfiguring (optionsBuilder);
}

重新启动项目之后,你就能看到@p0, @p1参数的值了。

https://github.com/XiLife-OSPC/Masuit.Tools

安装程序包

.NET Framework ≥4.6.1

1
PM> Install-Package Masuit.Tools.Net

.NET Core 2.x/3.x

1
PM> Install-Package Masuit.Tools.Core

为工具库注册配置

工具库需要用到外部配置节:

  1. EmailDomainWhiteList,邮箱校验需要用到的白名单域名,若未配置,则不启用邮箱校验白名单
  2. BaiduAK,获取IP/地理位置相关百度云APIKey,若未配置,则无法调用GetIPLocation以及GetPhysicalAddress相关方法
1
2
3
4
public Startup(IConfiguration configuration)
{
configuration.AddToMasuitTools(); // 若未调用,则默认自动尝试加载appsettings.json
}

特色功能示例代码

1.检验字符串是否是Email、手机号、URL、IP地址、身份证号

1
2
3
4
5
bool isEmail="3444764617@qq.com".MatchEmail(); // 可在appsetting.json中添加EmailDomainWhiteList配置邮箱域名白名单,逗号分隔
bool isInetAddress = "114.114.114.114".MatchInetAddress();
bool isUrl = "http://masuit.com".MatchUrl();
bool isPhoneNumber = "15205201520".MatchPhoneNumber();
bool isIdentifyCard = "312000199502230660".MatchIdentifyCard();// 校验中国大陆身份证号

2.硬件监测(仅支持Windows)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
float load = SystemInfo.CpuLoad;// 获取CPU占用率
long physicalMemory = SystemInfo.PhysicalMemory;// 获取物理内存总数
long memoryAvailable = SystemInfo.MemoryAvailable;// 获取物理内存可用率
double freePhysicalMemory = SystemInfo.GetFreePhysicalMemory();// 获取可用物理内存
Dictionary<string, string> diskFree = SystemInfo.DiskFree();// 获取磁盘每个分区可用空间
Dictionary<string, string> diskTotalSpace = SystemInfo.DiskTotalSpace();// 获取磁盘每个分区总大小
Dictionary<string, double> diskUsage = SystemInfo.DiskUsage();// 获取磁盘每个分区使用率
double temperature = SystemInfo.GetCPUTemperature();// 获取CPU温度
int cpuCount = SystemInfo.GetCpuCount();// 获取CPU核心数
IList<string> ipAddress = SystemInfo.GetIPAddress();// 获取本机所有IP地址
string localUsedIp = SystemInfo.GetLocalUsedIP();// 获取本机当前正在使用的IP地址
IList<string> macAddress = SystemInfo.GetMacAddress();// 获取本机所有网卡mac地址
string osVersion = SystemInfo.GetOsVersion();// 获取操作系统版本
RamInfo ramInfo = SystemInfo.GetRamInfo();// 获取内存信息

3.大文件操作

1
2
3
4
5
6
FileStream fs = new FileStream(@"D:\boot.vmdk", FileMode.OpenOrCreate, FileAccess.ReadWrite);
{
//fs.CopyToFile(@"D:\1.bak");//同步复制大文件
fs.CopyToFileAsync(@"D:\1.bak");//异步复制大文件
string md5 = fs.GetFileMD5Async().Result;//异步获取文件的MD5
}

4.html的防XSS处理:

1
2
3
4
5
6
7
8
9
10
11
12
13
string html = @"<link href='/Content/font-awesome/css' rel='stylesheet'/>
<!--[if IE 7]>
<link href='/Content/font-awesome-ie7.min.css' rel='stylesheet'/>
<![endif]-->
<script src='/Scripts/modernizr'></script>
<div id='searchBox' role='search'>
<form action='/packages' method='get'>
<span class='user-actions'><a href='/users/account/LogOff'>退出</a></span>
<input name='q' id='searchBoxInput'/>
<input id='searchBoxSubmit' type='submit' value='Submit' />
</form>
</div>";
string s = html.HtmlSantinizerStandard();//清理后:<div><span><a href="/users/account/LogOff">退出</a></span></div>

5.整理操作系统的内存:

1
Windows.ClearMemorySilent();

6.任意进制转换

可用于生成短id,短hash等操作,纯数学运算。

1
2
3
4
5
6
NumberFormater nf = new NumberFormater(36);//内置2-62进制的转换
//NumberFormater nf = new NumberFormater("0123456789abcdefghijklmnopqrstuvwxyz");// 自定义进制字符,可用于生成验证码
string s36 = nf.ToString(12345678);
long num = nf.FromString("7clzi");
Console.WriteLine("12345678的36进制是:" + s36); // 7clzi
Console.WriteLine("36进制的7clzi是:" + num); // 12345678
1
2
3
//扩展方法形式调用
var bin=12345678.ToBinary(36);//7clzi
var num="7clzi".FromBinary(36);//12345678
1
2
3
4
5
//超大数字的进制转换
var num = "E6186159D38CD50E0463A55E596336BD".FromBinaryBig(16);
Console.WriteLine(num); // 十进制:305849028665645097422198928560410015421
Console.WriteLine(num.ToBinary(64)); // 64进制:3C665pQUPl3whzFlVpoPqZ,22位长度
Console.WriteLine(num.ToBinary(36)); // 36进制:dmed4dkd5bhcg4qdktklun0zh,25位长度

7.纳秒级性能计时器

1
2
3
4
5
6
7
HiPerfTimer timer = HiPerfTimer.StartNew();
for (int i = 0; i < 100000; i++)
{
//todo
}
timer.Stop();
Console.WriteLine("执行for循环100000次耗时"+timer.Duration+"s");
1
2
3
4
5
6
7
8
double time = HiPerfTimer.Execute(() =>
{
for (int i = 0; i < 100000; i++)
{
//todo
}
});
Console.WriteLine("执行for循环100000次耗时"+time+"s");

8.单机产生唯一有序的短id

1
var token=Stopwatch.GetTimestamp().ToBinary(36);
1
2
3
4
5
6
7
8
9
10
var set = new HashSet<string>();
double time = HiPerfTimer.Execute(() =>
{
for (int i = 0; i < 1000000; i++)
{
set.Add(Stopwatch.GetTimestamp().ToBinary(36));
}
});
Console.WriteLine(set.Count==1000000);//True
Console.WriteLine("产生100w个id耗时"+time+"s");//1.6639039s

9.产生分布式唯一有序短id

1
2
3
var sf = SnowFlake.GetInstance();
string token = sf.GetUniqueId();// rcofqodori0w
string shortId = sf.GetUniqueShortId(8);// qodw9728
1
2
3
4
5
6
7
8
9
10
var set = new HashSet<string>();
double time = HiPerfTimer.Execute(() =>
{
for (int i = 0; i < 1000000; i++)
{
set.Add(SnowFlake.GetInstance().GetUniqueId());
}
});
Console.WriteLine(set.Count == 1000000); //True
Console.WriteLine("产生100w个id耗时" + time + "s"); //2.6891495s

10.农历转换

1
2
3
4
5
6
7
ChineseCalendar.CustomHolidays.Add(DateTime.Parse("2018-12-31"),"元旦节");//自定义节假日
ChineseCalendar today = new ChineseCalendar(DateTime.Parse("2018-12-31"));
Console.WriteLine(today.ChineseDateString);// 二零一八年十一月廿五
Console.WriteLine(today.AnimalString);// 生肖:狗
Console.WriteLine(today.GanZhiDateString);// 干支:戊戌年甲子月丁酉日
Console.WriteLine(today.DateHoliday);// 获取按公历计算的节假日
...

11.Linq表达式树扩展

1
2
3
4
Expression<Func<string, bool>> where1 = s => s.StartsWith("a");
Expression<Func<string, bool>> where2 = s => s.Length > 10;
Func<string, bool> func = where1.And(where2).Compile();
bool b=func("abcd12345678");//true
1
2
3
4
Expression<Func<string, bool>> where1 = s => s.StartsWith("a");
Expression<Func<string, bool>> where2 = s => s.Length > 10;
Func<string, bool> func = where1.Or(where2).Compile();
bool b=func("abc");// true

12.模版引擎

1
2
3
var tmp = new Template("{{name}},你好!");
tmp.Set("name", "万金油");
string s = tmp.Render();//万金油,你好!
1
2
var tmp = new Template("{{one}},{{two}},{{three}}");
string s = tmp.Set("one", "1").Set("two", "2").Set("three", "3").Render();// 1,2,3
1
2
3
var tmp = new Template("{{name}},{{greet}}!");
tmp.Set("name", "万金油");
string s = tmp.Render();// throw 模版变量{{greet}}未被使用

13.List转Datatable

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
var list = new List<MyClass>()
{
new MyClass()
{
Name = "张三",
Age = 22
},
new MyClass()
{
Name = "李四",
Age = 21
},
new MyClass()
{
Name = "王五",
Age = 28
}
};
var table = list.Select(c => new{姓名=c.Name,年龄=c.Age}).ToList().ToDataTable();// 将自动填充列姓名和年龄

14.文件压缩解压

.NET Framework

1
2
3
4
5
MemoryStream ms = SevenZipCompressor.ZipStream(new List<string>()
{
@"D:\1.txt",
"http://ww3.sinaimg.cn/large/87c01ec7gy1fsq6rywto2j20je0d3td0.jpg",
});//压缩成内存流
1
2
3
4
5
6
7
8
SevenZipCompressor.Zip(new List<string>()
{
@"D:\1.txt",
"http://ww3.sinaimg.cn/large/87c01ec7gy1fsq6rywto2j20je0d3td0.jpg",
}, zip);//压缩成zip
SevenZipCompressor.UnRar(@"D:\Download\test.rar", @"D:\Download\");//解压rar
SevenZipCompressor.Decompress(@"D:\Download\test.tar", @"D:\Download\");//自动识别解压压缩包
SevenZipCompressor.Decompress(@"D:\Download\test.7z", @"D:\Download\");

ASP.NET Core

Startup.cs

1
services.AddSevenZipCompressor();

构造函数注入ISevenZipCompressor

1
2
3
4
5
private readonly ISevenZipCompressor _sevenZipCompressor;
public Test(ISevenZipCompressor sevenZipCompressor)
{
_sevenZipCompressor = sevenZipCompressor;
}

使用方式同.NET Framework版本

15.日志组件

1
2
3
4
5
6
7
LogManager.LogDirectory=AppDomain.CurrentDomain.BaseDirectory+"/logs";
LogManager.Event+=info =>
{
//todo:注册一些事件操作
};
LogManager.Info("记录一次消息");
LogManager.Error(new Exception("异常消息"));

16.FTP客户端

1
2
3
4
5
6
7
8
9
10
FtpClient ftpClient = FtpClient.GetAnonymousClient("192.168.2.2");//创建一个匿名访问的客户端
//FtpClient ftpClient = FtpClient.GetClient("192.168.2.3","admin","123456");// 创建一个带用户名密码的客户端
ftpClient.Delete("/1.txt");// 删除文件
ftpClient.Download("/test/2.txt","D:\\test\\2.txt");// 下载文件
ftpClient.UploadFile("/test/22.txt","D:\\test\\22.txt",(sum, progress) =>
{
Console.WriteLine("已上传:"+progress*1.0/sum);
});//上传文件并检测进度
List<string> files = ftpClient.GetFiles("/");//列出ftp服务端文件列表
...

17.多线程后台下载

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
var mtd = new MultiThreadDownloader("https://attachments-cdn.shimo.im/yXwC4kphjVQu06rH/KeyShot_Pro_7.3.37.7z",Environment.GetEnvironmentVariable("temp"),"E:\\Downloads\\KeyShot_Pro_7.3.37.7z",8);
mtd.Configure(req =>
{
req.Referer = "https://masuit.com";
req.Headers.Add("Origin", "https://baidu.com");
});
mtd.TotalProgressChanged+=(sender, e) =>
{
var downloader = sender as MultiThreadDownloader;
Console.WriteLine("下载进度:"+downloader.TotalProgress+"%");
Console.WriteLine("下载速度:"+downloader.TotalSpeedInBytes/1024/1024+"MBps");
};
mtd.FileMergeProgressChanged+=(sender, e) =>
{
Console.WriteLine("下载完成");
};
mtd.Start();//开始下载
//mtd.Pause(); // 暂停下载
//mtd.Resume(); // 继续下载

18.Socket客户端操作类

1
2
3
4
5
6
var tcpClient = new TcpClient(AddressFamily.InterNetwork);
Socket socket = tcpClient.ConnectSocket(IPAddress.Any,5000);
socket.SendFile("D:\\test\\1.txt",false,i =>
{
Console.WriteLine("已发送"+i+"%");
});

19.加密解密

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
var enc="123456".MDString();// MD5加密
var enc="123456".MDString("abc");// MD5加盐加密
var enc="123456".MDString2();// MD5两次加密
var enc="123456".MDString2("abc");// MD5两次加盐加密
var enc="123456".MDString3();// MD5三次加密
var enc="123456".MDString3("abc");// MD5三次加盐加密

string aes = "123456".AESEncrypt();// AES加密为密文
string s = aes.AESDecrypt(); //AES解密为明文
string aes = "123456".AESEncrypt("abc");// AES密钥加密为密文
string s = aes.AESDecrypt("abc"); //AES密钥解密为明文

string enc = "123456".DesEncrypt();// DES加密为密文
string s = enc.DesDecrypt(); //DES解密为明文
string enc = "123456".DesEncrypt("abcdefgh");// DES密钥加密为密文
string s = enc.DesDecrypt("abcdefgh"); //DES密钥解密为明文

RsaKey rsaKey = RsaCrypt.GenerateRsaKeys();// 生成RSA密钥对
string encrypt = "123456".RSAEncrypt(rsaKey.PublicKey);// 公钥加密
string s = encrypt.RSADecrypt(rsaKey.PrivateKey);// 私钥解密

string s = "123".Crc32();// 生成crc32摘要
string s = "123".Crc64();// 生成crc64摘要

20.实体校验

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class MyClass
{
[IsEmail] //可在appsetting.json中添加EmailDomainWhiteList配置邮箱域名白名单,逗号分隔
public string Email { get; set; }

[IsPhone]
public string PhoneNumber { get; set; }

[IsIPAddress]
public string IP { get; set; }

[MinValue(0, ErrorMessage = "年龄最小为0岁"), MaxValue(100, ErrorMessage = "年龄最大100岁")]
public int Age { get; set; }

[ComplexPassword]//密码复杂度校验
public string Password { get; set; }
}

21.HTML操作

1
2
3
4
List<string> srcs = "html".MatchImgSrcs().ToList();// 获取html字符串里所有的img标签的src属性
var imgTags = "html".MatchImgTags();//获取html字符串里的所有的img标签
var str="html".RemoveHtmlTag(); // 去除html标签
...

22.DateTime扩展

1
2
3
4
5
6
double milliseconds = DateTime.Now.GetTotalMilliseconds();// 获取毫秒级时间戳
double microseconds = DateTime.Now.GetTotalMicroseconds();// 获取微秒级时间戳
double nanoseconds = DateTime.Now.GetTotalNanoseconds();// 获取纳秒级时间戳
double seconds = DateTime.Now.GetTotalSeconds();// 获取秒级时间戳
double minutes = DateTime.Now.GetTotalMinutes();// 获取分钟级时间戳
...

23.IP地址和URL

1
2
3
4
5
6
7
8
bool inRange = "192.168.2.2".IpAddressInRange("192.168.1.1","192.168.3.255");// 判断IP地址是否在这个地址段里
bool isPrivateIp = "172.16.23.25".IsPrivateIP();// 判断是否是私有地址
bool isExternalAddress = "http://baidu.com".IsExternalAddress();// 判断是否是外网的URL

//以下需要配置baiduAK
string isp = "114.114.114.114".GetISP(); // 获取ISP运营商信息
PhysicsAddress physicsAddress = "114.114.114.114".GetPhysicsAddressInfo().Result;// 获取详细地理信息对象
Tuple<string, List<string>> ipAddressInfo = "114.114.114.114".GetIPAddressInfo().Result;// 获取详细地理信息集合

24.元素去重

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
var list = new List<MyClass>()
{
new MyClass()
{
Email = "1@1.cn"
},
new MyClass()
{
Email = "1@1.cn"
},
new MyClass()
{
Email = "1@1.cn"
}
};
List<MyClass> classes = list.DistinctBy(c => c.Email).ToList();
Console.WriteLine(classes.Count==1);//True

25.对象实体映射

在使用前需要像automapper那样,对mapper进行初始化操作

1
using Masuit.Tools.Mapping;
1
2
3
4
5
6
ExpressionMapper.CreateMap<ClassA, ClassADto>();// 默认关系映射
ExpressionMapper.CreateMap<ClassB, ClassBDto>().ForMember(s => s.ClassC.PropertyName, d => d.CustomName, true);// 自定义关系映射

ExpressionMapper.ConstructServicesUsing((x) => DependencyResolver.Current.GetService(x));// 使用依赖注入容器进行构造映射
//ExpressionMapper.ConstructServicesUsing((x) => ServiceLocator.Current.GetInstance(x));// 使用依赖注入容器进行构造映射
ExpressionMapper.CreateMap<Product, IProduct>().ConstructUsingServiceLocator().ReverseMap();// 链式自定义关系映射和反向映射

测试class:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
public class TestClassA
{
public string MyProperty { get; set; }
public int Int { get; set; }
public double Double { get; set; }
public DateTime DateTime { get; set; }
public TestClassC TestClassC { get; set; }
public List<TestClassC> List { get; set; }
}

public class TestClassB
{
public string MyProperty { get; set; }
public int Int { get; set; }
public double Double { get; set; }
public DateTime DateTime { get; set; }
public TestClassC TestClassC { get; set; }
public List<TestClassD> List { get; set; }
}

public class TestClassC
{
public string MyProperty { get; set; }
public int Int { get; set; }
public double Double { get; set; }
public DateTime DateTime { get; set; }
public TestClassD Obj { get; set; }
}

public class TestClassD
{
public string MyProperty { get; set; }
public int Int { get; set; }
public double Double { get; set; }
public DateTime DateTime { get; set; }
public TestClassC Obj { get; set; }
}

构造一个结构相对复杂的对象:

1
2
3
4
5
6
7
8
9
10
11
12
13
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
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
var a = new TestClassA()
{
MyProperty = "ssssssssssssssssssssss",
DateTime = DateTime.Now,
Double = 123.33,
Int = 100,
TestClassC = new TestClassC()
{
MyProperty = "ccccccccccccccccccccccccccc",
DateTime = DateTime.Now,
Double = 2345.555,
Int = 10100,
Obj = new TestClassD()
{
MyProperty = "ddddddddddddddddddddddddd",
Obj = new TestClassC()
{
MyProperty = "cccccc",
DateTime = DateTime.Now,
Double = 23458894.555,
Int = 10100000,
Obj = new TestClassD()
}
}
},
List = new List<TestClassC>()
{
new TestClassC()
{
MyProperty = "cccccc",
DateTime = DateTime.Now,
Double = 2345.555,
Int = 10100,
Obj = new TestClassD()
{
MyProperty = "ddddddddddddddddddddddddddddddddddd",
DateTime = DateTime.Now,
Double = 2345.555,
Int = 10100,
Obj = new TestClassC()
{
MyProperty = "cccccccccccccccccccccccccccccc",
DateTime = DateTime.Now,
Double = 2345.555,
Int = 10100,
Obj = new TestClassD()
}
}
},
new TestClassC()
{
MyProperty = "cccccc",
DateTime = DateTime.Now,
Double = 2345.555,
Int = 10100,
Obj = new TestClassD()
{
MyProperty = "ddddddddddddddddddddddddddddddddddd",
DateTime = DateTime.Now,
Double = 2345.555,
Int = 10100,
Obj = new TestClassC()
{
MyProperty = "cccccccccccccccccccccccccccccc",
DateTime = DateTime.Now,
Double = 2345.555,
Int = 10100,
Obj = new TestClassD()
}
}
},
new TestClassC()
{
MyProperty = "cccccc",
DateTime = DateTime.Now,
Double = 2345.555,
Int = 10100,
Obj = new TestClassD()
{
MyProperty = "ddddddddddddddddddddddddddddddddddd",
DateTime = DateTime.Now,
Double = 2345.555,
Int = 10100,
Obj = new TestClassC()
{
MyProperty = "cccccccccccccccccccccccccccccc",
DateTime = DateTime.Now,
Double = 2345.555,
Int = 10100,
Obj = new TestClassD()
}
}
},
new TestClassC()
{
MyProperty = "cccccc",
DateTime = DateTime.Now,
Double = 2345.555,
Int = 10100,
Obj = new TestClassD()
{
MyProperty = "ddddddddddddddddddddddddddddddddddd",
DateTime = DateTime.Now,
Double = 2345.555,
Int = 10100,
Obj = new TestClassC()
{
MyProperty = "cccccccccccccccccccccccccccccc",
DateTime = DateTime.Now,
Double = 2345.555,
Int = 10100,
Obj = new TestClassD()
}
}
},
new TestClassC()
{
MyProperty = "cccccc",
DateTime = DateTime.Now,
Double = 2345.555,
Int = 10100,
Obj = new TestClassD()
{
MyProperty = "ddddddddddddddddddddddddddddddddddd",
DateTime = DateTime.Now,
Double = 2345.555,
Int = 10100,
Obj = new TestClassC()
{
MyProperty = "cccccccccccccccccccccccccccccc",
DateTime = DateTime.Now,
Double = 2345.555,
Int = 10100,
Obj = new TestClassD()
}
}
},
}
};
var b = a.Map<TestClassA, TestClassB>();

性能测试:i7-4700H+12GB DDR3

1
2
3
4
5
6
7
8
9
10
11
12
13
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
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
#region 配置automapper

Mapper.Initialize(e =>
{
e.CreateMap<TestClassA, TestClassB>().ReverseMap();
e.CreateMap<TestClassC, TestClassD>().ReverseMap();
});

#endregion

#region 配置ExpressionMapper

ExpressionMapper.CreateMap<TestClassA, TestClassB>().ReverseMap();
ExpressionMapper.CreateMap<TestClassC, TestClassD>().ReverseMap();

#endregion

#region 造一个大对象

var a = new TestClassA()
{
MyProperty = "ssssssssssssssssssssss",
DateTime = DateTime.Now,
Double = 123.33,
Int = 100,
TestClassC = new TestClassC()
{
MyProperty = "ccccccccccccccccccccccccccc",
DateTime = DateTime.Now,
Double = 2345.555,
Int = 10100,
Obj = new TestClassD()
{
MyProperty = "ddddddddddddddddddddddddd",
Obj = new TestClassC()
{
MyProperty = "cccccc",
DateTime = DateTime.Now,
Double = 23458894.555,
Int = 10100000,
Obj = new TestClassD()
}
}
},
List = new List<TestClassC>()
{
new TestClassC()
{
MyProperty = "cccccc",
DateTime = DateTime.Now,
Double = 2345.555,
Int = 10100,
Obj = new TestClassD()
{
MyProperty = "ddddddddddddddddddddddddddddddddddd",
DateTime = DateTime.Now,
Double = 2345.555,
Int = 10100,
Obj = new TestClassC()
{
MyProperty = "cccccccccccccccccccccccccccccc",
DateTime = DateTime.Now,
Double = 2345.555,
Int = 10100,
Obj = new TestClassD()
}
}
},
new TestClassC()
{
MyProperty = "cccccc",
DateTime = DateTime.Now,
Double = 2345.555,
Int = 10100,
Obj = new TestClassD()
{
MyProperty = "ddddddddddddddddddddddddddddddddddd",
DateTime = DateTime.Now,
Double = 2345.555,
Int = 10100,
Obj = new TestClassC()
{
MyProperty = "cccccccccccccccccccccccccccccc",
DateTime = DateTime.Now,
Double = 2345.555,
Int = 10100,
Obj = new TestClassD()
}
}
},
new TestClassC()
{
MyProperty = "cccccc",
DateTime = DateTime.Now,
Double = 2345.555,
Int = 10100,
Obj = new TestClassD()
{
MyProperty = "ddddddddddddddddddddddddddddddddddd",
DateTime = DateTime.Now,
Double = 2345.555,
Int = 10100,
Obj = new TestClassC()
{
MyProperty = "cccccccccccccccccccccccccccccc",
DateTime = DateTime.Now,
Double = 2345.555,
Int = 10100,
Obj = new TestClassD()
}
}
},
new TestClassC()
{
MyProperty = "cccccc",
DateTime = DateTime.Now,
Double = 2345.555,
Int = 10100,
Obj = new TestClassD()
{
MyProperty = "ddddddddddddddddddddddddddddddddddd",
DateTime = DateTime.Now,
Double = 2345.555,
Int = 10100,
Obj = new TestClassC()
{
MyProperty = "cccccccccccccccccccccccccccccc",
DateTime = DateTime.Now,
Double = 2345.555,
Int = 10100,
Obj = new TestClassD()
}
}
},
new TestClassC()
{
MyProperty = "cccccc",
DateTime = DateTime.Now,
Double = 2345.555,
Int = 10100,
Obj = new TestClassD()
{
MyProperty = "ddddddddddddddddddddddddddddddddddd",
DateTime = DateTime.Now,
Double = 2345.555,
Int = 10100,
Obj = new TestClassC()
{
MyProperty = "cccccccccccccccccccccccccccccc",
DateTime = DateTime.Now,
Double = 2345.555,
Int = 10100,
Obj = new TestClassD()
}
}
},
}
};

#endregion

var time = HiPerfTimer.Execute(() =>
{
a.Map<TestClassA, TestClassB>();
a.Map<TestClassA, TestClassB>();
});// 因为第一次需要编译表达式树,所以测试两次
Console.WriteLine($"ExpressionMapper映射2次耗时:{time}s");// 0.0270508s
time = HiPerfTimer.Execute(() =>
{
for (int i = 0; i < 1000000; i++)
{
var b = a.Map<TestClassA, TestClassB>();
}
});
Console.WriteLine($"ExpressionMapper映射1000000次耗时:{time}s");// 1.206569s

time = HiPerfTimer.Execute(() =>
{
Mapper.Map<TestClassB>(a);
Mapper.Map<TestClassB>(a);
});// 映射2次为了和ExpressionMapper保持相同情况
Console.WriteLine($"AutoMapper映射2次耗时:{time}s");// 0.0281503s
time = HiPerfTimer.Execute(() =>
{
for (int i = 0; i < 1000000; i++)
{
var b = Mapper.Map<TestClassB>(a);
}
});
Console.WriteLine($"AutoMapper映射1000000次耗时:{time}s");// 4.1858825s

26.枚举扩展

1
2
3
4
5
6
7
8
9
10
public enum MyEnum
{
[Display(Name = "读")]
[Description("读")]
Read,

[Display(Name = "写")]
[Description("写")]
Write
}
1
2
3
4
5
6
Dictionary<int, string> dic1 = typeof(MyEnum).GetDictionary();// 获取枚举值和字符串表示的字典映射
var dic2 = typeof(MyEnum).GetDescriptionAndValue();// 获取字符串表示和枚举值的字典映射
string desc = MyEnum.Read.GetDescription();// 获取Description标签
string display = MyEnum.Read.GetDisplay();// 获取Display标签的Name属性
var value = typeof(MyEnum).GetValue("Read");//获取字符串表示值对应的枚举值
string enumString = 0.ToEnumString(typeof(MyEnum));// 获取枚举值对应的字符串表示

27.定长队列实现

1
2
LimitedQueue<string> queue = new LimitedQueue<string>(32);// 声明一个容量为32个元素的定长队列
ConcurrentLimitedQueue<string> queue = new ConcurrentLimitedQueue<string>(32);// 声明一个容量为32个元素的线程安全的定长队列

28.反射操作

1
2
3
4
5
6
7
8
9
10
11
12
13
MyClass myClass = new MyClass();
PropertyInfo[] properties = myClass.GetProperties();// 获取属性列表
myClass.SetProperty("Email","1@1.cn");//给对象设置值

//动态增删对象的属性
MyClass myClass = new MyClass();
var mc = myClass.AddProperty(new List<ClassHelper.CustPropertyInfo>()
{
new ClassHelper.CustPropertyInfo(typeof(string), "Name", "张三"),
new ClassHelper.CustPropertyInfo(typeof(double), "Number", 123456.0),
});//添加属性
object newObj = mc.DeleteProperty(new List<string>() { "Email", "Age", "IP", "PhoneNumber" });// 删除属性
Console.WriteLine(newObj.ToJsonString());// {"Password":null,"Name":"张三","Number":123456.0}

29.获取线程内唯一对象

1
2
CallContext<T>.SetData("db",dbContext);//设置线程内唯一对象
CallContext<T>.GetData("db");//获取线程内唯一对象

30.asp.net core 获取静态的HttpContext对象

Startup.cs

1
2
3
4
5
6
7
8
9
10
11
12
13
public void ConfigureServices(IServiceCollection services)
{
// ...
services.AddStaticHttpContext();
// ...
}

public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
// ...
app.UseStaticHttpContext();
// ...
}
1
2
3
4
public async Task<IActionResult> Index()
{
HttpContext context = HttpContext2.Current;
}

31.邮件发送

1
2
3
4
5
6
7
8
9
10
11
12
13
14
new Email()
{
SmtpServer = "smtp.masuit.com",// SMTP服务器
SmtpPort = 25, // SMTP服务器端口
EnableSsl = true,//使用SSL
Username = "admin@masuit.com",// 邮箱用户名
Password = "123456",// 邮箱密码
Tos = "10000@qq.com,10001@qq.com", //收件人
Subject = "测试邮件",//邮件标题
Body = "你好啊",//邮件内容
}.SendAsync(s =>
{
Console.WriteLine(s);// 发送成功后的回调
});// 异步发送邮件

32.图像的简单处理

1
2
3
4
5
6
7
8
9
10
11
12
ImageUtilities.CompressImage(@"F:\src\1.jpg", @"F:\dest\2.jpg");//无损压缩图片

"base64".SaveDataUriAsImageFile();// 将Base64编码转换成图片

Image image = Image.FromFile(@"D:\1.jpg");
image.MakeThumbnail(@"D:\2.jpg", 120, 80, ThumbnailCutMode.LockWidth);//生成缩略图

Bitmap bmp = new Bitmap(@"D:\1.jpg");
Bitmap newBmp = bmp.BWPic(bmp.Width, bmp.Height);//转换成黑白
Bitmap newBmp = bmp.CutAndResize(new Rectangle(0, 0, 1600, 900), 160, 90);//裁剪并缩放
bmp.RevPicLR(bmp.Width, bmp.Height);//左右镜像
bmp.RevPicUD(bmp.Width, bmp.Height);//上下镜像

33.随机数

1
2
3
Random rnd = new Random();
int num = rnd.StrictNext();//产生真随机数
double gauss = rnd.NextGauss(20,5);//产生正态分布的随机数

34.权重筛选功能

1
2
3
4
5
6
7
8
9
var data=new List<WeightedItem<string>>()
{
new WeightedItem<string>("A", 1),
new WeightedItem<string>("B", 3),
new WeightedItem<string>("C", 4),
new WeightedItem<string>("D", 4),
};
var item=data.WeightedItem();//按权重选出1个元素
var list=data.WeightedItems(2);//按权重选出2个元素
1
2
3
4
5
6
7
8
9
var selector = new WeightedSelector<string>(new List<WeightedItem<string>>()
{
new WeightedItem<string>("A", 1),
new WeightedItem<string>("B", 3),
new WeightedItem<string>("C", 4),
new WeightedItem<string>("D", 4),
});
var item = selector.Select();//按权重选出1个元素
var list = selector.SelectMultiple(3);//按权重选出3个元素

35.EF Core支持AddOrUpdate方法

1
2
3
4
5
6
7
8
/// <summary>
/// 按Id添加或更新文章实体
/// </summary>
public override Post SavePost(Post t)
{
DataContext.Set<Post>().AddOrUpdate(t => t.Id, t);
return t;
}

38.敏感信息掩码

1
2
"13123456789".Mask(); // 131****5678
"admin@masuit.com".MaskEmail(); // a****n@masuit.com

39.集合扩展

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
var list = new List<string>()
{
"1","3","3","3"
};
list.AddRangeIf(s => s.Length > 1, "1", "11"); // 将被添加元素中的长度大于1的元素添加到list
list.AddRangeIfNotContains("1", "11"); // 将被添加元素中不包含的元素添加到list
list.RemoveWhere(s => s.Length<1); // 将集合中长度小于1的元素移除
list.InsertAfter(0, "2"); // 在第一个元素之后插入
list.InsertAfter(s => s == "1", "2"); // 在元素"1"后插入
var dic = list.ToDictionarySafety(s => s); // 安全的转换成字典类型,当键重复时只添加一个键
var dic = list.ToConcurrentDictionary(s => s); // 转换成并发字典类型,当键重复时只添加一个键
var dic = list.ToDictionarySafety(s => s, s => s.GetHashCode()); // 安全的转换成字典类型,当键重复时只添加一个键
dic.AddOrUpdate("4", 4); // 添加或更新键值对
dic.AddOrUpdate(new Dictionary<string, int>()
{
["5"] = 5,["55"]=555
}); // 批量添加或更新键值对
dic.AddOrUpdate("5", 6, (s, i) => 66); // 如果是添加,则值为6,若更新则值为66
dic.AddOrUpdate("5", 6, 666); // 如果是添加,则值为6,若更新则值为666
dic.AsConcurrentDictionary(); // 普通字典转换成并发字典集合
var table=list.ToDataTable(); // 转换成DataTable类型
table.AddIdentityColumn(); //给DataTable增加一个自增列
table.HasRows(); // 检查DataTable 是否有数据行
table.ToList<T>(); // datatable转List

40.Mime类型

1
2
3
var mimeMapper = new MimeMapper();
var mime = mimeMapper.GetExtensionFromMime("image/jpeg"); // .jpg
var ext = mimeMapper.GetMimeFromExtension(".jpg"); // image/jpeg

41.日期时间扩展

1
2
3
4
5
DateTime.Now.GetTotalSeconds(); // 获取该时间相对于1970-01-01 00:00:00的秒数
DateTime.Now.GetTotalMilliseconds(); // 获取该时间相对于1970-01-01 00:00:00的毫秒数
DateTime.Now.GetTotalMicroseconds(); // 获取该时间相对于1970-01-01 00:00:00的微秒数
DateTime.Now.GetTotalNanoseconds(); // 获取该时间相对于1970-01-01 00:00:00的纳秒数
...

42.流转换

1
2
stream.SaveAsMemoryStream(); // 任意流转换成内存流
stream.ToArray(); // 任意流转换成二进制数组

43.数值转换

1
2
3
1.2345678901.Digits8(); // 将小数截断为8位
1.23.To<int>(); // 小数转int
1.23.To<T>(); // 小数转T基本类型

44.简繁转换

1
2
var str="个体".ToTraditional(); // 转繁体
var str="個體".ToSimplified(); // 转简体

Asp.Net MVC和Asp.Net Core的支持断点续传和多线程下载的ResumeFileResult

允许你在ASP.NET Core中通过MVC/WebAPI应用程序传输文件数据时使用断点续传以及多线程下载。

它允许提供ETag标题以及Last-Modified标题。 它还支持以下前置条件标题:If-MatchIf-None-MatchIf-Modified-SinceIf-Unmodified-SinceIf-Range

支持 ASP.NET Core 2.0+

从.NET Core2.0开始,ASP.NET Core内部支持断点续传。 因此只是对FileResult做了一些扩展。 只留下了“Content-Disposition” Inline的一部分。 所有代码都依赖于基础.NET类。

如何使用

.NET Framework

在你的控制器中,你可以像在FileResult一样的方式使用它。

1
2
using Masuit.Tools.Mvc;
using Masuit.Tools.Mvc.ResumeFileResult;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
private readonly MimeMapper mimeMapper=new MimeMapper(); // 推荐使用依赖注入

public ActionResult ResumeFileResult()
{
var path = Server.MapPath("~/Content/test.mp4");
return new ResumeFileResult(path, mimeMapper.GetMimeFromPath(path), Request);
}

public ActionResult ResumeFile()
{
return this.ResumeFile("~/Content/test.mp4", mimeMapper.GetMimeFromPath(path), "test.mp4");
}

public ActionResult ResumePhysicalFile()
{
return this.ResumePhysicalFile(@"D:/test.mp4", mimeMapper.GetMimeFromPath(@"D:/test.mp4"), "test.mp4");
}

Asp.Net Core

要使用ResumeFileResults,必须在Startup.csConfigureServices方法调用中配置服务:

1
using Masuit.Tools.AspNetCore.ResumeFileResults.Extensions;
1
2
3
4
public void ConfigureServices(IServiceCollection services)
{
services.AddResumeFileResult();
}

然后在你的控制器中,你可以像在FileResult一样的方式使用它。

1
using Masuit.Tools.AspNetCore.ResumeFileResults.Extensions;
1
2
3
4
5
6
7
8
9
10
11
12
13
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
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
private const string EntityTag = "\"TestFile\"";

private readonly IHostingEnvironment _hostingEnvironment;

private readonly DateTimeOffset _lastModified = new DateTimeOffset(2016, 1, 1, 0, 0, 0, TimeSpan.Zero);

/// <summary>
///
/// </summary>
/// <param name="hostingEnvironment"></param>
public TestController(IHostingEnvironment hostingEnvironment)
{
_hostingEnvironment = hostingEnvironment;
}

[HttpGet("content/{fileName}/{etag}")]
public IActionResult FileContent(bool fileName, bool etag)
{
string webRoot = _hostingEnvironment.WebRootPath;
var content = System.IO.File.ReadAllBytes(Path.Combine(webRoot, "TestFile.txt"));
ResumeFileContentResult result = this.ResumeFile(content, "text/plain", fileName ? "TestFile.txt" : null, etag ? EntityTag : null);
result.LastModified = _lastModified;
return result;
}

[HttpGet("content/{fileName}")]
public IActionResult FileContent(bool fileName)
{
string webRoot = _hostingEnvironment.WebRootPath;
var content = System.IO.File.ReadAllBytes(Path.Combine(webRoot, "TestFile.txt"));
var result = new ResumeFileContentResult(content, "text/plain")
{
FileInlineName = "TestFile.txt",
LastModified = _lastModified
};
return result;
}

[HttpHead("file")]
public IActionResult FileHead()
{
ResumeVirtualFileResult result = this.ResumeFile("TestFile.txt", "text/plain", "TestFile.txt", EntityTag);
result.LastModified = _lastModified;
return result;
}

[HttpPut("file")]
public IActionResult FilePut()
{
ResumeVirtualFileResult result = this.ResumeFile("TestFile.txt", "text/plain", "TestFile.txt", EntityTag);
result.LastModified = _lastModified;
return result;
}

[HttpGet("stream/{fileName}/{etag}")]
public IActionResult FileStream(bool fileName, bool etag)
{
string webRoot = _hostingEnvironment.WebRootPath;
FileStream stream = System.IO.File.OpenRead(Path.Combine(webRoot, "TestFile.txt"));

ResumeFileStreamResult result = this.ResumeFile(stream, "text/plain", fileName ? "TestFile.txt" : null, etag ? EntityTag : null);
result.LastModified = _lastModified;
return result;
}

[HttpGet("stream/{fileName}")]
public IActionResult FileStream(bool fileName)
{
string webRoot = _hostingEnvironment.WebRootPath;
FileStream stream = System.IO.File.OpenRead(Path.Combine(webRoot, "TestFile.txt"));

var result = new ResumeFileStreamResult(stream, "text/plain")
{
FileInlineName = "TestFile.txt",
LastModified = _lastModified
};

return result;
}

[HttpGet("physical/{fileName}/{etag}")]
public IActionResult PhysicalFile(bool fileName, bool etag)
{
string webRoot = _hostingEnvironment.WebRootPath;

ResumePhysicalFileResult result = this.ResumePhysicalFile(Path.Combine(webRoot, "TestFile.txt"), "text/plain", fileName ? "TestFile.txt" : null, etag ? EntityTag : null);
result.LastModified = _lastModified;
return result;
}

[HttpGet("physical/{fileName}")]
public IActionResult PhysicalFile(bool fileName)
{
string webRoot = _hostingEnvironment.WebRootPath;

var result = new ResumePhysicalFileResult(Path.Combine(webRoot, "TestFile.txt"), "text/plain")
{
FileInlineName = "TestFile.txt",
LastModified = _lastModified
};

return result;
}

[HttpGet("virtual/{fileName}/{etag}")]
public IActionResult VirtualFile(bool fileName, bool etag)
{
ResumeVirtualFileResult result = this.ResumeFile("TestFile.txt", "text/plain", fileName ? "TestFile.txt" : null, etag ? EntityTag : null);
result.LastModified = _lastModified;
return result;
}

以上示例将为您的数据提供“Content-Disposition:attachment”。 当没有提供fileName时,数据将作为“Content-Disposition:inline”提供。 另外,它可以提供 ETagLastModified标题。

1
2
3
4
5
6
7
8
9
10
[HttpGet("virtual/{fileName}")]
public IActionResult VirtualFile(bool fileName)
{
var result = new ResumeVirtualFileResult("TestFile.txt", "text/plain")
{
FileInlineName = "TestFile.txt",
LastModified = _lastModified
};
return result;
}

https://www.cnblogs.com/tiger-wang/p/13471718.html

一、API的安全

作为一个Dotnet Core的老司机,写API时,能兼顾到API的安全,这是一种优雅。

通常,我们会用认证来保证API的安全,无敌的Authorize能解决我们很多的问题。

但是,总有一些场合,我们没办法用Authorize,而只能用匿名或不加验证的方式来访问。比方电商中查询SKU的列表并在前端展示,通常这个无关用户和权限,在完成API的时候,我们也不会加入认证 Authorize

这种情况下,如果直接写,不加入安全级别,这样的体系结构是有可能成为可供利用的安全漏洞的。

Dotnet Core框架已经提供了一些常见漏洞的解决方法,包括:

  • 跨站点脚本
  • SQL注入
  • 跨站点请求伪造(CSRF)
  • 重定向

等等。

但是,我们还需要更进一步,还需要照顾到以下常见的攻击:

  • 拒绝服务(DOS)
  • 分布式拒绝服务(DDOS)
  • 批量API调用
  • 探测响应
  • 数据抓取

这部分内容,需要我们自己实现。当然,这部分内容的实现,也可以从Web Server上进行设置。

本文讨论的,是代码的实现。

二、相关代码

今天偷个懒,不讲原理,以分享代码为主。

2.1 基于IP的客户端请求限制

通过限制客户端在指定的时间范围内的请求数量,防止恶意bot攻击。

代码中,我建立了一个基于IP的请求限制过滤器。

注意:有多个客户端位于同一个IP地址的情况,这个情况在这个代码中没有考虑。如果您希望实现这一点,可以把几种方式结合起来使用。

以下是代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
[AttributeUsage(AttributeTargets.Method)]
public class RequestLimitAttribute : ActionFilterAttribute
{
public string Name { get; }
public int NoOfRequest { get; set; }
public int Seconds { get; set; }

private static MemoryCache Cache { get; } = new MemoryCache(new MemoryCacheOptions());

public RequestLimitAttribute(string name, int noOfRequest = 5, int seconds = 10)
{
Name = name;
NoOfRequest = noOfRequest;
Seconds = seconds;
}
public override void OnActionExecuting(ActionExecutingContext context)
{
var ipAddress = context.HttpContext.Request.HttpContext.Connection.RemoteIpAddress;
var memoryCacheKey = $"{Name}-{ipAddress}";

Cache.TryGetValue(memoryCacheKey, out int prevReqCount);
if (prevReqCount >= NoOfRequest)
{
context.Result = new ContentResult
{
Content = $"Request limit is exceeded. Try again in {Seconds} seconds.",
};
context.HttpContext.Response.StatusCode = (int)HttpStatusCode.TooManyRequests;
}
else
{
var cacheEntryOptions = new MemoryCacheEntryOptions().SetAbsoluteExpiration(TimeSpan.FromSeconds(Seconds));
Cache.Set(memoryCacheKey, (prevReqCount + 1), cacheEntryOptions);
}
}
}

使用时,只要在需要的API前加属性即可:

1
2
3
4
5
6
[HttpGet]
[RequestLimit("DataGet", 5, 30)]
public IEnumerable<WeatherForecast> Get()
{
...
}

2.2 引用头检查

对API请求的请求引用头进行检查,可以防止API滥用,以及跨站点请求伪造(CSRF)攻击。

同样,也是采用自定义属性的方式。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
public class ValidateReferrerAttribute : ActionFilterAttribute
{
private IConfiguration _configuration;

public override void OnActionExecuting(ActionExecutingContext context)
{
_configuration = (IConfiguration)context.HttpContext.RequestServices.GetService(typeof(IConfiguration));

base.OnActionExecuting(context);

if (!IsValidRequest(context.HttpContext.Request))
{
context.Result = new ContentResult
{
Content = $"Invalid referer header",
};
context.HttpContext.Response.StatusCode = (int)HttpStatusCode.ExpectationFailed;
}
}
private bool IsValidRequest(HttpRequest request)
{
string referrerURL = "";

if (request.Headers.ContainsKey("Referer"))
{
referrerURL = request.Headers["Referer"];
}
if (string.IsNullOrWhiteSpace(referrerURL)) return true;

var allowedUrls = _configuration.GetSection("CorsOrigin").Get<string[]>()?.Select(url => new Uri(url).Authority).ToList();

bool isValidClient = allowedUrls.Contains(new Uri(referrerURL).Authority);

return isValidClient;
}
}

这里我用了一个配置,在appsetting.json中:

1
2
3
{
"CorsOrigin": ["https://test.com", "http://test1.cn:8080"]
}

CorsOrigin参数中加入允许引用的来源域名:端口列表。

使用时,在需要的API前加属性:

1
2
3
4
5
6
[HttpGet]
[ValidateReferrer]
public IEnumerable<WeatherForecast> Get()
{
...
}

2.3 DDOS攻击检查

DDOS攻击在网上很常见,这种攻击简单有效,可以让一个网站瞬间开始并长时间无法响应。通常来说,网站可以通过多种节流方法来避免这种情况。

下面我们换一种方式,用中间件MiddleWare来限制特定客户端IP的请求数量。

1
2
3
4
5
6
7
8
9
10
11
12
13
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
71
72
73
74
75
76
77
78
79
80
81
public class DosAttackMiddleware
{
private static Dictionary<string, short> _IpAdresses = new Dictionary<string, short>();
private static Stack<string> _Banned = new Stack<string>();
private static Timer _Timer = CreateTimer();
private static Timer _BannedTimer = CreateBanningTimer();

private const int BANNED_REQUESTS = 10;
private const int REDUCTION_INTERVAL = 1000; // 1 second
private const int RELEASE_INTERVAL = 5 * 60 * 1000; // 5 minutes
private RequestDelegate _next;

public DosAttackMiddleware(RequestDelegate next)
{
_next = next;
}
public async Task InvokeAsync(HttpContext httpContext)
{
string ip = httpContext.Connection.RemoteIpAddress.ToString();

if (_Banned.Contains(ip))
{
httpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden;
}

CheckIpAddress(ip);

await _next(httpContext);
}

private static void CheckIpAddress(string ip)
{
if (!_IpAdresses.ContainsKey(ip))
{
_IpAdresses[ip] = 1;
}
else if (_IpAdresses[ip] == BANNED_REQUESTS)
{
_Banned.Push(ip);
_IpAdresses.Remove(ip);
}
else
{
_IpAdresses[ip]++;
}
}


private static Timer CreateTimer()
{
Timer timer = GetTimer(REDUCTION_INTERVAL);
timer.Elapsed += new ElapsedEventHandler(TimerElapsed);
return timer;
}

private static Timer CreateBanningTimer()
{
Timer timer = GetTimer(RELEASE_INTERVAL);
timer.Elapsed += delegate {
if (_Banned.Any()) _Banned.Pop();
};
return timer;
}

private static Timer GetTimer(int interval)
{
Timer timer = new Timer();
timer.Interval = interval;
timer.Start();
return timer;
}

private static void TimerElapsed(object sender, ElapsedEventArgs e)
{
foreach (string key in _IpAdresses.Keys.ToList())
{
_IpAdresses[key]--;
if (_IpAdresses[key] == 0) _IpAdresses.Remove(key);
}
}
}

代码中设置:1秒(1000ms)中有超过10次访问时,对应的IP会被禁用5分钟。

使用时,在Startup.cs中直接加载中间件:

1
2
3
4
5
6
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
...
app.UseMiddleware<DosAttackMiddleware>();
...
}

三、结尾的话

以上代码仅为抛砖引玉之用。

公开的API,未经验证的API,在生产环境会因为种种原因被攻击。这几天公司的系统就因为这个出了大事。

所以,写API的时候,要充分考虑到这些网络攻击的可能性,通过正确的处理,来防止来自网络的攻击。

这是一份责任,也是一个理念。

与大家共勉!

(全文完)

本文的代码,我已经传到Github上,位置在:
https://github.com/humornif/Demo-Code/tree/master/0021/demo