| | 1 | = Advanced Application Development = |
| | 2 | |
| | 3 | == Transactions == |
| | 4 | |
| | 5 | === User Registration === |
| | 6 | |
| | 7 | {{{ |
| | 8 | public async Task<RegisterResponse> Handle(RegisterRequest request, CancellationToken cancellationToken) |
| | 9 | { |
| | 10 | var existing = await _userRepository.GetByEmailAsync(request.Email, cancellationToken); |
| | 11 | if (existing != null) |
| | 12 | throw new InvalidOperationException("Email already in use."); |
| | 13 | |
| | 14 | await using var transaction = await _context.BeginTransactionAsync(cancellationToken); |
| | 15 | try |
| | 16 | { |
| | 17 | var user = new Domain.Entities.User |
| | 18 | { |
| | 19 | Username = request.Username, |
| | 20 | Email = request.Email, |
| | 21 | Name = request.Name, |
| | 22 | Surname = request.Surname, |
| | 23 | Password = BCrypt.Net.BCrypt.HashPassword(request.Password), |
| | 24 | CreatedAt = DateTime.UtcNow, |
| | 25 | UpdatedAt = DateTime.UtcNow |
| | 26 | }; |
| | 27 | |
| | 28 | await _userRepository.AddAsync(user, cancellationToken); |
| | 29 | |
| | 30 | var writer = new Domain.Entities.Writer { Id = user.Id }; |
| | 31 | await _writerRepository.AddAsync(writer, cancellationToken); |
| | 32 | |
| | 33 | await transaction.CommitAsync(cancellationToken); |
| | 34 | return new RegisterResponse(user.Id, user.Username, user.Email); |
| | 35 | } |
| | 36 | catch |
| | 37 | { |
| | 38 | await transaction.RollbackAsync(cancellationToken); |
| | 39 | throw; |
| | 40 | } |
| | 41 | } |
| | 42 | }}} |
| | 43 | |
| | 44 | === Publishing a Story === |
| | 45 | |
| | 46 | {{{ |
| | 47 | public async Task<AddResponse> Handle(AddRequest request, CancellationToken cancellationToken) |
| | 48 | { |
| | 49 | await using var transaction = await _context.BeginTransactionAsync(cancellationToken); |
| | 50 | try |
| | 51 | { |
| | 52 | var story = new Domain.Entities.Story |
| | 53 | { |
| | 54 | MatureContent = request.MatureContent, |
| | 55 | ShortDescription = request.ShortDescription, |
| | 56 | Image = request.Image, |
| | 57 | Content = request.Content, |
| | 58 | UserId = request.UserId, |
| | 59 | CreatedAt = DateTime.UtcNow, |
| | 60 | UpdatedAt = DateTime.UtcNow |
| | 61 | }; |
| | 62 | |
| | 63 | await _storyRepository.AddAsync(story, cancellationToken); |
| | 64 | |
| | 65 | foreach (var genreName in request.Genres ?? []) |
| | 66 | { |
| | 67 | var genre = await _genreRepository.GetByNameAsync(genreName, cancellationToken); |
| | 68 | if (genre == null) continue; |
| | 69 | await _hasGenreRepository.AddAsync(new Domain.Entities.HasGenre { StoryId = story.Id, GenreId = genre.Id }, cancellationToken); |
| | 70 | } |
| | 71 | |
| | 72 | await transaction.CommitAsync(cancellationToken); |
| | 73 | return new AddResponse(story.Id); |
| | 74 | } |
| | 75 | catch |
| | 76 | { |
| | 77 | await transaction.RollbackAsync(cancellationToken); |
| | 78 | throw; |
| | 79 | } |
| | 80 | } |
| | 81 | }}} |
| | 82 | |
| | 83 | == Database Connection Pooling == |
| | 84 | |
| | 85 | Because we use .NET 9 with EF Core and Npgsql for the backend, we do not manage database connections manually. Connection pooling in Npgsql is handled automatically by the driver. |
| | 86 | |
| | 87 | Npgsql is a transitive dependency of `Npgsql.EntityFrameworkCore.PostgreSQL`, so adding the following to the project is sufficient: |
| | 88 | |
| | 89 | {{{ |
| | 90 | <PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="9.0.4" /> |
| | 91 | }}} |
| | 92 | |
| | 93 | The EF Core context is registered using `AddDbContextPool` instead of `AddDbContext`, which enables context instance pooling on top of Npgsql's own connection pool: |
| | 94 | |
| | 95 | {{{ |
| | 96 | services.AddDbContextPool<ApplicationDbContext>(options => |
| | 97 | options.UseNpgsql(connectionString)); |
| | 98 | }}} |
| | 99 | |
| | 100 | The pool parameters are read from `appsettings.json` at startup and appended to the Npgsql connection string: |
| | 101 | |
| | 102 | {{{ |
| | 103 | "ConnectionPool": { |
| | 104 | "MinPoolSize": 1, |
| | 105 | "MaxPoolSize": 20, |
| | 106 | "ConnectionIdleLifetime": 300, |
| | 107 | "ConnectionPruningInterval": 10, |
| | 108 | "CommandTimeout": 30, |
| | 109 | "Timeout": 15 |
| | 110 | } |
| | 111 | }}} |
| | 112 | |
| | 113 | These values can be changed as needed. The established connections can be observed in the SSH tunnel logs when the application starts: |
| | 114 | |
| | 115 | {{{ |
| | 116 | debug1: Connection to port 9999 forwarding to localhost port 5432 requested. |
| | 117 | debug1: channel 2: new direct-tcpip [direct-tcpip] (inactive timeout: 0) |
| | 118 | debug1: Connection to port 9999 forwarding to localhost port 5432 requested. |
| | 119 | debug1: channel 3: new direct-tcpip [direct-tcpip] (inactive timeout: 0) |
| | 120 | debug1: Connection to port 9999 forwarding to localhost port 5432 requested. |
| | 121 | debug1: channel 4: new direct-tcpip [direct-tcpip] (inactive timeout: 0) |
| | 122 | debug1: Connection to port 9999 forwarding to localhost port 5432 requested. |
| | 123 | debug1: channel 5: new direct-tcpip [direct-tcpip] (inactive timeout: 0) |
| | 124 | debug1: Connection to port 9999 forwarding to localhost port 5432 requested. |
| | 125 | debug1: channel 6: new direct-tcpip [direct-tcpip] (inactive timeout: 0) |
| | 126 | debug1: Connection to port 9999 forwarding to localhost port 5432 requested. |
| | 127 | debug1: channel 7: new direct-tcpip [direct-tcpip] (inactive timeout: 0) |
| | 128 | debug1: Connection to port 9999 forwarding to localhost port 5432 requested. |
| | 129 | debug1: channel 8: new direct-tcpip [direct-tcpip] (inactive timeout: 0) |
| | 130 | debug1: Connection to port 9999 forwarding to localhost port 5432 requested. |
| | 131 | debug1: channel 9: new direct-tcpip [direct-tcpip] (inactive timeout: 0) |
| | 132 | debug1: Connection to port 9999 forwarding to localhost port 5432 requested. |
| | 133 | debug1: channel 10: new direct-tcpip [direct-tcpip] (inactive timeout: 0) |
| | 134 | debug1: Connection to port 9999 forwarding to localhost port 5432 requested. |
| | 135 | debug1: channel 11: new direct-tcpip [direct-tcpip] (inactive timeout: 0) |
| | 136 | }}} |