Поиск  
Always will be ready notify the world about expectations as easy as possible: job change page
Apr 18

PostgreSQL in C# .NET with Npgsql, Dapper, and Entity Framework: The complete guide

PostgreSQL in C# .NET with Npgsql, Dapper, and Entity Framework: The complete guide
Автор:
Источник:
Просмотров:
17765

One of the most used databases these days is PostgreSQL (aka Postgres). Its accomplishments include being the most popular DB [among professional developers] according to Stack Overflow survey of 2022, the database in all of the fastest TechEmpower benchmarks, and being the most advanced open source database (self-proclaimed).

We’ll talk today about combining Postgres with my favorite language and runtime: C# and .NET. Just because Postgres isn’t a first-party Microsoft database, like SQL Server, doesn’t mean you don’t have full support for it in C#. You absolutely do, and in this article, you’ll see modern ways to do just that. You’ll see how to interact between Postgres and C# using the standard data provider library Npgsql, use it with Dapper, and even with Entity Framework. We’ll go over the basic operations like creating a table, inserting rows, deleting, and updating records using all of these frameworks.

Installing Postgres and pgAdmin

To get started, go to postgresql.org/download and get the latest PostgreSQL version.

During the installation, you’ll be able to install pgAdmin, which is the management tool for Postgres. You can also use a different management tool like the excellent DataGrip (which is paid unfortunately).

Once installed, you can run pgAdmin and interact with the service. The first order of business is to create a server if it wasn’t created already.

pgAdmin create server

Once a server is running, go ahead and create your first database.

pgAdmin create database

As soon as you have your server and a database, you can start using it with C#.

Using Postgres in C# with the standard data provider (Npgsql)

The official package to use Postgres is Npgsql. Add it to your project with dotnet add package Npgsql.

Once installed, you can use this code to create your first table.

var con = new NpgsqlConnection(
    connectionString: "Server=localhost;Port=5432;User Id=postgres;Password=passw0rd;Database=testdb;");
con.Open();
using var cmd = new NpgsqlCommand();
cmd.Connection = con;

cmd.CommandText = $"DROP TABLE IF EXISTS teachers");
await cmd.ExecuteNonQueryAsync();
cmd.CommandText= "CREATE TABLE teachers (id SERIAL PRIMARY KEY," +
    "first_name VARCHAR(255)," +
    "last_name VARCHAR(255)," +
    "subject VARCHAR(255)," +
    "salary INT)");
await cmd.ExecuteNonQueryAsync();

Looking at the connection string, you can see that my database’s name is testdb, the user name is postgres and the password is passw0rd. Port 5432 is the default port number for Postgres. You can see these settings in pgAdmin by right-clicking on the server -> Properties.

PostgreSQL 15 server properties

Let’s see some basic operations.

Adding records with Npgsql

// using the same 'cmd' (NpgsqlCommand) object from before
cmd.CommandText = $"INSERT INTO teachers (first_name, last_name, subject, salary) VALUES ('Severus', 'Snape', 'Potions', 10000)";
await cmd.ExecuteNonQueryAsync();

The above query can also be written as a parameterized query, this way:

cmd.CommandText = $"INSERT INTO teachers (first_name, last_name, subject, salary) VALUES (@firstName, @lastName, @subject, @salary)";
cmd.Parameters.AddWithValue("firstName", "Severus");
cmd.Parameters.AddWithValue("lastName", "Snape");
cmd.Parameters.AddWithValue("subject", "Potions");
cmd.Parameters.AddWithValue("salary", 10000);
await cmd.ExecuteNonQueryAsync();

Updating records with Npgsql

cmd.CommandText = $"UPDATE teachers" +
    $" SET first_name='{newValues.FirstName}'," +
    $"last_name='{newValues.LastName}'," +
    $"subject='{newValues.Subject}'," +
    $"salary={newValues.Salary}" +
    $" WHERE id = {id}";
await cmd.ExecuteNonQueryAsync();

Deleting records with Npgsql

cmd.CommandText = $"DELETE FROM teachers WHERE id = {id}";
await cmd.ExecuteNonQueryAsync();

Reading records with Npgsql

