Datenzugriff im Vergleich zu ORMs und Mapping-Strategien (Deutsch (German))

Datenzugriff im Vergleich zu ORMs und Mapping-Strategien

Wednesday, 03 December 2025

//

20 minute read

Willkommen bei Teil -2- unseres umfassenden Leitfadens für den Datenzugriff in TeilWir erforschten den Entity Framework Core in der Tiefe, einschließlich SQL-Generation, häufige Fallstricke und die kritische Warnung vor Proxys und Caching.

In diesem Artikel erkunden wir die leichteren Alternativen und wie man mehrere Ansätze für eine optimale Performance kombiniert

  • **Dapper**Die Mikro-- ORM Sweet Spot
  • **Rohes ADO-.-NET-/Npgsql**Maximale Leistung und Kontrolle
  • Objektmapping-Bibliotheken: Kartenspieler vs. Automapper
  • Hybride AnsätzeKombination von EF-Kern und Dapper-Mustern aus (CQRS
  • LeistungsvergleicheVergleich der realen Werte der Welt
  • EntscheidungsmatrixWählen Sie das richtige Werkzeug für Ihr Szenario

Inhaltsverzeichnis

Dapper, das Mikro-- ORM

Dapper ist ein leichtgewichtiges, hochleistungsfähiges Mikro-- ORM, das von Stack Overflow erstellt wurde.. . Es bietet eine dünne Schicht über ADOO. .NET ., . die mühsame Arbeit der Mapping Abfrageergebnisse zu Objekten während Sie voller SQL-Steuerung erhalten

Warum Dapper existiert

Dapper wurde aus Stack Overflow geboren.''s Bedarf an High--'performance-Datenzugriff Das Team fand heraus, dass volle ORMs wie Entity Framework (' Pre...-'Core-)' zu viel Overhead für ihre hohen -'traffic Szenarien hinzugefügt haben.MS K7' Dapper gibt Ihnen die Bequemlichkeit mit nur ,5-15%' Overhead über rohen ADO-MS K10'NET , .'

Hauptmerkmale

  • Rohes SQLSie schreiben alle SQL selbst
  • Hohe LeistungMinimaler Overhead über ADO-.-NET
  • Multi---MappingErfasse komplexe Abfragen an mehrere verwandte Typen
  • ParameterbearbeitungAutomatische Parametrierung verhindert SQL-Einspritzung
  • TransaktionsunterstützungVolle Kontrolle über Transaktionen
  • EinfachheitKeine Konfiguration, keine Änderung des Trackings, keine Magie
  • Async-/-AwaitVollständige async-Unterstützung für modernes

Einfache Dapper-Beispiele

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-:-Handhabung verbindet

Eine der leistungsstärksten Funktionen von Dapper-' ist Multi-M--Mapping-Software für effizientes Handling von Joins und Mapping zu mehreren verwandten Objekten.

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

Fortgeschrittene Dapper-Techniken

Dynamische Parameter für komplexe Abfragen

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

Benutzerdefinierte Typ-Handler für PostgreSQL-Typen

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

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

Transaktionsunterstützung

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

Wann ist Dapper zu verwenden?

Verwenden Sie den Dapper, wenn das :

  • Leistung ist kritisch, aber du brauchst absolute maximale Geschwindigkeit
  • Sie sind bequem SQL zu schreiben
  • Sie haben komplexe Abfragen, die don-'.t Karte gut zu LINQ
  • Sie benötigen eine feine Kontrolle über SQL-Generation
  • Sie arbeiten mit einem bestehenden Datenbankschema
  • Sie wollen minimale Overhead- und Abhängigkeiten
  • Speichernutzung ist ein Anliegen
  • Sie müssen Datenbank nutzen.--spezifische Funktionen ausgiebig

Vermeiden Sie Dapper, wenn -:

  • Ihr Team fühlt sich mit SQL nicht wohl
  • Sie benötigen automatische Änderungsverfolgung
  • Sie wollen Schemamigrationen als Code
  • Sie haben ein sich schnell entwickelndes Schema
  • Sie benötigen die Portabilität von cross--?
  • Sie bevorzugen LINQ als SQL Syntax

Leistungsmerkmale

  • AbfrageleistungÜber Kopf über dem rohen ADO-.-NET
  • SpeichernutzungKeine Änderung Tracking oder Proxies
  • MassenbetriebAusgezeichnet mit PostgreSQL COPY
  • Erste AbfrageKeine Kompilationsschritte
  • Produktivität des EntwicklersBenötigt SQL-Wissen, aber sehr vorhersehbar

Rohes ADO-.-NET mit Npgsql

Für absolute maximale Leistung und Kontrolle können Sie verwenden Npgsql direkt ohne ORM-Layer

Wenn rohes ADO-.-NET Sinn macht

Rohes ADO-.-NET ist angebracht, wenn es

  • Sie brauchen jede Millisekunde Leistung
  • Sie bauen Hoch-- Durchsatzdatenprozessoren
  • Sie arbeiten mit PostgreSQL---spezifischen Funktionen, die nicht von ORMs unterstützt werden.
  • Sie benötigen genaue Kontrolle über Speicherzuweisungen
  • Sie machen Massenoperationen oder streamen große Datensätze

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

Leistungsmerkmale

  • AbfrageleistungAusgangslage -- schnellstmöglich
  • SpeichernutzungSAMMLUNG Niedrigste Kontrolle über die Zuweisungen
  • MassenbetriebAusgezeichnet mit COPY-Protokoll
  • Produktivität des EntwicklersBenötigt die meisten manuellen Arbeiten

Objektmapping-Bibliotheken

Bei der Arbeit mit Dapper oder rohen ADOO-.-NET®,, müssen Sie häufig zwischen verschiedenen Objektdarstellungen kartieren.

Kartenerstellungs- und Leistungsmapping

Kartenspieler ist ein Fast-,-Konventionsobjektmapper, der die Quellgenerierung für eine optimale Performance nutzt.

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

AutomapperT:-Reifenkonvention--basierter Mapper

Automapper ist die beliebteste Mapping-Bibliothek, obwohl langsamer als 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());
    }
}

