A comprehensive .NET helper class library - Masuit.Tools

A comprehensive .NET helper class library - Masuit.Tools

Contains some commonly used operation classes, most of which are static classes

Last updated 3/11/2023 2:58 PM
懒得勤快
38 min read
Category
.NET
Tags
.NET C#

LICENSE nuget nuget codeSize language

Contains some commonly used operation classes, mostly static classes, encryption/decryption, reflection operations, weighted random selection algorithm, distributed short ids, expression trees, linq extensions, file compression, multi-threaded downloading and FTP client, hardware information, string extension methods, date/time extension operations, Chinese lunar calendar, large file copying, image cropping, captcha, resumable downloading, collection extensions, Excel export, and many other common wrappers. Many features in one, code size less than 2MB!

Official Tutorial

Masuit Tools

Project development model: daily code accumulation + online collection
⭐⭐⭐ If you like this project, please Star, Fork, Follow triple quality care ♂ note ⭐⭐⭐
If you have any questions about this project or encounter any problems during use, you can directly open an issue or contact me privately. I will provide you with completely free technical guidance. Of course, if you feel embarrassed to accept free guidance and want to tip me appropriately, I won't refuse! 🤣🤣🤣

This project is supported by JetBrains!

Star Trend

Please note:

Once any company that uses this open-source project or references this project or contains code from this project loses a lawsuit under labor law (including but not limited to illegal layoffs, overtime employment, child labor, etc.), once discovered, the author of this project has the right to claim usage fees for this project (2-5 times the registered capital of the company's business registration as the authorization fee for this project), or directly prohibit the use of any source code containing this project! Any nature of outsourcing companies or 996 companies that need to use this library must contact the author for commercial authorization! Other enterprises or individuals can use it freely without restrictions. 996 is not using people, it's wasting people. An 8-hour work system allows you to have time for self-improvement and future competitiveness. Oppose 996, it's everyone's responsibility!

Operating System: Windows 10 1903 and above
Development Tool: VisualStudio2019 v16.5 and above
SDK: .NET Core 2.1.0 and all later versions

Install Package

Basic Functionality Package

.NET Framework version of the package is temporarily suspended due to abnormal packaging environment.

.NET Framework ≥ 4.6.1

PM> Install-Package Masuit.Tools.Net

.NET Standard ≥ 2.1 or if you only want to use basic features

Recommended first choice for general projects

PM> Install-Package Masuit.Tools.Abstraction

.NET Core ≥ 2.1

Recommended first choice for .NET Core projects

PM> Install-Package Masuit.Tools.Core

.NET Framework 4.5 Special Edition

Please note: This is a .NET Framework 4.5 specific version. Compared to the 4.6.1 and .NET Core versions, it strips out Redis, HTML, file compression, ASP.NET extensions, hardware monitoring, Session extensions, and some other features. If your project version is higher than 4.5, be sure to use the above version package to enjoy the full feature experience!

PM> Install-Package Masuit.Tools.Net45

Value-Added Packages

Masuit.Tools.AspNetCore

Recommended first choice for AspNetCore projects

ASP.NET Core Web specific package, includes all features of Masuit.Tools.Core, and adds some additional support for ASP.NET Core Web functionality.

Masuit.Tools.Excel

Independent package for Excel import/export

Masuit.Tools.NoSQL.MongoDBClient

Independent package for MongoDB encapsulation operation class

Register Configuration for the Tool Library

The tool library requires external configuration sections. For .NET Framework projects, configure in the AppSettings section of web.config/app.config. For .NET Core projects, configure in appsettings.json:

  1. EmailDomainWhiteList, a whitelist of domain names needed for email validation, separated by commas, each element supports regular expressions. If not configured, the email whitelist validation is not enabled. Example: "^\\w{1,5}@qq.com,^\\w{1,5}@163.com,^\\w{1,5}@gmail.com,^\\w{1,5}@outlook.com"

  2. EmailDomainBlockList, a blacklist of domain names needed for email validation, separated by commas, each element supports regular expressions. The blacklist takes precedence over the whitelist. If not configured, the email blacklist/whitelist validation is not enabled.

public Startup(IConfiguration configuration)
{
    configuration.AddToMasuitTools(); // If not called, it will automatically attempt to load appsettings.json by default
}

Feature Demo Code

Online Experience

https://replit.com/@ldqk/MasuitToolsDemo?v=1#main.cs

1. Check if a string is an Email, phone number, URL, IP address, ID card number, etc.

var (isMatch, match) = "337845818@qq.com".MatchEmail(); // You can add EmailDomainWhiteList and EmailDomainBlockList in appsettings.json to configure email domain whitelist/blacklist, separated by commas, e.g., "EmailDomainBlockList": "^\\w{1,5}@qq.com,^\\w{1,5}@163.com,^\\w{1,5}@gmail.com,^\\w{1,5}@outlook.com",
bool isInetAddress = "114.114.114.114".MatchInetAddress();
bool isUrl = "http://ldqk.org/20/history".MatchUrl();
bool isPhoneNumber = "15205201520".MatchPhoneNumber();
bool isIdentifyCard = "312000199502230660".MatchIdentifyCard();// Validate Chinese mainland ID card number
bool isCNPatentNumber = "200410018477.9".MatchCNPatentNumber(); // Validate Chinese patent application number or patent number, with or without check digit, with or without "." before check digit, both can be validated. The number to be validated should not have CN, ZL prefix.

2. Hardware Monitoring (Windows only, some functions only support physical machine mode)

float load = SystemInfo.CpuLoad;// Get CPU usage
long physicalMemory = SystemInfo.PhysicalMemory;// Get total physical memory
long memoryAvailable = SystemInfo.MemoryAvailable;// Get available physical memory
double freePhysicalMemory = SystemInfo.GetFreePhysicalMemory();// Get available physical memory
double temperature = SystemInfo.GetCPUTemperature();// Get CPU temperature
int cpuCount = SystemInfo.GetCpuCount();// Get CPU core count
var ipAddress = SystemInfo.GetLocalIPs();// Get all local IP addresses
string localUsedIp = SystemInfo.GetLocalUsedIP();// Get the currently used IP address
IList<string> macAddress = SystemInfo.GetMacAddress();// Get all MAC addresses of network cards
string osVersion = Windows.GetOsVersion();// Get OS version
RamInfo ramInfo = SystemInfo.GetRamInfo();// Get memory info
var cpuSN=SystemInfo.GetCpuInfo()[0].SerialNumber; // CPU serial number
var driveSN=SystemInfo.GetDiskInfo()[0].SerialNumber; // Hard disk serial number

// Quick methods
var cpuInfos = CpuInfo.Locals; // Quickly get CPU info
var ramInfo = RamInfo.Local; // Quickly get memory info
var diskInfos = DiskInfo.Locals; // Quickly get hard disk info
var biosInfo = BiosInfo.Local; // Quickly get motherboard info

3. HTML Anti-XSS Processing:

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'>Logout</a></span>
        <input name='q' id='searchBoxInput'/>
        <input id='searchBoxSubmit' type='submit' value='Submit' />
        </form>
        </div>";
string s = html.HtmlSantinizerStandard();//After cleaning: <div><span><a href="/users/account/LogOff">Logout</a></span></div>

4. Clean Windows System Memory:

Similar to the acceleration ball function of various system optimization software

Windows.ClearMemorySilent();

5. Arbitrary Base Conversion

Can be used to generate short ids, short hashes, random strings, etc., pure mathematical operations.

NumberFormater nf = new NumberFormater(36);//Built-in 2-62 base conversion
//NumberFormater nf = new NumberFormater("0123456789abcdefghijklmnopqrstuvwxyz");// Custom base characters, can be used to generate captcha
string s36 = nf.ToString(12345678);
long num = nf.FromString("7clzi");
Console.WriteLine("12345678's base 36 is: " + s36); // 7clzi
Console.WriteLine("Base 36 '7clzi' is: " + num); // 12345678
var s = new NumberFormater(62).ToString(new Random().Next(100000, int.MaxValue)); // Generate random string with random number
// Extension method call
var bin=12345678.ToBase(36);// Decimal to base 36: 7clzi
var num="7clzi".FromBase(36);// Base 36 to decimal: 12345678
// Large number base conversion
var num = "e6186159d38cd50e0463a55e596336bd".FromBaseBig(16); // Large number hex to decimal
Console.WriteLine(num); // Decimal: 305849028665645097422198928560410015421
Console.WriteLine(num.ToBase(64)); // Base 64: 3C665pQUPl3whzFlVpoPqZ, 22 characters
Console.WriteLine(num.ToBase(36)); // Base 36: dmed4dkd5bhcg4qdktklun0zh, 25 characters
Console.WriteLine(num.ToBase(7)); // Base 7: 2600240311641665565300424545154525131265221035, 46 characters
Console.WriteLine(num.ToBase(12)); // Base 12: 5217744842749978a756b22135b16a5998a5, 36 characters
Console.WriteLine(num.ToBase(41)); // Base 41: opzeBda2aytcEeudEquuesbk, 24 characters

6. Nanosecond Precision Timer

HiPerfTimer timer = HiPerfTimer.StartNew();
for (int i = 0; i < 100000; i++)
{
    //todo
}
timer.Stop();
Console.WriteLine("Time for 100k for loop: "+timer.Duration+"s");
double time = HiPerfTimer.Execute(() =>
{
    for (int i = 0; i < 100000; i++)
    {
        //todo
    }
});
Console.WriteLine("Time for 100k for loop: "+time+"s");

7. Generate Distributed Unique Ordered Short ID (Snowflake ID)

var sf = SnowFlake.GetInstance();
string token = sf.GetUniqueId();// rcofqodori0w
string shortId = sf.GetUniqueShortId(8);// qodw9728
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("Time to generate 1M ids: " + time + "s"); //2.6891495s

8. Chinese Lunar Calendar Conversion

ChineseCalendar.CustomHolidays.Add(DateTime.Parse("2018-12-31"),"New Year's Day");//Custom holidays
ChineseCalendar today = new ChineseCalendar(DateTime.Parse("2018-12-31"));
Console.WriteLine(today.ChineseDateString);// 二零一八年十一月廿五
Console.WriteLine(today.AnimalString);// Zodiac: Dog
Console.WriteLine(today.GanZhiDateString);// Stem-Branch: 戊戌年甲子月丁酉日
Console.WriteLine(today.DateHoliday);// Get holidays based on Gregorian calendar
...

9. LINQ Expression Tree Extensions

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)
    .AndIf(!string.IsNullOrEmpty(name),s=>s==name)
    .Compile(); // And and AndIf available, perform And only if condition met
bool b=func("abcd12345678");//true
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)
    .OrIf(!string.IsNullOrEmpty(name),s=>s==name)
    .Compile(); // Or and OrIf available, perform Or only if condition met
bool b=func("abc");// true
queryable.WhereIf(!string.IsNullOrEmpty(name),e=>e.Name==name)
    .WhereIf(()=> age.HasValue,e=>e.Age>=age); // IQueryable WhereIf extension, perform Where only if condition met

10. Template Engine

var tmp = new Template("{{name}}, Hello!");
tmp.Set("name", "Jack");
string s = tmp.Render();//Jack, Hello!
var tmp = new Template("{{one}},{{two}},{{three}}");
string s = tmp.Set("one", "1").Set("two", "2").Set("three", "3").Render();// 1,2,3
var tmp = new Template("{{name}}, {{greet}}!");
tmp.Set("name", "Jack");
string s = tmp.Render();// throw Template variable {{greet}} not used

11. List to DataTable

var list = new List<MyClass>()
{
    new MyClass()
    {
        Name = "Zhang San",
        Age = 22
    },
    new MyClass()
    {
        Name = "Li Si",
        Age = 21
    },
    new MyClass()
    {
        Name = "Wang Wu",
        Age = 28
    }
};
var table = list.Select(c => new{Name=c.Name, Age=c.Age}).ToDataTable();// Will automatically fill columns Name and Age

