();
cfg.SetDataCompatibilityLevel(CompatibilityLevel.Version_180)
.UseSimpleAssemblyNameTypeSerializer()
.UseRecommendedSerializerSettings()
.UsePostgreSqlStorage(pgCfg =>
- pgCfg.UseNpgsqlConnection(postgresOptions.GetConnectionString("Hangfire"))
+ pgCfg.UseNpgsqlConnection(configuration.GetConnectionString("postgres"))
);
}
);
@@ -261,4 +263,33 @@ public static IServiceCollection ConfigureHangfire(this IServiceCollection servi
return serviceCollection;
}
+
+ public static WebApplicationBuilder ConfigureObservability(this WebApplicationBuilder builder)
+ {
+ // Custom config on top of ServiceDefaults
+
+ bool isDevelopment = builder.Environment.IsDevelopment();
+
+ builder
+ .Services.AddOpenTelemetry()
+ .ConfigureResource(cfg =>
+ {
+ cfg.AddService(serviceName: "dragalia-api", autoGenerateServiceInstanceId: false);
+ });
+
+ if (builder.HasOtlpTracesEndpoint())
+ {
+ builder
+ .Services.AddOpenTelemetry()
+ .WithTracing(tracing =>
+ tracing.AddEntityFrameworkCoreInstrumentation(options =>
+ options.SetDbStatementForText = isDevelopment
+ )
+ // Not compatible with IDistributedCache as requires IConnectionMultiplexer
+ // .AddRedisInstrumentation()
+ );
+ }
+
+ return builder;
+ }
}
diff --git a/DragaliaAPI/README.md b/DragaliaAPI/README.md
index 0d6060b5a..9e9ab82fe 100644
--- a/DragaliaAPI/README.md
+++ b/DragaliaAPI/README.md
@@ -6,45 +6,40 @@ DragaliaAPI is the main server component of Dawnshard, which handles the vast ma
The server depends on [`DragaliaBaas`](https://github.com/DragaliaLostRevival/DragaliaBaasServer) as an identity
provider. Clients are expected to go to an instance of the BaaS for login and authentication, and then come back
-to `/tool/auth` with a signed JSON web token to authenticate against DragaliaAPI.
+to `/tool/auth` with a signed JSON web token to authenticate against DragaliaAPI. The development instance is configured to point
+at a publicly hosted instance of BaaS.
## Development environment
### Run the server
-To get started, copy the `.env.default` file to `.env`. Choose some values for the database credentials, and then launch
-the compose project from your IDE. Or, if using the command line,
-use `docker-compose -f docker-compose.yml -f docker-compose.override.yml --profiles dragaliaapi`.
+For local development, the recommended workflow is using the [.NET Aspire](https://learn.microsoft.com/en-us/dotnet/aspire/get-started/aspire-overview) AppHost.
+This will handle starting up the app and spinning up Docker containers for the dependent services.
-The solution includes a `docker-compose.dcproj` project file which should be plug-and-play with Visual Studio and allow
-launching the API plus the supporting Postgres and Redis services. It is compatible with container fast mode, so you can
-iterate during development without rebuilding the containers each time. Other IDEs, including JetBrains Rider, should
-also able to use the `docker-compose.yml` file if you add a run configuration pointed at it (as well
-as `docker-compose.override.yml`). For users who are not using Visual Studio, ensure that your `docker-compose`
-configuration or command includes an instruction to use the `dragaliaapi` profile so that the API is launched.
+This has the following dependencies:
-If you have issues with using the container fast mode, you can use the docker-compose file to only launch the supporting
-services and then run the API directly on your machine. Either remove the profile arguments in your IDE or just
-run `docker-compose -f docker-compose.yml up -d` from the command line without any `--profile` arguments to start Redis
-and Postgres, and then launch the main project. You will need to configure the environment variables that it is run with
-to match what is set in `docker-compose.yml`, and also to adjust the hostnames of Redis and Postgres now that it is not
-running in the container network.
+- A container runtime. On Windows, [Docker Desktop](https://docs.docker.com/desktop/install/windows-install/) is required to run Linux containers.
+- The .NET Aspire workload.
-An example configuration for running outside a container which is supported by Rider, Visual Studio, and the `dotnet`
-cli, is included in [launchSettings.json](./DragaliaAPI/Properties/launchSettings.json). It does not include credentials
-as it is in source control. The recommended way to set the credentials is using user secrets. See the [Microsoft Learn](https://learn.microsoft.com/en-us/aspnet/core/security/app-secrets#secret-manager) documentation on user secrets for
-more information.
+For more information, see the [Microsoft documentation on setting up .NET Aspire](https://learn.microsoft.com/en-us/dotnet/aspire/fundamentals/setup-tooling).
-You will need to set the following values corresponding to the values being used by Docker in `.env`:
+The Aspire app host should be compatible with most IDEs like JetBrains Rider and Visual Studio. Simply select the `Dawnshard.AppHost: https` launch profile to start it up.
-- `PostgresOptions:Username`
-- `PostgresOptions:Password`
-- `PostgresOptions:Database`
+Rider:
+![Aspire on Rider](rider-aspire.png)
+Visual Studio:
+![Aspire on Visual Studio](vs-aspire.png)
+Alternatively, you can use the command line. From the repository root:
+
+```
+$ cd Dawnshard.AppHost
+$ dotnet run
+```
### Set up a client
-The `docker-compose.yml` / `launchSettings.json` file will start the server on port 80, so you can
+The `launchSettings.json` file will start the server on port 80, so you can
use [Dragalipatch](https://github.com/LukeFZ/DragaliPatch/releases/latest) with
your [PC's local IP address](https://support.microsoft.com/en-us/windows/find-your-ip-address-in-windows-f21a9bbc-c582-55cd-35e0-73431160a1b9)
to play on your local server with an emulator or mobile device. You must input the local IP address
@@ -61,7 +56,7 @@ information.
### Dedicated server
-On a dedicated server, the basic `docker-compose` setup will work, but additional considerations should be made
+On a dedicated server, additional considerations should be made
regarding reverse proxying, logging, etc. Speak to the maintainer if you are interested in hosting your own instance for
further guidance.
diff --git a/DragaliaAPI/rider-aspire.png b/DragaliaAPI/rider-aspire.png
new file mode 100644
index 0000000000000000000000000000000000000000..f02402fd7f06d0105326a60e4770cd47d34b43a7
GIT binary patch
literal 4308
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
new Dictionary
{
["PhotonOptions:Token"] = "photontoken",
- ["RedisOptions:Hostname"] = this.testContainersHelper.RedisHost,
- ["RedisOptions:Port"] = this.testContainersHelper.RedisPort.ToString(),
+ ["ConnectionStrings:Redis"] =
+ this.testContainersHelper.GetRedisConnectionString(),
}
)
);
diff --git a/PhotonStateManager/DragaliaAPI.Photon.StateManager.Test/TestContainersHelper.cs b/PhotonStateManager/DragaliaAPI.Photon.StateManager.Test/TestContainersHelper.cs
index ef9a050c6..1c95fab49 100644
--- a/PhotonStateManager/DragaliaAPI.Photon.StateManager.Test/TestContainersHelper.cs
+++ b/PhotonStateManager/DragaliaAPI.Photon.StateManager.Test/TestContainersHelper.cs
@@ -1,4 +1,5 @@
-using DotNet.Testcontainers.Builders;
+using System.Diagnostics.CodeAnalysis;
+using DotNet.Testcontainers.Builders;
using DotNet.Testcontainers.Containers;
namespace DragaliaAPI.Photon.StateManager.Test;
@@ -9,30 +10,21 @@ public class TestContainersHelper
private readonly IContainer? redisContainer;
- public string RedisHost { get; private set; }
-
- public int RedisPort
+ public string GetRedisConnectionString()
{
- get
+ if (IsGithubActions)
{
- if (IsGithubActions)
- return RedisContainerPort;
-
- ArgumentNullException.ThrowIfNull(this.redisContainer);
- return this.redisContainer.GetMappedPublicPort(RedisContainerPort);
+ return $"localhost:{RedisContainerPort}";
}
- }
- private static bool IsGithubActions =>
- Environment.GetEnvironmentVariable("GITHUB_ACTIONS") is not null;
+ this.ThrowIfRedisContainerNull();
+
+ return $"{this.redisContainer.Hostname}:{this.redisContainer.GetMappedPublicPort(RedisContainerPort)}";
+ }
public TestContainersHelper()
{
- if (IsGithubActions)
- {
- RedisHost = "localhost";
- }
- else
+ if (!IsGithubActions)
{
redisContainer = new ContainerBuilder()
.WithImage("redis/redis-stack")
@@ -40,26 +32,42 @@ public TestContainersHelper()
.WithPortBinding(RedisContainerPort, true)
.WithPortBinding(8001, true)
.Build();
-
- RedisHost = redisContainer.Hostname;
}
}
public async Task StartAsync()
{
if (IsGithubActions)
+ {
return;
+ }
+
+ this.ThrowIfRedisContainerNull();
- ArgumentNullException.ThrowIfNull(this.redisContainer);
await this.redisContainer.StartAsync();
}
public async Task StopAsync()
{
if (IsGithubActions)
+ {
return;
+ }
+
+ this.ThrowIfRedisContainerNull();
- ArgumentNullException.ThrowIfNull(this.redisContainer);
await this.redisContainer.StopAsync();
}
+
+ [MemberNotNull(nameof(this.redisContainer))]
+ private void ThrowIfRedisContainerNull()
+ {
+ if (this.redisContainer is null)
+ {
+ throw new InvalidOperationException("Redis container not initialized!");
+ }
+ }
+
+ private static bool IsGithubActions =>
+ Environment.GetEnvironmentVariable("GITHUB_ACTIONS") is not null;
}
diff --git a/PhotonStateManager/DragaliaAPI.Photon.StateManager/Dockerfile b/PhotonStateManager/DragaliaAPI.Photon.StateManager/Dockerfile
index aef48f9cf..272b0d4d9 100644
--- a/PhotonStateManager/DragaliaAPI.Photon.StateManager/Dockerfile
+++ b/PhotonStateManager/DragaliaAPI.Photon.StateManager/Dockerfile
@@ -12,6 +12,7 @@ COPY ["nuget.config", "."]
RUN dotnet restore "PhotonStateManager/DragaliaAPI.Photon.StateManager/DragaliaAPI.Photon.StateManager.csproj"
COPY [".editorconfig", ".editorconfig"]
COPY ["PhotonStateManager/", "PhotonStateManager/"]
+COPY ["Aspire/", "Aspire/"]
COPY ["Shared/", "Shared/"]
WORKDIR "/src/PhotonStateManager/DragaliaAPI.Photon.StateManager"
RUN dotnet publish "DragaliaAPI.Photon.StateManager.csproj" -c Release -o /app/publish/ /p:UseAppHost=false
diff --git a/PhotonStateManager/DragaliaAPI.Photon.StateManager/DragaliaAPI.Photon.StateManager.csproj b/PhotonStateManager/DragaliaAPI.Photon.StateManager/DragaliaAPI.Photon.StateManager.csproj
index c391f8ad3..003e6830a 100644
--- a/PhotonStateManager/DragaliaAPI.Photon.StateManager/DragaliaAPI.Photon.StateManager.csproj
+++ b/PhotonStateManager/DragaliaAPI.Photon.StateManager/DragaliaAPI.Photon.StateManager.csproj
@@ -16,6 +16,7 @@
+
@@ -30,6 +31,7 @@