I am developing a new WebApi using .NetCore2.2, Autofac4, Dapper. There are few very basic questions because this is my first WebApi project. As part of this project I have to write both unit-test and integration-test.
My questions are as follows (Sample Code is give below):
What is recommended return type between "Task< IActionResult >" and "Task< IEnumerable >"?
Recommended object Scope of the dependencies in startup class for my project?
Do I really need UnitOfWork for this given project structure?
What are the flaws if I follow this design?
Is there any better way to design this API?
As TDD do I need write test cases for API layer(Controller) and Infrastructure layer only or Doman Layer (it doesn't have any logic) as well?
What are the scenario I must include in my controller unit test?
Domain Layer:
[Table("Movie")]
public class Movie
{
[Key]
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public int ID {get;set;}
public string Title {get;set;}
}
public interface ICommandRepository<T> where T : class
{
Task CreateAsync(T entity);
Task UpdateAsync(T entity);
Task DeleteAsync(T entity);
}
public interface IQueryRepository<T> where T : class
{
Task<IEnumerable<T>> GetAllMoviesAsync();
Task<IEnumerable<T>> GetMoviesByTitleAsync(string title);
Task<T> GetMovieByIDAsync(int id);
}
Infrastructure Layer:
public class MovieCommandContext : DbContext
{
public MovieCommandContext(DbContextOptions<MovieCommandContext> options)
: base(options)
{}
public DbSet<Movie> Movies { get; set; }
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
}
}
public class MovieQueryContext : IDisposable
{
private readonly IDbConnection connection;
public MovieQueryContext(string connectionString)
{
connection = new SqlConnection(connectionString);
}
public async Task<IEnumerable<Movie>> GetAllMovies()
{
// Use Dapper->QueryAsync
throw new NotImplementedException();
}
...
public void Dispose()
{
if (connection?.State == ConnectionState.Open)
connection.Close();
}
}
public class MovieCommandRepository : ICommandRepository<Movie>
{
private readonly MovieCommandContext context;
public MovieCommandRepository(MovieCommandContext dbContext)
{
context = dbContext;
}
public async Task CreateAsync(Movie movie)
{
await context.AddAsync<Movie>(movie);
await context.SaveChangesAsync();
}
public async Task UpdateAsync(Movie movie)
{
var entity = context.Attach<Movie>(movie);
context.Entry<Movie>(movie).State = EntityState.Modified;
await context.SaveChangesAsync();
}
public async Task DeleteAsync(Movie movie)
{
context.Remove<Movie>(movie);
await context.SaveChangesAsync();
}
}
public class MovieQueryRepository : IQueryRepository<Movie>
{
private readonly MovieQueryContext context;
public MovieQueryRepository(MovieQueryContext dbContext)
{
context = dbContext;
}
public async Task<IEnumerable<Movie>> GetAllMoviesAsync()
{
return await context.GetAllMovies();
}
public async Task<IEnumerable<Movie>> GetMoviesByTitleAsync(string title)
{
return await context.GetMovieByName(title);
}
public async Task<Movie> GetMovieByIDAsync(int id)
{
return await context.GetMovieByID(id);
}
}
API Layer:
[Route("api/sample")]
[ApiController]
public class SampleController : ControllerBase
{
private readonly ICommandRepository<Movie> movieCommand;
private readonly IQueryRepository<Movie> movieQuery;
public SampleController(ICommandRepository<Movie> command, IQueryRepository<Movie> query)
{
movieCommand = command;
movieQuery = query;
}
[HttpGet]
public async Task<IActionResult> GetMoviesAsync()
{
try
{
var movies = await movieQuery.GetAllMoviesAsync();
return Ok(movies);
}
catch
{
// TODO: Logging
return BadRequest();
}
}
[Route("{name:alpha}")]
[HttpGet]
public async Task<IActionResult> GetMoviesByTitle(string movieTitle)
{
try
{
var movies = await movieQuery.GetMoviesByTitleAsync(movieTitle);
return Ok(movies);
}
catch
{
// TODO: Logging
return BadRequest();
}
}
[Route("{movieID:int:min(1)}")]
[HttpGet]
public async Task<IActionResult> GetMovieByID(int movieID)
{
try
{
var movie = await movieQuery.GetMovieByIDAsync(movieID);
return Ok(movie);
}
catch
{
// TODO: Logging
return BadRequest();
}
}
[Route("")]
[HttpDelete("{id:int:min(1)}")]
public async Task<IActionResult> Delete(int id)
{
try
{
var movie = await movieQuery.GetMovieByIDAsync(id);
if (movie == null)
return BadRequest();
await movieCommand.DeleteAsync(movie);
return Ok();
}
catch
{
// TODO: Logging
return BadRequest();
}
}
}
Startup.cs:
private void ConfigureContainer(ContainerBuilder builder)
{
var contextOptions = new DbContextOptionsBuilder<MovieCommandContext>()
.UseSqlServer(Configuration.GetConnectionString("DefaultConnection"))
.Options;
builder.RegisterType<MovieCommandContext>()
.WithParameter("options", contextOptions);
builder.RegisterType<MovieQueryContext>()
.AsSelf()
.WithParameter("connectionString",Configuration.GetConnectionString("DefaultConnection"));
builder.RegisterType<MovieCommandRepository>().As<ICommandRepository<Movie>>();
builder.RegisterType<MovieQueryRepository>().As<IQueryRepository<Movie>>();
}
1. What is recommended return type between "Task< IActionResult >" and "Task< IEnumerable < Movie > >"?
Even though the API allows you yo use the interface IActionResult, I wouldn't use it at all. Why? Semantics, the only way to know what the true return is, is to see the implementation. It's clearer if the returns is Task< IEnumerable< Movie>>.
If you need to throw a BadRequest or other http code, use the asp.net pipeline to handle this for you. See Notes below.
When using whatever tool to generate some sort of documentation of this API it won't help hiding the real result.
2. object Scope of the dependencies in startup class for my project?
Avoid sharing state between calls, to avoid future issues with synchronization just stick to scope dependencies per request. This may be a performance issue if you have a lot of requests, you can always change this later on. If it's an issue at all.
3. I really need UnitOfWork for this given project structure?
4. What are the flaws if I follow this design?
5. Is there any better way to design this API?
In hope of answering the above 3 questions. The problem I see is extending the functionality around Movie model. e.g. add a fourth action on ICommandRepository.
It seams it will grow vertically. It will only be a problem if multiple classes implement this interface, because they will all need to change. (Interface Segregation Principle)
A way to solve this is to use the Mediator pattern. Your controller will receive the mediator and the mediator will deliver the message to whoever handles it. With this type of solution you could have a class per operation and therefore your system can grow horizontally as new classes are added to the system. (Open Close Principle)
In time, you'll see that a lot of functionality can be reused and adding features is just a matter of configuration.
6. As TDD do I need write test cases for API layer(Controller) and Infrastructure layer only or Domain Layer (it doesn't have any logic) as well?
The idea of Testing in general is to test behavior, when TDDing that should be your mindset. In my experience I found that testing the whole behavior is better than multiple parts of the same behavior.
In this case, the API Layer is part of the infrastructure as is the persistence layer. They should have their own tests, the business rules (Application layer) should have their own tests. The application layer is what you want to last forever. The Api will change as technologies appear (windows forms, web forms, web apis, etc.) Regarding databases as well, you don't know if you want to stick with EF forever.
If the domain layer doesn't provide any behavior then there is nothing to test.
7. What are the scenario I must include in my controller unit test?
I would test using asp.net TestHost:
https://learn.microsoft.com/en-us/aspnet/core/test/integration-tests?view=aspnetcore-2.2
Test the if routing is correct, test failing scenarios and successful scenarios.
Some notes:
Hope it helped, let me know!