Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Updating a document in Firebase Firestore from .Net Core console app

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.

like image 944
rexdefuror Avatar asked Dec 31 '22 08:12

rexdefuror


1 Answers

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; }
}
like image 139
Phillip Avatar answered Feb 02 '23 20:02

Phillip