Back to "Datatillgång i .NET: Vergleich av ORMs och kartläggningsstrategier | ( | Parti |2 | - | Dapper | , | rådata SQL |, | och hybrida tillvägagångssätt |"

This is a viewer only at the moment see the article on how this works.

To update the preview hit Ctrl-Alt-R (or ⌘-Alt-R on Mac) or Enter to refresh. The Save icon lets you save the markdown file to disk

This is a preview from the server running through my markdig pipeline

.NET Dapper Database Performance PostgreSQL

Datatillgång i .NET: Vergleich av ORMs och kartläggningsstrategier | ( | Parti |2 | - | Dapper | , | rådata SQL |, | och hybrida tillvägagångssätt |

Wednesday, 03 December 2025

Välkomna till delen 2 av vår kompletta guide om datatillgång i .NET! I Del 1, vi undersökte subjekt ramverkets kärna i djupet , inklusive SQL-generation , vanliga mönster, , och den kritiska varningen om försedlar och kassering

I denna artikel, ,, ska vi undersöka de lättare alternativen till - och hur man kombinerar flera tillvägagångssätt för optimal prestation.

  • Damper: Mikron
  • Rout ADO.NET/Npgsql: Maximalna effekt och kontroll
  • Objektkarteringsbibliotek: Mapster mot AutoMapper
  • Hybrida förhållningssätt: Kombination av EF Core och Dapper
  • Leistungsindikatorer: Verkliga comparisoner
  • Bestämmande matrix: Välja det rätta verktyget för ditt scenario

Tabellen med innehåll

Dapper: Micro-ORM

Damper är en lightweight, hög -presterande mikroM SK2ORM skapad av Stack Overflow. Den erbjuder en tunn lager över ADOMska4NETMske5 som hanterar det tråkiga arbetet med att kartlägga sökresultat till objekt medan den ger dig full SQL-kontroll

Varför finns Dapper?

Dapper föddes ut ur Stack Overflow's behov av hög datatillgång-presterande datatillgängM SK2 Teamet fann att fulla ORM som Entity Framework MSC3pre~-Core ) lagt till för mycket överskott för deras höga överskottsmängderMska6trafikscenarierMske7 Dapper ger dig mska8 bekvämlighet med bara 5-15% överskott över råADO.NET

Nyckliga egenskaper

  • Rou SQL: Du skriver alla SQL själv - komplett kontroll
  • Högpresterande: Minimalt överhead över ADO.NET (5-15%)
  • Multi-Mapping: kartlägg komplexa frågor till flera besläktade typer
  • Handling av parametrer: Automatisk parameterisering förhindrar SQL-injiction
  • Transaktionsstöd: Hela kontroll över transaktioner
  • Enkelhet: Ingen konfiguration
  • Async/ Vänta: Full async support for modern .NET

Grundtredapper-exempla

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-Mapping

En av Dappers mest kraftfulla egenskaper är multi- -kartläggning. Man kan effektivt hantera kopplingar och kartlägga flera objekt.

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;
}

Advanced Dapper Techniques

Dynamiska parametrar för komplexa frågor:

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);
}

Custom-typbehandlare för PostgreSQL-typer:

// 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>>());

Storleksoperationer med 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();
}

Transaktionsstöd:

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;
    }
}

När man ska använda Dapper

✅ Använd datadapper när

  • Tillförseln är avgörande men du behöver inte
  • Du' är bekväm med att skriva SQL
  • Du har komplexa frågor som inte maprar bra på LINQ
  • Du behöver en fin kontroll över SQL-generering
  • Du' arbetar med ett existerande databasschema
  • Du vill ha minimala överkostnader och beroende
  • Att använda minnet är ett problem
  • Du behöver dra nytta av databasen-specifika egenskaper på ett brett sätt

❌ undvika däpper när

  • Din grupp är inte bekväm med SQL
  • Du behöver en automatisk förändringsspårning
  • Du vill ha skemas migrationer som kod
  • Du har ett snabbt utvecklande schema
  • Du behöver cross-databas förnybarhet
  • Du föredrar LINQ än SQL syntax

Förmågor och egenskaper

  • Försöksresultat: 5-15% överhead över rå ADO.NET
  • minnesanvändning: Minimalt - inga förändringsspårning eller försedelser
  • Operationer i stor skala: Utmärkt med PostgreSQL COPY
  • Den första fråga: snabb - inga kompilations steg
  • Developerproduktivitet: kräver SQL kunskap men är mycket förutsägbar

Rou ADO.NET med Npgsql

För absolut maxpresterande och kontroll Npgsql direkt utan någon ORM lager.

