Roslyn+T4+EnvDTE项目完全自动化 (一)
阅读原文时间:2021年11月29日阅读:3

前言

以前做一个金融软件项目,软件要英文、繁体版本,开始甲方弄了好几个月,手动一条一条替换,发现很容易出错,因为有金融专业术语,字符串在不同语义要特殊处理,第三方工具没法使用。最后我用Roslyn写了一个工具,只需10分钟就能翻译整个软件,100%准确

做完上个项目发现Roslyn还可以深度开发,写了一个工具:代码助手,可解决项目所有琐碎重复性操作,代码完全自动化

原理

Roslyn是啥?

XmlDocument,XDocument可以解析xml,同样 Roslyn 可解析项目中C#代码。c#常用插件ReSharper,只能重构一些很规范的代码(生成IEqualityComparer,IComparer接口…),用Roslyn可以自动化业务代码

自己写 代码助手

一,类->视图->增删改查 全自动

需求:一个数据库所有表的增删改查

实现:Roslyn+T4实现类自动生成增删改查界面

生成最终效果

每个属性对应不同的控件:

自动生成单个对象编辑预览

Roslyn+T4实现

  1. EnvDTE获取当前打开项目
  2. Roslyn api CodeAnalysis 分析当前项目
  3. 获取代码Symbol
  4. T4代码自动化

T4主要代码

<#@ include file="Include\base.t4" #>
<#@ include file="Include\CodeAnalysis.t4" #>
<# param.Task = DoAsync(); #>
<#+ Dictionary dic;
int depthLevel = 2; //嵌套类展开深度

