ASP.NET MVC WebAPI从异步任务中创建ViewModel

3

我使用ASP.NET MVC WebAPI编写Web应用程序,并希望将当前同步代码转换为异步以进行优化。问题在于,我需要从存储库中获取多个对象来填充ViewModel。这些来自存储库的调用应该是异步的。

假设我具有遵循此接口的存储库调用签名

public interface ICompanyRepository
{
    IEnumerable<Company> GetCompanies();
    IEnumerable<Address> GetAddresses();
}

视图模型定义
public class CompaniesFullViewModel
{
    public IEnumerable<Company> Companies { get; set; }
    public IEnumerable<Address> Addresses { get; set; }
}

控制器:

public class CompanyController
{
    public readonly ICompanyRepository Repository { get; private set; }

    public CompanyController(IRepository repository)
    {
        Repository = repository;
    }

    [ResponseType(typeof(CompaniesFullViewModel))]
    public HttpResponseMessage Get()
    {
        var companies = Repository.GetCompanies();
        var addresses = Repository.GetAddresses();

        HttpStatusCode statusCode = companies.Any()
             ? HttpStatusCode.OK
             : HttpStatusCode.PartialContent;

        return
            Request.CreateResponse(
                statusCode,
                new CompaniesFullViewModel
                {
                    Companies = companies,
                    Addresses = addresses
                });
    }
}

此外,我已经在控制器中实现了测试:
[TestClass]
public sealed class CompanyTestController : BaseTestController
{
    #region Fields

    private static Mock<ICompanyRepository> _repositoryMock;
    private static CompanyController _controller;

    #endregion

    [ClassInitialize]
    public static void Initialize(TestContext testContext)
    {
        // Mock repository
        _repositoryMock = new Mock<ICompanyRepository>();
        DependencyResolver.Default.Container.RegisterInstance(_repositoryMock.Object);

        // Create controller
        _controller =
            DependencyResolver.Default.Container.Resolve<CompanyController>();

        // Init request
        _controller.Request = new HttpRequestMessage();
        _controller.Request.SetConfiguration(new HttpConfiguration());
    }

    [ClassCleanup]
    public static void Cleanup()
    {
        _controller.Dispose();
    }

    [TestMethod]
    public void Get_ActionExecutes_ReturnsEmptyCompaniesViewModel()
    {
        var companies = new List<Company>();
        var addresses = new List<Address>();

        // Setup fake method
        _repositoryMock
            .Setup(c => c.GetCompanies())
            .Returns(companies);
        _repositoryMock
            .Setup(c => c.GetAddresses())
            .Returns(addresses);

        // Execute action
        var response = _controller.Get();

        // Check the response
        Assert.AreEqual(HttpStatusCode.PartialContent, response.StatusCode);
    }
}

如果存储库是异步的,签名如下,我该如何将控制器转换为异步?
public interface ICompanyRepository
{
    Task<IEnumerable<Company>> GetCompaniesAsync();
    Task<IEnumerable<Address>> GetAddressesAsync();
}

也许我漏掉了什么。控制器签名不只是改为public async Task<HttpResponseMessage> Get()吗?然后你只需要await你现在异步的存储库调用即可? - Brendan Green
@Andree:我想将当前同步代码转换为异步以进行优化。 你确定这样做会达到你的目的吗?记住,“异步”并不等于“更快”。 - Stephen Cleary
@StephenCleary 我知道这并不一定意味着更快,但我预计服务器将承受高负载,并且部分查询可能运行时间较长,所以我决定自己进行基准测试,比较同步和异步解决方案。 - Andree
1个回答

4
你需要做的是将控制器操作改为async,并将返回类型更改为Task<>。然后你可以等待异步仓库调用:
[ResponseType(typeof(CompaniesFullViewModel))]
public async Task<HttpResponseMessage> Get() // async keyword. 
{
    var companies = await Repository.GetCompaniesAsync(); // await
    var addresses = await Repository.GetAddressesAsync(); // await

    HttpStatusCode statusCode = companies.Any()
         ? HttpStatusCode.OK
         : HttpStatusCode.PartialContent;

    return
        Request.CreateResponse(
            statusCode,
            new CompaniesFullViewModel
            {
                Companies = companies,
                Addresses = addresses
            });
}

按照惯例,您也可以将控制器操作的名称更改为以Async结尾,但如果您正在使用RESTful约定和/或路由属性,则控制器操作的实际名称并不重要。
测试
我使用XUnitNUnit,但似乎MSTest也支持测试异步方法,而Moq还提供了设置的Async版本。
[Test]
public async Task Get_ActionExecutes_ReturnsEmptyCompaniesViewModel() // async Task
{
    var companies = new List<Company>();
    var addresses = new List<Address>();

    // Setup fake method
    _repositoryMock
        .Setup(c => c.GetCompaniesAsync())
        .ReturnsAsync(companies); // Async
    _repositoryMock
        .Setup(c => c.GetAddressesAsync())
        .ReturnsAsync(addresses); // Async

    // Execute action
    var response = await _controller.Get(); // Await

    // Check the response
    Assert.AreEqual(HttpStatusCode.PartialContent, response.StatusCode);
    _repositoryMock.Verify(m => m.GetAddressesAsync(), Times.Once);
    _repositoryMock.Verify(m => m.GetCompaniesAsync(), Times.Once);
}

顺便提一下,看起来您正在使用Setter依赖注入。另一个选择是使用构造函数注入,它的好处是确保类始终处于有效状态(即在等待设置依赖项时没有瞬态状态)。这也允许依赖项(在本例中为您的仓库)被设置为只读


谢谢你的回答。我实际上使用构造函数注入,只是没有提供构造函数。但还是谢谢你的提醒。 - Andree
欢迎 :) 我看到了 public ICompanyRepository 属性和公共 setter,就默认使用了 setter 注入。顺便说一下,在理论上,你也可以使用 Task.WhenAll 异步并行 处理这两个仓储调用,但这需要仓储实例是线程安全的,这可能不是情况。如果需要并行处理,另一种选择是注入一个 RepositoryFactory,然后为这两个调用创建两个单独的 Repo 实例。 - StuartLC
1
感谢这篇文章,它真的帮助我总结了关于这个问题的事实。 - Andree

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接