Choosing OrmLiteConverter when creating IDbDataParameter during update

Hello, recently we converted our dtos to have properties of type IReadOnlyCollection instead of byte because of CA1819: Properties should not return arrays (code analysis) - .NET | Microsoft Learn. The problem is that when crating the IDbDataParameter during an update, the actual type of the value is used instead of the type of the property for which the value is being created. Therefore ServiceStack tries to get a converter for eg. List instead of IReadOnlyCollection. Note that insertion and selection work fine.
Here is an example to demonstrate this (tested using net6.0 and ServiceStack.OrmLite.SqlServer 6.11.0 and 8.2.2)

using System;
using System.Collections.Generic;
using System.Data;
using System.Linq;
using ServiceStack.OrmLite;

public class Program
{
    public static void Main(string[] args)
    {
        OrmLiteConfig.DialectProvider = SqlServer2008Dialect.Provider;
        typeof(DatabaseEntity).GetModelMetadata();

        OrmLiteConfig.DialectProvider.RegisterConverter<IReadOnlyCollection<byte>>(ReadonlyByteCollectionConverter.Instance);

        // Change the connection string to some sql server instance
        var connectionString = "CHANGE";
        var connectionFactory = new OrmLiteConnectionFactory(connectionString, OrmLiteConfig.DialectProvider);
        using var db = connectionFactory.Open();
        db.CreateTableIfNotExists<DatabaseEntity>();

        Console.WriteLine("Testing with array");
        TestInsertGetAndUpdate(db, new byte[] { 1, 2, 3, 4, 5 }, new byte[] { 6, 7, 8, 9, 10 });
        Console.WriteLine("Testing with list");
        TestInsertGetAndUpdate(db, new List<byte> { 1, 2, 3, 4, 5 }, new List<byte> { 6, 7, 8, 9, 10 });
        Console.WriteLine("Finished");
    }

    private static void TestInsertGetAndUpdate(IDbConnection db, IReadOnlyCollection<byte> binaryContent, IReadOnlyCollection<byte> updatedContent)
    {
        var random = new Random();
        var sampleEntity = new DatabaseEntity
        {
            Id = random.NextInt64(),
            BinaryContent = binaryContent,
        };

        // ReadonlyByteConverter is called and correct value is inserted into db.
        db.Insert(sampleEntity);

        // Reading also calls ReadonlyByteConverter and correct value is set
        var fromDb = db.SingleById<DatabaseEntity>(sampleEntity.Id);

        var updatedEntity = new DatabaseEntity
        {
            BinaryContent = updatedContent,
        };

        // The problem's source is in ServiceStack.OrmLite, function
        // internal static void ConfigureParam(this IOrmLiteDialectProvider dialectProvider, IDbDataParameter p, object value, DbType? dbType)
        // More specifically, value.GetType() in the function, which then looks for a converter of the value's actual type instead of the type defined on the dto.
        db.UpdateOnlyFields(updatedEntity, [nameof(DatabaseEntity.BinaryContent)], x => x.Id == sampleEntity.Id);

        var newFromDb = db.SingleById<DatabaseEntity>(sampleEntity.Id);
    }
}

public class ReadonlyByteCollectionConverter : OrmLiteConverter
{
    public static ReadonlyByteCollectionConverter Instance { get; } = new();

    public override string ColumnDefinition => "VARBINARY(MAX)";

    public override DbType DbType => DbType.Binary;

    public override void InitDbParam(IDbDataParameter p, Type fieldType)
    {
        base.InitDbParam(p, fieldType);

        p.Size = -1;
    }

    public override object ToDbValue(Type fieldType, object value)
    {
        if (value is byte[] byteArr)
        {
            return byteArr;
        }

        return ((IEnumerable<byte>)value).ToArray();
    }
}

public class DatabaseEntity
{
    public long Id { get; set; }

    public IReadOnlyCollection<byte> BinaryContent { get; set; }
}

If we want to be sure that any IReadOnlyCollection value is supported, we would need to register the converter for all types that implement IReadOnlyCollection which we would rather not. Is there a workaround for this issue?

Kind regards,
Tomaž

You shouldn’t be using Interface properties on Data Models or DTOs as neither serializers nor ORMs will know what Concrete Type it needs to deserialize back into.

Don’t think this rule is relevant to DTOs or Data Models, but if you don’t want to use an array use a concrete collection like List<T> instead.

Hello, thanks for the immediate reply. Fair enough, we will use arrays and disable the warning.
Thanks,
Tomaž

1 Like