Skip to content

Commit

Permalink
feat: Added Initial PostgreSQL support (#399)
Browse files Browse the repository at this point in the history
* 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 <[email protected]>
  • Loading branch information
EliasMasche and linkdotnet authored Jan 28, 2025
1 parent 54e688c commit fca124d
Show file tree
Hide file tree
Showing 11 changed files with 45 additions and 9 deletions.
1 change: 1 addition & 0 deletions Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
<PackageVersion Include="Microsoft.Extensions.Diagnostics.HealthChecks.Abstractions" Version="9.0.1" />
<PackageVersion Include="MongoDB.Driver" Version="2.30.0" />
<PackageVersion Include="Pomelo.EntityFrameworkCore.MySql" Version="9.0.0-preview.2.efcore.9.0.0" />
<PackageVersion Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="9.0.2" />
<PackageVersion Include="RavenDB.Client" Version="6.2.3" />
</ItemGroup>
<ItemGroup Label="Web">
Expand Down
4 changes: 2 additions & 2 deletions docs/Setup/Configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 `<meta name="description">` 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 | |
Expand All @@ -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. |
| 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. |
12 changes: 10 additions & 2 deletions docs/Storage/Readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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 "<ConnectionString>"
Expand All @@ -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).
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).
2 changes: 1 addition & 1 deletion src/LinkDotNet.Blog.Domain/BlogPost.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
<PackageReference Include="Microsoft.Extensions.Diagnostics.HealthChecks.Abstractions" />
<PackageReference Include="MongoDB.Driver" />
<PackageReference Include="Pomelo.EntityFrameworkCore.MySql" />
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL"/>
<PackageReference Include="RavenDB.Client" />
</ItemGroup>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ public sealed class PersistenceProvider : Enumeration<PersistenceProvider>
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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@
<small for="scheduled" class="form-text text-body-secondary">
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.
</small>
<ValidationMessage For="() => model.ScheduledPublishDate"></ValidationMessage>
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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,
};
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<BlogDbContext>(
(s, builder) =>
{
var configuration = s.GetRequiredService<IOptions<ApplicationConfiguration>>();
var connectionString = configuration.Value.ConnectionString;
builder.UseNpgsql(connectionString)
#if DEBUG
.EnableDetailedErrors()
#endif
;
});
services.AddScoped(typeof(IRepository<>), typeof(Repository<>));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,11 @@ public static IServiceCollection AddStorageProvider(this IServiceCollection serv
services.UseMongoDBAsStorageProvider();
services.RegisterCachedRepository<Infrastructure.Persistence.MongoDB.Repository<BlogPost>>();
}
else if (persistenceProvider == PersistenceProvider.PostgreSql)
{
services.UsePostgreSqlAsStorageProvider();
services.RegisterCachedRepository<Infrastructure.Persistence.Sql.Repository<BlogPost>>();
}

return services;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down

0 comments on commit fca124d

Please sign in to comment.