diff --git a/.github/workflows/cd.yml b/.github/workflows/cd.yml index 2b6894e..5126b6a 100644 --- a/.github/workflows/cd.yml +++ b/.github/workflows/cd.yml @@ -169,7 +169,7 @@ jobs: echo A | xcopy .\bin\publish-x64 .\publish-result /s /e /h echo A | xcopy .\bin\publish .\publish-result /s /e /h dotnet tool restore - dotnet vpk pack -u Bit.TemplatePlayground.Client.Windows -v "${{ vars.APPLICATION_DISPLAY_VERSION }}" -p .\publish-result -e Bit.TemplatePlayground.Client.Windows-x64.exe -r win-x64 --framework net8.0.1-x64-desktop,webview2 --icon .\wwwroot\favicon.ico --packTitle 'Bit.TemplatePlayground' + dotnet vpk pack -u Bit.TemplatePlayground.Client.Windows -v "${{ vars.APPLICATION_DISPLAY_VERSION }}" -p .\publish-result -e Bit.TemplatePlayground.Client.Windows-x86.exe -r win-x86 --framework net8.0.1-x86-desktop,webview2 --icon .\wwwroot\favicon.ico --packTitle 'Bit.TemplatePlayground' - name: Upload artifact uses: actions/upload-artifact@v2 diff --git a/Bit.TemplatePlayground.Web.slnf b/Bit.TemplatePlayground.Web.slnf new file mode 100644 index 0000000..4c144ac --- /dev/null +++ b/Bit.TemplatePlayground.Web.slnf @@ -0,0 +1,11 @@ +{ + "solution": { + "path": "Bit.TemplatePlayground.sln", + "projects": [ + "src\\Bit.TemplatePlayground.Server\\Bit.TemplatePlayground.Server.csproj", + "src\\Bit.TemplatePlayground.Shared\\Bit.TemplatePlayground.Shared.csproj", + "src\\Client\\Bit.TemplatePlayground.Client.Core\\Bit.TemplatePlayground.Client.Core.csproj", + "src\\Client\\Bit.TemplatePlayground.Client.Web\\Bit.TemplatePlayground.Client.Web.csproj" + ] + } +} \ No newline at end of file diff --git a/Bit.TemplatePlayground.sln b/Bit.TemplatePlayground.sln index f7b2f62..612c7d1 100644 --- a/Bit.TemplatePlayground.sln +++ b/Bit.TemplatePlayground.sln @@ -84,6 +84,8 @@ Global {E3CB3C34-F5DE-4A96-B552-7D52BCAD1E1F} = {248D8229-BABD-4F0A-A9C6-0417B464507B} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution + RESX_NeutralResourcesLanguage = en-US + RESX_CodeReferences = {"Items":[{"Expression":"\\W($File.$Key)\\W","Extensions":".cs,.xaml,.cshtml,.razor","IsCaseSensitive":true,"SingleLineComment":"\/\/"},{"Expression":"\\W($File.$Key)\\W","Extensions":".vbhtml","IsCaseSensitive":false,"SingleLineComment":null},{"Expression":"ResourceManager.GetString\\(\"($Key)\"\\)","Extensions":".cs","IsCaseSensitive":true,"SingleLineComment":"\/\/"},{"Expression":"typeof\\((\\w+\\.)*($File)\\).+\"($Key)\"|\"($Key)\".+typeof\\((\\w+\\.)*($File)\\)","Extensions":".cs","IsCaseSensitive":true,"SingleLineComment":"\/\/"},{"Expression":"\\W($Key)\\W","Extensions":".vb","IsCaseSensitive":false,"SingleLineComment":"'"},{"Expression":"\\W($File::$Key)\\W","Extensions":".cpp,.c,.hxx,.h","IsCaseSensitive":true,"SingleLineComment":"\/\/"},{"Expression":"<%\\$\\s+Resources:\\s*($File)\\s*,\\s*($Key)\\s*%>","Extensions":".aspx,.ascx,.master","IsCaseSensitive":true,"SingleLineComment":null},{"Expression":"StringResourceKey\\.($Key)","Extensions":".cs","IsCaseSensitive":true,"SingleLineComment":"\/\/"},{"Expression":"\\.($Key)","Extensions":".ts,.html","IsCaseSensitive":true,"SingleLineComment":"\/\/"}]} SolutionGuid = {61F7FB11-1E47-470C-91E2-47F8143E1572} EndGlobalSection EndGlobal diff --git a/global.json b/global.json index 70f5783..76a1fd4 100644 --- a/global.json +++ b/global.json @@ -1,6 +1,6 @@ { "sdk": { "version": "8.0.101", - "rollForward": "disable" + "rollForward": "disable" } } \ No newline at end of file diff --git a/src/Bit.TemplatePlayground.Iac/Bit.TemplatePlayground.Iac.csproj b/src/Bit.TemplatePlayground.Iac/Bit.TemplatePlayground.Iac.csproj index 9056238..ea5e208 100644 --- a/src/Bit.TemplatePlayground.Iac/Bit.TemplatePlayground.Iac.csproj +++ b/src/Bit.TemplatePlayground.Iac/Bit.TemplatePlayground.Iac.csproj @@ -6,7 +6,7 @@ - + diff --git a/src/Bit.TemplatePlayground.Server/Bit.TemplatePlayground.Server.csproj b/src/Bit.TemplatePlayground.Server/Bit.TemplatePlayground.Server.csproj index e800959..2455a7e 100644 --- a/src/Bit.TemplatePlayground.Server/Bit.TemplatePlayground.Server.csproj +++ b/src/Bit.TemplatePlayground.Server/Bit.TemplatePlayground.Server.csproj @@ -5,31 +5,37 @@ - - + all runtime; build; native; contentfiles; analyzers; buildtransitive - + all runtime; build; native; contentfiles; analyzers; buildtransitive - + + + + + + + + + + + - all runtime; build; native; contentfiles; analyzers; buildtransitive - - all runtime; build; native; contentfiles; analyzers; buildtransitive @@ -43,22 +49,22 @@ - - - - - - - - - - + + + + + + + Always + + + @@ -67,7 +73,6 @@ - True True @@ -79,17 +84,11 @@ - - - Always - - - linux-x64 True - 21D250BE-CAF7-44E4-8DAC-AFA502B0E2F2 + 65C62680-719C-4F56-9BF7-88F34AF733EF Linux ..\.. diff --git a/src/Bit.TemplatePlayground.Server/Controllers/Dashboard/DashboardController.cs b/src/Bit.TemplatePlayground.Server/Controllers/Dashboard/DashboardController.cs index 14204ad..4d2aec3 100644 --- a/src/Bit.TemplatePlayground.Server/Controllers/Dashboard/DashboardController.cs +++ b/src/Bit.TemplatePlayground.Server/Controllers/Dashboard/DashboardController.cs @@ -22,7 +22,7 @@ public async Task GetOverallAnalyticsStats } [HttpGet] - public async Task> GetProductsCountPerCategoryStats(CancellationToken cancellationToken) + public IQueryable GetProductsCountPerCategoryStats() { return DbContext.Categories .Select(c => new ProductsCountPerCategoryResponseDto() @@ -30,11 +30,11 @@ public async Task> GetProd CategoryName = c.Name, CategoryColor = c.Color, ProductCount = c.Products!.Count() - }).AsAsyncEnumerable(); + }); } [HttpGet] - public async Task> GetProductsSalesStats(CancellationToken cancellationToken) + public IQueryable GetProductsSalesStats() { Random rand = new Random(); return DbContext.Products.Include(p => p.Category) @@ -43,12 +43,12 @@ public async Task> GetProductsSales ProductName = p.Name, CategoryColor = p.Category!.Color, SaleAmount = rand.Next(1, 10) * p.Price - }).AsAsyncEnumerable(); + }); } [HttpGet] - public async Task GetProductsPercentagePerCategoryStats(CancellationToken cancellationToken) + public async Task> GetProductsPercentagePerCategoryStats(CancellationToken cancellationToken) { var productsTotalCount = await DbContext.Products.CountAsync(cancellationToken); @@ -63,6 +63,6 @@ public async Task GetProductsPercenta CategoryName = c!.Name, CategoryColor = c.Color, ProductPercentage = (float)decimal.Divide(c.Products!.Count(), productsTotalCount) * 100 - }).ToArrayAsync(cancellationToken); + }).ToListAsync(cancellationToken); } } diff --git a/src/Bit.TemplatePlayground.Server/Startup/Middlewares.cs b/src/Bit.TemplatePlayground.Server/Program.Middlewares.cs similarity index 95% rename from src/Bit.TemplatePlayground.Server/Startup/Middlewares.cs rename to src/Bit.TemplatePlayground.Server/Program.Middlewares.cs index 4b0d41d..783a48b 100644 --- a/src/Bit.TemplatePlayground.Server/Startup/Middlewares.cs +++ b/src/Bit.TemplatePlayground.Server/Program.Middlewares.cs @@ -3,21 +3,24 @@ using System.Runtime.Loader; using System.Web; using Bit.TemplatePlayground.Client.Core.Services; -using Bit.TemplatePlayground.Server.Components; -using HealthChecks.UI.Client; using Microsoft.AspNetCore.Components.Endpoints; -using Microsoft.AspNetCore.Diagnostics.HealthChecks; using Microsoft.AspNetCore.Http.Extensions; -namespace Bit.TemplatePlayground.Server.Startup; +using Microsoft.AspNetCore.Diagnostics.HealthChecks; +using HealthChecks.UI.Client; + +namespace Bit.TemplatePlayground.Server; -public class Middlewares +public static partial class Program { /// /// https://learn.microsoft.com/en-us/aspnet/core/fundamentals/middleware/?view=aspnetcore-8.0#middleware-order /// - public static void Use(WebApplication app, IHostEnvironment env, IConfiguration configuration) + public static void ConfiureMiddlewares(this WebApplication app) { + var configuration = app.Configuration; + var env = app.Environment; + app.UseForwardedHeaders(); if (AppRenderMode.MultilingualEnabled) @@ -71,6 +74,7 @@ public static void Use(WebApplication app, IHostEnvironment env, IConfiguration app.UseAntiforgery(); + app.UseSwagger(); app.UseSwaggerUI(options => @@ -84,6 +88,7 @@ public static void Use(WebApplication app, IHostEnvironment env, IConfiguration QueryStringParameter = queryStringParameter }).WithTags("Test"); + app.MapGet("/.well-known/apple-app-site-association", async () => { // https://branch.io/resources/aasa-validator/ @@ -92,6 +97,7 @@ public static void Use(WebApplication app, IHostEnvironment env, IConfiguration return Results.Stream(File.OpenRead(path), contentType, "apple-app-site-association"); }).ExcludeFromDescription(); + app.MapControllers().RequireAuthorization(); var appSettings = configuration.GetSection(nameof(AppSettings)).Get()!; @@ -113,8 +119,9 @@ public static void Use(WebApplication app, IHostEnvironment env, IConfiguration }); } + // Handle the rest of requests with blazor - var blazorApp = app.MapRazorComponents() + var blazorApp = app.MapRazorComponents() .AddInteractiveServerRenderMode() .AddInteractiveWebAssemblyRenderMode() .AddAdditionalAssemblies(AssemblyLoadContext.Default.Assemblies.Where(asm => asm.GetName().Name?.Contains("Bit.TemplatePlayground") is true).Except([Assembly.GetExecutingAssembly()]).ToArray()); diff --git a/src/Bit.TemplatePlayground.Server/Extensions/IServiceCollectionExtensions.cs b/src/Bit.TemplatePlayground.Server/Program.Services.cs similarity index 57% rename from src/Bit.TemplatePlayground.Server/Extensions/IServiceCollectionExtensions.cs rename to src/Bit.TemplatePlayground.Server/Program.Services.cs index eb292b0..597cccb 100644 --- a/src/Bit.TemplatePlayground.Server/Extensions/IServiceCollectionExtensions.cs +++ b/src/Bit.TemplatePlayground.Server/Program.Services.cs @@ -1,21 +1,133 @@ -using System.Security.Cryptography.X509Certificates; -using Bit.TemplatePlayground.Server; -using Bit.TemplatePlayground.Server.Models.Identity; +using System.IO.Compression; using Bit.TemplatePlayground.Server.Services; +using Bit.TemplatePlayground.Client.Web; +using Microsoft.AspNetCore.HttpOverrides; +using Microsoft.AspNetCore.ResponseCompression; +using System.Net.Mail; +using System.Net; +using System.Security.Cryptography.X509Certificates; +using Bit.TemplatePlayground.Server.Models.Identity; +using Microsoft.AspNetCore.Components.Web; using Microsoft.AspNetCore.DataProtection; +using Microsoft.AspNetCore.OData; using Microsoft.IdentityModel.Tokens; using Microsoft.OpenApi.Models; -using Swashbuckle.AspNetCore.SwaggerGen; -namespace Microsoft.Extensions.DependencyInjection; +namespace Bit.TemplatePlayground.Server; -public static class IServiceCollectionExtensions +public static partial class Program { - public static void AddBlazor(this IServiceCollection services, IConfiguration configuration) + private static void ConfigureServices(this WebApplicationBuilder builder) + { + // Services being registered here can get injected in server project only. + + var services = builder.Services; + var configuration = builder.Configuration; + var env = builder.Environment; + + services.AddExceptionHandler(); + + services.Configure(options => + { + options.ForwardedHeaders = ForwardedHeaders.All; + options.ForwardedHostHeaderName = "X-Host"; + }); + + services.AddResponseCaching(); + + services.AddHttpContextAccessor(); + + services.AddResponseCompression(opts => + { + opts.EnableForHttps = true; + opts.MimeTypes = ResponseCompressionDefaults.MimeTypes.Concat(["application/octet-stream"]).ToArray(); + opts.Providers.Add(); + opts.Providers.Add(); + }) + .Configure(opt => opt.Level = CompressionLevel.Fastest) + .Configure(opt => opt.Level = CompressionLevel.Fastest); + + + var appSettings = configuration.GetSection(nameof(AppSettings)).Get()!; + + services.AddCors(); + + services + .AddControllers() + .AddOData(options => options.EnableQueryFeatures()) + .AddDataAnnotationsLocalization(options => options.DataAnnotationLocalizerProvider = StringLocalizerProvider.ProvideLocalizer) + .ConfigureApiBehaviorOptions(options => + { + options.InvalidModelStateResponseFactory = context => + { + throw new ResourceValidationException(context.ModelState.Select(ms => (ms.Key, ms.Value!.Errors.Select(e => new LocalizedString(e.ErrorMessage, e.ErrorMessage)).ToArray())).ToArray()); + }; + }); + + services.AddDbContext(options => + { + options.UseSqlite(configuration.GetConnectionString("SqliteConnectionString"), dbOptions => + { + dbOptions.UseQuerySplittingBehavior(QuerySplittingBehavior.SplitQuery); + }); + }); + + services.Configure(configuration.GetSection(nameof(AppSettings))); + + services.TryAddTransient(sp => sp.GetRequiredService>().Value); + + services.AddEndpointsApiExplorer(); + + services.AddSwaggerGen(); + + AddIdentity(builder); + + AddHealthChecks(builder); + + services.TryAddTransient(); + + var fluentEmailServiceBuilder = services.AddFluentEmail(appSettings.EmailSettings.DefaultFromEmail, appSettings.EmailSettings.DefaultFromName); + + if (appSettings.EmailSettings.UseLocalFolderForEmails) + { + var sentEmailsFolderPath = Path.Combine(AppContext.BaseDirectory, "sent-emails"); + + Directory.CreateDirectory(sentEmailsFolderPath); + + fluentEmailServiceBuilder.AddSmtpSender(() => new SmtpClient + { + DeliveryMethod = SmtpDeliveryMethod.SpecifiedPickupDirectory, + PickupDirectoryLocation = sentEmailsFolderPath + }); + } + else + { + if (appSettings.EmailSettings.HasCredential) + { + fluentEmailServiceBuilder.AddSmtpSender(() => new(appSettings.EmailSettings.Host, appSettings.EmailSettings.Port) + { + Credentials = new NetworkCredential(appSettings.EmailSettings.UserName, appSettings.EmailSettings.Password), + EnableSsl = true + }); + } + else + { + fluentEmailServiceBuilder.AddSmtpSender(appSettings.EmailSettings.Host, appSettings.EmailSettings.Port); + } + } + + AddBlazor(builder); + + } + + private static void AddBlazor(WebApplicationBuilder builder) { - services.AddTransient(); + var services = builder.Services; + var configuration = builder.Configuration; - services.AddTransient(sp => + services.TryAddTransient(); + + services.TryAddTransient(sp => { Uri.TryCreate(configuration.GetApiServerAddress(), UriKind.RelativeOrAbsolute, out var apiServerAddress); @@ -36,11 +148,14 @@ public static void AddBlazor(this IServiceCollection services, IConfiguration co services.AddMvc(); - services.AddClientWebServices(); + services.AddClientWebProjectServices(); } - public static void AddIdentity(this IServiceCollection services, IConfiguration configuration, IWebHostEnvironment hostEnv) + private static void AddIdentity(WebApplicationBuilder builder) { + var services = builder.Services; + var configuration = builder.Configuration; + var env = builder.Environment; var appSettings = configuration.GetSection(nameof(AppSettings)).Get()!; var settings = appSettings.IdentitySettings; @@ -48,7 +163,7 @@ public static void AddIdentity(this IServiceCollection services, IConfiguration var certificate = new X509Certificate2(certificatePath, appSettings.IdentitySettings.IdentityCertificatePassword, OperatingSystem.IsWindows() ? X509KeyStorageFlags.EphemeralKeySet : X509KeyStorageFlags.DefaultKeySet); bool isTestCertificate = certificate.Thumbprint is "55140A8C935AB5202949071E5781E6946CD60606"; // The default test certificate is still in use - if (isTestCertificate && hostEnv.IsDevelopment() is false) + if (isTestCertificate && env.IsDevelopment() is false) { throw new InvalidOperationException(@"The default test certificate is still in use. Please replace it with a new one by running the 'dotnet dev-certs https --export-path IdentityCertificate.pfx --password P@ssw0rdP@ssw0rd' command (or your preferred method for generating PFX files) in the server project's folder."); } @@ -118,8 +233,10 @@ public static void AddIdentity(this IServiceCollection services, IConfiguration services.AddAuthorization(); } - public static void AddSwaggerGen(this IServiceCollection services) + private static void AddSwaggerGen(WebApplicationBuilder builder) { + var services = builder.Services; + services.AddSwaggerGen(options => { options.IncludeXmlComments(Path.Combine(AppContext.BaseDirectory, "Bit.TemplatePlayground.Server.xml")); @@ -155,14 +272,18 @@ public static void AddSwaggerGen(this IServiceCollection services) }); } - public static IServiceCollection AddHealthChecks(this IServiceCollection services, IWebHostEnvironment env, IConfiguration configuration) + private static void AddHealthChecks(WebApplicationBuilder builder) { + var configuration = builder.Configuration; + var services = builder.Services; + var env = builder.Environment; + var appSettings = configuration.GetSection(nameof(AppSettings)).Get()!; var healthCheckSettings = appSettings.HealthCheckSettings; if (healthCheckSettings.EnableHealthChecks is false) - return services; + return; services.AddHealthChecksUI(setupSettings: setup => { @@ -191,7 +312,6 @@ public static IServiceCollection AddHealthChecks(this IServiceCollection service } }); } - - return services; } + } diff --git a/src/Bit.TemplatePlayground.Server/Program.cs b/src/Bit.TemplatePlayground.Server/Program.cs index 5d7d173..5db1590 100644 --- a/src/Bit.TemplatePlayground.Server/Program.cs +++ b/src/Bit.TemplatePlayground.Server/Program.cs @@ -1,17 +1,26 @@ -var builder = WebApplication.CreateBuilder(args); + +namespace Bit.TemplatePlayground.Server; -builder.Configuration.AddClientConfigurations(); - -// The following line (using the * in the URL), allows the emulators and mobile devices to access the app using the host IP address. -if (BuildConfiguration.IsDebug() && OperatingSystem.IsWindows()) +public static partial class Program { - builder.WebHost.UseUrls("http://localhost:5030", "http://*:5030"); -} + public static async Task Main(string[] args) + { + var builder = WebApplication.CreateBuilder(args); + + builder.Configuration.AddClientConfigurations(); -Bit.TemplatePlayground.Server.Startup.Services.Add(builder.Services, builder.Environment, builder.Configuration); + // The following line (using the * in the URL), allows the emulators and mobile devices to access the app using the host IP address. + if (BuildConfiguration.IsDebug() && OperatingSystem.IsWindows()) + { + builder.WebHost.UseUrls("http://localhost:5030", "http://*:5030"); + } -var app = builder.Build(); + builder.ConfigureServices(); -Bit.TemplatePlayground.Server.Startup.Middlewares.Use(app, builder.Environment, builder.Configuration); + var app = builder.Build(); -await app.RunAsync(); + app.ConfiureMiddlewares(); + + await app.RunAsync(); + } +} diff --git a/src/Bit.TemplatePlayground.Server/Properties/launchSettings.json b/src/Bit.TemplatePlayground.Server/Properties/launchSettings.json index 4a6e991..a4b5d1d 100644 --- a/src/Bit.TemplatePlayground.Server/Properties/launchSettings.json +++ b/src/Bit.TemplatePlayground.Server/Properties/launchSettings.json @@ -1,9 +1,8 @@ { "profiles": { - "Bit.TemplatePlayground.Server-Swagger": { + "Bit.TemplatePlayground.Server": { "commandName": "Project", "launchBrowser": true, - "launchUrl": "swagger", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" }, diff --git a/src/Bit.TemplatePlayground.Server/Extensions/ODataOperationFilter.cs b/src/Bit.TemplatePlayground.Server/Services/ODataOperationFilter.cs similarity index 93% rename from src/Bit.TemplatePlayground.Server/Extensions/ODataOperationFilter.cs rename to src/Bit.TemplatePlayground.Server/Services/ODataOperationFilter.cs index 3d2ba9b..d0b353c 100644 --- a/src/Bit.TemplatePlayground.Server/Extensions/ODataOperationFilter.cs +++ b/src/Bit.TemplatePlayground.Server/Services/ODataOperationFilter.cs @@ -1,7 +1,8 @@ using Microsoft.AspNetCore.Mvc.Controllers; using Microsoft.OpenApi.Models; +using Swashbuckle.AspNetCore.SwaggerGen; -namespace Swashbuckle.AspNetCore.SwaggerGen; +namespace Bit.TemplatePlayground.Server.Services; /// /// https://docs.microsoft.com/en-us/odata/concepts/queryoptions-overview @@ -10,11 +11,9 @@ public class ODataOperationFilter : IOperationFilter { public void Apply(OpenApiOperation operation, OperationFilterContext context) { - if (operation.Parameters == null) operation.Parameters = new List(); + if (operation.Parameters is null) operation.Parameters = []; - var descriptor = context.ApiDescription.ActionDescriptor as ControllerActionDescriptor; - - if (descriptor is null) + if (context.ApiDescription.ActionDescriptor is not ControllerActionDescriptor descriptor) return; var odataQueryOptionsParameter = descriptor!.Parameters.SingleOrDefault(p => typeof(ODataQueryOptions).IsAssignableFrom(p.ParameterType)); diff --git a/src/Bit.TemplatePlayground.Server/Startup/Services.cs b/src/Bit.TemplatePlayground.Server/Startup/Services.cs deleted file mode 100644 index 23ccb83..0000000 --- a/src/Bit.TemplatePlayground.Server/Startup/Services.cs +++ /dev/null @@ -1,110 +0,0 @@ -using System.IO.Compression; -using System.Net; -using System.Net.Mail; -using Bit.TemplatePlayground.Server.Services; -using Microsoft.AspNetCore.Components.Web; -using Microsoft.AspNetCore.HttpOverrides; -using Microsoft.AspNetCore.OData; -using Microsoft.AspNetCore.ResponseCompression; - -namespace Bit.TemplatePlayground.Server.Startup; - -public static class Services -{ - public static void Add(IServiceCollection services, IWebHostEnvironment env, IConfiguration configuration) - { - // Services being registered here can get injected into controllers and services in Server project. - - var appSettings = configuration.GetSection(nameof(AppSettings)).Get()!; - - services.AddExceptionHandler(); - - services.AddBlazor(configuration); - - - services.AddCors(); - - services - .AddControllers() - .AddOData(options => options.EnableQueryFeatures()) - .AddDataAnnotationsLocalization(options => options.DataAnnotationLocalizerProvider = StringLocalizerProvider.ProvideLocalizer) - .ConfigureApiBehaviorOptions(options => - { - options.InvalidModelStateResponseFactory = context => - { - throw new ResourceValidationException(context.ModelState.Select(ms => (ms.Key, ms.Value!.Errors.Select(e => new LocalizedString(e.ErrorMessage, e.ErrorMessage)).ToArray())).ToArray()); - }; - }); - - services.Configure(options => - { - options.ForwardedHeaders = ForwardedHeaders.All; - options.ForwardedHostHeaderName = "X-Host"; - }); - - services.AddResponseCaching(); - - services.AddHttpContextAccessor(); - - services.AddResponseCompression(opts => - { - opts.EnableForHttps = true; - opts.MimeTypes = ResponseCompressionDefaults.MimeTypes.Concat(["application/octet-stream"]).ToArray(); - opts.Providers.Add(); - opts.Providers.Add(); - }) - .Configure(opt => opt.Level = CompressionLevel.Fastest) - .Configure(opt => opt.Level = CompressionLevel.Fastest); - - services.AddDbContext(options => - { - options.UseSqlite(configuration.GetConnectionString("SqliteConnectionString"), dbOptions => - { - dbOptions.UseQuerySplittingBehavior(QuerySplittingBehavior.SplitQuery); - }); - }); - - services.Configure(configuration.GetSection(nameof(AppSettings))); - - services.AddTransient(sp => sp.GetRequiredService>().Value); - - services.AddEndpointsApiExplorer(); - - services.AddSwaggerGen(); - - services.AddIdentity(configuration, env); - - services.AddHealthChecks(env, configuration); - - services.AddTransient(); - - var fluentEmailServiceBuilder = services.AddFluentEmail(appSettings.EmailSettings.DefaultFromEmail, appSettings.EmailSettings.DefaultFromName); - - if (appSettings.EmailSettings.UseLocalFolderForEmails) - { - var sentEmailsFolderPath = Path.Combine(AppContext.BaseDirectory, "sent-emails"); - - Directory.CreateDirectory(sentEmailsFolderPath); - - fluentEmailServiceBuilder.AddSmtpSender(() => new SmtpClient - { - DeliveryMethod = SmtpDeliveryMethod.SpecifiedPickupDirectory, - PickupDirectoryLocation = sentEmailsFolderPath - }); - } - else - { - if (appSettings.EmailSettings.HasCredential) - { - fluentEmailServiceBuilder.AddSmtpSender(() => new(appSettings.EmailSettings.Host, appSettings.EmailSettings.Port) - { - Credentials = new NetworkCredential(appSettings.EmailSettings.UserName, appSettings.EmailSettings.Password) - }); - } - else - { - fluentEmailServiceBuilder.AddSmtpSender(appSettings.EmailSettings.Host, appSettings.EmailSettings.Port); - } - } - } -} diff --git a/src/Bit.TemplatePlayground.Server/appsettings.json b/src/Bit.TemplatePlayground.Server/appsettings.json index 769cad5..917544c 100644 --- a/src/Bit.TemplatePlayground.Server/appsettings.json +++ b/src/Bit.TemplatePlayground.Server/appsettings.json @@ -4,11 +4,11 @@ }, "AppSettings": { "IdentitySettings": { + "IdentityCertificatePassword": "P@ssw0rdP@ssw0rd", // It can also be configured using: dotnet user-secrets set "AppSettings:IdentitySettings:IdentityCertificatePassword" "P@ssw0rdP@ssw0rd" "Issuer": "Bit.TemplatePlayground", "Audience": "Bit.TemplatePlayground", - "IdentityCertificatePassword": "P@ssw0rdP@ssw0rd", // It can also be configured using: dotnet user-secrets set "AppSettings:IdentitySettings:IdentityCertificatePassword" "P@ssw0rdP@ssw0rd" - "BearerTokenExpiration": "0.01:00:00", // Format: D.HH:mm:ss "RefreshTokenExpiration": "14.00:00:00", // Format: D.HH:mm:ss + "BearerTokenExpiration": "0.01:00:00", // Used as jwt's expiration claim, access token's expires in and cookie's max age. Format: D.HH:mm:ss "PasswordRequireDigit": "false", "PasswordRequiredLength": "6", "PasswordRequireNonAlphanumeric": "false", @@ -20,7 +20,7 @@ }, "EmailSettings": { "Host": "LocalFolder", // Local folder means storing emails as .eml file in bin/Debug/net8.0/sent-emails folder (Recommended for testing purposes only) instead of sending them using smtp server. - "Port": "25", + "Port": "587", "DefaultFromEmail": "info@Bit.TemplatePlayground.com", "DefaultFromName": "Bit.TemplatePlayground", "UserName": null, diff --git a/src/Bit.TemplatePlayground.Shared/Bit.TemplatePlayground.Shared.csproj b/src/Bit.TemplatePlayground.Shared/Bit.TemplatePlayground.Shared.csproj index fe18f8a..d0975a6 100644 --- a/src/Bit.TemplatePlayground.Shared/Bit.TemplatePlayground.Shared.csproj +++ b/src/Bit.TemplatePlayground.Shared/Bit.TemplatePlayground.Shared.csproj @@ -5,11 +5,11 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/src/Bit.TemplatePlayground.Shared/Dtos/AppJsonContext.cs b/src/Bit.TemplatePlayground.Shared/Dtos/AppJsonContext.cs index 3173ccc..7f12b1f 100644 --- a/src/Bit.TemplatePlayground.Shared/Dtos/AppJsonContext.cs +++ b/src/Bit.TemplatePlayground.Shared/Dtos/AppJsonContext.cs @@ -11,14 +11,16 @@ namespace Bit.TemplatePlayground.Shared.Dtos; [JsonSourceGenerationOptions(PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase)] [JsonSerializable(typeof(Dictionary))] [JsonSerializable(typeof(UserDto))] -[JsonSerializable(typeof(ProductsCountPerCategoryResponseDto))] +[JsonSerializable(typeof(List))] [JsonSerializable(typeof(OverallAnalyticsStatsDataResponseDto))] -[JsonSerializable(typeof(ProductSaleStatResponseDto))] -[JsonSerializable(typeof(ProductPercentagePerCategoryResponseDto))] +[JsonSerializable(typeof(List))] +[JsonSerializable(typeof(List))] [JsonSerializable(typeof(ProductDto))] [JsonSerializable(typeof(PagedResult))] +[JsonSerializable(typeof(List))] [JsonSerializable(typeof(CategoryDto))] [JsonSerializable(typeof(PagedResult))] +[JsonSerializable(typeof(List))] [JsonSerializable(typeof(SignInRequestDto))] [JsonSerializable(typeof(TokenResponseDto))] [JsonSerializable(typeof(RefreshRequestDto))] diff --git a/src/Bit.TemplatePlayground.Shared/Extensions/IServiceCollectionExtensions.cs b/src/Bit.TemplatePlayground.Shared/Extensions/IServiceCollectionExtensions.cs index e633ab7..73d559d 100644 --- a/src/Bit.TemplatePlayground.Shared/Extensions/IServiceCollectionExtensions.cs +++ b/src/Bit.TemplatePlayground.Shared/Extensions/IServiceCollectionExtensions.cs @@ -2,7 +2,7 @@ public static class IServiceCollectionExtensions { - public static IServiceCollection AddSharedServices(this IServiceCollection services) + public static IServiceCollection AddSharedProjectServices(this IServiceCollection services) { // Services being registered here can get injected everywhere (Api, Web, Android, iOS, Windows and macOS) @@ -10,8 +10,9 @@ public static IServiceCollection AddSharedServices(this IServiceCollection servi services.TryAddTransient(); // Define authorization policies here to seamlessly integrate them across various components, - // including web api actions and razor pages using Authorize attribute, AuthorizeView in razor pages, and programmatically in C# for enhanced security and access control. - services.AddAuthorizationCore(options => options.AddPolicy("AdminOnly", authPolicyBuilder => authPolicyBuilder.RequireRole("Admin"))); + // including web api actions and razor pages using Authorize attribute, AuthorizeView in razor pages, + // and programmatically in C# by injecting IAuthorizationService for enhanced security and access control. + services.AddAuthorizationCore(options => options.AddPolicy("AdminsOnly", authPolicyBuilder => authPolicyBuilder.RequireRole("Admin"))); services.AddLocalization(); diff --git a/src/Bit.TemplatePlayground.Shared/Extensions/JsonSeralizerExtensions.cs b/src/Bit.TemplatePlayground.Shared/Extensions/JsonSeralizerExtensions.cs index c41706a..0562dda 100644 --- a/src/Bit.TemplatePlayground.Shared/Extensions/JsonSeralizerExtensions.cs +++ b/src/Bit.TemplatePlayground.Shared/Extensions/JsonSeralizerExtensions.cs @@ -14,6 +14,16 @@ public static JsonTypeInfo GetTypeInfo(this JsonSerializerOptions options) return (JsonTypeInfo)result; } - return JsonTypeInfo.CreateJsonTypeInfo(options); + throw new InvalidOperationException($"Add [JsonSerializable(typeof({GetTypeDisplayName(typeof(T))}))] to the {nameof(AppJsonContext)}"); + } + + private static string GetTypeDisplayName(Type type) + { + if (type.IsGenericType) + { + return $"{type.Name.Split('`')[0]}<{string.Join(", ", type.GetGenericArguments().Select(GetTypeDisplayName))}>"; + } + + return type.Name; } } diff --git a/src/Client/Bit.TemplatePlayground.Client.Core/Bit.TemplatePlayground.Client.Core.csproj b/src/Client/Bit.TemplatePlayground.Client.Core/Bit.TemplatePlayground.Client.Core.csproj index dc67710..2ffd4ff 100644 --- a/src/Client/Bit.TemplatePlayground.Client.Core/Bit.TemplatePlayground.Client.Core.csproj +++ b/src/Client/Bit.TemplatePlayground.Client.Core/Bit.TemplatePlayground.Client.Core.csproj @@ -16,16 +16,16 @@ - - - - - - + + + + + + all runtime; build; native; contentfiles; analyzers; buildtransitive - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/src/Client/Bit.TemplatePlayground.Client.Core/Components/Pages/Dashboard/ProductsCountPerCategoryWidget.razor.cs b/src/Client/Bit.TemplatePlayground.Client.Core/Components/Pages/Dashboard/ProductsCountPerCategoryWidget.razor.cs index 2342f66..ca344a6 100644 --- a/src/Client/Bit.TemplatePlayground.Client.Core/Components/Pages/Dashboard/ProductsCountPerCategoryWidget.razor.cs +++ b/src/Client/Bit.TemplatePlayground.Client.Core/Components/Pages/Dashboard/ProductsCountPerCategoryWidget.razor.cs @@ -33,7 +33,7 @@ private async Task GetData() { isLoading = true; - var data = await (await dashboardController.GetProductsCountPerCategoryStats(CurrentCancellationToken)).ToListAsync(CurrentCancellationToken); + var data = await dashboardController.GetProductsCountPerCategoryStats(CurrentCancellationToken); BitChartBarDataset chartDataSet = [.. data.Select(d => d.ProductCount)]; chartDataSet.BackgroundColor = data.Select(d => d.CategoryColor ?? string.Empty).ToArray(); diff --git a/src/Client/Bit.TemplatePlayground.Client.Core/Components/Pages/Dashboard/ProductsSalesWidget.razor.cs b/src/Client/Bit.TemplatePlayground.Client.Core/Components/Pages/Dashboard/ProductsSalesWidget.razor.cs index db346c1..cb84457 100644 --- a/src/Client/Bit.TemplatePlayground.Client.Core/Components/Pages/Dashboard/ProductsSalesWidget.razor.cs +++ b/src/Client/Bit.TemplatePlayground.Client.Core/Components/Pages/Dashboard/ProductsSalesWidget.razor.cs @@ -33,7 +33,7 @@ private async Task GetData() { isLoading = true; - var data = await (await dashboardController.GetProductsSalesStats(CurrentCancellationToken)).ToListAsync(CurrentCancellationToken); + var data = await dashboardController.GetProductsSalesStats(CurrentCancellationToken); BitChartBarDataset chartDataSet = [.. data.Select(d => d.SaleAmount)]; chartDataSet.BackgroundColor = data.Select(d => d.CategoryColor ?? string.Empty).ToArray(); diff --git a/src/Client/Bit.TemplatePlayground.Client.Core/Components/Pages/Products/AddOrEditProductModal.razor.cs b/src/Client/Bit.TemplatePlayground.Client.Core/Components/Pages/Products/AddOrEditProductModal.razor.cs index 5e231e2..e8753e5 100644 --- a/src/Client/Bit.TemplatePlayground.Client.Core/Components/Pages/Products/AddOrEditProductModal.razor.cs +++ b/src/Client/Bit.TemplatePlayground.Client.Core/Components/Pages/Products/AddOrEditProductModal.razor.cs @@ -41,7 +41,7 @@ private async Task LoadAllCategoriesAsync() try { - var categoryList = await (await categoryController.Get(CurrentCancellationToken)).ToListAsync(CurrentCancellationToken); + var categoryList = await categoryController.Get(CurrentCancellationToken); allCategoryList = categoryList.Select(c => new BitDropdownItem() { diff --git a/src/Client/Bit.TemplatePlayground.Client.Core/Controllers/Categories/ICategoryController.cs b/src/Client/Bit.TemplatePlayground.Client.Core/Controllers/Categories/ICategoryController.cs index fadf4f1..bb49209 100644 --- a/src/Client/Bit.TemplatePlayground.Client.Core/Controllers/Categories/ICategoryController.cs +++ b/src/Client/Bit.TemplatePlayground.Client.Core/Controllers/Categories/ICategoryController.cs @@ -21,5 +21,5 @@ public interface ICategoryController : IAppController Task> GetCategories(CancellationToken cancellationToken = default) => default!; [HttpGet] - Task> Get(CancellationToken cancellationToken) => default!; + Task> Get(CancellationToken cancellationToken) => default!; } diff --git a/src/Client/Bit.TemplatePlayground.Client.Core/Controllers/Dashboard/IDashboardController.cs b/src/Client/Bit.TemplatePlayground.Client.Core/Controllers/Dashboard/IDashboardController.cs index a812d19..c21b9c8 100644 --- a/src/Client/Bit.TemplatePlayground.Client.Core/Controllers/Dashboard/IDashboardController.cs +++ b/src/Client/Bit.TemplatePlayground.Client.Core/Controllers/Dashboard/IDashboardController.cs @@ -9,11 +9,11 @@ public interface IDashboardController : IAppController Task GetOverallAnalyticsStatsData(CancellationToken cancellationToken = default); [HttpGet] - Task> GetProductsCountPerCategoryStats(CancellationToken cancellationToken = default); + Task> GetProductsCountPerCategoryStats(CancellationToken cancellationToken = default) => default!; [HttpGet] - Task> GetProductsSalesStats(CancellationToken cancellationToken = default); + Task> GetProductsSalesStats(CancellationToken cancellationToken = default) => default!; [HttpGet] - Task GetProductsPercentagePerCategoryStats(CancellationToken cancellationToken = default); + Task> GetProductsPercentagePerCategoryStats(CancellationToken cancellationToken = default); } diff --git a/src/Client/Bit.TemplatePlayground.Client.Core/Controllers/Products/IProductController.cs b/src/Client/Bit.TemplatePlayground.Client.Core/Controllers/Products/IProductController.cs index 63e214a..dbe9d55 100644 --- a/src/Client/Bit.TemplatePlayground.Client.Core/Controllers/Products/IProductController.cs +++ b/src/Client/Bit.TemplatePlayground.Client.Core/Controllers/Products/IProductController.cs @@ -19,4 +19,7 @@ public interface IProductController : IAppController [HttpGet] Task> GetProducts(CancellationToken cancellationToken = default) => default!; + + [HttpGet] + Task> Get(CancellationToken cancellationToken) => default!; } diff --git a/src/Client/Bit.TemplatePlayground.Client.Core/Extensions/IServiceCollectionExtensions.cs b/src/Client/Bit.TemplatePlayground.Client.Core/Extensions/IServiceCollectionExtensions.cs index 5ae1331..35e4a33 100644 --- a/src/Client/Bit.TemplatePlayground.Client.Core/Extensions/IServiceCollectionExtensions.cs +++ b/src/Client/Bit.TemplatePlayground.Client.Core/Extensions/IServiceCollectionExtensions.cs @@ -5,9 +5,9 @@ namespace Microsoft.Extensions.DependencyInjection; public static class IServiceCollectionExtensions { - public static IServiceCollection AddClientSharedServices(this IServiceCollection services) + public static IServiceCollection AddClientCoreProjectServices(this IServiceCollection services) { - // Services registered in this class can be injected in client side (Web, Android, iOS, Windows, macOS) + // Services being registered here can get injected in client side (Web, Android, iOS, Windows, macOS) and server side (during pre rendering) services.TryAddTransient(); @@ -21,8 +21,8 @@ public static IServiceCollection AddClientSharedServices(this IServiceCollection services.TryAddTransient(); services.TryAddTransient(); - services.AddScoped(); - services.AddScoped(sp => (AuthenticationManager)sp.GetRequiredService()); + services.AddScoped(); // Use 'Add' instead of 'TryAdd' to override the aspnetcore's default AuthenticationStateProvider. + services.TryAddScoped(sp => (AuthenticationManager)sp.GetRequiredService()); services.TryAddTransient(); services.TryAddTransient(); @@ -32,8 +32,9 @@ public static IServiceCollection AddClientSharedServices(this IServiceCollection services.AddBitButilServices(); services.AddBitBlazorUIServices(); - services.AddSharedServices(); + + services.AddSharedProjectServices(); return services; } diff --git a/src/Client/Bit.TemplatePlayground.Client.Maui/Bit.TemplatePlayground.Client.Maui.csproj b/src/Client/Bit.TemplatePlayground.Client.Maui/Bit.TemplatePlayground.Client.Maui.csproj index f97de7d..edd9f7d 100644 --- a/src/Client/Bit.TemplatePlayground.Client.Maui/Bit.TemplatePlayground.Client.Maui.csproj +++ b/src/Client/Bit.TemplatePlayground.Client.Maui/Bit.TemplatePlayground.Client.Maui.csproj @@ -16,7 +16,7 @@ com.bitplatform.BP.Template - EA281D77-3592-4C09-9567-B1E5E79754D7 + 44861962-8347-470D-AC06-050D17A7F686 1.0 @@ -82,11 +82,11 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/src/Client/Bit.TemplatePlayground.Client.Maui/Extensions/IServiceCollectionExtensions.cs b/src/Client/Bit.TemplatePlayground.Client.Maui/Extensions/IServiceCollectionExtensions.cs deleted file mode 100644 index 1a47a95..0000000 --- a/src/Client/Bit.TemplatePlayground.Client.Maui/Extensions/IServiceCollectionExtensions.cs +++ /dev/null @@ -1,31 +0,0 @@ -using Bit.TemplatePlayground.Client.Maui; -using Bit.TemplatePlayground.Client.Maui.Services; - -namespace Microsoft.Extensions.DependencyInjection; - -public static class IServiceCollectionExtensions -{ - public static IServiceCollection AddClientMauiServices(this IServiceCollection services) - { - // Services registered in this class can be injected in Android, iOS, Windows, and macOS. - - services.TryAddTransient(); - services.TryAddTransient(); - services.TryAddSingleton(); - services.TryAddTransient(); - -#if ANDROID - services.AddClientAndroidServices(); -#elif iOS - services.AddClientiOSServices(); -#elif Mac - services.AddClientMacServices(); -#elif Windows - services.AddClientWindowsServices(); -#endif - - services.AddClientSharedServices(); - - return services; - } -} diff --git a/src/Client/Bit.TemplatePlayground.Client.Maui/MauiProgram.Services.cs b/src/Client/Bit.TemplatePlayground.Client.Maui/MauiProgram.Services.cs new file mode 100644 index 0000000..5a611c5 --- /dev/null +++ b/src/Client/Bit.TemplatePlayground.Client.Maui/MauiProgram.Services.cs @@ -0,0 +1,50 @@ +using Bit.TemplatePlayground.Client.Maui.Services; + +namespace Bit.TemplatePlayground.Client.Maui; + +public static partial class MauiProgram +{ + public static void ConfigureServices(this MauiAppBuilder builder) + { + // Services being registered here can get injected in Maui (Android, iOS, macOS, Windows) + + var services = builder.Services; + var configuration = builder.Configuration; + + services.AddMauiBlazorWebView(); + + if (BuildConfiguration.IsDebug()) + { + services.AddBlazorWebViewDeveloperTools(); + } + + Uri.TryCreate(configuration.GetApiServerAddress(), UriKind.Absolute, out var apiServerAddress); + + services.TryAddTransient(sp => + { + var handler = sp.GetRequiredKeyedService("DefaultMessageHandler"); + HttpClient httpClient = new(handler) + { + BaseAddress = apiServerAddress + }; + return httpClient; + }); + + services.TryAddTransient(); + services.TryAddTransient(); + services.TryAddSingleton(); + services.TryAddTransient(); + +#if ANDROID + services.AddClientMauiProjectAndroidServices(); +#elif iOS + services.AddClientMauiProjectIosServices(); +#elif Mac + services.AddClientMauiProjectMacCatalystServices(); +#elif Windows + services.AddClientMauiProjectWindowsServices(); +#endif + + services.AddClientCoreProjectServices(); + } +} diff --git a/src/Client/Bit.TemplatePlayground.Client.Maui/MauiProgram.cs b/src/Client/Bit.TemplatePlayground.Client.Maui/MauiProgram.cs index ee8eb2a..d3f244a 100644 --- a/src/Client/Bit.TemplatePlayground.Client.Maui/MauiProgram.cs +++ b/src/Client/Bit.TemplatePlayground.Client.Maui/MauiProgram.cs @@ -3,7 +3,7 @@ namespace Bit.TemplatePlayground.Client.Maui; -public static class MauiProgram +public static partial class MauiProgram { public static MauiApp CreateMauiApp() { @@ -15,28 +15,7 @@ public static MauiApp CreateMauiApp() .UseMauiApp() .Configuration.AddClientConfigurations(); - var services = builder.Services; - - services.AddMauiBlazorWebView(); - - if (BuildConfiguration.IsDebug()) - { - services.AddBlazorWebViewDeveloperTools(); - } - - Uri.TryCreate(builder.Configuration.GetApiServerAddress(), UriKind.Absolute, out var apiServerAddress); - - services.AddTransient(sp => - { - var handler = sp.GetRequiredKeyedService("DefaultMessageHandler"); - HttpClient httpClient = new(handler) - { - BaseAddress = apiServerAddress - }; - return httpClient; - }); - - services.AddClientMauiServices(); + builder.ConfigureServices(); builder.ConfigureLifecycleEvents(lifecycle => { diff --git a/src/Client/Bit.TemplatePlayground.Client.Maui/Platforms/Android/Extensions/IServiceCollectionExtensions.cs b/src/Client/Bit.TemplatePlayground.Client.Maui/Platforms/Android/Extensions/IServiceCollectionExtensions.cs index c669f4f..1a06246 100644 --- a/src/Client/Bit.TemplatePlayground.Client.Maui/Platforms/Android/Extensions/IServiceCollectionExtensions.cs +++ b/src/Client/Bit.TemplatePlayground.Client.Maui/Platforms/Android/Extensions/IServiceCollectionExtensions.cs @@ -1,10 +1,10 @@ namespace Microsoft.Extensions.DependencyInjection; -public static class IAndroidServiceCollectionExtensions +public static partial class IServiceCollectionExtensions { - public static IServiceCollection AddClientAndroidServices(this IServiceCollection services) + public static IServiceCollection AddClientMauiProjectAndroidServices(this IServiceCollection services) { - // Services registered in this class can be injected in Android. + // Services being registered here can get injected in Maui/Android. return services; } diff --git a/src/Client/Bit.TemplatePlayground.Client.Maui/Platforms/MacCatalyst/Extensions/IServiceCollectionExtensions.cs b/src/Client/Bit.TemplatePlayground.Client.Maui/Platforms/MacCatalyst/Extensions/IServiceCollectionExtensions.cs index 0cc5d4f..fc1bd2f 100644 --- a/src/Client/Bit.TemplatePlayground.Client.Maui/Platforms/MacCatalyst/Extensions/IServiceCollectionExtensions.cs +++ b/src/Client/Bit.TemplatePlayground.Client.Maui/Platforms/MacCatalyst/Extensions/IServiceCollectionExtensions.cs @@ -1,10 +1,10 @@ namespace Microsoft.Extensions.DependencyInjection; -public static class IMacServiceCollectionExtensions +public static partial class IServiceCollectionExtensions { - public static IServiceCollection AddClientMacServices(this IServiceCollection services) + public static IServiceCollection AddClientMauiProjectMacCatalystServices(this IServiceCollection services) { - // Services registered in this class can be injected in macOS. + // Services being registered here can get injected in Maui/macOS. return services; } diff --git a/src/Client/Bit.TemplatePlayground.Client.Maui/Platforms/Windows/Extensions/IServiceCollectionExtensions.cs b/src/Client/Bit.TemplatePlayground.Client.Maui/Platforms/Windows/Extensions/IServiceCollectionExtensions.cs index 27419a2..6c9c355 100644 --- a/src/Client/Bit.TemplatePlayground.Client.Maui/Platforms/Windows/Extensions/IServiceCollectionExtensions.cs +++ b/src/Client/Bit.TemplatePlayground.Client.Maui/Platforms/Windows/Extensions/IServiceCollectionExtensions.cs @@ -1,10 +1,10 @@ namespace Microsoft.Extensions.DependencyInjection; -public static class IWindowsServiceCollectionExtensions +public static partial class IServiceCollectionExtensions { - public static IServiceCollection AddClientWindowsServices(this IServiceCollection services) + public static IServiceCollection AddClientMauiProjectWindowsServices(this IServiceCollection services) { - // Services registered in this class can be injected in Windows. + // Services being registered here can get injected in Maui/windows. return services; } diff --git a/src/Client/Bit.TemplatePlayground.Client.Maui/Platforms/iOS/Extensions/IServiceCollectionExtensions.cs b/src/Client/Bit.TemplatePlayground.Client.Maui/Platforms/iOS/Extensions/IServiceCollectionExtensions.cs index 1688624..6be4e37 100644 --- a/src/Client/Bit.TemplatePlayground.Client.Maui/Platforms/iOS/Extensions/IServiceCollectionExtensions.cs +++ b/src/Client/Bit.TemplatePlayground.Client.Maui/Platforms/iOS/Extensions/IServiceCollectionExtensions.cs @@ -1,8 +1,8 @@ namespace Microsoft.Extensions.DependencyInjection; -public static class IiOSServiceCollectionExtensions +public static partial class IServiceCollectionExtensions { - public static IServiceCollection AddClientiOSServices(this IServiceCollection services) + public static IServiceCollection AddClientMauiProjectIosServices(this IServiceCollection services) { // Services registered in this class can be injected in iOS. diff --git a/src/Client/Bit.TemplatePlayground.Client.Web/Bit.TemplatePlayground.Client.Web.csproj b/src/Client/Bit.TemplatePlayground.Client.Web/Bit.TemplatePlayground.Client.Web.csproj index 88dc2aa..8ae4833 100644 --- a/src/Client/Bit.TemplatePlayground.Client.Web/Bit.TemplatePlayground.Client.Web.csproj +++ b/src/Client/Bit.TemplatePlayground.Client.Web/Bit.TemplatePlayground.Client.Web.csproj @@ -15,6 +15,7 @@ true Default true + true @@ -29,15 +30,16 @@ - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive - + all runtime; build; native; contentfiles; analyzers; buildtransitive + diff --git a/src/Client/Bit.TemplatePlayground.Client.Web/Extensions/IServiceCollectionExtensions.cs b/src/Client/Bit.TemplatePlayground.Client.Web/Extensions/IServiceCollectionExtensions.cs deleted file mode 100644 index e4d970a..0000000 --- a/src/Client/Bit.TemplatePlayground.Client.Web/Extensions/IServiceCollectionExtensions.cs +++ /dev/null @@ -1,18 +0,0 @@ -using Bit.TemplatePlayground.Client.Web.Services; - -namespace Microsoft.Extensions.DependencyInjection; - -public static class IServiceCollectionExtensions -{ - public static IServiceCollection AddClientWebServices(this IServiceCollection services) - { - // Services being registered here can get injected in web (blazor web assembly & blazor server) - - services.AddTransient(); - services.AddTransient(); - - services.AddClientSharedServices(); - - return services; - } -} diff --git a/src/Client/Bit.TemplatePlayground.Client.Web/Program.Services.cs b/src/Client/Bit.TemplatePlayground.Client.Web/Program.Services.cs new file mode 100644 index 0000000..2c2737f --- /dev/null +++ b/src/Client/Bit.TemplatePlayground.Client.Web/Program.Services.cs @@ -0,0 +1,38 @@ +using Bit.TemplatePlayground.Client.Web.Services; +using Microsoft.AspNetCore.Components.WebAssembly.Hosting; + +namespace Bit.TemplatePlayground.Client.Web; + +public static partial class Program +{ + public static void ConfigureServices(this WebAssemblyHostBuilder builder) + { + // Services being registered here can get injected in web project only. + + var services = builder.Services; + var configuration = builder.Configuration; + + configuration.AddClientConfigurations(); + + Uri.TryCreate(configuration.GetApiServerAddress(), UriKind.RelativeOrAbsolute, out var apiServerAddress); + + if (apiServerAddress!.IsAbsoluteUri is false) + { + apiServerAddress = new Uri(new Uri(builder.HostEnvironment.BaseAddress), apiServerAddress); + } + + services.TryAddTransient(sp => new HttpClient(sp.GetRequiredKeyedService("DefaultMessageHandler")) { BaseAddress = apiServerAddress }); + + services.AddClientWebProjectServices(); + } + + public static void AddClientWebProjectServices(this IServiceCollection services) + { + // Services being registered here can get injected in both web project and server (during prerendering). + + services.TryAddTransient(); + services.TryAddTransient(); + + services.AddClientCoreProjectServices(); + } +} diff --git a/src/Client/Bit.TemplatePlayground.Client.Web/Program.cs b/src/Client/Bit.TemplatePlayground.Client.Web/Program.cs index 5fa7cf7..c1c14fe 100644 --- a/src/Client/Bit.TemplatePlayground.Client.Web/Program.cs +++ b/src/Client/Bit.TemplatePlayground.Client.Web/Program.cs @@ -1,34 +1,41 @@ -#if BlazorWebAssemblyStandalone +using Microsoft.AspNetCore.Components.WebAssembly.Hosting; +#if BlazorWebAssemblyStandalone using Microsoft.AspNetCore.Components.Web; #endif -using Microsoft.AspNetCore.Components.WebAssembly.Hosting; -var builder = WebAssemblyHostBuilder.CreateDefault(args); +namespace Bit.TemplatePlayground.Client.Web; + +public static partial class Program +{ + public static async Task Main(string[] args) + { + var builder = WebAssemblyHostBuilder.CreateDefault(args); #if BlazorWebAssemblyStandalone builder.RootComponents.Add("#app-container"); builder.RootComponents.Add("head::after"); #endif -builder.Configuration.AddClientConfigurations(); + builder.ConfigureServices(); -Uri.TryCreate(builder.Configuration.GetApiServerAddress(), UriKind.RelativeOrAbsolute, out var apiServerAddress); - -if (apiServerAddress!.IsAbsoluteUri is false) -{ - apiServerAddress = new Uri(new Uri(builder.HostEnvironment.BaseAddress), apiServerAddress); -} + var host = builder.Build(); -builder.Services.AddTransient(sp => new HttpClient(sp.GetRequiredKeyedService("DefaultMessageHandler")) { BaseAddress = apiServerAddress }); + if (AppRenderMode.MultilingualEnabled) + { + var culture = await host.Services.GetRequiredService().GetItem("Culture"); + host.Services.GetRequiredService().SetCurrentCulture(culture); + } -builder.Services.AddClientWebServices(); - -var host = builder.Build(); + try + { + await host.RunAsync(); + } + catch (JSException exp) when (exp.Message is "Error: Could not find any element matching selector '#app-container'.") + { +#if BlazorWebAssemblyStandalone +await Console.Error.WriteLineAsync("Either run/publish Client.Web project or set BlazorWebAssemblyStandalone to false."); +#endif + } -if (AppRenderMode.MultilingualEnabled) -{ - var culture = await host.Services.GetRequiredService().GetItem("Culture"); - host.Services.GetRequiredService().SetCurrentCulture(culture); + } } - -await host.RunAsync(); diff --git a/src/Client/Bit.TemplatePlayground.Client.Web/wwwroot/service-worker.js b/src/Client/Bit.TemplatePlayground.Client.Web/wwwroot/service-worker.js index afc966f..6f0a2b7 100644 --- a/src/Client/Bit.TemplatePlayground.Client.Web/wwwroot/service-worker.js +++ b/src/Client/Bit.TemplatePlayground.Client.Web/wwwroot/service-worker.js @@ -1,4 +1,4 @@ -// bit version: 8.7.4-pre-01 +// bit version: 8.7.5-pre-01 // https://github.com/bitfoundation/bitplatform/tree/develop/src/Bswup self.assetsInclude = []; diff --git a/src/Client/Bit.TemplatePlayground.Client.Windows/.config/dotnet-tools.json b/src/Client/Bit.TemplatePlayground.Client.Windows/.config/dotnet-tools.json index c32f6b1..a6c91e9 100644 --- a/src/Client/Bit.TemplatePlayground.Client.Windows/.config/dotnet-tools.json +++ b/src/Client/Bit.TemplatePlayground.Client.Windows/.config/dotnet-tools.json @@ -3,7 +3,7 @@ "isRoot": true, "tools": { "vpk": { - "version": "0.0.212", + "version": "0.0.276", "commands": [ "vpk" ] diff --git a/src/Client/Bit.TemplatePlayground.Client.Windows/App.xaml b/src/Client/Bit.TemplatePlayground.Client.Windows/App.xaml index 4e2e47d..79f0717 100644 --- a/src/Client/Bit.TemplatePlayground.Client.Windows/App.xaml +++ b/src/Client/Bit.TemplatePlayground.Client.Windows/App.xaml @@ -1,4 +1,8 @@ - + diff --git a/src/Client/Bit.TemplatePlayground.Client.Windows/App.xaml.cs b/src/Client/Bit.TemplatePlayground.Client.Windows/App.xaml.cs index 16e3f64..2cccbdc 100644 --- a/src/Client/Bit.TemplatePlayground.Client.Windows/App.xaml.cs +++ b/src/Client/Bit.TemplatePlayground.Client.Windows/App.xaml.cs @@ -33,5 +33,16 @@ private void App_Exit(object sender, ExitEventArgs e) using StreamWriter writer = new StreamWriter(stream); writer.Write(JsonSerializer.Serialize(Properties)); } + + private void App_DispatcherUnhandledException(object sender, System.Windows.Threading.DispatcherUnhandledExceptionEventArgs e) + { + try + { + ((MainWindow)MainWindow).BlazorWebView.Services.GetRequiredService().Handle(e.Exception); + } + catch { } + + e.Handled = true; + } } diff --git a/src/Client/Bit.TemplatePlayground.Client.Windows/Bit.TemplatePlayground.Client.Windows.csproj b/src/Client/Bit.TemplatePlayground.Client.Windows/Bit.TemplatePlayground.Client.Windows.csproj index 5e7d5a3..334b85c 100644 --- a/src/Client/Bit.TemplatePlayground.Client.Windows/Bit.TemplatePlayground.Client.Windows.csproj +++ b/src/Client/Bit.TemplatePlayground.Client.Windows/Bit.TemplatePlayground.Client.Windows.csproj @@ -17,17 +17,17 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive - + all runtime; build; native; contentfiles; analyzers; buildtransitive - + PreserveNewest diff --git a/src/Client/Bit.TemplatePlayground.Client.Windows/Extensions/IServiceCollectionExtensions.cs b/src/Client/Bit.TemplatePlayground.Client.Windows/Extensions/IServiceCollectionExtensions.cs deleted file mode 100644 index 3ecd92a..0000000 --- a/src/Client/Bit.TemplatePlayground.Client.Windows/Extensions/IServiceCollectionExtensions.cs +++ /dev/null @@ -1,15 +0,0 @@ -using Bit.TemplatePlayground.Client.Windows.Services; - -namespace Microsoft.Extensions.DependencyInjection; - -public static class IServiceCollectionExtensions -{ - public static void AddWindowsServices(this IServiceCollection services) - { - services.TryAddTransient(); - services.AddTransient(); - services.AddTransient(); - - services.AddClientSharedServices(); - } -} diff --git a/src/Client/Bit.TemplatePlayground.Client.Windows/MainWindow.xaml b/src/Client/Bit.TemplatePlayground.Client.Windows/MainWindow.xaml index 2ea0217..0b0db7e 100644 --- a/src/Client/Bit.TemplatePlayground.Client.Windows/MainWindow.xaml +++ b/src/Client/Bit.TemplatePlayground.Client.Windows/MainWindow.xaml @@ -1,16 +1,22 @@ - + - + - + diff --git a/src/Client/Bit.TemplatePlayground.Client.Windows/MainWindow.xaml.cs b/src/Client/Bit.TemplatePlayground.Client.Windows/MainWindow.xaml.cs index b3f3345..2ffead4 100644 --- a/src/Client/Bit.TemplatePlayground.Client.Windows/MainWindow.xaml.cs +++ b/src/Client/Bit.TemplatePlayground.Client.Windows/MainWindow.xaml.cs @@ -8,26 +8,7 @@ public MainWindow() { AppRenderMode.IsBlazorHybrid = true; var services = new ServiceCollection(); - ConfigurationBuilder configurationBuilder = new(); - configurationBuilder.AddClientConfigurations(); - var configuration = configurationBuilder.Build(); - services.AddTransient(sp => configuration); - Uri.TryCreate(configuration.GetApiServerAddress(), UriKind.Absolute, out var apiServerAddress); - services.AddTransient(sp => - { - var handler = sp.GetRequiredKeyedService("DefaultMessageHandler"); - HttpClient httpClient = new(handler) - { - BaseAddress = apiServerAddress - }; - return httpClient; - }); - services.AddWpfBlazorWebView(); - if (BuildConfiguration.IsDebug()) - { - services.AddBlazorWebViewDeveloperTools(); - } - services.AddWindowsServices(); + services.ConfigureServices(); InitializeComponent(); BlazorWebView.Services = services.BuildServiceProvider(); BlazorWebView.Services.GetRequiredService().SetCurrentCulture(App.Current.Properties["Culture"]?.ToString()); diff --git a/src/Client/Bit.TemplatePlayground.Client.Windows/Program.Services.cs b/src/Client/Bit.TemplatePlayground.Client.Windows/Program.Services.cs new file mode 100644 index 0000000..63a6f69 --- /dev/null +++ b/src/Client/Bit.TemplatePlayground.Client.Windows/Program.Services.cs @@ -0,0 +1,40 @@ +using System.Net.Http; +using Bit.TemplatePlayground.Client.Windows.Services; + +namespace Bit.TemplatePlayground.Client.Windows; + +public static partial class Program +{ + public static void ConfigureServices(this IServiceCollection services) + { + // Services being registered here can get injected in windows project only. + + ConfigurationBuilder configurationBuilder = new(); + configurationBuilder.AddClientConfigurations(); + var configuration = configurationBuilder.Build(); + services.TryAddTransient(sp => configuration); + + Uri.TryCreate(configuration.GetApiServerAddress(), UriKind.Absolute, out var apiServerAddress); + services.TryAddTransient(sp => + { + var handler = sp.GetRequiredKeyedService("DefaultMessageHandler"); + HttpClient httpClient = new(handler) + { + BaseAddress = apiServerAddress + }; + return httpClient; + }); + + services.AddWpfBlazorWebView(); + if (BuildConfiguration.IsDebug()) + { + services.AddBlazorWebViewDeveloperTools(); + } + + services.TryAddTransient(); + services.TryAddTransient(); + services.TryAddTransient(); + + services.AddClientCoreProjectServices(); + } +} diff --git a/src/Client/Bit.TemplatePlayground.Client.Windows/Program.cs b/src/Client/Bit.TemplatePlayground.Client.Windows/Program.cs index 7a52259..f658fae 100644 --- a/src/Client/Bit.TemplatePlayground.Client.Windows/Program.cs +++ b/src/Client/Bit.TemplatePlayground.Client.Windows/Program.cs @@ -3,7 +3,7 @@ namespace Bit.TemplatePlayground.Client.Windows; -public class Program +public partial class Program { [STAThread] public static void Main(string[] args) diff --git a/src/Directory.Build.props b/src/Directory.Build.props index f9bcf57..eeb56c9 100644 --- a/src/Directory.Build.props +++ b/src/Directory.Build.props @@ -1,4 +1,4 @@ - + 12.0 @@ -8,6 +8,9 @@ $(NoWarn);CS1998;CS1591 $(WarningsAsErrors);CS0114 + + + en-US true false