Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added Initial PostgreSQL support #399

Merged
merged 7 commits into from
Jan 28, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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