12. File Compression and Decompression

.NET Framework

MemoryStream ms = SevenZipCompressor.ZipStream(new List<string>()
{
    @"D:\1.txt",
    "http://ww3.sinaimg.cn/large/87c01ec7gy1fsq6rywto2j20je0d3td0.jpg",
});//Compress to memory stream
SevenZipCompressor.Zip(new List<string>()
{
    @"D:\1.txt",
    "http://ww3.sinaimg.cn/large/87c01ec7gy1fsq6rywto2j20je0d3td0.jpg",
}, zip);//Compress to zip
SevenZipCompressor.UnRar(@"D:\Download\test.rar", @"D:\Download\");//Extract rar
SevenZipCompressor.Decompress(@"D:\Download\test.tar", @"D:\Download\");//Auto-detect and decompress archive
SevenZipCompressor.Decompress(@"D:\Download\test.7z", @"D:\Download\");

ASP.NET Core

Startup.cs

services.AddSevenZipCompressor();

Inject ISevenZipCompressor via constructor

private readonly ISevenZipCompressor _sevenZipCompressor;
public Test(ISevenZipCompressor sevenZipCompressor)
{
    _sevenZipCompressor = sevenZipCompressor;
}

Usage is the same as .NET Framework version

13. Simple Log Component (It works, okay? .jpg)

LogManager.LogDirectory=AppDomain.CurrentDomain.BaseDirectory+"/logs";
LogManager.Event+=info =>
{
    //todo: register some event operations
};
LogManager.Info("Log a message");
LogManager.Error(new Exception("Exception message"));

14. FTP Client

FtpClient ftpClient = FtpClient.GetAnonymousClient("192.168.2.2");//Create an anonymous client
//FtpClient ftpClient = FtpClient.GetClient("192.168.2.3","admin","123456");// Create a client with username and password
ftpClient.Delete("/1.txt");// Delete file
ftpClient.Download("/test/2.txt","D:\\test\\2.txt");// Download file
ftpClient.UploadFile("/test/22.txt","D:\\test\\22.txt",(sum, progress) =>
{
    Console.WriteLine("Uploaded: "+progress*1.0/sum);
});//Upload file with progress
List<string> files = ftpClient.GetFiles("/");//List ftp server files
...

15. Multi-threaded Background Download

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("Download progress: "+downloader.TotalProgress+"%");
    Console.WriteLine("Download speed: "+downloader.TotalSpeedInBytes/1024/1024+"MBps");
};
mtd.FileMergeProgressChanged+=(sender, e) =>
{
    Console.WriteLine("Download completed");
};
mtd.FileMergedComplete+=(sender,e)=>{
    Console.WriteLine("File merge completed");
};
mtd.Start();//Start download
//mtd.Pause(); // Pause download
//mtd.Resume(); // Resume download

16. Encryption/Decryption / Hash

var enc="123456".MDString();// MD5
var enc="123456".MDString("abc");// MD5 with salt
var enc="123456".MDString2();// MD5 double
var enc="123456".MDString2("abc");// MD5 double with salt
var enc="123456".MDString3();// MD5 triple
var enc="123456".MDString3("abc");// MD5 triple with salt

string aes = "123456".AESEncrypt();// AES encrypt to ciphertext
string s = aes.AESDecrypt(); //AES decrypt to plaintext
string aes = "123456".AESEncrypt("abc");// AES key encrypt to ciphertext
string s = aes.AESDecrypt("abc"); //AES key decrypt to plaintext

string enc = "123456".DesEncrypt();// DES encrypt to ciphertext
string s = enc.DesDecrypt(); //DES decrypt to plaintext
string enc = "123456".DesEncrypt("abcdefgh");// DES key encrypt to ciphertext
string s = enc.DesDecrypt("abcdefgh"); //DES key decrypt to plaintext

RsaKey rsaKey = RsaCrypt.GenerateRsaKeys();// Generate RSA key pair
string encrypt = "123456".RSAEncrypt(rsaKey.PublicKey);// Public key encrypt
string s = encrypt.RSADecrypt(rsaKey.PrivateKey);// Private key decrypt

string s = "123".Crc32();// Generate crc32 digest
string s = "123".Crc64();// Generate crc64 digest
string s = "123".SHA256();// Generate SHA256 digest

string pub="hello,world!";
string hidden="ldqk";
var str = pub.InjectZeroWidthString(hidden); // Extension method call: hide "ldqk" as zero-width string in "hello,world!"
var str = ZeroWidthCodec.Encrypt(pub,hidden); // Class call: hide "ldqk" as zero-width string in "hello,world!"
var dec = str.DecodeZeroWidthString(); // Extension method call: decrypt hidden string "ldqk" from zero-width string ciphertext
var dec = ZeroWidthCodec.Decrypt(str); // Class call: decrypt hidden string "ldqk" from zero-width string ciphertext
var enc = hidden.EncodeToZeroWidthText(); // Extension method call: encode string to zero-width string
var enc = ZeroWidthCodec.Encode(); // Class call: encode string to zero-width string

17. Entity Validation

public class MyClass
{
    [IsEmail] // You can add EmailDomainWhiteList in appsettings.json to configure email domain whitelist, separated by commas
    public string Email { get; set; }

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

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

    [MinValue(0, ErrorMessage = "Minimum age is 0"), MaxValue(100, ErrorMessage = "Maximum age is 100")]
    public int Age { get; set; }

    [ComplexPassword]// Password complexity validation
    public string Password { get; set; }

    [EnumOf] // Check if valid enum value
    public MyEnum MyEnum { get; set; }

    [MinItemsCount(1)] // Check collection has at least 1 element
    public List<string> Strs { get; set; }
}

18. HTML Operations

List<string> srcs = "html".MatchImgSrcs().ToList();// Get all img src attributes from HTML string
var imgTags = "html".MatchImgTags();// Get all img tags from HTML string
var str="html".RemoveHtmlTag(); // Remove HTML tags
...

19. DateTime Extensions

double milliseconds = DateTime.Now.GetTotalMilliseconds();// Get millisecond timestamp
double microseconds = DateTime.Now.GetTotalMicroseconds();// Get microsecond timestamp
double nanoseconds = DateTime.Now.GetTotalNanoseconds();// Get nanosecond timestamp
double seconds = DateTime.Now.GetTotalSeconds();// Get second timestamp
double minutes = DateTime.Now.GetTotalMinutes();// Get minute timestamp
...

20. IP Address and URL

bool inRange = "192.168.2.2".IpAddressInRange("192.168.1.1","192.168.3.255");// Check if IP address is within this range
bool isPrivateIp = "172.16.23.25".IsPrivateIP();// Check if private address
bool isExternalAddress = "http://baidu.com".IsExternalAddress();// Check if external URL

// Requires baiduAK configuration
string isp = "114.114.114.114".GetISP(); // Get ISP operator info
PhysicsAddress physicsAddress = "114.114.114.114".GetPhysicsAddressInfo().Result;// Get detailed geographic info object
Tuple<string, List<string>> ipAddressInfo = "114.114.114.114".GetIPAddressInfo().Result;// Get detailed geographic info collection

21. Element Deduplication

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

22. Enum Extensions

public enum MyEnum
{
    [Display(Name = "Read")]
    [Description("Read")]
    Read,

    [Display(Name = "Write")]
    [Description("Write")]
    Write
}
Dictionary<int, string> dic1 = typeof(MyEnum).GetDictionary();// Get enum value and string representation dictionary
var dic2 = typeof(MyEnum).GetDescriptionAndValue();// Get string representation and enum value dictionary
string desc = MyEnum.Read.GetDescription();// Get Description label
string display = MyEnum.Read.GetDisplay();// Get Display label Name property
var value = typeof(MyEnum).GetValue("Read");// Get enum value from string representation
string enumString = 0.ToEnumString(typeof(MyEnum));// Get string representation from enum value

23. Fixed-length Queue and ConcurrentHashSet Implementation

If .NET5 or above, it is recommended to use the framework's built-in Channel to achieve this functionality

LimitedQueue<string> queue = new LimitedQueue<string>(32);// Declare a fixed-length queue with capacity 32
ConcurrentLimitedQueue<string> queue = new ConcurrentLimitedQueue<string>(32);// Declare a thread-safe fixed-length queue with capacity 32
var set = new ConcurrentHashSet<string>(); // Usage consistent with hashset

24. Reflection Operations

MyClass myClass = new MyClass();
PropertyInfo[] properties = myClass.GetProperties();// Get property list
myClass.SetProperty("Email","1@1.cn");// Set value to object
myClass.DeepClone(); // Deep copy object with nested levels

25. Get Thread-Unique Object

CallContext<T>.SetData("db",dbContext);//Set thread-unique object
CallContext<T>.GetData("db");//Get thread-unique object

26. Email Sending

new Email()
{
    SmtpServer = "smtp.masuit.com",// SMTP server
    SmtpPort = 25, // SMTP server port
    EnableSsl = true,// Use SSL
    Username = "admin@masuit.com",// Email username
    Password = "123456",// Email password
    Tos = "10000@qq.com,10001@qq.com", // Recipients
    Subject = "Test Email",// Email subject
    Body = "Hello",// Email body
}.SendAsync(s =>
{
    Console.WriteLine(s);// Callback after successful send
});// Send email asynchronously

27. Simple Image Processing

ImageUtilities.CompressImage(@"F:\src\1.jpg", @"F:\dest\2.jpg");//Lossless image compression

"base64".SaveDataUriAsImageFile();// Convert Base64 string to image

Image image = Image.FromFile(@"D:\1.jpg");
image.MakeThumbnail(@"D:\2.jpg", 120, 80, ThumbnailCutMode.LockWidth);//Generate thumbnail

Bitmap bmp = new Bitmap(@"D:\1.jpg");
Bitmap newBmp = bmp.BWPic(bmp.Width, bmp.Height);//Convert to black and white
Bitmap newBmp = bmp.CutAndResize(new Rectangle(0, 0, 1600, 900), 160, 90);//Crop and resize
bmp.RevPicLR(bmp.Width, bmp.Height);//Mirror left-right
bmp.RevPicUD(bmp.Width, bmp.Height);//Mirror up-down

var marker=ImageWatermarker(stream);
stream=maker.AddWatermark("Watermark Text","Font File",FontSize,color,WatermarkPosition,Margin); // Add watermark text to image
stream=maker.AddWatermark(WatermarkImage,WatermarkPosition,Margin,FontSize,Font); // Add watermark image to image

// Image similarity comparison
var hasher = new ImageHasher();
var hash1 = hasher.DifferenceHash256("Image1"); // Use difference hash algorithm to compute 256-bit hash of image
var hash2 = hasher.DifferenceHash256("Image2"); // Use difference hash algorithm to compute 256-bit hash of image
//var hash1 = hasher.AverageHash64("Image1"); // Use average algorithm to compute 64-bit hash of image
//var hash2 = hasher.AverageHash64("Image2"); // Use average algorithm to compute 64-bit hash of image
//var hash1 = hasher.DctHash("Image1"); // Use DCT algorithm to compute 64-bit hash of image
//var hash2 = hasher.DctHash("Image2"); // Use DCT algorithm to compute 64-bit hash of image
//var hash1 = hasher.MedianHash64("Image1"); // Use median algorithm to compute 64-bit hash of image
//var hash2 = hasher.MedianHash64("Image2"); // Use median algorithm to compute 64-bit hash of image
var sim=ImageHasher.Compare(hash1,hash2); // Image similarity, range: [0,1]

var imageFormat=stream.GetImageType(); // Get actual image format

28. Random Numbers

Random rnd = new Random();
int num = rnd.StrictNext();//Generate true random number
double gauss = rnd.NextGauss(20,5);//Generate Gaussian normal distribution random number
var s = new NumberFormater(62).ToString(new Random().Next(100000, int.MaxValue));//Generate random string

29. Weighted Selection

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();//Select 1 element by weight
var list=data.WeightedItems(2);//Select 2 elements by weight
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();//Select 1 element by weight
var list = selector.SelectMultiple(3);//Select 3 elements by weight

30. EF Core Supports AddOrUpdate Method

/// <summary>
/// Add or update article entity by Id
/// </summary>
public override Post SavePost(Post t)
{
    DataContext.Set<Post>().AddOrUpdate(t => t.Id, t);
    return t;
}

31. Sensitive Information Masking

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

32. Collection Extensions

var list = new List<string>()
{
    "1","3","3","3"
};
list.AddRangeIf(s => s.Length > 1, "1", "11"); // Add elements with length > 1 to list
list.AddRangeIfNotContains("1", "11"); // Add elements not already in list
list.RemoveWhere(s => s.Length<1); // Remove elements with length < 1
list.InsertAfter(0, "2"); // Insert after first element
list.InsertAfter(s => s == "1", "2"); // Insert after element "1"
var dic = list.ToDictionarySafety(s => s); // Safely convert to dictionary, only add one key for duplicates
var dic = list.ToConcurrentDictionary(s => s); // Convert to concurrent dictionary, only add one key for duplicates
var dic = list.ToDictionarySafety(s => s, s => s.GetHashCode()); // Safely convert to dictionary, only add one key for duplicates
dic.AddOrUpdate("4", 4); // Add or update key-value pair
dic.AddOrUpdate(new Dictionary<string, int>()
{
    ["5"] = 5,["55"]=555
}); // Batch add or update key-value pairs
dic.AddOrUpdate("5", 6, (s, i) => 66); // If adding, value is 6; if updating, value is 66
dic.AddOrUpdate("5", 6, 666); // If adding, value is 6; if updating, value is 666
dic.GetOrAdd("7",77); // Dictionary get or add element
dic.GetOrAdd("7",()=>77); // Dictionary get or add element
dic.AsConcurrentDictionary(); // Convert normal dictionary to concurrent dictionary
var table=list.ToDataTable(); // Convert to DataTable
table.AddIdentityColumn(); // Add auto-increment column to DataTable
table.HasRows(); // Check if DataTable has data rows
table.ToList<T>(); // DataTable to List
var set = list.ToHashSet(s=>s.Name);// Convert to HashSet
var cts = new CancellationTokenSource(100); // Cancellation token
await list.ForeachAsync(async i=>{
    await Task.Delay(100);
    Console.WriteLine(i);
},cts.Token); // Async foreach

await list.ForAsync(async (item,index)=>{
    await Task.Delay(100);
    Console.WriteLine(item+"_"+index);
},cts.Token); // Async for with index
await list.SelectAsync(async i=>{
    await Task.Delay(100);
    return i*10;
}); // Async Select
await list.SelectAsync(async (item,index)=>{
    await Task.Delay(100);
    return item*10;
}); // Async Select with index
string s=list.Join(",");//Join string collection into comma-separated single string
var max=list.MaxOrDefault(); // Get max value, no error when collection is empty
var max=list.MaxOrDefault(selector); // Get max value, no error when collection is empty
var max=list.MaxOrDefault(selector,default); // Get max value, no error when collection is empty
var max=list.MinOrDefault(); // Get min value, no error when collection is empty
var max=list.MinOrDefault(selector); // Get min value, no error when collection is empty
var max=list.MinOrDefault(selector,default); // Get min value, no error when collection is empty
var stdDev=list.Select(s=>s.ConvertTo<int>()).StandardDeviation(); // Calculate standard deviation

var pages=queryable.ToPagedList(1,10); // Paged query
var pages=await queryable.ToPagedListAsync(1,10); // Paged query async

var nums=Enumerable.Range(1, 10).ExceptBy(Enumerable.Range(5, 10), i => i); // Set difference by key
var nums=Enumerable.Range(1, 10).IntersectBy(Enumerable.Range(5, 10), i => i); // Set intersection by key
var nums=Enumerable.Range(1, 10).SequenceEqual(Enumerable.Range(5, 10), i => i); // Check sequence equality
var nums=Enumerable.Range(1, 10).OrderByRandom(); // Random order

// Multiple collection intersection
var list=new List<List<MyClass>>(){
    new List<MyClass>(){
        new MyClass(){Name="aa",Age=11},
        new MyClass(){Name="bb",Age=12},
        new MyClass(){Name="cc",Age=13},
    },
    new List<MyClass>(){
        new MyClass(){Name="bb",Age=12},
        new MyClass(){Name="cc",Age=13},
        new MyClass(){Name="dd",Age=14},
    },
    new List<MyClass>(){
        new MyClass(){Name="cc",Age=13},
        new MyClass(){Name="dd",Age=14},
        new MyClass(){Name="ee",Age=15},
    },
};
var sect=list.IntersectAll(m=>m.Name); // new MyClass(){Name="cc",Age=13}

var list=new List<List<int>>(){
    new(){1,2,3},
    new(){2,3,4},
    new(){3,4,5}
};
var sect=list.IntersectAll();// [3]

// Change element index in collection
list.ChangeIndex(item,3); // Change index of element item to 3
list.ChangeIndex(t=>t.Id=="123",2); // Change index of element with id "123" to 2

33. Mime Types

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

34. DateTime Extensions

DateTime.Now.GetTotalSeconds(); // Get seconds since 1970-01-01 00:00:00
DateTime.Now.GetTotalMilliseconds(); // Get milliseconds since 1970-01-01 00:00:00
DateTime.Now.GetTotalMicroseconds(); // Get microseconds since 1970-01-01 00:00:00
DateTime.Now.GetTotalNanoseconds(); // Get nanoseconds since 1970-01-01 00:00:00
var indate=DateTime.Parse("2020-8-3").In(DateTime.Parse("2020-8-2"),DateTime.Parse("2020-8-4"));//true
DateTime time="2021-1-1 8:00:00".ToDateTime(); //String to DateTime

// Time range calculation tool
var range = new DateTimeRange(DateTime.Parse("2020-8-3"), DateTime.Parse("2020-8-5"));
range.Union(DateTime.Parse("2020-8-4"), DateTime.Parse("2020-8-6")); // Union two time ranges, result: 2020-8-3~2020-8-6
range.In(DateTime.Parse("2020-8-3"), DateTime.Parse("2020-8-6"));//Check if within a time range, true
var (intersected,range2) = range.Intersect(DateTime.Parse("2020-8-4"), DateTime.Parse("2020-8-6"));//Check if two time ranges intersect, (true,2020-8-3~2020-8-4)
range.Contains(DateTime.Parse("2020-8-3"), DateTime.Parse("2020-8-4"));//Check if contains a time range, true
...
stream.SaveAsMemoryStream(); // Convert any stream to memory stream
stream.ToArray(); // Convert any stream to byte array
stream.ToArrayAsync(); // Convert any stream to byte array async
stream.ShuffleCode(); // Stream shuffle, append a few null bytes at the end, use with caution for important data, may cause stream corruption

// Pooled memory stream, usage same as MemorySteam
using var ms=PooledMemoryStream();

// Large memory stream, supports up to 1TB memory data, recommended when data stream >2GB, usage same as MemorySteam
using var ms=LargeMemoryStream();

// Fast file stream copy
FileStream fs = new FileStream(@"D:\boot.vmdk", FileMode.OpenOrCreate, FileAccess.ReadWrite);
{
        //fs.CopyToFile(@"D:\1.bak");//Sync copy large file
        fs.CopyToFileAsync(@"D:\1.bak");//Async copy large file
        string md5 = fs.GetFileMD5Async().Result;//Async get file MD5
        string sha1 = fs.GetFileSha1();//Async get file SHA1
}
memoryStream.SaveFile("filename"); // Save memory stream to file

36. Numeric Conversion

1.2345678901.Digits8(); // Truncate decimal to 8 digits
1.23.ConvertTo<int>(); // Decimal to int
1.23.ConvertTo<T>(); // Decimal to T basic type
bool b=1.23.TryConvertTo<T>(out result); // Decimal to T basic type
var num=1.2345.ToDecimal(2); //Convert to decimal with 2 decimal places

37. INI Configuration File Operations (Windows only)

INIFile ini=new INIFile("filename.ini");
ini.IniWriteValue(section,key,value); // Write value
ini.IniReadValue(section,key); // Read value
ini.ClearAllSection(); // Clear all sections
ini.ClearSection(section); // Clear section

38. Radar Chart Calculation Engine

Use case: Calculate similarity of two polygons, user profiling, etc.

var points=RadarChartEngine.ComputeIntersection(chart1,chart2); // Get intersection area of two polygons
points.ComputeArea(); // Calculate polygon area

39. Tree Structure Implementation

Basic interfaces:
ITreeChildren: Interface with Children property
ITreeParent: Interface with Parent property
ITree: Inherits ITreeParent and ITreeChildren, plus Name property

Related extension methods:

trees.Filter(func); // Filter from tree collection
trees.Flatten(); // Flatten data
tree.AllChildren(); // Get all children
tree.AllParent(); // Get all parents
tree.IsRoot(); // Check if root node
tree.IsLeaf(); // Check if leaf node
tree.Level(); // Get depth/level
tree.Path(); // Get full path

var tree=list.ToTree(c => c.Id, c => c.Pid);//Convert collection inheriting ITreeParent<T>, ITreeChildren<T> to tree structure
var tree=list.ToTreeGeneral(c => c.Id, c => c.Pid);//Convert general collection to tree structure

40. Simple Excel Export

Requires additional dependency package: Masuit.Tools.Excel

var stream=list.Select(item=>new{
    Name=item.Name,
    Age=item.Age,
    item.Gender,
    Avatar=Image.FromStream(filestream) //Image column
}).ToDataTable().ToExcel("Sheet1"); //Custom column name export
var stream=list.ToDataTable("Sheet1").ToExcel("File password");

Some conventions:

  1. Image columns support Stream, Bitmap, IEnumerable, IEnumerable, IDictionary<string,Stream>, IDictionary<string,MemoryStream>, IDictionary<string,Bitmap> types;
  2. For IDictionary type image columns, the dictionary key is the full URL of the image hyperlink;
  3. Default field name as column name for export;
  4. If list is a specific strong type, it will first look for each field's Description attribute; if Description is present, use it as column name.
  5. ToExcel method supports direct calls on DataTable, List, Dictionary<string, DataTable>

41. EFCore Entity Comparison

Get changes of a specific entity

var changes=dbContext.GetChanges<Post>();//Get changed field info
var added=dbContext.GetAdded<Post>();//Get added entity field info
var removed=dbContext.GetRemoved<Post>();//Get removed entity field info
var allchanges=dbContext.GetAllChanges<Post>();//Get all added, modified, removed entity field info

Get changes of all entities

var changes=dbContext.GetChanges();//Get changed field info
var added=dbContext.GetAdded();//Get added entity field info
var removed=dbContext.GetRemoved();//Get removed entity field info
var allchanges=dbContext.GetAllChanges();//Get all added, modified, removed entity field info

Comparison info includes property info, old value, new value, entity info, key info, change status, etc.

42. Any Type Supports Chained Calls

a.Next(func1).Next(func2).Next(func3);
"123".Next(s=>s.ToInt32()).Next(x=>x*2).Next(x=>Math.Log(x));

43. Newtonsoft.Json Contract Resolvers for Allowed Field Deserialization Only

DeserializeOnlyContractResolver

This resolver, for class properties marked with DeserializeOnlyJsonPropertyAttribute, takes effect during deserialization and is ignored during serialization.

public class ClassDto
    {
        [DeserializeOnlyJsonProperty]
        public string MyProperty { get; set; }

        public int Num { get; set; }
    }

    JsonConvert.SerializeObject(new MyClass(),new JsonSerializerSettings()
    {
        ContractResolver = new DeserializeOnlyContractResolver() // Configure to use DeserializeOnlyContractResolver
    });

For global use in WebAPI:

        //In Startup.ConfigureServices
        services.AddMvc().AddNewtonsoftJson(options =>
             {
                 var resolver = new DeserializeOnlyContractResolver();
                 resolver.NamingStrategy = new CamelCaseNamingStrategy();
                 options.SerializerSettings.ContractResolver = resolver;
             });

FallbackJsonPropertyResolver

This resolver allows setting multiple aliases for a property, supporting multiple alias keys during deserialization, compensating for the limitation of the official JsonProperty alias attribute that only allows a single alias.

    public class ClassDto
    {
        [FallbackJsonProperty("MyProperty","a","b")]
        public string MyProperty { get; set; }

        public int Num { get; set; }
    }

    JsonConvert.SerializeObject(new MyClass(),new JsonSerializerSettings()
    {
        ContractResolver = new FallbackJsonPropertyResolver() // Configure to use FallbackJsonPropertyResolver
    });

CompositeContractResolver

This resolver is a combination of DeserializeOnlyContractResolver and FallbackJsonPropertyResolver.

44. ASP.NET Core Action Model Binder BodyOrDefaultModelBinder Supporting queryString, Form, and JSON Request Types

Usage:

Import package: Masuit.Tools.AspNetCore

PM> Install-Package Masuit.Tools.AspNetCore

Startup configuration:

    services.AddMvc(options =>
        {
             options.ModelBinderProviders.InsertBodyOrDefaultBinding();
        })

Mark the action parameter model with [FromBodyOrDefault] attribute, can also be omitted. Example code:

        [HttpGet("query"),HttpPost("query")]
        public IActionResult Query([FromBodyOrDefault]QueryModel query)
        {
            return Ok(...);
        }

        [HttpGet("query"),HttpPost("query")]
        public IActionResult Query([FromBodyOrDefault]int id,[FromBodyOrDefault]string name)
        {
            return Ok(...);
        }

45. String SimHash Similarity Algorithm

var dis="12345678".HammingDistance("1234567");
var dis=new SimHash("12345678").HammingDistance(new SimHash("1234567"));

46. Real File Type Detection

// Multiple ways, choose as you like
var detector=new FileInfo(filepath).DetectFiletype();
//var detector=File.OpenRead(filepath).DetectFiletype();
//var detector=FileSignatureDetector.DetectFiletype(filepath);
detector.Precondition;//Basic file type
detector.Extension;//Real extension
detector.MimeType;//MimeType
detector.FormatCategories;//Format category

Default Supported File Types

Extension Description
3GP 3GPP, 3GPP 2
7Z 7-Zip
APK ZIP based Android Package
AVI Audio-Video Interleave
SH Shell Script
BPLIST Binary Property List
BMP, DIB Bitmap
BZ2 Bunzip2 Compressed
CAB Microsoft Cabinet
CLASS Java Bytecode
CONFIG .NET Configuration File
CRT, CERT Certificate
CUR Cursor
DB Windows Thumbs.db Thumbnail Database
DDS DirectDraw Surface
DLL Windows Dynamic Linkage Library
DMG Apple Disk Mount Image
DMP Windows Memory Dump File
DOC Microsoft Office Word 97-2003 Document
DOCX Microsoft Office Word OpenXML Document
EPUB e-Pub Document
EXE Windows Executive
FLAC Loseless Audio
FLV Flash Video
GIF Graphics Interchage Format
GZ GZ Compressed
HDP HD Photo(JPEG XR) Image
HWP Legacy HWP, HWPML, CFBF HWP
ICO Icon
INI Initialization File
ISO ISO-9660 Disc Image
LNK Windows Shortcut Link
JP2 JPEG 2000 Image
JPG, JPEG Joint Photographic Experts Group Image
LZH LZH Compressed
M4A MP4 Container Contained Audio Only
M4V MP4 Container Contained Video
MID Midi Sound
MKA Matroska Container Contained Audio Only
MKV Matroska Container Contained Video
MOV QuickTime Movie Video
MP4 MP4 Container Contained Contents
MSI Microsoft Installer
OGG OGG Video or Audio
ODF OpenDocument Formula
ODG OpenDocument Graphics
ODP OpenDocument Presentation
ODS OpenDocument Spreadsheet
ODT OpenDocument Text
PAK PAK Archive or Quake Archive
PDB Microsoft Program Database
PDF Portable Document Format
PFX Microsoft Personal Information Exchange Certificate
PNG Portable Network Graphics Image
PPT Microsoft Office PowerPoint 97-2003 Document
PPTX Microsoft Office PowerPoint OpenXML Document
PPSX Microsoft Office PowerPoint OpenXML Document for Slideshow only
PSD Photoshop Document
RAR WinRAR Compressed
REG Windows Registry
RPM RedHat Package Manager Package
RTF Rich Text Format Document
SLN Microsoft Visual Studio Solution
SRT SubRip Subtitle
SWF Shockwave Flash
SQLITE, DB SQLite Database
TAR pre-ISO Type and UStar Type TAR Package
TIFF Tagged Image File Format Image
TXT Plain Text
WAV Wave Audio
WASM Binary WebAssembly
WEBM WebM Video
WEBP WebP Image
XAR XAR Package
XLS Microsoft Office Excel 97-2003 Document
XLSX Microsoft Office Excep OpenXML Document
XML Extensible Markup Language Document
Z Z Compressed
ZIP ZIP Package

Asp.Net MVC and ASP.NET Core ResumeFileResult Supporting Resumable Download and Multi-threaded Download

Provides resumable download and multi-threaded download support when transmitting file data through MVC/WebAPI applications in ASP.NET Core.

It provides ETag and Last-Modified headers. It also supports the following precondition headers: If-Match, If-None-Match, If-Modified-Since, If-Unmodified-Since, If-Range.

Supports ASP.NET Core 2.0+

From .NET Core 2.0, ASP.NET Core natively supports resumable download. So this is just an extension to FileResult. Only the "Content-Disposition" Inline part is left. All code relies on underlying .NET classes.

How to Use

.NET Framework

In your controller, you can use it like FileResult.

using Masuit.Tools.Mvc;
using Masuit.Tools.Mvc.ResumeFileResult;
private readonly MimeMapper mimeMapper=new MimeMapper(); // Dependency injection recommended

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

To use ResumeFileResults, you must configure the service in the ConfigureServices method of Startup.cs:

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

Then in your controller, you can use it like FileResult.

Click to view code
using Masuit.Tools.AspNetCore.ResumeFileResults.Extensions;
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;
}

The above examples will provide "Content-Disposition: attachment" for your data. When no fileName is provided, data will be provided as "Content-Disposition: inline". Additionally, it can provide ETag and LastModified headers.

[HttpGet("virtual/{fileName}")]
public IActionResult VirtualFile(bool fileName)
{
    var result = new ResumeVirtualFileResult("TestFile.txt", "text/plain")
    {
        FileInlineName = "TestFile.txt",
        LastModified = _lastModified
    };
    return result;
}

Full-text search engine based on EntityFrameworkCore and Lucene.NET: Masuit.LuceneEFCore.SearchEngine

Open-source blog system: Masuit.MyBlogs

Keep Exploring

Related Reading

More Articles