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

Firestore: more flexible serialization #2534

Closed
pauloevpr opened this issue Sep 25, 2018 · 19 comments
Closed

Firestore: more flexible serialization #2534

pauloevpr opened this issue Sep 25, 2018 · 19 comments
Assignees
Labels
api: firestore Issues related to the Firestore API. priority: p2 Moderately-important priority. Fix may not be included in next release. type: feature request ‘Nice-to-have’ improvement, new feature or different behavior or design.

Comments

@pauloevpr
Copy link

pauloevpr commented Sep 25, 2018

I am having some issues trying to get Firestore API to convert between StringValue and DDD value objects which are fully convertible from/to string. Let me give an example:

[FirestoreData]
public class UserAccount
{
     [FirestoreProperty]
     public Email EmailAddress { get; set; }
}
public class Email 
{ 
     // Email has a TypeConverter implementation that allow full conversion from/to string
     // Email also has implicit operators to allow direct conversion from/to string
}

When calling snapshot.ConvertTo<UserAccount>(), ValueDeserializer fails with System.ArgumentException: 'Unable to convert value type StringValue to Email'.

It seems ValueDeserializer ignores type converters and implicit operators completely.

@pauloevpr
Copy link
Author

The issue is the same for ValueSerializer.

@jskeet
Copy link
Collaborator

jskeet commented Sep 25, 2018

I definitely don't want to support TypeConverter - that would have to be for the net45 target only (as it's not in netstandard1.5), which could cause some really subtle issues. Adding a dependency on an extra package could change which version of Google.Cloud.Firestore is used, suddenly breaking code.

While we could support the implicit conversion, it does introduce extra complexity which I'd really rather not - as per our previous discussions. (If there are multiple implicit conversions, which one is used? How does inheritance play into this? What about explicit conversions?) Now that private properties are supported, it's easy to work around this though with no library change:

[FirestoreData]
public class UserAccount
{
    public Email EmailAddress { get; set; }

    // Private property to serialize the data for Firestore
    [FirestoreProperty("email")]    
    private string FirestoreEmail { get => EmailAddress; set => EmailAddress = value; }
}

@jskeet jskeet self-assigned this Sep 25, 2018
@jskeet jskeet added type: feature request ‘Nice-to-have’ improvement, new feature or different behavior or design. priority: p2 Moderately-important priority. Fix may not be included in next release. labels Sep 25, 2018
@pauloevpr
Copy link
Author

Thank you for your response.

I wasn't aware of net45 dependency of TypeConverter. I will agree with you on this. Keeping a low netstandard dependency is definitely something I support.

I will also agree that implicit conversation is not a good solution either.

Since this is clearly a feature request, I would like to propose a more generic solution to handle this and other scenarios where some sort of custom serialization is required:

Provide a generic interface like IFirestoreValueSerializer:

public interface IFirestoreValueSerializer 
{
     Value SerializeValue(object value);
     object DeserializeValue(Value value);
}

Allow FirestorePropertyAttribute to receive a Type which is an implementation of IFirestoreValueSerializer. So consumers can set a custom serialization mechanism for a specific property:

[FirestoreData]
public class UserAccount
{
    [FirestoreProperty("Email", FirestoreValueSerializerType = typeof(MyValueSerializer))]    
    public Email Email { get; set; }
}

@jskeet
Copy link
Collaborator

jskeet commented Sep 25, 2018

That sounds reasonable - I suspect there'll need to be a lot of warnings about not reusing Value instances etc, but it will at least be pretty flexible. I'll need to check that the Firestore team is happy with this plan as well.

I suspect the DeserializeValue method will want to take a target type as well, which would be the declared type of the property.

It's unlikely that I'll get to this for about a month I'm afraid due to other commitments - but at least the workaround should help you out for the moment.

@pauloevpr
Copy link
Author

I am happy with the workaround for now. Thank you.

Btw, are pull requests welcome on this repository?

@jskeet
Copy link
Collaborator

jskeet commented Sep 25, 2018

Yes, although I may well not be able to prioritize reviewing a PR any higher than writing the code itself, I'm afraid :(

@pauloevpr
Copy link
Author

Any updates on this case?

@jskeet
Copy link
Collaborator

jskeet commented Oct 29, 2018

