Flowsy.Db.Conventions 0.2.0

There is a newer version of this package available.
See the version list below for details.
dotnet add package Flowsy.Db.Conventions --version 0.2.0                
NuGet\Install-Package Flowsy.Db.Conventions -Version 0.2.0                
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="Flowsy.Db.Conventions" Version="0.2.0" />                
For projects that support PackageReference, copy this XML node into the project file to reference the package.
paket add Flowsy.Db.Conventions --version 0.2.0                
#r "nuget: Flowsy.Db.Conventions, 0.2.0"                
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
// Install Flowsy.Db.Conventions as a Cake Addin
#addin nuget:?package=Flowsy.Db.Conventions&version=0.2.0

// Install Flowsy.Db.Conventions as a Cake Tool
#tool nuget:?package=Flowsy.Db.Conventions&version=0.2.0                

Flowsy Db Conventions

This package is a wrapper for Dapper's extension methods on the IDbConnection interface, but with a focus on naming and formatting conventions for database objects like tables, columns, routines and parameters.

Behind the concepts and data structures of this package is the philosophy of team collaboration under a series of conventions that allow everybody to play by the same rules when it comes to naming database objects.

By always following the same conventions, you can make your code more readable and easy to maintain.

For instance, you and your teammates could define the following conventions for database object names:

Object Type Style Prefix Suffix Example
Table lower_snake_case None None tbl_user
Column lower_snake_case None None user_id
Routine lower_snake_case fn_ None fn_users_by_city
Parameter lower_snake_case p_ None p_city_name

Once you all agree on the conventions, you can configure them in a single place and use the extension methods provided by this package to interact with the database.

Defining Conventions

Given the following class and enum type:

namespace MyApp.Domain;

// Assuming you have a table named "user"
// with columns "user_id", "forename", "surname", "email_address", "city_name" and "status"
public class User
{
    public Guid UserId { get; set; }
    public string Forename { get; set; }
    public string Surname { get; set; }
    public string EmailAddress { get; set; }
    public string CityName { get; set; }
    public UserStatus Status { get; set; }
    // other properties...
}

public enum UserStatus
{
    Active,
    Inactive
}

You could define the conventions for the database objects like this:

using System.Text.Json;
using Flowsy.Core;
using Flowsy.Db.Conventions;

var entityTypes = System.Reflection.Assembly.GetExecutingAssembly().GetTypes()
    .Where(t => t.Namespace == "MyApp.Domain")
    .ToArray()

// Note: This example assumes the underlying database supports sotred functions
    
DbConventionSet.Default
    .ForConnections("MyConnection")
    .ForSchemas("public")
    .ForTables(CaseStyle.LowerSnakeCase) // lower_snake_case with no prefix or suffix
    .ForColumns(CaseStyle.LowerSnakeCase, entityTypes) // lower_snake_case with no prefix or suffix
    .ForRoutines(
        DbRoutineType.StoredFunction, // use stored functions (you can also use DbRoutineType.StoredProcedure)
        CaseStyle.LowerSnakeCase, // use lower_snake_case for routine names
        "fn_" // use a "fn_" prefix for routine names
    ) 
    .ForParameters(CaseStyle.LowerSnakeCase, "p_", null, (_, routineType, parameterName, _) => // lower_snake_case with a "p_" prefix
    {
        // This lambda expression allows you to customize the parameter placeholder used when calling a routine
        return routineType switch
        {
            DbRoutineType.StoredFunction => $"{parameterName} => @{parameterName}", // parameter placeholder for stored functions
            _ => $"@{parameterName}" // parameter placeholder for stored procedures
        };
    })
    .ForEnums(DbEnumFormat.Name)
    .ForDateTimeOffsets(DbDateTimeOffsetFormat.Utc)
    .ForPagination(500)
    .ForJson(new JsonSerializerOptions());

Executing Queries

Given the previous configuration, you could execute a query like this:

using Flowsy.Db.Conventions.Extensions;
using MyApp.Domain.User;

// GetConnection is a fictitious method to get an instance of IDbConnection
using var connection = GetConnection(); 
connection.Open();

var users = await connection.QueryAsync<User>(
    "UsersByCity", // simple function name translated to: select * from fn_users_by_city(p_city_name => @p_city_name, p_status => @p_status)
    new
    {
        CityName = "New York", // translated to "@p_city_name"
        Status = UserStatus.Active // parameter name translated to "@p_status" and value to "Active" (the enum value name)
    },
    CancellationToken.None
);

// users will be a collection of User objects holding the results of the query

Which would result in the invokation of the following function in the database:

create or replace function public.fn_users_by_city(
    p_city_name varchar,
    p_status varchar
) returns table (
    user_id uuid,
    forename varchar,
    surname varchar,
    email_address varchar,
    city_name varchar,
    status public.user_status
) as 
    $$
    begin
        return query
            select
                user_id, -- will be mapped to a property named UserId
                forename, -- will be mapped to a property named Forename
                surname, -- will be mapped to a property named Surname
                email_address, -- will be mapped to a property named EmailAddress
                city_name, -- will be mapped to a property named CityName
                status -- will be mapped to a property named Status
            from public.user
            where
                city_name = p_city_name and
                status = p_status::public.user_status -- assuming user_status is an enum type
        ;
    end;
    $$ language plpgsql;