Manuelle Mapping-Bedienung

Manchmal ist der beste Ansatz explizites manuelles Mapping.

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

Mapping Leistungsvergleich

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

Schlüssel zum Mitnehmen

  • Manuelles Mapping ist am schnellsten, erfordert aber mehr Code
  • Mapster ist fast so schnell mit weniger Code
  • AutoMapper ist bequem, hat aber einen höheren Overhead

Hybrid-Annäherungen Das Beste aus beiden Welten

In realen Anwendungen möchten Sie oft verschiedene Ansätze für unterschiedliche Szenarien in der gleichen Anwendung verwenden. der empfohlene Ansatz für die meisten Produktionssysteme

CQRS-Muster – :-Separieren liest und schreibt

Das CQRS-Muster für die Command Query Responsibility Segregation-Muster von ) ist eine natürliche Passform für hybride Datenzugriffsansätze für einen tieferen Einstieg in CQRF und Event Sourcing mit MartenSehen Sie meinen Artikel auf Moderne CQRS und Event Sourcing.

Wie Marten zu dieser Diskussion kommt

Marten ist eine Dokument-Datenbank und Ereignisspeicher auf PostgreSQL gebaut, die hybride Daten Zugriff auf ein anderes Level nimmt.

  • Beschaffung von Veranstaltungen für schreibt -(-unveränderliche Ereignisströme -)-
  • Projektionen für Lesegeräte ( materialisierte Ansichten optimiert für Abfragen )
  • PostgreSQL.'.s JSONB zur Aufbewahrung von Dokumenten
  • Eingebaut in CQRS-Muster

Während dieser Artikel konzentriert sich auf traditionelle relationale Daten-Zugriffe Mehr erfahren ,.Dapper),. Marten zeigt, wie Sie die erweiterten Funktionen von PostgreSQL.' nutzen können.

  • Separate Schreibmodelle für Transaktionen und Konsistenz optimiert
  • Separate Lesemodelle für Abfragen und Performance optimiert
  • Nutzen Sie das richtige Werkzeug für jeden Job
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

Dieses Muster nutzt .:

  • EF-Kern für schreibt::: :-Veränderungsverfolgung: ,-Validierung:,-Geschäftsregeln
  • Dapper für leses-:® Maximale Abfrageleistung und Flexibilität
  • Dieselbe Datenbank – unterschiedliche Zugriffsmuster optimiert für jeden Anwendungsfall

Implementierung: :-CQRS mit EF Core und 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-Kern mit gelegentlichem Raw SQL

Für Anwendungen, die in erster Linie EF Core sind, aber gelegentliche Performance-Optimierung benötigen

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

Leistungsvergleich

Schauen Sie sich die realen Leistungs-Benchmarks von - für gemeinsame Operationen mit PostgreSQL-: an

Benchmark-Messung von Messdaten

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-Datensätze einfügen

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-Abfrage für komplexes Join

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

Schlüssel zum Mitnehmen

  1. Raw Npgsql ist am schnellsten erfordert aber den meisten Code
  2. Dapper bietet hervorragende Leistung mit minimalen Abstraktionskosten schneller als EF-Core
  3. EF-Core-Abfragen sind für die meisten Szenarien angemessen
  4. Massengutbetrieb zeigen Sie die größten Leistungslücken
  5. Speicherzuweisungen einem ähnlichen Muster wie die Ausführungszeit folgen

Entscheidungsmatrix, welche Vorgehensweise zu verwenden ist

EF-Kern verwenden, wenn dies der Fall ist

  • Bau einer neuen Anwendung mit sich entwickelnden Anforderungen
  • Design mit reichen Entity-Modellen
  • Sie benötigen Migrationen und Schema-Management
  • Team ist komfortabler als SQL mit C.#
  • Das Schreibverhältnis ist ausgeglichen.
  • Die Abfrageleistung innerhalb von Optimum ist akzeptabel.
  • Sie wollen Änderungsverfolgung und Einheit des Arbeitsmusters

Siehe Teil für eine umfassende EF-Kernführung

Dapper verwenden, wenn -:

  • Die Leistung ist wichtig, aber nicht kritisch
  • Sie haben komplexe Abfragen, die don-'.t Karte gut zu LINQ
  • Bequemes Schreiben von SQL
  • Sie benötigen feine Kontrolle über SQL-Generierung
  • Arbeit mit bestehenden Datenbankschemas
  • Lesen Sie die schwersten Workloads mit einfachen Schreiben
  • Sie wollen minimale Abstraktion Overhead

Raw Npgsql verwenden, wenn

  • Höchste Leistungsfähigkeit ist entscheidend
  • Datenverarbeitungsmaschinen mit hohem Energieverbrauch
  • Die Arbeit mit PostgreSQL---spezifischen Eigenschaften
  • Massengut- und Batchgeschäfte
  • Jede Millisekunde und jede Megabyte zählt
  • Sie benötigen absolute Kontrolle

Hybrider Ansatz, wenn -:

  • Verschiedene Teile der Anwendung haben unterschiedliche Bedürfnisse
  • CQRS-Muster für Schreibmaschinen für Schreibgeräte für Schreibautomaten für Lesegeräte
  • Die meisten Abfragen verwenden EF Core, aber ein paar benötigen rohe SQL
  • Sie wollen allmähliche Migration zwischen den Ansätzen
  • Komplexe Anwendungen mit unterschiedlichen Anforderungen

Zusammenfassung der bewährten Verfahren

