如何最小化生成代码在磁盘上的程序集大小?

How To Minimize Assembly Size On Disk Of Generated Code?

我正在为在线平台 (Dynamics 365) 生成 DTO 对象。最大程序集大小有非常严格的限制,仅生成的 DTO 对象就占据了该限制的大约 80%。我可以对正在生成的代码进行哪些更改以减少编译后的程序集在磁盘上占用的 space 数量?

下面是生成的示例 class:

//------------------------------------------------------------------------------
// <auto-generated>
//     This code was generated by a tool.
//
//     Changes to this file may cause incorrect behavior and will be lost if
//     the code is regenerated.
// </auto-generated>
//------------------------------------------------------------------------------

namespace Contoso.Xrm.Entities
{

    /// <summary>
    /// Track changes to records for analysis, record keeping, and compliance.
    /// </summary>
    [System.Runtime.Serialization.DataContractAttribute()]
    [Microsoft.Xrm.Sdk.Client.EntityLogicalNameAttribute("audit")]
    [System.CodeDom.Compiler.GeneratedCodeAttribute("CrmSvcUtil", "8.2.1.8676")]
    public partial class Audit : Microsoft.Xrm.Sdk.Entity, System.ComponentModel.INotifyPropertyChanging, System.ComponentModel.INotifyPropertyChanged
    {

        public static class Fields
        {
            public const string Action = "action";
            public const string AttributeMask = "attributemask";
            public const string AuditId = "auditid";
            public const string Id = "auditid";
            public const string CallingUserId = "callinguserid";
            public const string CreatedOn = "createdon";
            public const string ObjectId = "objectid";
            public const string Operation = "operation";
            public const string RegardingObjectId = "regardingobjectid";
            public const string TransactionId = "transactionid";
            public const string UserAdditionalInfo = "useradditionalinfo";
            public const string UserId = "userid";
            public const string lk_audit_callinguserid = "lk_audit_callinguserid";
            public const string lk_audit_userid = "lk_audit_userid";
        }


        /// <summary>
        /// Default Constructor.
        /// </summary>
        [System.Diagnostics.DebuggerNonUserCode()]
        public Audit() : 
                base(EntityLogicalName)
        {
        }

        public const string EntityLogicalName = "audit";

        public const string PrimaryIdAttribute = "auditid";

        public const int EntityTypeCode = 4567;

        public event System.ComponentModel.PropertyChangedEventHandler PropertyChanged;

        public event System.ComponentModel.PropertyChangingEventHandler PropertyChanging;

        [System.Diagnostics.DebuggerNonUserCode()]
        private void OnPropertyChanged(string propertyName)
        {
            if ((this.PropertyChanged != null))
            {
                this.PropertyChanged(this, new System.ComponentModel.PropertyChangedEventArgs(propertyName));
            }
        }

        [System.Diagnostics.DebuggerNonUserCode()]
        private void OnPropertyChanging(string propertyName)
        {
            if ((this.PropertyChanging != null))
            {
                this.PropertyChanging(this, new System.ComponentModel.PropertyChangingEventArgs(propertyName));
            }
        }

        /// <summary>
        /// Actions the user can perform that cause a change
        /// </summary>
        [Microsoft.Xrm.Sdk.AttributeLogicalNameAttribute("action")]
        public Microsoft.Xrm.Sdk.OptionSetValue Action
        {
            [System.Diagnostics.DebuggerNonUserCode()]
            get
            {
                return this.GetAttributeValue<Microsoft.Xrm.Sdk.OptionSetValue>("action");
            }
        }

        /// <summary>
        /// Contains a CSV of the ColumnNumber metadata property of attributes
        /// </summary>
        [Microsoft.Xrm.Sdk.AttributeLogicalNameAttribute("attributemask")]
        public string AttributeMask
        {
            [System.Diagnostics.DebuggerNonUserCode()]
            get
            {
                return this.GetAttributeValue<string>("attributemask");
            }
        }

        /// <summary>
        /// Unique identifier of the auditing instance
        /// </summary>
        [Microsoft.Xrm.Sdk.AttributeLogicalNameAttribute("auditid")]
        public System.Nullable<System.Guid> AuditId
        {
            [System.Diagnostics.DebuggerNonUserCode()]
            get
            {
                return this.GetAttributeValue<System.Nullable<System.Guid>>("auditid");
            }
        }

