Achieving True DB Agnostic with Virto Commerce: A Journey without Breaking Changes

In this post, I will take you on a journey through our experience of transforming Virto Commerce into a DB Agnostic platform, supporting multiple database providers like MS SQL, PostgreSQL, and MySQL.

Our story showcases how we managed to migrate to a database-agnostic architecture without causing any breaking changes during updates. We believe that our approach, backed by examples of C# code and Entity Framework features, will be valuable to other developers seeking a similar level of flexibility in their solutions.

Chapter 1: Understanding the Problem and Setting the Goal

Our primary challenge was to make Virto Commerce seamlessly work with different database providers, offering users the freedom to switch between systems without disruption. We set the goal to achieve a true DB Agnostic architecture without any breaking changes, making it easier for developers to customize the solution as per their database requirements.

Chapter 2: Design Principles

Before implementation, we outlined the principles guiding our solution.

  • No Breaking Changes: Updates must be seamless, preserving existing functionality.
  • Use Entity Framework: We explored how Entity Framework offered features essential for our migration.
  • Database Provider Isolation: Isolating database-specific code in separate projects allowed each module to use its preferred provider without affecting the core project.
  • Support Customization: By allowing provider-level customization, developers could utilize unique DB features while maintaining DB Agnostic benefits.

Chapter 3: The Architecture of Virto Commerce and Entity Framework

By design, Virto Commerce module has Web, Data, and Core layers. This approach and Entity Framework help us to handle the migration process smoothly.

We split Data project into 4 projects:

  • CartModule.Data
  • CartModule.Data.MySql
  • CartModule.Data.PostgreSql
  • CartModule.Data.SqlServer

if you need to support a new database, you can create a new VC module and using Virto Commerce Extensibility Framework add a new database provider.


vc-module-cart/src at dev · VirtoCommerce/vc-module-cart (github.com)

These projects allow your module to be database agnostic, meaning it can be easily adapted to work with different database systems without significant code changes.

Each project provides a specific implementation for working with a particular database system, while the CartModule.Data project serves as the common base for all database-related functionality.

This architecture was designed to promote code reusability, maintainability, and scalability.

Chapter 4: Data.[Provider] Projects Structure

Data.[Provider] project has the following clean structure:

  1. Migrations folder - contains the migration files for the database system.
  2. DbContextOptionsBuilderExtensions class - contains the helper method for configuring the DbContextOptionsBuilder for specific database provider.
  3. [Provider]DbContextFactory class - contains implementation of IDesignTimeDbContextFactory for for specific database provider.
  4. Readme.md file - contains the instructions for configuring and migration creation the specific database provider.
  5. Model Customization files (optional) - entity type configuration to change schema for DB specific needs.

Here you can see an example of DbContextOptionsBuilderExtensions, you can see that we call native extension and then call MigrationsAssembly to store your migrations in a different project. Read more about Using a Separate Migrations Project - EF Core | Microsoft Learn

    public static class DbContextOptionsBuilderExtensions
    {
        /// <summary>
        /// Configures the context to use SqlServer.
        /// </summary>
        public static DbContextOptionsBuilder UseSqlServerDatabase(this DbContextOptionsBuilder builder, string connectionString)
        {
            return builder.UseSqlServer(connectionString, db => db
                .MigrationsAssembly(typeof(SqlServerDbContextFactory).Assembly.GetName().Name));
        }
    }

vc-module-cart/src/VirtoCommerce.CartModule.Data.SqlServer/SqlServerDbContextFactory.cs at dev · VirtoCommerce/vc-module-cart (github.com)

and CartDbContext initialization depends on DatabaseProvider:

           var databaseProvider = Configuration.GetValue("DatabaseProvider", "SqlServer");
            serviceCollection.AddDbContext<CartDbContext>(options =>
            {
                var connectionString = Configuration.GetConnectionString(ModuleInfo.Id) ?? Configuration.GetConnectionString("VirtoCommerce");

                switch (databaseProvider)
                {
                    case "MySql":
                        options.UseMySqlDatabase(connectionString);
                        break;
                    case "PostgreSql":
                        options.UsePostgreSqlDatabase(connectionString);
                        break;
                    default:
                        options.UseSqlServerDatabase(connectionString);
                        break;
                }
            });	

vc-module-cart/src/VirtoCommerce.CartModule.Web/Module.cs at dev · VirtoCommerce/vc-module-cart (github.com)