Allgemeine Leitlinien

  1. Beginnen Sie mit EF Core für neue Projekte, es sei denn, Sie haben spezifische Leistungsanforderungen
  2. Profil vor der Optimierung Angenommen, du brauchst Dapper, /raw SQL
  3. Hybride Ansätze verwenden Kombination von Stärken verschiedener Werkzeuge
  4. Datenzugriffslogik isoliert halten Das Projektarchivmuster hilft beim Umschalten von Implementierungen
  5. Verbindung Pooling verwenden Konfigurieren Sie richtig für Ihre Arbeitsbelastung
  6. Funktionen des Hebels PostgreSQL Abstract-Abstrakte leistungsfähige Datenbankfunktionen

Dapper-spezifische

  1. Parametrierte Abfragen verwenden SQL-Injektion zu verhindern
  2. Abfrageergebnis-Caching betrachten für teure, wiederholte Abfragen
  3. Multi---Mapping verwenden für Joins anstelle von mehreren runden --Trips
  4. Wiederverwendung von Verbindungen vom Anschlusspool
  5. Betrachten Sie Dapper-.-Contrib für einfache CRUD-Operationen

Leistungstipps

  1. Indexieren Sie Ihre Abfragen Analysieren Sie langsame Abfragen mit EXPLAIN ANALYZE
  2. Verwende vorbereitete Aussagen für wiederholte Abfragen
  3. Batch-Betrieb wenn möglich
  4. COPY verwenden für Masseneinsätze in PostgreSQL
  5. Überwachen Sie den Verbindungspool Konfigurieren Sie die Größe des Pools min-/.
  6. Betrachten Sie Lesenachbildungen für lesestarke Workloads

Schlußfolgerung

Mit PostgreSQL wählen Sie den richtigen Datenzugriffsansatz für Ihre Anwendung..-NET ist es, das richtige Tool zu finden, um Ihren spezifischen Bedürfnissen gerecht zu werden.

  • EF-Kern Siehe auch: Teil-)® zeichnet sich durch schnelle Entwicklung aus - , , Domänenmodellierung und Anwendungen, in denen Entwicklerproduktivität die Rohleistung trumpft
  • Dapper bietet einen ausgezeichneten Mittelweg mit nähe---optimaler Performance und vernünftiger Abstraktion
  • Rohe Npgsql liefert maximale Leistung und Kontrolle für den datenintensiven Betrieb
  • Mappen von Bibliotheken wie Mapster und AutoMapper reduzieren Boilerplate bei der Arbeit mit niedrigeren --Ebene Datenzugriff

In der Praxis verwenden die erfolgreichsten Anwendungen oft eine hybrider AnsatzDie Stärken jedes Werkzeugs ggf. nutzen

  • Verwendung EF-Kern für Ihre Domain-Logik und schreibt
  • Verwendung Dapper für komplexe Leseanfragen und Reporting
  • Verwendung Rohes Npgsql für Bulk Operations und Analytics

Der Schlüssel ist:

  1. Verstehen Sie Ihre Anforderungen Leistungsfähigkeit des Teams, Entwicklungsgeschwindigkeit, Teamfähigkeit
  2. Profil Ihrer Bewerbung Nicht angenommene Engpässe identifizieren
  3. Pragmatisch wählen Verwenden Sie das einfachste Werkzeug, das Ihren Bedürfnissen entspricht
  4. Flexibel bleiben Sie können Ansätze innerhalb der gleichen Anwendung mischen

Erinnern Sie sich an die vorzeitige Optimierung von : ist die Wurzel aller bösartigen ,, aber so ist der Aufbau eines Systems, das skalieren kann, wenn es benötigt wird scale .

Literaturhinweise und weitere Lektüre

Teil dieser Serie

Offizielle Dokumentation

Verwandte Artikel auf diesem Blog


Das schließt mit unserer zweiteiligen Serie über den Datenzugriff in . ab.!" Wir deckten alles ab, von EF-Core-''s leistungsstarken Abstraktionen bis hin zu Roh-SQL®''s maximaler Leistungsfähigkeit mit praktischer Anleitung zur Kombination von Ansätzen für optimale Ergebnisse.

Finding related posts...
logo

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