534b0cde91
Add logic to initialize at least one user if the startup wizard is not completed, both during application startup and in the startup user API. After user creation, reload the user entity with all related navigation properties to ensure the in-memory user object is fully populated. Also update build and publish metadata files.
325 lines
9.5 KiB
Markdown
325 lines
9.5 KiB
Markdown
# Repository Async Conversion Checklist
|
|
|
|
Use this checklist when converting a repository class from synchronous to asynchronous operations.
|
|
|
|
## 📋 Pre-Conversion Checklist
|
|
|
|
- [ ] **Identify all public methods** in the repository interface
|
|
- [ ] **Check for method dependencies** - what calls these methods?
|
|
- [ ] **Review current tests** - ensure you understand expected behavior
|
|
- [ ] **Create a feature branch** for the conversion
|
|
- [ ] **Document breaking changes** for consumers
|
|
|
|
## 🔄 Conversion Steps
|
|
|
|
### Step 1: Update Interface Definition
|
|
|
|
**File**: `MediaBrowser.Controller\Persistence\I[RepositoryName].cs`
|
|
|
|
- [ ] Add `using System.Threading.Tasks;`
|
|
- [ ] Add `CancellationToken` parameter to all methods
|
|
- [ ] Change return types:
|
|
- `void` → `Task`
|
|
- `T` → `Task<T>`
|
|
- `IEnumerable<T>` → `IAsyncEnumerable<T>` (for streaming) or `Task<List<T>>`
|
|
- [ ] Rename methods to include `Async` suffix
|
|
- [ ] Add XML documentation for cancellation token
|
|
|
|
**Example**:
|
|
```csharp
|
|
// BEFORE
|
|
void SaveUser(User user);
|
|
User? GetUser(Guid id);
|
|
IEnumerable<User> GetAllUsers();
|
|
|
|
// AFTER
|
|
Task SaveUserAsync(User user, CancellationToken cancellationToken = default);
|
|
Task<User?> GetUserAsync(Guid id, CancellationToken cancellationToken = default);
|
|
Task<List<User>> GetAllUsersAsync(CancellationToken cancellationToken = default);
|
|
// OR for streaming:
|
|
IAsyncEnumerable<User> GetAllUsersAsync(CancellationToken cancellationToken = default);
|
|
```
|
|
|
|
### Step 2: Update Implementation
|
|
|
|
**File**: `[Project]\[RepositoryName].cs`
|
|
|
|
#### 2.1 Method Signatures
|
|
- [ ] Add `async` modifier to method declarations
|
|
- [ ] Change return type to `Task` or `Task<T>`
|
|
- [ ] Add `CancellationToken` parameter
|
|
|
|
#### 2.2 Database Context Usage
|
|
- [ ] Change `using` to `await using` for DbContext
|
|
```csharp
|
|
// BEFORE: using var context = _dbProvider.CreateDbContext();
|
|
// AFTER: await using var context = _dbProvider.CreateDbContext();
|
|
```
|
|
|
|
#### 2.3 Transaction Handling
|
|
- [ ] `BeginTransaction()` → `BeginTransactionAsync(cancellationToken)`
|
|
- [ ] `Commit()` → `CommitAsync(cancellationToken)`
|
|
- [ ] `Rollback()` → `RollbackAsync(cancellationToken)`
|
|
- [ ] Change `using` to `await using` for transactions
|
|
|
|
#### 2.4 Query Operations
|
|
- [ ] `ToList()` → `ToListAsync(cancellationToken)`
|
|
- [ ] `ToArray()` → `ToArrayAsync(cancellationToken)`
|
|
- [ ] `ToDictionary()` → `ToDictionaryAsync(cancellationToken)`
|
|
- [ ] `FirstOrDefault()` → `FirstOrDefaultAsync(cancellationToken)`
|
|
- [ ] `First()` → `FirstAsync(cancellationToken)`
|
|
- [ ] `SingleOrDefault()` → `SingleOrDefaultAsync(cancellationToken)`
|
|
- [ ] `Single()` → `SingleAsync(cancellationToken)`
|
|
- [ ] `Any()` → `AnyAsync(cancellationToken)`
|
|
- [ ] `Count()` → `CountAsync(cancellationToken)`
|
|
- [ ] `LongCount()` → `LongCountAsync(cancellationToken)`
|
|
- [ ] `Sum()` → `SumAsync(cancellationToken)`
|
|
- [ ] `Average()` → `AverageAsync(cancellationToken)`
|
|
- [ ] `Min()` → `MinAsync(cancellationToken)`
|
|
- [ ] `Max()` → `MaxAsync(cancellationToken)`
|
|
|
|
#### 2.5 Command Operations
|
|
- [ ] `SaveChanges()` → `SaveChangesAsync(cancellationToken)`
|
|
- [ ] `ExecuteDelete()` → `ExecuteDeleteAsync(cancellationToken)`
|
|
- [ ] `ExecuteUpdate()` → `ExecuteUpdateAsync(cancellationToken)`
|
|
- [ ] `Add()` - no async version, but SaveChanges must be async
|
|
- [ ] `Remove()` - no async version, but SaveChanges must be async
|
|
- [ ] `Update()` - no async version, but SaveChanges must be async
|
|
|
|
#### 2.6 Navigation Properties
|
|
- [ ] `.Load()` → `.LoadAsync(cancellationToken)`
|
|
- [ ] `.Entry().Collection().Load()` → `.Entry().Collection().LoadAsync(cancellationToken)`
|
|
- [ ] `.Include()` - works with async, no change needed
|
|
|
|
#### 2.7 ForEach Loops
|
|
- [ ] Convert to `await foreach` for IAsyncEnumerable
|
|
```csharp
|
|
// BEFORE
|
|
foreach (var item in items)
|
|
{
|
|
// process
|
|
}
|
|
|
|
// AFTER
|
|
await foreach (var item in items.WithCancellation(cancellationToken))
|
|
{
|
|
// process
|
|
}
|
|
```
|
|
|
|
#### 2.8 Helper Methods
|
|
- [ ] Convert all private helper methods that use database to async
|
|
- [ ] Propagate async/await through call chain
|
|
|
|
### Step 3: Update Consumers
|
|
|
|
#### 3.1 Service Layer
|
|
- [ ] Update service interfaces to use `Task` return types
|
|
- [ ] Update service implementations to use `await`
|
|
- [ ] Add `CancellationToken` parameters
|
|
|
|
#### 3.2 API Controllers
|
|
- [ ] Add `async Task<ActionResult>` to controller methods
|
|
- [ ] Add `CancellationToken` parameter (ASP.NET Core will inject it)
|
|
- [ ] Use `await` when calling services
|
|
|
|
**Example**:
|
|
```csharp
|
|
[HttpGet("{id}")]
|
|
public async Task<ActionResult<UserDto>> GetUser(
|
|
[FromRoute] Guid id,
|
|
CancellationToken cancellationToken)
|
|
{
|
|
var user = await _userRepository.GetUserAsync(id, cancellationToken);
|
|
return user is null ? NotFound() : Ok(_mapper.Map<UserDto>(user));
|
|
}
|
|
```
|
|
|
|
#### 3.3 Background Services
|
|
- [ ] Ensure `ExecuteAsync` properly awaits all operations
|
|
- [ ] Pass cancellation token through the chain
|
|
|
|
### Step 4: Update Tests
|
|
|
|
#### 4.1 Test Method Signatures
|
|
- [ ] Change test methods to `async Task`
|
|
- [ ] Add `await` before repository calls
|
|
|
|
#### 4.2 Mock Setup
|
|
- [ ] Update mocks to return `Task` or `Task<T>`
|
|
```csharp
|
|
// BEFORE
|
|
_mockRepository.Setup(x => x.GetUser(It.IsAny<Guid>()))
|
|
.Returns(user);
|
|
|
|
// AFTER
|
|
_mockRepository.Setup(x => x.GetUserAsync(It.IsAny<Guid>(), It.IsAny<CancellationToken>()))
|
|
.ReturnsAsync(user);
|
|
```
|
|
|
|
#### 4.3 Test Assertions
|
|
- [ ] Use `await` for async operations in assertions
|
|
- [ ] Test cancellation scenarios
|
|
|
|
**Example Test**:
|
|
```csharp
|
|
[Fact]
|
|
public async Task GetUserAsync_ValidId_ReturnsUser()
|
|
{
|
|
// Arrange
|
|
var userId = Guid.NewGuid();
|
|
var expectedUser = new User { Id = userId };
|
|
_mockRepository
|
|
.Setup(x => x.GetUserAsync(userId, It.IsAny<CancellationToken>()))
|
|
.ReturnsAsync(expectedUser);
|
|
|
|
// Act
|
|
var result = await _service.GetUserAsync(userId, CancellationToken.None);
|
|
|
|
// Assert
|
|
Assert.NotNull(result);
|
|
Assert.Equal(userId, result.Id);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task GetUserAsync_Cancelled_ThrowsOperationCanceledException()
|
|
{
|
|
// Arrange
|
|
var cts = new CancellationTokenSource();
|
|
cts.Cancel();
|
|
|
|
// Act & Assert
|
|
await Assert.ThrowsAsync<OperationCanceledException>(
|
|
() => _service.GetUserAsync(Guid.NewGuid(), cts.Token)
|
|
);
|
|
}
|
|
```
|
|
|
|
## 🔍 Code Review Checklist
|
|
|
|
Before submitting PR:
|
|
|
|
- [ ] All synchronous database operations converted
|
|
- [ ] `CancellationToken` passed through entire call chain
|
|
- [ ] No `Task.Result` or `.Wait()` calls (causes deadlocks)
|
|
- [ ] No `async void` methods (except event handlers)
|
|
- [ ] Proper `ConfigureAwait(false)` usage in library code (optional in ASP.NET Core)
|
|
- [ ] All tests updated and passing
|
|
- [ ] No performance regressions
|
|
- [ ] Memory usage acceptable
|
|
- [ ] Documentation updated
|
|
|
|
## ⚡ Performance Optimization Opportunities
|
|
|
|
Consider these optimizations during conversion:
|
|
|
|
### 1. Parallel Operations
|
|
When operations are independent, use `Task.WhenAll`:
|
|
|
|
```csharp
|
|
// Sequential (slow)
|
|
await DeleteAncestorsAsync(id, cancellationToken);
|
|
await DeleteImagesAsync(id, cancellationToken);
|
|
await DeleteChaptersAsync(id, cancellationToken);
|
|
|
|
// Parallel (fast)
|
|
await Task.WhenAll(
|
|
DeleteAncestorsAsync(id, cancellationToken),
|
|
DeleteImagesAsync(id, cancellationToken),
|
|
DeleteChaptersAsync(id, cancellationToken)
|
|
);
|
|
```
|
|
|
|
### 2. Streaming Results
|
|
For large result sets, use `IAsyncEnumerable<T>`:
|
|
|
|
```csharp
|
|
public async IAsyncEnumerable<User> GetAllUsersAsync(
|
|
[EnumeratorCancellation] CancellationToken cancellationToken = default)
|
|
{
|
|
await using var context = _dbProvider.CreateDbContext();
|
|
|
|
await foreach (var user in context.Users.AsAsyncEnumerable()
|
|
.WithCancellation(cancellationToken))
|
|
{
|
|
yield return user;
|
|
}
|
|
}
|
|
```
|
|
|
|
### 3. Batch Operations
|
|
Use `ExecuteUpdateAsync` / `ExecuteDeleteAsync` instead of loading then saving:
|
|
|
|
```csharp
|
|
// Slow - loads all into memory
|
|
var items = await context.Items.Where(x => x.IsOld).ToListAsync();
|
|
context.Items.RemoveRange(items);
|
|
await context.SaveChangesAsync();
|
|
|
|
// Fast - executes in database
|
|
await context.Items
|
|
.Where(x => x.IsOld)
|
|
.ExecuteDeleteAsync(cancellationToken);
|
|
```
|
|
|
|
## 🐛 Common Pitfalls to Avoid
|
|
|
|
### ❌ DON'T: Use Task.Result or .Wait()
|
|
```csharp
|
|
// WRONG - causes deadlocks
|
|
var user = GetUserAsync(id).Result;
|
|
```
|
|
|
|
### ❌ DON'T: Forget cancellation token
|
|
```csharp
|
|
// WRONG - can't cancel long-running operations
|
|
public async Task<User> GetUserAsync(Guid id)
|
|
{
|
|
return await context.Users.FirstOrDefaultAsync(x => x.Id == id);
|
|
}
|
|
```
|
|
|
|
### ❌ DON'T: Use async void
|
|
```csharp
|
|
// WRONG - exceptions can't be caught
|
|
public async void SaveUser(User user) { }
|
|
```
|
|
|
|
### ✅ DO: Proper async pattern
|
|
```csharp
|
|
// CORRECT
|
|
public async Task<User?> GetUserAsync(Guid id, CancellationToken cancellationToken = default)
|
|
{
|
|
await using var context = _dbProvider.CreateDbContext();
|
|
return await context.Users
|
|
.FirstOrDefaultAsync(x => x.Id == id, cancellationToken);
|
|
}
|
|
```
|
|
|
|
## 📝 Commit Message Template
|
|
|
|
```
|
|
feat(repository): Convert [RepositoryName] to async
|
|
|
|
- Updated I[RepositoryName] interface with async signatures
|
|
- Converted all database operations to async/await
|
|
- Added CancellationToken support throughout
|
|
- Updated all tests to async
|
|
- Updated consumers in service and controller layers
|
|
|
|
BREAKING CHANGE: All [RepositoryName] methods are now async and require await.
|
|
Migration guide: [link to documentation]
|
|
|
|
Closes #[issue-number]
|
|
```
|
|
|
|
## 🎯 Success Criteria
|
|
|
|
- [ ] All tests passing
|
|
- [ ] No compiler warnings
|
|
- [ ] Code coverage maintained or improved
|
|
- [ ] No performance regressions
|
|
- [ ] Documentation updated
|
|
- [ ] Migration guide provided
|
|
- [ ] Breaking changes documented
|