No updates at the moment, other than that I need to write a doc to present to the Firestore team about this. (We're not developing in a vacuum here - it gives us slightly less flexibility, but will help with consistency.)

I do have that on my work backlog, but Spanner work is higher priority right now.

@pauloevpr
Copy link
Author

Thank you for the feedback.

@jskeet jskeet changed the title Firestore: ValueDeserializer does not use TypeConverter and implicit operators Firestore: more flexible serialization Nov 2, 2018
jskeet added a commit to jskeet/google-cloud-dotnet that referenced this issue Nov 6, 2018
This is to demonstrate *one* possible approach to googleapis#2534.
@jskeet
Copy link
Collaborator

jskeet commented Nov 6, 2018

Right, we've made some progress in internal discussions, and we'd like some feedback if possible.

We'd like the interface to use POCOs rather than protos, so that users don't have to know anything about protos at all. At first glance, that would suggest an interface like this:

public interface IFirestoreValueSerializer 
{
     object SerializeValue(object value);
     object DeserializeValue(object value);
}

However, I suspect it's actually going to be cleaner to view this as an interface that you use to decorate a type rather than a property. I've written a prototype with this interface instead:

/// <summary>
/// Converter used during serialization
/// </summary>
public interface IFirestoreConverter<T>
{
    /// <summary>
    /// Converts a value to its Firestore representation.
    /// </summary>
    /// <param name="value"></param>
    /// <returns></returns>
    object ToFirestore(T value);

    /// <summary>
    /// Converts a value from its Firestore representation.
    /// </summary>
    /// <param name="value"></param>
    /// <returns></returns>
    T FromFirestore(object value);
}

The user code might look like this:

[FirestoreData]
public sealed class CustomUser
{
    [FirestoreProperty]
    public int HighScore { get; set; }

    [FirestoreProperty]
    public string Name { get; set; }

    [FirestoreProperty]
    public Email Email { get; set; }
}

[FirestoreData(Converter = typeof(EmailConverter))]
public sealed class Email
{
    public string Address { get; }

    public Email(string address) => Address = address;
}

public class EmailConverter : IFirestoreConverter<Email>
{
    public Email FromFirestore(object value)
    {
        switch (value)
        {
            case null: return null;
            case string address: return new Email(address);
            default: throw new ArgumentException($"Unexpected data: {value.GetType()}");
        }
    }

    public object ToFirestore(Email value) => value?.Address;
}

The document would just be a map with two string values and one integer value. Types with custom converters could only be used as document "root" types if they serialized to dictionaries, but that's probably a reasonable restriction.

To be clear, this is not a promise that this is the direction we'll take, but it's an initial prototype just for feedback. Do you believe this would satisfy your requirements?

@pauloevpr
Copy link
Author

Hi John, thank for your feedback.

I will follow you on this one. Decorating a type rather than every property of that type is much cleaner. This would completely satisfy my requirements.

Regarding IFirestoreConverter, I was wondering if T FromFirestore(object value); should have a Value parameter instead of object. It seems easier to work with Value.

@jskeet
Copy link
Collaborator

jskeet commented Nov 6, 2018

The intention is that users shouldn't need to know about Value at all. (The Firestore team is very keen on this.) Instead, you'd just deal with regular .NET types - the "natural" deserialization of each Value, including Dictionary<string, object> for maps etc.

It's possible that we could add an option to the interface saying "Just pass me the value, I'll do everything" but that would probably come later.

@pauloevpr
Copy link
Author

It makes sense. I am happy with this solution.

@jskeet
Copy link
Collaborator

jskeet commented Nov 6, 2018

Hooray. I'll keep this issue updated with progress. (Even if the API is exactly as written, my current prototype implementation is decidedly not production-ready, or comprehensively tested!)

Thanks for all the feedback, helping to make the API better for all users.

@pauloevpr
Copy link
Author

Thank you.

@jskeet
Copy link
Collaborator

jskeet commented Nov 27, 2018

PR #2734 is the first pass of the "real" version of this.

jskeet added a commit to jskeet/google-cloud-dotnet that referenced this issue Nov 29, 2018
jskeet added a commit to jskeet/google-cloud-dotnet that referenced this issue Nov 29, 2018
jskeet added a commit to jskeet/google-cloud-dotnet that referenced this issue Nov 29, 2018
jskeet added a commit that referenced this issue Nov 29, 2018
This completes the work for #2534.
@jskeet
Copy link
Collaborator

jskeet commented Nov 29, 2018

Implemented in #2739 and #2734. I'll cut a new release soon (hopefully today) and update this issue accordingly.

@jskeet jskeet closed this as completed Nov 29, 2018
@jskeet
Copy link
Collaborator

jskeet commented Nov 29, 2018

Available in version 1.0.0-beta14.

@pauloevpr
Copy link
Author

Great! Thank you very much. I will give it a try next week.

@jskeet jskeet added api: firestore Issues related to the Firestore API. and removed api: firestore Issues related to the Firestore API. labels Apr 17, 2019
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
api: firestore Issues related to the Firestore API. priority: p2 Moderately-important priority. Fix may not be included in next release. type: feature request ‘Nice-to-have’ improvement, new feature or different behavior or design.
Projects
None yet
Development

No branches or pull requests

2 participants