        [Microsoft.Xrm.Sdk.AttributeLogicalNameAttribute("auditid")]
        public override System.Guid Id
        {
            [System.Diagnostics.DebuggerNonUserCode()]
            get
            {
                return base.Id;
            }
            [System.Diagnostics.DebuggerNonUserCode()]
            set
            {
                base.Id = value;
            }
        }

        /// <summary>
        /// Unique identifier of the calling user in case of an impersonated call
        /// </summary>
        [Microsoft.Xrm.Sdk.AttributeLogicalNameAttribute("callinguserid")]
        public Microsoft.Xrm.Sdk.EntityReference CallingUserId
        {
            [System.Diagnostics.DebuggerNonUserCode()]
            get
            {
                return this.GetAttributeValue<Microsoft.Xrm.Sdk.EntityReference>("callinguserid");
            }
        }

        /// <summary>
        /// Date and time when the audit record was created.
        /// </summary>
        [Microsoft.Xrm.Sdk.AttributeLogicalNameAttribute("createdon")]
        public System.Nullable<System.DateTime> CreatedOn
        {
            [System.Diagnostics.DebuggerNonUserCode()]
            get
            {
                return this.GetAttributeValue<System.Nullable<System.DateTime>>("createdon");
            }
        }

        /// <summary>
        /// Unique identifier of the record that is being audited
        /// </summary>
        [Microsoft.Xrm.Sdk.AttributeLogicalNameAttribute("objectid")]
        public Microsoft.Xrm.Sdk.EntityReference ObjectId
        {
            [System.Diagnostics.DebuggerNonUserCode()]
            get
            {
                return this.GetAttributeValue<Microsoft.Xrm.Sdk.EntityReference>("objectid");
            }
        }

        /// <summary>
        /// The action that causes the audit--it will be create, delete, or update
        /// </summary>
        [Microsoft.Xrm.Sdk.AttributeLogicalNameAttribute("operation")]
        public Microsoft.Xrm.Sdk.OptionSetValue Operation
        {
            [System.Diagnostics.DebuggerNonUserCode()]
            get
            {
                return this.GetAttributeValue<Microsoft.Xrm.Sdk.OptionSetValue>("operation");
            }
        }

        /// <summary>
        /// Unique identifier of the object with which the record is associated.
        /// </summary>
        [Microsoft.Xrm.Sdk.AttributeLogicalNameAttribute("regardingobjectid")]
        public Microsoft.Xrm.Sdk.EntityReference RegardingObjectId
        {
            [System.Diagnostics.DebuggerNonUserCode()]
            get
            {
                return this.GetAttributeValue<Microsoft.Xrm.Sdk.EntityReference>("regardingobjectid");
            }
            [System.Diagnostics.DebuggerNonUserCode()]
            set
            {
                this.OnPropertyChanging("RegardingObjectId");
                this.SetAttributeValue("regardingobjectid", value);
                this.OnPropertyChanged("RegardingObjectId");
            }
        }

        /// <summary>
        /// Unique identifier for multiple changes that are part of a single operation; this field contains the same GUID for all the audit rows generated in a single transaction
        /// </summary>
        [Microsoft.Xrm.Sdk.AttributeLogicalNameAttribute("transactionid")]
        public System.Nullable<System.Guid> TransactionId
        {
            [System.Diagnostics.DebuggerNonUserCode()]
            get
            {
                return this.GetAttributeValue<System.Nullable<System.Guid>>("transactionid");
            }
        }

        /// <summary>
        /// Additional information associated to the user who caused the change.
        /// </summary>
        [Microsoft.Xrm.Sdk.AttributeLogicalNameAttribute("useradditionalinfo")]
        public string UserAdditionalInfo
        {
            [System.Diagnostics.DebuggerNonUserCode()]
            get
            {
                return this.GetAttributeValue<string>("useradditionalinfo");
            }
            [System.Diagnostics.DebuggerNonUserCode()]
            set
            {
                this.OnPropertyChanging("UserAdditionalInfo");
                this.SetAttributeValue("useradditionalinfo", value);
                this.OnPropertyChanged("UserAdditionalInfo");
            }
        }

