Cast object[] to generic T

73 Views Asked by At

We're overriding the default Cosmos serialiser so we can put in custom schema on the fly migrations for cosmos documents when being read from Cosmos.

We've done this by creating our own Microsoft.Azure.Cosmos.CosmosSerializer.

Works well when reading a single document, but when deserialising an array of documents, when public override T FromStream<T>(Stream stream) is called, the type of T is an array of a type.

Anyway, so we loop through each document and do the schema upgrade, and end up with a object[], however the compiler won't allow a conversion of object[] to T even though the objects are all of element array type. Trying to instead do the conversion as a (T)(object)object[] will compile but the conversion fails with Unable to cast object of type System.Object[] to type OurCustomType[].

Relevant code:

    public override T FromStream<T>(Stream stream)
    {
        using (stream)
        {
            if (typeof(Stream).IsAssignableFrom(typeof(T)))
            {
                return (T)(object)stream;
            }

            using (var sr = new StreamReader(stream))
            {
                using (var jsonTextReader = new JsonTextReader(sr))
                {
                    var jsonSerializer = GetSerializer();
                    return Deserialise<T>(jsonSerializer.Deserialize<JToken>(jsonTextReader));
                }
            }
        }
    }

    private T Deserialise<T>(JToken jToken)
    {
        switch (jToken.Type)
        {
            case JTokenType.Object:
                var canUpgradeObject = typeof(T).GetInterfaces().Contains(typeof(IUpgradeableCosmosItem));
                return canUpgradeObject ? (T) UpdateSchemaVersionToCurrent(jToken.ToObject<JObject>(), typeof(T)) : jToken.ToObject<T>();
            case JTokenType.Array:
                var elementType = typeof(T).GetElementType()!;
                var canUpgradeArray = elementType.GetInterfaces().Contains(typeof(IUpgradeableCosmosItem));
                return canUpgradeArray ? (T)(object) UpdateSchemaVersionToCurrent(jToken.Values<JObject>(), elementType) : jToken.ToObject<T>();
            default:
                throw new NotImplementedException("Unknown Type to Deserialize");
        }
    }


    private static object UpdateSchemaVersionToCurrent(JObject jObject, Type t)
    {
        var obj = jObject.ToObject(t);
        var intObj = obj as IUpgradeableCosmosItem;
        while (intObj!._CurrentVersion < intObj._SchemaVersion)
        {
            var originalVersion = intObj._CurrentVersion;
            intObj.UpgradeSchemaAndIncrementVersion(intObj._CurrentVersion + 1, jObject);
            if (originalVersion == intObj._CurrentVersion)
            {
                throw new InvalidOperationException($"Schema for {t.Name} was being upgraded from {originalVersion} to {intObj._CurrentVersion + 1} and the version was not updated.");
            }
        }
        return obj;
    }

    private static object[] UpdateSchemaVersionToCurrent(IEnumerable<JObject> jObjects, Type t)
    {
        Type genericListType = typeof(List<>).MakeGenericType(t);
        var list = (IList)(Activator.CreateInstance(genericListType))!;
        foreach (var jObject in jObjects)
        {
            list.Add(UpdateSchemaVersionToCurrent(jObject, t));
        }
        var retVal = new object[list.Count];
        list.CopyTo(retVal, 0);
        return retVal;
    }
2

There are 2 best solutions below

0
shingo On BEST ANSWER

Try to use Array.CreateInstance and return Array instead of object[]

private static Array UpdateSchemaVersionToCurrent(IEnumerable<JObject> jObjects, Type t)
{
    ...
    var retVal = Array.CreateInstance(t, list.Count);
    list.CopyTo(retVal, 0);
    return retVal;
}
0
John Wu On

I think you're very close. You just need to convert the final return object.

Instead of

jToken.Values<JObject>()

you can try converting it using the rarely-used ArrayList.

new ArrayList(jToken.Values).ToArray(typeof(T).GetElementType())