Submission for March Pull Request#9
Conversation
… deleted, completed tests
Lab3 to Main
Run tests without building again.
Run tests without building again.
Included connection strings in environment of integration tests to remove them from app_settings.
Merge Lab3 to Main
| using (_logger.BeginScope(new Dictionary<string, object> | ||
| { | ||
| {"ConversationId", conversationId}, | ||
| {"SenderUsername", request.SenderUsername} |
| { | ||
| try | ||
| { | ||
| SendMessageResponse response = await _messageService.AddMessage(conversationId, false, request); |
There was a problem hiding this comment.
it's not clear what the false represents here, use a named parameter to make it obvious.
| try | ||
| { | ||
| var result = await _imageService.DownloadImage(imageId); | ||
| _logger.LogInformation("Downloaded image with id {id}.", imageId); |
There was a problem hiding this comment.
remove, only log info for things added / updated, not needed for get requests
| try | ||
| { | ||
| var profile = await _profileService.GetProfile(username); | ||
| _logger.LogInformation("Profile of {Username} fetched.", username); |
| [HttpPost] | ||
| public async Task<ActionResult<Profile>> PostProfile(Profile profile) | ||
| { | ||
| using (_logger.BeginScope("{Profile}", profile)) |
| "iisSettings": { | ||
| "windowsAuthentication": false, | ||
| "anonymousAuthentication": true, | ||
| "iisExpress": { | ||
| "applicationUrl": "http://localhost:61550", | ||
| "sslPort": 44359 | ||
| } | ||
| }, |
| "IIS Express": { | ||
| "commandName": "IISExpress", | ||
| "launchBrowser": true, | ||
| "launchUrl": "swagger", | ||
| "environmentVariables": { | ||
| "ASPNETCORE_ENVIRONMENT": "Development" | ||
| } | ||
| } |
| public interface IImageService | ||
| { | ||
| Task<UploadImageServiceResult> UploadImage(Image image); | ||
| Task<FileContentResult> DownloadImage(string imageId); |
There was a problem hiding this comment.
FileContentResult is the controller business, the service layer should not be aware that it is being called from a controller.
|
|
||
| public async Task<SendMessageResponse> AddFirstMessage(string conversationId, SendMessageRequest request) | ||
| { | ||
| return await AddMessage(conversationId, true, request); |
There was a problem hiding this comment.
use a named parameter to make it easy to read what the true represents
| ValidateConversationId(conversationId); | ||
| ValidateLimit(limit); | ||
| ValidateLastSeenConversationTime(lastSeenConversationTime); |
There was a problem hiding this comment.
Good job on using private methods to improve readability
| var result = await _messageStore.GetMessages( | ||
| conversationId, limit, orderBy, continuationToken, lastSeenConversationTime); | ||
|
|
||
| return new GetMessagesServiceResult |
There was a problem hiding this comment.
GetMessagesResult. If you have another one in the controller with the same name, the controller object should be called GetMessagesResponse
| await _imageService.DeleteImage(profile.ProfilePictureId); | ||
| await _profileStore.DeleteProfile(username); |
There was a problem hiding this comment.
Do it in parallel:
await Task.WhenAll(
_imageService.DeleteImage(profile.ProfilePictureId),
_profileStore.DeleteProfile(username)
);|
|
||
| public class UserConversationService : IUserConversationService | ||
| { | ||
|
|
| if (!await _profileService.ProfileExists(username1)) | ||
| { | ||
| throw new ProfileNotFoundException($"A profile with the username {username1} was not found."); | ||
| } | ||
|
|
||
| if (!await _profileService.ProfileExists(username2)) | ||
| { | ||
| throw new ProfileNotFoundException($"A profile with the username {username2} was not found."); | ||
| } |
There was a problem hiding this comment.
await Task.WhenAll(request.Participants.Select(username => ThrowIfParticipantNotFound(username)))and this can be done in a private function:
await EnsureThatParticipantsExist(request.Participants);
| { | ||
| await AddMultipleUserConversations(_userConversation1, _userConversation2, _userConversation3); | ||
|
|
||
| var response = await _userConversationStore.GetUserConversations(_userConversation.Username, 1, OrderBy.ASC, null, 1); |
There was a problem hiding this comment.
please always use named parameters when it's not clear what the parameter is. In this case, it's hard to guess what 1, null and 1 are without looking at the implementation
| await AddMultipleUserConversations(_userConversation1, _userConversation2, _userConversation3); | ||
|
|
||
| var response = await _userConversationStore.GetUserConversations(_userConversation.Username, 1, OrderBy.ASC, null, 1); | ||
| Assert.Equal(1, response.UserConversations.Count); |
There was a problem hiding this comment.
I believe you can use Assert.Single in this case
| response = await _userConversationStore.GetUserConversations(_userConversation.Username, 3, OrderBy.ASC, null, 1); | ||
| Assert.Equal(3, response.UserConversations.Count); | ||
|
|
||
| await DeleteMultipleUserConversations(_userConversation1, _userConversation2, _userConversation3); |
There was a problem hiding this comment.
this should be done in the cleanup not here because cleanup is executed even if the test fails halfway through
| if(_userConversation1.LastModifiedTime > lastSeenConversationTime) { userConversationsExpected.Add(_userConversation1); } | ||
| if(_userConversation2.LastModifiedTime > lastSeenConversationTime) { userConversationsExpected.Add(_userConversation2);} | ||
| if(_userConversation3.LastModifiedTime > lastSeenConversationTime) { userConversationsExpected.Add(_userConversation3);} | ||
| if(_userConversation.LastModifiedTime > lastSeenConversationTime) { userConversationsExpected.Add(_userConversation);} |
There was a problem hiding this comment.
if you had a list of conversation you can do this in a for loop instead of repeating 4 times the same code
|
|
||
| public async Task DisposeAsync() | ||
| { | ||
| await _userConversationStore.DeleteUserConversation(_userConversation.Username, _userConversation.ConversationId); |
There was a problem hiding this comment.
this cleanup can be used to delete all conversations, if you put them in a list
| UserConversation userConversation1 = new UserConversation | ||
| { | ||
| Username = username1, | ||
| ConversationId = conversationId, | ||
| LastModifiedTime = unixTimeNow | ||
| }; | ||
| await _userConversationStore.CreateUserConversation(userConversation1); | ||
|
|
||
| UserConversation userConversation2 = new UserConversation | ||
| { | ||
| Username = username2, | ||
| ConversationId = conversationId, | ||
| LastModifiedTime = unixTimeNow | ||
| }; | ||
| await _userConversationStore.CreateUserConversation(userConversation2); |
There was a problem hiding this comment.
await Task.WhenAll(
CreateUserConversation(username1, conversationId, unixTimeNow),
CreateUserConversation(username2, conversationId, unixTimeNow),
)Faster and easier on the eye :)
|
|
||
| foreach (UserConversation userConversation in userConversations) | ||
| { | ||
| string[] usernames = userConversation.ConversationId.Split('_'); |
There was a problem hiding this comment.
The "_" separator is spread in multiple places. If we decide to change it, we have to change multiple places in the code, which is not ideal. This separator should live in one place and be hidden from the rest of the application.
| UserConversation userConversation1 = new UserConversation | ||
| { | ||
| Username = username1, | ||
| ConversationId = conversationId, | ||
| LastModifiedTime = unixTimeNow | ||
| }; |
There was a problem hiding this comment.
There is a conceptual issue here. The UserConversation object doesn't say who is the other user involved in the conversation but the storage layer is not aware that the conversationId is a concatenation of usernames. I think you have two choices here:
1- The storage layer would own the knowledge of the separator being used and hide it from the rest of the application. You would pass in both usernames and ask the storage layer to create a conversation. The storage layer would create the conversationId using the concatenation and store the user conversation. The getUserConversations would return UserConversation list where each UserConversation has both participants. That way, the storage layer completely encapsulates the separator logic.
2- The service layer owns the separator logic. In this case, we should still pass both usernames and have them stored in the database because the storage layer doesn't know that the conversation id is a concatenations of usernames.
You current solution is a mix of the two approaches above, which is problematic because the logic is distributed in multiple places and the storage layer depends on the service layer indirectly (and vise versa).
| recipientUsername = usernames[0]; | ||
| } | ||
|
|
||
| Profile recipientProfile = await _profileService.GetProfile(recipientUsername); |
There was a problem hiding this comment.
every time you have multiple await calls, you should consider doing them in parallel if possible
| partitionKey: new PartitionKey(username), | ||
| new ItemRequestOptions | ||
| { | ||
| ConsistencyLevel = ConsistencyLevel.Session |
There was a problem hiding this comment.
move this to a private field instead of repeating every time, to make it easier to change the consistency level if needed (instead of having to change in multiple places).
| try | ||
| { | ||
| IQueryable<UserConversationEntity> query = Container | ||
| .GetItemLinqQueryable<UserConversationEntity>(false, continuationToken, options) |
There was a problem hiding this comment.
named parameter to show what the false is
| { | ||
| Task AddMessage(string conversationId, Message message); | ||
| Task<Message> GetMessage(string conversationId, string messageId); | ||
| Task<(List<Message> Messages, string NextContinuationToken)> GetMessages( |
There was a problem hiding this comment.
This is what mine looks like:
Task<GetMessagesResult> GetMessages(string conversationId, GetMessagesParameters parameters);this works too:
Task<GetMessagesResult> GetMessages(GetMessagesParameters parameters);Your signature is a bit too verbose.
| public interface IProfileStore | ||
| { | ||
| Task AddProfile(Profile profile); | ||
| Task<Profile?> GetProfile(string username); |
There was a problem hiding this comment.
I noticed that in other store classes your throwing an exception if the entity doesn't exist. You may want to choose one approach instead of mixing. Both are fine but nullable is a bit cleaner in this case.
| { | ||
| Task CreateUserConversation(UserConversation userConversation); | ||
| Task<UserConversation> GetUserConversation(string username, string conversationId); | ||
| Task<(List<UserConversation> UserConversations, string NextContinuationToken)> GetUserConversations( |
There was a problem hiding this comment.
Similar to messages, this signature can be simplified
| @@ -0,0 +1,14 @@ | |||
| namespace ChatService.Web.Utilities; | |||
|
|
|||
| public class ConversationIdUtilities | |||
There was a problem hiding this comment.
This class should be also responsible of splitting and should be the only class aware of the "_" separator
nehmebilal
left a comment
There was a problem hiding this comment.
Alright enough comments for this one :)
Looks pretty good overall, good job! Please make sure to address the comments and apply them to other similar cases in the code. I will review the changes in your next PR.
No description provided.