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 | |