欢迎使用 .NET! in 第一部分 第一部分我们深入探索了实体框架核心, 包括SQL一代、共同的陷阱,
在本条中,我们将探索较轻重量的替代办法, 以及如何结合多种方法实现最佳性能:
顶顶顶端 是一个由Stack 溢出创建的轻量、高性能微ORM。它为 ADO.NET 提供一层薄层,用于处理对对象进行查询结果绘图的烦琐工作,同时给予您完整的 SQL 控制。
Dapper是来自Stack Overflow对高性能数据访问的需要。团队发现,像实体框架(Cre前)这样的完整的ORMs为高流量的情景增加了过多的管理费。 Dapper给了你95%的方便,在原始 ADO.NET 上只有 5-15%的管理费。
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 });
}
}
Dapper最强大的特征之一是多图绘制-高效处理连接和测绘多个相关物体:
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;
}
复杂查询的动态参数 :
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);
}
PostgreSQL 类型自定义类型手动器 :
// Handle PostgreSQL arrays
public class PostgresArrayTypeHandler : SqlMapper.TypeHandler<string[]>
{
public override void SetValue(IDbDataParameter parameter, string[] value)
{
parameter.Value = value;
((NpgsqlParameter)parameter).NpgsqlDbType = NpgsqlDbType.Array | NpgsqlDbType.Text;
}
public override string[] Parse(object value)
{
return (string[])value;
}
}
// Handle PostgreSQL JSONB
public class JsonTypeHandler<T> : SqlMapper.TypeHandler<T>
{
public override void SetValue(IDbDataParameter parameter, T value)
{
parameter.Value = JsonSerializer.Serialize(value);
((NpgsqlParameter)parameter).NpgsqlDbType = NpgsqlDbType.Jsonb;
}
public override T Parse(object value)
{
return JsonSerializer.Deserialize<T>(value.ToString());
}
}
// Register handlers (in startup)
SqlMapper.AddTypeHandler(new PostgresArrayTypeHandler());
SqlMapper.AddTypeHandler(new JsonTypeHandler<Dictionary<string, object>>());
使用 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();
}
交易支持:
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;
}
}
使用 dapper 时 :
在下列情况下避免达珀:
绝对最大性能和控制最大性能和控制,您可以使用 Npgsql Npgsql 直接无任何ORM层。
Raw ADO. NET 适合当:
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)
};
}
}
}
当与 Dapper 或 原始 ADO.NET 合作时, 您通常需要绘制不同对象表达式( DTOs、 实体、 查看模型) 之间的地图 。 多个图书馆可以实现这个自动化 。
地图地图师 是一个快速的、基于公约的物体绘图仪,利用源生成来优化性能。
// Install: Mapster and Mapster.Tool
using Mapster;
public class BlogPostDto
{
public int Id { get; set; }
public string Title { get; set; }
public string Summary { get; set; }
public List<string> CategoryNames { get; set; }
}
public class BlogPost
{
public int Id { get; set; }
public string Title { get; set; }
public string Content { get; set; }
public List<Category> Categories { get; set; }
}
// Configuration
public class MappingConfig : IRegister
{
public void Register(TypeAdapterConfig config)
{
config.NewConfig<BlogPost, BlogPostDto>()
.Map(dest => dest.Summary, src => src.Content.Substring(0, Math.Min(200, src.Content.Length)))
.Map(dest => dest.CategoryNames, src => src.Categories.Select(c => c.Name).ToList());
// Reverse map with ignore
config.NewConfig<BlogPostDto, BlogPost>()
.Ignore(dest => dest.Content);
}
}
// Registration in Program.cs
TypeAdapterConfig.GlobalSettings.Scan(Assembly.GetExecutingAssembly());
// Usage with Dapper
public class BlogService
{
private readonly string _connectionString;
public async Task<List<BlogPostDto>> GetPostsAsync()
{
using var connection = new NpgsqlConnection(_connectionString);
var posts = await connection.QueryAsync<BlogPost>(@"
SELECT p.id, p.title, p.content
FROM blog_posts p
");
// Map to DTOs - very fast with Mapster
return posts.Adapt<List<BlogPostDto>>();
}
// Projection mapping (compile-time)
public async Task<List<BlogPostDto>> GetPostsDtosDirectlyAsync()
{
using var connection = new NpgsqlConnection(_connectionString);
// Query directly to DTO shape
return (await connection.QueryAsync<BlogPostDto>(@"
SELECT
id,
title,
SUBSTRING(content, 1, 200) as summary
FROM blog_posts
")).ToList();
}
}
自动 Mapper 自动管理器 地图图书馆是最受欢迎的地图图书馆,尽管比《地图》要慢。
// 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());
}
}
有时,最好的办法是明确人工绘图:
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();
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
密钥外卖 :
在实际应用中,您往往想要对同一应用中的不同情景使用不同的方法。这是 建议采用的方法 对于大多数生产系统来说都是如此。
CQRS(Comman和查询责任隔离)模式自然适合混合数据获取方法。 马尔坦,见我的文章 现代CQRS CQRS 和事件观察.
Marten与这次讨论有何关联:
Marten是一个基于 PostgreSQL 的文件数据库和事件存储库, 将混合数据存取到另一个级别。 它合并了 :
虽然本条侧重于传统关系数据存取(EF Core, Dapper),但Marten展示了如何利用PostgreSQL的先进功能(JSONB,事件流)来实施复杂的结构。
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
此模式的杠杆作用 :
// 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核心但需要偶尔优化性能的应用:
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;
}
}
让我们看看现实世界与PostgreSQL共同行动的业绩基准:
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
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
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
见见 第一部分 第一部分 全面的核心指导。
在 PostgreSQL 的.NET 应用程序中选择正确的数据访问方法, 不是为了找到“ 最佳” 工具,
在实践中,最成功的应用程序通常使用 混合办法酌情利用每种工具的优势:
关键是:
记住: 过早优化是所有邪恶的根源, 但建设一个无法在需要时缩放的系统也是如此。 开始简单、 测量业绩, 并优化其重要性所在 。
本系列第一部分:
正式文件:
本博客相关文章:
我们从EF核心的强力抽象到原始SQL的最大性能, 都包括了所有内容, 并附有关于整合最佳结果方法的实用指导。
© 2026 Scott Galloway — Unlicense — All content and source code on this site is free to use, copy, modify, and sell.