        /// <summary>
        /// Unique identifier of the user who caused a change
        /// </summary>
        [Microsoft.Xrm.Sdk.AttributeLogicalNameAttribute("userid")]
        public Microsoft.Xrm.Sdk.EntityReference UserId
        {
            [System.Diagnostics.DebuggerNonUserCode()]
            get
            {
                return this.GetAttributeValue<Microsoft.Xrm.Sdk.EntityReference>("userid");
            }
        }

        /// <summary>
        /// 1:N userentityinstancedata_audit
        /// </summary>
        [Microsoft.Xrm.Sdk.RelationshipSchemaNameAttribute("userentityinstancedata_audit")]
        public System.Collections.Generic.IEnumerable<Contoso.Xrm.Entities.UserEntityInstanceData> userentityinstancedata_audit
        {
            [System.Diagnostics.DebuggerNonUserCode()]
            get
            {
                return this.GetRelatedEntities<Contoso.Xrm.Entities.UserEntityInstanceData>("userentityinstancedata_audit", null);
            }
            [System.Diagnostics.DebuggerNonUserCode()]
            set
            {
                this.OnPropertyChanging("userentityinstancedata_audit");
                this.SetRelatedEntities<Contoso.Xrm.Entities.UserEntityInstanceData>("userentityinstancedata_audit", null, value);
                this.OnPropertyChanged("userentityinstancedata_audit");
            }
        }

        /// <summary>
        /// N:1 lk_audit_callinguserid
        /// </summary>
        [Microsoft.Xrm.Sdk.AttributeLogicalNameAttribute("callinguserid")]
        [Microsoft.Xrm.Sdk.RelationshipSchemaNameAttribute("lk_audit_callinguserid")]
        public Contoso.Xrm.Entities.SystemUser lk_audit_callinguserid
        {
            [System.Diagnostics.DebuggerNonUserCode()]
            get
            {
                return this.GetRelatedEntity<Contoso.Xrm.Entities.SystemUser>("lk_audit_callinguserid", null);
            }
        }

        /// <summary>
        /// N:1 lk_audit_userid
        /// </summary>
        [Microsoft.Xrm.Sdk.AttributeLogicalNameAttribute("userid")]
        [Microsoft.Xrm.Sdk.RelationshipSchemaNameAttribute("lk_audit_userid")]
        public Contoso.Xrm.Entities.SystemUser lk_audit_userid
        {
            [System.Diagnostics.DebuggerNonUserCode()]
            get
            {
                return this.GetRelatedEntity<Contoso.Xrm.Entities.SystemUser>("lk_audit_userid", null);
            }
        }

        /// <summary>
        /// Constructor for populating via LINQ queries given a LINQ anonymous type
        /// <param name="anonymousType">LINQ anonymous type.</param>
        /// </summary>
        [System.Diagnostics.DebuggerNonUserCode()]
        public Audit(object anonymousType) : 
                this()
        {
            foreach (var p in anonymousType.GetType().GetProperties())
            {
                var value = p.GetValue(anonymousType, null);
                var name = p.Name.ToLower();

                if (name.EndsWith("enum") && value.GetType().BaseType == typeof(System.Enum))
                {
                    value = new Microsoft.Xrm.Sdk.OptionSetValue((int) value);
                    name = name.Remove(name.Length - "enum".Length);
                }

                switch (name)
                {
                    case "id":
                        base.Id = (System.Guid)value;
                        Attributes["auditid"] = base.Id;
                        break;
                    case "auditid":
                        var id = (System.Nullable<System.Guid>) value;
                        if(id == null){ continue; }
                        base.Id = id.Value;
                        Attributes[name] = base.Id;
                        break;
                    case "formattedvalues":
                        // Add Support for FormattedValues
                        FormattedValues.AddRange((Microsoft.Xrm.Sdk.FormattedValueCollection)value);
                        break;
                    default:
                        Attributes[name] = value;
                        break;
                }
            }
        }

        [Microsoft.Xrm.Sdk.AttributeLogicalNameAttribute("action")]
        public virtual Audit_Action? ActionEnum
        {
            [System.Diagnostics.DebuggerNonUserCode()]
            get
            {
                return ((Audit_Action?)(EntityOptionSetEnum.GetEnum(this, "action")));
            }
        }

        [Microsoft.Xrm.Sdk.AttributeLogicalNameAttribute("operation")]
        public virtual Audit_Operation? OperationEnum
        {
            [System.Diagnostics.DebuggerNonUserCode()]
            get
            {
                return ((Audit_Operation?)(EntityOptionSetEnum.GetEnum(this, "operation")));
            }
        }
    }
}

