I have a working add repository. I want to write a separate add repository without touching it, but after adding it in this repository, I want it to return a value that is included in the response model. How can I do it ?
MyRepository
public class Repository<T> : IRepository<T> where T : EntityBase
{
private readonly ApplicationContext Context;
public Repository(ApplicationContext context)
{
Context = context;
}
public async Task<T> AddAsync(T entity)
{
Context.Set<T>().Add(entity);
await Context.SaveChangesAsync();
return entity;
}
public async Task<IEnumerable<T>> GetAllAsync(Expression<Func<T, bool>> predicate = null,
IList<string> includes = null, bool disableTracking = true)
{
IQueryable<T> query = Context.Set<T>().Where(x => !x.IsDelete);
if (predicate != null) query = query.Where(predicate);
if (includes != null)
query = includes.Aggregate(query, (current, include) => current.Include(include));
if (disableTracking) query = query.AsNoTracking();
return await query.ToListAsync();
}
Related
The dbcontext tracking behaviour is set to NoTracking, but I still get a fail in the test.
BaseDbContext in this context does not have any relevant code compared to IdentityDbContext provided by EF. Also is the same with BaseUser, where it is basically only IdentityUser.
Method for creating DbContext:
public static T GetDbContext<T>()
where T : BaseDbContext<BaseUser<Guid>>
{
var optionBuilder = new DbContextOptionsBuilder<T>();
optionBuilder.UseInMemoryDatabase(Guid.NewGuid().ToString());
optionBuilder.UseQueryTrackingBehavior(QueryTrackingBehavior.NoTracking);
var obj = Activator.CreateInstance(typeof(T), optionBuilder.Options);
if (obj == null)
{
throw new SystemException(typeof(T) + " was null!");
}
var ctx = (T)obj;
ctx.Database.EnsureDeleted();
ctx.Database.EnsureCreated();
return ctx;
}
The test, that fails:
[Fact]
public async void Test_UpdateSingle()
{
var dbContext = DbContextFactory.GetDbContext<SimpleDbContext>();
var uow = UowFactory.GetUow<SimpleUow, SimpleDbContext>(dbContext);
var id1 = Guid.NewGuid();
var name1 = Guid.NewGuid().ToString();
var testEntity1 = new DalSimpleEntity
{
Id = id1,
Name = name1
};
uow.SimpleRepo.Add(testEntity1);
await uow.SaveChangesAsync();
var newName = Guid.NewGuid().ToString();
testEntity1.Name = newName;
//Fails here:
uow.SimpleRepo.Update(testEntity1);
await uow.SaveChangesAsync();
Assert.Single(await uow.SimpleRepo.GetAllAsync());
var getEntity1 = await uow.SimpleRepo.FirstOrDefaultAsync(id1);
Assert.Equal(newName, getEntity1?.Name);
}
The UnitOfWork is for using it as a layer on top of the dbcontext.
The SaveChanges method calls directly the DbContext savechanges.
UnitOfWork also contains the reference for Repository.
The SimpleRepo is derived from BaseRepository. Nothing is changed in the SimpleRepo.
public abstract class BaseRepository<TDbContext, TEntityIn, TEntityOut> : BaseRepositoryWebApp<TDbContext, TEntityIn, TEntityOut, Guid>, IBaseRepositoryWebApp<TEntityOut>
where TEntityIn : class, IDomainEntityId, IDomainEntityId<Guid>
where TEntityOut : class, IDomainEntityId, IDomainEntityId<Guid>
where TDbContext : BaseDbContext<BaseUser<Guid>>
{
protected readonly DbContext RepoDbContext;
protected readonly DbSet<TEntityIn> RepoDbSet;
protected readonly IBaseMapper<TEntityIn, TEntityOut> Mapper;
public BaseRepository(TDbContext dbContext, IBaseMapper<TEntityIn, TEntityOut> mapper)
{
RepoDbContext = dbContext;
RepoDbSet = dbContext.Set<TEntityIn>();
Mapper = mapper;
}
public virtual async Task<IEnumerable<TEntityOut>> GetAllAsync(bool noTracking = true, Guid userId = default)
{
var entities = await InitQuery(noTracking, userId).ToListAsync();
return entities.Select(e => Mapper.Map(e)!);
}
public virtual async Task<TEntityOut?> FirstOrDefaultAsync(TKey id, bool noTracking = true, Guid userId = default)
{
var query = InitQuery(noTracking, userId).FirstOrDefaultAsync(e => e.Id.Equals(id));
return Mapper.Map(await query);
}
public virtual async Task<bool> ExistsAsync(TKey id, Guid userId = default)
{
return await InitQuery(userId: userId).AnyAsync(e => e.Id.Equals(id));
}
public virtual async Task<TEntityOut?> RemoveAsync(TKey id, Guid userId = default)
{
var entity = await InitQuery(userId: userId).FirstOrDefaultAsync(e => e.Id.Equals(id));
if (entity == null) return null;
return Mapper.Map(RepoDbSet.Remove(entity).Entity);
}
public TEntityOut Add(TEntityOut? entity)
{
return Mapper.Map(RepoDbSet.Add(Mapper.Map(entity)!).Entity)!;
}
public TEntityOut Update(TEntityOut? entity)
{
return Mapper.Map(RepoDbSet.Update(Mapper.Map(entity)!).Entity)!;
}
protected virtual IQueryable<TEntityIn> InitQuery(bool noTracking = true, Guid userId = default)
{
var query = RepoDbSet.AsQueryable();
if (typeof(IDomainEntityUsers).IsAssignableFrom(typeof(TEntityIn)))
{
query = query.Where(e => (e as IDomainEntityUsers)!.UserId.Equals(userId));
}
if (noTracking)
{
query = query.AsNoTracking();
}
return query;
}
}
My question is, have I forgotten some place, where I should also state to use NoTracking behaviour?
The problem was made difficult with the fact, that every time the entity moved down or up the layers (example: from uow to dbcontext and back, containing 2 mappings here alone).
This ensured that calling
dbContext.Entity(entity).State = EntityState.Detached;
was not an option, because Savechanges was called with the uow object, not dbContext. Also in the real use case the dbContext is out of reach, thus not being able to call functions from dbContext at all.
So I searched for options to detach all objects, that were still attached after saveChanges.
ChangeTracker.Clear();
satisfied this requirement.
The solution for this problem is:
public override Task<int> SaveChangesAsync(CancellationToken cancellationToken = new())
{
var res = await base.SaveChangesAsync(cancellationToken);
ChangeTracker.Clear();
return res;
}
I am writting an API backend application using .NET Core
BaseRepository:
public class BaseRepository<T, TPrimaryKey> : IBaseRepository<T, TPrimaryKey> where T : class where TPrimaryKey : struct
{
private readonly DatabaseContext _dbContext;
public BaseRepository(DatabaseContext dbContext)
{
_dbContext = dbContext;
}
public async Task<IEnumerable<T>> GetAll()
{
return await _dbContext.Set<T>().ToListAsync();
}
public IQueryable<T> GetQueryable()
{
return _dbContext.Set<T>();
}
public async Task<T> Find(TPrimaryKey id)
{
return await _dbContext.Set<T>().FindAsync(id);
}
public async Task<T> Add(T entity, bool saveChanges = true)
{
await _dbContext.Set<T>().AddAsync(entity);
if (saveChanges)
await _dbContext.SaveChangesAsync();
return await Task.FromResult(entity);
}
public async Task Edit(T entity, bool saveChanges = true)
{
_dbContext.Entry(entity).State = EntityState.Modified;
if (saveChanges)
await _dbContext.SaveChangesAsync();
}
public async Task Delete(T entity, bool saveChanges = true)
{
if (entity == null)
throw new NullReferenceException();
_dbContext.Set<T>().Remove(entity);
if (saveChanges)
await _dbContext.SaveChangesAsync();
}
public async Task<IEnumerable<T>> BulkInsert(IEnumerable<T> entities, bool saveChanges = true)
{
foreach (T entity in entities)
{
await _dbContext.Set<T>().AddAsync(entity);
}
if (saveChanges)
await _dbContext.SaveChangesAsync();
return await Task.FromResult(entities);
}
public async Task BulkUpdate(IEnumerable<T> entities, bool saveChanges = true)
{
foreach (T entity in entities)
{
_dbContext.Entry(entity).State = EntityState.Modified;
}
if (saveChanges)
await _dbContext.SaveChangesAsync();
}
public async Task Save()
{
await _dbContext.SaveChangesAsync();
}
}
IBaseRepository:
public interface IBaseRepository<T, E> where T : class where E : struct
{
Task<IEnumerable<T>> GetAll();
IQueryable<T> GetQueryable();
Task<T> Find(E id);
Task<T> Add(T entity, bool saveChanges = true);
Task Edit(T entity, bool saveChanges = true);
Task Delete(T entity, bool saveChanges = true);
Task<IEnumerable<T>> BulkInsert(IEnumerable<T> entities, bool saveC
Task BulkUpdate(IEnumerable<T> entities, bool saveChanges = true);
Task Save();
}
IServiceBase:
public interface IServiceBase<TEntity, TPrimaryKey>
{
Task<TEntity> GetById(TPrimaryKey id);
Task<TEntity> GetSingle(Expression<Func<TEntity, bool>> whereCondition);
Task<IEnumerable<TEntity>> GetAll();
IEnumerable<TEntity> GetAll(Expression<Func<TEntity, bool>> whereCondition);
IQueryable<TEntity> GetAllQueryable();
IQueryable<TEntity> Query(Expression<Func<TEntity, bool>> whereCondition);
Task<TEntity> Create(TEntity entity);
Task Delete(TEntity entity);
Task Update(TEntity entity);
Task<long> Count(Expression<Func<TEntity, bool>> whereCondition);
Task<long> Count();
Task<IEnumerable<TEntity>> BulkInsert(IEnumerable<TEntity> entities);
Task BulkUpdate(IEnumerable<TEntity> entities);
}
for example IAddressServices:
public interface IAddressService : IServiceBase<Address, Guid>
{
Task<Address> VerifyAddress(Address address);
}
ServiceBase:
public abstract class ServiceBase<TEntity, TRepository, TPrimaryKey> : IServiceBase<TEntity, TPrimaryKey>
where TEntity : class
where TPrimaryKey : struct
where TRepository : IBaseRepository<TEntity, TPrimaryKey>
{
public TRepository Repository;
public ServiceBase(IBaseRepository<TEntity, TPrimaryKey> rep)
{
Repository = (TRepository)rep;
}
public virtual async Task<TEntity> GetById(TPrimaryKey id)
{
return await Repository.Find(id);
}
public async Task<TEntity> GetSingle(Expression<Func<TEntity, bool>> whereCondition)
{
return await Repository.GetQueryable().Where(whereCondition).FirstOrDefaultAsync();
}
public async Task<IEnumerable<TEntity>> GetAll()
{
return await Repository.GetAll();
}
public IEnumerable<TEntity> GetAll(Expression<Func<TEntity, bool>> whereCondition)
{
return Repository.GetQueryable().Where(whereCondition);
}
public IQueryable<TEntity> GetAllQueryable()
{
return Repository.GetQueryable();
}
public IQueryable<TEntity> Query(Expression<Func<TEntity, bool>> whereCondition)
{
return Repository.GetQueryable().Where(whereCondition);
}
public virtual async Task<TEntity> Create(TEntity entity)
{
return await Repository.Add(entity);
}
public virtual async Task Delete(TEntity entity)
{
await Repository.Delete(entity);
}
public virtual async Task Update(TEntity entity)
{
await Repository.Edit(entity);
}
public async Task<long> Count(Expression<Func<TEntity, bool>> whereCondition)
{
return await Repository.GetQueryable().Where(whereCondition).CountAsync();
}
public async Task<long> Count()
{
return await Repository.GetQueryable().CountAsync();
}
public async Task<IEnumerable<TEntity>> BulkInsert(IEnumerable<TEntity> entities)
{
return await Repository.BulkInsert(entities);
}
public async Task BulkUpdate(IEnumerable<TEntity> entities)
{
await Repository.BulkUpdate(entities);
}
}
and concrete implementations of services:
AddressService:
public class AddressService : ServiceBase<Address, IBaseRepository<Address, Guid>, Guid>, IAddressService
{
public AddressService(IBaseRepository<Address, Guid> rep) : base(rep)
{
}
public async Task<Address> VerifyAddress(Address address)
{
//logic
}
}
ProductController:
public class ProductController : ControllerBase
{
private readonly IProductService _productService;
private readonly IAddressService _addressService;
private readonly ILogger _logger;
private readonly IMapper _mapper;
public ProductController (IProductService productService,
IAddressService addressService,
ILogger<ProductController> logger,
IMapper mapper)
{
_packageService = packageService;
_addressService = addressService;
_logger = logger;
_mapper = mapper;
}
[HttpGet]
public async Task<IActionResult> GetAllProductsWithAddresses()
{
try
{
var products = await _productService.GetAllQueryable().Include(x => x.Address).ToListAsync();
return Ok(_mapper.Map<List<ProductResponse>>(products));
}
catch (Exception e)
{
_logger.LogError($"An unexpected error occured: ${e}");
return StatusCode(StatusCodes.Status500InternalServerError);
}
}
}
Lets say for example if I had an POST endpoint in ProductController where I need to insert data in 3 different database tables: Address, ProductSize and ProductImage. I would have 3 services and I would call _addressService.Add(address), _productSize.Add(productSize) and _productImageService(image) in my controller. How can I support transactions here if DatabaseContext is located in BaseRepository, what is the best practice?
How can I support transactions here if DatabaseContext is located in BaseRepository, what is the best practice?
Best practice is to throw out all that junk and just have your controller talk to the DbContext, or have the controller talk to a business service that talks to the DbContext.
Barring that DI should inject the same DbContext instance in each service or repository, so you can expose Unit-of-work methods on any one of them. Or you could introduce an additional service for managing cross-repo operations. EG
public class UnitOfWork
{
DbContext db;
public UnitOfWork(DbContext db)
{
this.db = db;
}
IDbContextTransaction BeginTransaction() => db.Database.BeginTransaction();
void CommitTransaction() => db.Database.CommitTransaction();
int SaveChanges() => db.SaveChanges();
}
net core project. I am trying to implement generic repository pattern. I have base repository which is abstract. So I am overriding base repository and creating my other repositories. The implementation looks below.
BaseRepository.cs
public abstract class BaseRepository<T>: IBaseRepository<T>
where T : class
{
private readonly DbContext dbContext;
private readonly DbSet<T> dbSet;
public BaseRepository(DbContext dbContext)
{
this.dbContext = dbContext;
this.dbSet = dbContext?.Set<T>();
}
public async Task<IEnumerable<T>> GetAsync(Expression<Func<T, bool>> filter = null, Func<IQueryable<T>, IOrderedQueryable<T>> orderBy = null, params Expression<Func<T, object>>[] includes)
{
IQueryable<T> query = this.dbSet;
foreach (Expression<Func<T, object>> include in includes)
{
query = query.Include(include);
}
if (filter != null)
{
query = query.Where(filter);
}
if (orderBy != null)
{
query = orderBy(query);
}
return await query.ToListAsync().ConfigureAwait(false);
}
}
}
Repository.cs
public class Repository<T> : BaseRepository<T>, IRepository<T>
where T : class
{
private readonly DbContext dbContext;
private readonly DbSet<T> dbSet;
public Repository(DbContext context) : base(context)
{
}
public async Task<IEnumerable<T>> GetAsync(Expression<Func<T, bool>> filter = null, Func<IQueryable<T>, IOrderedQueryable<T>> orderBy = null, params Expression<Func<T, object>>[] includes)
{
IQueryable<T> query = this.dbSet;
foreach (Expression<Func<T, object>> include in includes)
{
query = query.Include(include);
}
if (filter != null)
{
query = query.Where(filter);
}
if (orderBy != null)
{
query = orderBy(query);
}
return await query.ToListAsync().ConfigureAwait(false);
}
}
}
UnitOfWork.cs
public class UnitOfWork<TContext> : IUnitOfWork<TContext>
where TContext : DbContext, IDisposable
{
private readonly MyContext context;
private Dictionary<(Type type, string name), object> _repositories;
public UnitOfWork(MyContext myContext)
{
if (this.context == null)
{
this.context = myContext;
}
}
public IRepository<TEntity> GetRepository<TEntity>() where TEntity : class
{
return (IRepository<TEntity>)GetOrAddRepository(typeof(TEntity), new Repository<TEntity>(Context));
}
public async Task<int> CompleteAsync()
{
try
{
return await this.context.SaveChangesAsync().ConfigureAwait(false);
}
catch (DbUpdateConcurrencyException ex)
{
ex.Entries.Single().Reload();
return await this.context.SaveChangesAsync().ConfigureAwait(false);
}
}
internal object GetOrAddRepository(Type type, object repo)
{
_repositories ??= new Dictionary<(Type type, string Name), object>();
if (_repositories.TryGetValue((type, repo.GetType().FullName), out var repository)) return repository;
_repositories.Add((type, repo.GetType().FullName), repo);
return repo;
}
}
}
Then In my startup.cs file when I register
services.AddDbContext<MyContext>(options =>
options.UseSqlServer(this.Configuration["AzureSQLConnectionString"]));
services.AddScoped(typeof(IRepository<>), typeof(Repository<>));
services.AddScoped<IUnitOfWork, UnitOfWork>();
UnitofWork throws me error using the generic type 'UnitofWork' requires 1 type of argument. Then I added services.AddScoped<IUnitOfWork, UnitOfWork<MyContext>>(). Now Its throwing error in GetAsync method in Repository.cs. The error is Parameter: source is required, it cannot be empty. Then I found
IQueryable query = this.dbSet;
query is empty. So what I understood is I have some issues with establishing connection to db. Can someone help me what exactly I am doing wrong here? Any help would be greatly appreciated. Thank you
Change these classes as follows:
public abstract class BaseRepository<T>: IBaseRepository<T>
where T : class
{
protected readonly DbContext dbContext;
protected readonly DbSet<T> dbSet;
public BaseRepository(DbContext dbContext)
{
this.dbContext = dbContext;
this.dbSet = dbContext?.Set<T>();
}
public virtual async Task<IEnumerable<T>> GetAsync(Expression<Func<T, bool>> filter = null, Func<IQueryable<T>, IOrderedQueryable<T>> orderBy = null, params Expression<Func<T, object>>[] includes)
{
IQueryable<T> query = this.dbSet;
foreach (Expression<Func<T, object>> include in includes)
{
query = query.Include(include);
}
if (filter != null)
{
query = query.Where(filter);
}
if (orderBy != null)
{
query = orderBy(query);
}
return await query.ToListAsync().ConfigureAwait(false);
}
}
public class Repository<T> : BaseRepository<T>, IRepository<T>
where T : class
{
public Repository(DbContext context) : base(context)
{
}
//override GetAsync if different than base. In your case it is not.
}
I have API built with .net core 2, and I am trying to implement change log feature.
I have done basic part, but I am not sure if it's a best way for doing this.
Here is my EntityBaseRepository
public class EntityBaseRepository<T> : IEntityBaseRepository<T> where T : class, IFullAuditedEntity, new()
{
private readonly ApplicationContext context;
public EntityBaseRepository(ApplicationContext context)
{
this.context = context;
}
public virtual IEnumerable<T> items => context.Set<T>().AsEnumerable().OrderByDescending(m => m.Id);
public virtual T this[int id] => context.Set<T>().FirstOrDefault(m => m.Id == id);
public virtual T GetSingle(int id) => context.Set<T>().FirstOrDefault(x => x.Id == id);
public virtual T Add(T entity) => Operations(entity: entity, state: EntityState.Added);
public virtual T Update(T entity) => Operations(entity: entity, state: EntityState.Modified);
public virtual T Delete(T entity) => Operations(entity: entity, state: EntityState.Deleted);
public virtual T Operations(T entity, EntityState state)
{
EntityEntry dbEntityEntry = context.Entry<T>(entity);
if (state == EntityState.Added)
{
entity.CreationDateTime = DateTime.UtcNow;
entity.CreationUserId = 1;
context.Set<T>().Add(entity);
dbEntityEntry.State = EntityState.Added;
}
else if (state == EntityState.Modified)
{
entity.LastModificationDateTime = DateTime.UtcNow;
entity.LastModificationUserId = 1;
var local = context.Set<T>().Local.FirstOrDefault(entry => entry.Id.Equals(entity.Id));
if (local != null)
{
context.Entry(local).State = EntityState.Detached;
}
dbEntityEntry.State = EntityState.Modified;
}
else if (state == EntityState.Deleted)
{
entity.DeletionFlag = true;
entity.DeletionUserId = 1;
entity.DeletionDateTime = DateTime.UtcNow;
dbEntityEntry.State = EntityState.Modified;
}
return entity;
}
Here is one of my controller.
[Produces("application/json")]
[Route("api/Item")]
public class ItemController : Controller
{
private readonly IItemRepository repository;
private readonly IChangeLogRepository changeLogRepository;
private readonly IMapper mapper;
public ItemController(IItemRepository repository, IChangeLogRepository _changeLogRepository, IMapper mapper)
{
this.repository = repository;
this.changeLogRepository = _changeLogRepository;
this.mapper = mapper;
}
[HttpPost]
public IActionResult Post([FromBody]ItemDto transactionItemDto)
{
if (!ModelState.IsValid)
{
return BadRequest(ModelState);
}
var item = repository.Add(mapper.Map<ItemDto, Item>(source: transactionItemDto));
repository.Commit();
ChangeLog log = new ChangeLog()
{
Log = "New Item Added"
};
changeLogRepository.Add(log);
changeLogRepository.Commit();
return new OkObjectResult(mapper.Map<Item, ItemDto>(source: item));
}
}
if you see in controller, I have added one item, and commited it, then I prepared log for that insertion, added and commited it.
Now, I have few questions, like
I have to commit my transaction twice, is there any way I can optimize it? I don't know if I can handle it on EntityBaseRepository or not.
I also want to check each property, if it gets changed or not. I want to log that to if it's changed. what would be the best way to handle it?
It would be great if anyone can help me with this. really appreciate. thanks.
You can use Action filters for a changelog Like
using System;
public class TrackMyChange : IActionFilter
{
private readonly string _chengeMessage;
private readonly IChangeLogRepository _changeLogRepository;
public TrackMyChange(string changeMessage,IChangeLogRepository changeLogRepository)
{
this._changeLogRepository = changeLogRepository;
this._chengeMessage = chengeMessage;
}
public void OnActionExecuting(ActionExecutingContext context)
{
// Do something before the action executes.
}
public void OnActionExecuted(ActionExecutedContext context)
{
// Do something after the action executes.
ChangeLog log = new ChangeLog()
{Log = this._chengeMessage};
changeLogRepository.Add(log);
changeLogRepository.Commit();
}
}
In your controller, you can use it before actions you want to log like
[TrackMyChange("Your change log here")]
public IActionResult Post()
{
}
Reference :
Action Filter Attributes in .NET core
DbContext is shared between multiple repositories within the same HTTP request scope. You won't need UoW. Just try use one Commit() and see if all changes are saved in one transaction.
https://learn.microsoft.com/en-us/dotnet/architecture/microservices/microservice-ddd-cqrs-patterns/infrastructure-persistence-layer-implemenation-entity-framework-core
Use yourDbContext.Entry(your_entity_obj).State to get entity state. Don't log if its state is EntityState.Unchanged.
https://learn.microsoft.com/en-us/dotnet/api/microsoft.entityframeworkcore.entitystate?view=efcore-2.1
I am having a little issue that I cannot fully comprehend.
On my controller I have:
[HttpPut("{id}")]
public async Task<IActionResult> PutExercise([FromRoute] int id, [FromBody] Exercise exercise)
{
logger.LogInformation("Updating item {ID}", id);
if (!ModelState.IsValid)
return BadRequest(ModelState);
if (id != exercise.ExerciseId)
return BadRequest();
var baseExercise = await repository.GetExercise(id);
if (baseExercise == null)
return NotFound(id);
baseExercise = exercise;
await unitOfWork.CompleteAsync();
return NoContent();
}
but it is NOT updating on the database with the values provided on the PUT body request. Although on memory they are changed, the object is not pushed... But if I set a breakpoint on await unitOfWork.CompleteAsync(); and then modify baseExercise with the debugger, it does update. Why is this happening?
public class UnitOfWork : IUnitOfWork
{
private readonly FitaholicContext context;
public UnitOfWork(FitaholicContext context)
{
this.context = context;
}
public Task CompleteAsync()
{
return context.SaveChangesAsync();
}
}
--
public async Task<Exercise> GetExercise(int id, bool includeRelated = false)
{
if (!includeRelated)
return await context.Exercise.SingleOrDefaultAsync(m => m.ExerciseId == id);
return await context.Exercise
.Include(e => e.Equipments)
.ThenInclude(ee => ee.Equipment)
.Include(e => e.TypeRecord)
.SingleOrDefaultAsync(m => m.ExerciseId == id);
}
You should using async/await in the function:
public async Task CompleteAsync()
{
await context.SaveChangesAsync();
}
You are missing the link between the Repository and the UnitOfWork. In order to get the pattern working, and then persist your data, you should only access the repository's methods through the unit of work, i.e. the UoW should be just a wrapper for all the repositories related to one database, with the extra purpose of maintaining your data consistency.
In the controller your first retrieve baseExercise via repository.GetExercise(id), then you're trying to persist the data with unitOfWork.CompleteAsync(). The problem is that you didn't instruct the uniOfWork to care about the repository behaviour.
That said, it should be a quick fix.
First, set the first part of the link in the UnitOfWork:
public class UnitOfWork : IUnitOfWork
{
private readonly FitaholicContext context;
public UnitOfWork(FitaholicContext context)
{
this.context = context;
}
private Repository _repository;
public Repository repository =>
this._repository ?? (this._repository =
new Repository(this.context));
public Task CompleteAsync()
{
return context.SaveChangesAsync();
}
}
Then the second half, in the Repository:
public class Repository
{
public Repository(FitaholicContext context)
{ }
public async Task<Exercise> GetExercise(int id, bool includeRelated = false)
{ ... }
}
Last step, update PutExercise() in the controller to reflect the logic changes:
[HttpPut("{id}")]
public async Task<IActionResult> PutExercise([FromRoute] int id, [FromBody] Exercise exercise)
{
...
var baseExercise = await this.unitOfWork.repository.GetExercise(id);
...
await this.unitOfWork.CompleteAsync();
return NoContent();
}
If you need to dig a bit deeper into the Repository-UnitOfWork pattern, check the Microsoft Docs.