public async Task<IEnumerable<Teacher>> GetBySubject(string subject)
{
    cmd.CommandText = $"SELECT * FROM teachers WHERE subject='{subject}'";
    NpgsqlDataReader reader = await cmd.ExecuteReaderAsync();
    var result = new List<Teacher>();
    while (await reader.ReadAsync())
    {
        result.Add(new Teacher(
            id: (int)reader["id"],
            first_name: reader[1] as string, // column index can be used
            last_name: reader.GetString(2), // another syntax option
            subject: reader["subject"] as string,
            salary: (int)reader["salary"]));

    }

    return result;
}

I left reading for last to compare to the way the same looks using Dapper.

Dapper

If you’re not familiar with Dapper, it’s a simple object mapper that allows you to query databases with regular SQL and map them to C# objects. It’s somewhere halfway between ADO.NET where you parse responses yourself to a full-blown ORM like Entity Framework.

To use Dapper with Postgres, install the NuGet package Dapper with dotnet add package Dapper.

Dapper adds extension methods like QueryAsync on the connection object NpgsqlConnection, so you don’t need to initialize anything extra.

The main advantage of Dapper is the auto-mapping in read operations. Instead of using NpgsqlDataReader, you can create a constructor with parameters matching column names and the code becomes much nicer. Here’s an example of implementing GetBySubject with Dapper:

public async Task<IEnumerable<Teacher>> GetBySubject(string subject)
{
    return await con.QueryAsync<Teacher>(
        $"SELECT * FROM {Teacher.TableName} WHERE subject='{subject}'");
}

For the above code to work, the class Teacher should have a constructor accepting all the table columns as parameters, like so:

public Teacher(int id, string first_name, string last_name, string subject, int salary)
{
    Id = id;
    FirstName = first_name;
    LastName = last_name;
    Subject = subject;
    Salary = salary;
}

In addition to reading records, you can use Dapper to execute any SQL command, like CREATE, UPDATE, or DELETE. Here are a couple of examples:

public async Task DeleteById(int id)
{
    await _connection.ExecuteAsync($"DELETE FROM teachers WHERE id = {id}");
}

public async Task Insert(Teacher teacher)
{
    string sqlCommand = $"INSERT INTO {Teacher.TableName} (first_name, last_name, subject, salary) VALUES (@firstName, @lastName, @Subject, @salary)";

    var queryArguments = new
    {
        firstName = teacher.FirstName,
        lastName = teacher.LastName,
        subject = teacher.Subject,
        salary = teacher.Salary,
            
    };

    await _connection.ExecuteAsync(sqlCommand, queryArguments);
}

ExecuteAsync is another Dapper extension method.

As you can see, Dapper has a slightly nicer syntax than Npgsql, especially for parameterized commands.

Entity Framework

Entity Framework is a big full-featured object-relational mapping framework (ORM) by Microsoft. Although it’s usually associated with SQL Server, EF supports PostgreSQL with the NuGet package Npgsql.EntityFrameworkCore.PostgreSQL.

To initialize EF for your Postgres database, you’ll have to create a DbContext class for the database and tables as follows:

public class SchoolContext : DbContext
{
    public DbSet<Teacher> Teachers { get; set; }
 
    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    {
        optionsBuilder.UseNpgsql(connectionString:
           "Server=localhost;Port=5432;User Id=postgres;Password=passw0rd;Database=testdb;");
        base.OnConfiguring(optionsBuilder);
    }
}

[Table("teachers")]
public class Teacher : DbContext
{
    [System.ComponentModel.DataAnnotations.Key]
    [Column("id")]
    public int Id { get; internal set; }

    [Column("first_name")]
    public string FirstName { get; internal set; }

    [Column("last_name")]
    public string LastName { get; internal set; }

    [Column("subject")]
    public string Subject { get; internal set; }

    [Column("salary")]
    public int Salary { get; internal set; }
}

Note that I’m using the attribute System.ComponentModel.DataAnnotations.Schema.Table to declare the name of the table and System.ComponentModel.DataAnnotations.Schema.Column to declare the names of the columns as they are set in the database. This allows me to have different field names in my class, ones that use the C# convention and start with an uppercase letter. Alternatively, you can name the properties the same as the columns are named in PostgreSQL, or you can modify these names by overriding the OnModelCreating method as follows:

// -------- Alternative to using [Column] and [Table] attributes
public class SchoolContext : DbContext
{
    public DbSet<Teacher> Teachers { get; set; }
 
    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    {
        optionsBuilder.UseNpgsql(
            connectionString: "Server=localhost;Port=5432;User Id=postgres;Password=passw0rd;Database=testdb;");
        base.OnConfiguring(optionsBuilder);
    }
    
    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.Entity<Teacher>(e => e.ToTable("teachers"));
        modelBuilder.Entity<Teacher>(entity =>
        {
            entity.Property(e => e.Id)
                                .HasColumnName("id")
                                .HasDefaultValueSql("nextval('account.item_id_seq'::regclass)");
            entity.Property(e => e.FirstName).IsRequired().HasColumnName("first_name");
            entity.Property(e => e.LastName).IsRequired().HasColumnName("last_name");
            entity.Property(e => e.Subject).IsRequired().HasColumnName("subject");
            entity.Property(e => e.Salary).HasColumnName("salary");
        });

        base.OnModelCreating(modelBuilder);
    }   
}

With the above configuration, you could have removed the [Column] attribute from the field names in Teacher class, which can be nice if you want separation of concerns or if your model is in external code that you can’t modify.

If you’re using ASP.NET Core, you can inject the DbContext the following way, instead of overriding OnConfiguring.

// ----------- Alternative to overriding OnConfiguring
services.AddDbContext<SchoolContext>(
    optionsBuilder => optionsBuilder.UseNpgsql(connectionString)
);

Reading records with Entity Framework

Here are a couple of ways to read rows in the database.

public async Task<Teacher> GetById(int id)
{
    using (var db = new SchoolContext())
    {
        return await db.Teachers.FindAsync(id);
    }
}

public async Task<List<Teacher>> GetBySubject(string subject)
{
    using (var db = new SchoolContext())
    {
        return await db.Teachers.Where(t => t.Subject == subject).ToListAsync();
    }
}

Inserting records with Entity Framework

Don’t forget to call SaveChangesAsync at the end of inserts and updates.

public async Task Insert(Teacher teacher)
{
    using var db = new SchoolContext();
    await db.Teachers.AddAsync(teacher);
    await db.SaveChangesAsync();
}

Updating records with Entity Framework

public async Task UpdateSalary(int id, int newSalary)
{
    using (var db = new SchoolContext())
    {
        var t = await db.Teachers.FindAsync(id);
        t.Salary = newSalary;
        await db.SaveChangesAsync();
    }
}

public async Task UpdateAllValues(int id, Teacher teacher)
{
    using (var db = new SchoolContext())
    {
        var t = await db.Teachers.FindAsync(id);
        teacher.Id = t.Id; // you might need to return to original value
        db.Entry(t).CurrentValues.SetValues(teacher);
        await db.SaveChangesAsync();
    }
}

Once everything is set up, Entity Framework is really nice to work with. You can stay in the comfort zone of C# code, treating your database tables like any other .NET collection. Well, mostly. On the other hand, EF has its downsides. That additional wrapper can lead to performance issues, trouble in the initial setup, difficulties with schema changes, an incomplete feature set, and downright wrong behavior if there are bugs in the EF implementation.

Finishing up

As you can see, PostgreSQL is fully supported by C# and you can interact with it in different ways, including Dapper and Entity Framework. Everything is mostly easy and straightforward, which describes well the overall experience with PostgreSQL.

You might be interested in another article I wrote about Postgres performance: Performance benchmarks of PostgreSQL in .NET: Npgsql vs Dapper vs Entity Framework

Похожее
Jun 7
Author: Dev Leader
In software engineering, composition plays an important role in object-oriented programming. If you’ve come across other articles or videos I’ve put out about object-oriented programming, you’ll know that I try to push composition over inheritance as much as I can....
Oct 26, 2023
Author: Alex Maher
Entity Framework Core Features in 2023 EF Core 2023 has rolled out some pretty cool stuff, and I’m excited to share it with you. So, grab a cup of coffee, and let’s get started! 1. Cosmos DB Provider Improvements Azure...
Jun 2, 2023
Cyclomatic complexity is a code metric (integer value 1 or more) used to measure how complex a function/method is. It does not take into account lines of code but instead considers complexity to be the distinct paths through a function....
May 13, 2023
Author: Juan Alberto España Garcia
Introduction to Async and Await in C# Asynchronous programming has come a long way in C#. Prior to the introduction of async and await, developers had to rely on callbacks, events and other techniques like the BeginXXX/EndXXX pattern or BackgroundWorker....
Написать сообщение
Тип
Почта
Имя
*Сообщение