本文将使用C#从0到1带你新建一个简单的框架
注:本文不是最好的框架,只是用于给新手学习的简单框架,当然,小型项目用起来也没有问题。
首先,我们需要以下软件:
- Visual Studio 2022(本文将使用.Net 6,需要至少为Visual Studio 2022)
- SQLserver 2008 R2及以上
- Redis或Redis for Windows
- Navicat或SQL Server Management Studio(SSMS)
- 选择性使用:PDManer(PDMan数据库建模 (pdmaner.com))
其次,我们将使用以下开源组件
- Furion【依赖极少的开源组件,已经帮你完成了绝大部分的帮助类,包括:日志、Swagger,JWT鉴权等】(让 .NET 开发更简单,更通用,更流行。 Furion | Furion (baiqian.ltd))
- Sqlsugar【国产超级ORM,比EFCore更快,支持所有主流数据库和绝大部分国产数据库,支持百万数据写入和亿级查询,支持自动按照租户分库,支持自动分表分库】(SqlSugar .Net ORM 5.X 官网 、文档、教程 - SqlSugar 5x - .NET果糖网 (donet5.com))
- CSRedis【由FreeSQL作者开发的开源免费的Redis库,内置了常用的Redis帮助方法】(github.com/2881099/csr...
- LazyCaptcha【一个开源的图片验证库,支持多种图片验证码】 (LazyCaptcha: 仿EasyCaptcha的.net core 下的图形验证码 (gitee.com))
- IP2Region.Net【开源IP地址库,可以快速查询IP地址所属地】(ip2region/binding/csharp at master · lionsoul2014/ip2region (github.com))
- UAParser【开源UA信息读取库,解析当前用户所使用的浏览器、设备等信息】(github.com/ua-parser/u...
- Yitter.IdGenerator【雪花Id生成库,可以快速生成唯一数字Id】(yitter/IdGenerator: 💎多语言实现,高性能生成唯一数字ID。 💎优化的雪花算法(SnowFlake)------雪花漂移算法,在缩短ID长度的同时,具备极高瞬时并发处理能力(50W/0.1s)。 💎原生支持 C#/Java/Go/Rust/C/JavaScript/TypeScript/Python/Pascal 多语言,提供其它适用于其它语言的多线程安全调用动态库(FFI)。💎支持容器环境自动扩容(自动注册 WorkerId ),单机或分布式唯一IdGenerator。💎顶尖优化,超强效能。 (github.com))
- 选择性使用:Aliyun.OSS.NetCore或Tencent.QCloud.Cos.Sdk【阿里云或腾讯云对象存储SDK】
本文默认你已经学会了C#和Visual Studio的基础操作,如果你不会,建议先保存本文,然后先去学一下基础知识。
建立项目
- 项目分层()
名称 | 作用 | 引用层 |
---|---|---|
YourProject.Core | 此层为各种帮助方法 | YourProject.Globals |
YourProject.Globals | 此层为全局使用的一些静态配置等 | 无 |
YourProject.Entities | 此层为实体和传输对象 | YourProject.Core,YourProject.Globals |
YourProject.Service | 此层为服务所在层 | YourProject.Core,YourProject.Globals,YourProject.Entities |
YourProject.Web.Core | 此层用于API | YourProject.Globals,YourProject.Web.Core,YourProject.Entities,YourProject.Service |
YourProject.Web | 此层仅用于启动和前端页面 | YourProject.Globals,YourProject.Web.Core |
YourProject.CodeFirst | 【选用】此层仅用于使用CodeFirst时自动向数据库注入基础数据 | YourProject.Globals,YourProject.Web.Core |
开始项目
我们需要在YourProject.Web.Core新建类:Startup.cs
Csharp
/// <summary>
/// 注册启动服务
/// </summary>
[AppStartup(1)]//《==此处为Furion中的启动配置,用于在配置启动时需要的注册
public class StartUp : AppStartup
{
public void ConfigureService(IServiceCollection services)
{
}
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
}
}
其次在YourProject.Web的Program.cs中,将所有代码注释后,写入:
Csharp
Serve.Run();
接下来,我们需要配置SQLSugar,因为接下来的日志组件需要使用:
Csharp
/// <summary>
/// Sqlsugar配置
/// </summary>
/// <typeparam name="T"></typeparam>
public class SugarDbContext<T> where T : class, new()
{
protected static readonly SqlSugarScope DbInstance = new(new ConnectionConfig()
{
DbType = DbType.SqlServer,//你的数据库类型
ConnectionString = App.Configuration["ConnectionStrings:DefaultConnectionString"],//连接字符串
IsAutoCloseConnection = true,
InitKeyType = InitKeyType.Attribute,
LanguageType = LanguageType.Default,
ConfigureExternalServices = new ConfigureExternalServices()//其他配置
{
EntityNameService = (type, entity) =>//表配置
{
entity.IsDisabledUpdateAll = true;//禁用全表升级
entity.IsDisabledDelete = true;//禁用删除
}
}
},
db =>
{
db.CodeFirst.SetStringDefaultLength(255).InitTables<T>();//设置代码优先,并配置默认字段长度255,并初始化
db.Ado.CommandTimeOut = 120;//执行超时时间
db.Aop.OnLogExecuting = (sql, pars) =>//在SQL执行前执行
{
if (App.Configuration["AppSettings:IsDev"].ToBool())
{
var sqlString = WriteSQL(sql, pars);
Console.ForegroundColor = ConsoleColor.Yellow;
Console.WriteLine(sqlString);
sqlString.LogDebug();
Console.ForegroundColor = ConsoleColor.White;
}
};
db.Aop.OnError = (execption) =>//SQL执行出错
{
string errorInfo = $"SQL执行出错!【SQL语句】:{execption.Sql}\r\n【消息内容】:{execption.Message}";
errorInfo.LogError();
};
db.Aop.OnExecutingChangeSql = (sql, pars) => //可以修改SQL和参数的值
{
return new KeyValuePair<string, SugarParameter[]>(sql, pars);
};
db.Aop.DataExecuting = (oldValue, entityInfo) =>//执行期间自动执行
{
//添加时自动添加创建时间
if (entityInfo.PropertyName == "CreateTime" && entityInfo.OperationType == DataFilterType.InsertByObject)
{
entityInfo.SetValue(DateTime.Now);
}
//修改时自动添加修改时间
if (entityInfo.PropertyName == "UpdateTime" && entityInfo.OperationType == DataFilterType.UpdateByObject)
{
entityInfo.SetValue(DateTime.Now);
}
};
db.Aop.OnDiffLogEvent = it =>//差异日志
{
var log = "差异日志:";
log += "BeforeData:" + it.BeforeData + "\r\n";//操作前的值(如果是Insert操作则为NULL),
log += "AfterData" + it.AfterData + "\r\n"; //操作后的值,包含:字段描述 列名 值 表名 表描述
log += "SQL:" + it.Sql + "\r\n"; //错误SQL
log += "Parameters:" + it.Parameters + "\r\n";//参数
log += "Data:" + it.BusinessData + "\r\n"; //传入的值
log += "ExecTime:" + it.Time + "\r\n"; //执行时间
log += "DiffType:" + it.DiffType + "\r\n"; //操作类型 Enum类型,有Insert,Update
log.LogInformation();
};
db.Aop.OnLogExecuted = (sql, pars) =>//执行完成后的操作
{
int sqlExecTimeout = App.Configuration["ConnectionStrings:MaxExecTime"].ToInt32(); //在配置文件中设置最大等待SQL执行时间
sqlExecTimeout = sqlExecTimeout > 0 ? sqlExecTimeout : 1;//赋值默认值
double i = db.Ado.SqlExecutionTime.TotalMilliseconds;
if (i > sqlExecTimeout * 1000)//如果SQL执行缓慢,则写入日志
{
string sqlString = WriteSQL(sql, pars);
Console.ForegroundColor = ConsoleColor.Yellow;
("SQL语句:" + sqlString + "执行缓慢,用时" + i + "毫秒,请排查").LogWarning();
Console.ForegroundColor = ConsoleColor.White;
}
};
});
/// <summary>
/// 输出赋值后的SQL
/// </summary>
/// <param name="sql"></param>
/// <param name="pars"></param>
/// <returns></returns>
private static string WriteSQL(string sql, SugarParameter[] pars)
{
//查看赋值后的SQL语句,仅供测试时使用
StringBuilder sb_sql = new(sql);
var tempOrderPars = pars.Where(p => p.Value != null).OrderByDescending(p => p.ParameterName.Length).ToList();//防止 @par1错误替换@par12
for (var index = 0; index < tempOrderPars.Count; index++)
{
sb_sql.Replace(tempOrderPars[index].ParameterName, "'" + tempOrderPars[index].Value.ToString() + "'");
}
return sb_sql.ToString();
}
}
其次,我们需要日志组件,使用.Net自带的Log,配合Furion进行配置:
【选用】格式化控制台输出的内容
Csharp
//添加控制台格式化输出
services.AddConsoleFormatter(options =>
{
options.DateFormat = "yyyy-MM-dd HH:mm:ss.fffffff zzz dddd";//格式化输出日期
options.ColorBehavior = LoggerColorBehavior.Enabled;//启用颜色美化
options.WithTraceId = true;//输出TraceId
options.WithStackFrame = true;//输出日志
options.WriteHandler = (logMsg, scopeProvider, writer, fmtMsg, opt) =>//自定义日志输出程序
{
writer.WriteLine(fmtMsg);
};
options.MessageFormat = (logMsg) =>
{
if (logMsg.LogLevel == LogLevel.Warning)
{
Console.ForegroundColor = ConsoleColor.Yellow;
}
else if (logMsg.LogLevel == LogLevel.Error)
{
Console.ForegroundColor = ConsoleColor.Red;
}
else if (logMsg.LogLevel == LogLevel.Critical)
{
Console.ForegroundColor = ConsoleColor.DarkRed;
}
else
{
Console.ForegroundColor = ConsoleColor.Green;
}
var template = TP.Wrapper("日志内容", $"日志等级:{logMsg.LogLevel}",
$"##【时间】## {logMsg.LogDateTime:yyyy-MM-dd HH:mm:ss:fffff}",
$"##【名称】## {logMsg.LogName}",
$"##【线程】## {logMsg.EventId}",
$"##【消息】## {logMsg.Message}",
$"##【错误内容】## {logMsg.Exception?.ToString()}");
return template;
};
});//日志格式化
效果如图:
Csharp
//日志分级写入文件
Array.ForEach(new[] { LogLevel.Debug, LogLevel.Information, LogLevel.Warning, LogLevel.Error, LogLevel.Critical }, logLevel =>
{
services.AddFileLogging(Environment.CurrentDirectory + @"\Logs\Debug\workstation-" + DateTime.Now.ToString("yyyy-MM-dd") + ".log", options =>
{
options.WriteFilter = (logMsg) =>
{
return logMsg.LogLevel == LogLevel.Debug;
};
options.FileNameRule = fileName =>
{
return string.Format(fileName, DateTime.Now);
};
options.MessageFormat = (logMsg) =>
{
StringBuilder log = new();
log.Append("【时间】:" + DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fffff") + "\r\n");
log.Append("【等级】:" + logMsg.LogLevel.ToString() + "\r\n");
log.Append("【名称】:" + logMsg.LogName + "\r\n");
log.Append("【线程】" + logMsg.EventId.Id + "\r\n");
log.Append("【消息】:" + logMsg.Message + "\r\n");
log.Append("【错误内容】:" + logMsg.Exception?.ToString() + "\r\n" + "\r\n");
return log.ToString();
};
options.DateFormat = "yyyy-MM-dd HH:mm:ss.fffff";
options.HandleWriteError = (writeError) =>
{
writeError.UseRollbackFileName(Path.GetFileNameWithoutExtension(writeError.CurrentFileName) + "-oops" + Path.GetExtension(writeError.CurrentFileName));
};
options.FileSizeLimitBytes = 500 * 1024;
options.MaxRollingFiles = 30;
});//Debug日志写入本地文件
services.AddFileLogging(Environment.CurrentDirectory + @"\Logs\Information\workstation-" + DateTime.Now.ToString("yyyy-MM-dd") + ".log", options =>
{
options.WriteFilter = (logMsg) =>
{
return logMsg.LogLevel == LogLevel.Information;
};
options.FileNameRule = fileName =>
{
return string.Format(fileName, DateTime.Now);
};
options.MessageFormat = (logMsg) =>
{
StringBuilder log = new();
log.Append("【时间】:" + DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fffff") + "\r\n");
log.Append("【等级】:" + logMsg.LogLevel.ToString() + "\r\n");
log.Append("【名称】:" + logMsg.LogName + "\r\n");
log.Append("【线程】" + logMsg.EventId.Id + "\r\n");
log.Append("【消息】:" + logMsg.Message + "\r\n");
log.Append("【错误内容】:" + logMsg.Exception?.ToString() + "\r\n" + "\r\n");
return log.ToString();
};
options.DateFormat = "yyyy-MM-dd HH:mm:ss.fffff";
options.HandleWriteError = (writeError) =>
{
writeError.UseRollbackFileName(Path.GetFileNameWithoutExtension(writeError.CurrentFileName) + "-oops" + Path.GetExtension(writeError.CurrentFileName));
};
options.FileSizeLimitBytes = 500 * 1024;
options.MaxRollingFiles = 30;
});//information日志写入本地文件
services.AddFileLogging(Environment.CurrentDirectory + @"\Logs\Warning\workstation-" + DateTime.Now.ToString("yyyy-MM-dd") + ".log", options =>
{
options.WriteFilter = (logMsg) =>
{
return logMsg.LogLevel == LogLevel.Warning;
};
options.FileNameRule = fileName =>
{
return string.Format(fileName, DateTime.Now);
};
options.MessageFormat = (logMsg) =>
{
StringBuilder log = new();
log.Append("【时间】:" + DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fffff") + "\r\n");
log.Append("【等级】:" + logMsg.LogLevel.ToString() + "\r\n");
log.Append("【名称】:" + logMsg.LogName + "\r\n");
log.Append("【线程】" + logMsg.EventId.Id + "\r\n");
log.Append("【消息】:" + logMsg.Message + "\r\n");
log.Append("【错误内容】:" + logMsg.Exception?.ToString() + "\r\n" + "\r\n");
return log.ToString();
};
options.DateFormat = "yyyy-MM-dd HH:mm:ss.fffff";
options.HandleWriteError = (writeError) =>
{
writeError.UseRollbackFileName(Path.GetFileNameWithoutExtension(writeError.CurrentFileName) + "-oops" + Path.GetExtension(writeError.CurrentFileName));
};
options.FileSizeLimitBytes = 500 * 1024;
options.MaxRollingFiles = 30;
});//warning日志写入本地文件
services.AddFileLogging(Environment.CurrentDirectory + @"\Logs\Error\workstation-" + DateTime.Now.ToString("yyyy-MM-dd") + ".log", options =>
{
options.WriteFilter = (logMsg) =>
{
return logMsg.LogLevel == LogLevel.Error;
};
options.FileNameRule = fileName =>
{
return string.Format(fileName, DateTime.Now);
};
options.MessageFormat = (logMsg) =>
{
StringBuilder log = new();
log.Append("【时间】:" + DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fffff") + "\r\n");
log.Append("【等级】:" + logMsg.LogLevel.ToString() + "\r\n");
log.Append("【名称】:" + logMsg.LogName + "\r\n");
log.Append("【线程】" + logMsg.EventId.Id + "\r\n");
log.Append("【消息】:" + logMsg.Message + "\r\n");
log.Append("【错误内容】:" + logMsg.Exception?.ToString() + "\r\n" + "\r\n");
return log.ToString();
};
options.DateFormat = "yyyy-MM-dd HH:mm:ss.fffff";
options.HandleWriteError = (writeError) =>
{
writeError.UseRollbackFileName(Path.GetFileNameWithoutExtension(writeError.CurrentFileName) + "-oops" + Path.GetExtension(writeError.CurrentFileName));
};
options.FileSizeLimitBytes = 500 * 1024;
options.MaxRollingFiles = 30;
});//error日志写入本地文件
services.AddFileLogging(Environment.CurrentDirectory + @"\Logs\Critical\workstation-" + DateTime.Now.ToString("yyyy-MM-dd") + ".log", options =>
{
options.WriteFilter = (logMsg) =>
{
return logMsg.LogLevel == LogLevel.Critical;
};
options.FileNameRule = fileName =>
{
return string.Format(fileName, DateTime.Now);
};
options.MessageFormat = (logMsg) =>
{
StringBuilder log = new();
log.Append("【时间】:" + DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fffff") + "\r\n");
log.Append("【等级】:" + logMsg.LogLevel.ToString() + "\r\n");
log.Append("【名称】:" + logMsg.LogName + "\r\n");
log.Append("【线程】" + logMsg.EventId.Id + "\r\n");
log.Append("【消息】:" + logMsg.Message + "\r\n");
log.Append("【错误内容】:" + logMsg.Exception?.ToString() + "\r\n" + "\r\n");
return log.ToString();
};
options.DateFormat = "yyyy-MM-dd HH:mm:ss.fffff";
options.HandleWriteError = (writeError) =>
{
writeError.UseRollbackFileName(Path.GetFileNameWithoutExtension(writeError.CurrentFileName) + "-oops" + Path.GetExtension(writeError.CurrentFileName));
};
options.FileSizeLimitBytes = 500 * 1024;
options.MaxRollingFiles = 30;
});//critical日志写入本地文件
});//日志分级并根据日期写入文件
效果如图:
【选用】将日志写入数据库
Csharp
//日志写入数据库
services.AddDatabaseLogging<WriteLogToDB>(options =>
{
options.MinimumLevel = LogLevel.Warning;
options.IgnoreReferenceLoop = true;
});//日志写入数据库
我们需要在YourProject.Core中新建Logger文件夹 新建类:WriteLogToDB,代码如下:
Csharp
/// <summary>
/// 将日志写入数据库
/// </summary>
public class WriteLogToDB : SugarDbContext<Log_LogRecord_Entity>, IDatabaseLoggingWriter
{
public void Write(LogMessage logMsg, bool flush)
{
Log_LogRecord_Entity log = new()
{
LogName = logMsg.LogName,
LogLevel = (int)logMsg.LogLevel,
EventId = logMsg.EventId.ToString(),
Message = logMsg.Message,
Exception = logMsg.Exception.ToString(),
State = logMsg.State.ToString(),
LogDateTime = logMsg.LogDateTime,
ThreadId = logMsg.ThreadId,
Context = JsonHelper.Serialize(logMsg.Context),
};
DbInstance.Insertable(log).ExecuteCommand();
}
}
新建类:Log_LogRecord_Entity【用于向数据库记录】
Csharp
using SqlSugar;
/*
* @author : xkdong@163.com
* @date : 2023-4-26
* @desc : 日志记录表
*/
namespace Mofang.Workstation.Core
{
/// <summary>
/// 日志记录表
/// </summary>
[SugarTable("Log_LogRecord", TableDescription = "日志记录表")]
public class Log_LogRecord_Entity
{
/// <summary>
/// 编号
/// </summary>
[SugarColumn(IsIdentity = true, IsPrimaryKey = true, ColumnDescription = "编号")]
public int Id { get; set; }
/// <summary>
/// 日志名称
/// </summary>
[SugarColumn(ColumnDescription = "日志名称")]
public string LogName { get; set; }
/// <summary>
/// 日志等级
/// </summary>
[SugarColumn(ColumnDescription = "日志等级")]
public int LogLevel { get; set; }
/// <summary>
/// 事件Id
/// </summary>
[SugarColumn(ColumnDescription = "事件Id")]
public string EventId { get; set; }
/// <summary>
/// 消息内容
/// </summary>
[SugarColumn(ColumnDescription = "消息内容", ColumnDataType = "TEXT")]
public string Message { get; set; }
/// <summary>
/// 错误内容
/// </summary>
[SugarColumn(ColumnDescription = "错误内容", ColumnDataType = "TEXT")]
public string Exception { get; set; }
/// <summary>
/// 状态
/// </summary>
[SugarColumn(ColumnDescription = "状态")]
public string State { get; set; }
/// <summary>
/// 日志时间
/// </summary>
[SugarColumn(ColumnDescription = "日志时间")]
public DateTime LogDateTime { get; set; }
/// <summary>
/// 线程Id
/// </summary>
[SugarColumn(ColumnDescription = "线程Id")]
public int ThreadId { get; set; }
/// <summary>
/// 日志上下文
/// </summary>
[SugarColumn(ColumnDescription = "日志上下文", ColumnDataType = "TEXT")]
public string Context { get; set; }
}
}
效果如图: 【(⊙o⊙)...木有图,自己想象下,就普通的表】