Skip to content

Commit

Permalink
Refactor DbWrites into multiple commands
Browse files Browse the repository at this point in the history
  • Loading branch information
mythz committed Apr 6, 2024
1 parent cb52d49 commit 71a0e7e
Show file tree
Hide file tree
Showing 27 changed files with 775 additions and 453 deletions.
16 changes: 16 additions & 0 deletions MyApp.ServiceInterface/App/AnswerAddedToPostCommand.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
using System.Data;
using MyApp.Data;
using MyApp.ServiceModel;
using ServiceStack.OrmLite;

namespace MyApp.ServiceInterface.App;

public class AnswerAddedToPostCommand(IDbConnection db) : IExecuteCommandAsync<AnswerAddedToPost>
{
public async Task ExecuteAsync(AnswerAddedToPost request)
{
await db.UpdateAddAsync(() => new Post {
AnswerCount = 1,
}, x => x.Id == request.Id);
}
}
41 changes: 41 additions & 0 deletions MyApp.ServiceInterface/App/CompletePostJobsCommand.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
using System.Data;
using MyApp.Data;
using MyApp.ServiceModel;
using ServiceStack.Messaging;
using ServiceStack.OrmLite;

namespace MyApp.ServiceInterface.App;

public class CompletePostJobsCommand(IDbConnection Db, ModelWorkerQueue modelWorkers, IMessageProducer mqClient) : IExecuteCommandAsync<CompletePostJobs>
{
public async Task ExecuteAsync(CompletePostJobs request)
{
var jobIds = request.Ids;
await Db.UpdateOnlyAsync(() => new PostJob {
CompletedDate = DateTime.UtcNow,
},
x => jobIds.Contains(x.Id));
var postJobs = await Db.SelectAsync(Db.From<PostJob>()
.Where(x => jobIds.Contains(x.Id)));

foreach (var postJob in postJobs)
{
// If there's no outstanding model answer jobs for this post, add a rank job
if (!await Db.ExistsAsync(Db.From<PostJob>()
.Where(x => x.PostId == postJob.PostId && x.CompletedDate == null)))
{
var rankJob = new PostJob
{
PostId = postJob.PostId,
Model = "rank",
Title = postJob.Title,
CreatedDate = DateTime.UtcNow,
CreatedBy = nameof(DbWrites),
};
await Db.InsertAsync(rankJob);
modelWorkers.Enqueue(rankJob);
mqClient.Publish(new SearchTasks { AddPostToIndex = postJob.PostId });
}
}
}
}
80 changes: 80 additions & 0 deletions MyApp.ServiceInterface/App/CreateAnswerCommand.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
using System.Data;
using MyApp.Data;
using MyApp.ServiceModel;
using ServiceStack;
using ServiceStack.OrmLite;

namespace MyApp.ServiceInterface.App;

public class CreateAnswerCommand(AppConfig appConfig, IDbConnection db) : IExecuteCommandAsync<Post>
{
public async Task ExecuteAsync(Post answer)
{
var postId = answer.ParentId!.Value;
var refId = $"{postId}-{answer.CreatedBy}";
if (!await db.ExistsAsync(db.From<StatTotals>().Where(x => x.Id == refId)))
{
await db.InsertAsync(new StatTotals
{
Id = refId,
PostId = postId,
ViewCount = 0,
FavoriteCount = 0,
UpVotes = 0,
DownVotes = 0,
StartingUpVotes = 0,
CreatedBy = answer.CreatedBy,
});
}

var post = await db.SingleByIdAsync<Post>(postId);
if (post?.CreatedBy != null)
{
if (post.CreatedBy != answer.CreatedBy)
{
await db.InsertAsync(new Notification
{
UserName = post.CreatedBy,
Type = NotificationType.NewAnswer,
RefId = refId,
PostId = postId,
CreatedDate = answer.CreationDate,
Summary = answer.Summary.SubstringWithEllipsis(0, 100),
RefUserName = answer.CreatedBy,
});
appConfig.IncrUnreadNotificationsFor(post.CreatedBy);
}

if (!string.IsNullOrEmpty(answer.Body))
{
var cleanBody = answer.Body.StripHtml().Trim();
var userNameMentions = cleanBody.FindUserNameMentions()
.Where(x => x != post.CreatedBy && x != answer.CreatedBy).ToList();
if (userNameMentions.Count > 0)
{
var existingUsers = await db.SelectAsync(db.From<ApplicationUser>()
.Where(x => userNameMentions.Contains(x.UserName!)));

foreach (var existingUser in existingUsers)
{
var firstMentionPos = cleanBody.IndexOf(existingUser.UserName!, StringComparison.Ordinal);
if (firstMentionPos < 0) continue;

var startPos = Math.Max(0, firstMentionPos - 50);
await db.InsertAsync(new Notification
{
UserName = existingUser.UserName!,
Type = NotificationType.AnswerMention,
RefId = $"{postId}",
PostId = postId,
CreatedDate = answer.CreationDate,
Summary = cleanBody.SubstringWithEllipsis(startPos, 100),
RefUserName = answer.CreatedBy,
});
appConfig.IncrUnreadNotificationsFor(existingUser.UserName!);
}
}
}
}
}
}
15 changes: 15 additions & 0 deletions MyApp.ServiceInterface/App/CreateNotificationCommand.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
using System.Data;
using MyApp.Data;
using MyApp.ServiceModel;
using ServiceStack.OrmLite;

