DEV Community

Cover image for EF Core Database/Tables exists
Karen Payne
Karen Payne

Posted on

EF Core Database/Tables exists

Introduction

Learn how to determine if a database exists and if the required tables exist, which can be used in an application or a dotnet tool using EF Core.

Reasons for these checks

Before going to staging and/or production, the connection string must be correct, the database must exist, and the required tables must exist.

The code

  • Each method is generic
  • Rather than lump the code into one method, three are used so that a developer is not locked into all three checks.
  • DatabaseExists checks if the database exists from the DbContext connection string.
  • HasTables is a check to see if there are any tables in the database.
  • TablesExist accepts a string array (params string[]) of table names and checks if they exists. In the example project, table names are read from a section in appsettings.json
"DatabaseTables": {
  "Customer": "Customer",
  "ContactTypes": "ContactTypes",
  "Genders": "Genders"
}
Enter fullscreen mode Exit fullscreen mode

In both the console project and class project these methods are fully documented.

internal class DbContextHelpers
{

    public static bool DatabaseExists<TContext>(TContext context) where TContext : DbContext
    {
        if (context.GetService<IDatabaseCreator>() is not RelationalDatabaseCreator databaseCreator)
        {
            throw new InvalidOperationException("Database creator service is not available.");
        }

        return databaseCreator.Exists();
    }
    public static bool HasTables<TContext>(TContext context) where TContext : DbContext
    {
        if (context.GetService<IRelationalDatabaseCreator>() is not RelationalDatabaseCreator databaseCreator)
        {
            throw new InvalidOperationException("Database creator service is not available.");
        }

        return databaseCreator.HasTables();
    }


    public static bool TablesExist(DbContext context, params string[] tableNames)
    {

        var existingTables = new HashSet<string>(
            context.Model.GetEntityTypes()
                .Select(t => t.GetTableName())
                .Where(t => !string.IsNullOrEmpty(t)) 
                .Cast<string>() 
        );

        return existingTables.IsSupersetOf(tableNames);
    }

    public static bool FullCheck<TContext>(TContext context, params string[] tableNames) where TContext : DbContext 
        => DatabaseExists(context) && HasTables(context) && TablesExist(context, tableNames);
}
Enter fullscreen mode Exit fullscreen mode

Example

We create an instance of the DbContext and get table names to check from appsettings.json.

Using DbContextHelpers.FullCheck(context, tableNames) check if the database exists and required tables exists. If FullCheck returns true, perform a query and present the results to the console window while returning false from FullCheck displays an error message to the console window.

internal partial class Program
{
    static async Task Main(string[] args)
    {

        await Setup();

        await using var context = new Context();

        var tableNames = ApplicationConfiguration.GetTableNames();

        if (DbContextHelpers.FullCheck(context, tableNames))
        {
            var ops = new DataOperations(context);
            var customers = ops.GetCustomers();

            AnsiConsole.MarkupLine(
                ObjectDumper.Dump(customers)
                    .Replace("{Customer}", "{[cyan]Customer[/]}")
                    .Replace("{ContactType}", "{[yellow]ContactType[/]}")
                    .Replace("{Gender}", "{[yellow]Gender[/]}")
                    .Replace("null --> Circular reference detected", "") // no need to show this
                    );
        }
        else
        {
            AnsiConsole.MarkupLine("[red]Create the database and run the script under[/][cyan] Data scripts[/]");
        }

        // Allows a developer to see the log file before the console closes e.g. in VS Code
        await Log.CloseAndFlushAsync();

        ExitPrompt();

    }
}
Enter fullscreen mode Exit fullscreen mode

Project setup

First, build and run to receive an error message. Next, create the database CustomerDatabase. SQLEXPRESS, or if using a named instance of SQL Server, make sure to change the connection string in appsetings.json.

Under the folder Data scripts run the script CreateAndPopulate.sql.

Run the project again to see a list of customers.

Modifications / other uses

Rather than returning Booleans, a developer using these methods in an application could throw exceptions. As coded, used in ValidateOnStart in an ASP.Net Core project.

Get the row count for each table

The best way to achieve this is to write an SQL statement, get the connection string from an instance of the DbContext, and use the NuGet package Dapper, as shown below.

class EntityQueryHelpers
{
    public static async Task<List<TableInfo>> GetTableRowCountsAsync()
    {
        await using var context = new Context();
        await using var connection = context.Database.GetDbConnection();
        const string sql = """
                           SELECT 
                               TableSchema = s.name,
                               Name = t.name,
                               [RowCount] = p.rows
                           FROM sys.tables t
                           INNER JOIN sys.schemas s ON t.schema_id = s.schema_id
                           INNER JOIN sys.indexes i ON t.object_id = i.object_id
                           INNER JOIN sys.partitions p ON i.object_id = p.object_id AND i.index_id = p.index_id
                           WHERE t.is_ms_shipped = 0
                           GROUP BY t.name, s.name, p.rows
                           ORDER BY s.name, t.name;
                           """;

        return (await connection.QueryAsync<TableInfo>(sql)).AsList();
    }
}

public class TableInfo
{
    public string TableSchema { get; set; }
    public string Name { get; set; }
    public long RowCount { get; set; }
}
Enter fullscreen mode Exit fullscreen mode

Another version for specific tables which in this version reads the connection string from appsettings.json.

public static async Task<List<TableInfo>> GetTableRowCountsAsync(params string[] tableNames)
{

    /*
     * Config is defined in GlobalUsings.cs
     * ConnectionStrings is defined in ConsoleConfigurationLibrary
     */
    var connectionString = Config.Configuration.JsonRoot()
        .GetSection(nameof(ConnectionStrings))
        .GetValue<string>(nameof(ConnectionStrings.MainConnection));

    IDbConnection cn = new SqlConnection(connectionString);

    const string sql =
        """
        SELECT 
            TableSchema = s.name,
            Name = t.name,
            [RowCount] = p.rows
        FROM sys.tables t
        INNER JOIN sys.schemas s ON t.schema_id = s.schema_id
        INNER JOIN sys.indexes i ON t.object_id = i.object_id
        INNER JOIN sys.partitions p ON i.object_id = p.object_id AND i.index_id = p.index_id
        WHERE t.is_ms_shipped = 0
        AND t.name IN @TableNames
        GROUP BY t.name, s.name, p.rows
        ORDER BY s.name, t.name;
        """;

    return (await cn.QueryAsync<TableInfo>(sql, new { TableNames = tableNames })).ToList();
}
Enter fullscreen mode Exit fullscreen mode

Source code

Console project Source code Class project Source code

Summary

Methods for EF Core have been presented to assist with validating a database and tables, which can help determine if an environment is properly set up. What is out of scope is checking if the required records are in the tables.

Top comments (0)