For SqlServer, we just move code from Data project. For others, we created them from scratch.

Chapter 5: Model Customization

One of the features of Virto Commerce is that it allows you to configure various aspects of the entity type required for a specific database provider needs. You can specify the properties, keys, indexes, relationships, etc. of your entities using a fluent API. For example, MySql doesn’t support money.

To provide the ability to customize entity type mapping for specific databases we use ApplyConfigurationsFromAssembly feature. Virto Commerce Module calls ApplyConfigurationsFromAssembly from OnModelCreating in DbContext implementation.

... 
            // Allows configuration for an entity type for different database types.
            // Applies configuration from all <see cref="IEntityTypeConfiguration{TEntity}" in VirtoCommerce.CartModule.Data.XXX project. /> 
            switch (this.Database.ProviderName)
            {
                case "Pomelo.EntityFrameworkCore.MySql":
                    modelBuilder.ApplyConfigurationsFromAssembly(Assembly.Load("VirtoCommerce.CartModule.Data.MySql"));
                    break;
                case "Npgsql.EntityFrameworkCore.PostgreSQL":
                    modelBuilder.ApplyConfigurationsFromAssembly(Assembly.Load("VirtoCommerce.CartModule.Data.PostgreSql"));
                    break;
                case "Microsoft.EntityFrameworkCore.SqlServer":
                    modelBuilder.ApplyConfigurationsFromAssembly(Assembly.Load("VirtoCommerce.CartModule.Data.SqlServer"));
                    break;
            }
...

vc-module-cart/src/VirtoCommerce.CartModule.Data/Repositories/CartDbContext.cs at dev · VirtoCommerce/vc-module-cart (github.com)

To use this feature, you need to implement the IEntityTypeConfiguration interface in your entity configuration classes. This interface defines a method called Configure that takes an EntityTypeBuilder parameter and configures various aspects of the entity type.

The following code snippet shows an example of how to implement this interface for a DiscountEntity entity:

    public class DiscountEntityConfiguration : IEntityTypeConfiguration<DiscountEntity>
    {
        public void Configure(EntityTypeBuilder<DiscountEntity> builder)
        {
            builder.Property(x => x.DiscountAmount).HasColumnType("decimal").HasPrecision(18, 4);
            builder.Property(x => x.DiscountAmountWithTax).HasColumnType("decimal").HasPrecision(18, 4);
        }
    }

vc-module-cart/src/VirtoCommerce.CartModule.Data.MySql/DiscountEntityConfiguration.cs at dev · VirtoCommerce/vc-module-cart (github.com)

Chapter 6: Collaboration with the Community

We emphasized the importance of sharing experiences and knowledge with the Virto Commerce community. By collaborating and addressing concerns, we fostered a supportive ecosystem, helping others embark on a similar migration journey. It helped us implement and test DB Agnostic faster.

Chapter 7: Conclusion

Our migration to a DB Agnostic architecture was a resounding success, delivering greater flexibility and maintainability without breaking changes. The experience gained from leveraging Entity Framework and providing customization options is a valuable resource for other developers seeking to achieve similar goals in their .NET solutions.

References

Using a Separate Migrations Project - EF Core | Microsoft Learn
Creating and Configuring a Model - EF Core | Microsoft Learn
DB Agnostic Architecture - Overview
DB Agnostic Architecture - Creating Custom Module with DB Agnostic Approach
DB Agnostic Architecture - Transforming Custom Module to Support DB Agnostic Approach
Virto Commerce Cart Module.

Epilogue

We hope our journey and examples of C# code inspire and assist developers in adopting DB Agnostic principles. By sharing our experience, we aim to contribute to a community of resilient, flexible, and forward-thinking solutions in the world of ecommerce and beyond

@OlegoO What data should be entered in DataSource appsetting.json for postgres? I can’t connect to the postgres database in localhost
image

Here my example of Postgre Sql connection string:

{
  "DatabaseProvider": "PostgreSql",
  "ConnectionStrings": {
    "VirtoCommerce": "User ID=postgres;Password=aA111111;Host=localhost;Port=5432;Database=vc-platform-dba-webinar;"
  },
1 Like

You need to use the correct PostgreSql connection string.

Thanks, everything works. I think it’s worth mentioning in the documentation. There are several critical points

1 Like

Dear customers! Here you can find the ConnectionStrings examples and more.