【问题标题】:How to call .NET Core Web Api from another service, both running in seperate docker containers如何从另一个服务调用 .NET Core Web Api,两者都在单独的 docker 容器中运行
【发布时间】:2020-01-16 01:16:41
【问题描述】:

在我的场景中,我有两个在两个单独的 docker 容器上运行的 .NET Core Web API。

第一个服务被称为Catalog.API,这是控制器的代码:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Catalog.API.Models;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;

namespace Catalog.API.Controllers
{
    [ApiController]
    [Route("api/[controller]")]
    public class CatalogController : ControllerBase
    {
        private readonly CatalogItemContext _context;

        public CatalogController(CatalogItemContext context)
        {
            _context = context;
        }


        [Route("items")]
        [HttpGet]
        public async Task<ActionResult<List<CatalogItem>>> GetItemsAsync()
        {
            var items = await _context.CatalogItems.ToListAsync();
            if (items == null)
            {
                return NotFound();
            }
            return items;
        }

        [Route("items/{id}")]
        [HttpGet]
        public async Task<ActionResult<CatalogItem>> GetItemByIdAsync(int id)
        {
            var item = await _context.CatalogItems.SingleOrDefaultAsync(model => model.Id == id);
            if (item == null)
            {
                return NotFound();
            }
            return item;
        }

        [HttpPost]
        [Route("items")]
        public async Task<ActionResult> CreateProductAsync([FromBody] CatalogItem product)
        {
            var item = new CatalogItem(product.Name, product.Price, product.Count);
            _context.CatalogItems.Add(item);
            await _context.SaveChangesAsync();
            return CreatedAtAction(nameof(GetItemByIdAsync), new {id = item.Id}, null);
        }

        [HttpPut]
        [Route("items")]
        public async Task<ActionResult> UpdateProductAsync([FromBody] CatalogItem productToUpdate)
        {
            try
            {
                _context.Entry(productToUpdate).State = EntityState.Modified;
                await _context.SaveChangesAsync();
                return CreatedAtAction(nameof(GetItemByIdAsync), new {id = productToUpdate.Id}, null);
            }
            catch (Exception ex)
            {
                return BadRequest();
            }
        }

        [HttpPost]
        [Route("items/{id}")]
        public async Task<ActionResult> DeleteProductById(int id)
        {
            var itemToDelete = _context.CatalogItems.SingleOrDefault(model => model.Id == id);
            if (itemToDelete == null)
            {
                return NotFound();
            }

            _context.CatalogItems.Remove(itemToDelete);
            await _context.SaveChangesAsync();
            return NoContent();
        }
    }
}

如你所见,我有一些基本的方法,目前没有什么特别的。此服务在localhost:80 上运行(例如,http://localhost:80/api/catalog/items

这是 Catalog.API 的 dockerfile,通过 docker-compose.yml 调用:

FROM mcr.microsoft.com/dotnet/core/aspnet:2.2 AS base
WORKDIR /app
EXPOSE 80

FROM mcr.microsoft.com/dotnet/core/sdk:2.2 AS build
WORKDIR /src

COPY src/Services/Catalog/Catalog.API/Catalog.API.csproj /src/csproj-files/

WORKDIR ./csproj-files
RUN dotnet restore


WORKDIR /src

COPY . .
WORKDIR /src/src/Services/Catalog/Catalog.API/
RUN dotnet publish -c Release -o /app

FROM build AS publish

FROM base as final
WORKDIR /app
COPY --from=publish /app .
ENTRYPOINT ["dotnet", "Catalog.API.dll"]

第二个服务被称为Basket.API,这是控制器:

using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Basket.API.Models;
using Basket.API.Services;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;

namespace Basket.API.Controllers
{
    [ApiController]
    [Route("api/[controller]")]
    public class BasketController : ControllerBase
    {
        private readonly BasketContext _context;
        private readonly IBasketService _basketService;

        public BasketController(BasketContext context, IBasketService basketService)
        {
            _context = context;
            _basketService = basketService;
        }

        [HttpGet]
        [Route("entries")]
        public async Task<ActionResult<List<Models.Basket>>> GetAllBasketAsync()
        {
            var basketList = await _context.UserBaskets.Include(basket => basket.Items).ToListAsync(); //include needed to load Items List
            if (basketList == null)
            {
                return NoContent();
            }

            return basketList;
        }

        [HttpGet]
        [Route("entries/{id}")]
        public async Task<ActionResult<Models.Basket>> GetBasketByIdAsync(int id)
        {
            var basket = await _context.UserBaskets.Where(b => b.UserId == id).Include(m => m.Items).SingleOrDefaultAsync();
            if (basket == null)
            {
                return NoContent();
            }

            return basket;
        }

        [HttpGet]
        [Route("test")]
        public async Task<ActionResult> TestCall()
        {
            var test1 = await _basketService.GetBasketByIdAsync(1);
            return Ok(test1);
        }
    }
}

在控制器内部,我使用了一个名为 BasketService 的类,它被注入到 Startup.cs

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Basket.API.Models;
using Basket.API.Services;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.HttpsPolicy;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;

namespace Basket.API
{
    public class Startup
    {
        public Startup(IConfiguration configuration)
        {
            Configuration = configuration;
        }

        public IConfiguration Configuration { get; }

        // This method gets called by the runtime. Use this method to add services to the container.
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddDbContext<BasketContext>(builder =>
            {
                builder.UseSqlServer(Configuration.GetConnectionString("Default"));
            });

            services.AddHttpClient<IBasketService, BasketService>();
            services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2);
        }

        // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
        public void Configure(IApplicationBuilder app, IHostingEnvironment env)
        {
            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }
            else
            {
                // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
                app.UseHsts();
            }

            app.UseHttpsRedirection();
            app.UseMvc();
        }
    }
}

这是BasketService.cs的代码

using System.Collections.Generic;
using System.Net.Http;
using System.Threading.Tasks;
using Newtonsoft.Json;

namespace Basket.API.Services
{
    public class BasketService : IBasketService
    {
        private readonly HttpClient _client;

        public BasketService(HttpClient client)
        {
            _client = client;
        }

        public async Task<IEnumerable<Models.Basket>> GetAllBasketAsync()
        {
            var stringContent = await _client.GetStringAsync("http://localhost:80/api/basket/entries");
            return JsonConvert.DeserializeObject<List<Models.Basket>>(stringContent);
        }

        public async Task<Models.Basket> GetBasketByIdAsync(int id)
        {
            var stringContent = await _client.GetStringAsync("http://localhost:80/api/basket/entries/" + id);
            return JsonConvert.DeserializeObject<Models.Basket>(stringContent);
        }
    }
}

Basket.APIlocalhost:81 上运行,这是 Dockerfile,它也被 docker-compose.yml 调用:

FROM mcr.microsoft.com/dotnet/core/aspnet:2.2 AS base
WORKDIR /app
EXPOSE 81

FROM mcr.microsoft.com/dotnet/core/sdk:2.2 AS build
WORKDIR /src

COPY src/Services/Basket/Basket.API/Basket.API.csproj /src/csproj-files/

WORKDIR ./csproj-files
RUN dotnet restore

WORKDIR /src

COPY . .
WORKDIR /src/src/Services/Basket/Basket.API/
RUN dotnet publish -c Release -o /app

FROM build AS publish

FROM base as final
WORKDIR /app
COPY --from=publish /app .
ENTRYPOINT ["dotnet", "Basket.API.dll"]

docker-compose.yml:

version: '3.4'

services:

  sqldata:
    ports:
      - 1433:1433
    image: mcr.microsoft.com/mssql/server:2017-latest-ubuntu
    environment:
      - ACCEPT_EULA=Y
      - SA_PASSWORD=Pass@word

  catalog.api:
    ports:
      - 80:80
    build:
      context: .
      dockerfile: src/Services/Catalog/Catalog.API/Dockerfile
    depends_on:
      - sqldata

  basket.api:
    ports:
      - 81:80
    build:
      context: .
      dockerfile: src/Services/Basket/Basket.API/Dockerfile
    depends_on:
      - sqldata

我的目标是使用 http 客户端从 BasketService.cs 类中调用 Catalog.API。您可能已经注意到,我尝试使用 localhost:80 调用 Web Api 但这不起作用,因为两个 API 都在不同的容器上运行,并且 localhost 仅对 Basket 容器有效(我有 BasketService 类)。我不确定如何正确调用 Catalog.API。我可以使用 docker inspect 手动查找容器的 IP,但是每次我重新启动或重建容器时,IP 都会更改。所以这不是一个好方法。使用服务名称不起作用。

解决此问题的最佳方法是什么?我应该使用哪种方式调用 Api,在另一个容器上运行?

【问题讨论】:

  • 您可以在同一个docker-compose.yml文件中使用每个服务的services:块的名称作为主机名;连接到容器内的进程正在侦听的端口(在这种情况下,两个容器都是 80)。如果您不想从 Docker 外部访问服务,则不需要特别声明 ports:。 Docker 中的localhost 几乎总是意味着“这个容器”。
  • @joey 我不知道您所说的“链式 api 调用”是什么意思,但是从另一个 api 调用一个 api 而两者都在不同的服务中实现是构建微服务的方式。当整个行业都搬到那里时,你几乎不能称微服务为坏习惯。
  • @DavidMaze:谢谢你的回答。我改为使用服务名称作为主机,它按预期工作。端口在那里,因为到目前为止,我正在访问 docker 外部的容器以测试结果。
  • @Artur :感谢您的澄清,有些时候我对 joey 的评论有点困惑。但正如你所说,这有点像你构建它们的方式。
  • 嗨@Artur,所以“链式api调用”的意思正是它听起来的样子:1个api在请求/响应生命周期中依赖于另一个api。如果您是第一次听说它,可能听起来很混乱。 “此外,微服务之间存在 HTTP 依赖关系,例如在使用 HTTP 请求链创建较长的请求/响应周期时,如图 4-15 的第一部分所示,不仅会使您的微服务不自治,而且它们的性能也会立即受到影响因为该链中的一项服务表现不佳。”

标签: c# docker asp.net-core-webapi


【解决方案1】:

您必须创建一个网络来通信两个容器

docker 网络创建你的网络 docker network connect your-network the-service-container

然后,你可以写 _client.GetStringAsync("http://the-service-container:80/api/basket/entries");

【讨论】:

    猜你喜欢
    • 2021-04-16
    • 2017-07-01
    • 1970-01-01
    • 2020-10-11
    • 1970-01-01
    • 1970-01-01
    • 2021-10-26
    • 2019-07-20
    • 1970-01-01
    相关资源
    最近更新 更多