看看您的 DLaB.Xrm.Entities.dll 程序集,它有 4.5Mb,包含大约 700 个 类,其中许多 类 具有大量属性。 该程序集包含大约 1/3 的字符串(如果您使用二进制编辑器查看它)。

没有明显需要优化的地方。但是,这里有一些想法:

  • 删除所有可选属性。我会投票给 DebuggerNonUserCode、GeneratedCodeAttribute 和可能的 EnumMemberAttribute(你必须测试对你的环境有什么影响)。
  • 删除多余的内容。我会投票支持其字符串似乎已在别处声明的静态字段结构。
  • factor大了类比较多,比如这是一个常见的模式:

public partial class Account : Microsoft.Xrm.Sdk.Entity, System.ComponentModel.INotifyPropertyChanging, System.ComponentModel.INotifyPropertyChanged
{
    ...
    public Microsoft.Xrm.Sdk.OptionSetValue AccountRatingCode
    {
        [System.Diagnostics.DebuggerNonUserCode()]
        get
        {
            return this.GetAttributeValue<Microsoft.Xrm.Sdk.OptionSetValue>("accountratingcode");
        }
        [System.Diagnostics.DebuggerNonUserCode()]
        set
        {
            this.OnPropertyChanging("AccountRatingCode");
            this.SetAttributeValue("accountratingcode", value);
            this.OnPropertyChanged("AccountRatingCode");
        }
    }
    ...
}

我会尝试摆脱 OnPropxxx 调用,如下所示:

public partial class Account : MyBaseEntity
{
    ...
    public Microsoft.Xrm.Sdk.OptionSetValue AccountRatingCode
    {
        get
        {
            return this.GetAttributeValue<Microsoft.Xrm.Sdk.OptionSetValue>("accountratingcode");
        }
        set
        {
            this.SetAttributeValue("accountratingcode", value);
        }
    }
    ...
}

此外,所有实体 类 的构造函数似乎都遵循标准模式。也许可以看看。

无论如何,我已经使用 Roslyn CSharpSyntaxRewriter 进行了一些测试。通过我可以删除的所有内容,将组件的大小减少 20-25%。

无论你决定减少什么,写一个 Roslyn 重写器似乎都是很好的工具,所以这是我的示例代码:

class Program
{
    static void Main(string[] args)
    {
        Do().Wait();
    }

    static async Task Do()
    {
        var ws = MSBuildWorkspace.Create();
        var project = await ws.OpenProjectAsync(@"..\..\DLaB.Xrm.Entities\DLaB.Xrm.Entities.csproj"); // initial DLaB.Xrm.Entities project from your github

        Task.WaitAll(new List<Task>(project.Documents.Select(Rewrite)).ToArray());
    }

