Daryl
Daryl

Reputation: 18895

How To Minimize Assembly Size On Disk Of Generated Code?

I'm generating DTO objects for an online platform (Dynamics 365). There are pretty strict constraints on maximum assembly size, and just the generated DTO objects take up about 80% of that limit. What changes can I make to the code that is being generated to decrease the amount of space on disk the compiled assembly takes up?

Here is an example class that is generated:

//------------------------------------------------------------------------------
// <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")));
            }
        }
    }
}

Upvotes: 4

Views: 574

Answers (2)

Simon Mourier
Simon Mourier

Reputation: 139187

Looking at your DLaB.Xrm.Entities.dll assembly, which is 4.5Mb, it contains around 700 classes, and many of these classes have a big numbers of properties. The assembly contains about 1/3 of strings (if you look at it with a binary editor).

There's nothing obvious to optimize. However, here are some ideas:

  • remove all optional attributes. I'd vote for DebuggerNonUserCode, GeneratedCodeAttribute and possibly EnumMemberAttribute (you'll have to test what is the impact in your environment).
  • remove what's redundant. I'd vote for the static Fields structure whose strings seem to already be declared elsewhere.
  • factor the big classes more, for example, this is a common pattern:

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");
        }
    }
    ...
}

I'd try to get rid of OnPropxxx calls, something like this:

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);
        }
    }
    ...
}

Also, the constructor of all entity classes seems to do something that follows a standard pattern. Maybe that could be looked at.

Anyway, I've done some testing using a Roslyn CSharpSyntaxRewriter. And with all that I can remove reduce the size of the assembly by 20-25%.

Whatever you decide to reduce, writing a Roslyn rewriter seems the good tool for this anyway, so here is my sample code:

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);
    }
}

Upvotes: 4

Jeremy Thompson
Jeremy Thompson

Reputation: 65702

Over at CodeGolf there is a thread with all the tricks to shorten C# Code: https://codegolf.stackexchange.com/questions/173/tips-for-code-golfing-in-c

Update

Does less characters translate to less compiled bytes on disc?

I don't actually think it can, if so not by much.

Unfortunately I whipped up a bad example because the small is actually larger, never the less it proves the point you can minimise the footprint by Code Golfing. Albeit, not nearly as effective as Simon's approach would yield.

Small:

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

= Size: 5.00 KB (5,120 bytes)

.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

Large:

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

= Size: 4.50 KB (4,608 bytes)

.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

At least we can see this approach of compression has little value.

Upvotes: 0

Related Questions