【问题标题】:How to ConfigureServices in Asp.Net core WebAPI from another assembly如何从另一个程序集配置 Asp.Net 核心 WebAPI 中的服务
【发布时间】:2020-01-11 00:49:49
【问题描述】:

在微服务环境中,我需要为基于合约的测试构建一个框架。 我目前正在研究如何将单个服务与其外部依赖项隔离,以便执行 Provider 测试。

我需要做的是:

  • 保持 WebApi 项目不变
  • 使用一些配置差异启动 WepApi 实例
  • 模拟选定的依赖项

我的解决方案结构是这样的:

Case-Solution/  
├── src/  
|   ├──Case.Api  
|   └──Case.Application  
├── test/  
|   ├──Case.Api.Unittest  
|   ├──(other tests)  
|   ├──Case.Pact.CunsumerTest  
|   └──Case.Pact.ProviderTest  

我已经阅读了this dotnet 中有关 Pact 测试的指南。 专注于Case.Pace.ProviderTest,我需要以编程方式从Case.Pact.ProviderTest(以及另一个WebHost for Pact it self)开始Case.Api,并替换其中的一些依赖项。

到目前为止,我得到了这个:

public class ProviderApiTests : IDisposable
{
    private string ProviderUri { get; }
    private string PactServiceUri { get; }
    private IWebHost PactServiceWebHost { get; }
    private IWebHost CasesWebHost { get; }
    private ITestOutputHelper OutputHelper { get; }
    public static IConfiguration CaseConfiguration { get; } = new ConfigurationBuilder()
        .SetBasePath(Directory.GetCurrentDirectory())
        .AddJsonFile("appsettings.json", optional: false, reloadOnChange: true)
        .AddJsonFile($"appsettings.{Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT" ?? "Production"}.json", optional: true)
        .AddEnvironmentVariables()
        .Build();


    public ProviderApiTests(ITestOutputHelper output)
    {
        OutputHelper = output;
        ProviderUri = "http://localhost:9000";
        PactServiceUri = "http://localhost:9001";

        CasesWebHost = WebHost.CreateDefaultBuilder()
            .UseUrls(ProviderUri)
            .UseStartup<CaseStartup>()
            .UseConfiguration(CaseConfiguration)
            .Build();
        CasesWebHost.Start();

        PactServiceWebHost = WebHost.CreateDefaultBuilder()
            .UseUrls(PactServiceUri)
            .UseStartup<ProviderServerStartup>()
            .Build();
        PactServiceWebHost.Start();
    }

    [Fact]
    public void EnsureProviderApiHonoursPactWithConsumer()
    {
        //Arrange
        var config = new PactVerifierConfig
        {
            Outputters = new List<IOutput>
            {
                new XUnitOutput(OutputHelper)
            },
            Verbose = true,
            CustomHeader = new KeyValuePair<string, string>("X-apikey", "XXX")
        };
        //Act //Assert
        IPactVerifier pactVerifier = new PactVerifier(config);
        pactVerifier.ProviderState($"{PactServiceUri}/provider-states")
            .ServiceProvider("CaseProvider", ProviderUri)
            .HonoursPactWith("CaseConsumer")
            .PactUri(@"..\..\..\..\..\pacts\caseconsumer-caseprovider.json")
            .Verify();
    }
    #region IDisposable Support
    //IDisposable code
    #endregion
}

在包含.UseStartup&lt;CaseStartup&gt;() 的行中,我只是从Case.Api 复制了Startup.cs 并更改了所需的依赖项,效果很好。

但我想要一个更通用的解决方案。仅仅复制代码并收工是不对的:),因为它不是通用的,也不能被其他服务重用。

所以我一直在挖掘,并想出了以下内容。

从其他程序集中添加控制器
我意识到,使用来自不同程序集的 StartUp 启动 IWebhost 不会自动从该程序集中添加控制器。这需要明确地完成。所以我这样做了:

public void ConfigureServices(IServiceCollection services)
{
    var assembly = Assembly.Load("Case.Api");
    services.AddMvc()
       .SetCompatibilityVersion(CompatibilityVersion.Version_2_1)
       .AddApplicationPart(assembly)
       .AddControllersAsServices();
    ......
}

太棒了!!!到目前为止一切顺利。

下一期:

替换依赖项:
redingthis文章,我创建了替换依赖的扩展方法:

