8

Use case

Angular Firebase application that uses firestore as a form of persistence needs to communicate with a Discord Bot. I've built a synchronizer bot to mediate between the existing external bot and the web application. There is sufficient information for document to be found and update to occur.

Problem

Update does not happen due to problem with conversion.
Exception: Unable to create converter for type Models.Participant

Question

After attempting several solutions, mostly using json conversion, I've simplified the code in order to get/give a better grasp of the situation. I'm assuming something obvious is lacking but due to my inexperience with firebase (firestore) I'm unable to see what at this point.

public async Task<bool> NextTurn(string encounterName)
{
    var encounterSnapshotQuery = await _encountersCollection.WhereEqualTo("name", encounterName).GetSnapshotAsync();

    foreach (DocumentSnapshot encounterSnapshot in encounterSnapshotQuery.Documents)
    {
        Dictionary<string, object> data = encounterSnapshot.ToDictionary();
        var name = data["name"].ToString();

        if (name == encounterName)
        {
            var participants = data["participants"].ToParticipants();
            var orderedParticipants = participants.OrderByDescending(x => x.initiative + x.roll).ToList();

            var current = orderedParticipants.Single(x => x.isCurrent != null && x.isCurrent is bool && (bool)x.isCurrent);
            var currentIndex = orderedParticipants.FindIndex(x => x.characterName == current.characterName);
            var next = orderedParticipants[currentIndex + 1];

            current.hasPlayedThisTurn = true;
            current.isCurrent = false;
            next.isCurrent = true;

            var updates = new Dictionary<FieldPath, object>
            {
                { new FieldPath("participants"),  orderedParticipants }
            };

            try
            {
                await encounterSnapshot.Reference.UpdateAsync(updates);
            }
            catch (Exception ex)
            {
                _logger.LogError(new EventId(), ex, "Update failed.");
            }
        }

    }

    return true;
}

If there are obvious mistakes in approach suggestions are also welcome.

Update

Full exception message:

 at Google.Cloud.Firestore.Converters.ConverterCache.CreateConverter(Type targetType)
   at Google.Cloud.Firestore.Converters.ConverterCache.<>c.<GetConverter>b__1_0(Type t)
   at System.Collections.Concurrent.ConcurrentDictionary`2.GetOrAdd(TKey key, Func`2 valueFactory)
   at Google.Cloud.Firestore.Converters.ConverterCache.GetConverter(Type targetType)
   at Google.Cloud.Firestore.SerializationContext.GetConverter(Type targetType)
   at Google.Cloud.Firestore.ValueSerializer.Serialize(SerializationContext context, Object value)
   at Google.Cloud.Firestore.Converters.ListConverterBase.Serialize(SerializationContext context, Object value)
   at Google.Cloud.Firestore.ValueSerializer.Serialize(SerializationContext context, Object value)
   at Google.Cloud.Firestore.WriteBatch.<>c__DisplayClass12_0.<Update>b__1(KeyValuePair`2 pair)
   at System.Linq.Enumerable.ToDictionary[TSource,TKey,TElement](IEnumerable`1 source, Func`2 keySelector, Func`2 elementSelector, IEqualityComparer`1 comparer)
   at System.Linq.Enumerable.ToDictionary[TSource,TKey,TElement](IEnumerable`1 source, Func`2 keySelector, Func`2 elementSelector)
   at Google.Cloud.Firestore.WriteBatch.Update(DocumentReference documentReference, IDictionary`2 updates, Precondition precondition)
   at Google.Cloud.Firestore.DocumentReference.UpdateAsync(IDictionary`2 updates, Precondition precondition, CancellationToken cancellationToken)

Participant Model

public class Participant
{
    public string playerName { get; set; }
    public int experience { get; set; }
    public int level { get; set; }
    public string characterName { get; set; }
    public string playerUid { get; set; }
    public object joined { get; set; }
    public string type { get; set; }
    public object abilities { get; set; }
    public int roll { get; set; }
    public bool? isCurrent { get; set; }
    public int sizeModifier { get; set; }
    public int initiative { get; set; }
    public bool? hasPlayedThisTurn { get; set; }
    public string portraitUrl { get; set; }
}