När rå ADO.NET blir begripligt

Rou ADO.NET är lämpligt när

  • Du behöver varje millisekunder av resultat
  • Du bygger höghastighetsdataprocessor
  • Du' arbetar med PostgreSQL-specifika drag som inte supporteras av ORMs
  • Du behöver en exakt kontroll över minnesfördelningar
  • Du gör stormängdoperationer eller strömmar stora datamängder

exempel: 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)
            };
        }
    }
}

Förmågor och egenskaper

  • Försöksresultat: Standard - snabbast möjligt
  • minnesanvändning: Den minsta - fullständig kontroll över fördelningar
  • Operationer i stor skala: Utmärkt med COPY-protokollet
  • Developerproduktivitet: kräver mest manualt arbete

Objektkarteringsbibliotek

När man arbetar med Dapper eller rå ADO.NET

Mapster: Höga-Leistungskartläggning

Mapster är en snabb, konventionell objektmapper som använder källgenerering för optimal prestation.

// 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();
    }
}

AutoMapper: Fortskridna Konventioner-baserade mapper

AutoMapper är den mest populära kartläggningsbiblioteket, även om den är långsammare än 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());
    }
}

Handkartlagning: Hela kontroll

Ibland är det bästa sättet att göra ett explicit handkartlag:

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();

Att kartlägga och jämföra prestationer

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

Keytagare:

  • Handkartläggning är snabbast men kräver mer kod
  • Mapster är nästan lika snabb med mindre kod
  • AutoMapper är bekvämt men har högre överbud

Hybrida tillvägagångssätt

I verkliga applikationer, vill du ofta använda olika angreppssätt för olika scenarior inom samma applikation. Detta är den tillämpna metoden för de flesta produktionssystem.

CQRS-mönster : Separerande läser och skriver

CQRS-mönstret (Kommandofrågans ansvarsskiktning Segregation ) är ett naturligt sätt att anpassa sig till hybrida datatillgångssätt. Marten, se min artikel på Modern CQRS och Event Sourcing.

Hur Marten relaterar till den här diskussionen:

Marten är en dokumentdatabas och eventbutik byggd på PostgreSQL som tar tillgång till data på ett annat nivå.

  • Trägning av händelser för skrivar (immutable event streams)
  • Prognoser för läsningar (materialiserade synpunkter optimerade för queryer)
  • PostgreSQL's JSONB för dokumentlagning
  • Inbyggda CQRS-mönster

Medan den här artikeln fokuserar på traditionell relationell data-tillgång (EF Core , Dapper), Marten visar hur man kan utnyttja PostgreSQLs avancerade drag och sträcka | | ' | Advanced Features | ( | JSONB | МSK5 | Event streams ♫ ) | för att implementera sofistikerade arkitekturer ♫

  • separata skrivande modeller (optimiserad för transaktioner och konsistens )
  • Separata läsade modeller (optimiserad för queryer och resultat)
  • Använd rätt verktyg för varje jobb
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

Detta mönster leverar

  • EF: s kärna för skrivar: ändra spårning , bekräftelse
  • Damper för läsningar: Maksimum sökresultat och flexibilitet
  • Samma database , olika tillgångsmönster optimerade för varje användarfall

Implementering: CQRS med EF Core och 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 med Occasional Raw SQL

För tillämpningar som är främst EF Core men som ibland behöver optimera prestationen:

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;
    }
}

Förmågorsvergleich

Låt oss titta på reala performance-benchmarker för vanliga operationer med PostgreSQL

Benchmarken : Läsning 1000 Rekord

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

Benchmark : Inserting 1000 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: komplexa anslutande fråga

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

