首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> .NET 網頁程式設計 >> C# >> 關於C#

Visual Studio 自訂單一檔案產生器 (IVsSingleFileGenerator) 進階:以程式碼為範本取代 XML 產生實體類別

     編輯:關於C#

背景痛點:XML 為基礎的程式碼產生器缺陷

傳統做法:定義 XSD → 撰寫 XML 模型 → 實作 IVsSingleFileGenerator 讀取 XML 產生 C# 實體類別。缺點:

  1. 產生衝突:每次儲存 XML 重新產生,開發者手動修改的程式碼 (加上 Attribute、自訂邏輯) 會被覆蓋。
  2. 擴充困難:需在 XML Schema 增加擴充屬性、修改產生器邏輯,維護成本高。
  3. 缺乏型別檢查:XML 編寫無編譯期型別保障,易拼錯屬性名稱、型別。

創新方案:以介面程式碼為範本

核心概念:開發者撰寫介面定義,產生器讀取介面語法樹,輸出實作類別。優勢:

  • 享有完整 IntelliSense、編譯期型別檢查、重構支援。
  • 介面上的 Attribute 直接保留於產生的屬性上,無衝突。
  • 產生器邏輯極簡:僅負責「介面成員 → 實作成員」轉譯,Attribute 原樣複製。

範本介面定義

[Table]
interface IUser {
    [ID]
    string UserName { get; set; }
    string BirthDate { get; set; }
    string Region { get; set; }
    string Remark { get; set; }
}

產生器核心邏輯 (簡化版)

