Vai al contenuto principale
Categorie articolo: Code

Gestire il polimorfismo in MongoDB con .NET

18 Novembre 2025 - 6 minuti di lettura

In un progetto basato su .NET Core, mi sono trovato a dover leggere e scrivere documenti MongoDB contenenti oggetti polimorfici, utilizzando il driver ufficiale MongoDB per .NET.

La problematica nasce quando è necessario memorizzare e ricostruire oggetti di tipi diversi, tutti derivati dalla stessa classe base, mantenendo un comportamento coerente nel database.

In questo articolo vi mostrerò come il driver MongoDB per .NET consente di gestire oggetti polimorfici in modo semplice, permettendo di leggere e scrivere documenti mantenendo la flessibilità tipica del polimorfismo.

Cos’è un oggetto polimorfico

Un oggetto polimorfico è un’istanza di una classe derivata che può essere trattata come un oggetto della sua classe base.

Questo principio, fondamentale nella programmazione a oggetti, permette a metodi o proprietà con lo stesso nome di essere invocati su tipi diversi, generando comportamenti specifici per ciascun tipo grazie all’ereditarietà e all’override.

Il polimorfismo consente quindi di scrivere codice più flessibile e riutilizzabile: lo stesso frammento di codice può lavorare con istanze di classi diverse in modo uniforme.

Nel nostro caso, i parametri del documento MongoDB saranno polimorfici:
BooleanParameter, NumberParameter e StringParameter derivano dalla classe base Parameter.

public enum ParameterType
{
Boolean = 1,
Number = 2,
String = 3
}
public class Parameter
{ 
    public string Id { get; init; } 
    public ParameterType Type { get; init; }
    public string Name { get; init; } 
    public bool Required { get; init; }
}

public class BooleanParameter : Parameter
{
    public bool? DefaultValue { get; init; }
}

public class NumberParameter : Parameter
{
    public double? DefaultValue { get; init; }
    public int? MinValue { get; init; }
    public int? MaxValue { get; init; }
}

public class StringParameter : Parameter
{
    public string? DefaultValue { get; init; }
    public int? MinLength { get; init; }
    public int? MaxLength { get; init; }
}

Vantaggi del polimorfismo

Il polimorfismo riduce la duplicazione del codice e migliora la manutenibilità.
Permette di lavorare con tipi diversi in modo uniforme (ad esempio, in collezioni miste) e di aggiungere nuovi tipi derivati senza modificare il codice esistente che lavora con la classe base.

Quando serve il polimorfismo in MongoDB

MongoDB, essendo un database schemaless, consente di memorizzare documenti con campi diversi anche all’interno della stessa collezione. Tuttavia, quando un’applicazione .NET utilizza classi derivate — per esempio per rappresentare tipi differenti di parametri o configurazioni — diventa necessario che il database conservi anche il tipo effettivo di ciascun oggetto.
Senza questa informazione, durante la deserializzazione il driver non saprebbe quale sottoclasse istanziare, e tutti gli oggetti verrebbero ricostruiti come istanze della classe base.
Per questo motivo, è importante definire correttamente le entità in modo da permettere al driver MongoDB di distinguere tra tipi diversi dello stesso modello.

Creare un’entità per MongoDB

Per leggere e scrivere oggetti su MongoDB con il driver .NET, definiamo le classi che rappresentano la struttura dei documenti.

In questo esempio, i parametri polimorfici vengono incapsulati all’interno di una classe principale (MyDocumentEntity), ma potrebbero anche essere salvati come documenti indipendenti.

L’implementazione e la definizione possono essere fatte principalmente in due modi:

  • utilizzando gli attributi a livello di classe;
  • utilizzando la classe BsonClassMap.

Per semplicità applicheremo la soluzione con gli attributi, ma la logica è la stessa anche con BsonClassMap.

Gli attributi più comuni che utilizzeremo sono:

  • [BsonIgnoreExtraElements]: ignora gli elementi non mappati durante la deserializzazione;
  • [BsonNoId]: indica che la classe non ha un campo _id;
  • [BsonId]: identifica il campo _id;
  • [BsonRequired]: rende obbligatoria la presenza del campo;
  • [BsonElement("name")]: assegna un nome specifico alla proprietà all’interno del documento;
  • [BsonRepresentation(BsonType.String)]: memorizza l’enum come stringa invece che come valore numerico;
  • [BsonIgnoreIfNull]: omette il campo se il valore è null.

Il codice seguente definisce un’entità.

[BsonNoId]
[BsonIgnoreExtraElements]
public class ParameterEntity
{
    [BsonRequired]
    [BsonElement("id")] 
    public string Id { get; init; } = Guid.NewGuid().ToString();

    [BsonRequired]
    [BsonRepresentation(BsonType.String)]
    [BsonElement("type")]
    public ParameterType Type { get; init; }

    [BsonRequired]
    [BsonElement("name")] 
    public string Name { get; init; }

    [BsonRequired]
    [BsonElement("required")]
    public bool Required { get; init; }
}

[BsonIgnoreExtraElements]
public class BooleanParameterEntity : ParameterEntity
{
    [BsonIgnoreIfNull]
    [BsonElement("defaultValue")]
    public bool? DefaultValue { get; init; }
}

[BsonIgnoreExtraElements]
public class NumberParameterEntity : ParameterEntity
{
    [BsonIgnoreIfNull]
    [BsonElement("defaultValue")]
    public double? DefaultValue { get; init; }

    [BsonIgnoreIfNull]
    [BsonElement("minValue")]
    public int? MinValue { get; init; }

    [BsonIgnoreIfNull]
    [BsonElement("maxValue")]
    public int? MaxValue { get; init; }
}

[BsonIgnoreExtraElements]
public class StringParameterEntity : ParameterEntity
{
    [BsonIgnoreIfNull]
    [BsonElement("defaultValue")]
    public string? DefaultValue { get; init; }

    [BsonIgnoreIfNull]
    [BsonElement("minLength")]
    public int? MinLength { get; init; }

    [BsonIgnoreIfNull]
    [BsonElement("maxLength")]
    public int? MaxLength { get; init; }
}

public class MyDocumentEntity
{
    [BsonId]
    public string Id { get; init; }

    [BsonRequired]
    [BsonElement("name")] 
    public string Name { get; init; }

    [BsonRequired]
    [BsonElement("parameters")] 
    public ParameterEntity[] Parameters { get; init; } = [];
}

Come funzionano i discriminatori di MongoDB

Il driver ufficiale MongoDB per .NET integra il supporto al polimorfismo grazie al meccanismo dei discriminatori.
A ogni salvataggio di un oggetto derivato, il driver aggiunge automaticamente un campo speciale (chiamato _t per default) che identifica il tipo concreto dell’istanza.
In fase di lettura, questo campo consente al driver di ricostruire automaticamente l’oggetto del tipo corretto, senza alcuna logica aggiuntiva da parte dello sviluppatore.

È comunque possibile personalizzare il comportamento dei discriminatori — ad esempio cambiando il nome del campo, o definendo valori specifici per ciascuna sottoclasse — attraverso attributi come [BsonDiscriminator] e [BsonKnownTypes].
Questa capacità rende i discriminatori uno strumento potente e flessibile per gestire il polimorfismo in modo nativo, mantenendo i documenti coerenti e facilmente estendibili.
In altre parole, il supporto al polimorfismo in MongoDB con .NET è una caratteristica integrata e pienamente supportata dal driver, non una soluzione alternativa o personalizzata.

Implementazione sulla classe base

Con l’attributo BsonKnownTypes si elencano tutte le classi derivate che il driver deve conoscere per la deserializzazione.

[BsonKnownTypes(
    typeof(BooleanParameterEntity),
    typeof(NumberParameterEntity),
    typeof(StringParameterEntity)
)]
public class ParameterEntity
{
    ...
}

Personalizzare il discriminatore nelle classi derivate

L’attributo BsonDiscriminator consente di definire il valore del campo `_t` che identifica ciascun tipo.
Se l’attributo non è specificato, il driver utilizza il nome della classe.

[BsonDiscriminator("boolean-parameter")]
public class BooleanParameterEntity : ParameterEntity 
{
    ...
}

[BsonDiscriminator("number-parameter")] 
public class NumberParameterEntity : ParameterEntity 
{
    ...
}

[BsonDiscriminator("string-parameter")]
public class StringParameterEntity : ParameterEntity
{
    ...
}

Esempio di scrittura con i discriminatori

Vediamo ora un esempio completo di documento con tre parametri di tipo diverso, che verrà inserito nella collection.

var document = new MyDocumentEntity
{
    Id = Guid.NewGuid().ToString(),
    Name = "Example Document",
    Parameters = new ParameterEntity[]
    {
        new BooleanParameterEntity
        {
            Id = "Boolean parameter",
            Type = ParameterType.Boolean,
            Name = "Enable Feature",
            Required = true,
            DefaultValue = true
        },
        new NumberParameterEntity
        {
            Id = "Number parameter",
            Type = ParameterType.Number,
            Name = "MaxValue Retries",
            Required = false,
            DefaultValue = 5,
            MinValue = 1,
            MaxValue = 10
        },
        new StringParameterEntity
        {
            Id = "String parameter",
            Type = ParameterType.String,
            Name = "Username",
            Required = true,
            DefaultValue = "user123",
            MinLength = 3,
            MaxLength = 15
        }
    }
};

await MyDocumentsCollection.InsertOneAsync(document);

Il documento salvato in MongoDB avrà la seguente struttura (JSON), con il campo discriminatore _t che identifica il tipo di ciascun parametro.

{
  "id": "",
  "name": "Example Document",
  "parameters": [
    {
      "_t": "boolean-parameter",
      "id": "Boolean parameter",
      "type": "Boolean",
      "name": "Enable Feature",
      "required": true,
      "defaultValue": true
    },
    {
      "_t": "number-parameter",
      "id": "Number parameter",
      "type": "Number",
      "name": "MaxValue Retries",
      "required": false,
      "defaultValue": 5,
      "minValue": 1,
      "maxValue": 10
    },
    {
      "_t": "string-parameter",
      "id": "String parameter",
      "type": "String",
      "name": "Username",
      "required": true,
      "defaultValue": "user123",
      "minLength": 3,
      "maxLength": 15
    }
  ]
}

Esempio di lettura con i discriminatori

Quando si legge il documento dalla collection, il driver utilizza il campo _t per istanziare correttamente gli oggetti polimorfici.

var retrievedDocument = await MyDocumentsCollection
    .Find(d => d.Id == document.Id)
    .FirstOrDefaultAsync();

Il driver riconosce automaticamente il campo _t e istanzia la sottoclasse corretta in base al suo valore.
Come mostrato nell’immagine seguente, il documento viene deserializzato correttamente, con ogni parametro ricostruito nel proprio tipo specifico.

polymorphism MongoDB .NET - Document read

Gestione del discriminatore globale

È possibile configurare il campo discriminatore anche a livello globale, utile se si desidera modificare il nome predefinito _t.
In generale, tuttavia, è consigliabile mantenere il valore di default per garantire la compatibilità dei documenti.

BsonSerializer.RegisterDiscriminatorConvention(
    typeof(ParameterEntity),
    new ScalarDiscriminatorConvention("typeTag")
    );

In questo modo, invece di utilizzare _t, MongoDB salverà il tipo nel campo typeTag.

Ricorda che modificare il nome del discriminatore può rendere incompatibili i documenti precedenti o impedire la deserializzazione corretta se non viene applicata la stessa convenzione ovunque nel progetto.

Riferimenti

Per approfondimenti, ti invito a consultare la documentazione ufficiale MongoDB per .NET sui discriminatori,

La versione utilizzata del driver MongoDB per .NET in questo articolo è la 3.4.3.

Conclusioni

I discriminatori rappresentano uno strumento semplice ma fondamentale per gestire il polimorfismo in MongoDB con .NET.
Configurandoli correttamente, è possibile mantenere la struttura dei dati coerente e facilmente estendibile, garantendo la compatibilità con la deserializzazione automatica del driver.

Articolo scritto da