ow
This commit is contained in:
commit
f05d2d215f
678 files changed
+73865
No files matched your search
@@ -0,0 +1,117 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Data;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace CloudBuilder.Core.Utility
|
||||
{
|
||||
public class DataTableConvertor
|
||||
{
|
||||
/// <summary>
|
||||
/// DataTable转对象
|
||||
/// </summary>
|
||||
/// <param name="dt"></param>
|
||||
/// <returns></returns>
|
||||
public static List<T> ConvertToList<T>(DataTable dt) where T : new()
|
||||
{
|
||||
// 定义集合
|
||||
var list = new List<T>();
|
||||
|
||||
if (dt == null || 0 == dt.Rows.Count)
|
||||
{
|
||||
return list;
|
||||
}
|
||||
|
||||
// 获得此模型的可写公共属性
|
||||
IEnumerable<PropertyInfo> propertys = typeof(T).GetProperties().Where(u => u.CanWrite);
|
||||
list = ConvertToEntity<T>(dt, propertys);
|
||||
|
||||
return list;
|
||||
}
|
||||
private static List<T> ConvertToEntity<T>(DataTable dt, IEnumerable<PropertyInfo> propertys) where T : new()
|
||||
{
|
||||
var list = new List<T>();
|
||||
|
||||
//遍历DataTable中所有的数据行
|
||||
foreach (DataRow dr in dt.Rows)
|
||||
{
|
||||
if (dr.RowState == DataRowState.Deleted) continue;
|
||||
|
||||
var entity = new T();
|
||||
|
||||
//遍历该对象的所有属性
|
||||
foreach (PropertyInfo p in propertys)
|
||||
{
|
||||
//将属性名称赋值给临时变量
|
||||
string tmpName = p.Name;
|
||||
|
||||
//检查DataTable是否包含此列(列名==对象的属性名)
|
||||
if (!dt.Columns.Contains(tmpName)) continue;
|
||||
//取值
|
||||
object value = dr[tmpName];
|
||||
|
||||
//如果非空,则赋给对象的属性
|
||||
if (value != DBNull.Value)
|
||||
{
|
||||
p.SetValue(entity, value, null);
|
||||
}
|
||||
}
|
||||
//对象添加到泛型集合中
|
||||
list.Add(entity);
|
||||
}
|
||||
return list;
|
||||
}
|
||||
/// <summary>
|
||||
/// 对象转DataTable
|
||||
/// </summary>
|
||||
/// <param name="list"></param>
|
||||
/// <returns></returns>
|
||||
public static DataTable ToDataTable<T>(IList<T> list)
|
||||
{
|
||||
return ToDataTable<T>(list, false);
|
||||
}
|
||||
|
||||
public static DataTable ToDataTable<T>(IList<T> list, bool blankOption)
|
||||
{
|
||||
Type elementType = typeof(T);
|
||||
|
||||
var t = new DataTable();
|
||||
|
||||
elementType.GetProperties().ToList().ForEach(propInfo => t.Columns.Add(propInfo.Name, Nullable.GetUnderlyingType(propInfo.PropertyType) ?? propInfo.PropertyType));
|
||||
|
||||
if (blankOption)
|
||||
{
|
||||
var row = t.NewRow();
|
||||
t.Rows.Add(row);
|
||||
}
|
||||
|
||||
foreach (T item in list)
|
||||
{
|
||||
var row = t.NewRow();
|
||||
elementType.GetProperties().ToList().ForEach(propInfo => row[propInfo.Name] = propInfo.GetValue(item, null) ?? DBNull.Value);
|
||||
t.Rows.Add(row);
|
||||
}
|
||||
return t;
|
||||
}
|
||||
|
||||
public static DataTable ToDataTable<T>(T[] list)
|
||||
{
|
||||
Type elementType = typeof(T);
|
||||
|
||||
var t = new DataTable();
|
||||
|
||||
elementType.GetProperties().ToList().ForEach(propInfo => t.Columns.Add(propInfo.Name, Nullable.GetUnderlyingType(propInfo.PropertyType) ?? propInfo.PropertyType));
|
||||
foreach (T item in list)
|
||||
{
|
||||
var row = t.NewRow();
|
||||
elementType.GetProperties().ToList().ForEach(propInfo => row[propInfo.Name] = propInfo.GetValue(item, null) ?? DBNull.Value);
|
||||
t.Rows.Add(row);
|
||||
}
|
||||
return t;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
using System.Xml.Serialization;
|
||||
|
||||
namespace CloudBuilder.Util
|
||||
{
|
||||
public class DeserializeHelper<T>
|
||||
{
|
||||
public static T Deserialize(string xml)
|
||||
{
|
||||
StringReader textReader = new StringReader(xml);
|
||||
XmlSerializer serializer = new XmlSerializer(typeof(T));
|
||||
return (T)serializer.Deserialize(textReader);
|
||||
}
|
||||
|
||||
public static string LoadXml(string filePath)
|
||||
{
|
||||
if (!File.Exists(filePath)) return null;
|
||||
return File.ReadAllText(Path.Combine(filePath));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Security.Cryptography;
|
||||
using System.Web;
|
||||
|
||||
namespace System
|
||||
{
|
||||
public static class EncryptHelper
|
||||
{
|
||||
/// <summary>
|
||||
/// AES秘钥(强烈要求.json自定义取出)
|
||||
/// </summary>
|
||||
private const string AesKey = "OwenItc-*&$%RTj3";
|
||||
|
||||
/// <summary>
|
||||
/// AES偏移向量(强烈要求.json自定义取出)
|
||||
/// </summary>
|
||||
private const string AesIV = "*&^KJ8a-Tj3*&$%R";
|
||||
|
||||
public static string AesEncrypt(string toEncryptString)
|
||||
{
|
||||
return AesEncrypt(toEncryptString, AesKey, AesIV);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// AES加密
|
||||
/// </summary>
|
||||
/// <param name="toEncryptString">待加密的明文</param>
|
||||
/// <returns></returns>
|
||||
public static string AesEncrypt(this string toEncryptString, string aesKey, string aesIV)
|
||||
{
|
||||
var rijndaelCipher = new RijndaelManaged
|
||||
{
|
||||
Mode = CipherMode.CBC,
|
||||
Padding = PaddingMode.PKCS7,
|
||||
KeySize = 128,
|
||||
BlockSize = 128
|
||||
};
|
||||
|
||||
var toEncryptBytes = Encoding.UTF8.GetBytes(toEncryptString);
|
||||
var keyBytes = Encoding.UTF8.GetBytes(aesKey);
|
||||
var ivBytes = Encoding.UTF8.GetBytes(aesIV);
|
||||
|
||||
rijndaelCipher.Key = keyBytes;
|
||||
rijndaelCipher.IV = ivBytes;
|
||||
|
||||
ICryptoTransform transform = rijndaelCipher.CreateEncryptor();
|
||||
byte[] cipherBytes = transform.TransformFinalBlock(toEncryptBytes, 0, toEncryptBytes.Length);
|
||||
return Convert.ToBase64String(cipherBytes);
|
||||
}
|
||||
|
||||
public static string AesDecrpt(string toDecrpt)
|
||||
{
|
||||
return AesDecrpt(toDecrpt, AesKey, AesIV);
|
||||
}
|
||||
/// <summary>
|
||||
/// AES解密
|
||||
/// </summary>
|
||||
/// <param name="toDecrpt"></param>
|
||||
/// <returns></returns>
|
||||
public static string AesDecrpt(this string toDecrpt, string aesKey, string aesIV)
|
||||
{
|
||||
var key = Encoding.UTF8.GetBytes(aesKey);
|
||||
var encryptedData = Convert.FromBase64String(toDecrpt);
|
||||
var iv = Encoding.UTF8.GetBytes(aesIV);
|
||||
|
||||
var rDel = new RijndaelManaged
|
||||
{
|
||||
Key = key,
|
||||
IV = iv,
|
||||
Mode = CipherMode.CBC,
|
||||
Padding = PaddingMode.PKCS7
|
||||
};
|
||||
|
||||
var cTransform = rDel.CreateDecryptor();
|
||||
var resultArray = cTransform.TransformFinalBlock(encryptedData, 0, encryptedData.Length);
|
||||
|
||||
return Encoding.UTF8.GetString(resultArray);
|
||||
}
|
||||
|
||||
public static string UrlEncode(string toEncryptString)
|
||||
{
|
||||
if (string.IsNullOrEmpty(toEncryptString)) return null;
|
||||
return HttpUtility.UrlEncode(AesEncrypt(toEncryptString), Encoding.UTF8);
|
||||
}
|
||||
|
||||
public static string UrlDecode(string toEncryptString)
|
||||
{
|
||||
if (string.IsNullOrEmpty(toEncryptString)) return null;
|
||||
return AesDecrpt(toEncryptString);
|
||||
}
|
||||
|
||||
public static string Decode(string toEncryptString)
|
||||
{
|
||||
if (string.IsNullOrEmpty(toEncryptString)) return null;
|
||||
return AesDecrpt(HttpUtility.UrlDecode(toEncryptString, Encoding.UTF8));
|
||||
}
|
||||
|
||||
public static string GetMd5(this string source)
|
||||
{
|
||||
if (string.IsNullOrEmpty(source)) return null;
|
||||
|
||||
MD5 md = MD5.Create();
|
||||
|
||||
byte[] bytes = md.ComputeHash(System.Text.Encoding.UTF8.GetBytes(source));
|
||||
|
||||
string dec = null;
|
||||
|
||||
foreach (byte b in bytes)
|
||||
{
|
||||
|
||||
dec += b.ToString("x2");
|
||||
|
||||
}
|
||||
|
||||
return dec;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
|
||||
/// <summary>
|
||||
/// 文件日志写入封装类,自动按日期分文件、超大小自动切割
|
||||
/// </summary>
|
||||
public class FileLog
|
||||
{
|
||||
private readonly IConfiguration _configuration;
|
||||
private readonly object _lockObj = new object(); // 独立锁,不依赖外部this
|
||||
|
||||
public FileLog(IConfiguration configuration)
|
||||
{
|
||||
_configuration = configuration ?? throw new ArgumentNullException(nameof(configuration));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 写入日志内容到文件
|
||||
/// </summary>
|
||||
/// <param name="log">日志文本</param>
|
||||
public void WriteToFile(string log)
|
||||
{
|
||||
try
|
||||
{
|
||||
// 读取配置,转换最大文件字节数
|
||||
int maxMb = _configuration.GetValue<int>("Logging:MaxFileSizeMB");
|
||||
long maxFileSizeBytes = (long)maxMb * 1024 * 1024;
|
||||
|
||||
string today = DateTime.Now.ToString("yyyyMMdd");
|
||||
string basePath = _configuration["Logging:LogFileDirectory"];
|
||||
Directory.CreateDirectory(basePath);
|
||||
|
||||
string baseFileName = $"log-{today}";
|
||||
string ext = ".log";
|
||||
string finalPath = Path.Combine(basePath, $"{baseFileName}{ext}");
|
||||
int index = 1;
|
||||
|
||||
// 文件超限则递增后缀切割
|
||||
while (File.Exists(finalPath) && new FileInfo(finalPath).Length >= maxFileSizeBytes)
|
||||
{
|
||||
finalPath = Path.Combine(basePath, $"{baseFileName}-{index}{ext}");
|
||||
index++;
|
||||
}
|
||||
|
||||
// 线程安全写入
|
||||
lock (_lockObj)
|
||||
{
|
||||
File.AppendAllText(finalPath, log + Environment.NewLine, Encoding.UTF8);
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// 原逻辑吞掉所有异常,如需日志可自行增加内部异常输出
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,147 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace System
|
||||
{
|
||||
/// <summary>
|
||||
/// 对象属性值复制
|
||||
/// </summary>
|
||||
public class ObjectCopyHelper
|
||||
{
|
||||
/// <summary>
|
||||
/// 属性映射(静态对象,无需重复建立属性映射关系,提高效率)
|
||||
/// </summary>
|
||||
public static Dictionary<string, List<string>> MapDic = new Dictionary<string, List<string>>();
|
||||
/// <summary>
|
||||
/// S复制到D(创建对象D)
|
||||
/// </summary>
|
||||
/// <typeparam name="D">输出对象类型</typeparam>
|
||||
/// <typeparam name="S">输入对象类型</typeparam>
|
||||
/// <param name="s">输入对象</param>
|
||||
/// <returns></returns>
|
||||
public static D Copy<S, D>(S s)
|
||||
where D : class, new()
|
||||
where S : class, new()
|
||||
{
|
||||
if (s == null)
|
||||
{
|
||||
return default(D);
|
||||
}
|
||||
//使用无参数构造函数,创建指定泛型类型参数所指定类型的实例
|
||||
D d = Activator.CreateInstance<D>();
|
||||
return Copy<S, D>(s, d);
|
||||
}
|
||||
/// <summary>
|
||||
/// S复制到D(对象D已存在)
|
||||
/// </summary>
|
||||
/// <typeparam name="D">输出对象类型</typeparam>
|
||||
/// <typeparam name="S">输入对象类型</typeparam>
|
||||
/// <param name="s">输入对象</param>
|
||||
/// <param name="d">输出对象</param>
|
||||
/// <returns></returns>
|
||||
public static D Copy<S, D>(S s, D d)
|
||||
where D : class, new()
|
||||
where S : class, new()
|
||||
{
|
||||
if (s == null || d == null)
|
||||
{
|
||||
return d;
|
||||
}
|
||||
try
|
||||
{
|
||||
var sType = s.GetType();
|
||||
var dType = typeof(D);
|
||||
//属性映射Key
|
||||
string mapkey = dType.FullName + "_" + sType.FullName;
|
||||
if (MapDic.ContainsKey(mapkey))
|
||||
{
|
||||
//已存在属性映射
|
||||
foreach (var item in MapDic[mapkey])
|
||||
{
|
||||
//按照属性映射关系赋值
|
||||
//.net 4
|
||||
//dType.GetProperty(item).SetValue(d, sType.GetProperty(item).GetValue(s, null), null);
|
||||
//.net 4.5
|
||||
dType.GetProperty(item).SetValue(d, sType.GetProperty(item).GetValue(s));
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
//不存在属性映射,需要建立属性映射
|
||||
List<string> namelist = new List<string>();
|
||||
Dictionary<string, TypeAndValue> dic = new Dictionary<string, TypeAndValue>();
|
||||
//遍历获取输入类型的属性(属性名称,类型,值)
|
||||
foreach (PropertyInfo sP in sType.GetProperties())
|
||||
{
|
||||
//.net 4
|
||||
//dic.Add(sP.Name, new TypeAndValue() { type = sP.PropertyType, value = sP.GetValue(s, null) });
|
||||
//.net 4.5
|
||||
dic.Add(sP.Name, new TypeAndValue() { type = sP.PropertyType, value = sP.GetValue(s) });
|
||||
}
|
||||
//遍历输出类型的属性,并与输入类型(相同名称和类型的属性)建立映射,并赋值
|
||||
foreach (PropertyInfo dP in dType.GetProperties())
|
||||
{
|
||||
if (dic.Keys.Contains(dP.Name))
|
||||
{
|
||||
if (dP.PropertyType == dic[dP.Name].type)
|
||||
{
|
||||
namelist.Add(dP.Name);
|
||||
//.net 4
|
||||
//dP.SetValue(d, dic[dP.Name].value, null);
|
||||
//.net 4.5
|
||||
dP.SetValue(d, dic[dP.Name].value);
|
||||
}
|
||||
}
|
||||
}
|
||||
//保存映射
|
||||
if (!MapDic.ContainsKey(mapkey))
|
||||
{
|
||||
MapDic.Add(mapkey, namelist);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
throw ex;
|
||||
}
|
||||
return d;
|
||||
}
|
||||
/// <summary>
|
||||
/// SList复制到DList
|
||||
/// </summary>
|
||||
/// <typeparam name="D">输出对象类型</typeparam>
|
||||
/// <typeparam name="S">输入对象类型</typeparam>
|
||||
/// <param name="sList">输入对象集合</param>
|
||||
/// <returns></returns>
|
||||
public static IQueryable<D> Copy<S, D>(IQueryable<S> sList)
|
||||
where D : class, new()
|
||||
where S : class, new()
|
||||
{
|
||||
List<D> dList = new List<D>();
|
||||
foreach (var item in sList)
|
||||
{
|
||||
dList.Add(Copy<S, D>(item));
|
||||
}
|
||||
return dList.AsQueryable();
|
||||
}
|
||||
}
|
||||
/// <summary>
|
||||
/// 类型和值
|
||||
/// </summary>
|
||||
class TypeAndValue
|
||||
{
|
||||
/// <summary>
|
||||
/// 类型
|
||||
/// </summary>
|
||||
public Type type { get; set; }
|
||||
/// <summary>
|
||||
/// 值
|
||||
/// </summary>
|
||||
public object value { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,126 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
|
||||
namespace System
|
||||
{
|
||||
public class QuotedString
|
||||
{
|
||||
public static string ToQuotedString(string[] vs)
|
||||
{
|
||||
if (vs == null || vs.Length == 0) return null;
|
||||
|
||||
return ToQuotedDelimitedList(vs);
|
||||
}
|
||||
|
||||
public static string ToQuotedString(int[] vs)
|
||||
{
|
||||
if (vs == null || vs.Length == 0) return null;
|
||||
|
||||
return ToQuotedDelimitedList(vs.Select(x => x.ToString()).ToArray()); ;
|
||||
}
|
||||
|
||||
public const char DELIMITER_CHAR = ',';
|
||||
/// <summary>
|
||||
/// 字符串安全转数组,分隔符','
|
||||
/// </summary>
|
||||
/// <param name="t"></param>
|
||||
/// <returns></returns>
|
||||
public static string[] DelimiteredListToArray(string t)
|
||||
{
|
||||
return DelimiteredListToArray(t, DELIMITER_CHAR);
|
||||
}
|
||||
|
||||
public static string[] DelimiteredListToArray(string t, char delim)
|
||||
{
|
||||
if (string.IsNullOrEmpty(t)) return null;
|
||||
return t.Split(delim);
|
||||
}
|
||||
/// <summary>
|
||||
/// 字符串安全转数组,分隔符','
|
||||
/// </summary>
|
||||
/// <param name="t"></param>
|
||||
/// <returns></returns>
|
||||
public static List<string> DelimiteredListToList(string t)
|
||||
{
|
||||
return DelimiteredListToList(t, DELIMITER_CHAR);
|
||||
}
|
||||
|
||||
public static List<string> DelimiteredListToList(string t, char delim)
|
||||
{
|
||||
if (string.IsNullOrEmpty(t)) return null;
|
||||
|
||||
List<string> l = new List<string>();
|
||||
l.AddRange(DelimiteredListToArray(t, delim));
|
||||
return l;
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// 数组变成字符串
|
||||
/// </summary>
|
||||
/// <param name="s"></param>
|
||||
/// <returns></returns>
|
||||
public static string ToDelimiteredList(string[] s)
|
||||
{
|
||||
return ToDelimiteredList(s, DELIMITER_CHAR);
|
||||
}
|
||||
|
||||
public static string ToQuotedDelimitedList(string[] s)
|
||||
{
|
||||
StringBuilder b = new StringBuilder();
|
||||
|
||||
foreach (string t in s)
|
||||
{
|
||||
if (b.Length > 0) b.Append(DELIMITER_CHAR);
|
||||
b.Append('\'' + t + '\'');
|
||||
}
|
||||
|
||||
return b.ToString();
|
||||
}
|
||||
|
||||
public static string ToDelimiteredList(string[] s, char delim)
|
||||
{
|
||||
StringBuilder b = new StringBuilder();
|
||||
foreach (string t in s)
|
||||
{
|
||||
b.Append(delim + t);
|
||||
}
|
||||
|
||||
string r = b.ToString();
|
||||
if (r.StartsWith(delim.ToString())) r = r.Substring(1);
|
||||
|
||||
return r;
|
||||
}
|
||||
|
||||
public static string CreateRandomNumber(int NumCount)
|
||||
{
|
||||
string text = "0,1,2,3,4,5,6,7,8,9";
|
||||
string[] array = text.Split(',');
|
||||
string text2 = "";
|
||||
int num = -1;
|
||||
Random random = new Random();
|
||||
for (int i = 0; i < NumCount; i++)
|
||||
{
|
||||
if (num != -1)
|
||||
{
|
||||
random = new Random(i * num * (int)DateTime.Now.Ticks);
|
||||
}
|
||||
|
||||
int num2 = random.Next(array.Length - 1);
|
||||
if (num == num2)
|
||||
{
|
||||
return CreateRandomNumber(NumCount);
|
||||
}
|
||||
|
||||
num = num2;
|
||||
text2 += array[num2];
|
||||
}
|
||||
|
||||
return text2;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,295 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
using CryptographyRSA = System.Security.Cryptography.RSA; // 别名避免与当前类名冲突
|
||||
|
||||
namespace System
|
||||
{
|
||||
/// <summary>
|
||||
/// RSA操作类(跨平台实现)
|
||||
/// </summary>
|
||||
public class RSA
|
||||
{
|
||||
// 记录当前实例是否仅包含公钥(替代原来的 rsa.PublicOnly)
|
||||
private bool publicOnly;
|
||||
|
||||
/// <summary>
|
||||
/// 导出XML格式密钥对,如果convertToPublic含私钥的RSA将只返回公钥,仅含公钥的RSA不受影响
|
||||
/// </summary>
|
||||
public string ToXML(bool convertToPublic = false)
|
||||
{
|
||||
bool includePrivate = !publicOnly && !convertToPublic;
|
||||
return rsa.ToXmlString(includePrivate);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 导出PEM PKCS#1格式密钥对,如果convertToPublic含私钥的RSA将只返回公钥,仅含公钥的RSA不受影响
|
||||
/// </summary>
|
||||
public string ToPEM_PKCS1(bool convertToPublic = false)
|
||||
{
|
||||
if (!publicOnly && !convertToPublic)
|
||||
{
|
||||
return rsa.ExportRSAPrivateKeyPem();
|
||||
}
|
||||
return rsa.ExportRSAPublicKeyPem();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 导出PEM PKCS#8格式密钥对,如果convertToPublic含私钥的RSA将只返回公钥,仅含公钥的RSA不受影响
|
||||
/// </summary>
|
||||
public string ToPEM_PKCS8(bool convertToPublic = false)
|
||||
{
|
||||
if (!publicOnly && !convertToPublic)
|
||||
{
|
||||
return rsa.ExportPkcs8PrivateKeyPem();
|
||||
}
|
||||
return rsa.ExportSubjectPublicKeyInfoPem();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 加密字符串(utf-8),出错抛异常
|
||||
/// </summary>
|
||||
public string Encode(string str)
|
||||
{
|
||||
return RSA_Unit.Base64EncodeBytes(Encode(Encoding.UTF8.GetBytes(str)));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 加密数据,出错抛异常
|
||||
/// </summary>
|
||||
public byte[] Encode(byte[] data)
|
||||
{
|
||||
int blockLen = rsa.KeySize / 8 - 11;
|
||||
if (data.Length <= blockLen)
|
||||
{
|
||||
return rsa.Encrypt(data, RSAEncryptionPadding.Pkcs1);
|
||||
}
|
||||
|
||||
using (var dataStream = new MemoryStream(data))
|
||||
using (var enStream = new MemoryStream())
|
||||
{
|
||||
byte[] buffer = new byte[blockLen];
|
||||
int len = dataStream.Read(buffer, 0, blockLen);
|
||||
|
||||
while (len > 0)
|
||||
{
|
||||
byte[] block = new byte[len];
|
||||
Array.Copy(buffer, 0, block, 0, len);
|
||||
|
||||
byte[] enBlock = rsa.Encrypt(block, RSAEncryptionPadding.Pkcs1);
|
||||
enStream.Write(enBlock, 0, enBlock.Length);
|
||||
|
||||
len = dataStream.Read(buffer, 0, blockLen);
|
||||
}
|
||||
|
||||
return enStream.ToArray();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 解密字符串(utf-8),解密异常返回null
|
||||
/// </summary>
|
||||
public string DecodeOrNull(string str)
|
||||
{
|
||||
if (string.IsNullOrEmpty(str))
|
||||
return null;
|
||||
|
||||
var bytes = RSA_Unit.Base64DecodeBytes(str);
|
||||
if (bytes == null)
|
||||
return null;
|
||||
|
||||
var decrypted = DecodeOrNull(bytes);
|
||||
if (decrypted == null)
|
||||
return null;
|
||||
|
||||
return Encoding.UTF8.GetString(decrypted);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 解密数据,解密异常返回null
|
||||
/// </summary>
|
||||
public byte[] DecodeOrNull(byte[] data)
|
||||
{
|
||||
try
|
||||
{
|
||||
int blockLen = rsa.KeySize / 8;
|
||||
if (data.Length <= blockLen)
|
||||
{
|
||||
return rsa.Decrypt(data, RSAEncryptionPadding.Pkcs1);
|
||||
}
|
||||
|
||||
using (var dataStream = new MemoryStream(data))
|
||||
using (var deStream = new MemoryStream())
|
||||
{
|
||||
byte[] buffer = new byte[blockLen];
|
||||
int len = dataStream.Read(buffer, 0, blockLen);
|
||||
|
||||
while (len > 0)
|
||||
{
|
||||
byte[] block = new byte[len];
|
||||
Array.Copy(buffer, 0, block, 0, len);
|
||||
|
||||
byte[] deBlock = rsa.Decrypt(block, RSAEncryptionPadding.Pkcs1);
|
||||
deStream.Write(deBlock, 0, deBlock.Length);
|
||||
|
||||
len = dataStream.Read(buffer, 0, blockLen);
|
||||
}
|
||||
|
||||
return deStream.ToArray();
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 对str进行签名,并指定hash算法(如:SHA256)
|
||||
/// </summary>
|
||||
public string Sign(string hash, string str)
|
||||
{
|
||||
return RSA_Unit.Base64EncodeBytes(Sign(hash, Encoding.UTF8.GetBytes(str)));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 对data进行签名,并指定hash算法(如:SHA256)
|
||||
/// </summary>
|
||||
public byte[] Sign(string hash, byte[] data)
|
||||
{
|
||||
return rsa.SignData(data, new HashAlgorithmName(hash), RSASignaturePadding.Pkcs1);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 验证字符串str的签名是否是sgin,并指定hash算法(如:SHA256)
|
||||
/// </summary>
|
||||
public bool Verify(string hash, string sgin, string str)
|
||||
{
|
||||
var bytes = RSA_Unit.Base64DecodeBytes(sgin);
|
||||
if (bytes == null)
|
||||
return false;
|
||||
|
||||
return Verify(hash, bytes, Encoding.UTF8.GetBytes(str));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 验证data的签名是否是sgin,并指定hash算法(如:SHA256)
|
||||
/// </summary>
|
||||
public bool Verify(string hash, byte[] sgin, byte[] data)
|
||||
{
|
||||
try
|
||||
{
|
||||
return rsa.VerifyData(data, sgin, new HashAlgorithmName(hash), RSASignaturePadding.Pkcs1);
|
||||
}
|
||||
catch
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private CryptographyRSA rsa;
|
||||
|
||||
public RSA() : this(2048)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 用指定密钥大小创建一个新的RSA,出错抛异常
|
||||
/// </summary>
|
||||
public RSA(int keySize)
|
||||
{
|
||||
rsa = CryptographyRSA.Create();
|
||||
rsa.KeySize = keySize;
|
||||
publicOnly = false; // 新生成的密钥对包含私钥
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 通过指定的密钥,创建一个RSA,xml内可以只包含一个公钥或私钥,或都包含,出错抛异常
|
||||
/// </summary>
|
||||
public RSA(string xml)
|
||||
{
|
||||
rsa = CryptographyRSA.Create();
|
||||
rsa.FromXmlString(xml);
|
||||
|
||||
// 判断是否仅包含公钥
|
||||
publicOnly = false;
|
||||
try
|
||||
{
|
||||
rsa.ExportParameters(true); // 能导出私钥参数说明含私钥
|
||||
}
|
||||
catch (CryptographicException)
|
||||
{
|
||||
publicOnly = true;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 通过一个pem文件创建RSA,pem为公钥或私钥,出错抛异常
|
||||
/// </summary>
|
||||
public RSA(string pem, bool noop)
|
||||
{
|
||||
rsa = CryptographyRSA.Create();
|
||||
|
||||
// 1. 兼容文件路径
|
||||
string pemContent = pem;
|
||||
if (!pemContent.Contains("BEGIN"))
|
||||
{
|
||||
pemContent = File.ReadAllText(pem);
|
||||
}
|
||||
|
||||
// 2. 提取 Base64 部分(忽略标签)
|
||||
var match = Regex.Match(pemContent,
|
||||
@"-----BEGIN [^-]+-----([\s\S]*?)-----END [^-]+-----",
|
||||
RegexOptions.Singleline);
|
||||
if (!match.Success)
|
||||
throw new CryptographicException("无效的 PEM 格式。");
|
||||
|
||||
string base64 = match.Groups[1].Value
|
||||
.Replace("\r", "")
|
||||
.Replace("\n", "")
|
||||
.Replace(" ", "")
|
||||
.Replace("\t", "")
|
||||
.Trim();
|
||||
|
||||
byte[] der = Convert.FromBase64String(base64);
|
||||
|
||||
// 3. 依次尝试常见 RSA 密钥格式(顺序可调整)
|
||||
// 先尝试 PKCS#8 私钥,再尝试 PKCS#1 私钥,然后公钥
|
||||
try
|
||||
{
|
||||
rsa.ImportPkcs8PrivateKey(der, out _);
|
||||
publicOnly = false;
|
||||
return;
|
||||
}
|
||||
catch (CryptographicException) { }
|
||||
|
||||
try
|
||||
{
|
||||
rsa.ImportRSAPrivateKey(der, out _);
|
||||
publicOnly = false;
|
||||
return;
|
||||
}
|
||||
catch (CryptographicException) { }
|
||||
|
||||
try
|
||||
{
|
||||
rsa.ImportSubjectPublicKeyInfo(der, out _);
|
||||
publicOnly = true;
|
||||
return;
|
||||
}
|
||||
catch (CryptographicException) { }
|
||||
|
||||
try
|
||||
{
|
||||
rsa.ImportRSAPublicKey(der, out _);
|
||||
publicOnly = true;
|
||||
return;
|
||||
}
|
||||
catch (CryptographicException) { }
|
||||
|
||||
throw new CryptographicException("无法识别的 RSA 密钥格式。");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,326 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace System
|
||||
{
|
||||
public class RSA_PEM
|
||||
{
|
||||
/// <summary>
|
||||
/// 用PEM格式密钥对创建RSA,支持PKCS#1、PKCS#8格式的PEM
|
||||
/// </summary>
|
||||
public static RSACryptoServiceProvider FromPEM(string pem)
|
||||
{
|
||||
var rsaParams = new CspParameters();
|
||||
rsaParams.Flags = CspProviderFlags.UseMachineKeyStore;
|
||||
var rsa = new RSACryptoServiceProvider(rsaParams);
|
||||
|
||||
var param = new RSAParameters();
|
||||
|
||||
var base64 = _PEMCode.Replace(pem, "");
|
||||
var data = RSA_Unit.Base64DecodeBytes(base64);
|
||||
if (data == null)
|
||||
{
|
||||
throw new Exception("PEM内容无效");
|
||||
}
|
||||
var idx = 0;
|
||||
|
||||
//读取长度
|
||||
Func<byte, int> readLen = (first) => {
|
||||
if (data[idx] == first)
|
||||
{
|
||||
idx++;
|
||||
if (data[idx] == 0x81)
|
||||
{
|
||||
idx++;
|
||||
return data[idx++];
|
||||
}
|
||||
else if (data[idx] == 0x82)
|
||||
{
|
||||
idx++;
|
||||
return (((int)data[idx++]) << 8) + data[idx++];
|
||||
}
|
||||
else if (data[idx] < 0x80)
|
||||
{
|
||||
return data[idx++];
|
||||
}
|
||||
}
|
||||
throw new Exception("PEM未能提取到数据");
|
||||
};
|
||||
//读取块数据
|
||||
Func<byte[]> readBlock = () => {
|
||||
var len = readLen(0x02);
|
||||
if (data[idx] == 0x00)
|
||||
{
|
||||
idx++;
|
||||
len--;
|
||||
}
|
||||
var val = data.sub(idx, len);
|
||||
idx += len;
|
||||
return val;
|
||||
};
|
||||
//比较data从idx位置开始是否是byts内容
|
||||
Func<byte[], bool> eq = (byts) => {
|
||||
for (var i = 0; i < byts.Length; i++, idx++)
|
||||
{
|
||||
if (idx >= data.Length)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if (byts[i] != data[idx])
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
|
||||
|
||||
|
||||
if (pem.Contains("PUBLIC KEY"))
|
||||
{
|
||||
/****使用公钥****/
|
||||
//读取数据总长度
|
||||
readLen(0x30);
|
||||
if (!eq(_SeqOID))
|
||||
{
|
||||
throw new Exception("PEM未知格式");
|
||||
}
|
||||
//读取1长度
|
||||
readLen(0x03);
|
||||
idx++;//跳过0x00
|
||||
//读取2长度
|
||||
readLen(0x30);
|
||||
|
||||
//Modulus
|
||||
param.Modulus = readBlock();
|
||||
|
||||
//Exponent
|
||||
param.Exponent = readBlock();
|
||||
}
|
||||
else if (pem.Contains("PRIVATE KEY"))
|
||||
{
|
||||
/****使用私钥****/
|
||||
//读取数据总长度
|
||||
readLen(0x30);
|
||||
|
||||
//读取版本号
|
||||
if (!eq(_Ver))
|
||||
{
|
||||
throw new Exception("PEM未知版本");
|
||||
}
|
||||
|
||||
//检测PKCS8
|
||||
var idx2 = idx;
|
||||
if (eq(_SeqOID))
|
||||
{
|
||||
//读取1长度
|
||||
readLen(0x04);
|
||||
//读取2长度
|
||||
readLen(0x30);
|
||||
|
||||
//读取版本号
|
||||
if (!eq(_Ver))
|
||||
{
|
||||
throw new Exception("PEM版本无效");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
idx = idx2;
|
||||
}
|
||||
|
||||
//读取数据
|
||||
param.Modulus = readBlock();
|
||||
param.Exponent = readBlock();
|
||||
param.D = readBlock();
|
||||
param.P = readBlock();
|
||||
param.Q = readBlock();
|
||||
param.DP = readBlock();
|
||||
param.DQ = readBlock();
|
||||
param.InverseQ = readBlock();
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new Exception("pem需要BEGIN END标头");
|
||||
}
|
||||
|
||||
rsa.ImportParameters(param);
|
||||
return rsa;
|
||||
}
|
||||
static private Regex _PEMCode = new Regex(@"--+.+?--+|\s+");
|
||||
static private byte[] _SeqOID = new byte[] { 0x30, 0x0D, 0x06, 0x09, 0x2A, 0x86, 0x48, 0x86, 0xF7, 0x0D, 0x01, 0x01, 0x01, 0x05, 0x00 };
|
||||
static private byte[] _Ver = new byte[] { 0x02, 0x01, 0x00 };
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// 将RSA中的密钥对转换成PEM格式,usePKCS8=false时返回PKCS#1格式,否则返回PKCS#8格式,如果convertToPublic含私钥的RSA将只返回公钥,仅含公钥的RSA不受影响
|
||||
/// </summary>
|
||||
public static string ToPEM(RSACryptoServiceProvider rsa, bool convertToPublic, bool usePKCS8)
|
||||
{
|
||||
//https://www.jianshu.com/p/25803dd9527d
|
||||
//https://www.cnblogs.com/ylz8401/p/8443819.html
|
||||
//https://blog.csdn.net/jiayanhui2877/article/details/47187077
|
||||
//https://blog.csdn.net/xuanshao_/article/details/51679824
|
||||
//https://blog.csdn.net/xuanshao_/article/details/51672547
|
||||
|
||||
var ms = new MemoryStream();
|
||||
//写入一个长度字节码
|
||||
Action<int> writeLenByte = (len) => {
|
||||
if (len < 0x80)
|
||||
{
|
||||
ms.WriteByte((byte)len);
|
||||
}
|
||||
else if (len <= 0xff)
|
||||
{
|
||||
ms.WriteByte(0x81);
|
||||
ms.WriteByte((byte)len);
|
||||
}
|
||||
else
|
||||
{
|
||||
ms.WriteByte(0x82);
|
||||
ms.WriteByte((byte)(len >> 8 & 0xff));
|
||||
ms.WriteByte((byte)(len & 0xff));
|
||||
}
|
||||
};
|
||||
//写入一块数据
|
||||
Action<byte[]> writeBlock = (byts) => {
|
||||
var addZero = (byts[0] >> 4) >= 0x8;
|
||||
ms.WriteByte(0x02);
|
||||
var len = byts.Length + (addZero ? 1 : 0);
|
||||
writeLenByte(len);
|
||||
|
||||
if (addZero)
|
||||
{
|
||||
ms.WriteByte(0x00);
|
||||
}
|
||||
ms.Write(byts, 0, byts.Length);
|
||||
};
|
||||
//根据后续内容长度写入长度数据
|
||||
Func<int, byte[], byte[]> writeLen = (index, byts) => {
|
||||
var len = byts.Length - index;
|
||||
|
||||
ms.SetLength(0);
|
||||
ms.Write(byts, 0, index);
|
||||
writeLenByte(len);
|
||||
ms.Write(byts, index, len);
|
||||
|
||||
return ms.ToArray();
|
||||
};
|
||||
|
||||
|
||||
if (rsa.PublicOnly || convertToPublic)
|
||||
{
|
||||
/****生成公钥****/
|
||||
var param = rsa.ExportParameters(false);
|
||||
|
||||
|
||||
//写入总字节数,不含本段长度,额外需要24字节的头,后续计算好填入
|
||||
ms.WriteByte(0x30);
|
||||
var index1 = (int)ms.Length;
|
||||
|
||||
//固定内容
|
||||
// encoded OID sequence for PKCS #1 rsaEncryption szOID_RSA_RSA = "1.2.840.113549.1.1.1"
|
||||
ms.writeAll(_SeqOID);
|
||||
|
||||
//从0x00开始的后续长度
|
||||
ms.WriteByte(0x03);
|
||||
var index2 = (int)ms.Length;
|
||||
ms.WriteByte(0x00);
|
||||
|
||||
//后续内容长度
|
||||
ms.WriteByte(0x30);
|
||||
var index3 = (int)ms.Length;
|
||||
|
||||
//写入Modulus
|
||||
writeBlock(param.Modulus);
|
||||
|
||||
//写入Exponent
|
||||
writeBlock(param.Exponent);
|
||||
|
||||
|
||||
//计算空缺的长度
|
||||
var byts = ms.ToArray();
|
||||
|
||||
byts = writeLen(index3, byts);
|
||||
byts = writeLen(index2, byts);
|
||||
byts = writeLen(index1, byts);
|
||||
|
||||
|
||||
return "-----BEGIN PUBLIC KEY-----\n" + RSA_Unit.TextBreak(RSA_Unit.Base64EncodeBytes(byts), 64) + "\n-----END PUBLIC KEY-----";
|
||||
}
|
||||
else
|
||||
{
|
||||
/****生成私钥****/
|
||||
var param = rsa.ExportParameters(true);
|
||||
|
||||
//写入总字节数,后续写入
|
||||
ms.WriteByte(0x30);
|
||||
int index1 = (int)ms.Length;
|
||||
|
||||
//写入版本号
|
||||
ms.writeAll(_Ver);
|
||||
|
||||
//PKCS8 多一段数据
|
||||
int index2 = -1, index3 = -1;
|
||||
if (usePKCS8)
|
||||
{
|
||||
//固定内容
|
||||
ms.writeAll(_SeqOID);
|
||||
|
||||
//后续内容长度
|
||||
ms.WriteByte(0x04);
|
||||
index2 = (int)ms.Length;
|
||||
|
||||
//后续内容长度
|
||||
ms.WriteByte(0x30);
|
||||
index3 = (int)ms.Length;
|
||||
|
||||
//写入版本号
|
||||
ms.writeAll(_Ver);
|
||||
}
|
||||
|
||||
//写入数据
|
||||
writeBlock(param.Modulus);
|
||||
writeBlock(param.Exponent);
|
||||
writeBlock(param.D);
|
||||
writeBlock(param.P);
|
||||
writeBlock(param.Q);
|
||||
writeBlock(param.DP);
|
||||
writeBlock(param.DQ);
|
||||
writeBlock(param.InverseQ);
|
||||
|
||||
|
||||
//计算空缺的长度
|
||||
var byts = ms.ToArray();
|
||||
|
||||
if (index2 != -1)
|
||||
{
|
||||
byts = writeLen(index3, byts);
|
||||
byts = writeLen(index2, byts);
|
||||
}
|
||||
byts = writeLen(index1, byts);
|
||||
|
||||
|
||||
var flag = " PRIVATE KEY";
|
||||
if (!usePKCS8)
|
||||
{
|
||||
flag = " RSA" + flag;
|
||||
}
|
||||
return "-----BEGIN" + flag + "-----\n" + RSA_Unit.TextBreak(RSA_Unit.Base64EncodeBytes(byts), 64) + "\n-----END" + flag + "-----";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace System
|
||||
{
|
||||
/// <summary>
|
||||
/// 封装的一些通用方法
|
||||
/// </summary>
|
||||
public class RSA_Unit
|
||||
{
|
||||
static public string Base64EncodeBytes(byte[] byts)
|
||||
{
|
||||
return Convert.ToBase64String(byts);
|
||||
}
|
||||
static public byte[] Base64DecodeBytes(string str)
|
||||
{
|
||||
try
|
||||
{
|
||||
return Convert.FromBase64String(str);
|
||||
}
|
||||
catch
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
/// <summary>
|
||||
/// 把字符串按每行多少个字断行
|
||||
/// </summary>
|
||||
static public string TextBreak(string text, int line)
|
||||
{
|
||||
var idx = 0;
|
||||
var len = text.Length;
|
||||
var str = new StringBuilder();
|
||||
while (idx < len)
|
||||
{
|
||||
if (idx > 0)
|
||||
{
|
||||
str.Append('\n');
|
||||
}
|
||||
if (idx + line >= len)
|
||||
{
|
||||
str.Append(text.Substring(idx));
|
||||
}
|
||||
else
|
||||
{
|
||||
str.Append(text.Substring(idx, line));
|
||||
}
|
||||
idx += line;
|
||||
}
|
||||
return str.ToString();
|
||||
}
|
||||
}
|
||||
|
||||
static public class Extensions
|
||||
{
|
||||
/// <summary>
|
||||
/// 从数组start开始到指定长度复制一份
|
||||
/// </summary>
|
||||
static public T[] sub<T>(this T[] arr, int start, int count)
|
||||
{
|
||||
T[] val = new T[count];
|
||||
for (var i = 0; i < count; i++)
|
||||
{
|
||||
val[i] = arr[start + i];
|
||||
}
|
||||
return val;
|
||||
}
|
||||
static public void writeAll(this Stream stream, byte[] byts)
|
||||
{
|
||||
stream.Write(byts, 0, byts.Length);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
using System.Text;
|
||||
using System.Xml.Serialization;
|
||||
|
||||
namespace CloudBuilder.Util
|
||||
{
|
||||
public class SerializeHelper
|
||||
{
|
||||
public static string Serialize(object o)
|
||||
{
|
||||
if (o == null) return null;
|
||||
|
||||
XmlSerializer serializer = new XmlSerializer(o.GetType());
|
||||
|
||||
using (MemoryStream stream = new MemoryStream())
|
||||
{
|
||||
StreamWriter writer = new StreamWriter(stream, Encoding.UTF8);
|
||||
serializer.Serialize((TextWriter)writer, o);
|
||||
stream.Seek(0L, SeekOrigin.Begin);
|
||||
StreamReader reader = new StreamReader(stream, Encoding.UTF8);
|
||||
return reader.ReadToEnd();
|
||||
}
|
||||
}
|
||||
|
||||
public static void WriteAllText(string filePath, object o)
|
||||
{
|
||||
File.WriteAllText(filePath, Serialize(o));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace System
|
||||
{
|
||||
public class TableName<T>
|
||||
{
|
||||
public TableName()
|
||||
{
|
||||
}
|
||||
|
||||
public const string VALUE = "Value";
|
||||
public const string DISPLAY_NAME = "DisplayName";
|
||||
public virtual string Selected { get; set; }
|
||||
public virtual T Value { get; set; }
|
||||
public virtual string DisplayName { get; set; }
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user