From 78a72aacf0fe6351ecbd83389a4bf06e0260f68b Mon Sep 17 00:00:00 2001 From: Elias Mascheroni Date: Fri, 3 Jan 2025 03:31:46 -0300 Subject: [PATCH 1/5] Added Initial PostgreSQL support --- Directory.Packages.props | 1 + .../LinkDotNet.Blog.Infrastructure.csproj | 1 + .../Persistence/PersistenceProvider.cs | 1 + .../SqlRegistrationExtensions.cs | 18 ++++++++++++++++++ .../StorageProviderExtensions.cs | 5 +++++ ...orageProviderRegistrationExtensionsTests.cs | 3 ++- 6 files changed, 28 insertions(+), 1 deletion(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 2e54c046..d39ce0c9 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -15,6 +15,7 @@ + diff --git a/src/LinkDotNet.Blog.Infrastructure/LinkDotNet.Blog.Infrastructure.csproj b/src/LinkDotNet.Blog.Infrastructure/LinkDotNet.Blog.Infrastructure.csproj index b23c2383..ea9e16e2 100644 --- a/src/LinkDotNet.Blog.Infrastructure/LinkDotNet.Blog.Infrastructure.csproj +++ b/src/LinkDotNet.Blog.Infrastructure/LinkDotNet.Blog.Infrastructure.csproj @@ -14,6 +14,7 @@ + diff --git a/src/LinkDotNet.Blog.Infrastructure/Persistence/PersistenceProvider.cs b/src/LinkDotNet.Blog.Infrastructure/Persistence/PersistenceProvider.cs index 8ea776d5..01f247d7 100644 --- a/src/LinkDotNet.Blog.Infrastructure/Persistence/PersistenceProvider.cs +++ b/src/LinkDotNet.Blog.Infrastructure/Persistence/PersistenceProvider.cs @@ -9,6 +9,7 @@ public sealed class PersistenceProvider : Enumeration public static readonly PersistenceProvider RavenDb = new(nameof(RavenDb)); public static readonly PersistenceProvider MySql = new(nameof(MySql)); public static readonly PersistenceProvider MongoDB = new(nameof(MongoDB)); + public static readonly PersistenceProvider PostgreSql = new(nameof(PostgreSql)); private PersistenceProvider(string key) : base(key) diff --git a/src/LinkDotNet.Blog.Web/RegistrationExtensions/SqlRegistrationExtensions.cs b/src/LinkDotNet.Blog.Web/RegistrationExtensions/SqlRegistrationExtensions.cs index 8787666d..463327da 100644 --- a/src/LinkDotNet.Blog.Web/RegistrationExtensions/SqlRegistrationExtensions.cs +++ b/src/LinkDotNet.Blog.Web/RegistrationExtensions/SqlRegistrationExtensions.cs @@ -66,4 +66,22 @@ public static void UseMySqlAsStorageProvider(this IServiceCollection services) }); services.AddScoped(typeof(IRepository<>), typeof(Repository<>)); } + + public static void UsePostgreSqlAsStorageProvider(this IServiceCollection services) + { + services.AssertNotAlreadyRegistered(typeof(IRepository<>)); + + services.AddPooledDbContextFactory( + (s, builder) => + { + var configuration = s.GetRequiredService>(); + var connectionString = configuration.Value.ConnectionString; + builder.UseNpgsql(connectionString) +#if DEBUG + .EnableDetailedErrors() +#endif + ; + }); + services.AddScoped(typeof(IRepository<>), typeof(Repository<>)); + } } diff --git a/src/LinkDotNet.Blog.Web/RegistrationExtensions/StorageProviderExtensions.cs b/src/LinkDotNet.Blog.Web/RegistrationExtensions/StorageProviderExtensions.cs index 348eeafc..9498d37d 100644 --- a/src/LinkDotNet.Blog.Web/RegistrationExtensions/StorageProviderExtensions.cs +++ b/src/LinkDotNet.Blog.Web/RegistrationExtensions/StorageProviderExtensions.cs @@ -43,6 +43,11 @@ public static IServiceCollection AddStorageProvider(this IServiceCollection serv services.UseMongoDBAsStorageProvider(); services.RegisterCachedRepository>(); } + else if (persistenceProvider == PersistenceProvider.PostgreSql) + { + services.UsePostgreSqlAsStorageProvider(); + services.RegisterCachedRepository>(); + } return services; } diff --git a/tests/LinkDotNet.Blog.UnitTests/StorageProviderRegistrationExtensionsTests.cs b/tests/LinkDotNet.Blog.UnitTests/StorageProviderRegistrationExtensionsTests.cs index aad788cc..3bf125fc 100644 --- a/tests/LinkDotNet.Blog.UnitTests/StorageProviderRegistrationExtensionsTests.cs +++ b/tests/LinkDotNet.Blog.UnitTests/StorageProviderRegistrationExtensionsTests.cs @@ -11,7 +11,8 @@ public class StorageProviderRegistrationExtensionsTests services => services.UseSqliteAsStorageProvider(), services => services.UseSqlAsStorageProvider(), services => services.UseRavenDbAsStorageProvider(), - services => services.UseMySqlAsStorageProvider() + services => services.UseMySqlAsStorageProvider(), + services => services.UsePostgreSqlAsStorageProvider() }; [Theory] From 024baf565545e1982b23dbf1c3b5fbdd24f1379d Mon Sep 17 00:00:00 2001 From: Elias Mascheroni <6045426+EliasMasche@users.noreply.github.com> Date: Mon, 6 Jan 2025 05:15:16 -0300 Subject: [PATCH 2/5] Updated with PostgreSql support --- docs/Storage/Readme.md | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/docs/Storage/Readme.md b/docs/Storage/Readme.md index 74772ef2..b5cf48be 100644 --- a/docs/Storage/Readme.md +++ b/docs/Storage/Readme.md @@ -7,6 +7,7 @@ Currently, there are 5 Storage-Provider: - Sqlite - Based on EF Core, it can be easily adapted for other Sql Dialects. The tables are automatically created. - SqlServer - Based on EF Core, it can be easily adapted for other Sql Dialects. The tables are automatically created. - MySql - Based on EF Core - also supports MariaDB. +- PostgreSql - Based on EF Core. The default (when you clone the repository) is the `Sqlite` option with an in-memory database. That means every time you restart the service, all posts and related objects are gone. This is useful for testing. @@ -31,9 +32,16 @@ For MySql use the following: "ConnectionString": "Server=YOURSERVER;User ID=YOURUSERID;Password=YOURPASSWORD;Database=YOURDATABASE" ``` +For PostgreSql use the following: + +``` +"PersistenceProvider": "PostgreSql" +"ConnectionString": "Server=YOURSERVER;User ID=YOURUSERID;Password=YOURPASSWORD;Database=YOURDATABASE" +``` + ## Entity Framework Migrations -For the SQL providers (`SqlServer`, `Sqlite`, `MySql`), you can use Entity Framework Core Migrations to create and manage the database schema. The whole documentation can be found under [*"Entity Framework Core tools reference"*](https://learn.microsoft.com/en-us/ef/core/cli/dotnet). The short version is that you can use the following steps: +For the SQL providers (`SqlServer`, `Sqlite`, `MySql`, `PostgreSql`), you can use Entity Framework Core Migrations to create and manage the database schema. The whole documentation can be found under [*"Entity Framework Core tools reference"*](https://learn.microsoft.com/en-us/ef/core/cli/dotnet). The short version is that you can use the following steps: ```bash dotnet ef database update --project src/LinkDotNet.Blog.Infrastructure --startup-project src/LinkDotNet.Blog.Web --connection "" @@ -51,4 +59,4 @@ Here is the full documentation: [*"Applying Migrations"*](https://learn.microsof Alternatively, the blog calls `Database.EnsureCreated()` on startup, which creates the database schema if it does not exist. So you are not forced to use migrations. ## Considerations -For most people a Sqlite database might be the best choice between convienence and ease of setup. As it runs "in-process" there are no additional dependencies or setup required (and therefore no additional cost). As the blog tries to cache many things, the load onto the database is not that big (performance considerations). The advantages of a "real" database like SqlServer or MySql are more in the realm of backups, replication, and other enterprise features (which are not needed often times for a simple blog). \ No newline at end of file +For most people a Sqlite database might be the best choice between convienence and ease of setup. As it runs "in-process" there are no additional dependencies or setup required (and therefore no additional cost). As the blog tries to cache many things, the load onto the database is not that big (performance considerations). The advantages of a "real" database like SqlServer or MySql are more in the realm of backups, replication, and other enterprise features (which are not needed often times for a simple blog). From 0f19dc4538c202e83e0b3b0e9a67114fc75e266b Mon Sep 17 00:00:00 2001 From: Elias Mascheroni <6045426+EliasMasche@users.noreply.github.com> Date: Tue, 7 Jan 2025 02:31:39 -0300 Subject: [PATCH 3/5] Update Configuration.md with PostgreSql support --- docs/Setup/Configuration.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/Setup/Configuration.md b/docs/Setup/Configuration.md index a202beed..28704dd5 100644 --- a/docs/Setup/Configuration.md +++ b/docs/Setup/Configuration.md @@ -82,7 +82,7 @@ The appsettings.json file has a lot of options to customize the content of the b | Description | MarkdownString | Small introduction text for yourself. This is also used for `` tag. For this the markup will be converted to plain text | | BackgroundUrl | string | Url or path to the background image. (Optional) | | ProfilePictureUrl | string | Url or path to your profile picture | -| [PersistenceProvider](./../Storage/Readme.md) | string | Declares the type of the storage provider (one of the following: `SqlServer`, `Sqlite`, `RavenDb`, `MongoDB`, `MySql`). More in-depth explanation [here](./../Storage/Readme.md) | +| [PersistenceProvider](./../Storage/Readme.md) | string | Declares the type of the storage provider (one of the following: `SqlServer`, `Sqlite`, `RavenDb`, `MongoDB`, `MySql`, `PostgreSql`). More in-depth explanation [here](./../Storage/Readme.md) | | ConnectionString | string | Is used for connection to a database. | | DatabaseName | string | Name of the database. Only used with `RavenDbStorageProvider` | | [AuthProvider](./../Authorization/Readme.md) | string | | @@ -108,4 +108,4 @@ The appsettings.json file has a lot of options to customize the content of the b | ConnectionString | string | The connection string for the image storage provider. Only used if `AuthenticationMode` is set to `ConnectionString` | | ServiceUrl | string | The host url of the Azure blob storage. Only used if `AuthenticationMode` is set to `Default` | | ContainerName | string | The container name for the image storage provider | -| CdnEndpoint | string | Optional CDN endpoint to use for uploaded images. If set, the blog will return this URL instead of the storage account URL for uploaded assets. | \ No newline at end of file +| CdnEndpoint | string | Optional CDN endpoint to use for uploaded images. If set, the blog will return this URL instead of the storage account URL for uploaded assets. | From d6e577c9da91bcf1b12989a4506cce2664d0a11a Mon Sep 17 00:00:00 2001 From: Elias Mascheroni <6045426+EliasMasche@users.noreply.github.com> Date: Wed, 8 Jan 2025 10:51:28 -0300 Subject: [PATCH 4/5] Update BlogPost.cs with UtcNow --- src/LinkDotNet.Blog.Domain/BlogPost.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/LinkDotNet.Blog.Domain/BlogPost.cs b/src/LinkDotNet.Blog.Domain/BlogPost.cs index 725efb27..3a52b28e 100644 --- a/src/LinkDotNet.Blog.Domain/BlogPost.cs +++ b/src/LinkDotNet.Blog.Domain/BlogPost.cs @@ -99,7 +99,7 @@ public static BlogPost Create( throw new InvalidOperationException("Can't schedule publish date if the blog post is already published."); } - var blogPostUpdateDate = scheduledPublishDate ?? updatedDate ?? DateTime.Now; + var blogPostUpdateDate = scheduledPublishDate ?? updatedDate ?? DateTime.UtcNow; var blogPost = new BlogPost { From eee04cf9b94f5c95790ac644c1d001484d7e5ebe Mon Sep 17 00:00:00 2001 From: Elias Mascheroni Date: Mon, 27 Jan 2025 23:09:46 -0300 Subject: [PATCH 5/5] Added ScheduledPublishedDate to UTC --- src/LinkDotNet.Blog.Domain/BlogPost.cs | 2 +- .../Admin/BlogPostEditor/Components/CreateNewBlogPost.razor | 1 + .../Admin/BlogPostEditor/Components/CreateNewModel.cs | 6 +++--- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/LinkDotNet.Blog.Domain/BlogPost.cs b/src/LinkDotNet.Blog.Domain/BlogPost.cs index 725efb27..3a52b28e 100644 --- a/src/LinkDotNet.Blog.Domain/BlogPost.cs +++ b/src/LinkDotNet.Blog.Domain/BlogPost.cs @@ -99,7 +99,7 @@ public static BlogPost Create( throw new InvalidOperationException("Can't schedule publish date if the blog post is already published."); } - var blogPostUpdateDate = scheduledPublishDate ?? updatedDate ?? DateTime.Now; + var blogPostUpdateDate = scheduledPublishDate ?? updatedDate ?? DateTime.UtcNow; var blogPost = new BlogPost { diff --git a/src/LinkDotNet.Blog.Web/Features/Admin/BlogPostEditor/Components/CreateNewBlogPost.razor b/src/LinkDotNet.Blog.Web/Features/Admin/BlogPostEditor/Components/CreateNewBlogPost.razor index 7e8c1c4f..a5bb2187 100644 --- a/src/LinkDotNet.Blog.Web/Features/Admin/BlogPostEditor/Components/CreateNewBlogPost.razor +++ b/src/LinkDotNet.Blog.Web/Features/Admin/BlogPostEditor/Components/CreateNewBlogPost.razor @@ -73,6 +73,7 @@ If set the blog post will be published at the given date. A blog post with a schedule date can't be set to published. + All dates are stored in UTC internally. diff --git a/src/LinkDotNet.Blog.Web/Features/Admin/BlogPostEditor/Components/CreateNewModel.cs b/src/LinkDotNet.Blog.Web/Features/Admin/BlogPostEditor/Components/CreateNewModel.cs index 6c7c5153..0a71e1ff 100644 --- a/src/LinkDotNet.Blog.Web/Features/Admin/BlogPostEditor/Components/CreateNewModel.cs +++ b/src/LinkDotNet.Blog.Web/Features/Admin/BlogPostEditor/Components/CreateNewModel.cs @@ -67,8 +67,8 @@ public bool ShouldUpdateDate [FutureDateValidation] public DateTime? ScheduledPublishDate { - get => scheduledPublishDate; - set => SetProperty(out scheduledPublishDate, value); + get => scheduledPublishDate?.ToLocalTime(); + set => SetProperty(out scheduledPublishDate, value?.ToUniversalTime()); } public string Tags @@ -108,7 +108,7 @@ public static CreateNewModel FromBlogPost(BlogPost blogPost) PreviewImageUrl = blogPost.PreviewImageUrl, originalUpdatedDate = blogPost.UpdatedDate, PreviewImageUrlFallback = blogPost.PreviewImageUrlFallback ?? string.Empty, - ScheduledPublishDate = blogPost.ScheduledPublishDate, + scheduledPublishDate = blogPost.ScheduledPublishDate?.ToUniversalTime(), IsDirty = false, }; }