BulkOptions dans DapperPlus : Le guide essentiel

DapperPlus est une extension du micro-ORM Dapper qui permet de réaliser des opérations Bulk. Ces opérations s’appuient sur des flux structurés, ce qui les rend bien plus performantes qu’un insert classique. BulkOptions est la classe qui permet d’en configurer précisément le comportement.
C’est via cette classe que tu pilotes la stratégie de tes requêtes : gestion des conflits, filtrage des colonnes et suivi d’impact.

C’est quoi UseBulkOptions ?

DapperPlus te permet de faire des opérations en masse (Bulk) en une seule passe SQL. Par défaut, il devine comment mapper tes entités. UseBulkOptions te laisse surcharger ce comportement : colonnes à ignorer, gestion des conflits, logs, comptage des lignes modifiées, etc.

Tu peux configurer les options à deux endroits.

Sur le mapping — persistant (context global), appliqué à chaque appel pour ce type :

DapperPlusManager.Entity<Product>()
    .Table("Product")
    .Identity(x => x.ProductID)
    .UseBulkOptions(x => x.InsertIfNotExists = true);

connection.BulkInsert(products);

Sur la connexion — ponctuel, appliqué uniquement à la chaîne qui suit :

connection
    .UseBulkOptions(o => o.Log = s => Console.WriteLine(s))
    .BulkMerge(products);
graph TD
    A[Configuration de BulkOptions] --> B{Où configurer ?}
    B -- Mapping --> C[DapperPlusManager.Entity]
    B -- Connexion --> D[connection.UseBulkOptions]
    C --> C1[<b>Persistant</b><br/>Règles métier, colonnes à ignorer]
    D --> D1[<b>Ponctuel</b><br/>Debug, Log, Timeout, BatchSize]

La règle simple : mapping pour les règles métier, connexion pour le debug ou les options techniques.

Les cas courants

Les champs d’audit

Problème classique : tu as CreatedDate et LastUpdatedDate. Tu ne veux pas écraser CreatedDate quand tu mets à jour et tu ne veux pas insérer LastUpdatedDate sur une nouvelle ligne.

DapperPlusManager.Entity<Product>()
    .Table("Product")
    .Identity(x => x.ProductID)
    .UseBulkOptions(x => {
        // Ne pas écraser CreatedDate lors d'un UPDATE
        x.IgnoreOnMergeUpdateExpression = y => new { y.CreatedDate };

        // Ne pas insérer LastUpdatedDate lors d'un INSERT
        x.IgnoreOnMergeInsertExpression = y => new { y.LastUpdatedDate };
    });

connection.BulkMerge(products); // ✅ Les champs d'audit sont respectés

Mettre à jour seulement si quelque chose a changé

Par défaut, BulkMerge écrit en base même si les données sont identiques. Pour n’écrire que si au moins une colonne a changé :

DapperPlusManager.Entity<Product>()
    .Table("Product")
    .Identity(x => x.ProductID)
    .UseBulkOptions(x => {
        // MAJ si la ligne match ET qu'au moins une de ces colonnes est différente
        x.MergeMatchedAndOneNotConditionExpression = y => new { y.Name, y.Description };
    });

connection.BulkMerge(products);
// ✅ N'écrit en base que si Name ou Description a changé

💡 Note sur les conditions : Il est important de distinguer MergeMatchedAndOneNotConditionExpression (qui déclenche la mise à jour si au moins une des colonnes listées a changé) de MergeMatchedAndConditionExpression (qui exige que toutes les conditions spécifiées soient vraies).

Utile pour ne pas déclencher des triggers ou des audits inutiles.

Récupérer le nombre de lignes affectées

Par défaut, les méthodes bulk ne retournent rien. Pour savoir combien de lignes ont été insérées ou mises à jour, active UseRowsAffected et passe un objet ResultInfo :

var resultInfo = new BulkOperationResultInfo();

connection
    .UseBulkOptions(o => {
        o.UseRowsAffected = true;
        o.ResultInfo = resultInfo;
    })
    .BulkMerge(products);

Console.WriteLine($"Insérés  : {resultInfo.RowsInserted}");
Console.WriteLine($"Mis à jour : {resultInfo.RowsUpdated}");
Console.WriteLine($"Supprimés  : {resultInfo.RowsDeleted}");

⚠️ Activer UseRowsAffected a un coût en performance. Un aller-retour supplémentaire est nécessaire pour récupérer les stats. Ne l’active que si tu en as besoin.

Insérer seulement si absent

Tu ne veux pas mettre à jour les lignes existantes — juste les ignorer et insérer les nouvelles. InsertIfNotExists est fait pour ça :

DapperPlusManager.Entity<Product>()
    .Table("Product")
    .Identity(x => x.ProductID)
    .UseBulkOptions(x => x.InsertIfNotExists = true);

connection.BulkInsert(products);
// ✅ Insère uniquement les nouveaux — les existants sont intacts

Autoriser les clés dupliquées

Si tu souhaites ignorer les erreurs liées aux clés dupliquées lors d’une opération, tu peux utiliser l’option AllowDuplicateKeys. Cela permet de continuer l’insertion des autres enregistrements sans faire échouer toute la transaction à cause d’un doublon.

connection.UseBulkOptions(x =>
    x.AllowDuplicateKeys = true
).BulkInsert(products);

Les pièges dans lesquels tu ne dois pas tomber

❌ InsertIfNotExists c’est pareil que BulkMerge ? Non. InsertIfNotExists ignore les lignes existantes. BulkMerge les met à jour. Ce sont deux comportements opposés sur les doublons.

❌ UseRowsAffected s’active une fois pour toutes ? Non, il doit être spécifié sur chaque appel à UseBulkOptions. Comme ConfigureAwait, chaque opération doit le déclarer explicitement.

// ❌ Incorrect — ResultInfo restera vide
var resultInfo = new BulkOperationResultInfo();
connection.BulkMerge(products);

// ✅ Correct
connection
    .UseBulkOptions(o => {
        o.UseRowsAffected = true;
        o.ResultInfo = resultInfo;
    })
    .BulkMerge(products);

❌ Les options sur la connexion s’appliquent à tous les types ? Non. Depuis la connexion, seules les options génériques sont disponibles (log, batch size, timeout…). Les règles métier (colonnes ignorées, conditions de merge) se configurent via le mapping.

En résumé

Besoin Option(s) Méthode
Insert seulement si absent InsertIfNotExists BulkInsert
Ignorer les doublons de clés AllowDuplicateKeys Toutes
Protéger les champs d’audit IgnoreOnMergeInsert/UpdateExpression BulkMerge
Compter les lignes modifiées UseRowsAffected + ResultInfo Toutes
Mettre à jour seulement si changé MergeMatchedAndOneNotConditionExpression BulkMerge
Déboguer le SQL généré Log Toutes

Conclusion

Tu n’as ici qu’un aperçu des possibilités offertes par BulkOptions. C’est un outil extrêmement complet et puissant qui permet de répondre à des scénarios de synchronisation de données complexes avec une finesse chirurgicale. Que ce soit pour des optimisations de performance extrêmes, des logiques métier d’audit ou des gestions de conflits avancées, BulkOptions offre le contrôle nécessaire pour maîtriser tes flux de données à grande échelle.

La documentation liste plus de 100 exemples.
Dans la pratique, celles du tableau ci-dessus couvrent 90% des cas. Pour aller plus loin : dapper-plus.net/options.
Pour en savoir plus sur les fonctionnalités d’audit : dapper-plus.net/audit