# Datenzugriff im Vergleich zu ORMs und Mapping-Strategien

<!-- category -- .NET, Dapper, PostgreSQL, Performance, Database -->
<datetime class="hidden">2025-12-03T15:00</datetime>

Willkommen bei Teil -2- unseres umfassenden Leitfadens für den Datenzugriff in [Teil](/blog/orm-mapping-comparison-part1)Wir 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](https://github.com/DapperLib/Dapper)**Die Mikro-- ORM Sweet Spot
- **Rohes ADO-.-NET-/[Npgsql](https://www.npgsql.org/)**Maximale Leistung und Kontrolle
- **Objektmapping-Bibliotheken**: [Kartenspieler](https://github.com/MapsterMapper/Mapster) vs. [Automapper](https://automapper.org/)
- **Hybride Ansätze**Kombination von EF-Kern und Dapper-Mustern aus (CQRS
- **Leistungsvergleiche**Vergleich der realen Werte der Welt
- **Entscheidungsmatrix**Wählen Sie das richtige Werkzeug für Ihr Szenario

## Inhaltsverzeichnis

## Dapper, das Mikro-- ORM

[Dapper](https://github.com/DapperLib/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 SQL**Sie schreiben alle SQL selbst
- **Hohe Leistung**Minimaler Overhead über ADO-.-NET
- **Multi---Mapping**Erfasse komplexe Abfragen an mehrere verwandte Typen
- **Parameterbearbeitung**Automatische Parametrierung verhindert SQL-Einspritzung
- **Transaktionsunterstützung**Volle Kontrolle über Transaktionen
- **Einfachheit**Keine Konfiguration, keine Änderung des Trackings, keine Magie
- **Async-/-Await**Vollständige async-Unterstützung für modernes

### Einfache Dapper-Beispiele

```csharp
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.

```csharp
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**

```csharp
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**

```csharp
// 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-:**

```csharp
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**

```csharp
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
- **Speichernutzung**Keine Änderung Tracking oder Proxies
- **Massenbetrieb**Ausgezeichnet mit PostgreSQL COPY
- **Erste Abfrage**Keine Kompilationsschritte
- **Produktivität des Entwicklers**Benötigt SQL-Wissen, aber sehr vorhersehbar

## Rohes ADO-.-NET mit Npgsql

Für absolute maximale Leistung und Kontrolle können Sie verwenden [Npgsql](https://www.npgsql.org/) 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

```csharp
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

- **Abfrageleistung**Ausgangslage -- schnellstmöglich
- **Speichernutzung**SAMMLUNG Niedrigste Kontrolle über die Zuweisungen
- **Massenbetrieb**Ausgezeichnet mit COPY-Protokoll
- **Produktivität des Entwicklers**Benö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](https://github.com/MapsterMapper/Mapster) ist ein Fast-,-Konventionsobjektmapper, der die Quellgenerierung für eine optimale Performance nutzt.

```csharp
// 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](https://automapper.org/) ist die beliebteste Mapping-Bibliothek, obwohl langsamer als Mapster.

```csharp
// 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.

```csharp
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 [Marten](https://martendb.io/)Sehen Sie meinen Artikel auf [Moderne CQRS und Event Sourcing](/blog/moderncqrsandeventsourcing).

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

```mermaid
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

```csharp
// 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

```csharp
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](/blog/orm-mapping-comparison-part1) 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](/blog/orm-mapping-comparison-part1)-)® 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 Ansatz**Die 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**

- [Zugriff auf Daten im Rahmen-Kernbereich der Einrichtung](/blog/orm-mapping-comparison-part1)

**Offizielle Dokumentation**

- [Dapper GitHub](https://github.com/DapperLib/Dapper)
- [Dapper Tutorial](https://dapper-tutorial.net/)
- [Npgsql Dokumentation](https://www.npgsql.org/doc/)
- [Npgsql ADO-.-NET-Anbieter](https://www.npgsql.org/doc/basic-usage.html)
- [Mapster GitHub](https://github.com/MapsterMapper/Mapster)
- [AutoMapper Dokumentation](https://docs.automapper.org/)
- [PostgreSQL-Dokumentation](https://www.postgresql.org/docs/)

**Verwandte Artikel auf diesem Blog**

- [Hinzufügen von Entity Framework für Blog-Posts](/blog/addingentityframeworkforblogpostspt1)
- [EF-Migrationen auf dem richtigen Weg](/blog/efmigrationstherightway)
- [Volltextsuche mit EF Core](/blog/textsearchingpt1)
- [Moderne CQRS und Event Sourcing](/blog/moderncqrsandeventsourcing)

---


*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.*