Skip to content

Instantly share code, notes, and snippets.

@haacked
Last active April 21, 2022 20:05
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save haacked/b47ed096201a47c9ebceb7397c09d28b to your computer and use it in GitHub Desktop.
Save haacked/b47ed096201a47c9ebceb7397c09d28b to your computer and use it in GitHub Desktop.
Upcast record without more specific type properties
// Suppose we have the two record types
public record Person(int Id) : PersonUpdate()
{
};
public record PersonUpdate()
{
public string Name {get; init;}
//... bunch of other properties.
};
// And the following third party API (I don't control it).
public interface IApi
{
Task<Person> GetPersonAsync(int id);
Task UpdatePersonAsync(int id, PersonUpdate personUpdate); // API rejects properties it doesn't expect such as "Id".
}
// What I want to do is get a `PersonUpdate` an existing `Person` without the `Id` property.
// Here's what I tried.
var person = await GetPersonAsync(42);
var personUpdate = (PersonUpdate)person with {Name: "new name"};
await UpdatePersonAsync(42, personUpdate); // Fails because personUpdate is still a `Person` with the `Id` property of `Person`.
// I also tried this.
public record PersonUpdate()
{
public PersonUpdate(PersonUpdate personUpdate) : base(personUpdate)
{
}
public string Name {get; init;}
//... bunch of other properties.
};
var personUpdate = new PersonUpdate(person); // Nothing got copied.
@NickCraver
Copy link

In case it's useful, LINQPad example when able to adjust serialization for this case:

void Main()
{
    var person = new Person(42)
    {
        Name = "Test"
    };
    JsonConvert.SerializeObject(person).Dump();
    var downcast = (PersonUpdate)person;
    JsonConvert.SerializeObject(downcast).Dump();

    var settings = new JsonSerializerSettings()
    {
        ContractResolver = new StrictContractResolver<PersonUpdate>()
    };
    JsonConvert.SerializeObject(downcast, settings).Dump();
}

public record Person(int Id) : PersonUpdate() {}

public record PersonUpdate()
{
    public string Name { get; init; }
}

public class StrictContractResolver<T> : DefaultContractResolver
{   
    protected override JsonProperty CreateProperty(MemberInfo memberInfo, MemberSerialization memberSerialization)
    {
        JsonProperty property = base.CreateProperty(memberInfo, memberSerialization);
        property.ShouldSerialize = instance => property.DeclaringType == typeof(T);
        return property;
    }
}

@haacked
Copy link
Author

haacked commented Apr 20, 2022

So it turns out that records have the copy constructor I want, but it's protected. So the solution was this:

public record PersonUpdate()
{
    // Properties

    public static PersonUpdate LossyCopy(PersonUpdate personUpdate) => new(personUpdate);
}

@sbenzenko
Copy link

sbenzenko commented Apr 21, 2022

So you're calling api like:

await UpdatePersonAsync(PersonUpdate.LossyCopy(person));

?

@haacked
Copy link
Author

haacked commented Apr 21, 2022

@sbenzenko exactly, though in practice it might look like this:

var person = await GetPersonAsync(42);
var personUpdate = PersonUpdate.LossyCopy(person) with { Name = "New Name };
await UpdatePersonAsync(42, personUpdate);

That would be how I would change the name of a person.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment