Skip to content

Commit

Permalink
fixed concurrency tokens in migrations for postgres, added indexes in…
Browse files Browse the repository at this point in the history
… migrations, added checking for unque constraint, added test for race conditions, additional fixes
  • Loading branch information
skrasekmichael committed Mar 27, 2024
1 parent 49390f7 commit d0d22d8
Show file tree
Hide file tree
Showing 20 changed files with 448 additions and 86 deletions.
18 changes: 15 additions & 3 deletions src/TeamUp.Infrastructure/Core/UnitOfWork.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;

using Npgsql;

using RailwayResult;
using RailwayResult.Errors;

Expand All @@ -12,8 +14,12 @@ namespace TeamUp.Infrastructure.Core;

internal sealed class UnitOfWork : IUnitOfWork
{
private static readonly ConflictError ConcurrencyError = new("Database.Concurrency.Conflict", "Multiple concurrent update requests have occurred.");
private static readonly InternalError UnexpectedError = new("Database.InternalError", "Unexpected error have occurred.");
//https://www.postgresql.org/docs/current/errcodes-appendix.html
private const string UniqueConstraintViolation = "23505";

internal static readonly ConflictError ConcurrencyError = new("Database.Concurrency.Conflict", "Multiple concurrent update requests have occurred.");
internal static readonly ConflictError UniqueConstraintError = new("Database.Constraints.PropertyConflict", "Unique property conflict has occurred.");
internal static readonly InternalError UnexpectedError = new("Database.InternalError", "Unexpected error have occurred.");

private readonly ApplicationDbContext _context;
private readonly IDomainEventsDispatcher _eventsDispatcher;
Expand Down Expand Up @@ -42,7 +48,13 @@ public async Task<Result> SaveChangesAsync(CancellationToken ct = default)
}
catch (DbUpdateException ex)
{
_logger.LogError(ex, "Database Update Exception");
_logger.LogError(ex.InnerException, "Database Update Exception");

if (ex.InnerException is PostgresException pex && pex.SqlState == UniqueConstraintViolation)
{
return UniqueConstraintError;
}

return UnexpectedError;
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,5 +26,15 @@ protected override void ConfigureEntity(EntityTypeBuilder<Event> eventEntityBuil
.WithOne()
.HasForeignKey(eventResponse => eventResponse.EventId)
.OnDelete(DeleteBehavior.Cascade);

eventEntityBuilder
.HasIndex(e => e.TeamId);

eventEntityBuilder
.HasIndex(e => e.EventTypeId);

eventEntityBuilder
.Property<uint>("RowVersion")
.IsRowVersion();
}
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
using Microsoft.EntityFrameworkCore.Metadata.Builders;

using TeamUp.Contracts.Events;
using TeamUp.Domain.Aggregates.Events;
using TeamUp.Domain.Aggregates.Teams;

using EventResponse = TeamUp.Domain.Aggregates.Events.EventResponse;
Expand All @@ -26,5 +25,12 @@ protected override void ConfigureEntity(EntityTypeBuilder<EventResponse> eventRe
eventResponseEntityBuilder
.Property(eventResponse => eventResponse.Message)
.HasMaxLength(255);

eventResponseEntityBuilder
.HasIndex(eventResponse => eventResponse.EventId);

eventResponseEntityBuilder
.HasIndex(eventResponse => new { eventResponse.EventId, eventResponse.TeamMemberId })
.IsUnique();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,15 +14,21 @@ protected override void ConfigureEntity(EntityTypeBuilder<Invitation> invitation
invitationEntityBuilder
.HasOne<User>()
.WithMany()
.HasForeignKey(e => e.RecipientId);
.HasForeignKey(invitation => invitation.RecipientId);

invitationEntityBuilder
.HasOne<Team>()
.WithMany()
.HasForeignKey(e => e.TeamId);
.HasForeignKey(invitation => invitation.TeamId);

invitationEntityBuilder
.Property<byte[]>("RowVersion")
.IsRowVersion();
.HasIndex(invitation => invitation.RecipientId);

invitationEntityBuilder
.HasIndex(invitation => invitation.TeamId);

invitationEntityBuilder
.HasIndex(invitation => new { invitation.TeamId, invitation.RecipientId })
.IsUnique();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,10 @@ protected override void ConfigureEntity(EntityTypeBuilder<TeamMember> teamMember
.HasMaxLength(255);

teamMemberEntityBuilder
.Property<byte[]>("RowVersion")
.HasIndex(team => team.TeamId);

teamMemberEntityBuilder
.Property<uint>("RowVersion")
.IsRowVersion();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ protected override void ConfigureEntity(EntityTypeBuilder<User> userEntityBuilde
.OnDelete(DeleteBehavior.Cascade);

userEntityBuilder
.Property<byte[]>("RowVersion")
.Property<uint>("RowVersion")
.IsRowVersion();
}
}

This file was deleted.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
using Microsoft.EntityFrameworkCore.Migrations;

#nullable disable

namespace TeamUp.Infrastructure.Persistence.Migrations
{
/// <inheritdoc />
public partial class FixConcurrencyAndIndexes : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<uint>(
name: "xmin",
table: "Users",
type: "xid",
rowVersion: true,
nullable: false,
defaultValue: 0u);

migrationBuilder.AddColumn<uint>(
name: "xmin",
table: "TeamMember",
type: "xid",
rowVersion: true,
nullable: false,
defaultValue: 0u);

migrationBuilder.AddColumn<uint>(
name: "xmin",
table: "Events",
type: "xid",
rowVersion: true,
nullable: false,
defaultValue: 0u);

migrationBuilder.CreateIndex(
name: "IX_Invitations_TeamId_RecipientId",
table: "Invitations",
columns: new[] { "TeamId", "RecipientId" },
unique: true);

migrationBuilder.CreateIndex(
name: "IX_EventResponse_EventId_TeamMemberId",
table: "EventResponse",
columns: new[] { "EventId", "TeamMemberId" },
unique: true);
}

/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropIndex(
name: "IX_Invitations_TeamId_RecipientId",
table: "Invitations");

migrationBuilder.DropIndex(
name: "IX_EventResponse_EventId_TeamMemberId",
table: "EventResponse");

migrationBuilder.DropColumn(
name: "xmin",
table: "Users");

migrationBuilder.DropColumn(
name: "xmin",
table: "TeamMember");

migrationBuilder.DropColumn(
name: "xmin",
table: "Events");
}
}
}
Loading

0 comments on commit d0d22d8

Please sign in to comment.