Ta med era nyckeln

  1. Rou Npgsql är den snabbaste men kräver den största koden
  2. Dapper erbjuder utmärkt prestation med minimala abstraktionskostnader (60-70% snabbare än EF CoreM SK1
  3. EF Core no-spårning sökningar är rimliga för de flesta scenarior
  4. Operationer i stor skala visa de största skillnaderna i prestationen (10-13x skillnad
  5. minnesfördelningar följa ett liknande mönster som följer med inspelningstid

Bestämmande matrix : Vilket sätt man ska använda

Använd EF Core när:

  • ✅ Att bygga en ny applikation med växande krav
  • ✅ Domain -driven design med rika subjektmodeller
  • ✅ Du behöver migrationer och schemamanagement
  • ✅ Team är bekvämare med C# än SQL
  • ✅ Läsa / skriva snittet är balanserat
  • ✅ Frågaresultatet inom 20-50% som är optimalt är acceptabelt
  • ✅ Du vill ändra spårning och arbetsmönster

Se Del 1 för en övergripande EF: s yttre vägledning

Använd datadapper när:

  • ✅ Performance är viktigt men inte kritiskt
  • ✅ Du har komplexa frågor som inte maprar bra på LINQ
  • ✅ Du ' är bekväm med att skriva SQL
  • ✅ Du behöver fin kontroll över SQL-generering
  • ✅ Att arbeta med existerande databasschemat
  • ✅ Läsa - tunga arbetslast med enkla skrivar
  • ✅ Du vill ha minimal abstraktion överhead

Använd rå Npgsql när:

  • ✅ Maksimumpresterande är viktigt
  • ✅ Att bygga höghastighetsdataprocessor
  • ✅ Att arbeta i stor utsträckning med PostgreSQL
  • ✅ Batchoperationer och stora mängder import
  • ✅ Varje millisekunder och megabyte spelar roll
  • ✅ Du behöver absolut kontroll

Hybrida tillvägagångssätt När:

  • ✅ Olika delar av applikationen har olika behov
  • ✅ CQRS-mönster
  • ✅ De flesta querier använder EF Core , men några behöver rå SQL
  • ✅ Du vill ha gradvis migration mellan angreppssätt
  • ✅ StoraM SK1 komplexa tillämpningar med olika krav

Best Practices Summary

Andra generallinjer

  1. Börja med EF Core för nya projekt om du inte har specifika målsättningar
  2. Profil före optimering - don ' antar inte att du behöver Dapper
  3. Använd hybrida angrepp - kombinerar styrkor hos olika verktyg
  4. Håll datatillgångslogiken isolerad - lageringsmönster hjälper till att byta implementeringar
  5. Använd kopplingspooling - konfigurera ordentligt för din arbetslast
  6. Leveragera PostgreSQLs egenskaper - don ' inte abstraher powerful database capabilities

Spesifikt

  1. Använd parameteriserade frågor för att förhindra SQL-injictioner
  2. Tänk på query resultat i kassering för dyra, upprepade frågor
  3. Använd multi-kartläggning för anslutningar istället för flera runda-vägar
  4. Återanvända kopplingar från kopplingspool
  5. Tänk på Dapper.Contrib för enkla CRUD-operationer

Performance-tips

  1. Indeksera dina frågor - analysera långsamma frågor med EXPLAIN ANALYZE
  2. Använd förberedda deklarationer för upprepade frågor
  3. Batchoperationer när möjligt
  4. Använd COPY för massiva wstawningar i PostgreSQL
  5. Att övervaka kopplingspoolen - konfigurera min
  6. Fundera på läsa repliker för att läsa- tunga arbetslaster

Slutsatsen

Att välja den rätta datatillgångssätten för ditt .NET-applikation med PostgreSQL handlar inte om att hitta det bästa | " | " | verktyget, utan om att matcha det rätta verktyget till dina specifika behov.

  • EF: s kärna (se Del 1) är bra på snabb utveckling , domain-modellering , och tillämpningar där utvecklarens produktivitet överträffar råprestentialen
  • Damper ger en utmärkt mellangrund med nära-optimala resultat och rimlig abstraktion
  • rå Npgsql tillhandahåller högsta effekt och kontroll för data
  • Bibliotek som kartläggs som Mapster och AutoMapper minskar boilerplatet när man arbetar med lägre datatillgång

I praktiken använder de mest framgångsrika applikationerna ofta hybrid metod, att dra nytta av varje verktygsdelar när det behövs

  • Använda EF: s kärna för din domainlogi och skriver
  • Använda Damper för komplexa läsande frågor och rapportering
  • Använda rå Npgsql för stormarknadsoperationer och analys

Nyckeln är till:

  1. Understand your requirements -presterande
  2. Profila din application - identifiera de faktiska bottlenecks
  3. Välj på ett pragmatiskt sätt - använda det enklaste verktyget som möter dina behov
  4. Bli flexibel - du kan blanda angrepp inom samma applikation

Kom ihåg att för tidigt optimering är grunden till allt ondske, men också att bygga ett system som kan skala upp när det behövs. . Starta enkelt, , mäta prestationer och optimera när det är viktigt.

Referenser och ytterligare svar

Del 1 av denna serie

officiella dokumentation:

Related Articles on This Blog:


Det avslutar vår två delar- -om datatillgång i ., NET, ! och '. Vi har omfattat allt från EF Core- -med kraftfulla abstraktioner till rå SQL- -och '- -möjliga resultat- -,- med praktiska instruktioner om att kombinera metoder för optimala resultat.

logo

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