【问题标题】:What are the recomendation for developing .net core 2.2 web api for following bulleted points? [closed]对于以下要点,开发 .net core 2.2 web api 有什么建议? [关闭]
【发布时间】:2019-05-29 15:28:23
【问题描述】:

我正在使用 .NetCore2.2Autofac4Dapper 开发新的 WebApi。很少有非常基本的问题,因为这是我的第一个 WebApi 项目。作为这个项目的一部分,我必须编写单元测试和集成测试。

我的问题如下(示例代码如下):

  1. Task”和“Task”之间的推荐返回类型是什么?

  2. 我的项目的启动类依赖的推荐对象范围?

  3. 对于这个给定的项目结构,我真的需要 UnitOfWork 吗?

  4. 如果我按照这个设计会有什么缺陷?

  5. 有没有更好的方法来设计这个 API?

  6. 作为 TDD,我是否只需要为 API 层(控制器)和基础设施层或 Doman 层(它没有任何逻辑)编写测试用例?

  7. 我必须在控制器单元测试中包含哪些场景?

领域层:

[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);
}

基础设施层:

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 层:

[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>>();
    }

【问题讨论】:

    标签: asp.net-core-webapi solid-principles separation-of-concerns web-api-testing command-query-separation


    【解决方案1】:

    1. “Task”和“Task>”之间推荐的返回类型是什么?

    尽管 API 允许您使用接口 IActionResult,但我根本不会使用它。为什么?语义,了解真正回报的唯一方法是查看实现。如果返回的是 Task>,那就更清楚了。

    如果您需要抛出 BadRequest 或其他 http 代码,请使用 asp.net 管道为您处理。请参阅下面的注释。

    当使用任何工具生成该 API 的某种文档时,它无助于隐藏真实结果。

    2。 object 我的项目的启动类中的依赖关系范围?

    避免在调用之间共享状态,以避免未来的同步问题,只需坚持每个请求的范围依赖性。如果您有很多请求,这可能是性能问题,您可以稍后随时更改。如果这是一个问题。

    3.对于这个给定的项目结构,我真的需要 UnitOfWork 吗?
    4. 如果我按照这个设计有什么缺陷?
    5. 有没有更好的方法来设计这个API?

    希望能回答以上3个问题。我看到的问题是围绕电影模型扩展功能。例如在 ICommandRepository 上添加第四个操作。

    它接缝会垂直生长。如果多个类都实现了这个接口,这只会是一个问题,因为它们都需要改变。 (接口隔离原则)

    解决这个问题的一种方法是使用中介者模式。您的控制器将接收中介,中介将消息传递给处理它的任何人。使用这种类型的解决方案,您可以为每个操作创建一个类,因此您的系统可以随着新类添加到系统而水平增长。 (开闭原则)

    随着时间的推移,您会发​​现很多功能可以重复使用,而添加功能只是配置问题。

    6.作为 TDD,我是否还需要为 API 层(控制器)和基础设施层或域层(它没有任何逻辑)编写测试用例?

    一般来说,测试的想法是测试行为,而 TDD 应该是你的心态。根据我的经验,我发现测试整个行为比测试同一行为的多个部分要好。

    在这种情况下,API 层和持久层一样是基础架构的一部分。他们应该有自己的测试,业务规则(应用层)应该有自己的测试。应用层是你想要永远存在的东西。 Api 会随着技术的出现而改变(windows 表单、web 表单、web api 等)。关于数据库也是如此,你不知道你是否想永远坚持使用 EF。

    如果域层不提供任何行为,那么就没有什么可测试的了。

    7.我必须在控制器单元测试中包含哪些场景?

    我会使用 asp.net TestHost 进行测试:

    https://docs.microsoft.com/en-us/aspnet/core/test/integration-tests?view=aspnetcore-2.2

    测试if路由是否正确,测试失败场景和成功场景。

    一些注意事项:

    • Controller 中的异常并不意味着 BadRequest。
    • 日志记录是一个跨领域的问题,不要到处都这样做。 要么使用 asp.net 管道,要么只是将这个问题转移到 应用层。
    • MovieQueryRepository 似乎什么都不做,所以您不需要它。
    • 这只是对您的问题的一些评论,还有更多内容。只要记住要保持简单和有条理。

    希望对您有所帮助,请告诉我!

    【讨论】:

      【解决方案2】:

      第 1 点:

      您应该返回 IActionResult 以返回适当的 Http 响应,而不是返回 Task&lt;IEnumerable&lt;Movie&gt;&gt;。这样你就可以保证 SOLID 原则的 S 和 I

      第 2 点和第 3 点:

      请看这里:Entity Framework Core service default lifetime

      第 4 点:

      IQueryRepository 作为一些不好的方法名称。名称与域概念紧密耦合,它们不应该。 您未能通过关注点分离(SOLID 的 S)。 ICommandRepository 作为一个 Add 方法,它被暴露给某个控制器并且没有被使用(与 Update 相同)在这里你在接口隔离上失败了。

      MovieQueryContext 没有正确实现 IDisposable 模式,请参阅here

      MovieQueryContext 在初始化方式上与 MovieCommandContext 不同。为什么?您应该尝试在设计类型时保持连贯性,因为它会为您提供可重用性并应用 DRY 原则。

      考虑一下如果对数据库的访问更改为 mongodb,您需要付出的努力。或者如果对数据库的访问更改为远程服务有多少更改,以及您在哪里进行更改以支持该更改?

      如果 Movie 是域类型,则它不应具有任何特定数据库访问权限的属性。尽可能保持它 POCO。

      第 5 点:

      要设计您的 API,请考虑 this post。注入依赖项的方式应该考虑这些对象的生命周期。请记住,在 aspnet.core ApiControllers 中,生命周期是每个请求的。您管理资源以访问数据库的方式应考虑到这一点。

      如果您正在考虑 CQRS,控制器应该是不同的。牢记关于这些责任的关注点的分离。一个控制器负责公开一些查询 API,另一个负责处理命令。有很好的框架来支持 CQRS 看到这个scott hanselman post

      约束存在于 Route 属性而非动词上。 日志记录和异常处理应该在 ActionAttribute 或某些特定的中间件上完成,因为它们被认为是cross cutting concerns

      删除操作不符合 Http 协议。请考虑http rfc:

      GetMoviesByTitle 动作没有名称参数。

      第 6 点:

      单元测试应该测试业务逻辑,使用与当前测试相关的值来模拟所有外部依赖项。 TDD 方法考虑 3 个主要步骤 (here for more details):

      1. 第一步是实施单元测试以使其失败
      2. 迭代正在测试的方法的实现,直到它成功通过
      3. 改进被测方法的实现

      如果您想测试您的 ApiController 是否与所有集成的中间件一起使用,您需要在不使用打开端口的实际服务器的情况下放置该环境。为此,请考虑使用 TestServer(请参阅 herehere

      【讨论】:

        猜你喜欢
        • 2019-11-30
        • 1970-01-01
        • 1970-01-01
        • 2019-08-06
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2019-07-21
        • 2017-02-08
        相关资源
        最近更新 更多