The QueryAsync method will automatically map the name of the function and parameters according to the conventions defined in the DbConventionSet.Default instance.

Take a look at the different extension methods provided by this package on the IDbConnection interface to see how you can interact with the database using the conventions you defined. Some of those method overloads allow you to pass a DbConventionSet instance to override the default conventions for a single query execution.

Besides, you can use a DbConventionSet instance to apply your conventions to specific names to build queries manually:

var conventions = DbConventionSet.Default.Clone();

// Customize the conventions for a specific scenario
conventions
    .ForTables(CaseStyle.UpperSnakeCase) //  use UPPER_SNAKE_CASE for table names with no prefix or suffix
    .ForColumns(CaseStyle.UpperSnakeCase) //  use UPPER_SNAKE_CASE for table names with no prefix or suffix
    .ForRoutines(DbRoutineType.StoredProcedure, CaseStyle.UpperSnakeCase, "SP_") // use stored procedures, UPPER_SNAKE_CASE and a "SP_" prefix for routines
    ;

// You can pass a single string or an array of strings to the Apply method

var tableNames = conventions.Tables.Apply("Customer", "PurchaseOrder")
// tableNames will be a IEnumerable<string> with the values: "CUSTOMER", "PURCHASE_ORDER"

var columnNames = conventions.Columns.Apply("PurchaseOrderId", "CustomerId", "CreationUserEmail");
// columnNames will be a IEnumerable<string> with the values: "PURCHASE_ORDER_ID", "CUSTOMER_ID", "CREATION_USER_EMAIL"

var routineName = conventions.Routines.Apply("GetPurchaseOrdersByCustomer");
// routineName will be SP_GET_PURCHASE_ORDERS_BY_CUSTOMER

Extension Methods

Besides the QueryAsync method mentioned above, this package provides the following extension methods on the IDbConnection interface, all of them with overloads that allow you to pass a DbConventionSet instance to override the default conventions for a single query execution:

  • Execute: Executes a query and returns the number of affected rows.
  • ExecuteAsync: Asynchronously executes a query and returns the number of affected rows.
  • Query: Executes a query and returns a collection of objects of the specified type.
  • QueryAsync: Asynchronously executes a query and returns a collection of objects of the specified type.
  • QueryFirst: Executes a query and returns the first result or throws an exception if none is found. The result is mapped to the specified type.
  • QueryFirstAsync: Asynchronously executes a query and returns the first result or throws an exception if none is found. The result is mapped to the specified type.
  • QueryFirstOrDefault: Executes a query and returns the first result or the default value if none is found. The result is mapped to the specified type.
  • QueryFirstOrDefaultAsync: Asynchronously executes a query and returns the first result or the default value if none is found. The result is mapped to the specified type.
  • QueryMultiple: Executes a query and returns multiple result sets.
  • QueryMultipleAsync: Asynchronously executes a query and returns multiple result sets.
  • QuerySingle: Executes a query and returns a single result or throws an exception if none or more than one is found. The result is mapped to the specified type.
  • QuerySingleAsync: Asynchronously executes a query and returns a single result or throws an exception if none or more than one is found. The result is mapped to the specified type.
  • QuerySingleOrDefault: Executes a query and returns a single result or the default value if none or more than one is found. The result is mapped to the specified type.
  • QuerySingleOrDefaultAsync: Asynchronously executes a query and returns a single result or the default value if none or more than one is found. The result is mapped to the specified type.

Connection Factory

The DbConnectionFactory class implements the IDbConnectionFactory interface and provides a way to create instances of IDbConnection. Its constructor receives a list of DbConnectionConfiguration objects identifed by a connection name and containing a connection string and the corresponding provider invariant name.

The following code snippets show how to use the DbConnectionFactory class in your applications.

Defining Connection Configurations

{
    "Databases": {
      "MyDatabase1": {
        "ConnectionString": "Server=pg.example.com;Database=my_database1;User Id=myuser;Password=mypassword;",
        "ProviderInvariantName": "Npgsql"
      },
      "MyDatabase2": {
        "ConnectionString": "Server=mssql.example.com;Database=my_database2;User Id=myuser;Password=mypassword;",
        "ProviderInvariantName": "Microsoft.Data.SqlClient"
      }
    }
}

Manually Creating Connection Factories

When you manually create instances of the DbConnectionFactory class, you must make sure to dispose of them when they are no longer needed.

using Flowsy.Db.Conventions;
using Microsoft.Data.SqlClient;
using Npgsql;

public class SomeDataService
{
    private readonly _connectionConfigurations;
    private bool providerFactoryRegistered;
    
    public SomeDataService()
    {
        if (!providerFactoryRegistered)
        {
            // Register database provider factories required by the application
            DbProviderFactories.RegisterFactory("Npgsql", NpgsqlFactory.Instance);
            DbProviderFactories.RegisterFactory("Microsoft.Data.SqlClient", SqlClientFactory.Instance);
            providerFactoryRegistered = true;
        }
        
        // Load connection configurations from appsettings.json
        _connectionConfigurations = builder.Configuration
            .GetSection("Databases")
            .GetChildren()
            .Select(section => DbConnectionConfiguration.LoadFrom(section))
            .ToArray();;
    }
    
