profileShare

rasmusjy / splitapp-backend-modular-monolith

Read-only snapshot

No repository description.

main default branch 418 files Expires Sep 13, 2026, 9:06 AM
Program.cs 9,483 bytes
1 using System.Globalization;
2 using Asp.Versioning;
3 using Asp.Versioning.ApiExplorer;
4 using Microsoft.AspNetCore.Localization;
5 using Microsoft.AspNetCore.Mvc;
6 using Microsoft.Extensions.Options;
7 using SplitApp.Modules.Expenses.Infrastructure;
8 using SplitApp.Modules.Trips.Infrastructure;
9 using SplitApp.Modules.Users.Infrastructure;
10 using SplitApp.WebApp.Hosting;
11 using Swashbuckle.AspNetCore.SwaggerGen;
12
13 var builder = WebApplication.CreateBuilder(args);
14
15 // MVC + API + view localization (matches phase 2). ApplicationParts surface every module's
16 // controllers without the host needing to know specifics.
17 builder.Services
18 .AddControllersWithViews(opts =>
19 {
20 opts.ModelBinderProviders.Insert(0, new InvariantDecimalModelBinderProvider());
21 })
22 .AddApplicationPart(typeof(SplitApp.Modules.Users.Api.Controllers.AccountController).Assembly)
23 .AddApplicationPart(typeof(SplitApp.Modules.Trips.Api.Controllers.TripsController).Assembly)
24 .AddApplicationPart(typeof(SplitApp.Modules.Expenses.Api.Controllers.ExpensesController).Assembly)
25 .AddViewLocalization()
26 .AddDataAnnotationsLocalization(options =>
27 {
28 // All [Display]/[Required]/[MaxLength] etc. annotations resolve their labels +
29 // error messages through Resources/Views/Shared.{resx,et.resx}. This restores
30 // phase 2's translated labels (e.g. asp-for="Trip.Name" → "Nimi") without
31 // needing per-entity resx files.
32 options.DataAnnotationLocalizerProvider = (type, factory) =>
33 factory.Create(typeof(App.Resources.Views.Shared));
34 });
35
36 builder.Services.AddRazorPages();
37
38 // API versioning
39 builder.Services.AddApiVersioning(options =>
40 {
41 options.ReportApiVersions = true;
42 options.DefaultApiVersion = new ApiVersion(1, 0);
43 options.AssumeDefaultVersionWhenUnspecified = true;
44 options.ApiVersionReader = new UrlSegmentApiVersionReader();
45 }).AddApiExplorer(options =>
46 {
47 options.GroupNameFormat = "'v'VVV";
48 options.SubstituteApiVersionInUrl = true;
49 });
50
51 builder.Services.AddEndpointsApiExplorer();
52 builder.Services.AddTransient<IConfigureOptions<SwaggerGenOptions>, ConfigureSwaggerOptions>();
53 builder.Services.AddSwaggerGen();
54
55 // Per-module DI registration. Composition root is the only place all three are visible.
56 builder.Services.AddUsersModule(builder.Configuration);
57 builder.Services.AddTripsModule(builder.Configuration);
58 builder.Services.AddExpensesModule(builder.Configuration);
59
60 // Composition-root facade: aggregates the three module DbContexts into a phase-2-style
61 // IAppUnitOfWork so the lifted BLL services keep working unchanged.
62 builder.Services.AddScoped<SplitApp.WebApp.Application.Contracts.IAppUnitOfWork,
63 SplitApp.WebApp.Application.Persistence.AppUnitOfWork>();
64 builder.Services.AddScoped<SplitApp.WebApp.Application.Persistence.CrossModuleNavigationLoader>();
65
66 // Phase 2 BLL services (lifted into WebApp/Application). These use IAppUnitOfWork.
67 builder.Services.AddScoped<SplitApp.WebApp.Application.Services.ITripService, SplitApp.WebApp.Application.Services.TripService>();
68 builder.Services.AddScoped<SplitApp.WebApp.Application.Services.IExpenseService, SplitApp.WebApp.Application.Services.ExpenseService>();
69 builder.Services.AddScoped<SplitApp.WebApp.Application.Services.ISettlementService, SplitApp.WebApp.Application.Services.SettlementService>();
70 builder.Services.AddScoped<SplitApp.WebApp.Application.Services.IInvitationService, SplitApp.WebApp.Application.Services.InvitationService>();
71 builder.Services.AddScoped<SplitApp.WebApp.Application.Services.IPollService, SplitApp.WebApp.Application.Services.PollService>();
72 builder.Services.AddScoped<SplitApp.WebApp.Application.Services.IBudgetCategoryService, SplitApp.WebApp.Application.Services.BudgetCategoryService>();
73 builder.Services.AddScoped<SplitApp.WebApp.Application.Services.IWishlistService, SplitApp.WebApp.Application.Services.WishlistService>();
74 builder.Services.AddScoped<SplitApp.WebApp.Application.Services.ISplitPresetService, SplitApp.WebApp.Application.Services.SplitPresetService>();
75 builder.Services.AddScoped<SplitApp.WebApp.Application.Services.Identity.IIdentityService, SplitApp.WebApp.Application.Services.Identity.IdentityService>();
76
77 // Admin BLL services
78 builder.Services.AddScoped<SplitApp.WebApp.Application.Services.Admin.IBudgetCategoryAdminService, SplitApp.WebApp.Application.Services.Admin.BudgetCategoryAdminService>();
79 builder.Services.AddScoped<SplitApp.WebApp.Application.Services.Admin.ICurrencyAdminService, SplitApp.WebApp.Application.Services.Admin.CurrencyAdminService>();
80 builder.Services.AddScoped<SplitApp.WebApp.Application.Services.Admin.ITripAdminService, SplitApp.WebApp.Application.Services.Admin.TripAdminService>();
81 builder.Services.AddScoped<SplitApp.WebApp.Application.Services.Admin.IExpenseAdminService, SplitApp.WebApp.Application.Services.Admin.ExpenseAdminService>();
82 builder.Services.AddScoped<SplitApp.WebApp.Application.Services.Admin.IPollAdminService, SplitApp.WebApp.Application.Services.Admin.PollAdminService>();
83 builder.Services.AddScoped<SplitApp.WebApp.Application.Services.Admin.IWishlistAdminService, SplitApp.WebApp.Application.Services.Admin.WishlistAdminService>();
84 builder.Services.AddScoped<SplitApp.WebApp.Application.Services.Admin.ISettlementPlanAdminService, SplitApp.WebApp.Application.Services.Admin.SettlementPlanAdminService>();
85 builder.Services.AddScoped<SplitApp.WebApp.Application.Services.Admin.ISettlementPaymentAdminService, SplitApp.WebApp.Application.Services.Admin.SettlementPaymentAdminService>();
86 builder.Services.AddScoped<SplitApp.WebApp.Application.Services.Admin.ISplitPresetAdminService, SplitApp.WebApp.Application.Services.Admin.SplitPresetAdminService>();
87 builder.Services.AddScoped<SplitApp.WebApp.Application.Services.Admin.ITripParticipantAdminService, SplitApp.WebApp.Application.Services.Admin.TripParticipantAdminService>();
88 builder.Services.AddScoped<SplitApp.WebApp.Application.Services.Admin.IInvitationAdminService, SplitApp.WebApp.Application.Services.Admin.InvitationAdminService>();
89 builder.Services.AddScoped<SplitApp.WebApp.Application.Services.Admin.IAdminStatsService, SplitApp.WebApp.Application.Services.Admin.AdminStatsService>();
90
91 // Localization — EN+ET. resx files in Resources/Views/Shared.{resx,et.resx} are
92 // embedded with explicit LogicalName so they match the App.Resources.Views.Shared
93 // marker type (declared in Resources/Shared.cs). ResourcesPath="" because the
94 // LogicalName already targets the namespace directly.
95 builder.Services.AddLocalization(options => options.ResourcesPath = "");
96
97 var supportedCultures = (builder.Configuration.GetSection("SupportedCultures").Get<string[]>()
98 ?? new[] { "en", "et" })
99 .Select(c => new CultureInfo(c)).ToArray();
100 builder.Services.Configure<RequestLocalizationOptions>(options =>
101 {
102 options.SupportedCultures = supportedCultures;
103 options.SupportedUICultures = supportedCultures;
104 options.DefaultRequestCulture = new RequestCulture("en", "en");
105 options.SetDefaultCulture("en");
106 options.RequestCultureProviders = new List<IRequestCultureProvider>
107 {
108 new QueryStringRequestCultureProvider(),
109 new CookieRequestCultureProvider()
110 };
111 });
112
113 builder.Services.AddAuthorization();
114
115 // CORS — same wide-open policy as phase 2 so external SPA frontends (Vite/Vue) can call
116 // /api/v1/* directly. WithExposedHeaders surfaces api-versioning headers to the JS client.
117 builder.Services.AddCors(options =>
118 {
119 options.AddPolicy("CorsAllowAll", policy =>
120 {
121 policy
122 .AllowAnyOrigin()
123 .AllowAnyHeader()
124 .AllowAnyMethod()
125 .WithExposedHeaders("X-Version", "X-Version-Created-At");
126 });
127 });
128
129 var app = builder.Build();
130
131 // Per-module pipeline + migration hooks. Skipped in Testing so WebApplicationFactory
132 // smoke tests can boot without a real database.
133 if (!app.Environment.IsEnvironment("Testing"))
134 {
135 app.UseUsersModule();
136 app.UseTripsModule();
137 app.UseExpensesModule();
138
139 // Cross-module example data — host-level concern, runs after all per-module
140 // migrations + per-module seeds (currencies, roles, identity users) are in place.
141 if (app.Configuration.GetValue<bool>("DataInitialization:SeedData"))
142 {
143 SplitApp.WebApp.Hosting.AppDataInit.SeedExampleData(app.Services);
144 }
145 }
146
147 if (app.Environment.IsDevelopment())
148 {
149 app.UseDeveloperExceptionPage();
150 }
151 else
152 {
153 app.UseExceptionHandler("/Home/Error");
154 }
155
156 // Swagger is always on so the Docker container (Production env) still serves it.
157 app.UseSwagger();
158 app.UseSwaggerUI(options =>
159 {
160 var provider = app.Services.GetRequiredService<IApiVersionDescriptionProvider>();
161 foreach (var description in provider.ApiVersionDescriptions)
162 {
163 options.SwaggerEndpoint(
164 $"/swagger/{description.GroupName}/swagger.json",
165 description.GroupName.ToUpperInvariant());
166 }
167 });
168
169 app.UseStaticFiles();
170 app.UseHttpsRedirection();
171 app.UseRouting();
172
173 app.UseRequestLocalization(app.Services.GetRequiredService<IOptions<RequestLocalizationOptions>>().Value);
174
175 app.UseCors("CorsAllowAll");
176
177 app.UseAuthentication();
178 app.UseAuthorization();
179
180 app.MapControllerRoute(
181 name: "areas",
182 pattern: "{area:exists}/{controller=Home}/{action=Index}/{id?}");
183 app.MapControllerRoute(
184 name: "default",
185 pattern: "{controller=Home}/{action=Index}/{id?}");
186 app.MapControllers();
187 app.MapRazorPages();
188
189 app.Run();
190
191 public partial class Program;
192