Any code that duplicates how the DebuggerDisplayAttribute generates the resulting string?

后端 未结 2 1573
甜味超标
甜味超标 2021-02-14 10:29

Anyone know of any code that duplicates how the DebuggerDisplayAttribute parses and gathers the resultant string?

I would like to create a custom attribute

2条回答
  •  滥情空心
    2021-02-14 11:12

    Hopefully this code all fits... I made a non-reflection version of what you are trying to do, using Microsoft Roslyn and its C# Scripting ability to run the "code" in the attribute value as C# code.

    To use this code, make a new C# project, and use NuGet to add a reference to Roslyn.

    First the classes I'm using to test, just so you can see the attributes I tried.

    using System.Diagnostics;
    
    namespace DebuggerDisplayStrings
    {
        [DebuggerDisplay("The Value Is {StringProp}.")]
        public class SomeClass
        {
            public string StringProp { get; set; }
        }
    
        [DebuggerDisplay("The Value Is {Foo.StringProp}.")]
        public class SomeClass2
        {
            public SomeClass Foo { get; set; }
        }
    
        [DebuggerDisplay("The Value Is {Seven() - 6}.")]
        public class SomeClass3
        {
            public int Seven()
            {
                return 7;
            }
        }
    }
    

    Now the tests (yes these all pass):

    using Microsoft.VisualStudio.TestTools.UnitTesting;
    
    namespace DebuggerDisplayStrings
    {
        [TestClass]
        public class DebuggerDisplayReaderTests
        {
            [TestMethod]
            public void CanReadStringProperty()
            {
                var target = new SomeClass {StringProp = "Foo"};
                var reader = new DebuggerDisplayReader();
                Assert.AreEqual("The Value Is Foo.", reader.Read(target));
            }
    
            [TestMethod]
            public void CanReadPropertyOfProperty()
            {
                var target = new SomeClass2 {Foo = new SomeClass {StringProp = "Foo"}};
                var reader = new DebuggerDisplayReader();
                Assert.AreEqual("The Value Is Foo.", reader.Read(target));
            }
    
            [TestMethod]
            public void CanReadMethodResultAndDoMath()
            {
                var target = new SomeClass3();
                var reader = new DebuggerDisplayReader();
                Assert.AreEqual("The Value Is 1.", reader.Read(target));
            }
        }
    }
    

    Finally, the real goods:

    using System.Collections.Generic;
    using System.Diagnostics;
    using System.Globalization;
    using System.Text.RegularExpressions;
    using Roslyn.Scripting.CSharp;
    
    namespace DebuggerDisplayStrings
    {
        public class DebuggerDisplayReader
        {
            // Get the fully evaluated string representation of the DebuggerDisplayAttribute's value.
            public string Read(object target)
            {
                var debuggerDisplayFormat = GetDebuggerDisplayFormat(target);
                if(string.IsNullOrWhiteSpace(debuggerDisplayFormat))
                    return target.ToString();
                return EvaluateDebuggerDisplayFormat(debuggerDisplayFormat, target);
            }
    
            // Gets the string off the attribute on the target class, or returns null if attribute not found.
            private static string GetDebuggerDisplayFormat(object target)
            {
                var attributes = target.GetType().GetCustomAttributes(typeof(DebuggerDisplayAttribute), false);
                return attributes.Length > 0 ? ((DebuggerDisplayAttribute)attributes[0]).Value : null;
            }
    
            // Executes each bracketed portion of the format string using Roslyn,
            // and puts the resulting value back into the final output string.
            private string EvaluateDebuggerDisplayFormat(string format, object target)
            {
                var scriptingEngine = new ScriptEngine(new[] { GetType().Assembly });
                var formatInfo = ExtractFormatInfoFromFormatString(format);
                var replacements = new List(formatInfo.FormatReplacements.Length);
                foreach (var codePart in formatInfo.FormatReplacements)
                {
                    var result = scriptingEngine.Execute(codePart, target);
                    replacements.Add((result ?? "").ToString());
                }
                return string.Format(formatInfo.FormatString, replacements.ToArray());
            }
    
            // Parse the format string from the attribute into its bracketed parts.
            // Prepares the string for string.Format() replacement.
            private static DebuggerDisplayFormatInfo ExtractFormatInfoFromFormatString(string format)
            {
                var result = new DebuggerDisplayFormatInfo();
                var regex = new Regex(@"\{(.*)\}");
                var matches = regex.Matches(format);
                result.FormatReplacements = new string[matches.Count];
                for (var i = matches.Count - 1; i >= 0; i-- )
                {
                    var match = matches[i];
                    result.FormatReplacements[i] = match.Groups[1].Value;
                    format = format.Remove(match.Index + 1, match.Length - 2).Insert(match.Index+1, i.ToString(CultureInfo.InvariantCulture));
                }
                result.FormatString = format;
                return result;
            }
        }
    
        internal class DebuggerDisplayFormatInfo
        {
            public string FormatString { get; set; }
            public string[] FormatReplacements { get; set; }
        }
    }
    
    
    

    Hopefully that helps you out. It was only about an hour and a half of work, so the unit testing isn't complete by any means, and I'm sure there are bugs in there somewhere, but it should be a solid start, if you are OK with the Roslyn approach.

    提交回复
    热议问题