Overview
Utility services provide supporting functionality for the AndanDo platform including review management, like/favorite system, and currency conversion.
ReviewService
Overview
Manages service reviews and ratings for tours.
public interface IReviewService
Location: ~/workspace/source/AndanDo/Services/Utility/ReviewService.cs
AddReviewAsync
Adds a review for a tour service.
Task AddReviewAsync (
int serviceId ,
int userId ,
int rating ,
string comment ,
CancellationToken cancellationToken = default )
The tour/service ID being reviewed
User ID submitting the review
Review comment text (max 2000 characters)
try
{
await reviewService . AddReviewAsync (
serviceId : 42 ,
userId : 123 ,
rating : 5 ,
comment : "Amazing tour! The guide was knowledgeable and the views were breathtaking."
);
Console . WriteLine ( "Review submitted successfully" );
}
catch ( ArgumentException ex )
{
Console . WriteLine ( $"Validation error: { ex . Message } " );
}
Validation : The method validates:
serviceId must be greater than 0
userId must be greater than 0
rating must be between 1 and 5
comment must not be empty or whitespace
Throws ArgumentException or ArgumentOutOfRangeException if validation fails.
Reviews are automatically set as visible (EsVisible = true) and timestamped with UTC time.
GetReviewsAsync
Retrieves visible reviews for a service.
Task < IReadOnlyList < ServiceReviewDto >> GetReviewsAsync (
int serviceId ,
int top = 50 ,
CancellationToken cancellationToken = default )
Maximum number of reviews to return
reviews
IReadOnlyList<ServiceReviewDto>
List of reviews ordered by creation date (most recent first) Show ServiceReviewDto properties
ReviewId (int) - Review identifier
ServiceId (int) - Tour/service ID
UsuarioId (int) - User ID of reviewer
Rating (int) - Star rating (1-5)
Comentario (string) - Review comment
EsVisible (bool) - Visibility flag
FechaCreacion (DateTime?) - Creation timestamp
FechaEdicion (DateTime?) - Last edit timestamp
Nombre (string?) - Reviewer first name
Apellido (string?) - Reviewer last name
FotoPerfilUrl (string?) - Reviewer profile photo URL
var reviews = await reviewService . GetReviewsAsync (
serviceId : 42 ,
top : 10
);
foreach ( var review in reviews )
{
var stars = new string ( '⭐' , review . Rating );
Console . WriteLine ( $" { stars } - { review . Nombre } { review . Apellido } " );
Console . WriteLine ( $" { review . Comentario } " );
Console . WriteLine ( $"Posted: { review . FechaCreacion : g } " );
Console . WriteLine ();
}
Only reviews marked as visible (EsVisible = true) are returned. This allows moderation of inappropriate content.
GetOwnerRatingAsync
Calculates aggregate rating statistics for all tours owned by a user.
Task <( double ? AvgRating , int RatingCount )> GetOwnerRatingAsync (
int ownerUserId ,
CancellationToken cancellationToken = default )
Average rating across all tours (null if no ratings)
Total number of ratings received
var ( avgRating , count ) = await reviewService . GetOwnerRatingAsync (
ownerUserId : 123
);
if ( avgRating . HasValue )
{
Console . WriteLine ( $"Host Rating: { avgRating . Value : F2 } ⭐ ( { count } reviews)" );
}
else
{
Console . WriteLine ( "No reviews yet" );
}
PostLikeService
Overview
Manages user likes/favorites for tours.
public interface IPostLikeService
Location: ~/workspace/source/AndanDo/Services/Utility/PostLikeService.cs
HasUserLikedAsync
Checks if a user has liked a specific tour.
Task < bool > HasUserLikedAsync (
int userId ,
int postId ,
CancellationToken cancellationToken = default )
Returns true if user has liked the tour, false otherwise
bool hasLiked = await postLikeService . HasUserLikedAsync (
userId : 123 ,
postId : 42
);
var buttonText = hasLiked ? "❤️ Liked" : "🤍 Like" ;
Returns false if either userId or postId is invalid (less than or equal to 0).
AddLikeAsync
Adds a like for a tour.
Task AddLikeAsync (
int userId ,
int postId ,
CancellationToken cancellationToken = default )
try
{
await postLikeService . AddLikeAsync (
userId : 123 ,
postId : 42
);
Console . WriteLine ( "Tour added to favorites" );
}
catch ( ArgumentException ex )
{
Console . WriteLine ( $"Invalid input: { ex . Message } " );
}
The method is idempotent - if the user has already liked the tour, it won’t create a duplicate entry.
RemoveLikeAsync
Removes a like from a tour.
Task RemoveLikeAsync (
int userId ,
int postId ,
CancellationToken cancellationToken = default )
User ID removing the like
public async Task ToggleLikeAsync ( int userId , int postId )
{
bool hasLiked = await postLikeService . HasUserLikedAsync ( userId , postId );
if ( hasLiked )
{
await postLikeService . RemoveLikeAsync ( userId , postId );
Console . WriteLine ( "Removed from favorites" );
}
else
{
await postLikeService . AddLikeAsync ( userId , postId );
Console . WriteLine ( "Added to favorites" );
}
}
GetUserLikedToursAsync
Retrieves all tours liked by a user.
Task < IReadOnlyList < LikedTourSummaryDto >> GetUserLikedToursAsync (
int userId ,
CancellationToken cancellationToken = default )
likedTours
IReadOnlyList<LikedTourSummaryDto>
List of liked tours with summary information Show LikedTourSummaryDto properties
TourId (int) - Tour identifier
Title (string) - Tour title
LocationLabel (string) - Location
FirstImageUrl (string?) - First image URL
DurationLabel (string?) - Duration text
FromPrice (decimal?) - Starting price
CurrencyCode (string) - Currency code
LikedAt (DateTime?) - When the tour was liked
var favorites = await postLikeService . GetUserLikedToursAsync (
userId : 123
);
Console . WriteLine ( $"You have { favorites . Count } favorite tours:" );
foreach ( var tour in favorites )
{
Console . WriteLine ( $"- { tour . Title } ( { tour . LocationLabel } )" );
if ( tour . FromPrice . HasValue )
{
Console . WriteLine ( $" From { tour . FromPrice . Value : C } { tour . CurrencyCode } " );
}
}
Results are ordered by liked date (most recent first) if the CreatedAt or FechaLike column exists, otherwise ordered by title.
GetOwnerLikeStatsAsync
Retrieves like statistics for all tours owned by a user.
Task <( int TotalLikes , int RecentLikes )> GetOwnerLikeStatsAsync (
int ownerUserId ,
int recentDays = 7 ,
CancellationToken cancellationToken = default )
Number of days to consider as “recent”
Total number of likes across all owner’s tours
Number of likes received in the recent period
var ( total , recent ) = await postLikeService . GetOwnerLikeStatsAsync (
ownerUserId : 123 ,
recentDays : 30
);
Console . WriteLine ( $"Total Favorites: { total } " );
Console . WriteLine ( $"New in last 30 days: { recent } " );
Recent likes count requires the CreatedAt column in the PostLikes table. If the column doesn’t exist, recentLikes will be 0.
CurrencyConversionService
Overview
Provides real-time currency conversion using the ExchangeRate API.
public interface ICurrencyConversionService
Location: ~/workspace/source/AndanDo/Services/Utility/CurrencyConversionService.cs
ConvertAsync
Converts an amount from one currency to another.
Task < decimal > ConvertAsync (
decimal amount ,
string fromCurrency ,
string toCurrency ,
CancellationToken cancellationToken = default )
Amount to convert (must be non-negative)
Source currency code (e.g., “USD”, “EUR”)
Converted amount in target currency
Basic Example
Multi-Currency Pricing
// Convert 100 USD to DOP
var dopAmount = await currencyService . ConvertAsync (
amount : 100m ,
fromCurrency : "USD" ,
toCurrency : "DOP"
);
Console . WriteLine ( $"$100 USD = $ { dopAmount : F2 } DOP" );
API : Uses the free ExchangeRate-API service at https://open.er-api.com/v6/latest/No API key required for basic usage.
If source and target currencies are the same, the method returns the original amount without making an API call.
Validation : The method validates:
amount must be non-negative
fromCurrency must not be empty
toCurrency must not be empty
Target currency must exist in API response
Throws ArgumentOutOfRangeException, ArgumentException, or InvalidOperationException if validation fails.
Error Handling
public async Task < decimal ?> SafeConvertAsync (
decimal amount ,
string fromCurrency ,
string toCurrency )
{
try
{
return await currencyService . ConvertAsync (
amount ,
fromCurrency ,
toCurrency
);
}
catch ( ArgumentOutOfRangeException ex )
{
Console . WriteLine ( $"Invalid amount: { ex . Message } " );
return null ;
}
catch ( ArgumentException ex )
{
Console . WriteLine ( $"Invalid currency code: { ex . Message } " );
return null ;
}
catch ( InvalidOperationException ex )
{
Console . WriteLine ( $"Conversion failed: { ex . Message } " );
return null ;
}
catch ( HttpRequestException ex )
{
Console . WriteLine ( $"Network error: { ex . Message } " );
return null ;
}
}
Caching Recommendations
Performance : Currency rates change frequently but not constantly. Consider implementing caching:
Cache rates for 1-6 hours
Use IMemoryCache or distributed cache
Refresh cache in background
Provide stale data if API is unavailable
Cached Conversion Example
public class CachedCurrencyService
{
private readonly ICurrencyConversionService _currencyService ;
private readonly IMemoryCache _cache ;
private const int CacheMinutes = 60 ;
public async Task < decimal > ConvertWithCacheAsync (
decimal amount ,
string fromCurrency ,
string toCurrency )
{
var cacheKey = $"rate_ { fromCurrency } _ { toCurrency } " ;
if ( ! _cache . TryGetValue < decimal >( cacheKey , out var rate ))
{
// Get rate by converting 1 unit
rate = await _currencyService . ConvertAsync (
1m ,
fromCurrency ,
toCurrency
);
_cache . Set ( cacheKey , rate , TimeSpan . FromMinutes ( CacheMinutes ));
}
return amount * rate ;
}
}
Common Use Cases
Display Tour with Reviews and Likes
public async Task < TourCardViewModel > GetTourCardAsync (
int tourId ,
int ? currentUserId = null )
{
var tour = await tourService . GetTourFullByIdAsync ( tourId );
if ( tour == null ) return null ;
// Get reviews
var reviews = await reviewService . GetReviewsAsync ( tourId , top : 5 );
var avgRating = reviews . Any ()
? reviews . Average ( r => r . Rating )
: ( double ?) null ;
// Get like status
bool isLiked = false ;
if ( currentUserId . HasValue )
{
isLiked = await postLikeService . HasUserLikedAsync (
currentUserId . Value ,
tourId
);
}
// Convert price if needed
decimal displayPrice = tour . TicketTypes . FirstOrDefault () ? . Price ?? 0 ;
string displayCurrency = "USD" ;
if ( userPreferredCurrency != "USD" )
{
displayPrice = await currencyService . ConvertAsync (
displayPrice ,
"USD" ,
userPreferredCurrency
);
displayCurrency = userPreferredCurrency ;
}
return new TourCardViewModel
{
Tour = tour ,
AverageRating = avgRating ,
ReviewCount = reviews . Count ,
IsLiked = isLiked ,
DisplayPrice = displayPrice ,
DisplayCurrency = displayCurrency
};
}
Host Dashboard Stats
public async Task < HostDashboardViewModel > GetHostStatsAsync ( int userId )
{
var ( totalTours , recentTours ) = await tourService . GetOwnerTourStatsAsync (
userId ,
recentDays : 7
);
var ( totalLikes , recentLikes ) = await postLikeService . GetOwnerLikeStatsAsync (
userId ,
recentDays : 7
);
var ( avgRating , ratingCount ) = await reviewService . GetOwnerRatingAsync ( userId );
var earnings = await tourService . GetOwnerEarningsAsync ( userId );
return new HostDashboardViewModel
{
TotalTours = totalTours ,
NewToursThisWeek = recentTours ,
TotalFavorites = totalLikes ,
NewFavoritesThisWeek = recentLikes ,
AverageRating = avgRating ?? 0 ,
TotalReviews = ratingCount ,
TotalEarnings = earnings
};
}
Configuration
// Register utility services
services . AddScoped < IReviewService , ReviewService >();
services . AddScoped < IPostLikeService , PostLikeService >();
// Currency service needs HttpClient
services . AddHttpClient < ICurrencyConversionService , CurrencyConversionService >();
Best Practices
Review Moderation : Reviews are auto-visible by default. Implement a moderation system to flag and hide inappropriate content.
Rate Limiting : The currency conversion service makes external API calls. Implement rate limiting and caching to avoid hitting API quotas.
Data Integrity : When deleting tours, ensure reviews and likes are also cleaned up. The TourService.DeleteTourAsync method handles this automatically.