public static void Replace<TRegisteredType>(this IServiceCollection services, TRegisteredType replcement)
{
    for (var i = 0; i < services.Count; i++)
    {
        if (services[i].ServiceType == typeof(TRegisteredType))
        {
            services[i] = new ServiceDescriptor(typeof(TRegisteredType), replcement);
        }
    }
}

所以我可以像这样替换我想要的依赖项:(在本例中为 QueryHandler)

public void ConfigureServices(IServiceCollection services)
{
    .....
    var queryHandler = Substitute.For<IQueryHandler<Query, QueryResult>>();
    queryHandler.Handle(Arg.Any<Query>()).Returns(new QueryResult(...));
    services.Replace(queryHandler);
    ......
}

但这并不能解决我复制代码的问题。

我的梦想是能够使用 Case.Api 中的 Startup.cs 并以某种方式调整 DI 以替换依赖项,而无需所有冗余代码。

任何意见都会受到高度评价。
谢谢:)

【问题讨论】:

    标签: dependency-injection asp.net-core-webapi pact-net


    【解决方案1】:

    我在使用 Pact.net 时也遇到了类似的情况。但我想使用TestServer,不幸的是 Pact.net 不支持 httpClient (Verifying pact with in-memory API)。最后,我使用了两个库的组合,可能不是验证所有场景的最佳选择。我使用 Pact.net 的消费者部分来生成合同,并使用 PactifyVerifier 部分来验证提供者是否履行了合同。验证者需要修改代码才能与 Pact.net 合约兼容。

    我还使用您的代码示例来替换使用 moq 的模拟的依赖项。

    [TestClass]
    public class EndpointShouldHonorContract
    {
    
        private HttpClient httpClient;
        private ApiWebApplicationFactory<Startup> testServerFactory;
        Mock<IRepository> repositoryMock =
            new Mock<IRepository>();
    
        public EndpointShouldHonorContract()
        {
            //omitting code... Creation of mock Data Set https://docs.microsoft.com/en-us/ef/ef6/fundamentals/testing/mocking?redirectedfrom=MSDN#testing-query-scenarios
            repositoryMock.Setup(s => s.GetQueryable()).Returns(mockDataSet.Object);
    
            testServerFactory = new ApiWebApplicationFactory<Startup>(services => 
            {
                services.Replace<IRepository>(repositoryMock.Object);
            });
    
            httpClient = testServerFactory.CreateClient();
        }
    
        [TestMethod]
        public async Task HonorContract() 
        {
              // this is my modified Pactify Verifier 
               await MyModifiedPactify
                .VerifyPact
                .PactVerifier
                .Create(httpClient)
                .Between("consumer", "provider")
                //location of contract, there is also an option where you can get contracts from a http location
                .RetrievedFromFile(@"\Pacts")
                .VerifyAsync();
        }
     }
    

    Web Api Factory : 这里我使用你的扩展来替换依赖

    public class ApiWebApplicationFactory<TStartUp>
        : WebApplicationFactory<TStartUp> where TStartUp: class
    {
    
        Action<IServiceCollection> serviceConfiguration { get; }
    
        public ApiWebApplicationFactory(Action<IServiceCollection> serviceConfiguration) : base()
        {
            this.serviceConfiguration = serviceConfiguration;
        }
    
        protected override void ConfigureWebHost(IWebHostBuilder builder)
        {
            if (this.serviceConfiguration != null)
            {
                builder.ConfigureServices(this.serviceConfiguration);
            }
        }
    
    
    }
    
    internal static class ServiceCollectionExtensions
    {
        public static void Replace<TRegisteredType>(this IServiceCollection services, TRegisteredType replacement)
        {
            for (var i = 0; i < services.Count; i++)
            {
                if (services[i].ServiceType == typeof(TRegisteredType))
                {
                    services[i] = new ServiceDescriptor(typeof(TRegisteredType), replacement);
                }
            }
    
        }
    
    }
    

    【讨论】:

    • 嗨,Isreal,你花时间回答这个问题真是太棒了。很抱歉,我无法再访问源代码,因此无法验证您的答案:(尤其是当您花了这么多时间处理此问题时。希望您能坚持下去:)欢迎来到社区
    猜你喜欢
    • 1970-01-01
    • 2020-09-08
    • 2020-09-23
    • 2021-07-13
    • 1970-01-01
    • 2017-06-24
    • 2018-04-27
    • 2020-09-26
    • 1970-01-01
    相关资源
    最近更新 更多