Accès aux données dans .NET: Comparaison des ORM et stratégies de cartographie (Parte 2 M- DapperM SK5 Raw SQLMST6 and Hybrid ApproachesMSC7 (Français (French))

Accès aux données dans .NET: Comparaison des ORM et stratégies de cartographie (Parte 2 M- DapperM SK5 Raw SQLMST6 and Hybrid ApproachesMSC7

Wednesday, 03 December 2025

//

23 minute read

Bienvenue à la partie 2 de notre guide complet sur l'accès aux données dans .NET! Dans Partie 1, nous avons étudié en profondeur le noyau du cadre d’entités , y compris la génération de SQL

Dans cet article, nous étudierons les solutions de remplacement moins lourdes et comment combiner plusieurs approches pour une performance optimale.

  • Dapper: Le point d’intérêt microM SK1ORM
  • Raw ADO.NET/Npgsql: Performance et contrôle maximaux
  • Bibliothèques de cartographie des objets: Mapster contre AutoMapper
  • Approches hybrides: Combinaison du noyau EF et de l'acier Dapper
  • Indicateurs de rendement: RealM SK1comparations mondiales
  • Matrice de décision: Choix de l’outil approprié pour votre scénario

Table des matières

Dapper: The Micro-ORM

Dapper est un appareil léger, élevéM SK1 micro de performance-ORM créé par Stack OverflowMSC3 Il fournit une couche mince sur ADO .NETMスク5 qui permet d'effectuer le travail ardu de cartographie des résultats de la demande aux objets tout en vous donnant un contrôle SQL complet

Pourquoi le Dapper existe-t-il?

Dapper a été créé à partir de Stack Overflow's besoin d’un accès élevé aux données -performance data accessM SK2 L’équipe a constaté que les ORM complets comme Entity Framework (preMSC4CoreMNK5 ajoutent trop de frais généraux pour leur niveau élevéMRK6traffic scenariosMMK7 Dapper vous donne 95% de la commodité avec seulement |5-15% surcoûts générés par un ADO .NET=.

Faits clés

  • SQL brut: Vous rédigez tous les SQL par vous-même - contrôle complet
  • Performance élevée: Coûts généraux minimaux au-dessus de l'ADO.NET (5-15%)
  • Multi-Mapping: Carte des demandes complexes à plusieurs types apparentés
  • Traitement des paramètres: Paramètreisation automatique empêche l'injection de SQL
  • Soutien à la transaction: Contrôle complet des opérations
  • Simplicitité: Aucune configurationM SK1 aucun suivi de changements, aucune magie
  • Async/Await: Soutien async complet pour le réseau moderne .NET

Exemples de Dapper de base

using Npgsql;
using Dapper;

public class DapperBlogRepository
{
    private readonly string _connectionString;

    public DapperBlogRepository(string connectionString)
    {
        _connectionString = connectionString;

        // Configure Dapper to work with PostgreSQL naming conventions
        DefaultTypeMap.MatchNamesWithUnderscores = true;
    }

    // Simple query
    public async Task<IEnumerable<BlogPost>> GetRecentPostsAsync(int count)
    {
        using var connection = new NpgsqlConnection(_connectionString);

        const string sql = @"
            SELECT id, title, content, tags, published_date, category_id
            FROM blog_posts
            ORDER BY published_date DESC
            LIMIT @Count";

        return await connection.QueryAsync<BlogPost>(sql, new { Count = count });
    }

    // Query with WHERE clause
    public async Task<BlogPost> GetPostByIdAsync(int id)
    {
        using var connection = new NpgsqlConnection(_connectionString);

        const string sql = @"
            SELECT id, title, content, published_date
            FROM blog_posts
            WHERE id = @Id";

        return await connection.QueryFirstOrDefaultAsync<BlogPost>(sql, new { Id = id });
    }

    // Insert with returning ID
    public async Task<int> CreatePostAsync(BlogPost post)
    {
        using var connection = new NpgsqlConnection(_connectionString);

        const string sql = @"
            INSERT INTO blog_posts (title, content, published_date, category_id)
            VALUES (@Title, @Content, @PublishedDate, @CategoryId)
            RETURNING id";

        return await connection.ExecuteScalarAsync<int>(sql, post);
    }

    // Update
    public async Task UpdatePostAsync(BlogPost post)
    {
        using var connection = new NpgsqlConnection(_connectionString);

        const string sql = @"
            UPDATE blog_posts
            SET title = @Title,
                content = @Content,
                published_date = @PublishedDate
            WHERE id = @Id";

        await connection.ExecuteAsync(sql, post);
    }

    // Delete
    public async Task DeletePostAsync(int id)
    {
        using var connection = new NpgsqlConnection(_connectionString);

        const string sql = "DELETE FROM blog_posts WHERE id = @Id";

        await connection.ExecuteAsync(sql, new { Id = id });
    }
}

Multi-MappingM SK1 Joints de manutention

L’une des caractéristiques les plus puissantes de Dapper' est le multiM SK1mapping -, qui permet de traiter efficacement les connexions et la cartographie à plusieurs objets connexes.

public async Task<IEnumerable<BlogPost>> GetPostsWithCategoryAsync()
{
    using var connection = new NpgsqlConnection(_connectionString);

    const string sql = @"
        SELECT
            p.id, p.title, p.content, p.published_date,
            c.id, c.name, c.description
        FROM blog_posts p
        INNER JOIN categories c ON p.category_id = c.id
        ORDER BY p.published_date DESC";

    return await connection.QueryAsync<BlogPost, Category, BlogPost>(
        sql,
        (post, category) =>
        {
            post.Category = category;
            return post;
        },
        splitOn: "id"  // Split at the second "id" column
    );
}

// More complex: Posts with comments
public async Task<IEnumerable<BlogPost>> GetPostsWithCommentsAsync()
{
    using var connection = new NpgsqlConnection(_connectionString);

    const string sql = @"
        SELECT
            p.id, p.title, p.content,
            c.id, c.author, c.content, c.created_at
        FROM blog_posts p
        LEFT JOIN comments c ON p.id = c.blog_post_id
        ORDER BY p.published_date DESC, c.created_at";

    var postDict = new Dictionary<int, BlogPost>();

    await connection.QueryAsync<BlogPost, Comment, BlogPost>(
        sql,
        (post, comment) =>
        {
            if (!postDict.TryGetValue(post.Id, out var existingPost))
            {
                existingPost = post;
                existingPost.Comments = new List<Comment>();
                postDict.Add(post.Id, existingPost);
            }

            if (comment != null)
            {
                existingPost.Comments.Add(comment);
            }

            return existingPost;
        },
        splitOn: "id"
    );

    return postDict.Values;
}

Techniques avancées du Dapper

Paramètres dynamiques pour les demandes complexes:

public async Task<IEnumerable<BlogPost>> SearchWithDynamicFiltersAsync(SearchCriteria criteria)
{
    using var connection = new NpgsqlConnection(_connectionString);

    var parameters = new DynamicParameters();
    var conditions = new List<string>();

    var sql = new StringBuilder("SELECT * FROM blog_posts");

    if (!string.IsNullOrEmpty(criteria.SearchTerm))
    {
        conditions.Add("search_vector @@ to_tsquery('english', @SearchTerm)");
        parameters.Add("SearchTerm", criteria.SearchTerm);
    }

    if (criteria.CategoryIds?.Any() == true)
    {
        conditions.Add("category_id = ANY(@CategoryIds)");
        parameters.Add("CategoryIds", criteria.CategoryIds);
    }

    if (criteria.FromDate.HasValue)
    {
        conditions.Add("published_date >= @FromDate");
        parameters.Add("FromDate", criteria.FromDate.Value);
    }

    if (criteria.Tags?.Any() == true)
    {
        conditions.Add("tags && @Tags");  // PostgreSQL array overlap
        parameters.Add("Tags", criteria.Tags);
    }

    if (conditions.Any())
    {
        sql.Append(" WHERE ");
        sql.Append(string.Join(" AND ", conditions));
    }

    sql.Append(" ORDER BY published_date DESC LIMIT @Limit");
    parameters.Add("Limit", criteria.Limit);

    return await connection.QueryAsync<BlogPost>(sql.ToString(), parameters);
}

Gestionnaire de type personnalisé pour les types PostgreSQL:

// Handle PostgreSQL arrays
public class PostgresArrayTypeHandler : SqlMapper.TypeHandler<string[]>
{
    public override void SetValue(IDbDataParameter parameter, string[] value)
    {
        parameter.Value = value;
        ((NpgsqlParameter)parameter).NpgsqlDbType = NpgsqlDbType.Array | NpgsqlDbType.Text;
    }

    public override string[] Parse(object value)
    {
        return (string[])value;
    }
}

// Handle PostgreSQL JSONB
public class JsonTypeHandler<T> : SqlMapper.TypeHandler<T>
{
    public override void SetValue(IDbDataParameter parameter, T value)
    {
        parameter.Value = JsonSerializer.Serialize(value);
        ((NpgsqlParameter)parameter).NpgsqlDbType = NpgsqlDbType.Jsonb;
    }

    public override T Parse(object value)
    {
        return JsonSerializer.Deserialize<T>(value.ToString());
    }
}

// Register handlers (in startup)
SqlMapper.AddTypeHandler(new PostgresArrayTypeHandler());
SqlMapper.AddTypeHandler(new JsonTypeHandler<Dictionary<string, object>>());

Opérations en vrac avec PostgreSQL COPY:

public async Task BulkInsertPostsAsync(IEnumerable<BlogPost> posts)
{
    using var connection = new NpgsqlConnection(_connectionString);
    await connection.OpenAsync();

    using var writer = await connection.BeginBinaryImportAsync(
        "COPY blog_posts (title, content, tags, published_date) FROM STDIN (FORMAT BINARY)"
    );

    foreach (var post in posts)
    {
        await writer.StartRowAsync();
        await writer.WriteAsync(post.Title);
        await writer.WriteAsync(post.Content);
        await writer.WriteAsync(post.Tags, NpgsqlDbType.Array | NpgsqlDbType.Text);
        await writer.WriteAsync(post.PublishedDate);
    }

    await writer.CompleteAsync();
}

Soutien à la transaction:

public async Task TransferPostToCategoryAsync(int postId, int newCategoryId)
{
    using var connection = new NpgsqlConnection(_connectionString);
    await connection.OpenAsync();

    using var transaction = await connection.BeginTransactionAsync();

    try
    {
        // Update the post
        await connection.ExecuteAsync(
            "UPDATE blog_posts SET category_id = @CategoryId WHERE id = @PostId",
            new { CategoryId = newCategoryId, PostId = postId },
            transaction
        );

        // Log the change
        await connection.ExecuteAsync(
            @"INSERT INTO category_history (post_id, category_id, changed_at)
              VALUES (@PostId, @CategoryId, @ChangedAt)",
            new { PostId = postId, CategoryId = newCategoryId, ChangedAt = DateTime.UtcNow },
            transaction
        );

        await transaction.CommitAsync();
    }
    catch
    {
        await transaction.RollbackAsync();
        throw;
    }
}

Quand utiliser le Dapper

✅ Utilisez le Dapper lorsque:

  • La performance est essentielle, mais vous n’avez pas besoin de vitesse maximale absolue.
  • Vous'êtes à l’aise pour écrire SQL
  • Vous avez des questions complexes qui ne donnent pas une bonne carte de LINQ.
  • Vous avez besoin d'un bon contrôle de la génération SQL
  • Vous' travaillez avec un schéma de base de données existant
  • Vous voulez des frais généraux et des dépendances minimales
  • L'utilisation de la mémoire est une préoccupation
  • Vous devez exploiter largement les fonctionnalités spécifiques de la base de données-

❌ Évitez le dapper lorsqueM SK1

  • Votre équipe n'est pas à l'aise avec SQL
  • Vous avez besoin d'un suivi automatique des changements
  • Vous voulez des migrations de schéma en tant que code
  • Vous avez un schéma qui évolue rapidement.
  • Vous avez besoin de la portabilité des bases de données cross-
  • Vous préférez LINQ plutôt que la syntaxe SQL

Caractéristiques du rendement

  • Rendement de la demande: 5-15% frais généraux par rapport à la production brute ADO.NET
  • Utilisation de la mémoire: Minimale - aucun suivi de changement ou des proxies
  • Opérations en vrac: Excellent avec PostgreSQL COPY
  • Première demande: Fast - Aucune étape de compilation
  • Productivité des développeurs: Besoins de connaissances SQL mais très prévisibles

Raw ADO.NET avec Npgsql

Pour une performance maximale et un contrôle absolus, vous pouvez utiliser Npgsql directement sans aucune couche ORM.

Lorsque le ADO.NET brut a sens

ADO Raw.NET est approprié lorsque

  • Vous avez besoin de chaque milliseconde de performance
  • Vous construisez des processeurs de données à haut débit
  • Vous'travaillez avec PostgreSQL-caractéristiques spécifiques non supportées par les ORM
  • Vous avez besoin d'un contrôle précis des affectations de mémoire
  • Vous'effectuez des opérations en vrac ou écoulez de grands ensembles de données

Exemple: Pure Npgsql

public class NpgsqlBlogRepository
{
    private readonly string _connectionString;

    public async Task<List<BlogPost>> GetRecentPostsAsync(int count)
    {
        var posts = new List<BlogPost>();

        using var connection = new NpgsqlConnection(_connectionString);
        await connection.OpenAsync();

        using var command = new NpgsqlCommand(
            "SELECT id, title, content, tags, published_date FROM blog_posts ORDER BY published_date DESC LIMIT @count",
            connection
        );

        command.Parameters.AddWithValue("count", count);

        using var reader = await command.ExecuteReaderAsync();

        while (await reader.ReadAsync())
        {
            posts.Add(new BlogPost
            {
                Id = reader.GetInt32(0),
                Title = reader.GetString(1),
                Content = reader.GetString(2),
                Tags = reader.GetFieldValue<string[]>(3),
                PublishedDate = reader.GetDateTime(4)
            });
        }

        return posts;
    }

    // Using prepared statements for repeated queries
    public async Task<BlogPost> GetPostByIdAsync(int id)
    {
        using var connection = new NpgsqlConnection(_connectionString);
        await connection.OpenAsync();

        using var command = new NpgsqlCommand(
            "SELECT id, title, content FROM blog_posts WHERE id = $1",
            connection
        );

        command.Parameters.AddWithValue(id);
        await command.PrepareAsync(); // Prepared statement for performance

        using var reader = await command.ExecuteReaderAsync();

        if (await reader.ReadAsync())
        {
            return new BlogPost
            {
                Id = reader.GetInt32(0),
                Title = reader.GetString(1),
                Content = reader.GetString(2)
            };
        }

        return null;
    }

    // Working with PostgreSQL JSONB
    public async Task<Dictionary<string, object>> GetPostMetadataAsync(int id)
    {
        using var connection = new NpgsqlConnection(_connectionString);
        await connection.OpenAsync();

        using var command = new NpgsqlCommand(
            "SELECT metadata FROM blog_posts WHERE id = $1",
            connection
        );

        command.Parameters.AddWithValue(id);

        var json = await command.ExecuteScalarAsync() as string;
        return JsonSerializer.Deserialize<Dictionary<string, object>>(json);
    }

    // Streaming large result sets
    public async IAsyncEnumerable<BlogPost> StreamAllPostsAsync()
    {
        using var connection = new NpgsqlConnection(_connectionString);
        await connection.OpenAsync();

        using var command = new NpgsqlCommand(
            "SELECT id, title, content FROM blog_posts ORDER BY id",
            connection
        );

        using var reader = await command.ExecuteReaderAsync();

        while (await reader.ReadAsync())
        {
            yield return new BlogPost
            {
                Id = reader.GetInt32(0),
                Title = reader.GetString(1),
                Content = reader.GetString(2)
            };
        }
    }
}

Caractéristiques du rendement

  • Rendement de la demande: Baseline - le plus rapidement possible
  • Utilisation de la mémoire: Le plus bas - contrôle complet des allocations
  • Opérations en vrac: Excellent avec le protocole COPY
  • Productivité des développeurs: Exige la plupart du travail manuel

Bibliothèques de cartographie des objets

Lorsque vous travaillez avec Dapper ou le ADO brut, il faut souvent faire une carte entre différentes représentations d'objets.

Mapping de la performance

Mapster est un mapper d’objet basé sur une convention rapide, - qui utilise la génération de sources pour optimiser les performances

// Install: Mapster and Mapster.Tool
using Mapster;

public class BlogPostDto
{
    public int Id { get; set; }
    public string Title { get; set; }
    public string Summary { get; set; }
    public List<string> CategoryNames { get; set; }
}

public class BlogPost
{
    public int Id { get; set; }
    public string Title { get; set; }
    public string Content { get; set; }
    public List<Category> Categories { get; set; }
}

// Configuration
public class MappingConfig : IRegister
{
    public void Register(TypeAdapterConfig config)
    {
        config.NewConfig<BlogPost, BlogPostDto>()
            .Map(dest => dest.Summary, src => src.Content.Substring(0, Math.Min(200, src.Content.Length)))
            .Map(dest => dest.CategoryNames, src => src.Categories.Select(c => c.Name).ToList());

        // Reverse map with ignore
        config.NewConfig<BlogPostDto, BlogPost>()
            .Ignore(dest => dest.Content);
    }
}

// Registration in Program.cs
TypeAdapterConfig.GlobalSettings.Scan(Assembly.GetExecutingAssembly());

// Usage with Dapper
public class BlogService
{
    private readonly string _connectionString;

    public async Task<List<BlogPostDto>> GetPostsAsync()
    {
        using var connection = new NpgsqlConnection(_connectionString);

        var posts = await connection.QueryAsync<BlogPost>(@"
            SELECT p.id, p.title, p.content
            FROM blog_posts p
        ");

        // Map to DTOs - very fast with Mapster
        return posts.Adapt<List<BlogPostDto>>();
    }

    // Projection mapping (compile-time)
    public async Task<List<BlogPostDto>> GetPostsDtosDirectlyAsync()
    {
        using var connection = new NpgsqlConnection(_connectionString);

        // Query directly to DTO shape
        return (await connection.QueryAsync<BlogPostDto>(@"
            SELECT
                id,
                title,
                SUBSTRING(content, 1, 200) as summary
            FROM blog_posts
        ")).ToList();
    }
}

Mapper automatique: Convention mature-Mapper basé

AutoMapper is the most popular mapping library, though slower than Mapster.

// Install: AutoMapper and AutoMapper.Extensions.Microsoft.DependencyInjection
using AutoMapper;

public class MappingProfile : Profile
{
    public MappingProfile()
    {
        CreateMap<BlogPost, BlogPostDto>()
            .ForMember(d => d.Summary, opt => opt.MapFrom(s =>
                s.Content.Length > 200 ? s.Content.Substring(0, 200) : s.Content))
            .ForMember(d => d.CategoryNames, opt => opt.MapFrom(s =>
                s.Categories.Select(c => c.Name)));

        // Reverse map
        CreateMap<BlogPostDto, BlogPost>()
            .ForMember(d => d.Content, opt => opt.Ignore());
    }
}

// Registration in Program.cs
services.AddAutoMapper(typeof(MappingProfile));

// Usage
public class BlogService
{
    private readonly IMapper _mapper;
    private readonly string _connectionString;

    public BlogService(IMapper mapper, IConfiguration configuration)
    {
        _mapper = mapper;
        _connectionString = configuration.GetConnectionString("DefaultConnection");
    }

    public async Task<List<BlogPostDto>> GetPostsAsync()
    {
        using var connection = new NpgsqlConnection(_connectionString);

        var posts = await connection.QueryAsync<BlogPost>(@"
            SELECT id, title, content FROM blog_posts
        ");

        return _mapper.Map<List<BlogPostDto>>(posts.ToList());
    }
}

Mapping manuel: Contrôle complet

Parfois, la meilleure approche consiste à cartographier explicitement par manuel:

public static class BlogPostMapper
{
    public static BlogPostDto ToDto(this BlogPost post)
    {
        return new BlogPostDto
        {
            Id = post.Id,
            Title = post.Title,
            Summary = post.Content.Length > 200
                ? post.Content.Substring(0, 200) + "..."
                : post.Content,
            CategoryNames = post.Categories?.Select(c => c.Name).ToList() ?? new List<string>()
        };
    }

    public static List<BlogPostDto> ToDtoList(this IEnumerable<BlogPost> posts)
    {
        return posts.Select(p => p.ToDto()).ToList();
    }

    // Inline mapping for simple cases
    public static BlogPostDto MapToDto(BlogPost post) => new()
    {
        Id = post.Id,
        Title = post.Title,
        Summary = post.Content[..Math.Min(200, post.Content.Length)]
    };
}

// Usage
var posts = await _repository.GetAllPostsAsync();
var dtos = posts.ToDtoList();

Comparaison du rendement de la cartographie

BenchmarkDotNet Results (mapping 1000 objects):

Method              | Mean      | Allocated
--------------------|-----------|----------
Manual Mapping      | 45.2 μs   | 78 KB
Mapster             | 52.1 μs   | 79 KB
AutoMapper          | 184.3 μs  | 156 KB

Key Takeaways:

  • La cartographie manuelle est la plus rapide mais nécessite davantage de code
  • Mapster est presque aussi rapide avec moins de code
  • AutoMapper est pratique mais a des frais généraux supérieurs

Approches hybrides: Le meilleur des deux mondes

Dans les applications réelles, vous voulez souvent utiliser des approches différentes pour différents scénarios dans la même application. l'approche recommandée pour la plupart des systèmes de production.

Modèle CQRS: Séparation des lectures et des écritures

Le modèle CQRS (Command Query Responsibility Segregation) est un bon choix pour les approches hybrides d’accès aux données . Pour une immersion plus approfondie dans le SCRS et la source des événements avec Marten, voir mon article sur CQRS moderne et recherche d'événements.

Comment Marten se rapporte à cette discussion:

Marten est une base de données de documents et un magasin d’événements construit sur PostgreSQL qui permet l’accès hybride aux données à un autre niveau.

  • Source des événements pour les écritures (produits d'événement immuables)
  • Prévisions pour les lectures ( vues matérialisées optimisées pour les demandes de renseignements
  • PostgreSQL's JSONB pour le stockage de documents
  • Modèles de CQRS construits-

Alors que cet article se concentre sur l'accès aux données relationnelles traditionnelles (EF CoreM SK1 Dapper), Marten montre comment vous pouvez tirer parti des fonctionnalités avancées de PostgreSQL ' (JSONBMNK5 flux d'événementsMSC6 pour mettre en œuvre des architectures sophistiquéesMST7 Les principes sont les mêmes MST8

  • Modèles d'écriture séparés (optimisé pour les opérations et la cohérenceM SK1
  • Modèles de lecture séparés (optimisé pour les demandes et la performance)
  • Utiliser l'outil approprié pour chaque emploi
graph TB
    Client[Client Application]

    subgraph "Write Side - Commands"
        WriteAPI[Write API / Commands]
        EFCore[EF Core Context]
        WriteDB[(PostgreSQL<br/>Write Operations)]
    end

    subgraph "Read Side - Queries"
        ReadAPI[Read API / Queries]
        Dapper[Dapper Repository]
        ReadDB[(PostgreSQL<br/>Read Operations)]
    end

    Client -->|Create/Update/Delete| WriteAPI
    WriteAPI --> EFCore
    EFCore -->|Change Tracking<br/>Validation<br/>Business Logic| WriteDB

    Client -->|Query/Search| ReadAPI
    ReadAPI --> Dapper
    Dapper -->|Optimized SQL<br/>DTOs<br/>No Tracking| ReadDB

    WriteDB -.->|Same Database| ReadDB

    style Client stroke:#6366f1,stroke-width:2px
    style WriteAPI stroke:#2563eb,stroke-width:2px
    style EFCore stroke:#2563eb,stroke-width:2px
    style WriteDB stroke:#2563eb,stroke-width:2px
    style ReadAPI stroke:#059669,stroke-width:2px
    style Dapper stroke:#059669,stroke-width:2px
    style ReadDB stroke:#059669,stroke-width:2px

Ce modèle permet d’augmenter le taux de participation

  • EF Core pour les écritures: Changement de suivi, validationM SK2 règles commerciales
  • Dapper pour les lectures: Performance et souplesse maximales de la demande
  • La même base de données, différents modèles d'accès optimisés pour chaque utilisation

Mise en œuvre: CQRS avec EF Core et Dapper

// Commands: Use EF Core for change tracking and validation
public class BlogCommandService
{
    private readonly BlogDbContext _context;
    private readonly ILogger<BlogCommandService> _logger;

    public BlogCommandService(BlogDbContext context, ILogger<BlogCommandService> logger)
    {
        _context = context;
        _logger = logger;
    }

    public async Task<int> CreatePostAsync(CreatePostCommand command)
    {
        // Business logic and validation
        var post = new BlogPost
        {
            Title = command.Title,
            Content = command.Content,
            CategoryId = command.CategoryId,
            PublishedDate = DateTime.UtcNow
        };

        _context.BlogPosts.Add(post);
        await _context.SaveChangesAsync();

        _logger.LogInformation("Created blog post {PostId}", post.Id);

        return post.Id;
    }

    public async Task UpdatePostAsync(UpdatePostCommand command)
    {
        var post = await _context.BlogPosts.FindAsync(command.Id);

        if (post == null)
            throw new InvalidOperationException($"Post {command.Id} not found");

        post.Title = command.Title;
        post.Content = command.Content;
        post.UpdatedAt = DateTime.UtcNow;

        await _context.SaveChangesAsync();

        _logger.LogInformation("Updated blog post {PostId}", post.Id);
    }

    public async Task DeletePostAsync(int id)
    {
        var post = await _context.BlogPosts.FindAsync(id);

        if (post != null)
        {
            _context.BlogPosts.Remove(post);
            await _context.SaveChangesAsync();

            _logger.LogInformation("Deleted blog post {PostId}", id);
        }
    }
}

// Queries: Use Dapper for read performance
public class BlogQueryService
{
    private readonly string _connectionString;
    private readonly ILogger<BlogQueryService> _logger;

    public BlogQueryService(IConfiguration configuration, ILogger<BlogQueryService> logger)
    {
        _connectionString = configuration.GetConnectionString("DefaultConnection");
        _logger = logger;
    }

    public async Task<BlogPostDto> GetPostBySlugAsync(string slug)
    {
        using var connection = new NpgsqlConnection(_connectionString);

        const string sql = @"
            SELECT
                p.id,
                p.title,
                p.slug,
                p.content,
                p.published_date,
                c.id as category_id,
                c.name as category_name,
                (SELECT COUNT(*) FROM comments WHERE blog_post_id = p.id) as comment_count
            FROM blog_posts p
            INNER JOIN categories c ON p.category_id = c.id
            WHERE p.slug = @Slug";

        var post = await connection.QueryFirstOrDefaultAsync<BlogPostDto>(sql, new { Slug = slug });

        if (post != null)
        {
            _logger.LogInformation("Retrieved blog post by slug {Slug}", slug);
        }

        return post;
    }

    public async Task<PagedResult<BlogPostSummaryDto>> GetRecentPostsAsync(int page, int pageSize)
    {
        using var connection = new NpgsqlConnection(_connectionString);

        const string sql = @"
            SELECT
                p.id,
                p.title,
                p.slug,
                LEFT(p.content, 200) as summary,
                p.published_date,
                c.name as category_name
            FROM blog_posts p
            INNER JOIN categories c ON p.category_id = c.id
            ORDER BY p.published_date DESC
            LIMIT @PageSize OFFSET @Offset";

        const string countSql = "SELECT COUNT(*) FROM blog_posts";

        var posts = await connection.QueryAsync<BlogPostSummaryDto>(
            sql,
            new { PageSize = pageSize, Offset = (page - 1) * pageSize }
        );

        var totalCount = await connection.ExecuteScalarAsync<int>(countSql);

        return new PagedResult<BlogPostSummaryDto>
        {
            Items = posts.ToList(),
            TotalCount = totalCount,
            Page = page,
            PageSize = pageSize
        };
    }

    public async Task<List<BlogPostDto>> SearchPostsAsync(string searchTerm)
    {
        using var connection = new NpgsqlConnection(_connectionString);

        const string sql = @"
            SELECT
                p.id,
                p.title,
                p.slug,
                p.content,
                p.published_date,
                c.name as category_name,
                ts_rank(p.search_vector, query) as relevance_score
            FROM blog_posts p
            INNER JOIN categories c ON p.category_id = c.id,
                 to_tsquery('english', @SearchTerm) query
            WHERE p.search_vector @@ query
            ORDER BY relevance_score DESC
            LIMIT 50";

        var posts = await connection.QueryAsync<BlogPostDto>(sql, new { SearchTerm = searchTerm });

        _logger.LogInformation(
            "Searched posts with term {SearchTerm}, found {Count} results",
            searchTerm,
            posts.Count()
        );

        return posts.ToList();
    }
}

// Service layer orchestrating commands and queries
public class BlogService
{
    private readonly BlogCommandService _commands;
    private readonly BlogQueryService _queries;

    public BlogService(BlogCommandService commands, BlogQueryService queries)
    {
        _commands = commands;
        _queries = queries;
    }

    // Write operations delegate to command service
    public Task<int> CreatePostAsync(CreatePostCommand command) => _commands.CreatePostAsync(command);
    public Task UpdatePostAsync(UpdatePostCommand command) => _commands.UpdatePostAsync(command);
    public Task DeletePostAsync(int id) => _commands.DeletePostAsync(id);

    // Read operations delegate to query service
    public Task<BlogPostDto> GetPostBySlugAsync(string slug) => _queries.GetPostBySlugAsync(slug);
    public Task<PagedResult<BlogPostSummaryDto>> GetRecentPostsAsync(int page, int pageSize)
        => _queries.GetRecentPostsAsync(page, pageSize);
    public Task<List<BlogPostDto>> SearchPostsAsync(string searchTerm)
        => _queries.SearchPostsAsync(searchTerm);
}

EF Core avec SQL brut occasionnel

Pour les applications qui sont essentiellement EF Core mais nécessitent une optimisation occasionnelle de la performance:

public class BlogService
{
    private readonly BlogDbContext _context;

    // 95% of queries: Use EF Core LINQ
    public async Task<List<BlogPost>> GetPostsByCategoryAsync(int categoryId)
    {
        return await _context.BlogPosts
            .Where(p => p.CategoryId == categoryId)
            .Include(p => p.Comments)
            .ToListAsync();
    }

    // 5% of queries: Use raw SQL for complex analytics
    public async Task<List<PostAnalytics>> GetPostAnalyticsAsync()
    {
        using var connection = _context.Database.GetDbConnection();
        await _context.Database.OpenConnectionAsync();

        using var command = connection.CreateCommand();
        command.CommandText = @"
            WITH post_metrics AS (
                SELECT
                    p.id,
                    p.title,
                    COUNT(DISTINCT c.id) as comment_count,
                    COUNT(DISTINCT v.id) as view_count,
                    AVG(c.sentiment_score) as avg_sentiment
                FROM blog_posts p
                LEFT JOIN comments c ON p.id = c.post_id
                LEFT JOIN post_views v ON p.id = v.post_id
                WHERE p.published_date >= NOW() - INTERVAL '30 days'
                GROUP BY p.id, p.title
            )
            SELECT * FROM post_metrics
            ORDER BY view_count DESC";

        var analytics = new List<PostAnalytics>();
        using var reader = await command.ExecuteReaderAsync();

        while (await reader.ReadAsync())
        {
            analytics.Add(new PostAnalytics
            {
                PostId = reader.GetInt32(0),
                Title = reader.GetString(1),
                CommentCount = reader.GetInt64(2),
                ViewCount = reader.GetInt64(3),
                AverageSentiment = reader.IsDBNull(4) ? 0 : reader.GetDouble(4)
            });
        }

        return analytics;
    }
}

Comparaison du rendement

Let's look at real-world performance benchmarks for common operations with PostgreSQL

Indicateur de référence: Lecture M SK1 Enregistrements

BenchmarkDotNet Results (Lower is Better):

Method                    | Mean       | Allocated
------------------------- |----------- |-----------
EF Core (No Tracking)    | 12.34 ms   | 2.4 MB
EF Core (With Tracking)  | 15.67 ms   | 4.8 MB
Dapper                   | 8.21 ms    | 1.8 MB
Raw Npgsql               | 7.45 ms    | 1.2 MB

Indicateur de référence: Inserting M SK1 Records

Method                    | Mean       | Allocated
------------------------- |----------- |-----------
EF Core (SaveChanges)    | 245.3 ms   | 15.2 MB
EF Core (BulkInsert)     | 42.1 ms    | 8.4 MB
Dapper (Loop)            | 189.7 ms   | 2.1 MB
Npgsql COPY              | 18.3 ms    | 0.8 MB

Benchmark: Complex Join Query

Method                    | Mean       | Allocated
------------------------- |----------- |-----------
EF Core (Include)        | 28.5 ms    | 5.2 MB
EF Core (Split Query)    | 24.1 ms    | 4.8 MB
Dapper (Multi-Map)       | 16.8 ms    | 3.1 MB
Raw Npgsql               | 15.2 ms    | 2.4 MB

Key Takeaways

  1. Raw Npgsql est le plus rapide mais nécessite le plus de code
  2. Dapper offre une excellente performance avec un coût d’abstraction minimal (60-70% plus rapide que EF CoreM SK1
  3. EF Core no-questions de suivi sont raisonnables pour la plupart des scénarios
  4. Opérations en vrac indiquer les lacunes de performance les plus importantes (10-13x différenceM SK1
  5. Répartition de la mémoire suivre un modèle semblable au temps d'exécution

Matrice de décision: Which Approach to Use

Utiliser EF Core When:

  • ✅ Élaborer une nouvelle application avec des exigences en évolution
  • ✅ Domaine-conception axée sur des modèles d'entités riches
  • ✅ Vous avez besoin de migrations et de gestion du schéma
  • ✅ L’équipe est plus à l’aise avec C# que SQL
  • ✅ Lire / le rapport écrit est équilibré
  • ✅ La performance de la demande à l’intérieur de 20-50% est acceptable
  • ✅ Vous voulez changer le suivi et l’unité du modèle de travail

Voir Partie 1 pour les lignes directrices de base complètes de l’EF.

Utiliser le Dapper quand:

  • ✅ Le rendement est important mais pas critique
  • ✅ Vous avez des questions complexes qui ne correspondent pas bien à LINQ
  • ✅ Vous'êtes à l’aise pour écrire SQL
  • ✅ Vous avez besoin d’un bon contrôle de la génération SQL
  • ✅ Travailler avec les schémas de base de données existants
  • ✅ Lire-charges de travail lourdes avec des écritures simples
  • ✅ Vous voulez une abstraction minimale au-dessus des coûts

Utiliser Raw Npgsql quand:

  • ✅ La performance maximale est essentielle
  • ✅ Établissement de processeurs de données à haut débit
  • ✅ Travailler en étroite collaboration avec PostgreSQL
  • ✅ Opérations en série et importations en vrac
  • ✅ Chaque milliseconde et chaque mégabyte sont importants
  • ✅ Vous avez besoin de contrôle absolu

Approche hybride Quand:

  • ✅ Les différentes parties de l’application ont des besoins différents
  • ✅ Modèle de CQRS ( Core EF pour les écritures, Dapper pour les lectures
  • ✅ La plupart des demandes utilisent EF Core, mais quelques-uns ont besoin de SQL brut
  • ✅ Vous voulez une migration graduelle entre les approches
  • ✅ Grandes applications complexes , avec des exigences variées

Résumé des meilleures pratiques

Lignes directrices générales

  1. Commencer par EF Core pour de nouveaux projets à moins que vous ayez des exigences spécifiques en matière de rendement
  2. Profil avant l'optimisation - donM SK1 ne présume pas que vous avez besoin de Dapper/raw SQL
  3. Utiliser des approches hybrides - combine les forces des différents outils
  4. Garder la logique de l'accès aux données isolée Le modèle de repos - aide à changer les mises en œuvre
  5. Utiliser la mise en commun des connexions - configure correctement votre charge de travail
  6. Utiliser les fonctionnalités de PostgreSQL - don 't abstraction des capacités de base de données puissantes

Établissements spécifiques à l'acier

  1. Utiliser des demandes paramétrisées pour empêcher l'injection SQL
  2. Examiner le caching des résultats de la demande pour les demandes répétées expensive,
  3. Utiliser la cartographie multi- pour les assemblages au lieu de multiples ronds-trips
  4. Réutiliser les connexions à partir de la base de connexion
  5. Considérer Dapper.Contrib pour des opérations de CRUD simples

Conseils sur la performance

  1. Indiquer vos demandes - analyser les demandes lentes avec ANALYSE DE L'EXPLAINT
  2. Utiliser des déclarations préparées pour les demandes répétées
  3. Opérations par lots lorsque possible
  4. Utiliser COPY pour les entrées en vrac dans PostgreSQL
  5. Surveillance du pool de connexion - configure min/ taille maximale du bassin
  6. Examiner les répliques de lecture pour lire-charges de travail lourdes

Conclusion

Choisir la bonne approche d’accès aux données pour votre application .NET avec PostgreSQL n’est pas abouti à trouver l’outil le mieux adapté à vos besoins particuliers, mais plutôt à trouver celui qui vous convient.

  • EF Core (voir Partie 1) excelle dans le développement rapideM SK1 la modélisation de domaines , et les applications où la productivité des développeurs est supérieure à la performance brute
  • Dapper fournit un excellent milieu moyen avec une performance optimale et une abstraction raisonnable proche de -
  • Npgsql brut offre une performance et un contrôle maximaux pour les opérations intensives de données-
  • Bibliothèques de cartographie comme Mapster et AutoMapper réduisent la plaque de boiler lorsqu’on travaille avec un accès à des données de niveau inférieur-

Dans la pratique,, les applications les plus réussies utilisent souvent un approche hybride, tirer parti des points forts de chaque outil, le cas échéant

  • Utilisation EF Core pour votre logique de domaine et écrits
  • Utilisation Dapper pour les demandes de lecture complexes et la production de rapports
  • Utilisation Npgsql brut pour les opérations en vrac et l'analyse

La clé est à:

  1. Comprendre vos exigences - rendementM SK1 rapidité de développement , compétences d'équipe
  2. Profil de votre application - Identifier les lacunes réelles , non supposées
  3. Choisissez pragmatiquement - utiliser l'outil le plus simple qui répond à vos besoins
  4. Maintenir la souplesse - vous pouvez mélanger les approches dans la même application

Rappelez-vous que l’optimisation prématurée est la source de tout mal, mais c’est aussi la construction d’un système qui peut mesurer les performances lorsque cela s’avère nécessaire.

Références et lectures complémentaires

Partie 1 de cette sérieM SK1

Documentation officielle:

Articles connexes sur ce blog:


Cela conclut notre série de deux volets sur l’accès aux données dans le .NET! Nous ' avons couvert tout, depuis les résumés puissants d’EF CoreM SK4 jusqu’à la SQL bruteM Sk5 la performance maximaleM sk6 avec des lignes directrices pratiques sur la combinaison d’approches pour obtenir des résultats optimaux M Sk7

Finding related posts...
logo

© 2026 Scott Galloway — Unlicense — All content and source code on this site is free to use, copy, modify, and sell.