From 67f3389288207e5029a8d344c2f65efdaa0f8960 Mon Sep 17 00:00:00 2001 From: Vitalii Lakatosh Date: Wed, 18 Feb 2026 00:39:01 +0200 Subject: [PATCH 01/11] Add DapperSqlGenerator helper --- .gitignore | 1 + .../Extensions/DapperSqlGenerator.cs | 52 +++++++++++++++++++ 2 files changed, 53 insertions(+) create mode 100644 BlogWebApp/Data/Blog.Data/Extensions/DapperSqlGenerator.cs diff --git a/.gitignore b/.gitignore index bd598afb..8b5408e2 100644 --- a/.gitignore +++ b/.gitignore @@ -118,3 +118,4 @@ /BlogWebApp/Tests/Blog.ServicesTests/bin/Debug/net10.0 /BlogWebApp/Tests/Blog.UnitTests/bin/Debug/net10.0 /Sdk/Blog.Sdk.ExampleUsage/bin/Debug/net10.0 +/BlogWebApp/Aspire/Blog.Aspire/Blog.Aspire.AppHost/bin/Debug/net10.0 diff --git a/BlogWebApp/Data/Blog.Data/Extensions/DapperSqlGenerator.cs b/BlogWebApp/Data/Blog.Data/Extensions/DapperSqlGenerator.cs new file mode 100644 index 00000000..97606143 --- /dev/null +++ b/BlogWebApp/Data/Blog.Data/Extensions/DapperSqlGenerator.cs @@ -0,0 +1,52 @@ +// +// Copyright (c) Blog. All rights reserved. +// + +using System.Linq; + +namespace Blog.Data.Extensions; + +/// +/// Dapper Sql generator. +/// s +public static class DapperSqlGenerator +{ + /// + /// Generate insert. + /// + /// The entity. + /// The table. + /// The Sql string. + /// The params. + public static (string Sql, object Params) GenerateInsert(T entity, string table) + { + var props = typeof(T).GetProperties() + .Where(p => p.Name != "Id"); + + var columns = string.Join(",", props.Select(p => p.Name)); + var values = string.Join(",", props.Select(p => "@" + p.Name)); + + var sql = $"INSERT INTO {table} ({columns}) VALUES ({values})"; + + return (sql, entity); + } + + /// + /// Generate update. + /// + /// The entity. + /// The table. + /// The Sql string. + /// The params. + public static (string Sql, object Params) GenerateUpdate(T entity, string table) + { + var props = typeof(T).GetProperties() + .Where(p => p.Name != "Id"); + + var setClause = string.Join(",", props.Select(p => $"{p.Name} = @{p.Name}")); + + var sql = $"UPDATE {table} SET {setClause} WHERE Id = @Id"; + + return (sql, entity); + } +} \ No newline at end of file From ae26bbb3829dd5f992487aab877cb3a9fc97b243 Mon Sep 17 00:00:00 2001 From: Vitalii Lakatosh Date: Wed, 18 Feb 2026 00:39:56 +0200 Subject: [PATCH 02/11] Create dapper repository --- BlogWebApp/Data/Blog.Data/Blog.Data.csproj | 1 + BlogWebApp/Data/Blog.Data/DapperRepository.cs | 92 +++++++++++++++++++ .../Blog.Data/Repository/IDapperRepository.cs | 67 ++++++++++++++ .../Pagination/DapperSearchQuery.cs | 33 +++++++ 4 files changed, 193 insertions(+) create mode 100644 BlogWebApp/Data/Blog.Data/DapperRepository.cs create mode 100644 BlogWebApp/Data/Blog.Data/Repository/IDapperRepository.cs create mode 100644 Shared/Blog.Core/Infrastructure/Pagination/DapperSearchQuery.cs diff --git a/BlogWebApp/Data/Blog.Data/Blog.Data.csproj b/BlogWebApp/Data/Blog.Data/Blog.Data.csproj index 421a66c5..466c1f6d 100644 --- a/BlogWebApp/Data/Blog.Data/Blog.Data.csproj +++ b/BlogWebApp/Data/Blog.Data/Blog.Data.csproj @@ -16,6 +16,7 @@ + diff --git a/BlogWebApp/Data/Blog.Data/DapperRepository.cs b/BlogWebApp/Data/Blog.Data/DapperRepository.cs new file mode 100644 index 00000000..dd2a27b4 --- /dev/null +++ b/BlogWebApp/Data/Blog.Data/DapperRepository.cs @@ -0,0 +1,92 @@ +// +// Copyright (c) Blog. All rights reserved. +// + +using Blog.Core; +using Blog.Core.Infrastructure.Pagination; +using Blog.Data.Extensions; +using Blog.Data.Repository; +using Dapper; +using System.Collections.Generic; +using System.Data; +using System.Linq; +using System.Threading.Tasks; + +namespace Blog.Data; + +/// +/// Dapper repository. +/// +/// IEntity. +public class DapperRepository(IDbConnection connection) + : IDapperRepository + where T : class, IEntity +{ + /// + /// The table name. + /// + private readonly string _tableName = typeof(T).Name; + + /// + public async Task> GetAllAsync() + => await connection.QueryAsync($"SELECT * FROM {_tableName}"); + + /// + public async Task GetByIdAsync(object id) + => await connection.QueryFirstOrDefaultAsync( + $"SELECT * FROM {_tableName} WHERE Id = @Id", new { Id = id }); + + /// + public async Task InsertAsync(T entity) + { + var insertQuery = DapperSqlGenerator.GenerateInsert(entity, _tableName); + + return await connection.ExecuteAsync(insertQuery.Sql, insertQuery.Params); + } + + /// + public async Task UpdateAsync(T entity) + { + var updateQuery = DapperSqlGenerator.GenerateUpdate(entity, _tableName); + + return await connection.ExecuteAsync(updateQuery.Sql, updateQuery.Params); + } + + /// + public async Task DeleteAsync(object id) + => await connection.ExecuteAsync( + $"DELETE FROM {_tableName} WHERE Id = @Id", new { Id = id }); + + /// + public async Task AnyAsync(string whereClause, object? param = null) + => await connection.ExecuteScalarAsync( + $"SELECT COUNT(1) FROM {_tableName} WHERE {whereClause}", param) > 0; + + /// + public async Task> SearchAsync(DapperSearchQuery query) + { + var where = string.IsNullOrWhiteSpace(query.WhereClause) ? string.Empty : $"WHERE {query.WhereClause}"; + var order = string.IsNullOrWhiteSpace(query.OrderBy) ? string.Empty : $"ORDER BY {query.OrderBy}"; + + var sqlCount = $"SELECT COUNT(1) FROM {_tableName} {where}"; + var total = await connection.ExecuteScalarAsync(sqlCount, query.Parameters); + + var sql = $""" + SELECT * FROM {_tableName} + {where} + {order} + OFFSET @Skip ROWS FETCH NEXT @Take ROWS ONLY + """; + + var data = await connection.QueryAsync(sql, + new { query.Skip, query.Take, query.Parameters }); + + return new PagedListResult + { + Entities = data.ToList(), + Count = total, + HasNext = query.Skip + query.Take < total, + HasPrevious = query.Skip > 0 + }; + } +} \ No newline at end of file diff --git a/BlogWebApp/Data/Blog.Data/Repository/IDapperRepository.cs b/BlogWebApp/Data/Blog.Data/Repository/IDapperRepository.cs new file mode 100644 index 00000000..a0db2ce2 --- /dev/null +++ b/BlogWebApp/Data/Blog.Data/Repository/IDapperRepository.cs @@ -0,0 +1,67 @@ +// +// Copyright (c) PlaceholderCompany. All rights reserved. +// + +using System.Collections.Generic; +using System.Threading.Tasks; +using Blog.Core; +using Blog.Core.Infrastructure.Pagination; + +namespace Blog.Data.Repository; + +/// +/// Dapper repository interface. +/// +/// Type. +public interface IDapperRepository + where T : IEntity +{ + /// + /// Gets all asynchronous. + /// + /// Type. + Task> GetAllAsync(); + + /// + /// Async get item by id async. + /// + /// id. + /// Task. + Task GetByIdAsync(object id); + + /// + /// Inserts the asynchronous. + /// + /// The entity. + /// A representing the asynchronous operation. + Task InsertAsync(T entity); + + /// + /// Updates the asynchronous. + /// + /// The entity. + /// A representing the asynchronous operation. + Task UpdateAsync(T entity); + + /// + /// Deletes the asynchronous. + /// + /// The identifier. + /// A representing the asynchronous operation. + Task DeleteAsync(object id); + + /// + /// Asynchronous check on any. + /// + /// The where clause. + /// The parameter. + /// Task. + Task AnyAsync(string whereClause, object? param = null); + + /// + /// Async search. + /// + /// The query. + /// Task. + Task> SearchAsync(DapperSearchQuery query); +} \ No newline at end of file diff --git a/Shared/Blog.Core/Infrastructure/Pagination/DapperSearchQuery.cs b/Shared/Blog.Core/Infrastructure/Pagination/DapperSearchQuery.cs new file mode 100644 index 00000000..7c531fce --- /dev/null +++ b/Shared/Blog.Core/Infrastructure/Pagination/DapperSearchQuery.cs @@ -0,0 +1,33 @@ +// +// Copyright (c) PlaceholderCompany. All rights reserved. +// + +namespace Blog.Core.Infrastructure.Pagination; + +public class DapperSearchQuery +{ + /// + /// Gets or sets the message + /// + public string? WhereClause { get; set; } + + /// + /// Gets or sets the message. + /// + public object? Parameters { get; set; } + + /// + /// Gets or sets order by. + /// + public string? OrderBy { get; set; } + + /// + /// Gets or sets skip. + /// + public int Skip { get; set; } + + /// + /// Gets or sets take. + /// + public int Take { get; set; } +} \ No newline at end of file From 38127bf41c5e9f40cc9c0f607d0c63621a952b29 Mon Sep 17 00:00:00 2001 From: Vitalii Lakatosh Date: Wed, 18 Feb 2026 01:07:31 +0200 Subject: [PATCH 03/11] Add general dapper service --- .../Interfaces/ICommentsDapperService.cs | 43 +++++++++++ .../GeneralService/GeneralDapperService.cs | 59 +++++++++++++++ .../GeneralService/IGeneralDapperService.cs | 74 +++++++++++++++++++ 3 files changed, 176 insertions(+) create mode 100644 BlogWebApp/Services/Blog.Services/DapperServices/Interfaces/ICommentsDapperService.cs create mode 100644 BlogWebApp/Services/Blog.Services/GeneralService/GeneralDapperService.cs create mode 100644 BlogWebApp/Services/Blog.Services/GeneralService/IGeneralDapperService.cs diff --git a/BlogWebApp/Services/Blog.Services/DapperServices/Interfaces/ICommentsDapperService.cs b/BlogWebApp/Services/Blog.Services/DapperServices/Interfaces/ICommentsDapperService.cs new file mode 100644 index 00000000..a41fd92b --- /dev/null +++ b/BlogWebApp/Services/Blog.Services/DapperServices/Interfaces/ICommentsDapperService.cs @@ -0,0 +1,43 @@ +using Blog.Contracts.V1.Responses.Chart; +using Blog.EntityServices.GeneralService; +using Blog.Services.Core.Dtos; +using Blog.Services.Core.Dtos.Posts; +using System.Threading.Tasks; +using Blog.Data.Models; + +namespace Blog.EntityServices.DapperServices.Interfaces; + +/// +/// Comments service interface. +/// +/// +public interface ICommentsDapperService : IGeneralDapperService +{ + /// + /// Gets the paged comments by post identifier. + /// + /// The post identifier. + /// The sort parameters. + /// A representing the asynchronous operation. + Task GetPagedCommentsByPostId(int postId, SortParametersDto sortParameters); + + /// + /// Gets the comment asynchronous. + /// + /// The identifier. + /// A The representing the asynchronous operation. + Task GetCommentAsync(int id); + + /// + /// Gets the paged comments. + /// + /// The sort parameters. + /// A The representing the asynchronous operation. + Task GetPagedComments(SortParametersDto sortParameters); + + /// + /// Asynchronous Get comments activity. + /// + /// Task. + Task GetCommentsActivity(); +} \ No newline at end of file diff --git a/BlogWebApp/Services/Blog.Services/GeneralService/GeneralDapperService.cs b/BlogWebApp/Services/Blog.Services/GeneralService/GeneralDapperService.cs new file mode 100644 index 00000000..b6a5ed5d --- /dev/null +++ b/BlogWebApp/Services/Blog.Services/GeneralService/GeneralDapperService.cs @@ -0,0 +1,59 @@ +// +// Copyright (c) PlaceholderCompany. All rights reserved. +// + +using Blog.Core; +using Blog.Core.Infrastructure.Pagination; +using Blog.Data.Repository; +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace Blog.EntityServices.GeneralService; + +/// +/// General dapper service. +/// +/// Type. +/// +/// Initializes a new instance of the class. +/// +/// repository. +public class GeneralDapperService(IDapperRepository repository) + : IGeneralDapperService + where T : class, IEntity +{ + /// + public async Task FindAsync(object id) + => await repository.GetByIdAsync(id); + + /// + public async Task InsertAsync(T entity) + => await repository.InsertAsync(entity); + + /// + public async Task InsertAsync(IEnumerable entities) + { + foreach (var entity in entities) + await repository.InsertAsync(entity); + } + + /// + public async Task UpdateAsync(T entity) + => await repository.UpdateAsync(entity); + + /// + public async Task DeleteAsync(object id) + => await repository.DeleteAsync(id); + + /// + public async Task> GetAllAsync() + => await repository.GetAllAsync(); + + /// + public async Task AnyAsync(string whereClause, object? param = null) + => await repository.AnyAsync(whereClause, param); + + /// + public async Task> SearchAsync(DapperSearchQuery query) + => await repository.SearchAsync(query); +} \ No newline at end of file diff --git a/BlogWebApp/Services/Blog.Services/GeneralService/IGeneralDapperService.cs b/BlogWebApp/Services/Blog.Services/GeneralService/IGeneralDapperService.cs new file mode 100644 index 00000000..5dae3fd1 --- /dev/null +++ b/BlogWebApp/Services/Blog.Services/GeneralService/IGeneralDapperService.cs @@ -0,0 +1,74 @@ +// +// Copyright (c) PlaceholderCompany. All rights reserved. +// + +using Blog.Core; +using Blog.Core.Infrastructure.Pagination; +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace Blog.EntityServices.GeneralService; + +/// +/// General dappers service interface. +/// +/// Type. +public interface IGeneralDapperService + where T : IEntity +{ + /// + /// Async find item by id. + /// + /// id. + /// Task. + Task FindAsync(object id); + + /// + /// Inserts the asynchronous. + /// + /// The enumerable. + /// A representing the asynchronous operation. + Task InsertAsync(T entity); + + /// + /// Inserts the asynchronous. + /// + /// The enumerable. + /// A representing the asynchronous operation. + Task InsertAsync(IEnumerable entities); + + /// + /// Update item. + /// + /// entity. + /// A representing the asynchronous operation. + Task UpdateAsync(T entity); + + /// + /// Delete item. + /// + /// entity. + /// A representing the asynchronous operation. + Task DeleteAsync(object id); + + /// + /// Gets all asynchronous. + /// + /// Task. + Task> GetAllAsync(); + + /// + /// Asynchronous check on any by specification. + /// + /// The where clause. + /// The param. + /// Task. + Task AnyAsync(string whereClause, object? param = null); + + /// + /// Async search. + /// + /// The query. + /// Task. + Task> SearchAsync(DapperSearchQuery query); +} \ No newline at end of file From b65b1b1ba54479dcb294850827b7a1351f2a5605 Mon Sep 17 00:00:00 2001 From: Vitalii Lakatosh Date: Wed, 18 Feb 2026 01:08:07 +0200 Subject: [PATCH 04/11] Create Comments dapper service --- .../DapperServices/CommentsDapperService.cs | 178 ++++++++++++++++++ .../Interfaces/ICommentsDapperService.cs | 6 +- 2 files changed, 183 insertions(+), 1 deletion(-) create mode 100644 BlogWebApp/Services/Blog.Services/DapperServices/CommentsDapperService.cs diff --git a/BlogWebApp/Services/Blog.Services/DapperServices/CommentsDapperService.cs b/BlogWebApp/Services/Blog.Services/DapperServices/CommentsDapperService.cs new file mode 100644 index 00000000..c4dd3fda --- /dev/null +++ b/BlogWebApp/Services/Blog.Services/DapperServices/CommentsDapperService.cs @@ -0,0 +1,178 @@ +// +// Copyright (c) PlaceholderCompany. All rights reserved. +// + +using System; +using Blog.Contracts.V1.Responses.Chart; +using Blog.Core.Helpers; +using Blog.Data.Models; +using Blog.Data.Repository; +using Blog.EntityServices.DapperServices.Interfaces; +using Blog.EntityServices.GeneralService; +using Blog.Services.Core.Dtos; +using Blog.Services.Core.Dtos.Posts; +using Dapper; +using System.Data; +using System.Linq; +using System.Threading.Tasks; + +namespace Blog.EntityServices.DapperServices; + +/// +/// Comments dapper service. +/// +/// +/// +/// +/// Initializes a new instance of the class. +/// +/// The repo. +/// The connection. +public class CommentsDapperService(IDapperRepository repo, IDbConnection connection) + : GeneralDapperService(repo), ICommentsDapperService +{ + /// + public async Task GetPagedCommentsByPostId(int postId, SortParametersDto sortParameters) + { + var param = new DynamicParameters(); + param.Add("@PostId", postId); + + const string countSql = "SELECT COUNT(*) FROM Comments WHERE PostId = @PostId"; + var total = await connection.ExecuteScalarAsync(countSql, param); + + const string sql = """ + SELECT c.*, u.Id, u.Email, u.FirstName, u.LastName, u.PhoneNumber + FROM Comments c + LEFT JOIN AspNetUsers u ON u.Id = c.UserId + WHERE c.PostId = @PostId + ORDER BY c.CreatedAt DESC + OFFSET @Skip ROWS FETCH NEXT @Take ROWS ONLY + """; + + param.Add("@Skip", (sortParameters.CurrentPage.Value - 1) * sortParameters.PageSize.Value); + param.Add("@Take", sortParameters.PageSize.Value); + + var comments = await connection.QueryAsync( + sql, + (c, u) => + { + c.User = u ?? new ApplicationUser(); + return c; + }, + param, + splitOn: "Id"); + + return new CommentsViewDto + { + Comments = comments.ToList(), + PageInfo = new PageInfo + { + PageNumber = sortParameters.CurrentPage.Value, + PageSize = sortParameters.PageSize.Value, + TotalItems = total + } + }; + } + + /// + public async Task GetCommentAsync(int id) + { + const string sql = """ + SELECT c.*, u.Id, u.Email, u.FirstName, u.LastName, u.PhoneNumber + FROM Comments c + LEFT JOIN AspNetUsers u ON u.Id = c.UserId + WHERE c.Id = @Id + """; + + return (await connection.QueryAsync( + sql, + (c, u) => + { + c.User = u; + return c; + }, + new { Id = id }, + splitOn: "Id")).FirstOrDefault(); + } + + /// + public async Task GetPagedComments(SortParametersDto sortParameters) + { + const string countSql = "SELECT COUNT(*) FROM Comments"; + var total = await connection.ExecuteScalarAsync(countSql); + + if (sortParameters.CurrentPage == null || sortParameters.PageSize == null) + { + var all = await connection.QueryAsync( + """ + SELECT c.*, u.Id, u.Email, u.FirstName, u.LastName, u.PhoneNumber + FROM Comments c + LEFT JOIN AspNetUsers u ON u.Id = c.UserId + """, + (c, u) => + { + c.User = u; + return c; + }, + splitOn: "Id"); + + return new CommentsViewDto { Comments = all.ToList() }; + } + + const string sql = """ + SELECT c.*, u.Id, u.Email, u.FirstName, u.LastName, u.PhoneNumber + FROM Comments c + LEFT JOIN AspNetUsers u ON u.Id = c.UserId + ORDER BY c.CreatedAt DESC + OFFSET @Skip ROWS FETCH NEXT @Take ROWS ONLY + """; + + var comments = await connection.QueryAsync( + sql, + (c, u) => + { + c.User = u; + return c; + }, + new + { + Skip = (sortParameters.CurrentPage.Value - 1) * sortParameters.PageSize.Value, + Take = sortParameters.PageSize.Value + }, + splitOn: "Id"); + + return new CommentsViewDto + { + Comments = comments.ToList(), + PageInfo = new PageInfo + { + PageNumber = sortParameters.CurrentPage.Value, + PageSize = sortParameters.PageSize.Value, + TotalItems = total + } + }; + } + + /// + public async Task GetCommentsActivity() + { + const string sql = """ + SELECT CONVERT(date, CreatedAt) Date, COUNT(*) Count + FROM Comments + GROUP BY CONVERT(date, CreatedAt) + ORDER BY Date + """; + + var data = await connection.QueryAsync(sql); + + return new ChartDataModel + { + Name = "Comments", + Series = data.Select(x => new ChartItem + { + Name = ((DateTime)x.Date).ToString("dd/MM/yyyy"), + Value = (int)x.Count + }).ToList() + }; + } +} \ No newline at end of file diff --git a/BlogWebApp/Services/Blog.Services/DapperServices/Interfaces/ICommentsDapperService.cs b/BlogWebApp/Services/Blog.Services/DapperServices/Interfaces/ICommentsDapperService.cs index a41fd92b..cf9e05ba 100644 --- a/BlogWebApp/Services/Blog.Services/DapperServices/Interfaces/ICommentsDapperService.cs +++ b/BlogWebApp/Services/Blog.Services/DapperServices/Interfaces/ICommentsDapperService.cs @@ -1,4 +1,8 @@ -using Blog.Contracts.V1.Responses.Chart; +// +// Copyright (c) PlaceholderCompany. All rights reserved. +// + +using Blog.Contracts.V1.Responses.Chart; using Blog.EntityServices.GeneralService; using Blog.Services.Core.Dtos; using Blog.Services.Core.Dtos.Posts; From 00a381b76daf26c5eda17cbc39b494b6c6595b70 Mon Sep 17 00:00:00 2001 From: Vitalii Lakatosh Date: Thu, 19 Feb 2026 01:11:16 +0200 Subject: [PATCH 05/11] Create Messages dapper service --- .../Interfaces/IMessagesDapperService.cs | 14 +++++++++++ .../DapperServices/MessagesDapperService.cs | 23 +++++++++++++++++++ 2 files changed, 37 insertions(+) create mode 100644 BlogWebApp/Services/Blog.Services/DapperServices/Interfaces/IMessagesDapperService.cs create mode 100644 BlogWebApp/Services/Blog.Services/DapperServices/MessagesDapperService.cs diff --git a/BlogWebApp/Services/Blog.Services/DapperServices/Interfaces/IMessagesDapperService.cs b/BlogWebApp/Services/Blog.Services/DapperServices/Interfaces/IMessagesDapperService.cs new file mode 100644 index 00000000..24d55e0a --- /dev/null +++ b/BlogWebApp/Services/Blog.Services/DapperServices/Interfaces/IMessagesDapperService.cs @@ -0,0 +1,14 @@ +// +// Copyright (c) PlaceholderCompany. All rights reserved. +// + +using Blog.Data.Models; +using Blog.EntityServices.GeneralService; + +namespace Blog.EntityServices.DapperServices.Interfaces; + +/// +/// Messages dapper service interface. +/// +/// +public interface IMessagesDapperService: IGeneralDapperService; \ No newline at end of file diff --git a/BlogWebApp/Services/Blog.Services/DapperServices/MessagesDapperService.cs b/BlogWebApp/Services/Blog.Services/DapperServices/MessagesDapperService.cs new file mode 100644 index 00000000..9b12fcd9 --- /dev/null +++ b/BlogWebApp/Services/Blog.Services/DapperServices/MessagesDapperService.cs @@ -0,0 +1,23 @@ +// +// Copyright (c) PlaceholderCompany. All rights reserved. +// + +using Blog.Data.Models; +using Blog.Data.Repository; +using Blog.EntityServices.DapperServices.Interfaces; +using Blog.EntityServices.GeneralService; +using Blog.EntityServices.Interfaces; + +namespace Blog.EntityServices.DapperServices; + +/// +/// Messages dapper service. +/// +/// +/// +/// +/// Initializes a new instance of the class. +/// +/// The repo. +public class MessagesDapperService(IDapperRepository repo) + : GeneralDapperService(repo), IMessagesDapperService; \ No newline at end of file From 0bd17bdda2ca0bb357fd47ef30835ba4150f134c Mon Sep 17 00:00:00 2001 From: Vitalii Lakatosh Date: Thu, 19 Feb 2026 01:11:41 +0200 Subject: [PATCH 06/11] Create posts dapper service --- .../Interfaces/IPostsDapperService.cs | 65 +++ .../DapperServices/PostsDapperService.cs | 428 ++++++++++++++++++ 2 files changed, 493 insertions(+) create mode 100644 BlogWebApp/Services/Blog.Services/DapperServices/Interfaces/IPostsDapperService.cs create mode 100644 BlogWebApp/Services/Blog.Services/DapperServices/PostsDapperService.cs diff --git a/BlogWebApp/Services/Blog.Services/DapperServices/Interfaces/IPostsDapperService.cs b/BlogWebApp/Services/Blog.Services/DapperServices/Interfaces/IPostsDapperService.cs new file mode 100644 index 00000000..8089221d --- /dev/null +++ b/BlogWebApp/Services/Blog.Services/DapperServices/Interfaces/IPostsDapperService.cs @@ -0,0 +1,65 @@ +// +// Copyright (c) PlaceholderCompany. All rights reserved. +// + +using Blog.Contracts.V1.Responses.Chart; +using Blog.Data.Models; +using Blog.EntityServices.GeneralService; +using Blog.Services.Core.Dtos; +using Blog.Services.Core.Dtos.Posts; +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace Blog.EntityServices.DapperServices.Interfaces; + +/// +/// Posts dapper service interfaces. +/// +public interface IPostsDapperService : IGeneralDapperService +{ + /// + /// Async get posts async. + /// + /// searchParameters. + /// Task. + Task GetPostsAsync(PostsSearchParametersDto searchParameters); + + /// + /// Async get post. + /// + /// id. + /// Task. + Task GetPostAsync(int id); + + /// + /// Async get post with comment. + /// + /// postId. + /// sortParameters. + /// Task. + Task GetPost(int postId, SortParametersDto sortParameters); + + /// + /// Async get user posts. + /// + /// userId. + /// searchParameters. + /// Task. + Task GetUserPostsAsync(string userId, PostsSearchParametersDto searchParameters); + + /// + /// Inserts the asynchronous. + /// + /// The post. + /// The tags. + /// Task. + Task InsertAsync(Post post, IEnumerable tags); + + /// + /// Asynchronous Get posts activity. + /// + /// Task. + Task GetPostsActivity(); + + Task ExportPostsToExcel(PostsSearchParametersDto searchParameters); +} \ No newline at end of file diff --git a/BlogWebApp/Services/Blog.Services/DapperServices/PostsDapperService.cs b/BlogWebApp/Services/Blog.Services/DapperServices/PostsDapperService.cs new file mode 100644 index 00000000..d0b91f2f --- /dev/null +++ b/BlogWebApp/Services/Blog.Services/DapperServices/PostsDapperService.cs @@ -0,0 +1,428 @@ +// +// Copyright (c) PlaceholderCompany. All rights reserved. +// + +using Blog.Contracts.V1.Responses.Chart; +using Blog.Core.Helpers; +using Blog.Data.Models; +using Blog.Data.Repository; +using Blog.EntityServices.DapperServices.Interfaces; +using Blog.EntityServices.GeneralService; +using Blog.Services.Core.Dtos; +using Blog.Services.Core.Dtos.Exports; +using Blog.Services.Core.Dtos.Posts; +using Blog.Services.Core.Dtos.User; +using Dapper; +using System; +using System.Collections.Generic; +using System.Data; +using System.Linq; +using System.Threading.Tasks; +using Profile = Blog.Data.Models.Profile; + +namespace Blog.EntityServices.DapperServices; + +/// +/// Posts dapper service. +/// +/// +/// Initializes a new instance of the class. +/// +/// The postsRepo. +/// The comments dapper service. +/// The connection. +public class PostsDapperService( + IDapperRepository postsRepo, + ICommentsDapperService commentsDapperService, + IDbConnection connection) + : GeneralDapperService(postsRepo), IPostsDapperService +{ + /// + public async Task GetPostsAsync(PostsSearchParametersDto searchParameters) + { + var where = "WHERE 1=1"; + var param = new DynamicParameters(); + + if (!string.IsNullOrEmpty(searchParameters.Search)) + { + where += " AND p.Title LIKE @Search"; + param.Add("@Search", $"%{searchParameters.Search}%"); + } + + if (!string.IsNullOrEmpty(searchParameters.Tag)) + { + where += " AND EXISTS (SELECT 1 FROM PostsTagsRelations r JOIN Tags t ON t.Id=r.TagId WHERE r.PostId=p.Id AND t.Title=@Tag)"; + param.Add("@Tag", searchParameters.Tag); + } + + var countSql = $"SELECT COUNT(*) FROM Posts p {where}"; + var total = await connection.ExecuteScalarAsync(countSql, param); + + var sql = $""" + SELECT p.*, u.*, COUNT(c.Id) CommentsCount + FROM Posts p + LEFT JOIN AspNetUsers u ON u.Id = p.AuthorId + LEFT JOIN Comments c ON c.PostId = p.Id + {where} + GROUP BY p.Id, u.Id + ORDER BY p.CreatedAt DESC + OFFSET @Skip ROWS FETCH NEXT @Take ROWS ONLY + """; + + param.Add("@Skip", (searchParameters.SortParameters.CurrentPage - 1) * searchParameters.SortParameters.PageSize); + param.Add("@Take", searchParameters.SortParameters.PageSize); + + var posts = await connection.QueryAsync( + sql, + (post, author) => + { + post.Author = author; + return post; + }, + param, + splitOn: "Id" + ); + + return new PostsViewDto + { + Posts = posts.ToList(), + PageInfo = new PageInfo + { + PageNumber = searchParameters.SortParameters.CurrentPage.Value, + PageSize = searchParameters.SortParameters.PageSize.Value, + TotalItems = total + } + }; + } + + /// + public async Task GetPostAsync(int id) + { + const string sql = """ + SELECT p.*, + u.*, + pr.*, + t.*, + ptr.* + FROM Posts p + LEFT JOIN AspNetUsers u ON u.Id = p.AuthorId + LEFT JOIN Profiles pr ON pr.UserId = u.Id + LEFT JOIN PostsTagsRelations ptr ON ptr.PostId = p.Id + LEFT JOIN Tags t ON t.Id = ptr.TagId + WHERE p.Id = @Id + """; + + var postDict = new Dictionary(); + + var result = await connection.QueryAsync( + sql, + (post, user, profile, tag, relation) => + { + if (!postDict.TryGetValue(post.Id, out var currentPost)) + { + currentPost = post; + currentPost.Author = user; + currentPost.Author.Profile = profile; + currentPost.PostsTagsRelations = new List(); + postDict.Add(post.Id, currentPost); + } + + if (relation == null) + { + return currentPost; + } + + relation.Tag = tag; + currentPost.PostsTagsRelations.Add(relation); + + return currentPost; + }, + new { Id = id }, + splitOn: "Id,Id,Id,Id" + ); + + return postDict.Values.FirstOrDefault(); + } + + /// + public async Task GetPost(int postId, SortParametersDto sortParameters) + { + const string sql = """ + SELECT + p.Id, p.Title, p.Description, p.Content, p.Seen, p.Likes, p.Dislikes, p.ImageUrl, + u.Id, u.FirstName, u.LastName, u.Email, + t.Id, t.Title + FROM Posts p + LEFT JOIN AspNetUsers u ON u.Id = p.AuthorId + LEFT JOIN PostsTagsRelations ptr ON ptr.PostId = p.Id + LEFT JOIN Tags t ON t.Id = ptr.TagId + WHERE p.Id = @Id + """; + + var postDict = new Dictionary(); + + await connection.QueryAsync( + sql, + (post, author, tag) => + { + if (!postDict.TryGetValue(post.Id, out var dto)) + { + dto = new PostShowViewDto + { + Post = post, + Tags = new List() + }; + dto.Post.Author = author; + postDict.Add(post.Id, dto); + } + + if (tag != null) + dto.Tags.Add(tag); + + return dto; + }, + new { Id = postId }, + splitOn: "Id,Id" + ); + + var model = postDict.Values.FirstOrDefault(); + model.Comments = await commentsDapperService.GetPagedCommentsByPostId(postId, sortParameters); + + return model; + } + + /// + public async Task GetUserPostsAsync(string userId, PostsSearchParametersDto searchParameters) + { + var where = "WHERE p.AuthorId = @UserId"; + var param = new DynamicParameters(new { UserId = userId }); + + if (!string.IsNullOrWhiteSpace(searchParameters.Search)) + { + where += " AND LOWER(p.Title) LIKE LOWER(@Search)"; + param.Add("@Search", $"%{searchParameters.Search}%"); + } + + if (!string.IsNullOrWhiteSpace(searchParameters.Tag)) + { + where += """ + AND EXISTS ( + SELECT 1 FROM PostsTagsRelations ptr + JOIN Tags t ON t.Id = ptr.TagId + WHERE ptr.PostId = p.Id AND LOWER(t.Title) = LOWER(@Tag) + ) + """; + param.Add("@Tag", searchParameters.Tag); + } + + var countSql = $"SELECT COUNT(*) FROM Posts p {where}"; + var total = await connection.ExecuteScalarAsync(countSql, param); + + var sql = $""" + SELECT p.*, + u.Id, u.Email, u.FirstName, u.LastName, + COUNT(DISTINCT c.Id) CommentsCount + FROM Posts p + LEFT JOIN AspNetUsers u ON u.Id = p.AuthorId + LEFT JOIN Comments c ON c.PostId = p.Id + {where} + GROUP BY p.Id, u.Id, u.Email, u.FirstName, u.LastName + ORDER BY p.CreatedAt DESC + OFFSET @Skip ROWS FETCH NEXT @Take ROWS ONLY + """; + + param.Add("@Skip", (searchParameters.SortParameters.CurrentPage.Value - 1) * searchParameters.SortParameters.PageSize.Value); + param.Add("@Take", searchParameters.SortParameters.PageSize.Value); + + var posts = await connection.QueryAsync( + sql, + (p, u) => + { + p.Author = u; + return p; + }, + param, + splitOn: "Id"); + + return new PostsViewDto + { + Posts = posts.ToList(), + PageInfo = new PageInfo + { + PageNumber = searchParameters.SortParameters.CurrentPage.Value, + PageSize = searchParameters.SortParameters.PageSize.Value, + TotalItems = total + } + }; + } + + /// + public async Task InsertAsync(Post post, IEnumerable tags) + { + using var transaction = connection.BeginTransaction(); + + try + { + // INSERT POST and get ID + var postId = await connection.ExecuteScalarAsync(@" + INSERT INTO Posts (Title, Description, Content, ImageUrl, AuthorId, CreatedAt) + VALUES (@Title, @Description, @Content, @ImageUrl, @AuthorId, GETUTCDATE()); + SELECT CAST(SCOPE_IDENTITY() as int);", + new + { + post.Title, + post.Description, + post.Content, + post.ImageUrl, + post.AuthorId + }, + transaction); + + // Add tags + foreach (var tag in tags) + { + var tagId = await connection.ExecuteScalarAsync(@" + SELECT Id FROM Tags WHERE Title = @Title", + new { tag.Title }, + transaction) ?? await connection.ExecuteScalarAsync(@" + INSERT INTO Tags (Title) + VALUES (@Title); + SELECT CAST(SCOPE_IDENTITY() as int);", + new { tag.Title }, + transaction); + + // Create post tag relation + await connection.ExecuteAsync(@" + INSERT INTO PostsTagsRelations (PostId, TagId) + VALUES (@PostId, @TagId)", + new { PostId = postId, TagId = tagId }, + transaction); + } + + transaction.Commit(); + } + catch + { + transaction.Rollback(); + throw; + } + } + + /// + public async Task GetPostsActivity() + { + const string sql = """ + SELECT CONVERT(date, CreatedAt) Date, COUNT(*) Count + FROM Posts + GROUP BY CONVERT(date, CreatedAt) + ORDER BY Date + """; + + var data = await connection.QueryAsync(sql); + + return new ChartDataModel + { + Name = "Posts", + Series = data.Select(x => new ChartItem + { + Name = ((DateTime)x.Date).ToString("dd/MM/yyyy"), + Value = (int)x.Count + }).ToList() + }; + } + + /// + public async Task ExportPostsToExcel(PostsSearchParametersDto searchParameters) + { + try + { + var sql = """ + SELECT + p.Title, + p.Description, + p.Content, + CONCAT(u.FirstName, ' ', u.LastName, ' (', u.Email, ')') AS Author, + p.Seen, + p.Likes, + p.Dislikes, + p.ImageUrl, + ISNULL(STRING_AGG(t.Title, ', '), '') AS Tags, + COUNT(DISTINCT c.Id) AS CommentsCount + FROM Posts p + LEFT JOIN AspNetUsers u ON u.Id = p.AuthorId + LEFT JOIN Comments c ON c.PostId = p.Id + LEFT JOIN PostsTagsRelations ptr ON ptr.PostId = p.Id + LEFT JOIN Tags t ON t.Id = ptr.TagId + WHERE 1 = 1 + + """; + + var parameters = new DynamicParameters(); + + if (!string.IsNullOrWhiteSpace(searchParameters.Search)) + { + sql += " AND LOWER(p.Title) LIKE LOWER(@Search)"; + parameters.Add("@Search", $"%{searchParameters.Search}%"); + } + + if (!string.IsNullOrWhiteSpace(searchParameters.Tag)) + { + sql += " AND LOWER(t.Title) = LOWER(@Tag)"; + parameters.Add("@Tag", searchParameters.Tag); + } + + sql += """ + GROUP BY + p.Id, p.Title, p.Description, p.Content, + u.FirstName, u.LastName, u.Email, + p.Seen, p.Likes, p.Dislikes, p.ImageUrl + """; + + var rows = await connection.QueryAsync(sql, parameters); + + var exportRequest = new ExportDataIntoExcelDto + { + Headers = + [ + new("Title"), + new("Description"), + new("Content"), + new("Author"), + new("Seen"), + new("Likes"), + new("Dislikes"), + new("ImageUrl"), + new("Tags"), + new("Comments count") + ], + Rows = [] + }; + + foreach (var post in rows) + { + var dataTable = new DataTable(); + var row = dataTable.NewRow(); + + row["Title"] = post.Title; + row["Description"] = post.Description; + row["Content"] = post.Content; + row["Author"] = post.Author; + row["Seen"] = post.Seen; + row["Likes"] = post.Likes; + row["Dislikes"] = post.Dislikes; + row["ImageUrl"] = post.ImageUrl; + row["Tags"] = post.Tags; + row["Comments count"] = post.CommentsCount; + + exportRequest.Rows.Add(row); + } + + return null; // exportsService.ExportDataIntoExcel(exportRequest); + } + catch (Exception e) + { + Console.WriteLine(e); + throw; + } + } +} \ No newline at end of file From e7d3602825c001d71d2d9503beae8d31a6f16063 Mon Sep 17 00:00:00 2001 From: Vitalii Lakatosh Date: Thu, 19 Feb 2026 01:12:05 +0200 Subject: [PATCH 07/11] Create Posts Tags relation dapper service --- .../IPostsTagsRelationsDapperService.cs | 35 +++++ .../PostsTagsRelationsDapperService.cs | 133 ++++++++++++++++++ 2 files changed, 168 insertions(+) create mode 100644 BlogWebApp/Services/Blog.Services/DapperServices/Interfaces/IPostsTagsRelationsDapperService.cs create mode 100644 BlogWebApp/Services/Blog.Services/DapperServices/PostsTagsRelationsDapperService.cs diff --git a/BlogWebApp/Services/Blog.Services/DapperServices/Interfaces/IPostsTagsRelationsDapperService.cs b/BlogWebApp/Services/Blog.Services/DapperServices/Interfaces/IPostsTagsRelationsDapperService.cs new file mode 100644 index 00000000..bc1c29a1 --- /dev/null +++ b/BlogWebApp/Services/Blog.Services/DapperServices/Interfaces/IPostsTagsRelationsDapperService.cs @@ -0,0 +1,35 @@ +// +// Copyright (c) PlaceholderCompany. All rights reserved. +// + +using Blog.Data.Models; +using Blog.EntityServices.GeneralService; +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace Blog.EntityServices.DapperServices.Interfaces; + +/// +/// Post tag relations dapper service interface. +/// +/// +public interface IPostsTagsRelationsDapperService : IGeneralDapperService +{ + /// + /// Adds the tags to post. + /// + /// The post identifier. + /// The posts tags relations. + /// The tags. + /// Task. + Task AddTagsToPost(int postId, List postsTagsRelations, IEnumerable tags); + + /// + /// Add tags to existing post. + /// + /// The post id. + /// The existing posts tags relations. + /// THe tags. + /// Task. + Task AddTagsToExistingPost(int postId, List existingPostsTagsRelations, IEnumerable tags); +} \ No newline at end of file diff --git a/BlogWebApp/Services/Blog.Services/DapperServices/PostsTagsRelationsDapperService.cs b/BlogWebApp/Services/Blog.Services/DapperServices/PostsTagsRelationsDapperService.cs new file mode 100644 index 00000000..b62136db --- /dev/null +++ b/BlogWebApp/Services/Blog.Services/DapperServices/PostsTagsRelationsDapperService.cs @@ -0,0 +1,133 @@ +// +// Copyright (c) PlaceholderCompany. All rights reserved. +// + +using Blog.Data.Models; +using Blog.Data.Repository; +using Blog.EntityServices.DapperServices.Interfaces; +using Blog.EntityServices.GeneralService; +using Blog.EntityServices.Interfaces; +using System; +using System.Collections.Generic; +using System.Data; +using System.Linq; +using System.Threading.Tasks; +using Dapper; + +namespace Blog.EntityServices.DapperServices; + +/// +/// Post tag relations dapper service. +/// +/// +/// +/// +/// Initializes a new instance of the class. +/// +/// The post tag relations dapperR repository. +/// The connection. +public class PostsTagsRelationsDapperService( + IDapperRepository postsTagsRelationsDapperRepository, + IDbConnection connection) + : GeneralDapperService(postsTagsRelationsDapperRepository), IPostsTagsRelationsDapperService +{ + /// + public async Task AddTagsToPost(int postId, List postsTagsRelations, IEnumerable tags) + { + using var transaction = connection.BeginTransaction(); + + try + { + var normalizedTitles = tags + .Where(t => !string.IsNullOrWhiteSpace(t.Title)) + .Select(t => t.Title.Trim()) + .Distinct(StringComparer.OrdinalIgnoreCase) + .ToList(); + + if (normalizedTitles.Count == 0) + { + return; + } + + // 1️⃣ Отримати всі існуючі теги одним запитом + var existingTags = (await connection.QueryAsync( + """ + SELECT Id, Title + FROM Tags + WHERE Title IN @Titles + """, + new { Titles = normalizedTitles }, + transaction)).ToList(); + + var existingLookup = existingTags + .ToDictionary(x => x.Title, StringComparer.OrdinalIgnoreCase); + + // Check new tags + var newTitles = normalizedTitles + .Where(title => !existingLookup.ContainsKey(title)) + .ToList(); + + // Batch insert new tags + if (newTitles.Count != 0) + { + var insertedTags = (await connection.QueryAsync( + """ + INSERT INTO Tags (Title) + OUTPUT INSERTED.Id, INSERTED.Title + VALUES (@Title) + """, + newTitles.Select(title => new { Title = title }), + transaction)).ToList(); + + foreach (var tag in insertedTags) + existingLookup[tag.Title] = tag; + } + + // Get existing relations + var existingRelations = await connection.QueryAsync( + """ + SELECT TagId + FROM PostsTagsRelations + WHERE PostId = @PostId + """, + new { PostId = postId }, + transaction); + + var existingRelationSet = new HashSet(existingRelations); + + // Prepare new relations + var relationsToInsert = existingLookup.Values + .Where(tag => !existingRelationSet.Contains(tag.Id)) + .Select(tag => new + { + PostId = postId, + TagId = tag.Id + }) + .ToList(); + + if (relationsToInsert.Count != 0) + { + await connection.ExecuteAsync( + """ + INSERT INTO PostsTagsRelations (PostId, TagId) + VALUES (@PostId, @TagId) + """, + relationsToInsert, + transaction); + } + + transaction.Commit(); + } + catch + { + transaction.Rollback(); + throw; + } + } + + /// + public async Task AddTagsToExistingPost(int postId, List existingPostsTagsRelations, IEnumerable tags) + { + await AddTagsToPost(postId, existingPostsTagsRelations, tags); + } +} \ No newline at end of file From 5c214ae6da35582513a80c9727dc39db73cee445 Mon Sep 17 00:00:00 2001 From: Vitalii Lakatosh Date: Thu, 19 Feb 2026 01:12:35 +0200 Subject: [PATCH 08/11] Create Profile dapper service --- .../Interfaces/IProfileDapperService.cs | 24 +++++++++ .../DapperServices/ProfileDapperService.cs | 50 +++++++++++++++++++ 2 files changed, 74 insertions(+) create mode 100644 BlogWebApp/Services/Blog.Services/DapperServices/Interfaces/IProfileDapperService.cs create mode 100644 BlogWebApp/Services/Blog.Services/DapperServices/ProfileDapperService.cs diff --git a/BlogWebApp/Services/Blog.Services/DapperServices/Interfaces/IProfileDapperService.cs b/BlogWebApp/Services/Blog.Services/DapperServices/Interfaces/IProfileDapperService.cs new file mode 100644 index 00000000..e9de4d5e --- /dev/null +++ b/BlogWebApp/Services/Blog.Services/DapperServices/Interfaces/IProfileDapperService.cs @@ -0,0 +1,24 @@ +// +// Copyright (c) PlaceholderCompany. All rights reserved. +// + +using Blog.Data.Models; +using Blog.EntityServices.GeneralService; +using Blog.Services.Core.Dtos.User; +using System.Threading.Tasks; + +namespace Blog.EntityServices.DapperServices.Interfaces; + +/// +/// Profile dapper service interface. +/// +/// +public interface IProfileDapperService : IGeneralDapperService +{ + /// + /// Gets the profile. + /// + /// The profile identifier. + /// Task. + Task GetProfile(int profileId); +} \ No newline at end of file diff --git a/BlogWebApp/Services/Blog.Services/DapperServices/ProfileDapperService.cs b/BlogWebApp/Services/Blog.Services/DapperServices/ProfileDapperService.cs new file mode 100644 index 00000000..b0aea47a --- /dev/null +++ b/BlogWebApp/Services/Blog.Services/DapperServices/ProfileDapperService.cs @@ -0,0 +1,50 @@ +// +// Copyright (c) PlaceholderCompany. All rights reserved. +// + +using Blog.Data.Repository; +using Blog.EntityServices.DapperServices.Interfaces; +using Blog.EntityServices.GeneralService; +using Blog.EntityServices.Interfaces; +using System.Data; +using System.Threading.Tasks; +using Blog.Services.Core.Dtos.User; +using Dapper; +using ProfileModel = Blog.Data.Models.Profile; + +namespace Blog.EntityServices.DapperServices; + +/// +/// Profile dapper service. +/// +/// +/// +/// +/// Initializes a new instance of the class. +/// +/// The profile repository. +/// The connection. +public class ProfileDapperService( + IDapperRepository profileRepository, + IDbConnection connection) + : GeneralDapperService(profileRepository), IProfileDapperService +{ + /// + public async Task GetProfile(int profileId) + { + const string sql = """ + SELECT u.Id, + u.FirstName, + u.LastName, + u.Email, + u.UserName + FROM Profiles p + INNER JOIN AspNetUsers u ON u.Id = p.UserId + WHERE p.Id = @ProfileId + """; + + return await connection.QueryFirstOrDefaultAsync( + sql, + new { ProfileId = profileId }); + } +} \ No newline at end of file From d114bf0a545772558fd38cfbb6b4aa0b3aa6e806 Mon Sep 17 00:00:00 2001 From: Vitalii Lakatosh Date: Fri, 20 Feb 2026 01:13:18 +0200 Subject: [PATCH 09/11] Create Tags dapper service --- .../Interfaces/ITagsDapperService.cs | 32 +++++ .../DapperServices/TagsDapperService.cs | 124 ++++++++++++++++++ 2 files changed, 156 insertions(+) create mode 100644 BlogWebApp/Services/Blog.Services/DapperServices/Interfaces/ITagsDapperService.cs create mode 100644 BlogWebApp/Services/Blog.Services/DapperServices/TagsDapperService.cs diff --git a/BlogWebApp/Services/Blog.Services/DapperServices/Interfaces/ITagsDapperService.cs b/BlogWebApp/Services/Blog.Services/DapperServices/Interfaces/ITagsDapperService.cs new file mode 100644 index 00000000..20a654f6 --- /dev/null +++ b/BlogWebApp/Services/Blog.Services/DapperServices/Interfaces/ITagsDapperService.cs @@ -0,0 +1,32 @@ +// +// Copyright (c) PlaceholderCompany. All rights reserved. +// + +using Blog.Contracts.V1.Responses.Chart; +using Blog.Data.Models; +using Blog.EntityServices.GeneralService; +using Blog.Services.Core.Dtos; +using Blog.Services.Core.Dtos.Posts; +using System.Threading.Tasks; + +namespace Blog.EntityServices.DapperServices.Interfaces; + +/// +/// Tags dapper service interface. +/// +/// +public interface ITagsDapperService : IGeneralDapperService +{ + /// + /// Gets the tags asynchronous. + /// + /// The search parameters. + /// Task. + Task GetTagsAsync(SearchParametersDto searchParameters); + + /// + /// Asynchronous Get tags activity. + /// + /// Task. + Task GetTagsActivity(); +} \ No newline at end of file diff --git a/BlogWebApp/Services/Blog.Services/DapperServices/TagsDapperService.cs b/BlogWebApp/Services/Blog.Services/DapperServices/TagsDapperService.cs new file mode 100644 index 00000000..4aafdecf --- /dev/null +++ b/BlogWebApp/Services/Blog.Services/DapperServices/TagsDapperService.cs @@ -0,0 +1,124 @@ +// +// Copyright (c) PlaceholderCompany. All rights reserved. +// + +using Blog.Contracts.V1.Responses.Chart; +using Blog.Core.Helpers; +using Blog.Data.Models; +using Blog.Data.Repository; +using Blog.EntityServices.DapperServices.Interfaces; +using Blog.EntityServices.GeneralService; +using Blog.Services.Core.Dtos; +using Blog.Services.Core.Dtos.Posts; +using Dapper; +using System.Data; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Blog.EntityServices.DapperServices; + +/// +/// Tags service. +/// +/// +/// +/// +/// Initializes a new instance of the class. +/// +/// The repo. +/// The connection. +public class TagsDapperService(IDapperRepository repo, + IDbConnection connection) + : GeneralDapperService(repo), ITagsDapperService +{ + /// + public async Task GetTagsAsync(SearchParametersDto searchParameters) + { + var result = new TagsViewDto(); + + var sqlBuilder = new StringBuilder(); + var countSqlBuilder = new StringBuilder(); + + sqlBuilder.Append("SELECT Id, Title FROM Tags WHERE 1=1 "); + countSqlBuilder.Append("SELECT COUNT(*) FROM Tags WHERE 1=1 "); + + var parameters = new DynamicParameters(); + + // Search + if (!string.IsNullOrWhiteSpace(searchParameters.Search)) + { + sqlBuilder.Append(" AND LOWER(Title) LIKE @Search "); + countSqlBuilder.Append(" AND LOWER(Title) LIKE @Search "); + parameters.Add("Search", $"%{searchParameters.Search.ToLower()}%"); + } + + // Sorting + if (searchParameters.SortParameters?.SortBy != null) + { + sqlBuilder.Append($" ORDER BY {searchParameters.SortParameters.OrderBy} "); + + sqlBuilder.Append(searchParameters.SortParameters.SortBy == "desc" ? " DESC " : " ASC "); + } + else + { + sqlBuilder.Append(" ORDER BY Id ASC "); + } + + // Pagination + if (searchParameters.SortParameters is { CurrentPage: not null, PageSize: not null }) + { + sqlBuilder.Append(" OFFSET @Offset ROWS FETCH NEXT @PageSize ROWS ONLY "); + + parameters.Add("Offset", + (searchParameters.SortParameters.CurrentPage.Value - 1) * + searchParameters.SortParameters.PageSize.Value); + + parameters.Add("PageSize", + searchParameters.SortParameters.PageSize.Value); + } + + // Execute + var tags = await connection.QueryAsync( + sqlBuilder.ToString(), + parameters); + + var totalCount = await connection.ExecuteScalarAsync( + countSqlBuilder.ToString(), + parameters); + + result.Tags = tags.ToList(); + + if (searchParameters.SortParameters is { CurrentPage: not null, PageSize: not null }) + { + result.PageInfo = new PageInfo + { + PageNumber = searchParameters.SortParameters.CurrentPage.Value, + PageSize = searchParameters.SortParameters.PageSize.Value, + TotalItems = totalCount + }; + } + + return result; + } + + /// + public async Task GetTagsActivity() + { + const string sql = """ + SELECT Title AS Name, + COUNT(*) AS Value + FROM Tags + GROUP BY Title + ORDER BY COUNT(*) DESC + """; + + var series = await connection.QueryAsync(sql); + + return new ChartDataModel + { + Name = "Posts", + Series = series.ToList() + }; + } +} \ No newline at end of file From 3887c5eb80236f53899c914a66e6fe53cc838bef Mon Sep 17 00:00:00 2001 From: Vitalii Lakatosh Date: Fri, 20 Feb 2026 01:14:41 +0200 Subject: [PATCH 10/11] Setup dapper repositories --- .../DataRepositoriesInstaller.cs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/BlogWebApp/Blog.Web/StartupConfigureServicesInstallers/DataRepositoriesInstaller.cs b/BlogWebApp/Blog.Web/StartupConfigureServicesInstallers/DataRepositoriesInstaller.cs index 0d0dbf10..ab05f486 100644 --- a/BlogWebApp/Blog.Web/StartupConfigureServicesInstallers/DataRepositoriesInstaller.cs +++ b/BlogWebApp/Blog.Web/StartupConfigureServicesInstallers/DataRepositoriesInstaller.cs @@ -26,5 +26,13 @@ public void InstallServices(IServiceCollection services, IConfiguration configur services.AddTransient, Repository>(); services.AddTransient, Repository>(); services.AddTransient, Repository>(); + + // Dapper repositories + services.AddTransient, DapperRepository>(); + services.AddTransient, DapperRepository>(); + services.AddTransient, DapperRepository>(); + services.AddTransient, DapperRepository>(); + services.AddTransient, DapperRepository>(); + services.AddTransient, DapperRepository>(); } } \ No newline at end of file From d3d58349bcafb552b2d102e66348149611de309d Mon Sep 17 00:00:00 2001 From: Vitalii Lakatosh Date: Fri, 20 Feb 2026 01:15:28 +0200 Subject: [PATCH 11/11] Setup dapper services --- .../ApplicationServicesInstaller.cs | 39 ++++++++++++------- 1 file changed, 25 insertions(+), 14 deletions(-) diff --git a/BlogWebApp/Blog.Web/StartupConfigureServicesInstallers/ApplicationServicesInstaller.cs b/BlogWebApp/Blog.Web/StartupConfigureServicesInstallers/ApplicationServicesInstaller.cs index 3591d50f..1014b3fd 100644 --- a/BlogWebApp/Blog.Web/StartupConfigureServicesInstallers/ApplicationServicesInstaller.cs +++ b/BlogWebApp/Blog.Web/StartupConfigureServicesInstallers/ApplicationServicesInstaller.cs @@ -1,11 +1,16 @@ -namespace Blog.Web.StartupConfigureServicesInstallers; +using Blog.EntityServices.DapperServices.Interfaces; -using Microsoft.AspNetCore.Authorization; -using Microsoft.AspNetCore.Identity; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Options; +namespace Blog.Web.StartupConfigureServicesInstallers; + +using Blog.EntityServices.DapperServices; +using Blog.Services.Core.Caching; +using Blog.Services.Core.Caching.Interfaces; +using Blog.Services.Core.Email.Templates; +using Blog.Services.Core.Security; +using CommonServices; +using CommonServices.EmailServices; using CommonServices.EmailServices.Interfaces; +using CommonServices.Interfaces; using Core; using Core.Configuration; using Core.Infrastructure; @@ -13,20 +18,18 @@ using Data; using Data.Models; using Data.Repository; -using Blog.Services.Core.Caching; -using Blog.Services.Core.Caching.Interfaces; -using Blog.Services.Core.Email.Templates; -using Blog.Services.Core.Security; -using CommonServices; -using CommonServices.EmailServices; -using CommonServices.Interfaces; using EntityServices.ControllerContext; -using EntityServices.Interfaces; using EntityServices.EntityFrameworkServices; using EntityServices.EntityFrameworkServices.Identity.Auth; using EntityServices.EntityFrameworkServices.Identity.RefreshToken; using EntityServices.EntityFrameworkServices.Identity.Registration; using EntityServices.EntityFrameworkServices.Identity.User; +using EntityServices.Interfaces; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Identity; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; /// /// Application services installer. @@ -71,5 +74,13 @@ public void InstallServices(IServiceCollection services, IConfiguration configur services.AddTransient(); services.AddTransient(); + + // Dapper services + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); } } \ No newline at end of file