Participant typescript interface used to create the model on firestore

export interface Participant {
    playerName: string,
    characterName: string,
    initiative: number,
    roll: number,
    playerUid: string,
    joined: Date,
    portraitUrl: string,
    level: number,
    experience: number,
    isCurrent: boolean,
    sizeModifier: number,
    type: string,
    abilities: {
        strength: number,
        dexterity: number,
        constitution: number,
        intelligence: number,
        wisdom: number,
        charisma: number
    },
    hasPlayedThisTurn: boolean
}

Do note that I've played around with changing the C# model to try and fix this. This is the current state. Message was the same regardless of what changes I've made.

rexdefuror
  • 573
  • 2
  • 13
  • 33
  • 1
    in c# the type that is being serialized automatically by firestore needs to be convertable by firestore. If I remember correctly there should be a bit more to that error, such as what causes the converter to fail possibly in the error call stack? Most of the code here is irrelevant because it just has to do with the definition of the Participants class. Does the error occur on this line: `await encounterSnapshot.Reference.UpdateAsync(updates);`? could you provide the definition for Models.Participant? – Phillip Jan 17 '20 at 21:14
  • Yes that is where the error happens and for the rest I have updated the question. – rexdefuror Jan 17 '20 at 21:29
  • cool thanks, i'll take a look – Phillip Jan 17 '20 at 21:51

1 Answers1

18

Here are two solutions:

  1. You need to decorate the class with
[FirestoreData]
public class Participant
{
    [FirestoreProperty]
    public string playerName { get; set; }

    [FirestoreProperty("playerExperience")] //you can give the properties custom names as well
    public int experience { get; set; }
 
    //so on
    public int level { get; set; }
    public string characterName { get; set; }
    public string playerUid { get; set; }
    public object joined { get; set; }
    public string type { get; set; }
    public object abilities { get; set; }
    public int roll { get; set; }
    public bool? isCurrent { get; set; }
    public int sizeModifier { get; set; }
    public int initiative { get; set; }
    public bool? hasPlayedThisTurn { get; set; }
    public string portraitUrl { get; set; }
}

  1. By converting the participant to an ExpandoObject ExpandoObject Reference

Converting the object without using Newtonsoft.Json is recommended: How do you convert any C# object to an ExpandoObject?

However using Newtonsoft.Json it is easy to understand and is what I do:

var serializedParticipant = JsonConvert.SerializeObject(participant);
var deserializedParticipant = JsonConvert.DeserializeObject<ExpandoObject>(serializedParticipant);

//setting the document
await documentReference.UpdateAsync(deserializedParticipant);

and then updating firestore with your Participants as an ExpandoObject instead of Model.Participant

With this method you may want to change the names of the written objects. You can do so with NamingStrategy:

var contractResolver = new DefaultContractResolver
{
    NamingStrategy = new CamelCaseNamingStrategy
    {
        OverrideSpecifiedNames = false
    }
};

var serializedParticipant = JsonConvert.SerializeObject(participant, new JsonSerializerSettings
{
    ContractResolver = contractResolver,
    Formatting = Formatting.Indented
});

var deserializedParticipant = JsonConvert.DeserializeObject<ExpandoObject>(serializedParticipant);

or by specifying the names explicitly:

[FirestoreData]
public class Participant
{    
    [JsonProperty("playerName")]
    [FirestoreProperty("playerName")]
    public string PlayerName { get; set; }

    [JsonProperty("playerExperience")]
    [FirestoreProperty("playerExperience")]
    public int Experience { get; set; }
}
Phillip
  • 631
  • 6
  • 15
  • Combination of attribute and expando object actually worked. Thanks a lot. – rexdefuror Jan 17 '20 at 22:58
  • 3
    I'm glad it worked! btw I believe if you're using the ExpandoObject way I don't think you need any firestore attributes unless you're converting it normally in another place. I do recommend using just the Firestore attributes but there are cases where it doesn't work. – Phillip Jan 17 '20 at 23:03
  • ExpandoObject worked for me. It is easier than using custom convertors. – Thilina Koggalage Jul 25 '21 at 20:37