.NET Shorts – Automatic Migration

Problem

Do you have a lot of “side-projects”? I do. Did you ever find yourself in situation where you open one of those “side-projects” thinking everything will work fine because you are an awesome developer and you are not making mistakes?

And then reality kicks in.

I must admit, when I was setting up my template, which I use for most (if not all) blog posts, I was too lazy to set up docker and automatic migration. I though it wouldn’t be a problem…

So, this is how it usually goes…

I get a burst of energy to write something I started a while ago. I open that “side-project” and I try to run it. It fails mostly because I forgot that I’m using some database and that appropriate docker container isn’t running.

So, now I have to start a container. Any normal person would go to the documentation and use it as a reference, but not me. I did it a milion times, what can go wrong? Then I spend next 10 minutes writing random commands in bash hoping one of them will start that container correctly. Then I give up and do what any normal person would do.

So, I have a database running and I have to update the database with my migrations. Any normal person would go to the documentation and use it as a reference, but not me. I think that you know the rest of the story…

It’s time to fix this.

Solution

  • Adding docker-compose file
  • Running migration automatically on project startup
Adding docker-compose.yml file
  • At the root of our project, let’s add docker-compose.yml
  • For this project, we are going to use Postgres
version: "3.3"
services:
  company-shorts-postgres:
    image: "postgres:15.1-alpine"
    container_name: company-shorts-postgres
    volumes: 
      - ./var/lib/postgresql/data
    environment:
      - POSTGRES_USER=postgres
      - POSTGRES_PASSWORD=postgres
    restart: unless-stopped    
    ports:
      - "5432:5432" 

Now when we run docker-compose up, it will run our database. Nice!

Running migration automatically
  • Inside our DependencyInjection.cs lets add another method
namespace Company.Shorts.Infrastructure.Db.Postgres
{
    using Company.Shorts.Infrastructure.Db.Postgres.Internal;
    using Microsoft.AspNetCore.Builder;
    using Microsoft.EntityFrameworkCore;
    using Microsoft.Extensions.DependencyInjection;

    public static class DependencyInjection
    {
        public static IServiceCollection AddPostgresDatabaseLayer(this IServiceCollection services, PostgresAdapterSettings settings)
        {
            services.AddDbContext<PostgresDbContext>(options =>
            {
                options.UseNpgsql(settings.Url);
            }, ServiceLifetime.Transient);

            DatabaseDependencyInjection.AddRepositories<PostgresDbContext>(services);

            return services;
        }

        public static IApplicationBuilder MigratePostgresDb(this IApplicationBuilder builder)
        {
            using var scope = builder.ApplicationServices.CreateScope();

            using var dbContext = scope.ServiceProvider.GetService<PostgresDbContext>();

            if (dbContext is null)
            {
                throw new ArgumentNullException(nameof(dbContext));
            }

            if (dbContext.Database.GetPendingMigrations().Count() > 0)
            {
                dbContext.Database.Migrate();
            }

            return builder;
        }
    }

    public class PostgresAdapterSettings
    {
        public const string Key = nameof(PostgresAdapterSettings);

        public string Url { get; set; } = default!;
    }
}
  • Let’s call that method in our Startup.cs class
namespace Company.Shorts
{
    using Company.Shorts.Application;
    using Company.Shorts.Blocks.Common.Mapping.Configuration;
    using Company.Shorts.Blocks.Common.Serilog.Configuration;
    using Company.Shorts.Blocks.Common.Swagger.Configuration;
    using Company.Shorts.Blocks.Presentation.Api.Configuration;
    using Company.Shorts.Infrastructure.Db.Postgres;
    using Company.Shorts.Infrastructure.ExampleAdapter;
    using Company.Shorts.Presentation.Api;
    using Hellang.Middleware.ProblemDetails;

    public sealed class Startup
    {
        public Startup(
            IConfiguration configuration,
            IWebHostEnvironment environment)
        {
            Configuration = configuration;
            Environment = environment;
        }

        public IConfiguration Configuration { get; }

        public IWebHostEnvironment Environment { get; }

        public ExampleAdapterSettings ExampleAdapterSettings =>
            Configuration
                .GetSection(ExampleAdapterSettings.Key)
                .Get<ExampleAdapterSettings>();

        public PostgresAdapterSettings PostgresAdapterSettings =>
            Configuration
                .GetSection(PostgresAdapterSettings.Key)
                .Get<PostgresAdapterSettings>();

        public void ConfigureServices(IServiceCollection services)
        {
            services.AddCors();
            services.AddHealthChecks();
            services.AddPostgresDatabaseLayer(PostgresAdapterSettings);
            services.AddInfrastructureExampleAdapter(ExampleAdapterSettings);
            services.AddApplicationLayer();
            services.AddPresentationLayer(Environment);
            services.AddAutoMapperConfiguration(AppDomain.CurrentDomain);
        }

        public void Configure(IApplicationBuilder app)
        {
            app.UseProblemDetails();

            if (!Environment.IsDevelopment())
            {
                app.UseHsts();
            }
            // Automatic Migration
            app.MigratePostgresDb();

            app.UseSwaggerConfiguration();

            app.UseHttpsRedirection();

            app.UseCors(options => options
                .AllowAnyOrigin()
                .AllowAnyMethod()
                .AllowAnyHeader());

            app.UseSerilogConfiguration();

            app.UseRouting();

            app.UseEndpoints(endpoints =>
            {
                endpoints.MapDefaultControllerRoute();
                endpoints.MapDefaultHealthCheckRoute();
            });
        }
    }
}

Benefits?

  • You can just run your project and everything will sync.

Github

Branch: steps/automatic-migration