CSRobot中的gen命令
CSRobot https://github.com/axzxs2001/CSRobot
gen命令是用来从数据库,生成实体类,前一篇文章说到要实现两个接口,其中一个是从数据库中查询出库,表,字段的信息,转成实体类。
首先说一下gen命令的属性
csrobot gen [options]
命令参数选项:
--dbtype | 数据库类型,必填,例如:--dbtype=mysql,--dbtype=mssql,--dbtype=postgressql |
--table | 指定数据库表名生成实体类,缺省默认全部库表 |
--out | 生成实体类的路径,缺省默认当前路径 |
--tep | 生成实体类的模板,可以是内置的模板cs,或指定本地路径,或指定url,生成文件的扩展名与指定的模板扩展名匹配。缺省默认cs内置模板,例如:--tep=/usr/abc/bcd.cs;--tep=https://github.com/abc/bcd.cs;--tep=cs |
--host | 连接数据所在主机,如果缺少此项,会查找当前目录或子目录中的是否存在appsettings.json配置文件,并取配置文件下的ConnectionStrings节点的第一个子节点的值作为连接字符串 |
--db | 数据库名称,如果缺少此项,会查找当前目录或子目录中的是否存在appsettings.json配置文件,并取配置文件下的ConnectionStrings节点的第一个子节点的值作为连接字符串 |
--user | 数据库用户名,如果缺少此项,会查找当前目录或子目录中的是否存在appsettings.json配置文件,并取配置文件下的ConnectionStrings节点的第一个子节点的值作为连接字符串 |
--pwd | 数据库密码,如果缺少此项,会查找当前目录或子目录中的是否存在appsettings.json配置文件,并取配置文件下的ConnectionStrings节点的第一个子节点的值作为连接字符串 |
--port | 数据库端口号,如果缺少此项,会查找当前目录或子目录中的是否存在appsettings.json配置文件,并取配置文件下的ConnectionStrings节点的第一个子节点的值作为连接字符串 |
ITraverser实现
ITraverser接口实现的步骤是这样的:
ITraverser接口->Traverser抽象类->MySqlTraverser实现类
ITraverser接口
/// <summary>
/// 完成从数据库生成数据库结构实体
/// </summary>
public interface ITraverser
{
DataBase Traverse();
}
初步规划是实现mysql,mssql,postgres三种类型的实的信息获取,接下来实现了一个Traverser类抽象类,来处理一些公共的验证,比如命令中缺少必填属性的处理。
Traverser类
public abstract class Traverser : ITraverser
{
protected bool IsExistOption { get; set; } = true;
public Traverser(CommandOptions options)
{
if (!options.ContainsKey("--host"))
{
IsExistOption = false;
Console.WriteLine("缺少 --host");
}
if (!options.ContainsKey("--db"))
{
IsExistOption = false;
Console.WriteLine("缺少 --db");
}
if (!options.ContainsKey("--user"))
{
IsExistOption = false;
Console.WriteLine("缺少 --user");
}
if (!options.ContainsKey("--pwd"))
{
IsExistOption = false;
Console.WriteLine("缺少 --pwd");
}
}
public abstract DataBase Traverse();
}
接下来是MySqlTraverser的实现。
MySqlTraverser类
public class MySqlTraverser : Traverser
{
MySqlConnectionStringBuilder _connectionStringBuilder;
public MySqlTraverser(CommandOptions options) : base(options)
{
if (IsExistOption)
{
_connectionStringBuilder = new MySqlConnectionStringBuilder()
{
Server = options["--host"],
Database = options["--db"],
UserID = options["--user"],
Password = options["--pwd"],
Port = options.ContainsKey("--port") ? uint.Parse(options["--port"]) : 3306,
};
}
else
{
var connectionString = Common.GetConnectionString();
if (string.IsNullOrEmpty(connectionString))
{
Console.WriteLine("本地配置文件中找不到数据库连接字符串");
}
_connectionStringBuilder = new MySqlConnectionStringBuilder(connectionString);
}
}
public override DataBase Traverse()
{
return GetDataBase();
}
DataBase GetDataBase()
{
var dataBase = new DataBase()
{
DataBaseName = _connectionStringBuilder.Database
};
using (var con = new MySqlConnection(_connectionStringBuilder.ConnectionString))
{
var sql = @$"select table_name as tablename,table_comment as tabledescribe from information_schema.tables where table_schema='{_connectionStringBuilder.Database}' and table_type='BASE TABLE';";
var cmd = new MySqlCommand(sql, con);
con.Open();
var reader = cmd.ExecuteReader();
while (reader.Read())
{
var table = new Table();
table.TableName = reader.GetFieldValue<string>("tablename");
table.TableDescribe = reader.GetFieldValue<string>("tabledescribe");
dataBase.Tables.Add(table);
}
con.Close();
}
GetFields(dataBase);
return dataBase;
}
void GetFields(DataBase dataBase)
{
foreach (var table in dataBase.Tables)
{
var sql = @$"select character_maximum_length as fieldsize,column_name as fieldname,data_type as dbtype,column_comment as fielddescribe from information_schema.columns where table_name = '{table.TableName}' ";
using (var con = new MySqlConnection(_connectionStringBuilder.ConnectionString))
{
var cmd = new MySqlCommand(sql, con);
con.Open();
var reader = cmd.ExecuteReader();
while (reader.Read())
{
var field = new Field();
field.FieldName = reader.GetFieldValue<string>("fieldname");
field.FieldDescribe = reader.GetFieldValue<string>("fielddescribe");
field.DBType = reader.GetFieldValue<string>("dbtype");
var size = reader.GetFieldValue<object>("fieldsize");
if (size != DBNull.Value)
{
field.FieldSize = Convert.ToInt64(size);
}
table.Fields.Add(field);
}
}
}
}
}
IBuilder实现
IBuilder接口
/// <summary>
/// 完成对应编程语言实体类生成
/// </summary>
public interface IBuilder
{
void Build(DataBase database, CommandOptions options);
}
CSharpBuilder实现类
using CSRobot.GenerateEntityTools.Entity;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Text;
using System.Text.RegularExpressions;
namespace CSRobot.GenerateEntityTools.Builders
{
public class CSharpBuilder : IBuilder
{
public void Build(DataBase database, CommandOptions options)
{
//取输出路径
var basePath = GetOut(options, database.DataBaseName);
var template = GetTamplate(options);
//生成独立的表
if (options.ContainsKey("--table"))
{
var table = database.Tables.SingleOrDefault(s => s.TableName == options["--table"]);
if (table != null)
{
var codeString = GetCodeString(database.DataBaseName, table, template.Template);
File.WriteAllText($"{basePath}/{table.TableName}{template.Extension}", codeString.ToString(), Encoding.UTF8);
}
else
{
throw new ApplicationException($"找不到{options["--table"]}表");
}
}
else
{
//生成所有表实体类
foreach (var table in database.Tables)
{
var filePath = $"{basePath}/{table.TableName}{template.Extension}";
if (File.Exists(filePath))
{
Console.WriteLine($"{filePath}已存在,是否覆盖?Y为覆盖,N为不覆盖");
if (Console.ReadLine().ToLower() == "y")
{
var codeString = GetCodeString(database.DataBaseName, table, template.Template);
File.WriteAllText(filePath, codeString.ToString(), Encoding.UTF8);
}
}
else
{
var codeString = GetCodeString(database.DataBaseName, table, template.Template);
File.WriteAllText(filePath, codeString.ToString(), Encoding.UTF8);
}
}
}
}
/// <summary>
/// 模板替换
/// </summary>
/// <param name="dataBaseName"></param>
/// <param name="table"></param>
/// <param name="template"></param>
/// <returns></returns>
private string GetCodeString(string dataBaseName, Table table, string template)
{
template = template.Replace("${DataBaseName}", dataBaseName);
template = template.Replace("${TableDescribe}", table.TableDescribe);
template = template.Replace("${TableName}", table.TableName);
var match = Regex.Match(template, @"(?<=\$\{Fields\})[\w\W]+(?=\$\{Fields\})");
if (match.Success)
{
var reg = new Regex(@"(?<=\$\{Fields\})[\w\W]+(?=\$\{Fields\})");
return reg.Replace(template, GetFieldString(table, match.Value.Trim(' ')).Trim()).Replace("${Fields}", "");
}
return template;
}
/// <summary>
/// 处理属性
/// </summary>
/// <param name="table"></param>
/// <param name="fieldTamplate"></param>
/// <returns></returns>
private string GetFieldString(Table table, string fieldTamplate)
{
var fields = new StringBuilder();
foreach (var field in table.Fields)
{
//把模板分成行,分别处理
var lines = fieldTamplate.Split(new char[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries);
var newFieldTamplate = new StringBuilder();
foreach (var line in lines)
{
var newLine = line;
if (newLine.Trim().StartsWith("$?{"))
{
var match = Regex.Match(newLine, @"(?<=\$\?\{)[\w]+(?=\})");
if (match.Success)
{
var valueResult = field.GetType().GetProperty(match.Value).GetValue(field);
if (valueResult != null && valueResult.ToString() != "")
{
newLine = newLine.Replace($"$?{{{match.Value}}}", "");
newFieldTamplate.AppendLine(newLine);
}
}
}
else
{
newFieldTamplate.AppendLine(newLine);
}
}
var fieldContent = newFieldTamplate.ToString();
foreach (var pro in field.GetType().GetProperties())
{
if (pro.Name != "DBType")
{
fieldContent = fieldContent.Replace($"${{{pro.Name}}}", pro.GetValue(field)?.ToString());
}
else
{
fieldContent = fieldContent.Replace("${DBType}", _typeMap[field.DBType]);
}
}
fields.AppendLine(fieldContent);
}
return fields.ToString();
}
/// <summary>
/// 处理输出路径
/// </summary>
/// <param name="options"></param>
/// <param name="dataBaseName"></param>
/// <returns></returns>
private string GetOut(CommandOptions options, string dataBaseName)
{
if (options.ContainsKey("--out"))
{
return options["--out"];
}
else
{
var basePath = $"{Directory.GetCurrentDirectory()}/{dataBaseName}";
Directory.CreateDirectory(basePath);
return basePath;
}
}
/// <summary>
/// 处理模板
/// </summary>
/// <param name="options"></param>
/// <returns></returns>
private (string Template, string Extension) GetTamplate(CommandOptions options)
{
var template = @"
using System;
namespace ${DataBaseName}
{
/// <summary>
/// ${TableDescribe}
/// </summary>
public class ${TableName}
{
${Fields}
$?{FieldDescribe}/// <summary>
$?{FieldDescribe}/// ${FieldDescribe}
$?{FieldDescribe}/// </summary>
$?{FieldSize}[BField(Length=${FieldSize},Name=""${FieldName}"")]
public ${DBType} ${FieldName}
{ get; set; }
${Fields}
}
}
";
if (options.ContainsKey("--tep"))
{
var path = options["--tep"].ToLower();
if (path == "cs")
{
return (template, ".cs");
}
if (path.StartsWith("http"))
{
var client = new HttpClient();
var request = new HttpRequestMessage(HttpMethod.Get, path);
var response = client.SendAsync(request).Result;
if (response.StatusCode == HttpStatusCode.OK)
{
return (response.Content.ReadAsStringAsync().Result, Path.GetExtension(path));
}
else
{
throw new ApplicationException("获取模版失败");
}
}
else
{
return (File.ReadAllText(path, Encoding.UTF8), Path.GetExtension(path));
}
}
else
{
return (template, ".cs");
}
}
Dictionary<string, string> _typeMap;
public CSharpBuilder(string dbType)
{
switch (dbType)
{
case "postgresql":
_typeMap = new Dictionary<string, string>
{
{"bigint","long" },// int8 有符号8字节整数
{"bigserial" ,"long" },// serial8 自增8字节整数
{"bit","bool" },// [ (n) ] 定长位串
{"boolean","bool" }, //bool 逻辑布尔值(真/假)
{"bool","bool" }, //bool 逻辑布尔值(真/假)
{"character","string" }, //varying [ (n) ] varchar [ (n) ] 可变长字符串
{"cidr","string" }, // IPv4 或 IPv6 网络地址
{"date","DateTime" } ,// 日历日期(年, 月, 日)
{"double","double" } ,// precision float8 双精度浮点数(8字节)
{"inet","string" } ,// IPv4 或 IPv6 主机地址
{"int4","int" } , // int, int4 有符号 4 字节整数
{"integer","int" } , // int, int4 有符号 4 字节整数
{"macaddr","string" } , // MAC (Media Access Control)地址
{"money","decimal" }, // 货币金额
{"numeric","decimal" } , // [ (p, s) ] decimal [ (p, s) ] 可选精度的准确数值数据类型
{"real","float" }, // float4 单精度浮点数(4 字节)
{"smallint","short" }, // int2 有符号 2 字节整数
{"smallserial","short" }, // serial2 自增 2 字节整数
{"serial","int" }, // serial4 自增 4 字节整数
{"text","string" }, // 可变长字符串
{"varchar","string" }, // 可变长字符串
{"time","DateTime" } , // [ (p) ] [ without time zone ] 一天中的时刻(无时区)
// {"time","DateTime" } , // [ (p) ] with time zone timetz 一天中的时刻,含时区
{"timestamp","DateTime" }, // [ (p) ] [ without time zone ] 日期与时刻(无时区)
// {"timestamp","DateTime" }, // [ (p) ] with time zone timestamptz 日期与时刻,含时区
{"tsquery","string" }, // 文本检索查询
{"tsvector","string" }, // 文本检索文档
{"txid_snapshot","string" }, // 用户级别的事务ID快照
{"uuid","string" } , // 通用唯一标识符
{"xml","string" } , // XML 数据
{"json","string" }, // JSON 数据
};
break;
case "mysql":
_typeMap = new Dictionary<string, string>
{
{"char","char" },
{"varchar","string" },
{"tinytext","string" },
{"text","string" },
{"blob","string" },
{"mediumtext","string" },
{"mediumblob","string" },
{"longblob","string" },
{"longtext","string" },
{"tinyint","short" },
{"smallint","short" },
{"mediumint","short" },
{"int","int" },
{"bigint","long" },
{"float","float" },
{"double","double" },
{"decimal","decimal" },
{"date","DateTime" },
{"datetime","DateTime" },
{"timestamp","string" },
{"time","DateTime" },
{"boolean","bool" },
};
break;
case "mssql":
_typeMap = new Dictionary<string, string>
{
{ "char", "string" },// 固定长度的字符串。最多 8,000 个字符。 n
{ "varchar", "string" },//(n) 可变长度的字符串。最多 8,000 个字符。
{ "text", "string" },// 可变长度的字符串。最多 2GB 字符数据。
{ "nchar", "string" },//(n) 固定长度的 Unicode 数据。最多 4,000 个字符。
{ "nvarchar", "string" },//(n) 可变长度的 Unicode 数据。最多 4,000 个字符。
{ "ntext", "string" },// 可变长度的 Unicode 数据。最多 2GB 字符数据。
{ "bit", "bool" },// 允许 0、1 或 NULL
{ "binary", "string" },//(n) 固定长度的二进制数据。最多 8,000 字节。
{ "varbinary", "string" },//(n) 可变长度的二进制数据。最多 8,000 字节。
{ "image", "string" },// 可变长度的二进制数据。最多 2GB。
{ "tinyint", "byte" },// 允许从 0 到 255 的所有数字。 1 字节
{ "smallint", "short" },// 允许从 -32,768 到 32,767 的所有数字。 2 字节
{ "int", "int" },// 允许从 -2,147,483,648 到 2,147,483,647 的所有数字。 4 字节
{ "bigint", "long" },// 允许介于 -9,223,372,036,854,775,808 和 9,223,372,036,854,775,807 之间的所有数字。 8 字节
{ "decimal", "decimal" },//
{ "numeric", "decimal" },//
{ "smallmoney", "decimal" },// 介于 -214,748.3648 和 214,748.3647 之间的货币数据。 4 字节
{ "money", "decimal" },// 介于 -922,337,203,685,477.5808 和 922,337,203,685,477.5807 之间的货币数据。 8 字节
{ "float", "float" },// 从 -1.79E + 308 到 1.79E + 308 的浮动精度数字数据。 参数 n 指示该字段保存 4 字节还是 8 字节。f
{ "real", "double" },// 从 -3.40E + 38 到 3.40
{ "datetime", "DateTime" },// 从 1753 年 1 月 1 日 到 9999 年 12 月 31 日,精度为 3.33 毫秒。 8 bytes
{ "datetime2", "DateTime" },// 从 1753 年 1 月 1 日 到 9999 年 12 月 31 日,精度为 100 纳秒。 6-8 bytes
{ "smalldatetime", "DateTime" },// 从 1900 年 1 月 1 日 到 2079 年 6 月 6 日,精度为 1 分钟。 4 bytes
{ "date", "DateTime" },// 仅存储日期。从 0001 年 1 月 1 日 到 9999 年 12 月 31 日。 3 bytes
{ "time", "DateTime" },// 仅存储时间。精度为 100 纳秒。 3-5 bytes
{ "datetimeoffset", "string" },// 与 datetime2 相同,外加时区偏移。 8-10 bytes
{ "timestamp", "string" }// 存储唯一的
};
break;
}
}
}
}
CSharpBuilder类完成的是根据gen命令的参数,生成模板,类型映射来生成一个纯文本的.cs文件。
感觉实现的不太优美,有时间再优化,如果大家在模板这块有什么好的建议,请留言。
其实还有一个处理的地方,就是各种命令进来以后,分各归其主方法去执行,实现如下
static class CSRobotTools
{
static Dictionary<string, Func<CommandOptions, bool>> _CSRobotDic;
static CSRobotTools()
{
_CSRobotDic = new Dictionary<string, Func<CommandOptions, bool>> {
{"-info", Info},
{"-h",Help},
{"gen",GenerateEntityTool.GenerateEntity}
};
}
public static bool Run(string[] args)
{
var options = GetOptions(args);
if (args.Length == 0)
{
return _CSRobotDic["--info"](options);
}
else if (_CSRobotDic.ContainsKey(args[0]))
{
return _CSRobotDic[args[0]](options);
}
else
{
return false;
}
}
static bool Help(CommandOptions options)
{
var mgr = new ResourceManager("CSRobot.Resource.gen", Assembly.GetExecutingAssembly());
Console.WriteLine(mgr.GetString("csrobot-h"), Assembly.GetEntryAssembly().GetCustomAttribute<AssemblyInformationalVersionAttribute>().InformationalVersion.ToString());
return true;
}
static bool Info(CommandOptions options)
{
var mgr = new ResourceManager("CSRobot.Resource.gen", Assembly.GetExecutingAssembly());
Console.WriteLine(mgr.GetString("csrobot-info"), Assembly.GetEntryAssembly().GetCustomAttribute<AssemblyInformationalVersionAttribute>().InformationalVersion.ToString());
return true;
}
static CommandOptions GetOptions(string[] args)
{
var options = new CommandOptions();
for (var i = 1; i < args.Length; i++)
{
if (string.IsNullOrEmpty(args[i].Trim()))
{
continue;
}
var arr = args[i].Split("=");
if (arr.Length < 2)
{
options.Add(arr[0], null);
}
else
{
options.Add(arr[0], arr[1]);
}
}
return options;
}
}
接下来要做的:
1、进一步细化各部门实现
2、数据库到csharp的类型map放的更灵活
3、模板适配更丰富,更灵活
想要更快更方便的了解相关知识,可以关注微信公众号
****欢迎关注我的asp.net core系统课程****
《asp.net core精要讲解》 https://ke.qq.com/course/265696
《asp.net core 3.0》 https://ke.qq.com/course/437517
《asp.net core项目实战》 https://ke.qq.com/course/291868
《基于.net core微服务》 https://ke.qq.com/course/299524
《asp.net core精要讲解》 https://ke.qq.com/course/265696
《asp.net core 3.0》 https://ke.qq.com/course/437517
《asp.net core项目实战》 https://ke.qq.com/course/291868
《基于.net core微服务》 https://ke.qq.com/course/299524