项目

微服务解决方案:添加新的微服务

你必须拥有ABP商业版或更高级别的许可证才能创建微服务解决方案。

本文档解释了如何向微服务解决方案模板添加新的微服务。在解决方案模板中,根目录下有一个名为services的文件夹,其中包含解决方案中的所有微服务。每个微服务都是一个独立的ASP.NET Core应用程序,可以独立开发、测试和部署。

此外,在根目录中还有一个名为_templates的文件夹。此文件夹包含可用于创建新微服务、API网关和应用程序的模板。这些模板可以根据你的需求进行定制。

文件夹结构

添加新的微服务

要向解决方案添加新的微服务,你可以使用service_nolayers模板。此模板会创建一个带有必要配置和依赖项的新ASP.NET Core应用程序。按照以下步骤添加新的微服务:

在ABP Studio的解决方案资源管理器中,右键单击services文件夹,选择添加 -> 新模块 -> 微服务

你也可以在项目创建过程中通过使用"附加服务"屏幕添加微服务。更多详细信息,请参阅附加服务部分。

新建微服务

这将打开创建新模块对话框。输入新微服务的名称,如果需要则指定输出目录,然后单击下一步按钮。有一个命名约定:模块名称应包含解决方案名称作为前缀,并且模块名称中不允许使用点(.)字符。

创建新模块

选择数据库提供程序,然后单击创建按钮。

创建新模块-数据库提供程序

当你创建一个新的微服务时,可以选择启用与当前解决方案的集成。如果勾选启用集成,新的微服务将被添加到解决方案中,并且必要的配置会自动完成,因此无需手动配置。如果不勾选启用集成选项,你将需要手动配置新的微服务。你可以按照本文档中的步骤操作,从配置 appsettings.json 部分开始。

创建新微服务-nolayers-启用集成

新的微服务已创建并添加到解决方案中。你可以在services文件夹中看到新的微服务。

产品微服务

配置 appsettings.json

新的微服务已创建了必要的配置和依赖项。我们应该通过修改appsettings.json文件来配置几个部分:

  • 设置CorsOrigins以允许Web网关访问该微服务。
  • 设置AuthServer配置以使微服务能够对用户进行身份验证和授权。

你可以复制现有微服务的配置,并根据新的微服务进行修改。以下是ProductService微服务的appsettings.json文件示例。

{
  "ConnectionStrings": {
    "Administration": "Server=localhost,1434; User Id=sa; Password=myPassw@rd; Database=Bookstore_Administration; TrustServerCertificate=true",
    "AbpBlobStoring": "Server=localhost,1434; User Id=sa; Password=myPassw@rd; Database=Bookstore_BlobStoring; TrustServerCertificate=true",
    "ProductService": "Server=localhost,1434; User Id=sa; Password=myPassw@rd; Database=Bookstore_ProductService; TrustServerCertificate=true"
  },
  "App": {
-   "CorsOrigins": "http://localhost:webgateway_port",
+   "CorsOrigins": "http://localhost:44333",
    "EnablePII": false
  },
  "Swagger": {
    "IsEnabled": true
  },
  "AuthServer": {
-   "Authority": "http://localhost:authserver_port",
-   "MetaAddress": "http://localhost:authserver_port",
+   "Authority": "http://localhost:44387",
+   "MetaAddress": "http://localhost:44387",
    "RequireHttpsMetadata": "false",
    "SwaggerClientId": "SwaggerTestUI",
    "Audience": "ProductService"
  },
  "Redis": {
    "Configuration": "localhost:6379"
  },
  "RabbitMQ": {
    "Connections": {
      "Default": {
        "HostName": "localhost"
      }
    },
    "EventBus": {
      "ClientName": "Bookstore_ProductService",
      "ExchangeName": "Bookstore"
    }
  },
  "AbpDistributedCache": {
    "KeyPrefix": "Bookstore:"
  },
  "DataProtection": {
    "ApplicationName": "Bookstore",
    "Keys": "Bookstore-Protection-Keys"
  },
  "ElasticSearch": {
    "IsLoggingEnabled": "true",
    "Url": "http://localhost:9200"
  },
  "StringEncryption": {
     "DefaultPassPhrase": "PDAWjbshpwlOwNB6"
  }
}

配置 OpenId 选项

我们应该通过修改Identity服务中的OpenIddictDataSeeder来配置OpenId选项。以下是Product微服务的OpenIddictDataSeeder选项示例。

OpenIddictDataSeeder类的CreateApiScopesAsyncCreateSwaggerClientsAsync方法中创建API范围,并为Swagger客户端添加所需的API范围。

private async Task CreateApiScopesAsync()
{
    await CreateScopesAsync("AuthServer");
    await CreateScopesAsync("IdentityService");
    await CreateScopesAsync("AdministrationService");
+   await CreateScopesAsync("ProductService");
}

private async Task CreateSwaggerClientsAsync()
{
    await CreateSwaggerClientAsync("SwaggerTestUI", new[]
    {
        "AuthServer",
        "IdentityService",
        "AdministrationService",
+       "ProductService"
    });
}

CreateSwaggerClientAsync方法中添加新服务的重定向URL。

private async Task CreateSwaggerClientAsync(string clientId, string[] scopes)
{
    ...
    ...
    ...
    var administrationServiceRootUrl = _configuration["OpenIddict:Resources:AdministrationService:RootUrl"]!.TrimEnd('/');
+   var productServiceRootUrl = _configuration["OpenIddict:Resources:ProductService:RootUrl"]!.TrimEnd('/');

    await CreateOrUpdateApplicationAsync(
        name: clientId,
        type:  OpenIddictConstants.ClientTypes.Public,
        consentType: OpenIddictConstants.ConsentTypes.Implicit,
        displayName: "Swagger 测试客户端",
        secret: null,
        grantTypes: new List<string>
        {
            OpenIddictConstants.GrantTypes.AuthorizationCode,
        },
        scopes: commonScopes.Union(scopes).ToList(),
        redirectUris: new List<string> {
            $"{webGatewaySwaggerRootUrl}/swagger/oauth2-redirect.html",
            $"{authServerRootUrl}/swagger/oauth2-redirect.html",
            $"{identityServiceRootUrl}/swagger/oauth2-redirect.html",
            $"{administrationServiceRootUrl}/swagger/oauth2-redirect.html",
+           $"{productServiceRootUrl}/swagger/oauth2-redirect.html"
        }
    );
}

CreateClientsAsync方法中为Web(前端)应用程序添加允许的范围。你可能为不同的UI应用程序(如Web、Angular、React等)拥有不同的客户端。确保将这些新的微服务添加到这些客户端的允许范围中。

private async Task CreateClientsAsync()
{
    var commonScopes = new List<string>
    {
        OpenIddictConstants.Permissions.Scopes.Address,
        OpenIddictConstants.Permissions.Scopes.Email,
        OpenIddictConstants.Permissions.Scopes.Phone,
        OpenIddictConstants.Permissions.Scopes.Profile,
        OpenIddictConstants.Permissions.Scopes.Roles
    };

    //Web 客户端
    var webClientRootUrl = _configuration["OpenIddict:Applications:Web:RootUrl"]!.EnsureEndsWith('/');
    await CreateOrUpdateApplicationAsync(
        name: "Web",
        type: OpenIddictConstants.ClientTypes.Confidential,
        consentType: OpenIddictConstants.ConsentTypes.Implicit,
        displayName: "Web 客户端",
        secret: "1q2w3e*",
        grantTypes: new List<string>
        {
            OpenIddictConstants.GrantTypes.AuthorizationCode,
            OpenIddictConstants.GrantTypes.Implicit
        },
        scopes: commonScopes.Union(new[]
        {
            "AuthServer",
            "IdentityService",
            "SaasService",
            "AuditLoggingService",
            "AdministrationService",
+           "ProductService"
        }).ToList(),
        redirectUris: new List<string> { $"{webClientRootUrl}signin-oidc" },
        postLogoutRedirectUris: new List<string>() { $"{webClientRootUrl}signout-callback-oidc" },
        clientUri: webClientRootUrl,
        logoUri: "/images/clients/aspnetcore.svg"
    );
}

将新服务的URL添加到Identity微服务的appsettings.json文件中。在此示例中,我们将编辑 Acme.Bookstore.IdentityService 项目的 appsettings.json 文件。

"OpenIddict": {
  "Applications": {
    ...
  },
  "Resources": {
    ...
+    "ProductService": {
+      "RootUrl": "http://localhost:44350"
+    }
  }
}

配置 AuthServer

我们应该为 AuthServer 应用程序的 appsettings.json 文件配置 CorsOriginsRedirectAllowedUrls 部分。

...
"App": {
  "SelfUrl": "http://localhost:***",
- "CorsOrigins": "http://localhost:44358,..",
+ "CorsOrigins": "http://localhost:44358,..,http://localhost:44350",
  "EnablePII": false,
- "RedirectAllowedUrls": "http://localhost:44358,..",
+ "RedirectAllowedUrls": "http://localhost:44358,..,http://localhost:44350"
},
...

配置 API 网关

我们应该配置API网关以访问新的微服务。首先,将 Product 部分添加到 WebGateway 项目的 appsettings.json 文件中。在此示例中,我们将编辑 Acme.Bookstore.WebGateway 项目的 appsettings.json 文件。

"ReverseProxy": {
    "Routes": {
      ...
+      "Product": {
+        "ClusterId": "Product",
+        "Match": {
+          "Path": "/api/product/{**catch-all}"
+        }
+      },
+      "ProductSwagger": {
+        "ClusterId": "Product",
+        "Match": {
+          "Path": "/swagger-json/Product/swagger/v1/swagger.json"
+        },
+        "Transforms": [
+          { "PathRemovePrefix": "/swagger-json/Product" }
+        ]
+      }
    },
    "Clusters": {
      ...
+      "Product": {
+        "Destinations": {
+          "Product": {
+            "Address": "http://localhost:44350/"
+          }
        }
      }
    }
}

之后,打开 WebGateway 项目中的 ProjectNameWebGatewayModule 类,并在 ConfigureSwaggerUI 方法中添加 ProductService。在此示例中,我们将编辑 BookstoreWebGatewayModule 文件。

private static void ConfigureSwaggerUI(
    IProxyConfig proxyConfig,
    SwaggerUIOptions options,
    IConfiguration configuration)
{
    foreach (var cluster in proxyConfig.Clusters)
    {
        options.SwaggerEndpoint($"/swagger-json/{cluster.ClusterId}/swagger/v1/swagger.json", $"{cluster.ClusterId} API");
    }

    options.OAuthClientId(configuration["AuthServer:SwaggerClientId"]);
    options.OAuthScopes(
        "AdministrationService",
        "AuthServer",
        ...,
+       "ProductService"
    );
}

配置 UI 服务

我们应该配置UI应用程序,以允许新的微服务通过Web网关访问。为此,我们应该在 WebBlazor 应用程序的 ProjectName...Module 类的 ConfigureAuthentication 方法中添加新的微服务范围。在此示例中,我们将编辑 BookstoreWebModule 文件。

private void ConfigureAuthentication(ServiceConfigurationContext context, IConfiguration configuration)
{
  context.Services.AddAuthentication(options =>
  {
    options.DefaultScheme = "Cookies";
    options.DefaultChallengeScheme = "oidc";
  })
  .AddCookie("Cookies", options =>
  {
    options.ExpireTimeSpan = TimeSpan.FromDays(365);
  })
  .AddAbpOpenIdConnect("oidc", options =>
  {
    ...
    options.Scope.Add("AuthServer");
    options.Scope.Add("IdentityService");
    options.Scope.Add("AdministrationService");
+   options.Scope.Add("ProductService");
  });
  ...
}

类似地,如果你有一个Angular应用程序,你应该在 environment.tsoAuthConfig 中添加新的服务范围:

const baseUrl = 'http://localhost:4200';

const oAuthConfig = {
  issuer: 'http://localhost:44387',
  redirectUri: baseUrl,
  clientId: 'Angular',
  responseType: 'code',
- scope: 'openid profile email roles AuthServer IdentityService AdministrationService',
+ scope: 'openid profile email roles AuthServer IdentityService AdministrationService ProductService',
  requireHttps: false
};

Prometheus 的 Docker 配置

如果你想在调试解决方案时使用Prometheus监控新的微服务,你应该将新的微服务添加到 etc/docker/prometheus 文件夹中的 prometheus.yml 文件中。你可以复制现有微服务的配置,并根据新的微服务进行修改。以下是 Product 微服务的 prometheus.yml 文件示例。

  - job_name: 'authserver'
    scheme: http
    metrics_path: 'metrics'
    static_configs:
    - targets: ['host.docker.internal:44398']
    ...
+ - job_name: 'product'
+   scheme: http
+   metrics_path: 'metrics'
+   static_configs:
+   - targets: ['host.docker.internal:44350']

为新微服务创建 Helm 图表

如果你希望将新的微服务部署到Kubernetes,你应该为该新的微服务创建一个Helm图表。

首先,我们需要将新的微服务添加到 etc/helm 文件夹中的 build-all-images.ps1 脚本中。你可以复制现有微服务的配置,并根据新的微服务进行修改。以下是 Product 微服务的 build-all-images.ps1 脚本示例。

...
  ./build-image.ps1 -ProjectPath "../../apps/auth-server/Acme.Bookstore.AuthServer/Acme.Bookstore.AuthServer.csproj" -ImageName bookstore/authserver
+ ./build-image.ps1 -ProjectPath "../../services/product/Acme.Bookstore.ProductService/Acme.Bookstore.ProductService.csproj" -ImageName bookstore/product

然后,我们需要将连接字符串添加到 etc/helm/projectname 文件夹中的 values.projectname-local.yaml 文件中。以下是 Product 微服务的 values.bookstore-local.yaml 文件示例。

global:
  ...
  connectionStrings:
    ...
+   product: "Server=[RELEASE_NAME]-sqlserver,1433; Database=Bookstore_ProductService; User Id=sa; Password=myPassw@rd; TrustServerCertificate=True"

之后,我们需要为新的微服务创建一个新的Helm图表。你可以复制现有微服务的配置,并根据新的微服务进行修改。以下是 Product 微服务的 product Helm图表示例。

产品微服务的 values.yaml 文件。

image:
  repository: "bookstore/product"
  tag: "latest"
  pullPolicy: IfNotPresent
swagger:
  isEnabled: "true"

产品微服务的 Chart.yaml 文件。

apiVersion: v2
name: product
version: 1.0.0
appVersion: "1.0"
description: Bookstore 产品服务

产品微服务的 product.yaml 文件。

apiVersion: apps/v1
kind: Deployment
metadata:
  name: "{{ .Release.Name }}-{{ .Chart.Name }}"
spec:
  selector:
    matchLabels:
      app: "{{ .Release.Name }}-{{ .Chart.Name }}"
  template:
    metadata:
      labels:
        app: "{{ .Release.Name }}-{{ .Chart.Name }}"
    spec:
      containers:
      - image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"
        imagePullPolicy: "{{ .Values.image.pullPolicy }}"
        name: "{{ .Release.Name }}-{{ .Chart.Name }}"
        ports:
        - name: "http"
          containerPort: 80
        env:
        - name: "DOTNET_ENVIRONMENT"
          value: "{{ .Values.global.dotnetEnvironment }}"
        - name: "ConnectionStrings__Administration"
          value: "{{ .Values.global.connectionStrings.administration | replace "[RELEASE_NAME]" .Release.Name }}"
        - name: "ConnectionStrings__AbpBlobStoring"
          value: "{{ .Values.global.connectionStrings.blobStoring | replace "[RELEASE_NAME]" .Release.Name }}"
        - name: "ConnectionStrings__ProductService"
          value: "{{ .Values.global.connectionStrings.product | replace "[RELEASE_NAME]" .Release.Name }}"
          ...

产品微服务的 product-service.yaml 文件。

apiVersion: v1
kind: Service
metadata:
  labels:
    name: "{{ .Release.Name }}-{{ .Chart.Name }}"
  name: "{{ .Release.Name }}-{{ .Chart.Name }}"
spec:
  ports:
    - name: "80"
      port: 80
  selector:
    app: "{{ .Release.Name }}-{{ .Chart.Name }}"

创建Helm图表后,你可以在ABP Studio中刷新子图表

kubernetes-刷新-子图表

然后,更新元数据信息,右键单击微服务名称子图表,选择属性,这将打开图表属性窗口。你应在元数据选项卡中添加以下键。

微服务图表属性-元数据

  • projectPath:指向微服务主机应用程序项目的路径。在bookstore示例中,此值为 ../../services/product/Acme.Bookstore.ProductService/Acme.Bookstore.ProductService.csproj
  • imageName:当我们构建Docker镜像时,它使用此值作为Docker镜像名称。我们将在Helm图表值中使用它。
  • projectType:你可以为Angular和.NET项目添加Helm图表,这就是为什么我们应该明确指定项目类型。

图表属性 -> Kubernetes服务选项卡中添加Kubernetes服务

微服务图表属性-kubernetes-服务

此值应与解决方案运行程序应用程序Kubernetes服务 值相同。这是浏览所必需的,因为当我们连接到Kubernetes集群时,我们应该浏览Kubernetes服务而不是使用启动URL。

最后但同样重要的是,我们需要为身份、认证服务器和网关应用程序配置Helm图表环境变量。

以下是 Identity 微服务的 identity.yaml 文件示例。

apiVersion: apps/v1
kind: Deployment
metadata:
  name: "{{ .Release.Name }}-{{ .Chart.Name }}"
spec:
  selector:
    matchLabels:
      app: "{{ .Release.Name }}-{{ .Chart.Name }}"
  template:
    metadata:
      labels:
        app: "{{ .Release.Name }}-{{ .Chart.Name }}"
    spec:
      containers:
      - image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"
        imagePullPolicy: "{{ .Values.image.pullPolicy }}"
        name: "{{ .Release.Name }}-{{ .Chart.Name }}"
        ports:
        - name: "http"
          containerPort: 80
        env:
        ...
+       - name: "OpenIddict__Resources__ProductService__RootUrl"
+         value: "http://{{ .Release.Name }}-product"

以下是 AuthServer 应用程序的 authserver.yaml 文件示例。

apiVersion: apps/v1
kind: Deployment
metadata:
  name: "{{ .Release.Name }}-{{ .Chart.Name }}"
spec:
  selector:
    matchLabels:
      app: "{{ .Release.Name }}-{{ .Chart.Name }}"
  template:
    metadata:
      labels:
        app: "{{ .Release.Name }}-{{ .Chart.Name }}"
    spec:
      containers:
      - image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"
        imagePullPolicy: "{{ .Values.image.pullPolicy }}"
        name: "{{ .Release.Name }}-{{ .Chart.Name }}"
        ports:
        - name: "http"
          containerPort: 80
        env:
        ...
        - name: "App__CorsOrigins"
-         value: "...,http://{{ .Release.Name }}-administration"
+         value: "...,http://{{ .Release.Name }}-administration,http://{{ .Release.Name }}-product"

以下是 WebApiGateway 应用程序的 webapigateway.yaml 文件示例。

apiVersion: apps/v1
kind: Deployment
metadata:
  name: "{{ .Release.Name }}-{{ .Chart.Name }}"
spec:
  selector:
    matchLabels:
      app: "{{ .Release.Name }}-{{ .Chart.Name }}"
  template:
    metadata:
      labels:
        app: "{{ .Release.Name }}-{{ .Chart.Name }}"
    spec:
      containers:
      - image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"
        imagePullPolicy: "{{ .Values.image.pullPolicy }}"
        name: "{{ .Release.Name }}-{{ .Chart.Name }}"
        ports:
        - name: "http"
          containerPort: 80
        env:
        ...
+       - name: "ReverseProxy__Clusters__Product__Destinations__Product__Address"
+         value: "http://{{ .Release.Name }}-product"

定制微服务模板

如果需要,你可以定制微服务模板。通过打开根目录中的 _templates 文件夹,然后打开 service_nolayers 文件夹,向模板添加新配置、依赖项或模块。根据需要修改 service_nolayers 模板。命名约定规定,microservicename 代表创建时微服务的名称。在模板文件中使用 microservicename 进行动态命名。现有的 service_nolayers 模板默认不包含SaaS审计日志模块。如果要创建包含这些模块的解决方案,请将必要的配置添加到 service_nolayers 模板文件中。

为新微服务开发 UI

将新的微服务添加到解决方案后,你可以为该新的微服务开发UI。对于.NET应用程序,你可以将微服务的Contracts包添加到UI应用程序,以访问新微服务提供的服务。之后,你可以使用generate-proxy命令为新微服务生成代理类。

abp generate-proxy -t csharp -url http://localhost:44333/ -m product --without-contracts

接下来,开始在UI应用程序中为新的微服务创建页面组件。类似地,如果你有Angular应用程序,你可以使用generate-proxy命令为新微服务生成代理类并开始开发UI。

abp generate-proxy -t ng -url http://localhost:44333/ -m product

在本文档中