Skip to content

Commit

Permalink
Add memory cache to reduce database reads (#97)
Browse files Browse the repository at this point in the history
* Added singleton IMemoryCache to DI

* Added obvious caching functionality

* Complete, v1, customer cache

* CustomerService now needs `IMemoryCache`

* Searches wouldn't be cached atm

* updated doc to keep track

* Added simple caching functionality

* Added simple caching functionality and docs

* Forgot async

* Contacts are now cached

* cleanup

* now implements basic caching functionality

* cleanup

* Constructor now expects IMemoryCache DI.

* Fixing TODO3 and now update caches entities

* internal to allow centralized access when needs to be change to avoid inconsistency

* update now cached

* update now cached
  • Loading branch information
aaroniz-bgu committed Nov 13, 2023
1 parent e0b0d6a commit b24fdf5
Show file tree
Hide file tree
Showing 9 changed files with 266 additions and 36 deletions.
155 changes: 139 additions & 16 deletions src/AppServices/Customers/CustomerService.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using AutoMapper;
using GaEpd.AppLibrary.Pagination;
using Microsoft.Extensions.Caching.Memory;
using MyApp.AppServices.Customers.Dto;
using MyApp.AppServices.Staff.Dto;
using MyApp.AppServices.UserServices;
Expand All @@ -11,25 +12,31 @@ namespace MyApp.AppServices.Customers;

public sealed class CustomerService : ICustomerService
{
//Change those if needed:
private const double CustomerExpirationMinutes = 5.0;
private const double ContactExpirationMinutes = 5.0;

private readonly IMapper _mapper;
private readonly IUserService _userService;
private readonly ICustomerRepository _customerRepository;
private readonly ICustomerManager _customerManager;
private readonly IContactRepository _contactRepository;
private readonly IMemoryCache _cache;

public CustomerService(
IMapper mapper, IUserService userService, ICustomerRepository customerRepository,
ICustomerManager customerManager, IContactRepository contactRepository)
ICustomerManager customerManager, IContactRepository contactRepository, IMemoryCache cache)
{
_mapper = mapper;
_userService = userService;
_customerRepository = customerRepository;
_customerManager = customerManager;
_contactRepository = contactRepository;
_cache = cache;
}

// Customer read

public async Task<IPaginatedResult<CustomerSearchResultDto>> SearchAsync(
CustomerSearchDto spec, PaginatedRequest paging, CancellationToken token = default)
{
Expand All @@ -41,13 +48,54 @@ public sealed class CustomerService : ICustomerService
? _mapper.Map<List<CustomerSearchResultDto>>(
await _customerRepository.GetPagedListAsync(predicate, paging, token))
: new List<CustomerSearchResultDto>();

return new PaginatedResult<CustomerSearchResultDto>(list, count, paging);
}

/// <summary>
/// Private method used to asynchronously retrieve <see cref="Customer"/> based on its unique identifier.
/// </summary>
/// <param name="id">Customer's unique identifier.</param>
/// <param name="token"><see cref="CancellationToken"/> (Optional)</param>
/// <returns>The <see cref="Customer"/> with the provided identifier if present and null otherwise</returns>
private async Task<Customer> GetCustomerCachedAsync(Guid id, CancellationToken token = default)
{
var customer = _cache.Get<Customer>(id);
if (customer is null)
{
customer = await _customerRepository.FindIncludeAllAsync(id, token);
if (customer is null)
{
throw new KeyNotFoundException(
$"No customer exists with {id.ToString()} id.");
}

_cache.Set(id, customer, TimeSpan.FromMinutes(CustomerExpirationMinutes));
}
return customer;
}

private async Task<Customer?> FindCustomerHelper(Guid id, CancellationToken token = default)
{
try
{
return await GetCustomerCachedAsync(id, token);
}
catch (KeyNotFoundException ex)

Check warning on line 84 in src/AppServices/Customers/CustomerService.cs

View workflow job for this annotation

GitHub Actions / Run unit tests

The variable 'ex' is declared but never used

Check warning on line 84 in src/AppServices/Customers/CustomerService.cs

View workflow job for this annotation

GitHub Actions / Run unit tests

The variable 'ex' is declared but never used

Check warning on line 84 in src/AppServices/Customers/CustomerService.cs

View workflow job for this annotation

GitHub Actions / Analyze with CodeQL (csharp)

The variable 'ex' is declared but never used

Check warning on line 84 in src/AppServices/Customers/CustomerService.cs

View workflow job for this annotation

GitHub Actions / Analyze with CodeQL (csharp)

The variable 'ex' is declared but never used

Check warning on line 84 in src/AppServices/Customers/CustomerService.cs

View workflow job for this annotation

GitHub Actions / Analyze with SonarCloud

The variable 'ex' is declared but never used

Check warning on line 84 in src/AppServices/Customers/CustomerService.cs

View workflow job for this annotation

GitHub Actions / Analyze with SonarCloud

The variable 'ex' is declared but never used
{
return null;
}
}

/// <summary>
/// Asynchronously retrieves a specific customer based on its unique identifier.
/// </summary>
/// <param name="id">Customer's unique identifier.</param>
/// <param name="token"><see cref="CancellationToken"/> (Optional)</param>
/// <returns><see cref="CustomerViewDto"/> associated with the required customer if present and null otherwise.</returns>
public async Task<CustomerViewDto?> FindAsync(Guid id, CancellationToken token = default)
{
var customer = await _customerRepository.FindIncludeAllAsync(id, token);
var customer = await FindCustomerHelper(id, token);
if (customer is null) return null;

var view = _mapper.Map<CustomerViewDto>(customer);
Expand All @@ -59,8 +107,16 @@ await _customerRepository.GetPagedListAsync(predicate, paging, token))
: view;
}