    static async Task Rewrite(Document doc)
    {
        var tree = await doc.GetSyntaxTreeAsync();
        var root = await tree.GetRootAsync();

        var optimizer = new EntitySizeOptimizer();
        var result = optimizer.Visit(root);
        string dir = @"..\..\DLaB.Xrm.Entities2"; // a copy of DLaB.Xrm.Entities project to compare size
        string path = Path.Combine(dir, string.Join(@"\", doc.Folders), doc.Name);
        File.WriteAllText(path, result.ToFullString());
        Console.WriteLine(path);
    }
}

class EntitySizeOptimizer : CSharpSyntaxRewriter
{
    // try the MyBaseEntity approach (note I just removed the OnPropXXX calls for testing, I've not created the whole base class, it's more work).
    static SyntaxNode Nop = SyntaxFactory.ParseExpression(""); // I'm sure there's something smarter than this...
    public override SyntaxNode VisitInvocationExpression(InvocationExpressionSyntax node)
    {
        string text = node.ToFullString().Trim();
        if (text.StartsWith("this.OnPropertyChang")) // bit of a hack...
            return Nop;

        return base.VisitInvocationExpression(node);
    }

    // remove Fields
    public override SyntaxNode VisitStructDeclaration(StructDeclarationSyntax node)
    {
        if (node.Identifier.ValueText == "Fields")
            return null;

        return base.VisitStructDeclaration(node);
    }

    // remove useless attributes
    public override SyntaxNode VisitAttributeList(AttributeListSyntax node)
    {
        var atts = new SeparatedSyntaxList<AttributeSyntax>().AddRange(FilterAttributes(node.Attributes));
        if (atts.Count == 0) // nothing left, remove this node
            return null;

        if (atts.Count == node.Attributes.Count) // no change, don't change the tree
            return base.VisitAttributeList(node);

        return node.WithAttributes(atts); // rewrite
    }

    private static IEnumerable<AttributeSyntax> FilterAttributes(IEnumerable<AttributeSyntax> atts)
    {
        foreach (var att in atts)
        {
            if (IsSameAttribute(att, typeof(EnumMemberAttribute).FullName))
                continue;

            if (IsSameAttribute(att, typeof(DebuggerNonUserCodeAttribute).FullName))
                continue;

            if (IsSameAttribute(att, typeof(GeneratedCodeAttribute).FullName))
                continue;

            yield return att;
        }
    }

    private static bool IsSameAttribute(AttributeSyntax node, string text)
    {
        return node.Name is QualifiedNameSyntax qn && IsSameAttribute(text, qn.ToFullString());
    }

    // note: we could (should?) use Roslyn Semantic Model, but this is faster...
    private static bool IsSameAttribute(string att1, string att2)
    {
        if (att1 == att2)
            return true;

        string StripAttribute(string att)
        {
            const string token = "Attribute";
            return att.EndsWith(token) ? att.Substring(0, att.Length - token.Length) : att;
        }
        return StripAttribute(att1) == StripAttribute(att2);
    }
}

在 CodeGolf 上有一个线程,其中包含缩短 C# 代码的所有技巧:https://codegolf.stackexchange.com/questions/173/tips-for-code-golfing-in-c

更新

Does less characters translate to less compiled bytes on disc?

我真的不认为它可以,如果可以的话不是很多

不幸的是,我提出了一个糟糕的例子,因为小的实际上更大,但它证明了可以通过代码 Golfing 最小化足迹的观点。 虽然效果不如 Simon 的方法那么有效。

小:

class Program
{
    static void Main(string[] args)
    {
        for (int i = 0;i< 5;i++)
        {
            Console.Write(i+"");
        }
    }
}

= 大小:5.00 KB(5,120 字节)

.method private hidebysig static 
    void Main (
        string[] args
    ) cil managed 
{
    // Method begins at RVA 0x2050
    // Code size 29 (0x1d)
    .maxstack 2
    .entrypoint
    .locals init (
        [0] int32 i
    )

    IL_0000: ldc.i4.0
    IL_0001: stloc.0
    IL_0002: br.s IL_0018
    // loop start (head: IL_0018)
        IL_0004: ldloc.0
        IL_0005: box [mscorlib]System.Int32
        IL_000a: call string [mscorlib]System.String::Concat(object)
        IL_000f: call void [mscorlib]System.Console::Write(string)
        IL_0014: ldloc.0
        IL_0015: ldc.i4.1
        IL_0016: add
        IL_0017: stloc.0

        IL_0018: ldloc.0
        IL_0019: ldc.i4.5
        IL_001a: blt.s IL_0004
    // end loop

    IL_001c: ret
} // end of method Program::Main

大:

class Program
{
    static void Main(string[] args)
    {
        for (int i = 0;i< 5;i++)
        {
            Console.Write(i.ToString());
        }
    }
}

= 大小:4.50 KB(4,608 字节)

.method private hidebysig static 
    void Main (
        string[] args
    ) cil managed 
{
    // Method begins at RVA 0x2050
    // Code size 25 (0x19)
    .maxstack 2
    .entrypoint
    .locals init (
        [0] int32
    )

    IL_0000: ldc.i4.0
    IL_0001: stloc.0
    IL_0002: br.s IL_0014
    // loop start (head: IL_0014)
        IL_0004: ldloca.s 0
        IL_0006: call instance string [mscorlib]System.Int32::ToString()
        IL_000b: call void [mscorlib]System.Console::Write(string)
        IL_0010: ldloc.0
        IL_0011: ldc.i4.1
        IL_0012: add
        IL_0013: stloc.0

        IL_0014: ldloc.0
        IL_0015: ldc.i4.5
        IL_0016: blt.s IL_0004
    // end loop

    IL_0018: ret
} // end of method Program::Main

至少我们可以看到这种压缩方法没有什么价值。