From 55fa21a92bef8cdd1c3a6318878492ecc7f1747d Mon Sep 17 00:00:00 2001 From: Yaser Moradi Date: Tue, 29 Oct 2024 17:11:41 +0100 Subject: [PATCH] feat(templates); add support for optimistic concurrency handling to Boilerplate #9048 (#9049) --- .../.template.config/template.json | 2 +- .../Boilerplate.Client.Core.csproj | 2 +- .../Components/AppComponentBase.cs | 2 +- .../Components/AppInitializer.cs | 3 + .../Categories/CategoriesPage.razor.cs | 2 +- .../Authorized/Products/ProductsPage.razor.cs | 2 +- .../Pages/NotAuthorizedPage.razor.cs | 8 +- .../Components/Routes.razor.cs | 4 +- .../Extensions/ByteArrayExtensions.cs | 14 +++ ...IClientCoreServiceCollectionExtensions.cs} | 2 +- .../Extensions/IJSRuntimeExtensions.cs | 4 +- .../Extensions/ILoggingBuilderExtensions.cs | 11 +++ .../Services/AppPlatform.cs | 2 +- .../Services/AuthenticationManager.cs | 6 +- .../Services/BrowserConsoleLoggerProvider.cs | 90 +++++++++++++++++++ .../Contracts/IPushNotificationService.cs | 4 +- .../Services/ExceptionHandlerBase.cs | 11 --- .../AuthDelegatingHandler.cs | 2 +- .../Services/PushNotificationServiceBase.cs | 22 ++--- .../Boilerplate.Client.Maui.csproj | 20 ++--- .../MauiProgram.Services.cs | 1 + ...=> IAndroidServiceCollectionExtensions.cs} | 2 +- .../Platforms/Android/MainActivity.cs | 2 +- .../AndroidPushNotificationService.cs | 26 +++++- .../Platforms/MacCatalyst/AppDelegate.cs | 2 +- ...g.plist => Entitlements.Development.plist} | 0 ...se.plist => Entitlements.Production.plist} | 0 ....cs => IMacServiceCollectionExtensions.cs} | 2 +- .../MacCatalystPushNotificationService.cs | 26 +++++- ...=> IWindowsServiceCollectionExtensions.cs} | 2 +- .../WindowsPushNotificationService.cs | 2 +- .../Platforms/iOS/AppDelegate.cs | 2 +- ....cs => IIosServiceCollectionExtensions.cs} | 2 +- .../Services/iOSPushNotificationService.cs | 26 +++++- .../Services/MauiLocalHttpServer.cs | 15 +++- .../Services/MauiTelemetryInitializer.cs | 4 +- .../Boilerplate.Client.Maui/appsettings.json | 10 +++ .../Services/WebPushNotificationService.cs | 4 +- .../Boilerplate.Client.Web/appsettings.json | 4 - .../MainWindow.xaml.cs | 4 + .../Program.Services.cs | 1 + .../Services/WindowsLocalHttpServer.cs | 17 +++- .../WindowsPushNotificationService.cs | 2 +- .../Boilerplate.Server.Api.csproj | 2 +- .../Categories/CategoryController.cs | 18 ++-- .../Identity/IdentityController.cs | 73 +++++++-------- .../Controllers/Products/ProductController.cs | 18 ++-- .../Data/AppDbContext.cs | 62 ++++++++++--- .../Category/CategoryConfiguration.cs | 11 +-- .../Product/ProductConfiguration.cs | 55 ++++++------ ...241028193317_InitialMigration.Designer.cs} | 48 ++++++++-- ....cs => 20241028193317_InitialMigration.cs} | 74 +++++++-------- .../Migrations/AppDbContextModelSnapshot.cs | 46 ++++++++-- .../Mappers/CategoriesMapper.cs | 4 +- .../Mappers/IdentityMapper.cs | 2 +- .../Mappers/ProductsMapper.cs | 11 +-- .../Mappers/PushNotificationMapper.cs | 2 +- .../Mappers/TodoMapper.cs | 2 +- .../Models/Categories/Category.cs | 2 + .../Models/Products/Product.cs | 2 + .../Services/PushNotificationService.cs | 6 +- .../Categories/ICategoryController.cs | 4 +- .../Products/IProductController.cs | 4 +- .../src/Shared/Dtos/Categories/CategoryDto.cs | 2 + .../src/Shared/Dtos/Products/ProductDto.cs | 2 + ... => ISharedServiceCollectionExtensions.cs} | 2 +- .../src/Shared/appsettings.json | 8 -- 67 files changed, 565 insertions(+), 264 deletions(-) create mode 100644 src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Extensions/ByteArrayExtensions.cs rename src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Extensions/{IServiceCollectionExtensions.cs => IClientCoreServiceCollectionExtensions.cs} (99%) create mode 100644 src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Extensions/ILoggingBuilderExtensions.cs create mode 100644 src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Services/BrowserConsoleLoggerProvider.cs rename src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Maui/Platforms/Android/Extensions/{IServiceCollectionExtensions.cs => IAndroidServiceCollectionExtensions.cs} (89%) rename src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Maui/Platforms/MacCatalyst/{Entitlements.Debug.plist => Entitlements.Development.plist} (100%) rename src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Maui/Platforms/MacCatalyst/{Entitlements.Release.plist => Entitlements.Production.plist} (100%) rename src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Maui/Platforms/MacCatalyst/Extensions/{IServiceCollectionExtensions.cs => IMacServiceCollectionExtensions.cs} (90%) rename src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Maui/Platforms/Windows/Extensions/{IServiceCollectionExtensions.cs => IWindowsServiceCollectionExtensions.cs} (89%) rename src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Maui/Platforms/iOS/Extensions/{IServiceCollectionExtensions.cs => IIosServiceCollectionExtensions.cs} (90%) rename src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Data/Migrations/{20241011170531_InitialMigration.Designer.cs => 20241028193317_InitialMigration.Designer.cs} (91%) rename src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Data/Migrations/{20241011170531_InitialMigration.cs => 20241028193317_InitialMigration.cs} (75%) rename src/Templates/Boilerplate/Bit.Boilerplate/src/Shared/Extensions/{IServiceCollectionExtensions.cs => ISharedServiceCollectionExtensions.cs} (96%) diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/.template.config/template.json b/src/Templates/Boilerplate/Bit.Boilerplate/.template.config/template.json index 8b4cf2fab3..3595be69aa 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/.template.config/template.json +++ b/src/Templates/Boilerplate/Bit.Boilerplate/.template.config/template.json @@ -332,7 +332,7 @@ "path": "README.md" }, { - "path": "Boilerplate.sln" + "path": "Boilerplate.Web.slnf" } ], "sources": [ diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Boilerplate.Client.Core.csproj b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Boilerplate.Client.Core.csproj index 91d4fb6c21..c8209c4434 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Boilerplate.Client.Core.csproj +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Boilerplate.Client.Core.csproj @@ -68,7 +68,7 @@ - + diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/AppComponentBase.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/AppComponentBase.cs index b3fd90bae4..cdf63beadb 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/AppComponentBase.cs +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/AppComponentBase.cs @@ -42,7 +42,7 @@ public partial class AppComponentBase : ComponentBase, IAsyncDisposable private readonly CancellationTokenSource cts = new(); protected CancellationToken CurrentCancellationToken => cts.Token; - protected bool InPrerenderSession => JSRuntime.IsInitialized() is false; + protected bool InPrerenderSession => AppPlatform.IsBlazorHybrid is false && JSRuntime.IsInitialized() is false; protected sealed override void OnInitialized() { diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/AppInitializer.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/AppInitializer.cs index e4e3efa550..6dff25ac30 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/AppInitializer.cs +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/AppInitializer.cs @@ -21,6 +21,7 @@ public partial class AppInitializer : AppComponentBase [AutoInject] private IStorageService storageService = default!; [AutoInject] private CultureInfoManager cultureInfoManager = default!; [AutoInject] private NavigationManager navigationManager = default!; + [AutoInject] private Bit.Butil.Console console = default!; protected async override Task OnInitAsync() { @@ -38,6 +39,8 @@ await storageService.GetItem("Culture") ?? // 2- User settings } await SetupBodyClasses(); + + BrowserConsoleLoggerProvider.SetConsole(console); } await base.OnInitAsync(); diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/Authorized/Categories/CategoriesPage.razor.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/Authorized/Categories/CategoriesPage.razor.cs index 3ebd5e5836..4e0e93df6a 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/Authorized/Categories/CategoriesPage.razor.cs +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/Authorized/Categories/CategoriesPage.razor.cs @@ -97,7 +97,7 @@ private async Task DeleteCategory() try { - await categoryController.Delete(deletingCategory.Id, CurrentCancellationToken); + await categoryController.Delete(deletingCategory.Id, deletingCategory.ConcurrencyStamp.ToStampString(), CurrentCancellationToken); await RefreshData(); } diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/Authorized/Products/ProductsPage.razor.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/Authorized/Products/ProductsPage.razor.cs index 966f0e357e..055df432d0 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/Authorized/Products/ProductsPage.razor.cs +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/Authorized/Products/ProductsPage.razor.cs @@ -119,7 +119,7 @@ private async Task DeleteProduct() try { - await productController.Delete(deletingProduct.Id, CurrentCancellationToken); + await productController.Delete(deletingProduct.Id, deletingProduct.ConcurrencyStamp.ToStampString(), CurrentCancellationToken); await RefreshData(); } diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/NotAuthorizedPage.razor.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/NotAuthorizedPage.razor.cs index c3a1415336..9d76ecf3ec 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/NotAuthorizedPage.razor.cs +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/NotAuthorizedPage.razor.cs @@ -1,4 +1,6 @@ -namespace Boilerplate.Client.Core.Components.Pages; +using Microsoft.Extensions.Logging; + +namespace Boilerplate.Client.Core.Components.Pages; public partial class NotAuthorizedPage { @@ -6,6 +8,8 @@ public partial class NotAuthorizedPage [SupplyParameterFromQuery(Name = "return-url"), Parameter] public string? ReturnUrl { get; set; } + [AutoInject] private ILogger logger = default!; + protected override async Task OnParamsSetAsync() { user = (await AuthenticationStateTask).User; @@ -25,6 +29,8 @@ protected override async Task OnAfterFirstRenderAsync() { await AuthenticationManager.RefreshToken(); + logger.LogInformation("Refreshing access token."); + if ((await AuthenticationStateTask).User.IsAuthenticated()) { if (ReturnUrl is not null) diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Routes.razor.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Routes.razor.cs index e0c1c2c508..9cdf639f43 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Routes.razor.cs +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Routes.razor.cs @@ -15,7 +15,9 @@ await Task.Run(async () => } }); - if (CultureInfoManager.MultilingualEnabled && forceLoad == false) + if (CultureInfoManager.MultilingualEnabled && + forceLoad == false && + (AppPlatform.IsAndroid || AppPlatform.IsIOS)) { var currentCulture = CultureInfo.CurrentUICulture.Name; var uri = new Uri(url); diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Extensions/ByteArrayExtensions.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Extensions/ByteArrayExtensions.cs new file mode 100644 index 0000000000..d84ee60dd2 --- /dev/null +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Extensions/ByteArrayExtensions.cs @@ -0,0 +1,14 @@ +namespace System; + +public static class ByteArrayExtensions +{ + public static string ToStampString(this byte[]? source) + { + if (source is null or { Length: 0 }) + source = [0, 0, 0, 0, 0, 0, 0, 0]; + + var base64String = Convert.ToBase64String(source); + + return Uri.EscapeDataString(base64String); + } +} diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Extensions/IServiceCollectionExtensions.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Extensions/IClientCoreServiceCollectionExtensions.cs similarity index 99% rename from src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Extensions/IServiceCollectionExtensions.cs rename to src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Extensions/IClientCoreServiceCollectionExtensions.cs index 406c441915..81df0f17e2 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Extensions/IServiceCollectionExtensions.cs +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Extensions/IClientCoreServiceCollectionExtensions.cs @@ -14,7 +14,7 @@ namespace Microsoft.Extensions.DependencyInjection; -public static partial class IServiceCollectionExtensions +public static partial class IClientCoreServiceCollectionExtensions { public static IServiceCollection AddClientCoreProjectServices(this IServiceCollection services, IConfiguration configuration) { diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Extensions/IJSRuntimeExtensions.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Extensions/IJSRuntimeExtensions.cs index 0f74c8543c..48518d9a2f 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Extensions/IJSRuntimeExtensions.cs +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Extensions/IJSRuntimeExtensions.cs @@ -1,4 +1,5 @@ //+:cnd:noEmit +using System.Reflection; //#if (notification == true) using Boilerplate.Shared.Dtos.PushNotification; //#endif @@ -47,7 +48,8 @@ public static bool IsInitialized(this IJSRuntime jsRuntime) { "UnsupportedJavaScriptRuntime" => false, // pre-rendering "RemoteJSRuntime" /* blazor server */ => (bool)type.GetProperty("IsInitialized")!.GetValue(jsRuntime)!, - _ => true // blazor wasm / hybrid + "WebViewJSRuntime" /* blazor hybrid */ => type.GetField("_ipcSender", BindingFlags.NonPublic | BindingFlags.Instance)!.GetValue(jsRuntime) is not null, + _ => true // blazor wasm }; } } diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Extensions/ILoggingBuilderExtensions.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Extensions/ILoggingBuilderExtensions.cs new file mode 100644 index 0000000000..b3088e8582 --- /dev/null +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Extensions/ILoggingBuilderExtensions.cs @@ -0,0 +1,11 @@ +namespace Microsoft.Extensions.Logging; + +public static class ILoggingBuilderExtensions +{ + public static ILoggingBuilder AddBrowserConsoleLogger(this ILoggingBuilder builder) + { + builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton()); + + return builder; + } +} diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Services/AppPlatform.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Services/AppPlatform.cs index 9bab3b3884..4368df6e3a 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Services/AppPlatform.cs +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Services/AppPlatform.cs @@ -13,7 +13,7 @@ public static partial class AppPlatform public static bool IsAndroid => OperatingSystem.IsAndroid(); [SupportedOSPlatformGuard("ios")] - public static bool IsIOS => OperatingSystem.IsIOS(); + public static bool IsIOS => OperatingSystem.IsIOS() && !IsIosOnMacOS; [SupportedOSPlatformGuard("windows")] public static bool IsWindows => OperatingSystem.IsWindows(); diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Services/AuthenticationManager.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Services/AuthenticationManager.cs index 6a94645c5b..daf233a748 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Services/AuthenticationManager.cs +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Services/AuthenticationManager.cs @@ -77,7 +77,9 @@ public override async Task GetAuthenticationStateAsync() { var access_token = await prerenderStateService.GetValue(() => tokenProvider.GetAccessToken()); - if (string.IsNullOrEmpty(access_token) && jsRuntime.IsInitialized()) + bool inPrerenderSession = AppPlatform.IsBlazorHybrid is false && jsRuntime.IsInitialized() is false; + + if (string.IsNullOrEmpty(access_token) && inPrerenderSession is false) { string? refresh_token = await storageService.GetItem("refresh_token"); @@ -126,7 +128,7 @@ private async Task StoreTokens(TokenResponseDto response, bool? rememberMe = nul await storageService.SetItem("access_token", response!.AccessToken, rememberMe is true); await storageService.SetItem("refresh_token", response!.RefreshToken, rememberMe is true); - if (jsRuntime.IsInitialized()) + if (AppPlatform.IsBlazorHybrid is false && jsRuntime.IsInitialized()) { await cookie.Set(new() { diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Services/BrowserConsoleLoggerProvider.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Services/BrowserConsoleLoggerProvider.cs new file mode 100644 index 0000000000..6cb91bca63 --- /dev/null +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Services/BrowserConsoleLoggerProvider.cs @@ -0,0 +1,90 @@ +//+:cnd:noEmit +using Microsoft.Extensions.Logging; + +namespace Boilerplate.Client.Core.Services; + +// https://learn.microsoft.com/en-us/aspnet/core/blazor/hybrid/developer-tools + +/// +/// This logger writes to the browser console in blazor hybrid. +/// +[ProviderAlias("BrowserConsolelogger")] +public partial class BrowserConsoleLoggerProvider : ILoggerProvider, ILogger, IDisposable +{ + private static Bit.Butil.Console? console; + + public static void SetConsole(Bit.Butil.Console console) + { + if (AppPlatform.IsBlazorHybrid is false) + throw new InvalidOperationException(); + BrowserConsoleLoggerProvider.console = console; + } + + public string? CategoryName { get; init; } + + private static readonly ConcurrentQueue states = new(); + + public ILogger CreateLogger(string categoryName) + { + return new BrowserConsoleLoggerProvider + { + CategoryName = categoryName + }; + } + + public IDisposable? BeginScope(TState state) + where TState : notnull + { + if (state is IDictionary dictionary) + { + dictionary["Category"] = CategoryName; + } + + states.Enqueue(state); + + return this; + } + + public bool IsEnabled(LogLevel logLevel) + { + return AppPlatform.IsBlazorHybrid + && console is not null; + } + + public void Log(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func formatter) + { + if (IsEnabled(logLevel) is false) return; + + var message = formatter(state, exception); + + var currentState = states.TryDequeue(out var stateFromQueue) ? stateFromQueue : state; + + switch (logLevel) + { + case LogLevel.Trace: + case LogLevel.Debug: + console!.Log(message, $"{Environment.NewLine}State:", currentState); + break; + case LogLevel.Information: + console!.Info(message, $"{Environment.NewLine}State:", currentState); + break; + case LogLevel.Warning: + console!.Warn(message, $"{Environment.NewLine}State:", currentState); + break; + case LogLevel.Error: + case LogLevel.Critical: + console!.Error(message, $"{Environment.NewLine}State:", currentState); + break; + case LogLevel.None: + break; + default: + console!.Log(message, $"{Environment.NewLine}State:", currentState); + break; + } + } + + public void Dispose() + { + states.TryDequeue(out _); + } +} diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Services/Contracts/IPushNotificationService.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Services/Contracts/IPushNotificationService.cs index 3ddf1b1c03..5651fbf090 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Services/Contracts/IPushNotificationService.cs +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Services/Contracts/IPushNotificationService.cs @@ -5,7 +5,7 @@ namespace Boilerplate.Client.Core.Services.Contracts; public interface IPushNotificationService { string Token { get; set; } - Task IsNotificationSupported(); - Task GetDeviceInstallation(); + Task IsNotificationSupported(CancellationToken cancellationToken); + Task GetDeviceInstallation(CancellationToken cancellationToken); Task RegisterDevice(CancellationToken cancellationToken); } diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Services/ExceptionHandlerBase.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Services/ExceptionHandlerBase.cs index 02acd6a01e..745076872f 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Services/ExceptionHandlerBase.cs +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Services/ExceptionHandlerBase.cs @@ -1,6 +1,5 @@ //-:cnd:noEmit using System.Diagnostics; -using System.Text; using Microsoft.Extensions.Logging; using System.Runtime.CompilerServices; @@ -40,16 +39,6 @@ protected virtual void Handle(Exception exception, Dictionary pa if (isDevEnv) { - if (AppPlatform.IsBrowser is false) - { - StringBuilder errorInfo = new(); - errorInfo.AppendLine(exceptionMessage); - foreach (var item in parameters) - { - errorInfo.AppendLine($"{item.Key}: {item.Value}"); - } - _ = Console.Error(errorInfo.ToString()); - } Debugger.Break(); } diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Services/HttpMessageHandlers/AuthDelegatingHandler.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Services/HttpMessageHandlers/AuthDelegatingHandler.cs index 68cc80f9cd..ba19185e0e 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Services/HttpMessageHandlers/AuthDelegatingHandler.cs +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Services/HttpMessageHandlers/AuthDelegatingHandler.cs @@ -29,7 +29,7 @@ protected override async Task SendAsync(HttpRequestMessage // Let's update the access token by refreshing it when a refresh token is available. // Following this procedure, the newly acquired access token may now include the necessary roles or claims. - if (jsRuntime.IsInitialized() is false) + if (AppPlatform.IsBlazorHybrid is false && jsRuntime.IsInitialized() is false) throw; // We don't have access to refresh_token during pre-rendering. if (request.RequestUri?.LocalPath?.Contains("api/Identity/Refresh", StringComparison.InvariantCultureIgnoreCase) is true) diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Services/PushNotificationServiceBase.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Services/PushNotificationServiceBase.cs index c085a528d5..bff1737d24 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Services/PushNotificationServiceBase.cs +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Services/PushNotificationServiceBase.cs @@ -1,33 +1,27 @@ using Boilerplate.Shared.Dtos.PushNotification; using Boilerplate.Shared.Controllers.PushNotification; +using Microsoft.Extensions.Logging; namespace Boilerplate.Client.Core.Services; public abstract partial class PushNotificationServiceBase : IPushNotificationService { + [AutoInject] protected ILogger Logger = default!; [AutoInject] protected IPushNotificationController pushNotificationController = default!; public virtual string Token { get; set; } - public virtual Task IsNotificationSupported() => Task.FromResult(false); - public abstract Task GetDeviceInstallation(); + public virtual Task IsNotificationSupported(CancellationToken cancellationToken) => Task.FromResult(false); + public abstract Task GetDeviceInstallation(CancellationToken cancellationToken); public async Task RegisterDevice(CancellationToken cancellationToken) { - if (await IsNotificationSupported() is false) - return; - - using CancellationTokenSource cts = new(TimeSpan.FromSeconds(30)); - using var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, cts.Token); - - while (string.IsNullOrEmpty(Token)) + if (await IsNotificationSupported(cancellationToken) is false) { - // After the NotificationsSupported Task completes with a result of true, - // we use FirebaseMessaging.Instance.GetToken and UNUserNotificationCenter.Current.Delegate. - // Those methods are asynchronous and we need to wait for them to complete. - await Task.Delay(TimeSpan.FromSeconds(1), linkedCts.Token); + Logger.LogInformation("Notifications are not supported/allowed on this device."); + return; } - var deviceInstallation = await GetDeviceInstallation(); + var deviceInstallation = await GetDeviceInstallation(cancellationToken); await pushNotificationController.RegisterDevice(deviceInstallation, cancellationToken); } diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Maui/Boilerplate.Client.Maui.csproj b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Maui/Boilerplate.Client.Maui.csproj index 4b44a196d8..2fe1328118 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Maui/Boilerplate.Client.Maui.csproj +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Maui/Boilerplate.Client.Maui.csproj @@ -22,7 +22,7 @@ 1 - True + True $(NoWarn);ClassWithoutModifierAnalyzer @@ -33,7 +33,7 @@ false - + true android-arm64 false @@ -43,13 +43,13 @@ - - Platforms/iOS/Entitlements.Debug.plist + + Platforms/iOS/Entitlements.Development.plist True - - Platforms/iOS/Entitlements.Release.plist + + Platforms/iOS/Entitlements.Production.plist True @@ -61,12 +61,12 @@ - - Platforms/MacCatalyst/Entitlements.Debug.plist + + Platforms/MacCatalyst/Entitlements.Development.plist - - Platforms/MacCatalyst/Entitlements.Release.plist + + Platforms/MacCatalyst/Entitlements.Production.plist true -all diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Maui/MauiProgram.Services.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Maui/MauiProgram.Services.cs index c720e8b700..76dc55d493 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Maui/MauiProgram.Services.cs +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Maui/MauiProgram.Services.cs @@ -37,6 +37,7 @@ public static void ConfigureServices(this MauiAppBuilder builder) if (AppEnvironment.IsDev()) { builder.Logging.AddDebug(); + builder.Logging.AddBrowserConsoleLogger(); } builder.Logging.AddConsole(); diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Maui/Platforms/Android/Extensions/IServiceCollectionExtensions.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Maui/Platforms/Android/Extensions/IAndroidServiceCollectionExtensions.cs similarity index 89% rename from src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Maui/Platforms/Android/Extensions/IServiceCollectionExtensions.cs rename to src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Maui/Platforms/Android/Extensions/IAndroidServiceCollectionExtensions.cs index af3287a7cf..56ef3f5a97 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Maui/Platforms/Android/Extensions/IServiceCollectionExtensions.cs +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Maui/Platforms/Android/Extensions/IAndroidServiceCollectionExtensions.cs @@ -5,7 +5,7 @@ namespace Microsoft.Extensions.DependencyInjection; -public static partial class IServiceCollectionExtensions +public static partial class IAndroidServiceCollectionExtensions { public static IServiceCollection AddClientMauiProjectAndroidServices(this IServiceCollection services, IConfiguration configuration) { diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Maui/Platforms/Android/MainActivity.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Maui/Platforms/Android/MainActivity.cs index 64e23f540c..b36da5b143 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Maui/Platforms/Android/MainActivity.cs +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Maui/Platforms/Android/MainActivity.cs @@ -53,7 +53,7 @@ protected override void OnCreate(Bundle? savedInstanceState) _ = Routes.OpenUniversalLink(new URL(url).File ?? Urls.HomePage); } //#if (notification == true) - PushNotificationService.IsNotificationSupported().ContinueWith(task => + PushNotificationService.IsNotificationSupported(default).ContinueWith(task => { if (task.Result) { diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Maui/Platforms/Android/Services/AndroidPushNotificationService.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Maui/Platforms/Android/Services/AndroidPushNotificationService.cs index bcc28575e6..e1ebff36f9 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Maui/Platforms/Android/Services/AndroidPushNotificationService.cs +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Maui/Platforms/Android/Services/AndroidPushNotificationService.cs @@ -2,12 +2,13 @@ using Plugin.LocalNotification; using static Android.Provider.Settings; using Boilerplate.Shared.Dtos.PushNotification; +using Microsoft.Extensions.Logging; namespace Boilerplate.Client.Maui.Platforms.Android.Services; public partial class AndroidPushNotificationService : PushNotificationServiceBase { - public async override Task IsNotificationSupported() + public async override Task IsNotificationSupported(CancellationToken cancellationToken) { return await MainThread.InvokeOnMainThreadAsync(async () => { @@ -23,8 +24,29 @@ public async override Task IsNotificationSupported() public string GetDeviceId() => Secure.GetString(Platform.AppContext.ContentResolver, Secure.AndroidId)!; - public override async Task GetDeviceInstallation() + public override async Task GetDeviceInstallation(CancellationToken cancellationToken) { + try + { + using CancellationTokenSource cts = new(TimeSpan.FromSeconds(30)); + using var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, cts.Token); + + while (string.IsNullOrEmpty(Token)) + { + // After the NotificationsSupported Task completes with a result of true, + // we use FirebaseMessaging.Instance.GetToken. + // This method is asynchronous and we need to wait for it to complete. + await Task.Delay(TimeSpan.FromSeconds(1), linkedCts.Token); + } + } + finally + { + if (Token is null) + { + Logger.LogError("Unable to resolve token for FCMv1."); + } + } + var installation = new DeviceInstallationDto { InstallationId = GetDeviceId(), diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Maui/Platforms/MacCatalyst/AppDelegate.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Maui/Platforms/MacCatalyst/AppDelegate.cs index 5c85782450..5f785a292f 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Maui/Platforms/MacCatalyst/AppDelegate.cs +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Maui/Platforms/MacCatalyst/AppDelegate.cs @@ -21,7 +21,7 @@ public partial class AppDelegate : MauiUIApplicationDelegate public override bool FinishedLaunching(UIApplication application, NSDictionary launchOptions) { //#if (notification == true) - NotificationService.IsNotificationSupported().ContinueWith(task => + NotificationService.IsNotificationSupported(default).ContinueWith(task => { if (task.Result) { diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Maui/Platforms/MacCatalyst/Entitlements.Debug.plist b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Maui/Platforms/MacCatalyst/Entitlements.Development.plist similarity index 100% rename from src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Maui/Platforms/MacCatalyst/Entitlements.Debug.plist rename to src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Maui/Platforms/MacCatalyst/Entitlements.Development.plist diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Maui/Platforms/MacCatalyst/Entitlements.Release.plist b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Maui/Platforms/MacCatalyst/Entitlements.Production.plist similarity index 100% rename from src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Maui/Platforms/MacCatalyst/Entitlements.Release.plist rename to src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Maui/Platforms/MacCatalyst/Entitlements.Production.plist diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Maui/Platforms/MacCatalyst/Extensions/IServiceCollectionExtensions.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Maui/Platforms/MacCatalyst/Extensions/IMacServiceCollectionExtensions.cs similarity index 90% rename from src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Maui/Platforms/MacCatalyst/Extensions/IServiceCollectionExtensions.cs rename to src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Maui/Platforms/MacCatalyst/Extensions/IMacServiceCollectionExtensions.cs index 0f88f2c4e7..31b9fae25b 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Maui/Platforms/MacCatalyst/Extensions/IServiceCollectionExtensions.cs +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Maui/Platforms/MacCatalyst/Extensions/IMacServiceCollectionExtensions.cs @@ -5,7 +5,7 @@ namespace Microsoft.Extensions.DependencyInjection; -public static partial class IServiceCollectionExtensions +public static partial class IMacServiceCollectionExtensions { public static IServiceCollection AddClientMauiProjectMacCatalystServices(this IServiceCollection services, IConfiguration configuration) { diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Maui/Platforms/MacCatalyst/Services/MacCatalystPushNotificationService.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Maui/Platforms/MacCatalyst/Services/MacCatalystPushNotificationService.cs index b3f236ae99..cb6e1f4901 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Maui/Platforms/MacCatalyst/Services/MacCatalystPushNotificationService.cs +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Maui/Platforms/MacCatalyst/Services/MacCatalystPushNotificationService.cs @@ -1,12 +1,13 @@ using UIKit; using Plugin.LocalNotification; using Boilerplate.Shared.Dtos.PushNotification; +using Microsoft.Extensions.Logging; namespace Boilerplate.Client.Maui.Platforms.MacCatalyst.Services; public partial class MacCatalystPushNotificationService : PushNotificationServiceBase { - public async override Task IsNotificationSupported() + public async override Task IsNotificationSupported(CancellationToken cancellationToken) { return await MainThread.InvokeOnMainThreadAsync(async () => { @@ -21,8 +22,29 @@ public async override Task IsNotificationSupported() public string GetDeviceId() => UIDevice.CurrentDevice.IdentifierForVendor.ToString(); - public override async Task GetDeviceInstallation() + public override async Task GetDeviceInstallation(CancellationToken cancellationToken) { + using CancellationTokenSource cts = new(TimeSpan.FromSeconds(30)); + using var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, cts.Token); + + try + { + while (string.IsNullOrEmpty(Token)) + { + // After the NotificationsSupported Task completes with a result of true, + // we use UNUserNotificationCenter.Current.Delegate. + // This method is asynchronous and we need to wait for it to complete. + await Task.Delay(TimeSpan.FromSeconds(1), linkedCts.Token); + } + } + finally + { + if (Token is null) + { + Logger.LogError("Unable to resolve token for APNS."); + } + } + var installation = new DeviceInstallationDto { InstallationId = GetDeviceId(), diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Maui/Platforms/Windows/Extensions/IServiceCollectionExtensions.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Maui/Platforms/Windows/Extensions/IWindowsServiceCollectionExtensions.cs similarity index 89% rename from src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Maui/Platforms/Windows/Extensions/IServiceCollectionExtensions.cs rename to src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Maui/Platforms/Windows/Extensions/IWindowsServiceCollectionExtensions.cs index f4809150fd..ba8e265f1c 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Maui/Platforms/Windows/Extensions/IServiceCollectionExtensions.cs +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Maui/Platforms/Windows/Extensions/IWindowsServiceCollectionExtensions.cs @@ -5,7 +5,7 @@ namespace Microsoft.Extensions.DependencyInjection; -public static partial class IServiceCollectionExtensions +public static partial class IWindowsServiceCollectionExtensions { public static IServiceCollection AddClientMauiProjectWindowsServices(this IServiceCollection services, IConfiguration configuration) { diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Maui/Platforms/Windows/Services/WindowsPushNotificationService.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Maui/Platforms/Windows/Services/WindowsPushNotificationService.cs index 0a86f421fe..555d1a1eae 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Maui/Platforms/Windows/Services/WindowsPushNotificationService.cs +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Maui/Platforms/Windows/Services/WindowsPushNotificationService.cs @@ -4,7 +4,7 @@ namespace Boilerplate.Client.Maui.Platforms.Windows.Services; public partial class WindowsPushNotificationService : PushNotificationServiceBase { - public override Task GetDeviceInstallation() => + public override Task GetDeviceInstallation(CancellationToken cancellationToken) => throw new NotImplementedException(); } diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Maui/Platforms/iOS/AppDelegate.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Maui/Platforms/iOS/AppDelegate.cs index 76cb3d0f96..63bf7f61f8 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Maui/Platforms/iOS/AppDelegate.cs +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Maui/Platforms/iOS/AppDelegate.cs @@ -21,7 +21,7 @@ public partial class AppDelegate : MauiUIApplicationDelegate public override bool FinishedLaunching(UIApplication application, NSDictionary launchOptions) { //#if (notification == true) - NotificationService.IsNotificationSupported().ContinueWith(task => + NotificationService.IsNotificationSupported(default).ContinueWith(task => { if (task.Result) { diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Maui/Platforms/iOS/Extensions/IServiceCollectionExtensions.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Maui/Platforms/iOS/Extensions/IIosServiceCollectionExtensions.cs similarity index 90% rename from src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Maui/Platforms/iOS/Extensions/IServiceCollectionExtensions.cs rename to src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Maui/Platforms/iOS/Extensions/IIosServiceCollectionExtensions.cs index 3330fbe286..af09a621ba 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Maui/Platforms/iOS/Extensions/IServiceCollectionExtensions.cs +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Maui/Platforms/iOS/Extensions/IIosServiceCollectionExtensions.cs @@ -5,7 +5,7 @@ namespace Microsoft.Extensions.DependencyInjection; -public static partial class IServiceCollectionExtensions +public static partial class IIosServiceCollectionExtensions { public static IServiceCollection AddClientMauiProjectIosServices(this IServiceCollection services, IConfiguration configuration) { diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Maui/Platforms/iOS/Services/iOSPushNotificationService.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Maui/Platforms/iOS/Services/iOSPushNotificationService.cs index 97f81178bd..388630f4c2 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Maui/Platforms/iOS/Services/iOSPushNotificationService.cs +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Maui/Platforms/iOS/Services/iOSPushNotificationService.cs @@ -1,12 +1,13 @@ using UIKit; using Plugin.LocalNotification; using Boilerplate.Shared.Dtos.PushNotification; +using Microsoft.Extensions.Logging; namespace Boilerplate.Client.Maui.Platforms.iOS.Services; public partial class iOSPushNotificationService : PushNotificationServiceBase { - public async override Task IsNotificationSupported() + public async override Task IsNotificationSupported(CancellationToken cancellationToken) { return await MainThread.InvokeOnMainThreadAsync(async () => { @@ -21,8 +22,29 @@ public async override Task IsNotificationSupported() public string GetDeviceId() => UIDevice.CurrentDevice.IdentifierForVendor.ToString(); - public override async Task GetDeviceInstallation() + public override async Task GetDeviceInstallation(CancellationToken cancellationToken) { + using CancellationTokenSource cts = new(TimeSpan.FromSeconds(30)); + using var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, cts.Token); + + try + { + while (string.IsNullOrEmpty(Token)) + { + // After the NotificationsSupported Task completes with a result of true, + // we use UNUserNotificationCenter.Current.Delegate. + // This method is asynchronous and we need to wait for it to complete. + await Task.Delay(TimeSpan.FromSeconds(1), linkedCts.Token); + } + } + finally + { + if (Token is null) + { + Logger.LogError("Unable to resolve token for APNS."); + } + } + var installation = new DeviceInstallationDto { InstallationId = GetDeviceId(), diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Maui/Services/MauiLocalHttpServer.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Maui/Services/MauiLocalHttpServer.cs index f01c8fa826..ebf213da7a 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Maui/Services/MauiLocalHttpServer.cs +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Maui/Services/MauiLocalHttpServer.cs @@ -3,6 +3,7 @@ using EmbedIO; using EmbedIO.Actions; using Boilerplate.Client.Core.Components; +using Microsoft.Extensions.Logging; namespace Boilerplate.Client.Maui.Services; @@ -10,6 +11,7 @@ public partial class MauiLocalHttpServer : ILocalHttpServer { [AutoInject] private IConfiguration configuration; [AutoInject] private IExceptionHandler exceptionHandler; + [AutoInject] private ILogger logger = default!; private WebServer? localHttpServer; @@ -28,7 +30,7 @@ public int Start(CancellationToken cancellationToken) ctx.Redirect(url); - _ = Routes.OpenUniversalLink(ctx.Request.Url.PathAndQuery, replace: true); + await Routes.OpenUniversalLink(ctx.Request.Url.PathAndQuery, replace: true); } catch (Exception exp) { @@ -36,6 +38,17 @@ public int Start(CancellationToken cancellationToken) } })); + localHttpServer.HandleHttpException(async (context, exception) => + { + using var scope = logger.BeginScope(new Dictionary() + { + { "StatusCode" , exception.StatusCode }, + { "ExceptionMessage" , exception.Message }, + { "RequestUri" , context.Request.Url }, + }); + logger.LogError("Local http server error."); + }); + _ = localHttpServer.RunAsync(cancellationToken) .ContinueWith(task => { diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Maui/Services/MauiTelemetryInitializer.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Maui/Services/MauiTelemetryInitializer.cs index a04759fdb9..3d4e08b875 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Maui/Services/MauiTelemetryInitializer.cs +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Maui/Services/MauiTelemetryInitializer.cs @@ -18,9 +18,9 @@ public void Initialize(ITelemetry telemetry) telemetry.Context.Component.Version = AppInfo.Current.VersionString; - if (AppPlatform.IsIOS) + if (AppPlatform.IsIosOnMacOS) { - telemetry.Context.GlobalProperties["IsiOSApplicationOnMac"] = AppPlatform.IsIosOnMacOS.ToString(); + telemetry.Context.GlobalProperties["IsiOSApplicationOnMac"] = "true"; } } } diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Maui/appsettings.json b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Maui/appsettings.json index d81b30bff0..d46905417e 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Maui/appsettings.json +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Maui/appsettings.json @@ -1,3 +1,13 @@ { + "Logging": { + //#if (appCenter == true) + "AppCenterLoggerProvider": { + "LogLevel": { + "Default": "Warning", + "Microsoft.EntityFrameworkCore.Database.Command": "Information" + } + }, + //#endif + }, "$schema": "https://json.schemastore.org/appsettings.json" } diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Web/Services/WebPushNotificationService.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Web/Services/WebPushNotificationService.cs index d5c8e0a0ff..4d9ace684a 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Web/Services/WebPushNotificationService.cs +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Web/Services/WebPushNotificationService.cs @@ -7,11 +7,11 @@ public partial class WebPushNotificationService : PushNotificationServiceBase [AutoInject] private readonly IJSRuntime jSRuntime = default!; [AutoInject] private readonly ClientWebSettings clientWebSettings = default!; - public async override Task GetDeviceInstallation() + public async override Task GetDeviceInstallation(CancellationToken cancellationToken) { return await jSRuntime.GetDeviceInstallation(clientWebSettings.AdsPushVapid!.PublicKey); } - public override async Task IsNotificationSupported() => clientWebSettings.WebAppRender.PwaEnabled + public override async Task IsNotificationSupported(CancellationToken cancellationToken) => clientWebSettings.WebAppRender.PwaEnabled && string.IsNullOrEmpty(clientWebSettings.AdsPushVapid?.PublicKey) is false; } diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Web/appsettings.json b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Web/appsettings.json index 3a8ed7e626..5ce72aad16 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Web/appsettings.json +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Web/appsettings.json @@ -4,10 +4,6 @@ "BlazorMode": "BlazorServer", "BlazorMode_Comment": "Default value of Client.Core/appsettings.Production.json is BlazorAuto" }, - "WindowsUpdate": { - "FilesUrl": null, - "AutoReload": true - }, //#if (notification == true) "AdsPushVapid_Comment": "https://github.com/adessoTurkey-dotNET/AdsPush", "AdsPushVapid": { diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Windows/MainWindow.xaml.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Windows/MainWindow.xaml.cs index faf15f19a0..bc390c3fa6 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Windows/MainWindow.xaml.cs +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Windows/MainWindow.xaml.cs @@ -53,8 +53,12 @@ public MainWindow() settings.IsZoomControlEnabled = false; settings.AreBrowserAcceleratorKeysEnabled = false; } + bool hasBlazorStarted = false; AppWebView.WebView.NavigationCompleted += async delegate { + if (hasBlazorStarted) + return; + hasBlazorStarted = true; await AppWebView.WebView.ExecuteScriptAsync("Blazor.start()"); }; }; diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Windows/Program.Services.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Windows/Program.Services.cs index 56267d3722..fb81ecc008 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Windows/Program.Services.cs +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Windows/Program.Services.cs @@ -48,6 +48,7 @@ public static void AddClientWindowsProjectServices(this IServiceCollection servi if (AppEnvironment.IsDev()) { loggingBuilder.AddDebug(); + loggingBuilder.AddBrowserConsoleLogger(); } loggingBuilder.AddConsole(); //#if (appCenter == true) diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Windows/Services/WindowsLocalHttpServer.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Windows/Services/WindowsLocalHttpServer.cs index 4877a32ba1..a6b5357ff8 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Windows/Services/WindowsLocalHttpServer.cs +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Windows/Services/WindowsLocalHttpServer.cs @@ -3,6 +3,7 @@ using EmbedIO; using EmbedIO.Actions; using Boilerplate.Client.Core.Components; +using Microsoft.Extensions.Logging; namespace Boilerplate.Client.Windows.Services; @@ -10,6 +11,7 @@ public partial class WindowsLocalHttpServer : ILocalHttpServer { [AutoInject] private IConfiguration configuration; [AutoInject] private IExceptionHandler exceptionHandler; + [AutoInject] private ILogger logger = default!; private WebServer? localHttpServer; @@ -28,9 +30,9 @@ public int Start(CancellationToken cancellationToken) ctx.Redirect(url); - _ = Routes.OpenUniversalLink(ctx.Request.Url.PathAndQuery, replace: true); - await App.Current.Dispatcher.InvokeAsync(() => App.Current.MainWindow.Activate()); + + await Routes.OpenUniversalLink(ctx.Request.Url.PathAndQuery, replace: true); } catch (Exception exp) { @@ -38,6 +40,17 @@ public int Start(CancellationToken cancellationToken) } })); + localHttpServer.HandleHttpException(async (context, exception) => + { + using var scope = logger.BeginScope(new Dictionary() + { + { "StatusCode" , exception.StatusCode }, + { "ExceptionMessage" , exception.Message }, + { "RequestUri" , context.Request.Url }, + }); + logger.LogError("Local http server error."); + }); + _ = localHttpServer.RunAsync(cancellationToken) .ContinueWith(task => { diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Windows/Services/WindowsPushNotificationService.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Windows/Services/WindowsPushNotificationService.cs index 562e939f32..c28d3d98a2 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Windows/Services/WindowsPushNotificationService.cs +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Windows/Services/WindowsPushNotificationService.cs @@ -4,6 +4,6 @@ namespace Boilerplate.Client.Windows.Services; public partial class WindowsPushNotificationService : PushNotificationServiceBase { - public override Task GetDeviceInstallation() => + public override Task GetDeviceInstallation(CancellationToken cancellationToken) => throw new NotImplementedException(); } diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Boilerplate.Server.Api.csproj b/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Boilerplate.Server.Api.csproj index ce557d156f..f729be6517 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Boilerplate.Server.Api.csproj +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Boilerplate.Server.Api.csproj @@ -90,7 +90,7 @@ --> Linux - True + True diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Controllers/Categories/CategoryController.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Controllers/Categories/CategoryController.cs index 1393733a7a..145e21a5b2 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Controllers/Categories/CategoryController.cs +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Controllers/Categories/CategoryController.cs @@ -52,32 +52,26 @@ public async Task Create(CategoryDto dto, CancellationToken cancell [HttpPut] public async Task Update(CategoryDto dto, CancellationToken cancellationToken) { - var entityToUpdate = await DbContext.Categories.FirstOrDefaultAsync(t => t.Id == dto.Id, cancellationToken); + var entityToUpdate = dto.Map(); - if (entityToUpdate is null) - throw new ResourceNotFoundException(Localizer[nameof(AppStrings.ProductCouldNotBeFound)]); - - dto.Patch(entityToUpdate); + DbContext.Update(entityToUpdate); await DbContext.SaveChangesAsync(cancellationToken); return entityToUpdate.Map(); } - [HttpDelete("{id}")] - public async Task Delete(Guid id, CancellationToken cancellationToken) + [HttpDelete("{id}/{concurrencyStamp}")] + public async Task Delete(Guid id, string concurrencyStamp, CancellationToken cancellationToken) { if (await DbContext.Products.AnyAsync(p => p.CategoryId == id, cancellationToken)) { throw new BadRequestException(Localizer[nameof(AppStrings.CategoryNotEmpty)]); } - DbContext.Categories.Remove(new() { Id = id }); - - var affectedRows = await DbContext.SaveChangesAsync(cancellationToken); + DbContext.Categories.Remove(new() { Id = id, ConcurrencyStamp = Convert.FromBase64String(Uri.UnescapeDataString(concurrencyStamp)) }); - if (affectedRows < 1) - throw new ResourceNotFoundException(Localizer[nameof(AppStrings.CategoryCouldNotBeFound)]); + await DbContext.SaveChangesAsync(cancellationToken); } } diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Controllers/Identity/IdentityController.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Controllers/Identity/IdentityController.cs index c2064bc51a..39a7508bf7 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Controllers/Identity/IdentityController.cs +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Controllers/Identity/IdentityController.cs @@ -182,58 +182,51 @@ private UserSession CreateUserSession(string? device) [HttpPost] public async Task> Refresh(RefreshRequestDto request) { - var refreshTokenProtector = bearerTokenOptions.Get(IdentityConstants.BearerScheme).RefreshTokenProtector; - var refreshTicket = refreshTokenProtector.Unprotect(request.RefreshToken); + UserSession? userSession = null; + User? user = null; - if (refreshTicket?.Principal.IsAuthenticated() is false) - throw new UnauthorizedException(); + try + { + var refreshTokenProtector = bearerTokenOptions.Get(IdentityConstants.BearerScheme).RefreshTokenProtector; + var refreshTicket = refreshTokenProtector.Unprotect(request.RefreshToken); - string? userId = null; User? user = null; UserSession? userSession = null; + if (refreshTicket?.Principal.IsAuthenticated() is false + || (refreshTicket!.Properties.ExpiresUtc ?? DateTimeOffset.MinValue) < DateTimeOffset.UtcNow) + throw new UnauthorizedException(); - userId = refreshTicket?.Principal?.GetUserId().ToString(); - if (string.IsNullOrEmpty(userId) is false) - { - user = await userManager.FindByIdAsync(userId) ?? throw new UnauthorizedException(); + var userId = refreshTicket!.Principal.GetUserId().ToString() ?? throw new InvalidOperationException("User id could not be found"); + var currentSessionId = Guid.Parse(refreshTicket.Principal.FindFirstValue("session-id") ?? throw new InvalidOperationException("session id could not be found")); + + user = await userManager.FindByIdAsync(userId) ?? throw new UnauthorizedException(); // User might have been deleted. + userSession = user!.Sessions.Find(s => s.SessionUniqueId == currentSessionId) ?? throw new UnauthorizedException(); // User session might have been deleted. + + if (await signInManager.ValidateSecurityStampAsync(refreshTicket.Principal) is not User _) + throw new UnauthorizedException(); + + userSession.RenewedOn = DateTimeOffset.UtcNow; + + userClaimsPrincipalFactory.SessionClaims.Add(new("session-id", currentSessionId.ToString())); + + var newPrincipal = await signInManager.CreateUserPrincipalAsync(user!); + + return SignIn(newPrincipal, authenticationScheme: IdentityConstants.BearerScheme); } - if (Guid.TryParse(refreshTicket?.Principal?.FindFirstValue("session-id"), out var currentSessionId)) + catch when (userSession is not null) { - userSession = user!.Sessions.Find(s => s.SessionUniqueId == currentSessionId); + user!.Sessions.Remove(userSession); + throw; } - - bool isExpiredSession = refreshTicket?.Properties?.ExpiresUtc is not { } expiresUtc || - DateTimeOffset.UtcNow >= expiresUtc || - await signInManager.ValidateSecurityStampAsync(refreshTicket.Principal) is not User _ || - userSession is null; - - if (userSession is not null) + finally { - if (isExpiredSession) - { - user!.Sessions.Remove(userSession); - } - else - { - userSession!.RenewedOn = DateTimeOffset.UtcNow; - } - try { - await userManager.UpdateAsync(user!); + if (user is not null) + { + await userManager.UpdateAsync(user); + } } catch (ConflictException) { /* When access_token gets expired and user navigates to the page that sends multiple requests in parallel, multiple concurrent refresh token api call happens and this will results into concurrency exception during updating session's renewed on. */ } } - - if (isExpiredSession) - { - // Return 401 if refresh token is either invalid or expired. - throw new UnauthorizedException(); - } - - userClaimsPrincipalFactory.SessionClaims.Add(new("session-id", currentSessionId.ToString())); - - var newPrincipal = await signInManager.CreateUserPrincipalAsync(user!); - - return SignIn(newPrincipal, authenticationScheme: IdentityConstants.BearerScheme); } /// diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Controllers/Products/ProductController.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Controllers/Products/ProductController.cs index ab3ca99644..da7d72247d 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Controllers/Products/ProductController.cs +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Controllers/Products/ProductController.cs @@ -52,27 +52,21 @@ public async Task Create(ProductDto dto, CancellationToken cancellat [HttpPut] public async Task Update(ProductDto dto, CancellationToken cancellationToken) { - var entityToUpdate = await DbContext.Products.FirstOrDefaultAsync(t => t.Id == dto.Id, cancellationToken); + var entityToUpdate = dto.Map(); - if (entityToUpdate is null) - throw new ResourceNotFoundException(Localizer[nameof(AppStrings.ProductCouldNotBeFound)]); - - dto.Patch(entityToUpdate); + DbContext.Update(entityToUpdate); await DbContext.SaveChangesAsync(cancellationToken); return entityToUpdate.Map(); } - [HttpDelete("{id}")] - public async Task Delete(Guid id, CancellationToken cancellationToken) + [HttpDelete("{id}/{concurrencyStamp}")] + public async Task Delete(Guid id, string concurrencyStamp, CancellationToken cancellationToken) { - DbContext.Products.Remove(new() { Id = id }); - - var affectedRows = await DbContext.SaveChangesAsync(cancellationToken); + DbContext.Products.Remove(new() { Id = id, ConcurrencyStamp = Convert.FromBase64String(Uri.UnescapeDataString(concurrencyStamp)) }); - if (affectedRows < 1) - throw new ResourceNotFoundException(Localizer[nameof(AppStrings.ProductCouldNotBeFound)]); + await DbContext.SaveChangesAsync(cancellationToken); } } diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Data/AppDbContext.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Data/AppDbContext.cs index ea1a304dd2..081ce477e8 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Data/AppDbContext.cs +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Data/AppDbContext.cs @@ -18,28 +18,32 @@ namespace Boilerplate.Server.Api.Data; public partial class AppDbContext(DbContextOptions options) : IdentityDbContext(options), IDataProtectionKeyContext { - public DbSet DataProtectionKeys { get; set; } + public DbSet DataProtectionKeys { get; set; } = default!; //#if (sample == "Todo") - public DbSet TodoItems { get; set; } + public DbSet TodoItems { get; set; } = default!; //#elif (sample == "Admin") - public DbSet Categories { get; set; } - public DbSet Products { get; set; } + public DbSet Categories { get; set; } = default!; + public DbSet Products { get; set; } = default!; //#endif //#if (notification == true) - public DbSet DeviceInstallations { get; set; } + public DbSet DeviceInstallations { get; set; } = default!; //#endif - protected override void OnModelCreating(ModelBuilder builder) + protected override void OnModelCreating(ModelBuilder modelBuilder) { - base.OnModelCreating(builder); + base.OnModelCreating(modelBuilder); - builder.ApplyConfigurationsFromAssembly(typeof(AppDbContext).Assembly); + modelBuilder.ApplyConfigurationsFromAssembly(typeof(AppDbContext).Assembly); //#if (database != "Cosmos") - ConfigureIdentityTableNames(builder); + ConfigureIdentityTableNames(modelBuilder); //#else - ConfigureContainers(builder); + ConfigureContainers(modelBuilder); + //#endif + + //#if (database != "Sqlite" && database != "Cosmos") + ConcurrencyStamp(modelBuilder); //#endif } @@ -190,4 +194,42 @@ private void ConfigureContainers(ModelBuilder builder) //#endif } //#endif + + //#if (database != "Sqlite" && database != "Cosmos") + private void ConcurrencyStamp(ModelBuilder modelBuilder) + { + //#if (IsInsideProjectTemplate == true) + if (Database.ProviderName!.EndsWith("Sqlite", StringComparison.InvariantCulture) || + Database.ProviderName!.EndsWith("Cosmos", StringComparison.InvariantCulture)) + return; + //#endif + + foreach (var entityType in modelBuilder.Model.GetEntityTypes()) + { + foreach (var property in entityType.GetProperties() + .Where(p => p.Name is "ConcurrencyStamp")) + { + var builder = new PropertyBuilder(property); + builder.IsConcurrencyToken() + .IsRowVersion(); + + //#if (IsInsideProjectTemplate == true) + if (Database.ProviderName.EndsWith("PostgreSQL", StringComparison.InvariantCulture)) + { + //#endif + //#if (database == "PostgreSQL") + if (property.ClrType == typeof(byte[])) + { + builder.HasConversion(new ValueConverter( + v => BitConverter.ToUInt32(v, 0), + v => BitConverter.GetBytes(v))); + } + //#endif + //#if (IsInsideProjectTemplate == true) + } + //#endif + } + } + } + //#endif } diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Data/Configurations/Category/CategoryConfiguration.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Data/Configurations/Category/CategoryConfiguration.cs index 9e099d9f71..aff3bd6c55 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Data/Configurations/Category/CategoryConfiguration.cs +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Data/Configurations/Category/CategoryConfiguration.cs @@ -6,12 +6,13 @@ public partial class CategoryConfiguration : IEntityTypeConfiguration { public void Configure(EntityTypeBuilder builder) { + var defaultConcurrencyStamp = new byte[] { 0, 0, 0, 0, 0, 0, 0, 0 }; builder.HasData( - new() { Id = Guid.Parse("31d78bd0-0b4f-4e87-b02f-8f66d4ab2845"), Name = "Ford", Color = "#FFCD56" }, - new() { Id = Guid.Parse("582b8c19-0709-4dae-b7a6-fa0e704dad3c"), Name = "Nissan", Color = "#FF6384" }, - new() { Id = Guid.Parse("6fae78f3-b067-40fb-a2d5-9c8dd5eb2e08"), Name = "Benz", Color = "#4BC0C0" }, - new() { Id = Guid.Parse("ecf0496f-f1e3-4d92-8fe4-0d7fa2b4ffa4"), Name = "BMW", Color = "#FF9124" }, - new() { Id = Guid.Parse("747f6d66-7524-40ca-8494-f65e85b5ee5d"), Name = "Tesla", Color = "#2B88D8" }); + new() { Id = Guid.Parse("31d78bd0-0b4f-4e87-b02f-8f66d4ab2845"), Name = "Ford", Color = "#FFCD56", ConcurrencyStamp = defaultConcurrencyStamp }, + new() { Id = Guid.Parse("582b8c19-0709-4dae-b7a6-fa0e704dad3c"), Name = "Nissan", Color = "#FF6384", ConcurrencyStamp = defaultConcurrencyStamp }, + new() { Id = Guid.Parse("6fae78f3-b067-40fb-a2d5-9c8dd5eb2e08"), Name = "Benz", Color = "#4BC0C0", ConcurrencyStamp = defaultConcurrencyStamp }, + new() { Id = Guid.Parse("ecf0496f-f1e3-4d92-8fe4-0d7fa2b4ffa4"), Name = "BMW", Color = "#FF9124", ConcurrencyStamp = defaultConcurrencyStamp }, + new() { Id = Guid.Parse("747f6d66-7524-40ca-8494-f65e85b5ee5d"), Name = "Tesla", Color = "#2B88D8", ConcurrencyStamp = defaultConcurrencyStamp }); } } diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Data/Configurations/Product/ProductConfiguration.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Data/Configurations/Product/ProductConfiguration.cs index c36a9eee2b..2f134d2d9c 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Data/Configurations/Product/ProductConfiguration.cs +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Data/Configurations/Product/ProductConfiguration.cs @@ -6,36 +6,37 @@ public partial class ProductConfiguration : IEntityTypeConfiguration { public void Configure(EntityTypeBuilder builder) { + var defaultConcurrencyStamp = new byte[] { 0, 0, 0, 0, 0, 0, 0, 0 }; DateTimeOffset baseDate = DateTimeOffset.Parse("2022-07-12", styles: DateTimeStyles.AssumeUniversal); builder.HasData( - new() { Id = Guid.Parse("9a59dda2-7b12-4cc1-9658-d2586eef91d4"), Name = "Mustang", Price = 27155, Description = "The Ford Mustang is ranked #1 in Sports Cars", CreatedOn = baseDate.AddDays(-10), CategoryId = Guid.Parse("31d78bd0-0b4f-4e87-b02f-8f66d4ab2845") }, - new() { Id = Guid.Parse("a42914e2-92da-4f0b-aab0-b9572c9671b4"), Name = "GT", Price = 500000, Description = "The Ford GT is a mid-engine two-seater sports car manufactured and marketed by American automobile manufacturer", CreatedOn = baseDate.AddDays(-15), CategoryId = Guid.Parse("31d78bd0-0b4f-4e87-b02f-8f66d4ab2845") }, - new() { Id = Guid.Parse("f75325c8-a213-470b-ab93-4677ca4caeef"), Name = "Ranger", Price = 25000, Description = "Ford Ranger is a nameplate that has been used on multiple model lines of pickup trucks sold by Ford worldwide.", CreatedOn = baseDate.AddDays(-25), CategoryId = Guid.Parse("31d78bd0-0b4f-4e87-b02f-8f66d4ab2845") }, - new() { Id = Guid.Parse("43a82ec1-aab6-445f-83af-a85028417cf7"), Name = "Raptor", Price = 53205, Description = "Raptor is a SCORE off-road trophy truck living in a asphalt world", CreatedOn = baseDate.AddDays(-30), CategoryId = Guid.Parse("31d78bd0-0b4f-4e87-b02f-8f66d4ab2845") }, - new() { Id = Guid.Parse("f01b32bb-eccd-43be-aaf3-3c788a7d7558"), Name = "Maverick", Price = 22470, Description = "The Ford Maverick is a compact pickup truck produced by Ford Motor Company.", CreatedOn = baseDate.AddDays(-35), CategoryId = Guid.Parse("31d78bd0-0b4f-4e87-b02f-8f66d4ab2845") }, - - new() { Id = Guid.Parse("d53bb159-f4f9-493a-b4dc-215fd765ca25"), Name = "Roadster", Price = 42800, Description = "A powerful convertible sports car", CreatedOn = baseDate.AddDays(-10), CategoryId = Guid.Parse("582b8c19-0709-4dae-b7a6-fa0e704dad3c") }, - new() { Id = Guid.Parse("74bb268f-18cf-45ec-9f2f-30b34b18fb3c"), Name = "Altima", Price = 24550, Description = "A perfectly adequate family sedan with sharp looks", CreatedOn = baseDate.AddDays(-15), CategoryId = Guid.Parse("582b8c19-0709-4dae-b7a6-fa0e704dad3c") }, - new() { Id = Guid.Parse("eb787e1a-7ba8-4708-924b-9f7964fa0f64"), Name = "GT-R", Price = 113540, Description = "Legendary supercar with AWD, 4 seats, a powerful V6 engine and the latest tech", CreatedOn = baseDate.AddDays(-25), CategoryId = Guid.Parse("582b8c19-0709-4dae-b7a6-fa0e704dad3c") }, - new() { Id = Guid.Parse("362a6638-0031-485d-825f-e8aeae63a334"), Name = "Juke", Price = 28100, Description = "A new smart SUV", CreatedOn = baseDate.AddDays(-35), CategoryId = Guid.Parse("582b8c19-0709-4dae-b7a6-fa0e704dad3c") }, - - new() { Id = Guid.Parse("8629931e-e26e-4885-b561-e447197d4b69"), Name = "H247", Price = 54950, Description = "", CreatedOn = baseDate.AddDays(-10), CategoryId = Guid.Parse("6fae78f3-b067-40fb-a2d5-9c8dd5eb2e08") }, - new() { Id = Guid.Parse("a1c1987d-ee6c-41ad-9647-18de4504303a"), Name = "V297", Price = 103360, Description = "", CreatedOn = baseDate.AddDays(-15), CategoryId = Guid.Parse("6fae78f3-b067-40fb-a2d5-9c8dd5eb2e08") }, - new() { Id = Guid.Parse("59eea437-bdf2-4c11-b262-06643b253288"), Name = "R50", Price = 2000000, Description = "", CreatedOn = baseDate.AddDays(-35), CategoryId = Guid.Parse("6fae78f3-b067-40fb-a2d5-9c8dd5eb2e08") }, - - new() { Id = Guid.Parse("01d223a3-182d-406a-9722-19dab083f96e"), Name = "M550i", Price = 77790, Description = "", CreatedOn = baseDate.AddDays(-10), CategoryId = Guid.Parse("ecf0496f-f1e3-4d92-8fe4-0d7fa2b4ffa4") }, - new() { Id = Guid.Parse("64a2616f-3af6-4248-86cf-4a605095a644"), Name = "540i", Price = 60945, Description = "", CreatedOn = baseDate.AddDays(-15), CategoryId = Guid.Parse("ecf0496f-f1e3-4d92-8fe4-0d7fa2b4ffa4") }, - new() { Id = Guid.Parse("ac50dc29-4b7e-4d4d-b23a-4227d91f2bb0"), Name = "530e", Price = 56545, Description = "", CreatedOn = baseDate.AddDays(-20), CategoryId = Guid.Parse("ecf0496f-f1e3-4d92-8fe4-0d7fa2b4ffa4") }, - new() { Id = Guid.Parse("fb41cc51-9abd-4b45-b0d9-ea8f565ec502"), Name = "530i", Price = 55195, Description = "", CreatedOn = baseDate.AddDays(-25), CategoryId = Guid.Parse("ecf0496f-f1e3-4d92-8fe4-0d7fa2b4ffa4") }, - new() { Id = Guid.Parse("e159b1ad-12aa-4e02-a39b-d5e4a32eaf99"), Name = "M850i", Price = 100045, Description = "", CreatedOn = baseDate.AddDays(-30), CategoryId = Guid.Parse("ecf0496f-f1e3-4d92-8fe4-0d7fa2b4ffa4") }, - new() { Id = Guid.Parse("4d9cb0f4-1f32-45d5-8c84-d7f15bc569d5"), Name = "X7", Price = 77980, Description = "", CreatedOn = baseDate.AddDays(-35), CategoryId = Guid.Parse("ecf0496f-f1e3-4d92-8fe4-0d7fa2b4ffa4") }, - new() { Id = Guid.Parse("1b22319e-0a58-471e-82b6-75cd8b9d98e1"), Name = "IX", Price = 87000, Description = "", CreatedOn = baseDate.AddDays(-40), CategoryId = Guid.Parse("ecf0496f-f1e3-4d92-8fe4-0d7fa2b4ffa4") }, - - new() { Id = Guid.Parse("96c73b9c-03df-4f70-ac8d-75c32b89881a"), Name = "Model 3", Price = 61990, Description = "rapid acceleration and dynamic handling", CreatedOn = baseDate.AddDays(-10), CategoryId = Guid.Parse("747f6d66-7524-40ca-8494-f65e85b5ee5d") }, - new() { Id = Guid.Parse("840ba759-bde9-4821-b49b-c981c082bb96"), Name = "Model S", Price = 135000, Description = "finishes near the top of our luxury electric car rankings.", CreatedOn = baseDate.AddDays(-15), CategoryId = Guid.Parse("747f6d66-7524-40ca-8494-f65e85b5ee5d") }, - new() { Id = Guid.Parse("840e113b-5074-4b1c-86bd-e9affb659412"), Name = "Model X", Price = 138890, Description = "Heart-pumping acceleration, long drive range", CreatedOn = baseDate.AddDays(-20), CategoryId = Guid.Parse("747f6d66-7524-40ca-8494-f65e85b5ee5d") }, - new() { Id = Guid.Parse("b2db9074-a0a9-4054-87e2-206b7a55c793"), Name = "Model Y", Price = 67790, Description = "extensive driving range, lots of standard safety features", CreatedOn = baseDate.AddDays(-35), CategoryId = Guid.Parse("747f6d66-7524-40ca-8494-f65e85b5ee5d") } + new() { Id = Guid.Parse("9a59dda2-7b12-4cc1-9658-d2586eef91d4"), Name = "Mustang", Price = 27155, Description = "The Ford Mustang is ranked #1 in Sports Cars", CreatedOn = baseDate.AddDays(-10), CategoryId = Guid.Parse("31d78bd0-0b4f-4e87-b02f-8f66d4ab2845"), ConcurrencyStamp = defaultConcurrencyStamp }, + new() { Id = Guid.Parse("a42914e2-92da-4f0b-aab0-b9572c9671b4"), Name = "GT", Price = 500000, Description = "The Ford GT is a mid-engine two-seater sports car manufactured and marketed by American automobile manufacturer", CreatedOn = baseDate.AddDays(-15), CategoryId = Guid.Parse("31d78bd0-0b4f-4e87-b02f-8f66d4ab2845"), ConcurrencyStamp = defaultConcurrencyStamp }, + new() { Id = Guid.Parse("f75325c8-a213-470b-ab93-4677ca4caeef"), Name = "Ranger", Price = 25000, Description = "Ford Ranger is a nameplate that has been used on multiple model lines of pickup trucks sold by Ford worldwide.", CreatedOn = baseDate.AddDays(-25), CategoryId = Guid.Parse("31d78bd0-0b4f-4e87-b02f-8f66d4ab2845"), ConcurrencyStamp = defaultConcurrencyStamp }, + new() { Id = Guid.Parse("43a82ec1-aab6-445f-83af-a85028417cf7"), Name = "Raptor", Price = 53205, Description = "Raptor is a SCORE off-road trophy truck living in a asphalt world", CreatedOn = baseDate.AddDays(-30), CategoryId = Guid.Parse("31d78bd0-0b4f-4e87-b02f-8f66d4ab2845"), ConcurrencyStamp = defaultConcurrencyStamp }, + new() { Id = Guid.Parse("f01b32bb-eccd-43be-aaf3-3c788a7d7558"), Name = "Maverick", Price = 22470, Description = "The Ford Maverick is a compact pickup truck produced by Ford Motor Company.", CreatedOn = baseDate.AddDays(-35), CategoryId = Guid.Parse("31d78bd0-0b4f-4e87-b02f-8f66d4ab2845"), ConcurrencyStamp = defaultConcurrencyStamp }, + + new() { Id = Guid.Parse("d53bb159-f4f9-493a-b4dc-215fd765ca25"), Name = "Roadster", Price = 42800, Description = "A powerful convertible sports car", CreatedOn = baseDate.AddDays(-10), CategoryId = Guid.Parse("582b8c19-0709-4dae-b7a6-fa0e704dad3c"), ConcurrencyStamp = defaultConcurrencyStamp }, + new() { Id = Guid.Parse("74bb268f-18cf-45ec-9f2f-30b34b18fb3c"), Name = "Altima", Price = 24550, Description = "A perfectly adequate family sedan with sharp looks", CreatedOn = baseDate.AddDays(-15), CategoryId = Guid.Parse("582b8c19-0709-4dae-b7a6-fa0e704dad3c"), ConcurrencyStamp = defaultConcurrencyStamp }, + new() { Id = Guid.Parse("eb787e1a-7ba8-4708-924b-9f7964fa0f64"), Name = "GT-R", Price = 113540, Description = "Legendary supercar with AWD, 4 seats, a powerful V6 engine and the latest tech", CreatedOn = baseDate.AddDays(-25), CategoryId = Guid.Parse("582b8c19-0709-4dae-b7a6-fa0e704dad3c"), ConcurrencyStamp = defaultConcurrencyStamp }, + new() { Id = Guid.Parse("362a6638-0031-485d-825f-e8aeae63a334"), Name = "Juke", Price = 28100, Description = "A new smart SUV", CreatedOn = baseDate.AddDays(-35), CategoryId = Guid.Parse("582b8c19-0709-4dae-b7a6-fa0e704dad3c"), ConcurrencyStamp = defaultConcurrencyStamp }, + + new() { Id = Guid.Parse("8629931e-e26e-4885-b561-e447197d4b69"), Name = "H247", Price = 54950, Description = "", CreatedOn = baseDate.AddDays(-10), CategoryId = Guid.Parse("6fae78f3-b067-40fb-a2d5-9c8dd5eb2e08"), ConcurrencyStamp = defaultConcurrencyStamp }, + new() { Id = Guid.Parse("a1c1987d-ee6c-41ad-9647-18de4504303a"), Name = "V297", Price = 103360, Description = "", CreatedOn = baseDate.AddDays(-15), CategoryId = Guid.Parse("6fae78f3-b067-40fb-a2d5-9c8dd5eb2e08"), ConcurrencyStamp = defaultConcurrencyStamp }, + new() { Id = Guid.Parse("59eea437-bdf2-4c11-b262-06643b253288"), Name = "R50", Price = 2000000, Description = "", CreatedOn = baseDate.AddDays(-35), CategoryId = Guid.Parse("6fae78f3-b067-40fb-a2d5-9c8dd5eb2e08"), ConcurrencyStamp = defaultConcurrencyStamp }, + + new() { Id = Guid.Parse("01d223a3-182d-406a-9722-19dab083f96e"), Name = "M550i", Price = 77790, Description = "", CreatedOn = baseDate.AddDays(-10), CategoryId = Guid.Parse("ecf0496f-f1e3-4d92-8fe4-0d7fa2b4ffa4"), ConcurrencyStamp = defaultConcurrencyStamp }, + new() { Id = Guid.Parse("64a2616f-3af6-4248-86cf-4a605095a644"), Name = "540i", Price = 60945, Description = "", CreatedOn = baseDate.AddDays(-15), CategoryId = Guid.Parse("ecf0496f-f1e3-4d92-8fe4-0d7fa2b4ffa4"), ConcurrencyStamp = defaultConcurrencyStamp }, + new() { Id = Guid.Parse("ac50dc29-4b7e-4d4d-b23a-4227d91f2bb0"), Name = "530e", Price = 56545, Description = "", CreatedOn = baseDate.AddDays(-20), CategoryId = Guid.Parse("ecf0496f-f1e3-4d92-8fe4-0d7fa2b4ffa4"), ConcurrencyStamp = defaultConcurrencyStamp }, + new() { Id = Guid.Parse("fb41cc51-9abd-4b45-b0d9-ea8f565ec502"), Name = "530i", Price = 55195, Description = "", CreatedOn = baseDate.AddDays(-25), CategoryId = Guid.Parse("ecf0496f-f1e3-4d92-8fe4-0d7fa2b4ffa4"), ConcurrencyStamp = defaultConcurrencyStamp }, + new() { Id = Guid.Parse("e159b1ad-12aa-4e02-a39b-d5e4a32eaf99"), Name = "M850i", Price = 100045, Description = "", CreatedOn = baseDate.AddDays(-30), CategoryId = Guid.Parse("ecf0496f-f1e3-4d92-8fe4-0d7fa2b4ffa4"), ConcurrencyStamp = defaultConcurrencyStamp }, + new() { Id = Guid.Parse("4d9cb0f4-1f32-45d5-8c84-d7f15bc569d5"), Name = "X7", Price = 77980, Description = "", CreatedOn = baseDate.AddDays(-35), CategoryId = Guid.Parse("ecf0496f-f1e3-4d92-8fe4-0d7fa2b4ffa4"), ConcurrencyStamp = defaultConcurrencyStamp }, + new() { Id = Guid.Parse("1b22319e-0a58-471e-82b6-75cd8b9d98e1"), Name = "IX", Price = 87000, Description = "", CreatedOn = baseDate.AddDays(-40), CategoryId = Guid.Parse("ecf0496f-f1e3-4d92-8fe4-0d7fa2b4ffa4"), ConcurrencyStamp = defaultConcurrencyStamp }, + + new() { Id = Guid.Parse("96c73b9c-03df-4f70-ac8d-75c32b89881a"), Name = "Model 3", Price = 61990, Description = "rapid acceleration and dynamic handling", CreatedOn = baseDate.AddDays(-10), CategoryId = Guid.Parse("747f6d66-7524-40ca-8494-f65e85b5ee5d"), ConcurrencyStamp = defaultConcurrencyStamp }, + new() { Id = Guid.Parse("840ba759-bde9-4821-b49b-c981c082bb96"), Name = "Model S", Price = 135000, Description = "finishes near the top of our luxury electric car rankings.", CreatedOn = baseDate.AddDays(-15), CategoryId = Guid.Parse("747f6d66-7524-40ca-8494-f65e85b5ee5d"), ConcurrencyStamp = defaultConcurrencyStamp }, + new() { Id = Guid.Parse("840e113b-5074-4b1c-86bd-e9affb659412"), Name = "Model X", Price = 138890, Description = "Heart-pumping acceleration, long drive range", CreatedOn = baseDate.AddDays(-20), CategoryId = Guid.Parse("747f6d66-7524-40ca-8494-f65e85b5ee5d"), ConcurrencyStamp = defaultConcurrencyStamp }, + new() { Id = Guid.Parse("b2db9074-a0a9-4054-87e2-206b7a55c793"), Name = "Model Y", Price = 67790, Description = "extensive driving range, lots of standard safety features", CreatedOn = baseDate.AddDays(-35), CategoryId = Guid.Parse("747f6d66-7524-40ca-8494-f65e85b5ee5d"), ConcurrencyStamp = defaultConcurrencyStamp } ); } } diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Data/Migrations/20241011170531_InitialMigration.Designer.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Data/Migrations/20241028193317_InitialMigration.Designer.cs similarity index 91% rename from src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Data/Migrations/20241011170531_InitialMigration.Designer.cs rename to src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Data/Migrations/20241028193317_InitialMigration.Designer.cs index 8cb07bfeba..25f3376906 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Data/Migrations/20241011170531_InitialMigration.Designer.cs +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Data/Migrations/20241028193317_InitialMigration.Designer.cs @@ -6,7 +6,7 @@ namespace Boilerplate.Server.Api.Data.Migrations; [DbContext(typeof(AppDbContext))] -[Migration("20241011170531_InitialMigration")] +[Migration("20241028193317_InitialMigration")] partial class InitialMigration { /// @@ -24,6 +24,10 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) b.Property("Color") .HasColumnType("TEXT"); + b.Property("ConcurrencyStamp") + .IsRequired() + .HasColumnType("BLOB"); + b.Property("Name") .IsRequired() .HasMaxLength(64) @@ -42,30 +46,35 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) { Id = new Guid("31d78bd0-0b4f-4e87-b02f-8f66d4ab2845"), Color = "#FFCD56", + ConcurrencyStamp = new byte[] { 0, 0, 0, 0, 0, 0, 0, 1 }, Name = "Ford" }, new { Id = new Guid("582b8c19-0709-4dae-b7a6-fa0e704dad3c"), Color = "#FF6384", + ConcurrencyStamp = new byte[] { 0, 0, 0, 0, 0, 0, 0, 1 }, Name = "Nissan" }, new { Id = new Guid("6fae78f3-b067-40fb-a2d5-9c8dd5eb2e08"), Color = "#4BC0C0", + ConcurrencyStamp = new byte[] { 0, 0, 0, 0, 0, 0, 0, 1 }, Name = "Benz" }, new { Id = new Guid("ecf0496f-f1e3-4d92-8fe4-0d7fa2b4ffa4"), Color = "#FF9124", + ConcurrencyStamp = new byte[] { 0, 0, 0, 0, 0, 0, 0, 1 }, Name = "BMW" }, new { Id = new Guid("747f6d66-7524-40ca-8494-f65e85b5ee5d"), Color = "#2B88D8", + ConcurrencyStamp = new byte[] { 0, 0, 0, 0, 0, 0, 0, 1 }, Name = "Tesla" }); }); @@ -78,9 +87,7 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) b.Property("ConcurrencyStamp") .IsConcurrencyToken() - .ValueGeneratedOnAddOrUpdate() - .HasColumnType("TEXT") - .HasAnnotation("Cosmos:PropertyName", "_etag"); + .HasColumnType("TEXT"); b.Property("Name") .HasMaxLength(50) @@ -117,9 +124,7 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) b.Property("ConcurrencyStamp") .IsConcurrencyToken() - .ValueGeneratedOnAddOrUpdate() - .HasColumnType("TEXT") - .HasAnnotation("Cosmos:PropertyName", "_etag"); + .HasColumnType("TEXT"); b.Property("Email") .HasMaxLength(256) @@ -223,7 +228,7 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) EmailConfirmed = true, EmailTokenRequestedOn = 1306790461440000000L, FullName = "Boilerplate test account", - Gender = 2, + Gender = 0, LockoutEnabled = true, NormalizedEmail = "TEST@BITPLATFORM.DEV", NormalizedUserName = "TEST", @@ -246,6 +251,10 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) b.Property("CategoryId") .HasColumnType("TEXT"); + b.Property("ConcurrencyStamp") + .IsRequired() + .HasColumnType("BLOB"); + b.Property("CreatedOn") .HasColumnType("INTEGER"); @@ -276,6 +285,7 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) { Id = new Guid("9a59dda2-7b12-4cc1-9658-d2586eef91d4"), CategoryId = new Guid("31d78bd0-0b4f-4e87-b02f-8f66d4ab2845"), + ConcurrencyStamp = new byte[] { 0, 0, 0, 0, 0, 0, 0, 1 }, CreatedOn = 1306466648064000000L, Description = "The Ford Mustang is ranked #1 in Sports Cars", Name = "Mustang", @@ -285,6 +295,7 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) { Id = new Guid("a42914e2-92da-4f0b-aab0-b9572c9671b4"), CategoryId = new Guid("31d78bd0-0b4f-4e87-b02f-8f66d4ab2845"), + ConcurrencyStamp = new byte[] { 0, 0, 0, 0, 0, 0, 0, 1 }, CreatedOn = 1306457800704000000L, Description = "The Ford GT is a mid-engine two-seater sports car manufactured and marketed by American automobile manufacturer", Name = "GT", @@ -294,6 +305,7 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) { Id = new Guid("f75325c8-a213-470b-ab93-4677ca4caeef"), CategoryId = new Guid("31d78bd0-0b4f-4e87-b02f-8f66d4ab2845"), + ConcurrencyStamp = new byte[] { 0, 0, 0, 0, 0, 0, 0, 1 }, CreatedOn = 1306440105984000000L, Description = "Ford Ranger is a nameplate that has been used on multiple model lines of pickup trucks sold by Ford worldwide.", Name = "Ranger", @@ -303,6 +315,7 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) { Id = new Guid("43a82ec1-aab6-445f-83af-a85028417cf7"), CategoryId = new Guid("31d78bd0-0b4f-4e87-b02f-8f66d4ab2845"), + ConcurrencyStamp = new byte[] { 0, 0, 0, 0, 0, 0, 0, 1 }, CreatedOn = 1306431258624000000L, Description = "Raptor is a SCORE off-road trophy truck living in a asphalt world", Name = "Raptor", @@ -312,6 +325,7 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) { Id = new Guid("f01b32bb-eccd-43be-aaf3-3c788a7d7558"), CategoryId = new Guid("31d78bd0-0b4f-4e87-b02f-8f66d4ab2845"), + ConcurrencyStamp = new byte[] { 0, 0, 0, 0, 0, 0, 0, 1 }, CreatedOn = 1306422411264000000L, Description = "The Ford Maverick is a compact pickup truck produced by Ford Motor Company.", Name = "Maverick", @@ -321,6 +335,7 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) { Id = new Guid("d53bb159-f4f9-493a-b4dc-215fd765ca25"), CategoryId = new Guid("582b8c19-0709-4dae-b7a6-fa0e704dad3c"), + ConcurrencyStamp = new byte[] { 0, 0, 0, 0, 0, 0, 0, 1 }, CreatedOn = 1306466648064000000L, Description = "A powerful convertible sports car", Name = "Roadster", @@ -330,6 +345,7 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) { Id = new Guid("74bb268f-18cf-45ec-9f2f-30b34b18fb3c"), CategoryId = new Guid("582b8c19-0709-4dae-b7a6-fa0e704dad3c"), + ConcurrencyStamp = new byte[] { 0, 0, 0, 0, 0, 0, 0, 1 }, CreatedOn = 1306457800704000000L, Description = "A perfectly adequate family sedan with sharp looks", Name = "Altima", @@ -339,6 +355,7 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) { Id = new Guid("eb787e1a-7ba8-4708-924b-9f7964fa0f64"), CategoryId = new Guid("582b8c19-0709-4dae-b7a6-fa0e704dad3c"), + ConcurrencyStamp = new byte[] { 0, 0, 0, 0, 0, 0, 0, 1 }, CreatedOn = 1306440105984000000L, Description = "Legendary supercar with AWD, 4 seats, a powerful V6 engine and the latest tech", Name = "GT-R", @@ -348,6 +365,7 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) { Id = new Guid("362a6638-0031-485d-825f-e8aeae63a334"), CategoryId = new Guid("582b8c19-0709-4dae-b7a6-fa0e704dad3c"), + ConcurrencyStamp = new byte[] { 0, 0, 0, 0, 0, 0, 0, 1 }, CreatedOn = 1306422411264000000L, Description = "A new smart SUV", Name = "Juke", @@ -357,6 +375,7 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) { Id = new Guid("8629931e-e26e-4885-b561-e447197d4b69"), CategoryId = new Guid("6fae78f3-b067-40fb-a2d5-9c8dd5eb2e08"), + ConcurrencyStamp = new byte[] { 0, 0, 0, 0, 0, 0, 0, 1 }, CreatedOn = 1306466648064000000L, Description = "", Name = "H247", @@ -366,6 +385,7 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) { Id = new Guid("a1c1987d-ee6c-41ad-9647-18de4504303a"), CategoryId = new Guid("6fae78f3-b067-40fb-a2d5-9c8dd5eb2e08"), + ConcurrencyStamp = new byte[] { 0, 0, 0, 0, 0, 0, 0, 1 }, CreatedOn = 1306457800704000000L, Description = "", Name = "V297", @@ -375,6 +395,7 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) { Id = new Guid("59eea437-bdf2-4c11-b262-06643b253288"), CategoryId = new Guid("6fae78f3-b067-40fb-a2d5-9c8dd5eb2e08"), + ConcurrencyStamp = new byte[] { 0, 0, 0, 0, 0, 0, 0, 1 }, CreatedOn = 1306422411264000000L, Description = "", Name = "R50", @@ -384,6 +405,7 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) { Id = new Guid("01d223a3-182d-406a-9722-19dab083f96e"), CategoryId = new Guid("ecf0496f-f1e3-4d92-8fe4-0d7fa2b4ffa4"), + ConcurrencyStamp = new byte[] { 0, 0, 0, 0, 0, 0, 0, 1 }, CreatedOn = 1306466648064000000L, Description = "", Name = "M550i", @@ -393,6 +415,7 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) { Id = new Guid("64a2616f-3af6-4248-86cf-4a605095a644"), CategoryId = new Guid("ecf0496f-f1e3-4d92-8fe4-0d7fa2b4ffa4"), + ConcurrencyStamp = new byte[] { 0, 0, 0, 0, 0, 0, 0, 1 }, CreatedOn = 1306457800704000000L, Description = "", Name = "540i", @@ -402,6 +425,7 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) { Id = new Guid("ac50dc29-4b7e-4d4d-b23a-4227d91f2bb0"), CategoryId = new Guid("ecf0496f-f1e3-4d92-8fe4-0d7fa2b4ffa4"), + ConcurrencyStamp = new byte[] { 0, 0, 0, 0, 0, 0, 0, 1 }, CreatedOn = 1306448953344000000L, Description = "", Name = "530e", @@ -411,6 +435,7 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) { Id = new Guid("fb41cc51-9abd-4b45-b0d9-ea8f565ec502"), CategoryId = new Guid("ecf0496f-f1e3-4d92-8fe4-0d7fa2b4ffa4"), + ConcurrencyStamp = new byte[] { 0, 0, 0, 0, 0, 0, 0, 1 }, CreatedOn = 1306440105984000000L, Description = "", Name = "530i", @@ -420,6 +445,7 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) { Id = new Guid("e159b1ad-12aa-4e02-a39b-d5e4a32eaf99"), CategoryId = new Guid("ecf0496f-f1e3-4d92-8fe4-0d7fa2b4ffa4"), + ConcurrencyStamp = new byte[] { 0, 0, 0, 0, 0, 0, 0, 1 }, CreatedOn = 1306431258624000000L, Description = "", Name = "M850i", @@ -429,6 +455,7 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) { Id = new Guid("4d9cb0f4-1f32-45d5-8c84-d7f15bc569d5"), CategoryId = new Guid("ecf0496f-f1e3-4d92-8fe4-0d7fa2b4ffa4"), + ConcurrencyStamp = new byte[] { 0, 0, 0, 0, 0, 0, 0, 1 }, CreatedOn = 1306422411264000000L, Description = "", Name = "X7", @@ -438,6 +465,7 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) { Id = new Guid("1b22319e-0a58-471e-82b6-75cd8b9d98e1"), CategoryId = new Guid("ecf0496f-f1e3-4d92-8fe4-0d7fa2b4ffa4"), + ConcurrencyStamp = new byte[] { 0, 0, 0, 0, 0, 0, 0, 1 }, CreatedOn = 1306413563904000000L, Description = "", Name = "IX", @@ -447,6 +475,7 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) { Id = new Guid("96c73b9c-03df-4f70-ac8d-75c32b89881a"), CategoryId = new Guid("747f6d66-7524-40ca-8494-f65e85b5ee5d"), + ConcurrencyStamp = new byte[] { 0, 0, 0, 0, 0, 0, 0, 1 }, CreatedOn = 1306466648064000000L, Description = "rapid acceleration and dynamic handling", Name = "Model 3", @@ -456,6 +485,7 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) { Id = new Guid("840ba759-bde9-4821-b49b-c981c082bb96"), CategoryId = new Guid("747f6d66-7524-40ca-8494-f65e85b5ee5d"), + ConcurrencyStamp = new byte[] { 0, 0, 0, 0, 0, 0, 0, 1 }, CreatedOn = 1306457800704000000L, Description = "finishes near the top of our luxury electric car rankings.", Name = "Model S", @@ -465,6 +495,7 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) { Id = new Guid("840e113b-5074-4b1c-86bd-e9affb659412"), CategoryId = new Guid("747f6d66-7524-40ca-8494-f65e85b5ee5d"), + ConcurrencyStamp = new byte[] { 0, 0, 0, 0, 0, 0, 0, 1 }, CreatedOn = 1306448953344000000L, Description = "Heart-pumping acceleration, long drive range", Name = "Model X", @@ -474,6 +505,7 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) { Id = new Guid("b2db9074-a0a9-4054-87e2-206b7a55c793"), CategoryId = new Guid("747f6d66-7524-40ca-8494-f65e85b5ee5d"), + ConcurrencyStamp = new byte[] { 0, 0, 0, 0, 0, 0, 0, 1 }, CreatedOn = 1306422411264000000L, Description = "extensive driving range, lots of standard safety features", Name = "Model Y", diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Data/Migrations/20241011170531_InitialMigration.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Data/Migrations/20241028193317_InitialMigration.cs similarity index 75% rename from src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Data/Migrations/20241011170531_InitialMigration.cs rename to src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Data/Migrations/20241028193317_InitialMigration.cs index dc5732e4a7..91d3adaafc 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Data/Migrations/20241011170531_InitialMigration.cs +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Data/Migrations/20241028193317_InitialMigration.cs @@ -16,7 +16,8 @@ protected override void Up(MigrationBuilder migrationBuilder) { Id = table.Column(type: "TEXT", nullable: false), Name = table.Column(type: "TEXT", maxLength: 64, nullable: false), - Color = table.Column(type: "TEXT", nullable: true) + Color = table.Column(type: "TEXT", nullable: true), + ConcurrencyStamp = table.Column(type: "BLOB", nullable: false) }, constraints: table => { @@ -44,7 +45,7 @@ protected override void Up(MigrationBuilder migrationBuilder) Id = table.Column(type: "TEXT", nullable: false), Name = table.Column(type: "TEXT", maxLength: 50, nullable: true), NormalizedName = table.Column(type: "TEXT", maxLength: 256, nullable: true), - ConcurrencyStamp = table.Column(type: "TEXT", rowVersion: true, nullable: true) + ConcurrencyStamp = table.Column(type: "TEXT", nullable: true) }, constraints: table => { @@ -73,7 +74,7 @@ protected override void Up(MigrationBuilder migrationBuilder) EmailConfirmed = table.Column(type: "INTEGER", nullable: false), PasswordHash = table.Column(type: "TEXT", nullable: true), SecurityStamp = table.Column(type: "TEXT", nullable: true), - ConcurrencyStamp = table.Column(type: "TEXT", rowVersion: true, nullable: true), + ConcurrencyStamp = table.Column(type: "TEXT", nullable: true), PhoneNumber = table.Column(type: "TEXT", nullable: true), PhoneNumberConfirmed = table.Column(type: "INTEGER", nullable: false), TwoFactorEnabled = table.Column(type: "INTEGER", nullable: false), @@ -95,7 +96,8 @@ protected override void Up(MigrationBuilder migrationBuilder) Price = table.Column(type: "TEXT", nullable: false), Description = table.Column(type: "TEXT", maxLength: 512, nullable: true), CreatedOn = table.Column(type: "INTEGER", nullable: false), - CategoryId = table.Column(type: "TEXT", nullable: false) + CategoryId = table.Column(type: "TEXT", nullable: false), + ConcurrencyStamp = table.Column(type: "BLOB", nullable: false) }, constraints: table => { @@ -261,49 +263,49 @@ protected override void Up(MigrationBuilder migrationBuilder) migrationBuilder.InsertData( table: "Categories", - columns: new[] { "Id", "Color", "Name" }, + columns: new[] { "Id", "Color", "ConcurrencyStamp", "Name" }, values: new object[,] { - { new Guid("31d78bd0-0b4f-4e87-b02f-8f66d4ab2845"), "#FFCD56", "Ford" }, - { new Guid("582b8c19-0709-4dae-b7a6-fa0e704dad3c"), "#FF6384", "Nissan" }, - { new Guid("6fae78f3-b067-40fb-a2d5-9c8dd5eb2e08"), "#4BC0C0", "Benz" }, - { new Guid("747f6d66-7524-40ca-8494-f65e85b5ee5d"), "#2B88D8", "Tesla" }, - { new Guid("ecf0496f-f1e3-4d92-8fe4-0d7fa2b4ffa4"), "#FF9124", "BMW" } + { new Guid("31d78bd0-0b4f-4e87-b02f-8f66d4ab2845"), "#FFCD56", new byte[] { 0, 0, 0, 0, 0, 0, 0, 1 }, "Ford" }, + { new Guid("582b8c19-0709-4dae-b7a6-fa0e704dad3c"), "#FF6384", new byte[] { 0, 0, 0, 0, 0, 0, 0, 1 }, "Nissan" }, + { new Guid("6fae78f3-b067-40fb-a2d5-9c8dd5eb2e08"), "#4BC0C0", new byte[] { 0, 0, 0, 0, 0, 0, 0, 1 }, "Benz" }, + { new Guid("747f6d66-7524-40ca-8494-f65e85b5ee5d"), "#2B88D8", new byte[] { 0, 0, 0, 0, 0, 0, 0, 1 }, "Tesla" }, + { new Guid("ecf0496f-f1e3-4d92-8fe4-0d7fa2b4ffa4"), "#FF9124", new byte[] { 0, 0, 0, 0, 0, 0, 0, 1 }, "BMW" } }); migrationBuilder.InsertData( table: "Users", - columns: new[] { "Id", "AccessFailedCount", "BirthDate", "Email", "EmailConfirmed", "EmailTokenRequestedOn", "FullName", "Gender", "LockoutEnabled", "LockoutEnd", "NormalizedEmail", "NormalizedUserName", "OtpRequestedOn", "PasswordHash", "PhoneNumber", "PhoneNumberConfirmed", "PhoneNumberTokenRequestedOn", "ProfileImageName", "ResetPasswordTokenRequestedOn", "SecurityStamp", "Sessions", "TwoFactorEnabled", "TwoFactorTokenRequestedOn", "UserName" }, - values: new object[] { new Guid("8ff71671-a1d6-4f97-abb9-d87d7b47d6e7"), 0, 1306790461440000000L, "test@bitplatform.dev", true, 1306790461440000000L, "Boilerplate test account", 2, true, null, "TEST@BITPLATFORM.DEV", "TEST", null, "AQAAAAIAAYagAAAAEP0v3wxkdWtMkHA3Pp5/JfS+42/Qto9G05p2mta6dncSK37hPxEHa3PGE4aqN30Aag==", "+31684207362", true, null, null, null, "959ff4a9-4b07-4cc1-8141-c5fc033daf83", "[]", false, null, "test" }); + columns: new[] { "Id", "AccessFailedCount", "BirthDate", "ConcurrencyStamp", "Email", "EmailConfirmed", "EmailTokenRequestedOn", "FullName", "Gender", "LockoutEnabled", "LockoutEnd", "NormalizedEmail", "NormalizedUserName", "OtpRequestedOn", "PasswordHash", "PhoneNumber", "PhoneNumberConfirmed", "PhoneNumberTokenRequestedOn", "ProfileImageName", "ResetPasswordTokenRequestedOn", "SecurityStamp", "Sessions", "TwoFactorEnabled", "TwoFactorTokenRequestedOn", "UserName" }, + values: new object[] { new Guid("8ff71671-a1d6-4f97-abb9-d87d7b47d6e7"), 0, 1306790461440000000L, "315e1a26-5b3a-4544-8e91-2760cd28e231", "test@bitplatform.dev", true, 1306790461440000000L, "Boilerplate test account", 0, true, null, "TEST@BITPLATFORM.DEV", "TEST", null, "AQAAAAIAAYagAAAAEP0v3wxkdWtMkHA3Pp5/JfS+42/Qto9G05p2mta6dncSK37hPxEHa3PGE4aqN30Aag==", "+31684207362", true, null, null, null, "959ff4a9-4b07-4cc1-8141-c5fc033daf83", "[]", false, null, "test" }); migrationBuilder.InsertData( table: "Products", - columns: new[] { "Id", "CategoryId", "CreatedOn", "Description", "Name", "Price" }, + columns: new[] { "Id", "CategoryId", "ConcurrencyStamp", "CreatedOn", "Description", "Name", "Price" }, values: new object[,] { - { new Guid("01d223a3-182d-406a-9722-19dab083f96e"), new Guid("ecf0496f-f1e3-4d92-8fe4-0d7fa2b4ffa4"), 1306466648064000000L, "", "M550i", 77790m }, - { new Guid("1b22319e-0a58-471e-82b6-75cd8b9d98e1"), new Guid("ecf0496f-f1e3-4d92-8fe4-0d7fa2b4ffa4"), 1306413563904000000L, "", "IX", 87000m }, - { new Guid("362a6638-0031-485d-825f-e8aeae63a334"), new Guid("582b8c19-0709-4dae-b7a6-fa0e704dad3c"), 1306422411264000000L, "A new smart SUV", "Juke", 28100m }, - { new Guid("43a82ec1-aab6-445f-83af-a85028417cf7"), new Guid("31d78bd0-0b4f-4e87-b02f-8f66d4ab2845"), 1306431258624000000L, "Raptor is a SCORE off-road trophy truck living in a asphalt world", "Raptor", 53205m }, - { new Guid("4d9cb0f4-1f32-45d5-8c84-d7f15bc569d5"), new Guid("ecf0496f-f1e3-4d92-8fe4-0d7fa2b4ffa4"), 1306422411264000000L, "", "X7", 77980m }, - { new Guid("59eea437-bdf2-4c11-b262-06643b253288"), new Guid("6fae78f3-b067-40fb-a2d5-9c8dd5eb2e08"), 1306422411264000000L, "", "R50", 2000000m }, - { new Guid("64a2616f-3af6-4248-86cf-4a605095a644"), new Guid("ecf0496f-f1e3-4d92-8fe4-0d7fa2b4ffa4"), 1306457800704000000L, "", "540i", 60945m }, - { new Guid("74bb268f-18cf-45ec-9f2f-30b34b18fb3c"), new Guid("582b8c19-0709-4dae-b7a6-fa0e704dad3c"), 1306457800704000000L, "A perfectly adequate family sedan with sharp looks", "Altima", 24550m }, - { new Guid("840ba759-bde9-4821-b49b-c981c082bb96"), new Guid("747f6d66-7524-40ca-8494-f65e85b5ee5d"), 1306457800704000000L, "finishes near the top of our luxury electric car rankings.", "Model S", 135000m }, - { new Guid("840e113b-5074-4b1c-86bd-e9affb659412"), new Guid("747f6d66-7524-40ca-8494-f65e85b5ee5d"), 1306448953344000000L, "Heart-pumping acceleration, long drive range", "Model X", 138890m }, - { new Guid("8629931e-e26e-4885-b561-e447197d4b69"), new Guid("6fae78f3-b067-40fb-a2d5-9c8dd5eb2e08"), 1306466648064000000L, "", "H247", 54950m }, - { new Guid("96c73b9c-03df-4f70-ac8d-75c32b89881a"), new Guid("747f6d66-7524-40ca-8494-f65e85b5ee5d"), 1306466648064000000L, "rapid acceleration and dynamic handling", "Model 3", 61990m }, - { new Guid("9a59dda2-7b12-4cc1-9658-d2586eef91d4"), new Guid("31d78bd0-0b4f-4e87-b02f-8f66d4ab2845"), 1306466648064000000L, "The Ford Mustang is ranked #1 in Sports Cars", "Mustang", 27155m }, - { new Guid("a1c1987d-ee6c-41ad-9647-18de4504303a"), new Guid("6fae78f3-b067-40fb-a2d5-9c8dd5eb2e08"), 1306457800704000000L, "", "V297", 103360m }, - { new Guid("a42914e2-92da-4f0b-aab0-b9572c9671b4"), new Guid("31d78bd0-0b4f-4e87-b02f-8f66d4ab2845"), 1306457800704000000L, "The Ford GT is a mid-engine two-seater sports car manufactured and marketed by American automobile manufacturer", "GT", 500000m }, - { new Guid("ac50dc29-4b7e-4d4d-b23a-4227d91f2bb0"), new Guid("ecf0496f-f1e3-4d92-8fe4-0d7fa2b4ffa4"), 1306448953344000000L, "", "530e", 56545m }, - { new Guid("b2db9074-a0a9-4054-87e2-206b7a55c793"), new Guid("747f6d66-7524-40ca-8494-f65e85b5ee5d"), 1306422411264000000L, "extensive driving range, lots of standard safety features", "Model Y", 67790m }, - { new Guid("d53bb159-f4f9-493a-b4dc-215fd765ca25"), new Guid("582b8c19-0709-4dae-b7a6-fa0e704dad3c"), 1306466648064000000L, "A powerful convertible sports car", "Roadster", 42800m }, - { new Guid("e159b1ad-12aa-4e02-a39b-d5e4a32eaf99"), new Guid("ecf0496f-f1e3-4d92-8fe4-0d7fa2b4ffa4"), 1306431258624000000L, "", "M850i", 100045m }, - { new Guid("eb787e1a-7ba8-4708-924b-9f7964fa0f64"), new Guid("582b8c19-0709-4dae-b7a6-fa0e704dad3c"), 1306440105984000000L, "Legendary supercar with AWD, 4 seats, a powerful V6 engine and the latest tech", "GT-R", 113540m }, - { new Guid("f01b32bb-eccd-43be-aaf3-3c788a7d7558"), new Guid("31d78bd0-0b4f-4e87-b02f-8f66d4ab2845"), 1306422411264000000L, "The Ford Maverick is a compact pickup truck produced by Ford Motor Company.", "Maverick", 22470m }, - { new Guid("f75325c8-a213-470b-ab93-4677ca4caeef"), new Guid("31d78bd0-0b4f-4e87-b02f-8f66d4ab2845"), 1306440105984000000L, "Ford Ranger is a nameplate that has been used on multiple model lines of pickup trucks sold by Ford worldwide.", "Ranger", 25000m }, - { new Guid("fb41cc51-9abd-4b45-b0d9-ea8f565ec502"), new Guid("ecf0496f-f1e3-4d92-8fe4-0d7fa2b4ffa4"), 1306440105984000000L, "", "530i", 55195m } + { new Guid("01d223a3-182d-406a-9722-19dab083f96e"), new Guid("ecf0496f-f1e3-4d92-8fe4-0d7fa2b4ffa4"), new byte[] { 0, 0, 0, 0, 0, 0, 0, 1 }, 1306466648064000000L, "", "M550i", 77790m }, + { new Guid("1b22319e-0a58-471e-82b6-75cd8b9d98e1"), new Guid("ecf0496f-f1e3-4d92-8fe4-0d7fa2b4ffa4"), new byte[] { 0, 0, 0, 0, 0, 0, 0, 1 }, 1306413563904000000L, "", "IX", 87000m }, + { new Guid("362a6638-0031-485d-825f-e8aeae63a334"), new Guid("582b8c19-0709-4dae-b7a6-fa0e704dad3c"), new byte[] { 0, 0, 0, 0, 0, 0, 0, 1 }, 1306422411264000000L, "A new smart SUV", "Juke", 28100m }, + { new Guid("43a82ec1-aab6-445f-83af-a85028417cf7"), new Guid("31d78bd0-0b4f-4e87-b02f-8f66d4ab2845"), new byte[] { 0, 0, 0, 0, 0, 0, 0, 1 }, 1306431258624000000L, "Raptor is a SCORE off-road trophy truck living in a asphalt world", "Raptor", 53205m }, + { new Guid("4d9cb0f4-1f32-45d5-8c84-d7f15bc569d5"), new Guid("ecf0496f-f1e3-4d92-8fe4-0d7fa2b4ffa4"), new byte[] { 0, 0, 0, 0, 0, 0, 0, 1 }, 1306422411264000000L, "", "X7", 77980m }, + { new Guid("59eea437-bdf2-4c11-b262-06643b253288"), new Guid("6fae78f3-b067-40fb-a2d5-9c8dd5eb2e08"), new byte[] { 0, 0, 0, 0, 0, 0, 0, 1 }, 1306422411264000000L, "", "R50", 2000000m }, + { new Guid("64a2616f-3af6-4248-86cf-4a605095a644"), new Guid("ecf0496f-f1e3-4d92-8fe4-0d7fa2b4ffa4"), new byte[] { 0, 0, 0, 0, 0, 0, 0, 1 }, 1306457800704000000L, "", "540i", 60945m }, + { new Guid("74bb268f-18cf-45ec-9f2f-30b34b18fb3c"), new Guid("582b8c19-0709-4dae-b7a6-fa0e704dad3c"), new byte[] { 0, 0, 0, 0, 0, 0, 0, 1 }, 1306457800704000000L, "A perfectly adequate family sedan with sharp looks", "Altima", 24550m }, + { new Guid("840ba759-bde9-4821-b49b-c981c082bb96"), new Guid("747f6d66-7524-40ca-8494-f65e85b5ee5d"), new byte[] { 0, 0, 0, 0, 0, 0, 0, 1 }, 1306457800704000000L, "finishes near the top of our luxury electric car rankings.", "Model S", 135000m }, + { new Guid("840e113b-5074-4b1c-86bd-e9affb659412"), new Guid("747f6d66-7524-40ca-8494-f65e85b5ee5d"), new byte[] { 0, 0, 0, 0, 0, 0, 0, 1 }, 1306448953344000000L, "Heart-pumping acceleration, long drive range", "Model X", 138890m }, + { new Guid("8629931e-e26e-4885-b561-e447197d4b69"), new Guid("6fae78f3-b067-40fb-a2d5-9c8dd5eb2e08"), new byte[] { 0, 0, 0, 0, 0, 0, 0, 1 }, 1306466648064000000L, "", "H247", 54950m }, + { new Guid("96c73b9c-03df-4f70-ac8d-75c32b89881a"), new Guid("747f6d66-7524-40ca-8494-f65e85b5ee5d"), new byte[] { 0, 0, 0, 0, 0, 0, 0, 1 }, 1306466648064000000L, "rapid acceleration and dynamic handling", "Model 3", 61990m }, + { new Guid("9a59dda2-7b12-4cc1-9658-d2586eef91d4"), new Guid("31d78bd0-0b4f-4e87-b02f-8f66d4ab2845"), new byte[] { 0, 0, 0, 0, 0, 0, 0, 1 }, 1306466648064000000L, "The Ford Mustang is ranked #1 in Sports Cars", "Mustang", 27155m }, + { new Guid("a1c1987d-ee6c-41ad-9647-18de4504303a"), new Guid("6fae78f3-b067-40fb-a2d5-9c8dd5eb2e08"), new byte[] { 0, 0, 0, 0, 0, 0, 0, 1 }, 1306457800704000000L, "", "V297", 103360m }, + { new Guid("a42914e2-92da-4f0b-aab0-b9572c9671b4"), new Guid("31d78bd0-0b4f-4e87-b02f-8f66d4ab2845"), new byte[] { 0, 0, 0, 0, 0, 0, 0, 1 }, 1306457800704000000L, "The Ford GT is a mid-engine two-seater sports car manufactured and marketed by American automobile manufacturer", "GT", 500000m }, + { new Guid("ac50dc29-4b7e-4d4d-b23a-4227d91f2bb0"), new Guid("ecf0496f-f1e3-4d92-8fe4-0d7fa2b4ffa4"), new byte[] { 0, 0, 0, 0, 0, 0, 0, 1 }, 1306448953344000000L, "", "530e", 56545m }, + { new Guid("b2db9074-a0a9-4054-87e2-206b7a55c793"), new Guid("747f6d66-7524-40ca-8494-f65e85b5ee5d"), new byte[] { 0, 0, 0, 0, 0, 0, 0, 1 }, 1306422411264000000L, "extensive driving range, lots of standard safety features", "Model Y", 67790m }, + { new Guid("d53bb159-f4f9-493a-b4dc-215fd765ca25"), new Guid("582b8c19-0709-4dae-b7a6-fa0e704dad3c"), new byte[] { 0, 0, 0, 0, 0, 0, 0, 1 }, 1306466648064000000L, "A powerful convertible sports car", "Roadster", 42800m }, + { new Guid("e159b1ad-12aa-4e02-a39b-d5e4a32eaf99"), new Guid("ecf0496f-f1e3-4d92-8fe4-0d7fa2b4ffa4"), new byte[] { 0, 0, 0, 0, 0, 0, 0, 1 }, 1306431258624000000L, "", "M850i", 100045m }, + { new Guid("eb787e1a-7ba8-4708-924b-9f7964fa0f64"), new Guid("582b8c19-0709-4dae-b7a6-fa0e704dad3c"), new byte[] { 0, 0, 0, 0, 0, 0, 0, 1 }, 1306440105984000000L, "Legendary supercar with AWD, 4 seats, a powerful V6 engine and the latest tech", "GT-R", 113540m }, + { new Guid("f01b32bb-eccd-43be-aaf3-3c788a7d7558"), new Guid("31d78bd0-0b4f-4e87-b02f-8f66d4ab2845"), new byte[] { 0, 0, 0, 0, 0, 0, 0, 1 }, 1306422411264000000L, "The Ford Maverick is a compact pickup truck produced by Ford Motor Company.", "Maverick", 22470m }, + { new Guid("f75325c8-a213-470b-ab93-4677ca4caeef"), new Guid("31d78bd0-0b4f-4e87-b02f-8f66d4ab2845"), new byte[] { 0, 0, 0, 0, 0, 0, 0, 1 }, 1306440105984000000L, "Ford Ranger is a nameplate that has been used on multiple model lines of pickup trucks sold by Ford worldwide.", "Ranger", 25000m }, + { new Guid("fb41cc51-9abd-4b45-b0d9-ea8f565ec502"), new Guid("ecf0496f-f1e3-4d92-8fe4-0d7fa2b4ffa4"), new byte[] { 0, 0, 0, 0, 0, 0, 0, 1 }, 1306440105984000000L, "", "530i", 55195m } }); migrationBuilder.CreateIndex( diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Data/Migrations/AppDbContextModelSnapshot.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Data/Migrations/AppDbContextModelSnapshot.cs index 3d0684e653..eebe4f3f8e 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Data/Migrations/AppDbContextModelSnapshot.cs +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Data/Migrations/AppDbContextModelSnapshot.cs @@ -22,6 +22,10 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("Color") .HasColumnType("TEXT"); + b.Property("ConcurrencyStamp") + .IsRequired() + .HasColumnType("BLOB"); + b.Property("Name") .IsRequired() .HasMaxLength(64) @@ -40,30 +44,35 @@ protected override void BuildModel(ModelBuilder modelBuilder) { Id = new Guid("31d78bd0-0b4f-4e87-b02f-8f66d4ab2845"), Color = "#FFCD56", + ConcurrencyStamp = new byte[] { 0, 0, 0, 0, 0, 0, 0, 1 }, Name = "Ford" }, new { Id = new Guid("582b8c19-0709-4dae-b7a6-fa0e704dad3c"), Color = "#FF6384", + ConcurrencyStamp = new byte[] { 0, 0, 0, 0, 0, 0, 0, 1 }, Name = "Nissan" }, new { Id = new Guid("6fae78f3-b067-40fb-a2d5-9c8dd5eb2e08"), Color = "#4BC0C0", + ConcurrencyStamp = new byte[] { 0, 0, 0, 0, 0, 0, 0, 1 }, Name = "Benz" }, new { Id = new Guid("ecf0496f-f1e3-4d92-8fe4-0d7fa2b4ffa4"), Color = "#FF9124", + ConcurrencyStamp = new byte[] { 0, 0, 0, 0, 0, 0, 0, 1 }, Name = "BMW" }, new { Id = new Guid("747f6d66-7524-40ca-8494-f65e85b5ee5d"), Color = "#2B88D8", + ConcurrencyStamp = new byte[] { 0, 0, 0, 0, 0, 0, 0, 1 }, Name = "Tesla" }); }); @@ -76,9 +85,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("ConcurrencyStamp") .IsConcurrencyToken() - .ValueGeneratedOnAddOrUpdate() - .HasColumnType("TEXT") - .HasAnnotation("Cosmos:PropertyName", "_etag"); + .HasColumnType("TEXT"); b.Property("Name") .HasMaxLength(50) @@ -115,9 +122,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("ConcurrencyStamp") .IsConcurrencyToken() - .ValueGeneratedOnAddOrUpdate() - .HasColumnType("TEXT") - .HasAnnotation("Cosmos:PropertyName", "_etag"); + .HasColumnType("TEXT"); b.Property("Email") .HasMaxLength(256) @@ -221,7 +226,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) EmailConfirmed = true, EmailTokenRequestedOn = 1306790461440000000L, FullName = "Boilerplate test account", - Gender = 2, + Gender = 0, LockoutEnabled = true, NormalizedEmail = "TEST@BITPLATFORM.DEV", NormalizedUserName = "TEST", @@ -244,6 +249,10 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("CategoryId") .HasColumnType("TEXT"); + b.Property("ConcurrencyStamp") + .IsRequired() + .HasColumnType("BLOB"); + b.Property("CreatedOn") .HasColumnType("INTEGER"); @@ -274,6 +283,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) { Id = new Guid("9a59dda2-7b12-4cc1-9658-d2586eef91d4"), CategoryId = new Guid("31d78bd0-0b4f-4e87-b02f-8f66d4ab2845"), + ConcurrencyStamp = new byte[] { 0, 0, 0, 0, 0, 0, 0, 1 }, CreatedOn = 1306466648064000000L, Description = "The Ford Mustang is ranked #1 in Sports Cars", Name = "Mustang", @@ -283,6 +293,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) { Id = new Guid("a42914e2-92da-4f0b-aab0-b9572c9671b4"), CategoryId = new Guid("31d78bd0-0b4f-4e87-b02f-8f66d4ab2845"), + ConcurrencyStamp = new byte[] { 0, 0, 0, 0, 0, 0, 0, 1 }, CreatedOn = 1306457800704000000L, Description = "The Ford GT is a mid-engine two-seater sports car manufactured and marketed by American automobile manufacturer", Name = "GT", @@ -292,6 +303,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) { Id = new Guid("f75325c8-a213-470b-ab93-4677ca4caeef"), CategoryId = new Guid("31d78bd0-0b4f-4e87-b02f-8f66d4ab2845"), + ConcurrencyStamp = new byte[] { 0, 0, 0, 0, 0, 0, 0, 1 }, CreatedOn = 1306440105984000000L, Description = "Ford Ranger is a nameplate that has been used on multiple model lines of pickup trucks sold by Ford worldwide.", Name = "Ranger", @@ -301,6 +313,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) { Id = new Guid("43a82ec1-aab6-445f-83af-a85028417cf7"), CategoryId = new Guid("31d78bd0-0b4f-4e87-b02f-8f66d4ab2845"), + ConcurrencyStamp = new byte[] { 0, 0, 0, 0, 0, 0, 0, 1 }, CreatedOn = 1306431258624000000L, Description = "Raptor is a SCORE off-road trophy truck living in a asphalt world", Name = "Raptor", @@ -310,6 +323,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) { Id = new Guid("f01b32bb-eccd-43be-aaf3-3c788a7d7558"), CategoryId = new Guid("31d78bd0-0b4f-4e87-b02f-8f66d4ab2845"), + ConcurrencyStamp = new byte[] { 0, 0, 0, 0, 0, 0, 0, 1 }, CreatedOn = 1306422411264000000L, Description = "The Ford Maverick is a compact pickup truck produced by Ford Motor Company.", Name = "Maverick", @@ -319,6 +333,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) { Id = new Guid("d53bb159-f4f9-493a-b4dc-215fd765ca25"), CategoryId = new Guid("582b8c19-0709-4dae-b7a6-fa0e704dad3c"), + ConcurrencyStamp = new byte[] { 0, 0, 0, 0, 0, 0, 0, 1 }, CreatedOn = 1306466648064000000L, Description = "A powerful convertible sports car", Name = "Roadster", @@ -328,6 +343,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) { Id = new Guid("74bb268f-18cf-45ec-9f2f-30b34b18fb3c"), CategoryId = new Guid("582b8c19-0709-4dae-b7a6-fa0e704dad3c"), + ConcurrencyStamp = new byte[] { 0, 0, 0, 0, 0, 0, 0, 1 }, CreatedOn = 1306457800704000000L, Description = "A perfectly adequate family sedan with sharp looks", Name = "Altima", @@ -337,6 +353,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) { Id = new Guid("eb787e1a-7ba8-4708-924b-9f7964fa0f64"), CategoryId = new Guid("582b8c19-0709-4dae-b7a6-fa0e704dad3c"), + ConcurrencyStamp = new byte[] { 0, 0, 0, 0, 0, 0, 0, 1 }, CreatedOn = 1306440105984000000L, Description = "Legendary supercar with AWD, 4 seats, a powerful V6 engine and the latest tech", Name = "GT-R", @@ -346,6 +363,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) { Id = new Guid("362a6638-0031-485d-825f-e8aeae63a334"), CategoryId = new Guid("582b8c19-0709-4dae-b7a6-fa0e704dad3c"), + ConcurrencyStamp = new byte[] { 0, 0, 0, 0, 0, 0, 0, 1 }, CreatedOn = 1306422411264000000L, Description = "A new smart SUV", Name = "Juke", @@ -355,6 +373,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) { Id = new Guid("8629931e-e26e-4885-b561-e447197d4b69"), CategoryId = new Guid("6fae78f3-b067-40fb-a2d5-9c8dd5eb2e08"), + ConcurrencyStamp = new byte[] { 0, 0, 0, 0, 0, 0, 0, 1 }, CreatedOn = 1306466648064000000L, Description = "", Name = "H247", @@ -364,6 +383,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) { Id = new Guid("a1c1987d-ee6c-41ad-9647-18de4504303a"), CategoryId = new Guid("6fae78f3-b067-40fb-a2d5-9c8dd5eb2e08"), + ConcurrencyStamp = new byte[] { 0, 0, 0, 0, 0, 0, 0, 1 }, CreatedOn = 1306457800704000000L, Description = "", Name = "V297", @@ -373,6 +393,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) { Id = new Guid("59eea437-bdf2-4c11-b262-06643b253288"), CategoryId = new Guid("6fae78f3-b067-40fb-a2d5-9c8dd5eb2e08"), + ConcurrencyStamp = new byte[] { 0, 0, 0, 0, 0, 0, 0, 1 }, CreatedOn = 1306422411264000000L, Description = "", Name = "R50", @@ -382,6 +403,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) { Id = new Guid("01d223a3-182d-406a-9722-19dab083f96e"), CategoryId = new Guid("ecf0496f-f1e3-4d92-8fe4-0d7fa2b4ffa4"), + ConcurrencyStamp = new byte[] { 0, 0, 0, 0, 0, 0, 0, 1 }, CreatedOn = 1306466648064000000L, Description = "", Name = "M550i", @@ -391,6 +413,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) { Id = new Guid("64a2616f-3af6-4248-86cf-4a605095a644"), CategoryId = new Guid("ecf0496f-f1e3-4d92-8fe4-0d7fa2b4ffa4"), + ConcurrencyStamp = new byte[] { 0, 0, 0, 0, 0, 0, 0, 1 }, CreatedOn = 1306457800704000000L, Description = "", Name = "540i", @@ -400,6 +423,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) { Id = new Guid("ac50dc29-4b7e-4d4d-b23a-4227d91f2bb0"), CategoryId = new Guid("ecf0496f-f1e3-4d92-8fe4-0d7fa2b4ffa4"), + ConcurrencyStamp = new byte[] { 0, 0, 0, 0, 0, 0, 0, 1 }, CreatedOn = 1306448953344000000L, Description = "", Name = "530e", @@ -409,6 +433,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) { Id = new Guid("fb41cc51-9abd-4b45-b0d9-ea8f565ec502"), CategoryId = new Guid("ecf0496f-f1e3-4d92-8fe4-0d7fa2b4ffa4"), + ConcurrencyStamp = new byte[] { 0, 0, 0, 0, 0, 0, 0, 1 }, CreatedOn = 1306440105984000000L, Description = "", Name = "530i", @@ -418,6 +443,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) { Id = new Guid("e159b1ad-12aa-4e02-a39b-d5e4a32eaf99"), CategoryId = new Guid("ecf0496f-f1e3-4d92-8fe4-0d7fa2b4ffa4"), + ConcurrencyStamp = new byte[] { 0, 0, 0, 0, 0, 0, 0, 1 }, CreatedOn = 1306431258624000000L, Description = "", Name = "M850i", @@ -427,6 +453,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) { Id = new Guid("4d9cb0f4-1f32-45d5-8c84-d7f15bc569d5"), CategoryId = new Guid("ecf0496f-f1e3-4d92-8fe4-0d7fa2b4ffa4"), + ConcurrencyStamp = new byte[] { 0, 0, 0, 0, 0, 0, 0, 1 }, CreatedOn = 1306422411264000000L, Description = "", Name = "X7", @@ -436,6 +463,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) { Id = new Guid("1b22319e-0a58-471e-82b6-75cd8b9d98e1"), CategoryId = new Guid("ecf0496f-f1e3-4d92-8fe4-0d7fa2b4ffa4"), + ConcurrencyStamp = new byte[] { 0, 0, 0, 0, 0, 0, 0, 1 }, CreatedOn = 1306413563904000000L, Description = "", Name = "IX", @@ -445,6 +473,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) { Id = new Guid("96c73b9c-03df-4f70-ac8d-75c32b89881a"), CategoryId = new Guid("747f6d66-7524-40ca-8494-f65e85b5ee5d"), + ConcurrencyStamp = new byte[] { 0, 0, 0, 0, 0, 0, 0, 1 }, CreatedOn = 1306466648064000000L, Description = "rapid acceleration and dynamic handling", Name = "Model 3", @@ -454,6 +483,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) { Id = new Guid("840ba759-bde9-4821-b49b-c981c082bb96"), CategoryId = new Guid("747f6d66-7524-40ca-8494-f65e85b5ee5d"), + ConcurrencyStamp = new byte[] { 0, 0, 0, 0, 0, 0, 0, 1 }, CreatedOn = 1306457800704000000L, Description = "finishes near the top of our luxury electric car rankings.", Name = "Model S", @@ -463,6 +493,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) { Id = new Guid("840e113b-5074-4b1c-86bd-e9affb659412"), CategoryId = new Guid("747f6d66-7524-40ca-8494-f65e85b5ee5d"), + ConcurrencyStamp = new byte[] { 0, 0, 0, 0, 0, 0, 0, 1 }, CreatedOn = 1306448953344000000L, Description = "Heart-pumping acceleration, long drive range", Name = "Model X", @@ -472,6 +503,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) { Id = new Guid("b2db9074-a0a9-4054-87e2-206b7a55c793"), CategoryId = new Guid("747f6d66-7524-40ca-8494-f65e85b5ee5d"), + ConcurrencyStamp = new byte[] { 0, 0, 0, 0, 0, 0, 0, 1 }, CreatedOn = 1306422411264000000L, Description = "extensive driving range, lots of standard safety features", Name = "Model Y", diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Mappers/CategoriesMapper.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Mappers/CategoriesMapper.cs index e0591f8826..22451a258c 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Mappers/CategoriesMapper.cs +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Mappers/CategoriesMapper.cs @@ -7,7 +7,7 @@ namespace Boilerplate.Server.Api.Mappers; /// /// More info at Server/Mappers/README.md /// -[Mapper(UseDeepCloning = true)] +[Mapper] public static partial class CategoriesMapper { public static partial IQueryable Project(this IQueryable query); @@ -20,6 +20,4 @@ public static partial class CategoriesMapper [MapProperty(nameof(@Category.Products.Count), nameof(@CategoryDto.ProductsCount))] public static partial CategoryDto Map(this Category source); public static partial Category Map(this CategoryDto source); - public static partial void Patch(this CategoryDto source, Category destination); - public static partial void Patch(this Category source, CategoryDto destination); } diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Mappers/IdentityMapper.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Mappers/IdentityMapper.cs index 714a58e688..f0be27a736 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Mappers/IdentityMapper.cs +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Mappers/IdentityMapper.cs @@ -7,7 +7,7 @@ namespace Boilerplate.Server.Api.Mappers; /// /// More info at Server/Mappers/README.md /// -[Mapper(UseDeepCloning = true)] +[Mapper] public static partial class IdentityMapper { public static partial UserDto Map(this User source); diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Mappers/ProductsMapper.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Mappers/ProductsMapper.cs index 4826dce036..a12cd69244 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Mappers/ProductsMapper.cs +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Mappers/ProductsMapper.cs @@ -7,7 +7,7 @@ namespace Boilerplate.Server.Api.Mappers; /// /// More info at Server/Mappers/README.md /// -[Mapper(UseDeepCloning = true)] +[Mapper] public static partial class ProductsMapper { public static partial IQueryable Project(this IQueryable query); @@ -20,13 +20,4 @@ public static partial class ProductsMapper [MapProperty(nameof(@Product.Category.Name), nameof(@ProductDto.CategoryName))] public static partial ProductDto Map(this Product source); public static partial Product Map(this ProductDto source); - public static partial void Patch(this ProductDto source, Product destination); - - // It is important to note that when a ProductDto is sent to the server, - // it may contain a 'CategoryName'. However, while Mappingly's automatic conventions may fill Category property of product model - // with only its Name property populated by the 'CategoryName' value while the Id remains 0, - // this oversight could lead to ef core mistakenly assuming a new category is being added to the database during product saving, - // resulting in unintended data persistence. That's why we need to ignore 'CategoryName' during Patching and Mapping manually. - [MapperIgnoreSource(nameof(Product.Category))] - public static partial void Patch(this Product source, ProductDto destination); } diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Mappers/PushNotificationMapper.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Mappers/PushNotificationMapper.cs index 3addefe254..34789b0034 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Mappers/PushNotificationMapper.cs +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Mappers/PushNotificationMapper.cs @@ -7,7 +7,7 @@ namespace Boilerplate.Server.Api.Mappers; /// /// More info at Server/Mappers/README.md /// -[Mapper(UseDeepCloning = true)] +[Mapper] public static partial class PushNotificationMapper { public static partial void Patch(this DeviceInstallationDto source, DeviceInstallation destination); diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Mappers/TodoMapper.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Mappers/TodoMapper.cs index a677f50528..55687a8c49 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Mappers/TodoMapper.cs +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Mappers/TodoMapper.cs @@ -7,7 +7,7 @@ namespace Boilerplate.Server.Api.Mappers; /// /// More info at Server/Mappers/README.md /// -[Mapper(UseDeepCloning = true)] +[Mapper] public static partial class TodoMapper { public static partial IQueryable Project(this IQueryable query); diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Models/Categories/Category.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Models/Categories/Category.cs index d41575eb7f..499f296964 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Models/Categories/Category.cs +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Models/Categories/Category.cs @@ -11,5 +11,7 @@ public partial class Category public string? Color { get; set; } + public byte[] ConcurrencyStamp { get; set; } = []; + public IList Products { get; set; } = []; } diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Models/Products/Product.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Models/Products/Product.cs index ed66a9e53d..ba8e23715d 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Models/Products/Product.cs +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Models/Products/Product.cs @@ -21,4 +21,6 @@ public partial class Product public Category? Category { get; set; } public Guid CategoryId { get; set; } + + public byte[] ConcurrencyStamp { get; set; } = []; } diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Services/PushNotificationService.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Services/PushNotificationService.cs index dd7fc18d72..0b49fd0eae 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Services/PushNotificationService.cs +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Services/PushNotificationService.cs @@ -24,7 +24,11 @@ public async Task RegisterDevice([Required] DeviceInstallationDto dto, Cancellat if (deviceInstallation is null) { - dbContext.DeviceInstallations.Add(deviceInstallation = new() { InstallationId = dto.InstallationId }); + dbContext.DeviceInstallations.Add(deviceInstallation = new() + { + InstallationId = dto.InstallationId, + Platform = dto.Platform + }); } dto.Patch(deviceInstallation); diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Shared/Controllers/Categories/ICategoryController.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Shared/Controllers/Categories/ICategoryController.cs index c3db0748a0..e05c46bb5a 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Shared/Controllers/Categories/ICategoryController.cs +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Shared/Controllers/Categories/ICategoryController.cs @@ -14,8 +14,8 @@ public interface ICategoryController : IAppController [HttpPut] Task Update(CategoryDto dto, CancellationToken cancellationToken); - [HttpDelete("{id}")] - Task Delete(Guid id, CancellationToken cancellationToken); + [HttpDelete("{id}/{concurrencyStamp}")] + Task Delete(Guid id, string concurrencyStamp, CancellationToken cancellationToken); [HttpGet] Task> GetCategories(CancellationToken cancellationToken) => default!; diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Shared/Controllers/Products/IProductController.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Shared/Controllers/Products/IProductController.cs index b7905d59dc..669f71e376 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Shared/Controllers/Products/IProductController.cs +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Shared/Controllers/Products/IProductController.cs @@ -14,8 +14,8 @@ public interface IProductController : IAppController [HttpPut] Task Update(ProductDto dto, CancellationToken cancellationToken); - [HttpDelete("{id}")] - Task Delete(Guid id, CancellationToken cancellationToken); + [HttpDelete("{id}/{concurrencyStamp}")] + Task Delete(Guid id, string concurrencyStamp, CancellationToken cancellationToken); [HttpGet] Task> GetProducts(CancellationToken cancellationToken) => default!; diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Shared/Dtos/Categories/CategoryDto.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Shared/Dtos/Categories/CategoryDto.cs index 91b296fa8d..974370208e 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Shared/Dtos/Categories/CategoryDto.cs +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Shared/Dtos/Categories/CategoryDto.cs @@ -14,4 +14,6 @@ public partial class CategoryDto public string? Color { get; set; } = "#FFFFFF"; public int ProductsCount { get; set; } + + public byte[] ConcurrencyStamp { get; set; } = []; } diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Shared/Dtos/Products/ProductDto.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Shared/Dtos/Products/ProductDto.cs index c7ef12a4d2..94b3aab110 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Shared/Dtos/Products/ProductDto.cs +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Shared/Dtos/Products/ProductDto.cs @@ -25,4 +25,6 @@ public partial class ProductDto [Display(Name = nameof(AppStrings.Category))] public string? CategoryName { get; set; } + + public byte[] ConcurrencyStamp { get; set; } = []; } diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Shared/Extensions/IServiceCollectionExtensions.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Shared/Extensions/ISharedServiceCollectionExtensions.cs similarity index 96% rename from src/Templates/Boilerplate/Bit.Boilerplate/src/Shared/Extensions/IServiceCollectionExtensions.cs rename to src/Templates/Boilerplate/Bit.Boilerplate/src/Shared/Extensions/ISharedServiceCollectionExtensions.cs index 11ee242677..4fa80055a7 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Shared/Extensions/IServiceCollectionExtensions.cs +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Shared/Extensions/ISharedServiceCollectionExtensions.cs @@ -4,7 +4,7 @@ namespace Microsoft.Extensions.DependencyInjection; -public static partial class IServiceCollectionExtensions +public static partial class ISharedServiceCollectionExtensions { public static IServiceCollection AddSharedProjectServices(this IServiceCollection services, IConfiguration configuration) { diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Shared/appsettings.json b/src/Templates/Boilerplate/Bit.Boilerplate/src/Shared/appsettings.json index c5b89a62a5..3902535bbe 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Shared/appsettings.json +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Shared/appsettings.json @@ -4,14 +4,6 @@ "Default": "Warning", "Microsoft.EntityFrameworkCore.Database.Command": "Information" }, - //#if (appCenter == true) - "AppCenterLoggerProvider": { - "LogLevel": { - "Default": "Warning", - "Microsoft.EntityFrameworkCore.Database.Command": "Information" - } - }, - //#endif //#if (appInsights == true) "ApplicationInsights": { "LogLevel": {