public async Task<CustomerSearchResultDto?> FindBasicInfoAsync(Guid id, CancellationToken token = default) =>
_mapper.Map<CustomerSearchResultDto>(await _customerRepository.FindAsync(id, token));
/// <summary>
/// Asynchronously retrieves information about a customer identified by their unique identifier.
/// </summary>
/// <param name="id">The unique identifier of the customer desired.</param>
/// <param name="token"><see cref="CancellationToken"/> (Optional)</param>
/// <returns>
/// DTO containing basic information about the customer, or null if the customer is not found.
/// </returns>
public async Task<CustomerSearchResultDto?> FindBasicInfoAsync(Guid id, CancellationToken token = default) =>
_mapper.Map<CustomerSearchResultDto>(await FindCustomerHelper(id, token));

// Customer write

Expand All @@ -77,13 +133,29 @@ public async Task<Guid> CreateAsync(CustomerCreateDto resource, CancellationToke
await _customerRepository.InsertAsync(customer, autoSave: false, token: token);
await CreateContactAsync(customer, resource.Contact, user, token);

_cache.Set(customer.Id, customer, TimeSpan.FromMinutes(CustomerExpirationMinutes));

await _customerRepository.SaveChangesAsync(token);
return customer.Id;
}


/// <summary>
/// Asynchronously retrieves customer for updating purposes by their unique identifier.
/// </summary>
/// <param name="id">The unique identifier of the customer desired.</param>
/// <param name="token"><see cref="CancellationToken"/> (Optional)</param>
/// <returns>
/// DTO with the data of the user as it currently is, meant to be updated, if not found will return null.
/// </returns>
public async Task<CustomerUpdateDto?> FindForUpdateAsync(Guid id, CancellationToken token = default) =>
_mapper.Map<CustomerUpdateDto>(await _customerRepository.FindAsync(id, token));

_mapper.Map<CustomerUpdateDto>(await FindCustomerHelper(id, token));