namespace MyApp.ServiceInterface.App;

public class CreateNotificationCommand(AppConfig appConfig, IDbConnection db) : IExecuteCommandAsync<Notification>
{
public async Task ExecuteAsync(Notification request)
{
await db.InsertAsync(request);
appConfig.IncrUnreadNotificationsFor(request.UserName);
}
}
77 changes: 77 additions & 0 deletions MyApp.ServiceInterface/App/CreatePostCommand.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
using System.Data;
using Microsoft.Extensions.Logging;
using MyApp.Data;
using MyApp.ServiceModel;
using ServiceStack;
using ServiceStack.OrmLite;

namespace MyApp.ServiceInterface.App;

public class CreatePostCommand(ILogger log, AppConfig appConfig, IDbConnection db) : IExecuteCommandAsync<Post>
{
public async Task ExecuteAsync(Post post)
{
var body = post.Body;
post.Body = null;
post.Id = (int)await db.InsertAsync(post, selectIdentity: true);
var createdBy = post.CreatedBy;
if (createdBy != null && post.PostTypeId == 1)
{
await appConfig.ResetUserQuestionsAsync(db, createdBy);
}

try
{
await db.InsertAsync(new StatTotals
{
Id = $"{post.Id}",
PostId = post.Id,
UpVotes = 0,
DownVotes = 0,
StartingUpVotes = 0,
CreatedBy = post.CreatedBy,
});
}
catch (Exception e)
{
log.LogWarning("Couldn't insert StatTotals for Post {PostId}: '{Message}', updating instead...", post.Id,
e.Message);
await db.UpdateOnlyAsync(() => new StatTotals
{
PostId = post.Id,
CreatedBy = post.CreatedBy,
}, x => x.Id == $"{post.Id}");
}

if (!string.IsNullOrEmpty(body))
{
var cleanBody = body.StripHtml().Trim();
var userNameMentions = cleanBody.FindUserNameMentions()
.Where(x => x != createdBy).ToList();
if (userNameMentions.Count > 0)
{
var existingUsers = await db.SelectAsync(db.From<ApplicationUser>()
.Where(x => userNameMentions.Contains(x.UserName!)));

foreach (var existingUser in existingUsers)
{
var firstMentionPos = cleanBody.IndexOf(existingUser.UserName!, StringComparison.Ordinal);
if (firstMentionPos < 0) continue;

var startPos = Math.Max(0, firstMentionPos - 50);
await db.InsertAsync(new Notification
{
UserName = existingUser.UserName!,
Type = NotificationType.QuestionMention,
RefId = $"{post.Id}",
PostId = post.Id,
CreatedDate = post.CreationDate,
Summary = cleanBody.SubstringWithEllipsis(startPos, 100),
RefUserName = createdBy,
});
appConfig.IncrUnreadNotificationsFor(existingUser.UserName!);
}
}
}
}
}
14 changes: 14 additions & 0 deletions MyApp.ServiceInterface/App/CreatePostJobsCommand.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
using System.Data;
using MyApp.Data;
using ServiceStack.OrmLite;

namespace MyApp.ServiceInterface.App;

public class CreatePostJobsCommand(IDbConnection db, ModelWorkerQueue modelWorkers) : IExecuteCommandAsync<CreatePostJobs>
{
public async Task ExecuteAsync(CreatePostJobs request)
{
await db.SaveAllAsync(request.PostJobs);
request.PostJobs.ForEach(modelWorkers.Enqueue);
}
}
53 changes: 53 additions & 0 deletions MyApp.ServiceInterface/App/CreatePostVotesCommand.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
using System.Data;
using MyApp.ServiceModel;
using ServiceStack.Messaging;
using ServiceStack.OrmLite;

namespace MyApp.ServiceInterface.App;