    public async Task<IEnumerable<SomeEntity>> GetSomeEntitiesAsync(CancellationToken cancellationToken)
    {
        // Create an instance of the DbConnectionFactory class.
        // Declare the connectionFactory variable in a scope that allows you to dispose of it when it is no longer needed
        using var connectionFactory = new DbConnectionFactory(_connectionConfigurations);
        
        // Get an instance of IDbConnection
        // Do not dispose the connection, the connection factory will handle disposal of connections obtained by calling the GetConnection method.
        var connection = connectionFactory.GetConnection("MyDatabase1");
        
        // Execute a query
        return await connection.QueryAsync<SomeData>(
            "some_stored_routine", 
            new
            {
                SomeParameter = "some_value"
            }, 
            DbConventionSet.Default,
            cancellationToken
            );
        
        // The connectionFactory instance will be disposed when the "using" block ends.
    }
}

Using Dependency Injection

To facilitate the management of database connections, you can register the DbConnectionFactory class as a scoped service in the dependency injection container of your application. For instance, in an ASP.NET application, you could register the DbConnectionFactory like this:

Program.cs
using Flowsy.Db.Conventions;
using Microsoft.Data.SqlClient;
using Npgsql;
using System.Data.Common;
// More namespaces...

var builder = WebApplication.CreateBuilder(args);
// Register services
// ...

// Register database provider factories required by the application
DbProviderFactories.RegisterFactory("Npgsql", NpgsqlFactory.Instance);
DbProviderFactories.RegisterFactory("Microsoft.Data.SqlClient", SqlClientFactory.Instance);

// Load connection configurations from appsettings.json
var connectionConfigurations = builder.Configuration
        .GetSection("Databases")
        .GetChildren()
        .Select(section => DbConnectionConfiguration.LoadFrom(section))
        .ToArray();
// Each connection configuration will be identified by a connection name based on the key of the corresponding configuration section.

builder.Services.AddConnectionFactory(connectionConfigurations);

var app = builder.Build();
// Activate services
app.Run();
Data Access Layer
// This class is a service that depends on the IDbConnectionFactory registered in the dependency injection container.
// The IDbConnectionFactory instance will exist in the context of a given user request, so it will be available to other scoped and transient services.
public class SomeDataService
{
    private readonly IDbConnectionFactory _connectionFactory;

    public SomeDataService(IDbConnectionFactory connectionFactory)
    {
        _connectionFactory = connectionFactory;
    }

    public async Task<IEnumerable<SomeEntity>> GetSomeEntitiesAsync(CancellationToken cancellationToken)
    {
        // Do not dispose the connection, the connection factory will handle disposal of connections obtained by calling the GetConnection method.
        var connection = _connectionFactory.GetConnection("MyDatabase1");

        return await connection.QueryAsync<SomeData>(
            "some_stored_routine", 
            new
            {
                SomeParameter = "some_value"
            }, 
            DbConventionSet.Default,
            cancellationToken
            );
    }
}

As you can see, by using dependency injection you can avoid the need to manually create and dispose of instances of the DbConnectionFactory class, you just need to inject the IDbConnectionFactory instance into the services that need to interact with the database.

Product Compatible and additional computed target framework versions.
.NET net5.0 was computed.  net5.0-windows was computed.  net6.0 was computed.  net6.0-android was computed.  net6.0-ios was computed.  net6.0-maccatalyst was computed.  net6.0-macos was computed.  net6.0-tvos was computed.  net6.0-windows was computed.  net7.0 was computed.  net7.0-android was computed.  net7.0-ios was computed.  net7.0-maccatalyst was computed.  net7.0-macos was computed.  net7.0-tvos was computed.  net7.0-windows was computed.  net8.0 was computed.  net8.0-android was computed.  net8.0-browser was computed.  net8.0-ios was computed.  net8.0-maccatalyst was computed.  net8.0-macos was computed.  net8.0-tvos was computed.  net8.0-windows was computed. 
.NET Core netcoreapp3.0 was computed.  netcoreapp3.1 was computed. 
.NET Standard netstandard2.1 is compatible. 
MonoAndroid monoandroid was computed. 
MonoMac monomac was computed. 
MonoTouch monotouch was computed. 
Tizen tizen60 was computed. 
Xamarin.iOS xamarinios was computed. 
Xamarin.Mac xamarinmac was computed. 
Xamarin.TVOS xamarintvos was computed. 
Xamarin.WatchOS xamarinwatchos was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages (1)

Showing the top 1 NuGet packages that depend on Flowsy.Db.Conventions:

Package Downloads
Flowsy.Db.Repository.Sql

Implementations of data repositories and related operations in the context of a unit of work using SQL databases.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last updated
0.2.1 117 9/14/2024
0.2.0 107 8/25/2024
0.1.0 101 8/24/2024