'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.
Solution 1:[1]
Here are two solutions:
- 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; }
}
- 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; }
}
Sources
This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.
Source: Stack Overflow
Solution | Source |
---|---|
Solution 1 |