diff --git a/Directory.Packages.props b/Directory.Packages.props index e31e23ac..7f4f9bdc 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -15,6 +15,7 @@ + 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. | 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). 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.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/Features/Admin/BlogPostEditor/Components/CreateNewBlogPost.razor b/src/LinkDotNet.Blog.Web/Features/Admin/BlogPostEditor/Components/CreateNewBlogPost.razor index e028410f..ad217d01 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, }; } 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]