Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

OmitDefaults doesn't work for properties of sub-objects #840

Closed
lunarcloud opened this issue Aug 25, 2023 · 14 comments
Closed

OmitDefaults doesn't work for properties of sub-objects #840

lunarcloud opened this issue Aug 25, 2023 · 14 comments

Comments

@lunarcloud
Copy link

lunarcloud commented Aug 25, 2023

Describe the bug
When using "DefaultValuesHandling.OmitDefaults", I would expect serializing an object with an object inside to omit defaults of that sub-object.

To Reproduce

public class ExampleB
{
    public ExampleB() { }

    [DefaultValue(3)]
    public int PropOfB { get; set; } = 3;
}

public class ExampleA
{
    public ExampleA() { }

    //[DefaultValue(null)] - can't set to anything but null, because can't make a const ExampleB
    public ExampleB PropOfA { get; set; } = new;
}

//...

[TestMethod]
public void TestThis() {
    var instanceOfA = new ExampleA(); // leaving the defaults
    var text = new SerializerBuilder()
            .ConfigureDefaultValuesHandling(DefaultValuesHandling.OmitDefaults)
            .Build()
            .Serialize(instanceOfA);

    Assert.AreNotEqual("PropOfA:\r\n  PropOfB: 3\r\n", text) // fails
}
@lunarcloud
Copy link
Author

it seems to work if there are no objects inside that sub-object without defaults. I'm still not sure the exact scenarios this is and is-not working in

@EdwardCooke
Copy link
Collaborator

I haven’t looked to closely at this yet. But. According to this class/code in order to not output PropOfA you would need to set DefaultValue(null) on it.

The way you have your defaultvalue on the integer I would have assumed would work based on the below code. I’ll take a closer look maybe today.

var defaultValue = key.GetCustomAttribute<DefaultValueAttribute>()?.Value ?? GetDefault(key.Type);

@EdwardCooke
Copy link
Collaborator

I do feel there is a bug in there since defaults of an instance type is always null. So that should probably be taken into account in that class.

@lunarcloud
Copy link
Author

lunarcloud commented Aug 28, 2023

I do feel there is a bug in there since defaults of an instance type is always null. So that should probably be taken into account in that class.

So there's no way to omit a non-null default for a YAML sub-category or Array?

@EdwardCooke
Copy link
Collaborator

On an instance type you would need to add the defaultvalue of null attribute like you have commented out. I think. I haven’t tested or played with that. I was hoping to have time over the weekend but that didn’t happen.

@EdwardCooke
Copy link
Collaborator

Arrays are insurance type. So defaults of that would be null. But you couldn’t do an empty array as a default. If that makes sense.

@lunarcloud
Copy link
Author

Right, so you're confirming that only primitives can have non-null defaults ( and there's no way to say "this is a static-readonly value I consider default for you to omit").

Okay. That's dissapointing.
Default-omission is the only real way of writing configs that are safe from needing config versioning and a migrator tool between major software releases.

@EdwardCooke
Copy link
Collaborator

Ok. Got to spend about 5 minutes. You might. And I strongly emphasize might. Be able to create a custom System.ComponentModel.TypeDescriptionProvider. Register it with System.ComponentModel.TypeConverter.AddProvider.

Then when you specify the DefaultValue attribute put the type and probably and empty string.

You will also need to create an Equals override method on your class. Then you could do all sorts of things. No guarantees that it will work though.

@ecooke-macu
Copy link

Figured it out for you using TypeConverters and no changes necessary to the YamlDotNet code base. The following code will exclude the PropOfA attribute from the yaml if PropOfB is 3. This results in the output of {} (an empty Yaml). Is that what you are looking for?

var instanceOfA = new ExampleA(); // leaving the defaults
var text = new SerializerBuilder()
        .ConfigureDefaultValuesHandling(DefaultValuesHandling.OmitDefaults)
        .Build()
        .Serialize(instanceOfA);
Console.WriteLine(text);
Console.ReadLine();

public class ExampleBTypeConverter : TypeConverter
{
    public override bool CanConvertFrom(ITypeDescriptorContext? context, Type sourceType)
    {
        return sourceType == typeof(string);
    }

    public override object? ConvertFrom(ITypeDescriptorContext? context, CultureInfo? culture, object value)
    {
        if (string.IsNullOrEmpty(value as string))
        {
            // Default value to check against
            return new ExampleB { PropOfB = 3 };
        }

        return null;
    }
}

[TypeConverter(typeof(ExampleBTypeConverter))]
public class ExampleB
{
    public ExampleB() { }

    public int PropOfB { get; set; } = 3;

    public override bool Equals(object? obj)
    {
        return (obj as ExampleB)?.PropOfB == PropOfB;
    }
}

public class ExampleA
{
    public ExampleA() { }

    [DefaultValue(typeof(ExampleB), null)]
    public ExampleB PropOfA { get; set; } = new();
}

@ecooke-macu
Copy link

If you want to exclude PropOfB from the output, where PropOfA is still there, then this works by outputting PropOfA: {}:

var instanceOfA = new ExampleA(); // leaving the defaults
var text = new SerializerBuilder()
        .ConfigureDefaultValuesHandling(DefaultValuesHandling.OmitDefaults)
        .Build()
        .Serialize(instanceOfA);
Console.WriteLine(text);
Console.ReadLine();


public class ExampleB
{
    public ExampleB() { }

    [DefaultValue(3)]
    public int PropOfB { get; set; } = 3;

    public override bool Equals(object? obj)
    {
        return (obj as ExampleB)?.PropOfB == PropOfB;
    }
}

public class ExampleA
{
    public ExampleA() { }

    public ExampleB PropOfA { get; set; } = new();
}

@lunarcloud
Copy link
Author

It doesn't solve my issues with arrays, but it absolutely fixes the bug report as it's written.

@ecooke-macu
Copy link

If you don't have to have it as an array, you could create another class inheriting List<ExampleB>. There's also another option if you do have to have it exposed as an array, use [YamlMember] and [YamlIgnore] and have one property be this subclassed list, and another being the array.

Like this:

var instanceOfA = new ExampleA
{
    PropOfA = new ExampleBList
    {
        new ExampleB
        {
            PropOfB = 3
        }
    }
};

var text = new SerializerBuilder()
        .ConfigureDefaultValuesHandling(DefaultValuesHandling.OmitDefaults)
        .Build()
        .Serialize(instanceOfA);
Console.WriteLine(text);
Console.ReadLine();

public class ExampleB
{
    public ExampleB() { }

    public int PropOfB { get; set; }

    public override bool Equals(object? obj)
    {
        return (obj as ExampleB)?.PropOfB == PropOfB;
    }
}

public class ExampleA
{
    public ExampleA() { }

    [DefaultValue(typeof(ExampleBList), null)]
    public ExampleBList PropOfA { get; set; }
}

public class ExampleBListTypeConverter : TypeConverter
{
    public override bool CanConvertFrom(ITypeDescriptorContext? context, Type sourceType)
    {
        return sourceType == typeof(string);
    }

    public override object? ConvertFrom(ITypeDescriptorContext? context, CultureInfo? culture, object value)
    {
        if (string.IsNullOrEmpty(value as string))
        {
            return new ExampleBList
            {
                new ExampleB()
                {
                     PropOfB = 3
                }
            };
        }

        return null;
    }
}

[TypeConverter(typeof(ExampleBListTypeConverter))]
public class ExampleBList : List<ExampleB>
{
    public override bool Equals(object? obj)
    {
        var list = obj as IEnumerable<ExampleB>;

        if (list != null)
        {
            if (this.Count == list.Count())
            {
                foreach (var item in this)
                {
                    //make sure each item in the incoming list only exists once in this list
                    if (list.Where(x => x.Equals(item)).Count() != 1)
                    {
                        return false;
                    }
                }
                return true;
            }
        }
        return false;
    }
}

@ecooke-macu
Copy link

ecooke-macu commented Aug 28, 2023

If you need to have it exposed as an array, you can do something like this with the YamlMember and YamlIgnore attributes.

var instanceOfA = new ExampleA
{
    PropOfA = new []
    {
        new ExampleB
        {
            PropOfB = 3
        }
    }
};

var text = new SerializerBuilder()
        .ConfigureDefaultValuesHandling(DefaultValuesHandling.OmitDefaults)
        .Build()
        .Serialize(instanceOfA);
Console.WriteLine(text);
Console.ReadLine();

public class ExampleB
{
    public ExampleB() { }

    public int PropOfB { get; set; }

    public override bool Equals(object? obj)
    {
        return (obj as ExampleB)?.PropOfB == PropOfB;
    }
}

public class ExampleA
{
    public ExampleA() { }

    [DefaultValue(typeof(ExampleBList), null)]
    [YamlMember(Alias = "PropOfA")]
    public ExampleBList PropOfADefaulted { get; set; }

    [YamlIgnore]
    public ExampleB[] PropOfA
    {
        get
        {
            return PropOfADefaulted.ToArray();
        }
        set
        {
            PropOfADefaulted = new ExampleBList(value);
        }
    }
}

public class ExampleBListTypeConverter : TypeConverter
{
    public override bool CanConvertFrom(ITypeDescriptorContext? context, Type sourceType)
    {
        return sourceType == typeof(string);
    }

    public override object? ConvertFrom(ITypeDescriptorContext? context, CultureInfo? culture, object value)
    {
        if (string.IsNullOrEmpty(value as string))
        {
            return new ExampleBList
            {
                new ExampleB()
                {
                     PropOfB = 3
                }
            };
        }

        return null;
    }
}

[TypeConverter(typeof(ExampleBListTypeConverter))]
public class ExampleBList : List<ExampleB>
{
    public ExampleBList() { }
    public ExampleBList(IEnumerable<ExampleB> collection) : base(collection) { }

    public override bool Equals(object? obj)
    {
        var list = obj as IEnumerable<ExampleB>;

        if (list != null)
        {
            if (this.Count == list.Count())
            {
                foreach (var item in this)
                {
                    //make sure each item in the incoming list only exists once in this list
                    if (list.Where(x => x.Equals(item)).Count() != 1)
                    {
                        return false;
                    }
                }
                return true;
            }
        }
        return false;
    }
}

@lunarcloud
Copy link
Author

lunarcloud commented Aug 29, 2023

Thank you very much. No my secondary problem was with having a default value for a primitive array like int[] but this is very very helpful info.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants