用.NET、Angular和MongoDB开发一个实时全栈应用程序(详细教程)

68 阅读6分钟

在今天的教程中,我们将创建一个简单的回顾性应用。它将保存讨论的主题信息,并实时显示给用户,这样他们就可以在当前的会议中轻松看到之前、当前和未来的主题。代码是用.NET 5编写的。它被分为几个服务,每一个服务都负责一个上下文。

我们假设每个服务都应该包含健康检查OpenAPI文档,并通过
docker-compose

作为一个数据存储,我们将使用MongoDB

回顾性应用的架构

该应用程序将分为3个服务
:-Facade 是一个应用程序的前端,将客户的请求引导到下面提到的其他服务。
-Topic 负责将回顾性主题保存到数据库(MongoDB)。
-Notification 收集来自其他服务的所有通知,并将它们推送给订阅的服务/客户。在本演示中,我们可以假设它的工作方式类似于推送通知。

它们之间的通信是通过发布/订阅完成的。

以下是架构图:

Developing a realtime full stack app with .NET, Angular, and MongoDB

实时全栈应用的架构图。

为了简化上述内容,我们将使用一个开箱即用的pub/sub服务。Ably符合这个要求,原因如下。

什么是Ably?

Ably提供了几种服务,包括pub/sub消息的历史持久性、移动应用的推送通知等。它还有一个可供使用的.NET库

作为一个简短的介绍,我想描述一下Ably中使用的一些基本术语:

-Channel 是连接到它的服务/客户端的通信源。
-Message Type ,用于区分通过单一通道发送的不同类型的数据。服务可以订阅一个(多个)具体的类型或所有的类型。

深入到代码中

我们现在就可以检查一下代码是什么样子的。

界面

第一个服务,Facade ,向客户显示信息。服务是用.NET 5(C#)编写的。它是一个WebAPI,前端部分用Angular 8编写。

OpenAPI和健康检查

这个服务只依赖于Ably,因为启动文件包含了检查Ably状态的配方,并通过Swashbuckle生成OpenAPI文档。

public void ConfigureServices(IServiceCollection services)
{
    var config = Configuration
        .GetSection("Ably")
        .Get<AblyConfig>();
    var ably = newAblyRealtime(config.ApiKey);

    services.AddSwaggerGen(c =>
    {
        c.SwaggerDoc("v1",newOpenApiInfo{Title="WebAppAPI",Version="v1"});
    });
    services.AddHealthChecks()
        .AddCheck(
            "AblyChannel",
            newAblyChannelHealthCheck(
                ably,
                "Topic"
            )
        )
        .AddCheck(
            "AblyTimer",
            newAblyTimerHealthCheck(
                ably,
                "Topic",
                FSharpOption<TimeSpan>.Some(TimeSpan.FromSeconds(1)),
                FSharpOption<TimeSpan>.Some(TimeSpan.FromSeconds(1))
            )
        );
    services
        .AddHealthChecksUI(s =>
            s
                .SetEvaluationTimeInSeconds(60)
                .AddHealthCheckEndpoint("Self",$"http://{Dns.GetHostName()}/health"))
                .AddInMemoryStorage();
        );
    …
    services.AddAutofac();
    …
}

public void Configure(IApplicationBuilder app,IWebHostEnvironment env)
{
    …
    app.UseSwagger();
    app.UseSwaggerUI(c =>
    {
        c.SwaggerEndpoint("/swagger/v1/swagger.json","DashboardAPIV1");
    });

    app.UseEndpoints(endpoints =>
    {
        endpoints.MapControllerRoute(
            name:"default",
            pattern:"{controller}/{action=Index}/{id?}");
        endpoints.MapHealthChecksUI( setup =>
            {
                setup.UIPath="/ui-health";
                setup.ApiPath="/api-ui-health";
            }
        );
        endpoints.MapHealthChecks(
            "/health",
            newHealthCheckOptions {
                Predicate=( _ => true),
                ResponseWriter=UIResponseWriter.WriteHealthCheckUIResponse
            }
        );
    });

    ...
}

Developing a realtime full stack app with .NET, Angular, and MongoDB

为Facade生成的Swagger方法规范

Developing a realtime full stack app with .NET, Angular, and MongoDB

健康检查的仪表板视图。

行动

服务包含一个单一的控制器,有两个动作:POSTPUTGET 动作在这里只是为了测试目的)。POST 创建一个主题,PUT 更新它:

[HttpPost]
public async Task<StatusCodeResult> Save([FromBody]CreateDto create)
{
    var data = new Topic
        (
            name: Name.NewName("name"),
            id: Identifier.NewId(""),
            creator: Creator.NewCreator(create.Creator),
            description: Description.NewDescription(create.Description),
            done: Done.NewDone(false)
        );

    var channel = _ably.Channels.Get(_ablyConfig.Topic.Name);
    var result = await channel.PublishAsync(_ablyConfig.Topic.MessageType,data);
    if (!result.IsFailure)
        returnOk();
    _logger.LogError(result.Error.Message);
    return new StatusCodeResult(500);
}

看一下代码,我们可以看到用户的请求带着DTO对象来到一个控制器动作。然后这个对象被转化为一个消息,被Topic 服务所知道。之后,该消息通过Ably通过topic 通道发送。

Ably注册

这里我们使用了一个注入到控制器中的Ably服务,但值得展示一下它的注册情况。我们使用Autofac作为DI,整个代码在下面:

public class WebAppModule : Module
{
    private readonly AblyConfig _config;

    public WebAppModule(AblyConfig config) => _config = config;

    protected override void Load(ContainerBuilder builder)
    {
        base.Load(builder);

        builder.Register(_ => _config).AsSelf().SingleInstance();
        builder.Register(_ => new AblyRealtime(_config.ApiKey)).AsSelf().SingleInstance();
    }
}
public void ConfigureContainer(ContainerBuilder builder) =>
    builder.RegisterModule(new WebAppModule(Configuration
        .GetSection("Ably")
        .Get<AblyConfig>()));
public class Program
{
    public static Task Main(string[] args) =>
        CreateWebHostBuilder(args).Build().RunAsync();

    private static IHostBuilder CreateWebHostBuilder(string[] args) =>
        Host.CreateDefaultBuilder(args)
            .UseServiceProviderFactory(new AutofacServiceProviderFactory())
            .ConfigureWebHostDefaults(wb =>
            {
                wb.UseStartup<Startup>();
                wb.ConfigureLogging((ctx, lg) =>
                {
                    lg.ClearProviders();
                    lg.AddDebug();
                    lg.AddConsole();
                });
            });
}

应用程序的用户界面(在Angular 8中)

前面提到的前端部分是用Angular 8编写的。看一下代码,我们有两个主要的Angular组件。

-Home-component.ts 显示主页面,其中有一个实际主题的列表和一个添加新主题的表单。
-Activity-component.ts 显示了一个包含所有用户活动的页面。

主页组件

显示主页面的第一个组件被分为两部分。第一部分显示了一个添加新主题的表单,它被提取为一个单独的组件(create-component.ts),它看起来如下所示:

export class CreateComponent {
  public model: Create = { Description: "", Creator: "" };
  constructor(
    private readonly http: HttpClient,
    @Inject('BASE_URL') private readonly baseUrl: string) {
  }

  public onSubmit = () =>
    this.http.post(
        `${this.baseUrl}Facade`,
        JSON.stringify(this.model),
      { headers: { 'content-type': 'application/json'}})
      .toPromise()
      .then(_ => this.reset());

  public reset = () =>
    this.model = { Description: "", Creator: "" }
}

Developing a realtime full stack app with .NET, Angular, and MongoDB

回溯列表的组件视图。

在组件视图中,我们看到一个创建新话题的表单来讨论。表单逻辑里面发生了什么?我们从表单中映射数据,并在点击提交按钮后将其发送到服务器。

主页组件的第二部分包含一个表格,里面有所有被创建和讨论的主题。当我们创建一个话题时,它不会立即显示在列表中,因为它是通过Aply通道来自服务器的。

为了接收这样的消息,我们要附加到一个Ably频道(Notification channel)。

export class HomeComponent {
  public topics: Topic [];

  constructor(
    private readonly http: HttpClient,
    private readonly changeDetector: ChangeDetectorRef,
    @Inject('BASE_URL') private readonly baseUrl: string) {
    this.http.get(`${this.baseUrl}Facade`)
      .toPromise()
      .then((e: AblyConfig) => {
        let client = new Ably.Realtime({ key: e.apiKey });
        let channel = client.channels.get(e.push.name);
        channel.subscribe(e.push.messageType, msg => {
            let additionalData = JSON.parse(msg.data).AdditionalData.Item;
            let newRecord = JSON.parse(additionalData);
            this.topics = [newRecord, ...this.topics];

            this.changeDetector.detectChanges();
          })
        });
    this.topics = [];
  }

  public markAsDone (model) {
    this.http.post(
      `${this.baseUrl}Facade`,
      JSON.stringify(model),
      { headers: { 'content-type': 'application/json'}})
      .toPromise()
      .then(_ => {
        this.topics = this.topics.filter(x => x !== model);
        this.changeDetector.detectChanges();
      });
  }
}

请注意,创建一个Ably对象,附加到一个通道,并监听一个传入的消息,所需的代码与我们在服务器端显示的非常相似。

简而言之,我们获取频道,订阅它,当有感兴趣的消息传来时,我们就更新主页组件的模型。还请注意markAsDone 方法,该方法处理将一个话题标记为 "已经讨论过",这反过来又触发了对某个话题的更新。

活动反馈

第二个主要组件(activity-component.ts )负责显示应用程序中发生的所有事件的历史。它看起来与主页相似,但信息的到达方式略有不同。

export class ActivityComponent {
  public activities: Topic [];
  constructor(
    private readonly http: HttpClient,
    private readonly changeDetector: ChangeDetectorRef,
    @Inject('BASE_URL') private readonly baseUrl: string) {
    this.http.get(`${this.baseUrl}Facade`)
      .toPromise()
      .then((e: AblyConfig) => {
        let client = new Ably.Realtime({ key: e.apiKey });
        let channel = client.channels.get(e.push.name);
        channel.attach(err => {
          channel.history({untilAttach: true}, (error, results) => {
            let isMsg = (maybeMsg: PaginatedResult<Message> | undefined):
              maybeMsg is PaginatedResult<Message> =>
                (maybeMsg as PaginatedResult<Message>) !== undefined;

            if (isMsg(results))
              this.activities =
                  results
                  .items
                  .map(msg => {
                    if (msg.name === e.push.messageType) {
                      let additionalData = JSON.parse(msg.data).AdditionalData.Item;
                      return JSON.parse(additionalData);
                    }
                    else {
                      return { Creator: "Server request", Description: "Server request", Id: msg.id, Done: false };
                    }
                  });
            else
              this.activities = [];
            this.changeDetector.detectChanges();
          })})
      });
  }
}

我们不获取频道,而是直接附加到频道上,收听过去24小时内通过这个特定频道传来的所有消息。坚持信息是在Ably平台上为每个频道设置的。我们不能以编程的方式来设置它,而是通过频道页面来设置。

信息保留

要设置信息保留,我们需要进入Ably仪表盘并导航到渠道页面。

Developing a realtime full stack app with .NET, Angular, and MongoDB

Ably仪表盘上的频道规则视图。

Developing a realtime full stack app with .NET, Angular, and MongoDB

在Ably仪表盘上编辑频道规则页面

启用 "保留所有消息",就可以了。

Developing a realtime full stack app with .NET, Angular, and MongoDB

Retrospective应用程序的最后活动仪表板

现在我们知道它在客户端和Facade 方面是如何工作的。我们还可以转发到Topic
Notification
服务。

主题

Topic 是一个用.NET 5(F#)编写的WebAPI。它使用MongoDB作为数据库。它有3个主要职责:

  1. 接收和处理关于新主题的消息。
  2. 保存主题到数据库。
  3. 通知已处理的主题。

处理信息

Topic 订阅所有感兴趣的信息,特别是那些旨在创建或更新主题的信息。该服务的主要职责是根据要求处理通过Ably通道发送的上述消息:

member private this.ConfigureHandlers (di: DI) (logger: ILogger<Startup>) =
    let channel = di.ably.Channels.Get di.ablyConfig.Topic.Name
    channel.Subscribe(
        di.ablyConfig.Topic.MessageType,
        fun msg ->
            async {
                match! Topic.Application.Topics.Save.trySave di msg.Data CancellationToken.None with
                | Choice1Of2 result -> logger.LogDebug $"Process message: {result}"
                | Choice2Of2 err -> logger.LogError $"Error occurred while processing message: {err}"
                ()
            } |> Async.RunSynchronously)

当新消息到来时,我们接收它并将其映射到业务模型中:

type InvalidTopicFormat = exn

[<RequireQualifiedAccess>]
module Save =
    let save
        (di: DI)
        topic
        token =
        async {
            let! result = Repository.create token di.config topic
            
            match result with
            | Choice1Of2 (DTOResult.Create data) ->
                let serializedData = JsonSerializer.Serialize(data, Serialization.serializerOpt)
                let (Topic.Contract.Id id) = data.Id
                let msg = {
                    Id = Identifier (Guid.NewGuid())
                    RelatedId = Id id
                    AdditionalData = AdditionalData serializedData
                }
                let channel = di.ably.Channels.Get di.ablyConfig.Notification.Name
                let! pubResult = channel.PublishAsync (di.ablyConfig.Notification.MessageType, msg) |> Async.AwaitTask
                if pubResult.IsFailure then
                    return Choice2Of2 (pubResult.Error.AsException ())
                else return result
            | Choice1Of2 res ->
                di.logger.Log(LogLevel.Warning, $"Expected to get a create result, but got something else: {res}")
                return result
            | Choice2Of2 ex ->
                di.logger.Log(LogLevel.Error, $"Error occurred while processing data in repository: {ex.Message}")
                return result
        }
    
    let trySave
        (di: DI)
        (topic: obj)
        token =
        async {
            try
                let deserializedTopic = (topic :?> JObject).ToObject<Topic>()
                return! save di deserializedTopic token
            with msg ->
                return
                    msg.Message
                    |> InvalidTopicFormat
                    |> Choice2Of2
        }

保存数据到MongoDB

然后,我们将其保存到MongoDB,并以docker镜像的形式运行(与.NET服务的方式相同):

version: '3'
volumes:
  retrospectiveLocalDb:
  retrospectiveLocalDbConfig:
services:
  database:
    image: db
    build:
      context: ./storage/mongo
      dockerfile: ./Dockerfile
    environment:
      - MONGO_INITDB_ROOT_USERNAME=sample
      - MONGO_INITDB_ROOT_PASSWORD=Sample123!
    volumes:
      - retrospectiveLocalDb:/data/db
      - retrospectiveLocalDbConfig:/data/configdb
    ports:
      - '27017-27019:27017-27019'
FROM mongo:4.2.3
COPY init-mongo.js /docker-entrypoint-initdb.d/
db = db.getSiblingDB('admin')
db.createUser(
    {
        user: "admin",
        pwd: "123",
        roles: [
            { role: "readWrite", db: "admin" }
        ]
    });

db = db.getSiblingDB('retrospective')
db.createUser(
    {
        user: "admin",
        pwd: "123",
        roles: [
            { role: "readWrite", db: "retrospective" }
        ]
    });
let private queryDb token config ``type`` (model: Option<Topic>) =
    match ``type`` with
    | TopicOperation.Create ->
        async {
            let fOpt = InsertOneOptions ()

            let! _ =
                connection config
                |> Query.insertOne token fOpt model.Value
            return QueryResult.Create model.Value
        }
    | TopicOperation.Update ->
        async {
            let fOpt = FindOneAndReplaceOptions<Topic> ()
            fOpt.ReturnDocument <- ReturnDocument.After
            let filter =
                Filter.eq (fun (x: Topic) -> x._id) model.Value._id

            let! result =
                connection config
                |> Query.updateWhole token fOpt model.Value filter
            return QueryResult.Update result
        }
    | ...

...

let private query token config data ``type`` =
    async {
        let! mapped = ToDomain.map data
        return! mapped |> (queryDb token config ``type``)
    }

...

let create token config data =
    async {
        let! result = query token config (Some data) TopicOperation.Create
        return result |> ToDTO.map
    } |> Async.Catch

这段代码看起来并不太复杂。简单地从对象映射到对象,并保存到数据库。当保存成功时,我们希望将这一事实告知客户端应用程序。因为我们的项目可以包含很多其他的服务,我们创建了一个服务,它将把各种推送消息聚集在一个地方。

将消息转发到通知书

我们将所有被成功保存的聚合消息映射到一个由Notification服务接受的对象,并通过Ably通道将其发送给它.与 Facade 发送数据给Topic 服务的方式相同,只是我们使用的是不同的通道和消息类型。

...
match result with
| Choice1Of2 (DTOResult.Create data) ->
    let serializedData = JsonSerializer.Serialize(data, Serialization.serializerOpt)
    let (Topic.Contract.Id id) = data.Id
    let msg = {
        Id = Identifier (Guid.NewGuid())
        RelatedId = Id id
        AdditionalData = AdditionalData serializedData
    }
    let channel = di.ably.Channels.Get di.ablyConfig.Notification.Name
    let! pubResult = channel.PublishAsync (di.ablyConfig.Notification.MessageType, msg) |> Async.AwaitTask
    if pubResult.IsFailure then
        return Choice2Of2 (pubResult.Error.AsException ())
    else return result
| Choice1Of2 res ->
    di.logger.Log(LogLevel.Warning, $"Expected to get a create result, but got something else: {res}")
    return result
| Choice2Of2 ex ->
    di.logger.Log(LogLevel.Error, $"Error occurred while processing data in repository: {ex.Message}")
    return result
…

开放API和健康检查

Facade 相比,Topic 服务依赖于两件事。第一个是Ably。第二个是我们在健康检查中描述的MongoDB。

member this.ConfigureServices(services: IServiceCollection) =
    let config = Config ()
    this.Configuration.Bind config
    let ably = new AblyRealtime (config.Ably.ApiKey)
    let loggerFactory =
            LoggerFactory.Create(fun builder ->
            builder.AddConsole() |> ignore               
            );
    let logger = loggerFactory.CreateLogger<Startup>()
    
    let di = DI.create config ably logger
    services.AddSingleton<DI> (fun _ -> di) |> ignore
    
    this.ConfigureHandlers di logger
    
    services.AddSwaggerGen(fun c ->
            c.SwaggerDoc("v1", OpenApiInfo(Title = "Topic Api", Version = "v1"))
    ) |> ignore
    
    services.AddHealthChecks()
        .AddCheck(
            "Ably Channel",
            AblyChannelHealthCheck(
                ably,
                "Topic"
            )
        )
        .AddCheck(
            "Ably Timer",
            AblyTimerHealthCheck(
                ably,
                "Topic",
                TimeSpan.FromSeconds 1.,
                TimeSpan.FromSeconds 1.
            )
        )
        .AddMongoDb((MongoConfig.map config.MongoDb).GetConnectionString(), name = "MongoDB")
    |> ignore
    services
        .AddHealthChecksUI(fun s ->
            s
                .SetEvaluationTimeInSeconds(60)
                .AddHealthCheckEndpoint("Self", $"http://{Dns.GetHostName()}/health")
            |> ignore)
        .AddInMemoryStorage() |> ignore
    ...

member this.Configure(app: IApplicationBuilder, env: IWebHostEnvironment) =
    ...
    app.UseSwagger(fun opt -> ()) |> ignore
    app.UseSwaggerUI(fun opt ->
            opt.SwaggerEndpoint("/swagger/v1/swagger.json", "Topic Api V1")
            opt.RoutePrefix <- String.Empty
    ) |> ignore
    ...
    app.UseEndpoints(fun endpoints ->
            endpoints.MapControllers() |> ignore
            endpoints.MapHealthChecksUI(fun setup ->
                setup.UIPath <- "/ui-health"
                setup.ApiPath <- "/api-ui-health"
            ) |> ignore
            endpoints.MapHealthChecks(
                "/health",
                HealthCheckOptions(
                    Predicate = (fun _ -> true),
                    ResponseWriter = Func<HttpContext, HealthReport, Task>(fun (context) (c: HealthReport) -> UIResponseWriter.WriteHealthCheckUIResponse(context, c))
                )
            ) |> ignore
        ) |> ignore

Developing a realtime full stack app with .NET, Angular, and MongoDB

Swagger的主题API方法定义

我们还定义了一些HTTP端点,只是为了测试目的。你可以在OpenAPI文档中看到它们,如下:

Developing a realtime full stack app with .NET, Angular, and MongoDB

健康检查的仪表板视图。

通知服务

现在我们可以切换到Notification 服务。这又是一个用.NET 5(F#)编写的WebAPI。

处理Ably消息

Topic 服务类似,Notification 服务依附于Ably通道,以获取具体通道上的新消息,然后在它们进来时进行处理。

member private this.ConfigureHandlers (ably: AblyRealtime) (config: AblyConfig) (logger: ILogger) =
    let channel = ably.Channels.Get config.Channels.Notification.Name
    channel.Subscribe(
        config.Channels.Notification.MessageType,
        fun msg -> WebApi.Notifications.Save.handle ably config msg.Data logger |> Async.RunSynchronously)

Notification 服务中处理消息基本上意味着映射一个消息,然后通过Ably通道将其发送给所有感兴趣的客户/服务。在我们的方案中,它是Facade 的TypeScript部分。

转发通知(推送)消息到所有客户端

let private serializerOpt =
    let options = JsonSerializerOptions()
    JsonFSharpConverter(
        unionEncoding = (
            // Base encoding:
            JsonUnionEncoding.Untagged  
            // Additional options:
            ||| JsonUnionEncoding.UnwrapOption
            ||| JsonUnionEncoding.UnwrapRecordCases
        )
    )
    |> options.Converters.Add
    
    options

let internal handle (ably: AblyRealtime) (ablyConfig: AblyConfig) (msg: obj) (logger: ILogger) =
    try
        let deserialized = (msg :?> JObject).ToObject<Notification.Contract.Message>()
        async {
            let channel = ably.Channels.Get ablyConfig.Channels.Push.Name
            let msg = JsonSerializer.Serialize(deserialized, serializerOpt)
            let! _ = channel.PublishAsync (ablyConfig.Channels.Push.MessageType, msg) |> Async.AwaitTask
            ()
        }
    with er ->
        logger.LogError $"Error occurred while processing notification message: {er.Message}"
        async {
            let channel = ably.Channels.Get ablyConfig.Channels.Push.Name
            let! _ = channel.PublishAsync (ablyConfig.Channels.Push.MessageType, er.Message) |> Async.AwaitTask
            ()
        }

通过省略后端代码,Facade 聆听所有的通知。我们定义了一个更强大的消息序列化类型(由Fsharp.System.Text.Json 完成),因为我们想序列化所有F#特定的类型,如UnionsRecords ,使它们可以通过TypeScript轻松接近。

OpenAPI和健康检查

最后我们配置了startup.fs 文件的一小部分,在那里我们定义了健康检查和OpenAPI信息。类似于Facade ,从外部来看,Notification 服务只依赖于Ably服务。

member this.ConfigureServices(services: IServiceCollection) =
    let config = Config ()
    this.Configuration.Bind config
    let ably = new AblyRealtime (config.Ably.ApiKey)
    services.AddSingleton<AblyRealtime>(fun _ -> ably) |> ignore
    services.AddSingleton<Config> (fun _ -> config) |> ignore
    
    let loggerFactory =
            LoggerFactory.Create(fun builder ->
            builder.AddConsole() |> ignore               
            );
    let logger = loggerFactory.CreateLogger();
    this.ConfigureHandlers ably config.Ably logger
    
    services.AddSwaggerGen(fun c ->
            c.SwaggerDoc("v1", OpenApiInfo(Title = "Notification Api", Version = "v1"))
    ) |> ignore
    
    services.AddHealthChecks()
        .AddCheck(
            "Ably Channel",
            AblyChannelHealthCheck(
                ably,
                "Notification"
            )
        )
        .AddCheck(
            "Ably Timer",
            AblyTimerHealthCheck(
                ably,
                "Topic",
                TimeSpan.FromSeconds 1.,
                TimeSpan.FromSeconds 1.
            )
        ) |> ignore
    services
        .AddHealthChecksUI(fun s ->
            s
                .SetEvaluationTimeInSeconds(60)
                .AddHealthCheckEndpoint(
                    "Self",
                    $"http://{Dns.GetHostName()}/health"
                ) |> ignore)
        .AddInMemoryStorage() |> ignore
        
    ...

member this.Configure(app: IApplicationBuilder, env: IWebHostEnvironment) =
    ...
    app.UseSwagger(fun opt -> ()) |> ignore
    app.UseSwaggerUI(fun opt ->
            opt.SwaggerEndpoint("/swagger/v1/swagger.json", "Notification Api V1")
            opt.RoutePrefix <- String.Empty
    ) |> ignore
    ...
    app.UseEndpoints(fun endpoints ->
            endpoints.MapControllers() |> ignore
            endpoints.MapHealthChecksUI(fun setup ->
                setup.UIPath <- "/ui-health"
                setup.ApiPath <- "/api-ui-health"
            ) |> ignore
            endpoints.MapHealthChecks(
                "/health",
                HealthCheckOptions(
                    Predicate = (fun _ -> true),
                    ResponseWriter = Func<HttpContext, HealthReport, Task>(fun (context) (c: HealthReport) -> UIResponseWriter.WriteHealthCheckUIResponse(context, c))
                )
            ) |> ignore
        ) |> ignore

Developing a realtime full stack app with .NET, Angular, and MongoDB

健康检查的仪表板视图

摘要

这就是整个 "企业 "应用程序,给你一些关于如何在你的应用程序中使用Ably的图片。源代码可在此获得。

一次性运行

要运行整个应用程序,你可以简单地运行docker compose up ,因为docker-compose 文件已经到位了。

version: '3'
volumes:
  retrospectiveLocalDb:
  retrospectiveLocalDbConfig:
services:
  database:
    image: db
    build:
      context: ./storage/mongo
      dockerfile: ./Dockerfile
    environment:
      - MONGO_INITDB_ROOT_USERNAME=sample
      - MONGO_INITDB_ROOT_PASSWORD=Sample123!
    volumes:
      - retrospectiveLocalDb:/data/db
      - retrospectiveLocalDbConfig:/data/configdb
    ports:
      - '27017-27019:27017-27019'
  topic:
    image: topic
    build:
      context: ./src
      dockerfile: ./Topic/Dockerfile
    ports:
      - '2137:80'
    depends_on:
      - database
    environment:
      MongoDB__Host: database
    hostname: topic
  notification:
    image: notification
    build:
      context: ./src
      dockerfile: ./Notification/Dockerfile
    ports:
      - '2138:80'
    hostname: notification
  facade:
    image: facade
    build:
      context: ./src
      dockerfile: ./Facade/Dockerfile
    ports:
      - '2111:5000'
    depends_on:
      - notification
      - topic
    hostname: facade

通过这个简单的 "企业 "应用程序的例子,我们可以看到,你可以用.NET、Angular、MongoDB和Ably构建一个实时全栈应用程序。

关于Ably


Ably是一个企业级的 pub/sub消息平台,它使高效设计、快速发货和无缝扩展直接交付给终端用户的关键实时功能变得容易。Ably每天为数千家公司的数百万用户提供数十亿条实时信息。

Ably平台的数学模型是围绕着 "可靠性的四大支柱 "来设计的,以确保信息不会丢失,同时还能通过 安全、可靠和 高度可用全球边缘网络以低延迟交付。试试Ably的API,看看为什么从初创公司到工业巨头的开发者都选择在Ably上构建,以简化工程,尽量减少DevOps的开销,并提高开发速度。