As an exercise I was asked to moved logic from controller to service layer. With methods such as Get or GetById it wasn't a problem. Difficult part is to move Update and Patch methods. Below, you will find what I do have in Controler class and how I've changed them to the new requirement.
ToursController.cs before change
public class ToursController : ControllerBase
{
private readonly ToursRepository _repo;
private readonly IMapper _mapper;
public ToursController(ToursRepository repository, IMapper mapper)
{
_repo = repository ?? throw new ArgumentNullException(nameof(repository));
_mapper = mapper ?? throw new ArgumentNullException(nameof(mapper));
}
[HttpPut("{id}")]
public async Task<ActionResult> Update(int id, TourForUpdateDto updateDto)
{
var tour = await _repo.GetByIdAsync(id);
if (tour is null)
{
return NotFound();
}
_mapper.Map(updateDto, tour);
await _repo.SaveChangesAsync();
return NoContent();
}
[HttpPatch("{id}")]
public async Task<ActionResult> Patch(int id, JsonPatchDocument patchDocument)
{
var tour = await _repo.GetByIdAsync(id);
if (tour is null)
{
return NotFound();
}
var updateDto = _mapper.Map<TourForUpdateDto>(tour);
patchDocument.ApplyTo(updateDto);
if (!ModelState.IsValid)
{
return BadRequest(ModelState);
}
if (!TryValidateModel(patchDocument))
{
return BadRequest(ModelState);
}
_mapper.Map(updateDto, tour);
await _repo.SaveChangesAsync();
return NoContent();
}
}
ToursController.cs after change
public class ToursController : ControllerBase
{
private readonly ToursRepository _repo;
private readonly IMapper _mapper;
private readonly ToursService _toursService;
public ToursController(ToursRepository repository, IMapper mapper)
{
_repo = repository ?? throw new ArgumentNullException(nameof(repository));
_mapper = mapper ?? throw new ArgumentNullException(nameof(mapper));
_toursService = toursService ?? throw new ArgumentNullException(nameof(toursService));
}
[HttpPut("{id}")]
public async Task<ActionResult> Update(int id, TourForUpdateDto updateDto)
{
if (!_toursService.Exists(id))
{
return NotFound();
}
await _toursService.Update(id, updateDto);
return NoContent();
}
[HttpPatch("{id}")]
public async Task<ActionResult> Patch(int id, JsonPatchDocument patchDocument)
{
// No idea how to update this method.
var tour = await _repo.GetByIdAsync(id);
if (tour is null)
{
return NotFound();
}
var updateDto = _mapper.Map<TourForUpdateDto>(tour);
patchDocument.ApplyTo(updateDto);
if (!ModelState.IsValid)
{
return BadRequest(ModelState);
}
if (!TryValidateModel(patchDocument))
{
return BadRequest(ModelState);
}
_mapper.Map(updateDto, tour);
await _repo.SaveChangesAsync();
return NoContent();
}
}
ToursService.cs after change
using AutoMapper;
using BandManager.Api.Models;
using BandManager.Api.Services.Repositories;
public class ToursService
{
private readonly ToursRepository _repo;
private readonly IMapper _mapper;
public ToursService(ToursRepository repo, IMapper mapper)
{
_repo = repo ?? throw new ArgumentNullException(nameof(repo));
_mapper = mapper ?? throw new ArgumentNullException(nameof(mapper));
}
public async Task Update(int id, TourForUpdateDto updateDto)
{
var tour = await GetByIdAsync(id);
if (tour is null)
return;
_mapper.Map(updateDto, tour);
await _repo.SaveChangesAsync();
}
public async Task<TourDto?> GetByIdAsync(int id)
{
return _mapper.Map<TourDto>(await _repo.GetByIdAsync(id));
}
public bool Exists(int id)
{
return _repo.Exists(id);
}
}
Questions and thoughts
Should I ever return objects which are implementing IActionResult or any MVC related types from my services? My gut feeling is - no.
Should service be able to use AutoMapper to convert entities to DTOs? I think, yes.
Should my service, in the same method call, be able to call SaveChangesAsync on repository?
Related
I am learning to code in c# and I am developing an API application. My GET method works but I have issues with my DELETE, it returns a success code(200) but it does not delete from my database which is connected to my application. I am using the unit of work and repository patterns and my code is as follows:
Controller code:
private readonly IOrderService _orderService;
public OrdersController(IOrderService orderService)
{
_orderService = orderService;
}
[HttpDelete("{id}")]
public async Task<ActionResult> RemoveOrder(int id)
{
try
{
await _orderService.Delete(id);
return StatusCode(200);
}
catch (Exception ex)
{
return StatusCode(500);
}
}
Service Interface
public interface IOrderService
{
Task<Order> Get(int id);
Task Add(Order order);
Task Delete(int id);
Task Update(int id, Order order);
Task<IEnumerable<Order>> GetAllOrdersAsync();
Task<IEnumerable<OrderDTO>> GetOrdersToCityAsync(string cityName);
Task<OrderDTO> GetEmployeeOrdersToCountryAsync
(
string countryName, string employeeLastName
);
}
Service class:
public class OrderService : IOrderService
{
private readonly IUnitOfWork _unitOfWork;
public OrderService(IUnitOfWork unitOfWork)
{
_unitOfWork = unitOfWork;
}
public async Task Delete(int id)
{
try
{
var order = await _unitOfWork.OrderRepository.Get(id);
_unitOfWork.OrderRepository.Delete(order);
await _unitOfWork.CommitAsync();
}
catch (Exception e)
{
await _unitOfWork.RollbackAsync();
throw;
}
}
}
Unit of work class:
public class UnitOfWork : IUnitOfWork
{
private readonly NorthwindContext _db;
private List<object> _customRepositories = new List<object>();
private IProductRepository _productRepository;
private IOrderRepository _orderRepository;
public UnitOfWork(
NorthwindContext db,
IProductRepository ProductRepository,
IOrderRepository orderRepository
)
{
_db = db;
_productRepository = ProductRepository;
_orderRepository = orderRepository;
}
uow interface:
public interface IUnitOfWork
{
IProductRepository ProductRepository { get; }
IOrderRepository OrderRepository { get; }
Task CommitAsync();
Task RollbackAsync();
}
Order repository interface which extends my genericRepository:
public interface IOrderRepository : IGenericRepository<Order>
{
Task<IEnumerable<OrderDTO>> GetOrdersToCityAsync(string cityName);
Task<OrderDTO> GetEmployeeOrdersToCountryAsync
(
string countryName, string employeeLastName
);
}
Order repository:
public class OrderRepository : GenericRepository<Order>, IOrderRepository
{
private readonly NorthwindContext _northwindContext;
public OrderRepository(NorthwindContext db) : base(db)
{
_northwindContext = db;
}
generic repository:
public class GenericRepository<T> : IGenericRepository<T> where T : class
{
protected readonly NorthwindContext _db;
public GenericRepository(NorthwindContext db)
{
_db = db;
}
public void Delete(T entity)
{
_db.Set<T>().Remove(entity);
}
Please ignore if the curly braces are not closed properly, in my application they are.
Please check the following items:
Unit of work implementation calls SaveChanges() on CommitAsync implementation.
NorthwindContext object instance passed to repositories is the same instance as the one injected into UnitOfWork class. Otherwise you'll be working in different transactions.
You could delete an entry from database while using the following code.
[HttpDelete{"id"}]
Public async<Task<ActionResult>> DeleteItem (int id)
{ try{
Var itemToDelete = await UnitofWork.OrderRepository.GetAsync(id);
If (Id!= null) {
await Orderservice.Delete(itemToDelete);
var result = await UnitofWork.SaveChangesAsync();
If (result) return StatusCode(200);
Else return BadRequest();
return ok(itemToDelete);
} catch(exception ex) {
Throw new Error("Problem while deleting!");
}
Return BadRequest();
}
}
Just check if the item you would want ro delete is null or not. Then attempt to delete it from database. Hoping it would give you an idea.
I'm currently working with JsonResult. There's a problem: if I call JsonResult in my UserService, I won't be able to call it with a parameter, but if I call it like the same in the UserController, it works with my parameter. So my question now is, the architecture is the same as in the controller so what's wrong?
UserService.cs:
public class UserService : IUserService
{
private readonly IMapper mapper;
private readonly ILogger<UserService> logger;
public UserService(
IMapper mapper,
ILogger<UserService> logger)
{
this.mapper = mapper;
this.logger = logger;
}
private static IList<Contact> GetStaticContacts(string fileName)
{
var jsonText = System.IO.File.ReadAllText(fileName);
var contacts = JsonSerializer.Deserialize<IList<Contact>>(jsonText);
return JsonResult(contacts);
}
Task<IList<Contact>> IUserService.GetNationalCoordinators()
{
return new JsonResult(GetStaticContacts("Test1.json"));
}
Task<IList<Contact>> IUserService.GetLocalCoordinators()
{
return new JsonResult(GetStaticContacts("Test2.json"));
}
Task<IList<Contact>> IUserService.GetMedicalAdvisors()
{
return new JsonResult(GetStaticContacts("Test3.json"));
}
}
UsersController:
public async Task<IActionResult> GetLocalCoordinators(CancellationToken cancellationToken = default)
{
var contacts = await userService.GetLocalCoordinators();
var result = mapper.Map<IList<ContactDto>>(contacts);
return new JsonResult(result);
}
Check this out -> JsonResult in services layer
Also the documentation about JsonResult here https://learn.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.mvc.jsonresult?view=aspnetcore-6.0 specifies that the JsonResult object comes from ActionResult which is used in the Controller.
I have controller's method which return mapped Dto. In case I find nothing in my Db (null) I want to return NotFound(), else I want to return Ok(item). But IDE shows me error (like in title). Code below:
[Route("api/[controller]")]
[ApiController]
public class ArticleController : ControllerBase
{
private readonly IArticleRepository _repository;
private readonly IMapper _mapper;
public ArticleController(IArticleRepository articleRepository,IMapper mapper)
{
_repository = articleRepository;
_mapper = mapper;
}
// GET: api/Article/5
[HttpGet("{id}", Name = "Get")]
public async Task<ArticleReadDto> Get(int id)
{
var item = await _repository.GetArticle(id);
if(item == null)
{
return NotFound(item);
}
var mapped_item = _mapper.Map<ArticleReadDto>(item);
return Ok(item);
}```
Change
public async Task<ArticleReadDto>
to:
async Task<IActonResult<ArticleReadDto>>
and
var mapped_item = _mapper.Map<ArticleReadDto>(item);
return Ok(item);
To
var mapped_item = _mapper.Map<ArticleReadDto>(item);
return Ok(mapped_item );
and
return NotFound(item);
to
return NotFound();
//or
return NotFound( new ArticleReadDto());
This is because you are calling the methods Ok and NotFound that are fulfilled by the ControllerBase class.
Documentation: https://learn.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.mvc.controllerbase?view=aspnetcore-5.0
The methods that you are using return an ActionResult: https://learn.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.mvc.controllerbase.ok?view=aspnetcore-5.0#Microsoft_AspNetCore_Mvc_ControllerBase_Ok
If you wish to return these statuses then having a IActionResult return type might be more appropriate:
[HttpGet("{id}", Name = "Get")]
public async Task<IActionResult> Get(int id)
{
var item = await _repository.GetArticle(id);
if(item == null)
{
return NotFound(item);
}
var mapped_item = _mapper.Map<ArticleReadDto>(item);
return Ok(item);
}
I'm working on ASP.NET Core WebAPI with EFCore 3.
How can I handle transactions in Generic Repository approach? I'm aware of Repository patterns pros and cons, as well as its generic repository approach, but something that confuses me is how to handle transactions while using them??
Below is BaseRepository I use. I have a problem implementing transaction like this:
Add Employee
Get created EmployeeId
Add Employee's address with just created EmployeeId
The problem I have is that I must call SaveChanges to get autogenerated Employee's Id, to be able to insert Employee's address. SaveChanges commits the transaction (created by EF Core).
How can I do this in transaction, while using BaseRepository? Should I expose BeginTransaction and Commit methods in BaseRepository, so that Service class can create the transaction and close it?
public abstract class BaseRepository<TEntity> : IRepository<TEntity>
where TEntity : class, IEntity
{
private readonly DemoDb_context _context;
public BaseRepository(DemoDb_context context)
{
_context = context;
}
public async Task<TEntity> GetById(int id)
{
return await _context.Set<TEntity>().FindAsync(id);
}
public async Task<List<TEntity>> GetAll()
{
return await _context.Set<TEntity>().ToListAsync();
}
public async Task<TEntity> Add(TEntity entity)
{
_context.Set<TEntity>().Add(entity);
await _context.SaveChangesAsync();
return entity;
}
public async Task<TEntity> Delete(int id)
{
var entity = await _context.Set<TEntity>().FindAsync(id);
if (entity == null)
{
return entity;
}
_context.Set<TEntity>().Remove(entity);
await _context.SaveChangesAsync();
return entity;
}
public async Task<TEntity> Update(TEntity entity)
{
_context.Entry(entity).State = EntityState.Modified;
await _context.SaveChangesAsync();
return entity;
}
}
This is my service class, where I think, I should add transaction:
public class EmployeeService
{
private readonly IEmployeeRepository _employeeRepository;
private readonly IAddressRepository _addressRepository;
private readonly IMapper _mapper;
public EmployeeService(IEmployeeRepository employeeRepository, IAddressRepository addressRepository, IMapper mapper)
{
_employeeRepository = employeeRepository;
_addressRepository = addressRepository;
_mapper = mapper;
}
public async Task<EmployeeDto> Add(EmployeeDto employeeDto)
{
// TODO: Start transaction here ???
var employee = _mapper.Map<Employee>(employeeDto);
var addedEmployee = await _employeeRepository.Add(employee);
var employeeAddress = Generate_Employee_Address_Entity_With_EmployeeId(addedEmployee);
var addedAddress = await _addressRepository.Add(employeeAddress);
var output = Generate_Employee_Dto(addedEmployee, addedAddress);
// TODO: Commit transaction here ???
return outputs;
}
}
One of the advantages of entity framework is that you can avoid using transactions in base case scenarios like the one you showed us. I would suggest the following:
In BaseRepository remove the SaveChangesAsync from Add, Update and Delete methods and expose a method called SaveDbChangesAsync().
public abstract class BaseRepository<TEntity> : IRepository<TEntity>
where TEntity : class, IEntity
{
private readonly DemoDb_context _context;
public BaseRepository(DemoDb_context context)
{
_context = context;
}
public async Task<TEntity> GetById(int id)
{
return await _context.Set<TEntity>().FindAsync(id);
}
public async Task<List<TEntity>> GetAll()
{
return await _context.Set<TEntity>().ToListAsync();
}
public async Task<TEntity> Add(TEntity entity)
{
_context.Set<TEntity>().Add(entity);
return entity;
}
public async Task<TEntity> Delete(int id)
{
var entity = await _context.Set<TEntity>().FindAsync(id);
if (entity == null)
{
return entity;
}
_context.Set<TEntity>().Remove(entity);
return entity;
}
public async Task<TEntity> Update(TEntity entity)
{
_context.Entry(entity).State = EntityState.Modified;
return entity;
}
public async Task SaveDbChangesAsync()
{
await _context.SaveChangesAsync();
}
}
In this way you do not store changes to the database but changes are only tracked in memory in the context.
When you are done with all your logic you can call the SaveDbChangesAsync() method from your service class (in general the class that is using the repository) and persist the change.
public class EmployeeService
{
private readonly IEmployeeRepository _employeeRepository;
private readonly IAddressRepository _addressRepository;
private readonly IMapper _mapper;
public EmployeeService(IEmployeeRepository employeeRepository, IAddressRepository addressRepository, IMapper mapper)
{
_employeeRepository = employeeRepository;
_addressRepository = addressRepository;
_mapper = mapper;
}
public async Task<EmployeeDto> Add(EmployeeDto employeeDto)
{
// TODO: Start transaction here ???
var employee = _mapper.Map<Employee>(employeeDto);
var addedEmployee = await _employeeRepository.Add(employee);
var employeeAddress = Generate_Employee_Address_Entity_With_EmployeeId(addedEmployee);
var addedAddress = await _addressRepository.Add(employeeAddress);
var output = Generate_Employee_Dto(addedEmployee, addedAddress);
//// Here instead of commit transaction, we save changes to
//// the database. If anything goes wrong changes will be discarded
//// anyways when you context gets out of scope;
await SaveDbChangesAsync();
return outputs;
}
Benefits:
Avoid to use a performance heavy operation like Transactions
You use EF in the right way
However if you insist using transactions you can do the following:
using (EntitiesContext context = new EntitiesContext())
{
using (var transaction = context.Database.BeginTransaction())
{
}
}
I try to start asp.net core web api routing attribute as default route but when I access routing with parameter, I could not get any response
[Route("api/[controller]")]
[ApiController]
public class WarehousesController : ControllerBase
{
private readonly ApplicationDbContext _context;
public WarehousesController(ApplicationDbContext context)
{
_context = context;
}
//http://localhost:2394/api/Warehouses/Project/1 (Not working)
[HttpGet("Project/{projectId}")]
public async Task<IActionResult> GetWarehouseByProjectId([FromRoute] int projectId)
{
if (!ModelState.IsValid)
{
return BadRequest(ModelState);
}
var warehouse = await _context.warehouses.Include(x => x.Projects).Where(y => y.Projects.FirstOrDefault().ProjectId == projectId).ToListAsync();
if (warehouse == null)
{
return NotFound();
}
return Ok(warehouse);
}
}
Try with this one .This works fine for me
[HttpGet("Project/{projectId}")]
public async Task<IActionResult> GetWarehouseByProjectId([FromRoute] int projectId)
{
if (!ModelState.IsValid)
{
return BadRequest("Invalid state");
}
var warehouse =await _context.warehouses.FindAsync(projectId);
if (warehouse == null)
{
return NotFound("not found");
}
return Ok(warehouse);
}