public class CreatePostVotesCommand(IDbConnection db, IMessageProducer mqClient) : IExecuteCommandAsync<Vote>
{
public async Task ExecuteAsync(Vote vote)
{
if (string.IsNullOrEmpty(vote.RefId))
throw new ArgumentNullException(nameof(vote.RefId));
if (string.IsNullOrEmpty(vote.UserName))
throw new ArgumentNullException(nameof(vote.UserName));

var isAnswer = vote.RefId.IndexOf('-') >= 0;
var voteUp = isAnswer ? AchievementType.AnswerUpVote : AchievementType.QuestionUpVote;
var voteDown = isAnswer ? AchievementType.AnswerDownVote : AchievementType.QuestionDownVote;

var rowsDeleted = await db.DeleteAsync<Vote>(new { vote.RefId, vote.UserName });
if (rowsDeleted > 0 && vote.RefUserName != null)
{
// If they rescinded their previous vote, also remove the Ref User's previous achievement for that Q or A
await db.ExecuteNonQueryAsync(
"DELETE FROM Achievement WHERE UserName = @TargetUser AND RefUserName = @VoterUserName AND RefId = @RefId AND Type IN (@voteUp,@voteDown)",
new { TargetUser = vote.RefUserName, VoterUserName = vote.UserName , vote.RefId, voteUp, voteDown });
}

if (vote.Score != 0)
{
await db.InsertAsync(vote);

if (vote.RefUserName != null)
{
await db.InsertAsync(new Achievement
{
UserName = vote.RefUserName,
RefUserName = vote.UserName,
PostId = vote.PostId,
RefId = vote.RefId,
Type = vote.Score > 0 ? voteUp : voteDown,
Score = vote.Score > 0 ? 10 : -1, // 10 points for UpVote, -1 point for DownVote
CreatedDate = DateTime.UtcNow,
});
}
}

mqClient.Publish(new RenderComponent {
RegenerateMeta = vote.PostId
});
}
}
20 changes: 20 additions & 0 deletions MyApp.ServiceInterface/App/DeleteCommentCommand.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
using System.Data;
using MyApp.Data;
using MyApp.ServiceModel;
using ServiceStack.OrmLite;

namespace MyApp.ServiceInterface.App;

public class DeleteCommentCommand(AppConfig appConfig, IDbConnection db) : IExecuteCommandAsync<DeleteComment>
{
public async Task ExecuteAsync(DeleteComment request)
{
var refId = $"{request.Id}-{request.Created}";
var rowsAffected = await db.DeleteAsync(db.From<Notification>()
.Where(x => x.RefId == refId && x.RefUserName == request.CreatedBy));
if (rowsAffected > 0)
{
appConfig.ResetUsersUnreadNotifications(db);
}
}
}
20 changes: 20 additions & 0 deletions MyApp.ServiceInterface/App/DeletePostCommand.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
using System.Data;
using MyApp.Data;
using MyApp.ServiceModel;
using ServiceStack.OrmLite;

namespace MyApp.ServiceInterface.App;

public class DeletePostCommand(AppConfig appConfig, IDbConnection db) : IExecuteCommandAsync<DeletePost>
{
public async Task ExecuteAsync(DeletePost request)
{
foreach (var postId in request.Ids)
{
await db.DeleteAsync<PostJob>(x => x.PostId == postId);
await db.DeleteAsync<Vote>(x => x.PostId == postId);
await db.DeleteByIdAsync<Post>(postId);
appConfig.ResetInitialPostId(db);
}
}
}
32 changes: 32 additions & 0 deletions MyApp.ServiceInterface/App/FailJobCommand.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
using System.Data;
using MyApp.Data;
using MyApp.ServiceModel;
using ServiceStack.OrmLite;

namespace MyApp.ServiceInterface.App;

public class FailJobCommand(IDbConnection db, ModelWorkerQueue modelWorkers) : IExecuteCommandAsync<FailJob>
{
public async Task ExecuteAsync(FailJob request)
{
await db.UpdateAddAsync(() => new PostJob {
Error = request.Error,
RetryCount = 1,
},
x => x.PostId == request.Id);
var postJob = await db.SingleByIdAsync<PostJob>(request.Id);
if (postJob != null)
{
if (postJob.RetryCount > 3)
{
await db.UpdateOnlyAsync(() =>
new PostJob { CompletedDate = DateTime.UtcNow },
x => x.PostId == request.Id);
}
else
{
modelWorkers.Enqueue(postJob);
}
}
}
}
Loading

0 comments on commit 71a0e7e

Please sign in to comment.