[ComVisible(true)]
[Guid("...")]
[CodeGeneratorRegistration(typeof(InterfaceToClassGenerator), "Interface To Class Generator", vsContextGuids.vsContextGuidVCSProject)]
public class InterfaceToClassGenerator : BaseCodeGeneratorWithSite {
    protected override byte[] GenerateCode(string inputFileName, string inputFileContent) {
        // 1. 解析輸入檔案 (應為 .cs 含介面定義)
        var syntaxTree = CSharpSyntaxTree.ParseText(inputFileContent);
        var root = syntaxTree.GetCompilationUnitRoot();
        var interfaceDecl = root.DescendantNodes().OfType().FirstOrDefault();
        if (interfaceDecl == null) return Encoding.UTF8.GetBytes("// No interface found");

        // 2. 建構輸出類別語法樹
        var className = interfaceDecl.Identifier.Text.TrimStart('I'); // IUser → User
        var members = new List();

        foreach (var prop in interfaceDecl.Members.OfType()) {
            // 複製 Attribute 清單
            var attrs = prop.AttributeLists;
            // 產生私有欄位
            var fieldName = "_" + char.ToLower(prop.Identifier.Text[0]) + prop.Identifier.Text.Substring(1);
            var field = SyntaxFactory.FieldDeclaration(
                SyntaxFactory.VariableDeclaration(prop.Type)
                    .WithVariables(SyntaxFactory.SingletonSeparatedList(
                        SyntaxFactory.VariableDeclarator(fieldName))))
                .WithModifiers(SyntaxFactory.TokenList(SyntaxFactory.Token(SyntaxKind.PrivateKeyword)));

            // 產生屬性實作 (含 EntityState.FieldChange 通知)
            var property = SyntaxFactory.PropertyDeclaration(prop.Type, prop.Identifier)
                .WithModifiers(SyntaxFactory.TokenList(SyntaxFactory.Token(SyntaxKind.PublicKeyword)))
                .WithAttributeLists(attrs)
                .WithAccessorList(SyntaxFactory.AccessorList(
                    SyntaxFactory.List(new[] {
                        SyntaxFactory.AccessorDeclaration(SyntaxKind.GetAccessorDeclaration)
                            .WithBody(SyntaxFactory.Block(SyntaxFactory.ReturnStatement(SyntaxFactory.IdentifierName(fieldName)))),
                        SyntaxFactory.AccessorDeclaration(SyntaxKind.SetAccessorDeclaration)
                            .WithBody(SyntaxFactory.Block(
                                SyntaxFactory.ExpressionStatement(SyntaxFactory.AssignmentExpression(SyntaxKind.SimpleAssignmentExpression, SyntaxFactory.IdentifierName(fieldName), SyntaxFactory.IdentifierName("value"))),
                                SyntaxFactory.ExpressionStatement(SyntaxFactory.InvocationExpression(
                                    SyntaxFactory.MemberAccessExpression(SyntaxKind.SimpleMemberAccessExpression,
                                        SyntaxFactory.IdentifierName("EntityState"),
                                        SyntaxFactory.IdentifierName("FieldChange")))
                                    .WithArgumentList(SyntaxFactory.ArgumentList(
                                        SyntaxFactory.SingletonSeparatedList(
                                            SyntaxFactory.Argument(SyntaxFactory.LiteralExpression(SyntaxKind.StringLiteralExpression, SyntaxFactory.Literal(prop.Identifier.Text)))))))))
                    })));

            // 靜態 FieldInfo 欄位 (供 ORM 使用)
            var staticField = SyntaxFactory.FieldDeclaration(
                SyntaxFactory.VariableDeclaration(SyntaxFactory.ParseTypeName("FieldInfo"))
                    .WithVariables(SyntaxFactory.SingletonSeparatedList(
                        SyntaxFactory.VariableDeclarator(prop.Identifier.Text + "Field")
                            .WithInitializer(SyntaxFactory.EqualsValueClause(
                                SyntaxFactory.ObjectCreationExpression(SyntaxFactory.ParseTypeName("FieldInfo"))
                                    .WithArgumentList(SyntaxFactory.ArgumentList(
                                        SyntaxFactory.SeparatedList(new[] {
                                            SyntaxFactory.Argument(SyntaxFactory.LiteralExpression(SyntaxKind.StringLiteralExpression, SyntaxFactory.Literal(className))),
                                            SyntaxFactory.Argument(SyntaxFactory.LiteralExpression(SyntaxKind.StringLiteralExpression, SyntaxFactory.Literal(prop.Identifier.Text))))
                                        })))))))
                .WithModifiers(SyntaxFactory.TokenList(SyntaxFactory.Token(SyntaxKind.PublicKeyword), SyntaxFactory.Token(SyntaxKind.StaticKeyword)));

            members.Add(field);
            members.Add(property);
            members.Add(staticField);
        }

        var classDecl = SyntaxFactory.ClassDeclaration(className)
            .WithModifiers(SyntaxFactory.TokenList(SyntaxFactory.Token(SyntaxKind.PublicKeyword)))
            .WithBaseList(SyntaxFactory.BaseList(
                SyntaxFactory.SingletonSeparatedList(
                    SyntaxFactory.SimpleBaseType(SyntaxFactory.ParseTypeName("DataObject")))))
            .WithAttributeLists(interfaceDecl.AttributeLists) // 複製介面上的 [Table] 等
            .WithMembers(SyntaxFactory.List(members));

        var namespaceDecl = SyntaxFactory.NamespaceDeclaration(SyntaxFactory.ParseName("MyApp.Entities"))
            .WithMembers(SyntaxFactory.SingletonSeparatedList(classDecl));

        var outputTree = SyntaxFactory.CompilationUnit().AddMembers(namespaceDecl).NormalizeWhitespace();
        return Encoding.UTF8.GetBytes(outputTree.ToFullString());
    }
}

部署與使用

  1. 專案類型:VSX Project (VSIX 專案),輸出 .vsix 安裝至 Visual Studio。
  2. 目標檔案屬性:Custom Tool = "InterfaceToClassGenerator",Custom Tool Namespace 可選填。
  3. 開發者編輯 IUser.cs (介面定義) → 儲存 → 產生 IUser.Generated.cs (實作類別) → 專案引用產生的類別。
  4. 修改介面 → 重新產生,手寫邏輯置於 partial 類別另一分部檔案,互不干擾。

進階擴充

  • 支援介面繼承鏈合併成員。
  • 依 Attribute 決定是否產生 virtual、override、[NotMapped] 等。
  • 整合 Roslyn Source Generators (.NET 5+),取代舊式 IVsSingleFileGenerator,享有更佳效能與診斷報告能力。
  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved