Interface throwing exception when deserialising interface

I’m currently utilising .NET8 with its corresponding MongoDB driver. I have an array comprising mixed JSON objects that represent various actions to be executed based on rule evaluation, such as sending emails or setting timers.

To facilitate their storage an abstract BaseAction class has been implemented, which adheres to the IAction interface.

Derived from this base class are several action classes, each inheriting properties and functionalities from BaseAction. Serialisation to the database, specifically Cosmos DB for MongoDB, occurs smoothly without any issue when I implement the property of type BaseAction (as seen in the example of strategy class below) and I can deserialise the data. All seems well except that upon examination of the data returned using breakpoints there is the presence of all necessary properties having been fetched.

However inspecting the final returned strategies output via the associated swagger page, it becomes evident that the additional properties inherited from subclasses e.g. Hours, Minutes, Seconds or Recipients, EmailSubject, EmailMessage are discarded, leaving only those belonging to BaseAction.

I have tried replacing BaseAction with IAction but at this point the request to the database throws an exception leaving the solution to this issue rather uncertain.

Any help with this would be really helpful as I’m new to developing with MongoDb and would really appreciate any input.

Service:

{
    private readonly MongoClient _client;
    private IMongoDatabase? _database;
    private IMongoCollection<Strategy>? _strategyCollection;

    public StrategyService(IOptions<Settings> mongoDBSettings)
    {
        _client = new MongoClient(mongoDBSettings.Value.ConnectionURI);
    }

    private void SetDatabase(string networkId)
    {
        _database = _client.GetDatabase(networkId);
        _strategyCollection = _database.GetCollection<Strategy>("Strategies");
    }

    public async Task<IEnumerable<Strategy>> GetStrategies(string networkId, Guid? strategyId)
    {
        try
        {
            SetDatabase(networkId);
            var filter = Builders<Strategy>.Filter.Empty;

            if (strategyId.HasValue)
            {
                filter = Builders<Strategy>.Filter.Eq("Id", strategyId);
            }

            var strategies = await _strategyCollection.Find<Strategy>(filter).ToListAsync();

            return strategies;
        }
        catch (Exception ex)
        {
            Console.WriteLine($"Error in GetStrategies: {ex.Message}");
            throw;
        }
    }


    public async Task<IActionResult> AddStrategy(string networkId, StrategyRequest strategy)
    {
        try
        {
            SetDatabase(networkId);

            var convertRequest = new Strategy(strategy);
            if (_strategyCollection == null)
            {
                return new ObjectResult("Failed to add strategy. _strategyCollection is null.") { StatusCode = 500 };
            }
            await _strategyCollection.InsertOneAsync(convertRequest);

            return new OkResult();
        }
        catch (Exception ex)
        {
            return new ObjectResult($"Failed to add strategy: {ex.Message}") { StatusCode = 500 };
        }
    }
}

Strategy Class which contains a list of actions

{
    public List<BaseAction> Actions { get; set; } = [];
}

BaseAction Class:

[BsonKnownTypes(typeof(ActionChangeDataItem), typeof(ActionChangeStatus), typeof(ActionSendBatchCommand), typeof(ActionSendEmail), typeof(ActionSwitchPlan), typeof(ActionTimer))]
public abstract class BaseAction : IAction
{
    public Guid Id { get; set; }
    public string ActionOrigin { get; set; }
    public string ActionType { get; set; }
    public int Order { get; set; }

    public BaseAction() { }
}

IAction Interface

{
    Guid Id { get; set; }
    string ActionOrigin { get; set; }
    string ActionType { get; set; }
    int Order { get; set; }
}

Example of actions inheriting from BaseAction / IAction

{
    public List<string> Recipients { get; set; }
    public string EmailSubject { get; set; }
    public string EmailContent { get; set; }
}

public class ActionTimer : BaseAction
{
    public int? Hours { get; set; }
    public int? Minutes { get; set; }
    public int? Seconds { get; set; }
    public ActionTimer() { }
}

Hi, @Olivia_Bates,

Welcome to the MongoDB Community Forums. The MongoDB .NET/C# Driver implements persistence of polymorphic types through type discriminators using the HierarchicalDiscriminatorConvention by default. This stores type information as an _t field where the value is a string or array of strings from the base class to the concrete type. For example, _t: ["Animal", "Mammal", "Dog"].

Note that we do not currently support in interface discriminators for a few reasons. Notably the number of interfaces is unbounded whereas the number of base classes varies linearly with class hierarchy depth. Another reason is interfaces such as IEquitable<T>, IComparable<T>, IDisposable, and other generic interfaces - many of which can be created by application authors. The question then becomes how do you differentiate in a generic way between interfaces relevant to the class hierarchy and structural interfaces needed by C#. Another problem is given that we haven’t supported interface discriminators in the past, existing data with _t definitions would need to be updated. Support for this feature has been requested in CSHARP-1907 but we haven’t scheduled it due to the above-mentioned problems.

That said, a type hierarchy based on BaseAction should work as expected. You can retrieve an ActionChangeDataItem from your BaseAction collection, the driver will instantiate the correct type and set its properties appropriately. You mention that you are using Microsoft CosmosDB, which is not a MongoDB product. If this code is not behaving correctly with Microsoft CosmosDB, I encourage you to reach out to Microsoft Technical Support for assistance.

Sincerely,
James