/// <summary>
/// Asynchronously updates a customer identified by their unique identifier using the provided data in the resource.
/// </summary>
/// <param name="id">Customer's unique identifier.</param>
/// <param name="resource">Updated data resource of the specified customer.</param>
/// <param name="token"><see cref="CancellationToken"/> (Optional)</param>
public async Task UpdateAsync(Guid id, CustomerUpdateDto resource, CancellationToken token = default)
{
var item = await _customerRepository.GetAsync(id, token);
Expand All @@ -93,21 +165,29 @@ public async Task UpdateAsync(Guid id, CustomerUpdateDto resource, CancellationT
item.Description = resource.Description;
item.County = resource.County;
item.MailingAddress = resource.MailingAddress;


_cache.Set(id, item, TimeSpan.FromMinutes(CustomerExpirationMinutes));

await _customerRepository.UpdateAsync(item, token: token);
}

public async Task DeleteAsync(Guid id, string? deleteComments, CancellationToken token = default)
{
_cache.Remove(id);
var item = await _customerRepository.GetAsync(id, token);
item.SetDeleted((await _userService.GetCurrentUserAsync())?.Id);
item.DeleteComments = deleteComments;
await _customerRepository.UpdateAsync(item, token: token);
}

/// <summary>
/// Asynchronously restores a previously deleted customer identified by their unique identifier.
/// </summary>
/// <param name="id">The unique identifier of the customer to restore.</param>
/// <param name="token"><see cref="CancellationToken"/> (Optional)</param>
public async Task RestoreAsync(Guid id, CancellationToken token = default)
{
var item = await _customerRepository.GetAsync(id, token);
var item = await GetCustomerCachedAsync(id, token);
item.SetNotDeleted();
await _customerRepository.UpdateAsync(item, token: token);
}
Expand All @@ -116,9 +196,10 @@ public async Task RestoreAsync(Guid id, CancellationToken token = default)

public async Task<Guid> AddContactAsync(ContactCreateDto resource, CancellationToken token = default)
{
var customer = await _customerRepository.GetAsync(resource.CustomerId, token);
var customer = await GetCustomerCachedAsync(resource.CustomerId, token);
var id = await CreateContactAsync(customer, resource, await _userService.GetCurrentUserAsync(), token);
await _contactRepository.SaveChangesAsync(token);

return id;
}

Expand All @@ -138,16 +219,54 @@ public async Task<Guid> AddContactAsync(ContactCreateDto resource, CancellationT
contact.Address = resource.Address;
contact.EnteredBy = user;

_cache.Set(contact.Id, contact, TimeSpan.FromMinutes(ContactExpirationMinutes));

await _contactRepository.InsertAsync(contact, autoSave: false, token: token);
return contact.Id;
}

/// <summary>
/// Asynchronously retrieves the <see cref="Contact"/> associated with the provided identifier.
/// Caches it accordingly.
/// </summary>
/// <param name="contactId">Contact's unique identifier.</param>
/// <param name="token"><see cref="CancellationToken"/> (Optional)</param>
/// <returns>Contact object associated with the provided identifier if present and null otherwise.</returns>
private async Task<Contact?> GetContactCachedAsync(Guid contactId, CancellationToken token = default)
{
// Did not do the same as I did in GetCustomerCachedAsync to throw an exception
// seems useless & verbose in this case, but can be accomplished if required.
var contact = _cache.Get<Contact>(contactId);
if (contact is null)
{
contact = await _contactRepository.FindAsync(e => e.Id == contactId && !e.IsDeleted, token);
if (contact is null) return null;
_cache.Set(contact.Id, contact, TimeSpan.FromMinutes(ContactExpirationMinutes));
}
return contact.IsDeleted ? null : contact;
}

/// <summary>
/// Asynchronously retrieves Contact for display purposes.
/// </summary>
/// <param name="contactId">Contact's unique identifier.</param>
/// <param name="token"><see cref="CancellationToken"/> (Optional)</param>
/// <returns>
/// <see cref="ContactViewDto"/>.
/// </returns>
public async Task<ContactViewDto?> FindContactAsync(Guid contactId, CancellationToken token = default) =>
_mapper.Map<ContactViewDto>(await _contactRepository.FindAsync(e => e.Id == contactId && !e.IsDeleted, token));

_mapper.Map<ContactViewDto>(await GetContactCachedAsync(contactId, token));

/// <summary>
/// Asynchronously retrieves Contact for updating purposes.
/// </summary>
/// <param name="contactId">Contact's unique identifier.</param>
/// <param name="token"><see cref="CancellationToken"/> (Optional)</param>
/// <returns>
/// <see cref="ContactUpdateDto"/>.
/// </returns>
public async Task<ContactUpdateDto?> FindContactForUpdateAsync(Guid contactId, CancellationToken token = default) =>
_mapper.Map<ContactUpdateDto>(
await _contactRepository.FindAsync(e => e.Id == contactId && !e.IsDeleted, token));
_mapper.Map<ContactUpdateDto>(await GetContactCachedAsync(contactId, token));

public async Task UpdateContactAsync(Guid contactId, ContactUpdateDto resource, CancellationToken token = default)
{
Expand All @@ -161,12 +280,16 @@ public async Task UpdateContactAsync(Guid contactId, ContactUpdateDto resource,
item.Email = resource.Email;
item.Notes = resource.Notes;
item.Address = resource.Address;

_cache.Set(contactId, item, TimeSpan.FromMinutes(ContactExpirationMinutes));

await _contactRepository.UpdateAsync(item, token: token);
}

public async Task DeleteContactAsync(Guid contactId, CancellationToken token = default)
{
_cache.Remove(contactId);

var item = await _contactRepository.GetAsync(contactId, token);
item.SetDeleted((await _userService.GetCurrentUserAsync())?.Id);
await _contactRepository.UpdateAsync(item, token: token);
Expand Down
51 changes: 48 additions & 3 deletions src/AppServices/Offices/OfficeService.cs
Original file line number Diff line number Diff line change
@@ -1,29 +1,55 @@
using AutoMapper;
using GaEpd.AppLibrary.ListItems;
using Microsoft.Extensions.Caching.Memory;
using MyApp.AppServices.UserServices;
using MyApp.Domain.Entities.Offices;

namespace MyApp.AppServices.Offices;

public sealed class OfficeService : IOfficeService
{
private const double OfficeExpirationMinutes = 5.0;

private readonly IOfficeRepository _repository;
private readonly IOfficeManager _manager;
private readonly IMapper _mapper;
private readonly IUserService _users;
private readonly IMemoryCache _cache;
private readonly IMapper _mapper;

public OfficeService(
IOfficeRepository repository,
IOfficeManager manager,
IMapper mapper,
IUserService users)
IUserService users,
IMemoryCache cache)
{
_repository = repository;
_manager = manager;
_mapper = mapper;
_users = users;
_cache = cache;
}

/// <summary>
/// Asynchronously retrieves <see cref="Office"/> associated with the provided identifier.
/// Caches it accordingly.
/// </summary>
/// <param name="id">Office's unique identifier.</param>
/// <param name="token"><see cref="CancellationToken"/> (Optional).</param>
/// <returns>Office object associated with the provided identifier if present and null otherwise.</returns>
private async Task<Office?> GetOfficeCachedAsync(Guid id, CancellationToken token = default)
{
var office = _cache.Get<Office>(id);
if (office is null)
{
office = await _repository.FindAsync(id, token);
if (office is null) return null;

_cache.Set(office.Id, office, TimeSpan.FromMinutes(OfficeExpirationMinutes));
}
return office.Active ? office : null;
}

public async Task<IReadOnlyList<OfficeViewDto>> GetListAsync(CancellationToken token = default)
{
var list = (await _repository.GetListAsync(token)).OrderBy(e => e.Name).ToList();
Expand All @@ -34,16 +60,33 @@ public async Task<IReadOnlyList<OfficeViewDto>> GetListAsync(CancellationToken t
(await _repository.GetListAsync(e => e.Active, token)).OrderBy(e => e.Name)
.Select(e => new ListItem(e.Id, e.Name)).ToList();

/// <summary>
/// Creates new <see cref="Office"/> object and persists it.
/// </summary>
/// <param name="resource">create request.</param>
/// <param name="token"><see cref="CancellationToken"/> (Optional).</param>
/// <returns>Identifier of the newly created office object.</returns>
public async Task<Guid> CreateAsync(OfficeCreateDto resource, CancellationToken token = default)
{
var item = await _manager.CreateAsync(resource.Name, (await _users.GetCurrentUserAsync())?.Id, token);
await _repository.InsertAsync(item, token: token);

_cache.Set(item.Id, item, TimeSpan.FromMinutes(OfficeExpirationMinutes));

return item.Id;
}

/// <summary>
/// Asynchronously retrieves office associated with the provided identifier for update purposes.
/// </summary>
/// <param name="id">Office's unique identifier.</param>
/// <param name="token"><see cref="CancellationToken"/> (Optional).</param>
/// <returns>
/// <see cref="OfficeUpdateDto"/>
/// </returns>
public async Task<OfficeUpdateDto?> FindForUpdateAsync(Guid id, CancellationToken token = default)
{
var item = await _repository.FindAsync(id, token);
var item = await GetOfficeCachedAsync(id, token);
return _mapper.Map<OfficeUpdateDto>(item);
}

Expand All @@ -56,6 +99,8 @@ public async Task UpdateAsync(Guid id, OfficeUpdateDto resource, CancellationTok
await _manager.ChangeNameAsync(item, resource.Name, token);
item.Active = resource.Active;

_cache.Set(id, item, TimeSpan.FromMinutes(OfficeExpirationMinutes));

await _repository.UpdateAsync(item, token: token);
}

Expand Down
Loading

0 comments on commit b24fdf5

Please sign in to comment.