From c503083df7e8e85c2d70c07f84e67c731c13b273 Mon Sep 17 00:00:00 2001 From: LittleSheep Date: Thu, 1 Jan 2026 15:06:49 +0800 Subject: [PATCH] :tada: Initialize the DysonNetwork.Messager service --- DysonNetwork.Messager/.gitignore | 5 + DysonNetwork.Messager/AppDatabase.cs | 109 ++++++++++++++++++ DysonNetwork.Messager/Dockerfile | 41 +++++++ .../DysonNetwork.Messager.csproj | 39 +++++++ DysonNetwork.Messager/Program.cs | 42 +++++++ .../Startup/ApplicationConfiguration.cs | 23 ++++ .../Startup/ScheduledJobsConfiguration.cs | 21 ++++ .../Startup/ServiceCollectionExtensions.cs | 52 +++++++++ DysonNetwork.Messager/appsettings.json | 29 +++++ DysonNetwork.sln | 14 +++ 10 files changed, 375 insertions(+) create mode 100644 DysonNetwork.Messager/.gitignore create mode 100644 DysonNetwork.Messager/AppDatabase.cs create mode 100644 DysonNetwork.Messager/Dockerfile create mode 100644 DysonNetwork.Messager/DysonNetwork.Messager.csproj create mode 100644 DysonNetwork.Messager/Program.cs create mode 100644 DysonNetwork.Messager/Startup/ApplicationConfiguration.cs create mode 100644 DysonNetwork.Messager/Startup/ScheduledJobsConfiguration.cs create mode 100644 DysonNetwork.Messager/Startup/ServiceCollectionExtensions.cs create mode 100644 DysonNetwork.Messager/appsettings.json diff --git a/DysonNetwork.Messager/.gitignore b/DysonNetwork.Messager/.gitignore new file mode 100644 index 0000000..9c738c8 --- /dev/null +++ b/DysonNetwork.Messager/.gitignore @@ -0,0 +1,5 @@ +Keys +Uploads +DataProtection-Keys + +.DS_Store diff --git a/DysonNetwork.Messager/AppDatabase.cs b/DysonNetwork.Messager/AppDatabase.cs new file mode 100644 index 0000000..dd97320 --- /dev/null +++ b/DysonNetwork.Messager/AppDatabase.cs @@ -0,0 +1,109 @@ +using System.Linq.Expressions; +using DysonNetwork.Shared.Data; +using DysonNetwork.Shared.Models; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Design; +using Microsoft.EntityFrameworkCore.Query; +using NodaTime; +using Quartz; + +namespace DysonNetwork.Messager; + +public class AppDatabase( + DbContextOptions options, + IConfiguration configuration +) : DbContext(options) +{ + protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) + { + optionsBuilder.UseNpgsql( + configuration.GetConnectionString("App"), + opt => opt + .ConfigureDataSource(optSource => optSource.EnableDynamicJson()) + .UseQuerySplittingBehavior(QuerySplittingBehavior.SplitQuery) + .UseNodaTime() + ).UseSnakeCaseNamingConvention(); + + base.OnConfiguring(optionsBuilder); + } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + base.OnModelCreating(modelBuilder); + + modelBuilder.ApplySoftDeleteFilters(); + } + + private static void SetSoftDeleteFilter(ModelBuilder modelBuilder) + where TEntity : ModelBase + { + modelBuilder.Entity().HasQueryFilter(e => e.DeletedAt == null); + } + + public override async Task SaveChangesAsync(CancellationToken cancellationToken = default) + { + this.ApplyAuditableAndSoftDelete(); + return await base.SaveChangesAsync(cancellationToken); + } +} + +public class AppDatabaseRecyclingJob(AppDatabase db, ILogger logger) : IJob +{ + public async Task Execute(IJobExecutionContext context) + { + var now = SystemClock.Instance.GetCurrentInstant(); + + logger.LogInformation("Deleting soft-deleted records..."); + + var threshold = now - Duration.FromDays(7); + + var entityTypes = db.Model.GetEntityTypes() + .Where(t => typeof(ModelBase).IsAssignableFrom(t.ClrType) && t.ClrType != typeof(ModelBase)) + .Select(t => t.ClrType); + + foreach (var entityType in entityTypes) + { + var set = (IQueryable)db.GetType().GetMethod(nameof(DbContext.Set), Type.EmptyTypes)! + .MakeGenericMethod(entityType).Invoke(db, null)!; + var parameter = Expression.Parameter(entityType, "e"); + var property = Expression.Property(parameter, nameof(ModelBase.DeletedAt)); + var condition = Expression.LessThan(property, Expression.Constant(threshold, typeof(Instant?))); + var notNull = Expression.NotEqual(property, Expression.Constant(null, typeof(Instant?))); + var finalCondition = Expression.AndAlso(notNull, condition); + var lambda = Expression.Lambda(finalCondition, parameter); + + var queryable = set.Provider.CreateQuery( + Expression.Call( + typeof(Queryable), + "Where", + [entityType], + set.Expression, + Expression.Quote(lambda) + ) + ); + + var toListAsync = typeof(EntityFrameworkQueryableExtensions) + .GetMethod(nameof(EntityFrameworkQueryableExtensions.ToListAsync))! + .MakeGenericMethod(entityType); + + var items = await (dynamic)toListAsync.Invoke(null, [queryable, CancellationToken.None])!; + db.RemoveRange(items); + } + + await db.SaveChangesAsync(); + } +} + +public class AppDatabaseFactory : IDesignTimeDbContextFactory +{ + public AppDatabase CreateDbContext(string[] args) + { + var configuration = new ConfigurationBuilder() + .SetBasePath(Directory.GetCurrentDirectory()) + .AddJsonFile("appsettings.json") + .Build(); + + var optionsBuilder = new DbContextOptionsBuilder(); + return new AppDatabase(optionsBuilder.Options, configuration); + } +} diff --git a/DysonNetwork.Messager/Dockerfile b/DysonNetwork.Messager/Dockerfile new file mode 100644 index 0000000..9849020 --- /dev/null +++ b/DysonNetwork.Messager/Dockerfile @@ -0,0 +1,41 @@ +# Stage 1: Base runtime image +FROM mcr.microsoft.com/dotnet/aspnet:10.0 AS base +RUN apt-get update && \ + apt-get install -y --no-install-recommends \ + libkrb5-3 \ + libgssapi-krb5-2 \ + && rm -rf /var/lib/apt/lists/* +USER $APP_UID +WORKDIR /app +EXPOSE 8080 +EXPOSE 8081 + +# Stage 2: Build .NET application +FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build +ARG BUILD_CONFIGURATION=Release +WORKDIR /src + +# First copy only the solution and project files to restore packages +COPY ["DysonNetwork.Messager/DysonNetwork.Messager.csproj", "DysonNetwork.Messager/"] +COPY ["DysonNetwork.Shared/DysonNetwork.Shared.csproj", "DysonNetwork.Shared/"] + +# Restore packages +RUN dotnet restore "DysonNetwork.Messager/DysonNetwork.Messager.csproj" + +# Copy everything else and build +COPY . . + +# Build the application +WORKDIR "/src/DysonNetwork.Messager" +RUN dotnet build "DysonNetwork.Messager.csproj" -c $BUILD_CONFIGURATION -o /app/build + +# Stage 4: Publish +FROM build AS publish +ARG BUILD_CONFIGURATION=Release +RUN dotnet publish "DysonNetwork.Messager.csproj" -c $BUILD_CONFIGURATION -o /app/publish /p:UseAppHost=false + +# Final stage: Runtime +FROM base AS final +WORKDIR /app +COPY --from=publish /app/publish . +ENTRYPOINT ["dotnet", "DysonNetwork.Messager.dll"] diff --git a/DysonNetwork.Messager/DysonNetwork.Messager.csproj b/DysonNetwork.Messager/DysonNetwork.Messager.csproj new file mode 100644 index 0000000..955c80b --- /dev/null +++ b/DysonNetwork.Messager/DysonNetwork.Messager.csproj @@ -0,0 +1,39 @@ + + + + net10.0 + enable + enable + Linux + cfdec342-d2f2-4a86-800b-93f0a0e4abde + en-US;zh-Hans + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + + + .dockerignore + + + + + + + + diff --git a/DysonNetwork.Messager/Program.cs b/DysonNetwork.Messager/Program.cs new file mode 100644 index 0000000..ec763fb --- /dev/null +++ b/DysonNetwork.Messager/Program.cs @@ -0,0 +1,42 @@ +using DysonNetwork.Shared.Auth; +using DysonNetwork.Shared.Http; +using DysonNetwork.Shared.Registry; +using DysonNetwork.Messager; +using DysonNetwork.Messager.Startup; +using Microsoft.EntityFrameworkCore; + +var builder = WebApplication.CreateBuilder(args); + +builder.AddServiceDefaults(); + +builder.ConfigureAppKestrel(builder.Configuration); + +builder.Services.AddAppServices(); +builder.Services.AddAppAuthentication(); +builder.Services.AddDysonAuth(); +builder.Services.AddAccountService(); +builder.Services.AddRingService(); + +builder.Services.AddAppBusinessServices(builder.Configuration); +builder.Services.AddAppScheduledJobs(); + +builder.AddSwaggerManifest( + "DysonNetwork.Messager", + "The real-time messaging service in the Solar Network." +); + +var app = builder.Build(); + +app.MapDefaultEndpoints(); + +using (var scope = app.Services.CreateScope()) +{ + var db = scope.ServiceProvider.GetRequiredService(); + await db.Database.MigrateAsync(); +} + +app.ConfigureAppMiddleware(builder.Configuration); + +app.UseSwaggerManifest("DysonNetwork.Messager"); + +app.Run(); diff --git a/DysonNetwork.Messager/Startup/ApplicationConfiguration.cs b/DysonNetwork.Messager/Startup/ApplicationConfiguration.cs new file mode 100644 index 0000000..ba631d8 --- /dev/null +++ b/DysonNetwork.Messager/Startup/ApplicationConfiguration.cs @@ -0,0 +1,23 @@ +using DysonNetwork.Shared.Auth; +using DysonNetwork.Shared.Http; + +namespace DysonNetwork.Messager.Startup; + +public static class ApplicationConfiguration +{ + public static WebApplication ConfigureAppMiddleware(this WebApplication app, IConfiguration configuration) + { + app.ConfigureForwardedHeaders(configuration); + + app.UseWebSockets(); + app.UseAuthentication(); + app.UseAuthorization(); + app.UseMiddleware(); + + app.MapControllers(); + + app.MapGrpcReflectionService(); + + return app; + } +} diff --git a/DysonNetwork.Messager/Startup/ScheduledJobsConfiguration.cs b/DysonNetwork.Messager/Startup/ScheduledJobsConfiguration.cs new file mode 100644 index 0000000..2553db8 --- /dev/null +++ b/DysonNetwork.Messager/Startup/ScheduledJobsConfiguration.cs @@ -0,0 +1,21 @@ +using Quartz; + +namespace DysonNetwork.Messager.Startup; + +public static class ScheduledJobsConfiguration +{ + public static IServiceCollection AddAppScheduledJobs(this IServiceCollection services) + { + services.AddQuartz(q => + { + q.AddJob(opts => opts.WithIdentity("AppDatabaseRecycling")); + q.AddTrigger(opts => opts + .ForJob("AppDatabaseRecycling") + .WithIdentity("AppDatabaseRecyclingTrigger") + .WithCronSchedule("0 0 0 * * ?")); + }); + services.AddQuartzHostedService(q => q.WaitForJobsToComplete = true); + + return services; + } +} diff --git a/DysonNetwork.Messager/Startup/ServiceCollectionExtensions.cs b/DysonNetwork.Messager/Startup/ServiceCollectionExtensions.cs new file mode 100644 index 0000000..951f379 --- /dev/null +++ b/DysonNetwork.Messager/Startup/ServiceCollectionExtensions.cs @@ -0,0 +1,52 @@ +using System.Globalization; +using System.Text.Json; +using System.Text.Json.Serialization; +using NodaTime; +using NodaTime.Serialization.SystemTextJson; + +namespace DysonNetwork.Messager.Startup; + +public static class ServiceCollectionExtensions +{ + public static IServiceCollection AddAppServices(this IServiceCollection services) + { + services.AddDbContext(); + services.AddHttpContextAccessor(); + + services.AddHttpClient(); + + services + .AddControllers() + .AddJsonOptions(options => + { + options.JsonSerializerOptions.NumberHandling = + JsonNumberHandling.AllowNamedFloatingPointLiterals; + options.JsonSerializerOptions.PropertyNamingPolicy = + JsonNamingPolicy.SnakeCaseLower; + + options.JsonSerializerOptions.ConfigureForNodaTime(DateTimeZoneProviders.Tzdb); + }); + + services.AddGrpc(options => + { + options.EnableDetailedErrors = true; + }); + services.AddGrpcReflection(); + + return services; + } + + public static IServiceCollection AddAppAuthentication(this IServiceCollection services) + { + services.AddAuthorization(); + return services; + } + + public static IServiceCollection AddAppBusinessServices( + this IServiceCollection services, + IConfiguration configuration + ) + { + return services; + } +} diff --git a/DysonNetwork.Messager/appsettings.json b/DysonNetwork.Messager/appsettings.json new file mode 100644 index 0000000..860fa77 --- /dev/null +++ b/DysonNetwork.Messager/appsettings.json @@ -0,0 +1,29 @@ +{ + "Debug": true, + "BaseUrl": "http://localhost:5072", + "SiteUrl": "https://solian.app", + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*", + "ConnectionStrings": { + "App": "Host=localhost;Port=5432;Database=dyson_messager;Username=postgres;Password=postgres;Include Error Detail=True;Maximum Pool Size=20;Connection Idle Lifetime=60" + }, + "KnownProxies": [ + "127.0.0.1", + "::1" + ], + "Etcd": { + "Insecure": true + }, + "Cache": { + "Serializer": "MessagePack" + }, + "Service": { + "Name": "DysonNetwork.Messager", + "Url": "https://localhost:7100" + } +} diff --git a/DysonNetwork.sln b/DysonNetwork.sln index 5123f71..d5cce91 100644 --- a/DysonNetwork.sln +++ b/DysonNetwork.sln @@ -25,6 +25,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DysonNetwork.Insight", "Dys EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DysonNetwork.Zone", "DysonNetwork.Zone\DysonNetwork.Zone.csproj", "{E255B723-CAC9-4AC8-AA3B-116CC256E63C}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DysonNetwork.Messager", "DysonNetwork.Messager\DysonNetwork.Messager.csproj", "{4011F9B8-D691-4BCE-B2F8-2766688C5FFB}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -155,6 +157,18 @@ Global {E255B723-CAC9-4AC8-AA3B-116CC256E63C}.Release|x64.Build.0 = Release|Any CPU {E255B723-CAC9-4AC8-AA3B-116CC256E63C}.Release|x86.ActiveCfg = Release|Any CPU {E255B723-CAC9-4AC8-AA3B-116CC256E63C}.Release|x86.Build.0 = Release|Any CPU + {4011F9B8-D691-4BCE-B2F8-2766688C5FFB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {4011F9B8-D691-4BCE-B2F8-2766688C5FFB}.Debug|Any CPU.Build.0 = Debug|Any CPU + {4011F9B8-D691-4BCE-B2F8-2766688C5FFB}.Debug|x64.ActiveCfg = Debug|Any CPU + {4011F9B8-D691-4BCE-B2F8-2766688C5FFB}.Debug|x64.Build.0 = Debug|Any CPU + {4011F9B8-D691-4BCE-B2F8-2766688C5FFB}.Debug|x86.ActiveCfg = Debug|Any CPU + {4011F9B8-D691-4BCE-B2F8-2766688C5FFB}.Debug|x86.Build.0 = Debug|Any CPU + {4011F9B8-D691-4BCE-B2F8-2766688C5FFB}.Release|Any CPU.ActiveCfg = Release|Any CPU + {4011F9B8-D691-4BCE-B2F8-2766688C5FFB}.Release|Any CPU.Build.0 = Release|Any CPU + {4011F9B8-D691-4BCE-B2F8-2766688C5FFB}.Release|x64.ActiveCfg = Release|Any CPU + {4011F9B8-D691-4BCE-B2F8-2766688C5FFB}.Release|x64.Build.0 = Release|Any CPU + {4011F9B8-D691-4BCE-B2F8-2766688C5FFB}.Release|x86.ActiveCfg = Release|Any CPU + {4011F9B8-D691-4BCE-B2F8-2766688C5FFB}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE