# Dataåtkomst i .NET: Jämföra ORM och kartläggningsstrategier (Del 2 - Dapper, Raw SQL, och Hybrid Approaches)

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

Välkommen till del 2 i vår omfattande guide till dataåtkomst i .NET! [Häfte 1](/blog/orm-mapping-comparison-part1), Vi utforskade Entity Framework Core på djupet, inklusive SQL generation, vanliga fallgropar, och den kritiska varning om proxies och caching.

I den här artikeln kommer vi att undersöka de lättare vikt alternativ och hur man kan kombinera flera metoder för optimal prestanda:

- **[Gräslök](https://github.com/DapperLib/Dapper)**: Mikro-ORM söt plats
- **Rå ADO.NET/[I enlighet med artikel 4 i förordning (EU) nr 1307/2013 ska kommissionen, i enlighet med artikel 5 i förordning (EU) nr 1307/2013, anta delegerade akter i enlighet med artikel 264 i förordning (EU) nr 1303/2013, för att se till att de åtgärder som föreskrivs i denna förordning är förenliga med yttrandet från ständiga kommittén för växter, djur, livsmedel och foder.](https://www.npgsql.org/)**: Maximal prestanda och kontroll
- **Objektkartläggning av bibliotek**: [Karta](https://github.com/MapsterMapper/Mapster) vs [Automatisk mapp](https://automapper.org/)
- **Hybridmetoder**: Kombinera EF Core och Dapper (CQRS-mönster)
- **Riktmärken för prestanda**: Real-world jämförelser
- **Beslutsmatris**: Välja rätt verktyg för ditt scenario

## Innehållsförteckning

## Mikro-ORMEN

[Gräslök](https://github.com/DapperLib/Dapper) är en lätt, högpresterande mikro-ORM skapad av Stack Overflow. Det ger ett tunt lager över ADO.NET, hantera det tråkiga arbetet med att kartlägga frågeresultat till objekt samtidigt som du får full SQL kontroll.

### Varför Dapper finns

Dapper föddes ur Stack Overflow behov av högpresterande dataåtkomst. Teamet fann att fullständiga ORM som Entity Framework (pre-Core) lagt till för mycket overhead för sina högtrafikscenarier. Dapper ger dig 95% av bekvämligheten med endast 5-15% overhead över rå ADO.NET.

### Nyckelfunktioner

- **Rå SQL**: Du skriver alla SQL själv - fullständig kontroll
- **Hög prestanda**: Minimal overhead över ADO.NET (5-15%)
- **Multimappning**: Karta komplexa frågor till flera relaterade typer
- **Parameterhantering**: Automatisk parameterisering förhindrar SQL-injektion
- **Stöd för transaktioner**: Fullständig kontroll över transaktioner
- **Enkelhet**: Ingen konfiguration, ingen ändringsspårning, ingen magi
- **Asynkronisering/vänta**: Fullt async stöd för modern .NET

### Grundläggande exempel

```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: Hantering går ihop

En av Dappers mest kraftfulla funktioner är multi-mapping - effektiv hantering går och mappning till flera relaterade objekt:

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

### Avancerade dappertekniker

**Dynamiska parametrar för komplexa frågor:**

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

**Anpassade typhanterare för PostgreSQL-typer:**

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

**Bulktransaktioner med 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();
}
```

**Transaktionsstöd:**

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

### När du ska använda Dapper

**Använd Dapper när:**

- Prestanda är avgörande men du behöver inte absolut maximal hastighet
- Du är bekväm med att skriva SQL
- Du har komplexa frågor som inte kartlägger väl till LINQ
- Du behöver fin kontroll över SQL-generationen
- Du arbetar med en befintlig databas schema
- Du vill ha minimala omkostnader och beroenden
- Minnesanvändning är ett bekymmer
- Du måste utnyttja databas-specifika funktioner i stor utsträckning

**❌ Undvik dapper när:**

- Ditt team är inte bekvämt med SQL
- Du behöver automatisk förändringsspårning
- Du vill ha schema migreringar som kod
- Du har ett snabbt föränderligt schema
- Du behöver korsdatabasportabilitet
- Du föredrar LINQ framför SQL syntax

### Prestandaegenskaper

- **Fråga resultat**: 5-15% overhead över rå ADO.NET
- **Minnesanvändning**: Minimal - ingen ändring spårning eller proxies
- **Bulktransaktioner**: Utmärkt med PostgreSQL Copy
- **Första frågestunden**: Snabb - ingen sammanställning steg
- **Utvecklarproduktivitet**: Kräver SQL kunskap men mycket förutsägbar

## Rå ADO.NET med Npgsql

För absolut maximal prestanda och kontroll, kan du använda [I enlighet med artikel 4 i förordning (EU) nr 1307/2013 ska kommissionen, i enlighet med artikel 5 i förordning (EU) nr 1307/2013, anta delegerade akter i enlighet med artikel 264 i förordning (EU) nr 1303/2013, för att se till att de åtgärder som föreskrivs i denna förordning är förenliga med yttrandet från ständiga kommittén för växter, djur, livsmedel och foder.](https://www.npgsql.org/) direkt utan något ORM-lager.

### När rå ADO.NET gör känsla

Rå ADO.NET är lämpligt när

- Du behöver varje millisekund av prestanda
- Du bygger dataprocessorer med hög genomströmning
- Du arbetar med PostgreSQL-specifika funktioner som inte stöds av ORMs
- Du behöver exakt kontroll över minnestilldelningar
- Du gör bulkoperationer eller strömmar stora datauppsättningar

### Exempel: Ren 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)
            };
        }
    }
}
```

### Prestandaegenskaper

- **Fråga resultat**: Baslinje - snabbast möjliga
- **Minnesanvändning**: Lägsta - fullständig kontroll över tilldelningar
- **Bulktransaktioner**: Utmärkt med Copy protokoll
- **Utvecklarproduktivitet**: Kräver mest manuellt arbete

## Objektkartläggning av bibliotek

När du arbetar med Dapper eller rå ADO.NET behöver du ofta kartlägga mellan olika objekt representationer (DTOs, enheter, visa modeller). Flera bibliotek kan automatisera detta.

### Karta: Högpresterande karta

[Karta](https://github.com/MapsterMapper/Mapster) är en snabb, konventionsbaserad objektmappare som använder källgenerering för optimal prestanda.

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

### AutoMapper: Mogen Convention-baserad Mapper

[Automatisk mapp](https://automapper.org/) är det mest populära kartbiblioteket, men långsammare än 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());
    }
}
```

### Manuell kartläggning: Full kontroll

Ibland är den bästa metoden explicit manuell kartläggning:

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

### Kartläggning av prestandajämförelse

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

**Nyckelutflykter:**

- Manuell kartlä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 tak

## Hybridmetoder: Det bästa av båda världarna

I verkliga applikationer vill du ofta använda olika tillvägagångssätt för olika scenarier inom samma applikation. **rekommenderat tillvägagångssätt** för de flesta produktionssystem.

### CQRS-mönster: Att skilja läs- och skrivböcker åt

Mönstret för CQRS (Command Query Responsibility Segregation) är en naturlig passform för inflygningar för hybriddataåtkomst. [Marten Ordförande](https://martendb.io/), se min artikel om [Modern CQRS och Händelseförslitning](/blog/moderncqrsandeventsourcing).

**Hur Marten relaterar till denna diskussion:**

Marten är en dokumentdatabas och eventbutik byggd på PostgreSQL som tar hybriddataåtkomst till en annan nivå. Den kombinerar:

- **Händelseinköp** för skriver (immutable händelseströmmar)
- **Prognoser** för läsningar (materialiserade vyer optimerade för frågor)
- **PostgreSQL: s JSONB** för dokumentlagring
- Inbyggda CQRS-mönster

Medan denna artikel fokuserar på traditionell relationell dataåtkomst (EF Core, Dapper), Marten visar hur du kan utnyttja PostgreSQL avancerade funktioner (JSONB, händelseströmmar) för att genomföra sofistikerade arkitekturer. Principerna är desamma:

- Separata skrivmodeller (optimerade för transaktioner och konsekvens)
- Separata läsmodeller (optimerade för frågor och prestanda)
- Använd rätt verktyg för varje jobb

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

Detta mönster har en hävstångseffekt:

- **EF-kärna** för skriver: Ändra spårning, validering, affärsregler
- **Gräslök** för avläsningar: Maximal frågeprestanda och flexibilitet
- Samma databas, olika åtkomstmönster optimerade för varje användningsfall

### Genomförande: CQRS med EF Core och 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 Core med tillfällig rå SQL

För tillämpningar som i första hand är EF Core men som kräver tillfällig prestandaoptimering:

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

## Resultatjämförelse

Låt oss titta på verkliga prestanda riktmärken för gemensamma operationer med PostgreSQL:

### Riktmärke: Läser 1000 Records

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

### Riktmärke: Lägga till 1000 poster

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

### Riktmärke: Komplex anslutning 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
```

### Nyckeluttagningar

1. **Rå Npgsql är snabbast** men kräver mest kod
2. **Dapper erbjuder utmärkt prestanda** med minimal abstraktionskostnad (60-70 % snabbare än EF Core)
3. **EF Core no-tracking-frågor** är rimliga för de flesta scenarier
4. **Bulktransaktioner** visa de största prestandaluckorna (10-13x skillnad!)
5. **Minnestilldelning** följa ett liknande mönster som exekveringstiden

## Beslutsmatris: Vilken metod att använda

### Använd EF Core när:

- på att bygga en ny tillämpning med föränderliga krav
- på domändriven design med rika enhetsmodeller
- Du behöver migrering och schemahantering
- Team är bekvämare med C# än SQL
- på läs- och skrivförhållandet är balanserat
- på 20-50 % av den optimala prestandan är acceptabel
- Du vill ändra spårning och enhet för arbetsmönster

Se också [Häfte 1](/blog/orm-mapping-comparison-part1) för omfattande EF Core-vägledning.

### Använd Dapper när:

- på prestanda är viktigt men inte kritiskt
- Du har komplexa frågor som inte kartlägger väl till LINQ
- Du är bekväm med att skriva SQL
- Du behöver fin kontroll över SQL-generationen
- på att arbeta med befintliga databasscheman
- på lästunga arbetsbelastningar med enkla skrivningar
- Du vill ha minimal abstraction overhead

### Använd rå Npgsql när:

- och maximal prestanda är avgörande
- på att bygga dataprocessorer med hög genomströmning
- och arbetar i stor utsträckning med PostgreSQL-specifika funktioner
- tillverkningssatser och import av bulkvaror
- Varje millisekund och megabyte betyder något
- Du behöver absolut kontroll

### Hybridmetod när:

- på olika delar av applikationen har olika behov
- på CQRS-mönster (EF Core för texter, Dapper för läsning)
- De flesta frågor använder EF Core, men några behöver rå SQL
- Du vill ha en gradvis migration mellan olika tillvägagångssätt
- på stora, komplexa applikationer med varierande krav

## Sammanfattning av bästa praxis

### Allmänna riktlinjer

1. **Börja med EF Core** för nya projekt om du inte har specifika prestandakrav
2. **Profil innan optimering** - anta inte att du behöver Dapper/Raw SQL
3. **Använd hybridmetoder** - kombinera styrkor av olika verktyg
4. **Håll dataåtkomstlogiken isolerad** - arkivmönster hjälper till att byta implementationer
5. **Använd anslutningspoolning** - konfigurera ordentligt för din arbetsbelastning
6. **Dragkraft PostgreSQL-funktioner** - inte absorbera bort kraftfulla databaskapaciteter

### Specificerad för dapper

1. **Använd parameteriserade frågor** för att förhindra SQL- injektion
2. **Överväg frågeresultat caching** för dyra, upprepade frågor
3. **Använd multi-mappning** för skarvar i stället för flera tur och returer
4. **Återanvändningsanslutningar** från anslutningspoolen
5. **Tänk till exempel på Dapper. Contrib** För enkla CRUD-transaktioner

### Prestandatips

1. **Indexera dina frågor** - analysera långsamma frågor med EXPLAIN ANALYZE
2. **Använd förberedda uttalanden** för upprepade frågor
3. **Batchtransaktioner** om möjligt
4. **Använd Copy** för bulkinlägg i PostgreSQL
5. **Övervakningspool för anslutning** - konfigurera min/max pool storlek
6. **Överväg att läsa repliker** för lästunga arbetsbelastningar

## Slutsatser

Att välja rätt dataåtkomst för din .NET-applikation med PostgreSQL handlar inte om att hitta det "bästa" verktyget - det handlar om att matcha rätt verktyg till dina specifika behov:

- **EF-kärna** (se [Häfte 1](/blog/orm-mapping-comparison-part1)) utmärker sig vid snabb utveckling, domänmodellering och tillämpningar där utvecklarens produktivitet överträffar rå prestanda
- **Gräslök** ger en utmärkt medelväg med nära optimal prestanda och rimlig abstraktion
- **Rå Npgsql** ger maximal prestanda och kontroll för dataintensiv verksamhet
- **Kartläggning av bibliotek** som Mapster och AutoMapper minska pannplatta vid arbete med lägre nivå dataåtkomst

I praktiken använder de mest framgångsrika tillämpningarna ofta en **hybridmetoden**, där så är lämpligt, utnyttja varje verktygs styrka

- Användning **EF-kärna** för din domänlogik och skriver
- Användning **Gräslök** för komplexa läsfrågor och rapportering
- Användning **rå Npgsql** för bulkverksamhet och analys

Nyckeln är att:

1. **Förstå dina krav** - prestanda, utvecklingshastighet, teamfärdigheter
2. **Profilera din ansökan** - identifiera verkliga flaskhalsar, inte förmodade flaskhalsar
3. **Välj pragmatiskt** - använd det enklaste verktyget som uppfyller dina behov
4. **Fortsätt att vara flexibel** - du kan blanda infallsvinklar inom samma applikation

Kom ihåg: tidig optimering är roten till allt ont, men så är att bygga ett system som inte kan skala när det behövs. Starta enkelt, mäta prestanda, och optimera där det spelar någon roll.

## Referenser och ytterligare läsning

**Del 1 i denna serie:**

- [Tillgång till uppgifter i .NET Part 1: Enhetsramar](/blog/orm-mapping-comparison-part1)

**Officiell dokumentation:**

- [Dapper GitHub Ordförande](https://github.com/DapperLib/Dapper)
- [Handledning till dapper](https://dapper-tutorial.net/)
- [Npgsql-dokumentation](https://www.npgsql.org/doc/)
- [Npgsql ADO.NET Leverantör](https://www.npgsql.org/doc/basic-usage.html)
- [Karta GitHub](https://github.com/MapsterMapper/Mapster)
- [Automatisk mappdokumentation](https://docs.automapper.org/)
- [PostgreSQL-dokumentation](https://www.postgresql.org/docs/)

**Relaterade artiklar på denna blogg:**

- [Lägga till entitetsramverk för blogginlägg](/blog/addingentityframeworkforblogpostspt1)
- [EF Migrationer på rätt sätt](/blog/efmigrationstherightway)
- [Fullständig textsökning med EF Core](/blog/textsearchingpt1)
- [Modern CQRS och Händelseförslitning](/blog/moderncqrsandeventsourcing)

---


*Det avslutar vår två-delars serie om dataåtkomst i .NET! Vi har täckt allt från EF Core kraftfulla abstraktioner till rå SQL: s maximala prestanda, med praktisk vägledning om att kombinera strategier för optimala resultat.*