async Task<string> DoAsync()  
{  
    var Modules = "Modules";  
    var ns = "Test.Database";               //获取名称空间 Test.Database 所有类  
    var DbContextName = "sakilaEntities";   //测试数据库  
    var skipClass = new\[\]  
    {  
        DbContextName,  
    }.ToHashSet();                          //排除类

    var modulesProjectItem = new ProjectItemEntry(@"Test\\View\\Modules");        //获取项目view的路径  
    var viewModelProjectItem = new ProjectItemEntry(@"Test\\ViewModel\\Modules"); //获取项目ViewModel的路径  
    /\*  
    modulesProjectItem.Delete();  
    viewModelProjectItem.Delete();  
    return "";  
    \*/

    var analysisCore = await param.TryAnalysisSolutionAsync(cancellationToken); //Roslyn 分析当前工程  
    var solution = analysisCore.Workspace.CurrentSolution;                      //Roslyn 的解决方案

    await TypeSymbols.InitializeTypeSymbolsAsync(solution, cancellationToken);

    //获取名称空间 Test.Database 所有类  
    var list = await solution.GetAllSymbolsAsync((project, symbol) =>  
    {  
        var displayString = symbol.ContainingNamespace.ToDisplayString();  
        if (displayString == ns && !skipClass.Contains(symbol.Name))  
        {  
            return true;  
        }  
        return false;  
    });  
    var items = list.Select(p =>  
    {  
        var entry = new ClassEntry(p);  
        entry.DoProperty();  
        return entry;  
    }).ToList();  

#>

T4生成文本

<#+ //xaml { var notMapHashSet = new HashSet(); //每个属性类型对就的控件,如果没有映射,写日志
double index = 0;
dic = items.ToDictionary(p=> p.Name);
foreach (var c in items)
{
index++;

        var xamlOutput = StartNewFile(modulesProjectItem.GetRelativePath(c.MainViewPair.XamlFileName), true); //打开一个新文件  
        param.Log($@"{index / items.Count:P1}:{c.MainViewPair.ClassName}");  

//xaml
#>
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:xctk="http://schemas.xceed.com/wpf/xaml/toolkit"
xmlns:custcombobox="clr-namespace:Test.View.CustComboBox"
xmlns:local="clr-namespace:<#=modulesProjectItem.Namespace#>"
xmlns:vm="clr-namespace:<#=viewModelProjectItem.Namespace#>"
d:DataContext="{d:DesignInstance IsDesignTimeCreatable=True,
Type={x:Type vm:<#=c.MainViewPair.ViewModelClassName#>}}"
d:DesignHeight="450"
d:DesignWidth="800"
mc:Ignorable="d"
>
……………………………………

<#+
public class XamlCsPair
{
public readonly string ClassName = string.Empty; //类名
public readonly string XamlFileName = string.Empty; //View绝对路径
public readonly string CsFileName = string.Empty; //ViewModel绝对路径

public readonly string ViewModelClassName = string.Empty;   //View类名  
public readonly string ViewModelFileName = string.Empty;    //ViewModel 文件名  
public XamlCsPair(string className, string vewModelClassName)  
{  
    ClassName = className;  
    className = className.GetValidFileName("\_");            //类名到文件名,移除非法字符

    XamlFileName = $"{className}.xaml";  
    CsFileName = $"{XamlFileName}.cs";  
    ViewModelClassName = vewModelClassName;  
    ViewModelFileName = $"{ViewModelClassName}.cs";  
}  

}
public class ClassEntry
{
public ProjectSymbolEntry Entry { get; }
public INamedTypeSymbol Symbol { get; } //类Symbol
public readonly string Name = string.Empty; //类的名称
public readonly string NameZh = string.Empty; //类的中文名称
public readonly ITypeSymbol Type; //类的类型Symbol
public readonly string TypeName = string.Empty; //类的类型名称

public readonly string ClassName = string.Empty;    //目标类名

public readonly XamlCsPair MainViewPair;  
public readonly XamlCsPair EditViewPair;

public List<PropertyEntry> Properties { get; set; } = new List<PropertyEntry>();//类的属性集合  
public ClassEntry(ProjectSymbolEntry entry)  
{  
    Entry = entry;  
    var symbol = entry.Symbol;  
    Symbol = symbol;  
    Name = symbol.Name;  
    NameZh = Name.ToZh(cacheMode: CacheMode.OneWay); //百度api英文翻译中文  
    Type = symbol.GetTypeSymbol();  
    TypeName = Type.GetDisplayShortName();

    ClassName = Name.ToValidIdentifier(removeChars:"\_".ToCharArray());  
    MainViewPair = new XamlCsPair($"{ClassName}View", $"{ClassName}ViewModel");  
    EditViewPair = new XamlCsPair($"{ClassName}ViewEditorWindow", $"{ClassName}ViewEditorViewModel");  
}

public void DoProperty()  
{  
    Properties.Clear();  
    foreach (var member in Symbol.GetMembers().OfType<IPropertySymbol>())  
    {  
        Properties.Add(new PropertyEntry(this, member));  
    }  
}  

}
public class PropertyEntry
{
public ClassEntry Class { get; }
public IPropertySymbol Symbol { get; } //属性的Symbol
public readonly string Name = string.Empty;
public readonly string NameZh = string.Empty; //属性中文名称
public readonly ITypeSymbol Type; //属性类型
public readonly string TypeName = string.Empty; //属性类型名称

public PropertyEntry(ClassEntry entry, IPropertySymbol symbol)  
{  
    Class = entry;  
    Symbol = symbol;  
    Name = symbol.Name;  
    NameZh = Name.ToZh(cacheMode: CacheMode.OneWay);//百度api英文翻译中文  
    Type = symbol.GetTypeSymbol();                  //属性Roslyn类型  
    TypeName = Type.GetDisplayShortName();          //属性Roslyn名称  
}  

}
public class ProjectItemEntry
{
public ProjectItem ProjectItem { get; } //dte对应的一个项目文件
public string Namespace { get; } //ProjectItem的命名空间
public string FileName { get; } //项目文件绝对路径

public ProjectItemEntry(string projectRelativePath)  
{  
    ProjectItem = dte.GetProjectItemByRelativePath(projectRelativePath).TryCreateDir();  
    Namespace = (string)ProjectItem.Properties.Item("DefaultNamespace").Value;  
    FileName = ProjectItem.GetFileName();

}  
public void Delete()  
{  
    ProjectItem.DeleteChildren();  
    FileName.DeleteSubFiles();  
}  
public string GetRelativePath(string relativePath)  
{  
    return Path.Combine(FileName, relativePath);  
}  

}
#>