Skip to content

Commit fca124d

Browse files
feat: Added Initial PostgreSQL support (#399)
* Added Initial PostgreSQL support * Updated with PostgreSql support * Update Configuration.md with PostgreSql support * Update BlogPost.cs with UtcNow * Added ScheduledPublishedDate to UTC --------- Co-authored-by: Steven Giesel <stgiesel35@gmail.com>
1 parent 54e688c commit fca124d

File tree

11 files changed

+45
-9
lines changed

11 files changed

+45
-9
lines changed

Directory.Packages.props

+1
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
<PackageVersion Include="Microsoft.Extensions.Diagnostics.HealthChecks.Abstractions" Version="9.0.1" />
1616
<PackageVersion Include="MongoDB.Driver" Version="2.30.0" />
1717
<PackageVersion Include="Pomelo.EntityFrameworkCore.MySql" Version="9.0.0-preview.2.efcore.9.0.0" />
18+
<PackageVersion Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="9.0.2" />
1819
<PackageVersion Include="RavenDB.Client" Version="6.2.3" />
1920
</ItemGroup>
2021
<ItemGroup Label="Web">

docs/Setup/Configuration.md

+2-2
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,7 @@ The appsettings.json file has a lot of options to customize the content of the b
8282
| Description | MarkdownString | Small introduction text for yourself. This is also used for `<meta name="description">` tag. For this the markup will be converted to plain text |
8383
| BackgroundUrl | string | Url or path to the background image. (Optional) |
8484
| ProfilePictureUrl | string | Url or path to your profile picture |
85-
| [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) |
85+
| [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) |
8686
| ConnectionString | string | Is used for connection to a database. |
8787
| DatabaseName | string | Name of the database. Only used with `RavenDbStorageProvider` |
8888
| [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
108108
| ConnectionString | string | The connection string for the image storage provider. Only used if `AuthenticationMode` is set to `ConnectionString` |
109109
| ServiceUrl | string | The host url of the Azure blob storage. Only used if `AuthenticationMode` is set to `Default` |
110110
| ContainerName | string | The container name for the image storage provider |
111-
| 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. |
111+
| 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. |

docs/Storage/Readme.md

+10-2
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ Currently, there are 5 Storage-Provider:
77
- Sqlite - Based on EF Core, it can be easily adapted for other Sql Dialects. The tables are automatically created.
88
- SqlServer - Based on EF Core, it can be easily adapted for other Sql Dialects. The tables are automatically created.
99
- MySql - Based on EF Core - also supports MariaDB.
10+
- PostgreSql - Based on EF Core.
1011

1112
The default (when you clone the repository) is the `Sqlite` option with an in-memory database.
1213
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:
3132
"ConnectionString": "Server=YOURSERVER;User ID=YOURUSERID;Password=YOURPASSWORD;Database=YOURDATABASE"
3233
```
3334

35+
For PostgreSql use the following:
36+
37+
```
38+
"PersistenceProvider": "PostgreSql"
39+
"ConnectionString": "Server=YOURSERVER;User ID=YOURUSERID;Password=YOURPASSWORD;Database=YOURDATABASE"
40+
```
41+
3442
## Entity Framework Migrations
3543

36-
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:
44+
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:
3745

3846
```bash
3947
dotnet ef database update --project src/LinkDotNet.Blog.Infrastructure --startup-project src/LinkDotNet.Blog.Web --connection "<ConnectionString>"
@@ -51,4 +59,4 @@ Here is the full documentation: [*"Applying Migrations"*](https://learn.microsof
5159
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.
5260

5361
## Considerations
54-
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).
62+
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).

src/LinkDotNet.Blog.Domain/BlogPost.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,7 @@ public static BlogPost Create(
9999
throw new InvalidOperationException("Can't schedule publish date if the blog post is already published.");
100100
}
101101

102-
var blogPostUpdateDate = scheduledPublishDate ?? updatedDate ?? DateTime.Now;
102+
var blogPostUpdateDate = scheduledPublishDate ?? updatedDate ?? DateTime.UtcNow;
103103

104104
var blogPost = new BlogPost
105105
{

src/LinkDotNet.Blog.Infrastructure/LinkDotNet.Blog.Infrastructure.csproj

+1
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
<PackageReference Include="Microsoft.Extensions.Diagnostics.HealthChecks.Abstractions" />
1515
<PackageReference Include="MongoDB.Driver" />
1616
<PackageReference Include="Pomelo.EntityFrameworkCore.MySql" />
17+
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL"/>
1718
<PackageReference Include="RavenDB.Client" />
1819
</ItemGroup>
1920

src/LinkDotNet.Blog.Infrastructure/Persistence/PersistenceProvider.cs

+1
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ public sealed class PersistenceProvider : Enumeration<PersistenceProvider>
99
public static readonly PersistenceProvider RavenDb = new(nameof(RavenDb));
1010
public static readonly PersistenceProvider MySql = new(nameof(MySql));
1111
public static readonly PersistenceProvider MongoDB = new(nameof(MongoDB));
12+
public static readonly PersistenceProvider PostgreSql = new(nameof(PostgreSql));
1213

1314
private PersistenceProvider(string key)
1415
: base(key)

src/LinkDotNet.Blog.Web/Features/Admin/BlogPostEditor/Components/CreateNewBlogPost.razor

+1
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@
7373
<small for="scheduled" class="form-text text-body-secondary">
7474
If set the blog post will be published at the given date.
7575
A blog post with a schedule date can't be set to published.
76+
All dates are stored in UTC internally.
7677
</small>
7778
<ValidationMessage For="() => model.ScheduledPublishDate"></ValidationMessage>
7879
</div>

src/LinkDotNet.Blog.Web/Features/Admin/BlogPostEditor/Components/CreateNewModel.cs

+3-3
Original file line numberDiff line numberDiff line change
@@ -67,8 +67,8 @@ public bool ShouldUpdateDate
6767
[FutureDateValidation]
6868
public DateTime? ScheduledPublishDate
6969
{
70-
get => scheduledPublishDate;
71-
set => SetProperty(out scheduledPublishDate, value);
70+
get => scheduledPublishDate?.ToLocalTime();
71+
set => SetProperty(out scheduledPublishDate, value?.ToUniversalTime());
7272
}
7373

7474
public string Tags
@@ -108,7 +108,7 @@ public static CreateNewModel FromBlogPost(BlogPost blogPost)
108108
PreviewImageUrl = blogPost.PreviewImageUrl,
109109
originalUpdatedDate = blogPost.UpdatedDate,
110110
PreviewImageUrlFallback = blogPost.PreviewImageUrlFallback ?? string.Empty,
111-
ScheduledPublishDate = blogPost.ScheduledPublishDate,
111+
scheduledPublishDate = blogPost.ScheduledPublishDate?.ToUniversalTime(),
112112
IsDirty = false,
113113
};
114114
}

src/LinkDotNet.Blog.Web/RegistrationExtensions/SqlRegistrationExtensions.cs

+18
Original file line numberDiff line numberDiff line change
@@ -66,4 +66,22 @@ public static void UseMySqlAsStorageProvider(this IServiceCollection services)
6666
});
6767
services.AddScoped(typeof(IRepository<>), typeof(Repository<>));
6868
}
69+
70+
public static void UsePostgreSqlAsStorageProvider(this IServiceCollection services)
71+
{
72+
services.AssertNotAlreadyRegistered(typeof(IRepository<>));
73+
74+
services.AddPooledDbContextFactory<BlogDbContext>(
75+
(s, builder) =>
76+
{
77+
var configuration = s.GetRequiredService<IOptions<ApplicationConfiguration>>();
78+
var connectionString = configuration.Value.ConnectionString;
79+
builder.UseNpgsql(connectionString)
80+
#if DEBUG
81+
.EnableDetailedErrors()
82+
#endif
83+
;
84+
});
85+
services.AddScoped(typeof(IRepository<>), typeof(Repository<>));
86+
}
6987
}

src/LinkDotNet.Blog.Web/RegistrationExtensions/StorageProviderExtensions.cs

+5
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,11 @@ public static IServiceCollection AddStorageProvider(this IServiceCollection serv
4343
services.UseMongoDBAsStorageProvider();
4444
services.RegisterCachedRepository<Infrastructure.Persistence.MongoDB.Repository<BlogPost>>();
4545
}
46+
else if (persistenceProvider == PersistenceProvider.PostgreSql)
47+
{
48+
services.UsePostgreSqlAsStorageProvider();
49+
services.RegisterCachedRepository<Infrastructure.Persistence.Sql.Repository<BlogPost>>();
50+
}
4651

4752
return services;
4853
}

tests/LinkDotNet.Blog.UnitTests/StorageProviderRegistrationExtensionsTests.cs

+2-1
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,8 @@ public class StorageProviderRegistrationExtensionsTests
1111
services => services.UseSqliteAsStorageProvider(),
1212
services => services.UseSqlAsStorageProvider(),
1313
services => services.UseRavenDbAsStorageProvider(),
14-
services => services.UseMySqlAsStorageProvider()
14+
services => services.UseMySqlAsStorageProvider(),
15+
services => services.UsePostgreSqlAsStorageProvider()
1516
};
1617

1718
[Theory]

0 commit comments

Comments
 (0)