XSLT存档  

不及格的程序员-八神

 查看分类:  ASP.NET XML/XSLT JavaScripT   我的MSN空间Blog

Welcome To Learn Dapper

This site is for developers who want to learn how to use Dapper - The micro ORM created by the people behind Stack Overflow.

What is Dapper? 

Dapper is an open-source object-relational mapping (ORM) library for .NET and .NET Core applications. The library allows developers quickly and easily access data from databases without the need to write tedious code. Dapper allows you to execute raw SQL queries, map the results to objects, and execute stored procedures, among other things. It is available as a NuGet package.

  • Dapper is lightweight and fast, making it an ideal choice for applications that require low latency and high performance.
  • It is a simple yet powerful object mapping tool for any .NET language, such as C#, that enables developers to quickly and easily map query results from ADO.NET data readers to instances of business objects.
  • It has excellent support for both asynchronous and synchronous database queries and batching multiple queries together into a single call.
  • Additionally, dapper supports parameterized queries to help protect against SQL injection attacks.

Does Dapper support .NET Core? 

Yes, Dapper has been supporting .NET Core since the release of version 1.50 in July 2016. Dapper is a cross-platform .NET library, which means that it can be used on any platform that supports .NET, including .NET Core.

Does Dapper support C#? 

Yes, you can use Dapper with C# as multiple other languages, such as VB.NET and F#. In fact, Dapper was written in C# and is a popular choice for data access in C# applications because of its simplicity and efficiency.

Is Dapper an ORM? 

Dapper falls into a family of tools known as micro-ORMs. These tools perform only a subset of the functionality of full-blown Object Relations Mappers, such as Entity Framework Core, but Dapper is known for its speed and simple implementation compared to others. The following table provides a general idea of the capabilities that you can expect to find in a micro ORM compared to an ORM:

 Micro ORMORM
Map queries to objects    
Caching results    
Change tracking  1  
SQL generation  2  
Identity management    
Association management    
Lazy loading    
Unit of work support    
Database migrations    
     

Dapper concentrates its efforts on the O and M of ORM - Object Mapping.

[1] Some extensions have been added to Dapper that provide the minimal change-tracking capability

[2] Dapper does generate SQL but in a limited fashion. Third-party libraries such as Dapper Plus can generate the full SQL for insertupdate, and delete statements

When Should You Use Dapper? 

When deciding whether to use Dapper or not, one should bear in mind the primary reason for its existence - performance. The original developers of Dapper were using Entity Framework Core's predecessor - the short-lived Linq to SQL. However, they found that query performance wasn't good enough for the increasing traffic that the site in question (Stack Overflow) was experiencing, so they wrote their own micro ORM.

Dapper is, therefore, a good choice in scenarios where read-only data changes frequently and is requested often. It is particularly good in stateless scenarios (e.g. the web) where there is no need to persist complex object graphs in memory for any duration.

Dapper does not translate queries written in .NET languages to SQL like a full-blown ORM. So you need to be comfortable writing queries in SQL or have someone write them for you.

Dapper has no real expectations about the schema of your database. It is not reliant on conventions in the same way as Entity Framework Core, so Dapper is also a good choice where the database structure isn't particularly normalized.

Dapper works with an ADO.NET IDbConnection object, which means that it will work with any database system for which there is an ADO.NET provider.

There is no reason why you cannot use both an ORM and a micro ORM in the same project.

What does Dapper do? 

Here is a standard ADO.NET C# code for retrieving data from a database and materializing it as a collection of Product objects:

var sql = "select * from products";
var products = new List<Product>();
using (var connection = new SqlConnection(connString))
{
    connection.Open();
    using (var command = new SqlCommand(sql, connection))
    {
        using (var reader = command.ExecuteReader())
        {
            var product = new Product
            {
                ProductId = reader.GetInt32(reader.GetOrdinal("ProductId")),
                ProductName = reader.GetString(reader.GetOrdinal("ProductName")),
                SupplierId = reader.GetInt32(reader.GetOrdinal("SupplierId")),
                CategoryId = reader.GetInt32(reader.GetOrdinal("CategoryId")),
                QuantityPerUnit = reader.GetString(reader.GetOrdinal("QuantityPerUnit")),
                UnitPrice = reader.GetDecimal(reader.GetOrdinal("UnitPrice")),
                UnitsInStock = reader.GetInt16(reader.GetOrdinal("UnitsInStock")),
                UnitsOnOrder = reader.GetInt16(reader.GetOrdinal("UnitsOnOrder")),
                ReorderLevel = reader.GetInt16(reader.GetOrdinal("ReorderLevel")),
                Discontinued = reader.GetBoolean(reader.GetOrdinal("Discontinued")),
                DiscontinuedDate = reader.GetDateTime(reader.GetOrdinal("DiscontinuedDate"))
            };
            products.Add(product);
        }
    }
}

At its most basic level, Dapper replaces the highlighted block of assignment code in the example above with the following:

products = connection.Query<Product>(sql).ToList();

Dapper also creates the command and opens the connection if needed. If you use Dapper to manage basic assignments like this, it will save you hours. However, Dapper is capable of doing quite a bit more.

 

public static List<Entity_XSJL> getXSJL_ByDapper(string strConn, string accountSetsID, string accountSetsName)
        {
            using (System.Data.SqlClient.SqlConnection sqlConn = new SqlConnection(strConn))
            {
                using (SqlCommand sqlcmd = new SqlCommand())
                {
                    sqlcmd.CommandType = System.Data.CommandType.StoredProcedure;
                    sqlcmd.CommandText = string.Format("{0}..pGansu_Data_XSJL", accountSetsID);
                    sqlcmd.Connection = sqlConn;
                    using (var resultQuery = sqlConn.QueryMultiple(new CommandDefinition(
                        sqlcmd.CommandText,
                        new { dFromDate = Utils.BaseInfo.defaultStartDate },
                        null,
                        null,
                        sqlcmd.CommandType,
                        CommandFlags.None)))
                    {
                        List<Entity_XSJL> listResult = new List<Entity_XSJL>(100);
                        var timestamp = IoriUtilityLib.Iori.TimeUtil.GetTimeStamp();
                        var rowMain = resultQuery.Read<Entity_XSJL.Req>().ToList();
                        var rowSubAll = resultQuery.Read<Entity_XSJL.FhdInfoItem>().ToList();
                        foreach (var rowParent in rowMain)
                        {
                            rowParent.timestamp = timestamp;
                            rowParent.dataSet.fhdInfo = rowSubAll.Where(exp => exp.cBillCode == rowParent.event_id).ToList();
                            Entity_XSJL entity = new Entity_XSJL() {
                                req = rowParent,
                                AccountSetsName = accountSetsName,
                                AccountSetsID = accountSetsID
                            };
                            listResult.Add(entity);
                        }
                        return listResult;
                    }
                }
            }
        }

 


 

Handling Multiple Resultsets

While not a very common usage scenario, it can come handy sometimes

It may happen that you are interested in executing a batch of SQL commands whose results produce not only one resultset, but more than one. This helps to avoid doing additional roundtrips to the database. For example you can return suppliers and customers at once executing the following statement in the same command:

SELECT … FROM dbo.Suppliers; 
SELECT … FROM dbo.Customers;

I’m not really fond of this approach, and if the two objects are independent from each other (like in the sample) I would rather prefer two separate asynchronous (parallel would even be better) calls to the database, but you don’t always have this option, or maybe you’re in a case where a plain and simple approach is preferred.

In any case, if you have multiple resultset, Dapper can help you, via the QueryMultiple method:

var results = conn.QueryMultiple(@"
SELECT Id, FirstName, LastName FROM dbo.Users;
SELECT Id, CompanyName FROM dbo.Companies
");
var users = results.Read<User>();
var companies = results.Read<Company>();

QueryMultiple returns a GridReader object that allows you to access the available resultset in sequence, from the first to the last. All you have to do is call the Read method, as shown in the above code, to access each results. Read behave exactly like the Query method we already discussed in the first article. In fact it supports all the specialized method that also Query supports

  • ReadFirst
  • ReadFirstOrDefault
  • ReadSingle
  • ReadSingleOrDefault

all the Read methods can also be called asynchronously via ReadAsync.

A typical Use Case

So, even if not so common, a use case for such method exits, and is related to the necessity to create complex object with data coming from different tables.

Let’s go with another typical example here: Customers and Orders. You want to load a customer object with all related orders.

As one can guess, the Customer object has a list of Orders:

If you’re a database guy you probably immediately thought to solve the problem by joining Customers and Orders table

which will produce the following result:

Unfortunately there are a few problems with this approach from a developer perspective, even if the correct one from a database perspective.

The first problem is that we had to create column name alias, since both the tables have the Id column. By default Dapper maps columns to properties by name matching, and thus the introduced alias will prevent this native behavior to work properly. As we’ll see in a future article, we can handle this case, but it will make our code a little bit more complex. And I don’t like making the code complex of such small thing: introducing complexity when it is not really needed is always a bad idea.

The second problem is that the resultset have has many rows as many orders the customer placed. This in turn means that customer data (Id and Name in the example) is returned for each order. Now, beside the fact that this will waste bandwidth, impacting on overall performance and resource usage, we also have to make sure that we actually create just one Customer object. Dapper won’t do that for us, so, again, additional complexity needs to be introduced.

As you can understand, the two aforementioned problems prevent the usage of the SELECT…JOIN approach with Dapper. The correct solution is to query the Customer table and create the single Customer object and then query the Order table, create the Order objects and the link them to the created customer.

To avoid doing two roundtrips to the database to get customer and orders data separately, the multiple resultset feature can be used:

Of course in case you’re dealing with multiple commands like above you may also want to make sure you wrap everything in a transaction, to assure data consistency. Transaction support will be discussed in a future post but in case you want to go forward, know that Dapper supports transaction via the TransactionScope option.

Samples

As usual an example that shows how to use the discussed feature is available here:

yorek/dapper-samples

dapper-samples - Samples that shows how to use Dapper .NET

github.com

 

Conclusions

The described feature helps to solve a very specific problem, keeping the performance high and the code clean, but has a big limitation: what if, in addition to the Order we also decided to get the Order Items? The discussed feature wouldn’t have helped and we would had to do a separate query to the database, for each order, to get the related items. Really not good for performance. Luckily if your database supports JSON, (SQL Server 2016 and after and Azure SQL both supports it) there are a much better and powerful way to elegantly solve the problem of mapping rows to complex objects. We’ll discuss it very soon.

What’s next

Next article will be dedicated to a feature somehow related with what discussed here: “Multiple Mapping” or automatically mapping a single row to multiple objects.


Querying Multiple Results With Dapper

You can execute multiple queries in a single SQL statement using the QueryMultiple method (if the database provider supports it). Once executed, you can map the returned results to multiple objects using methods such as Read<T>ReadFirst<T>ReadSingle<T> and more.

The Dapper QueryMultiple method allows you to select multiple results from a database query. That feature is very useful for selecting multiple results sets at once, thus avoiding unnecessary round trips to the database server.

  • The QueryMultiple method provides a simple way of fetching data stored in different tables or views with a single database query.
  • By using dapper's QueryMultiple method, you can select multiple rows from different tables in one go, saving time and resources.
  • Dapper's QueryMultiple method allows you to map results from multiple queries into strongly typed collections that can be easily consumed within your application code.
  • Using Dapper QueryMultiple is an efficient way to select multiple results from a single database query.
  • It saves time and resources by avoiding unnecessary round trips to the database server and eliminates the need for complex SQL queries.
MethodDescription
QueryMultiple Execute multiple queries within a single database command and return a GridReader to map the results to multiple objects
QueryMultipleAsync Execute multiple queries within a single database command and return a GridReader to map the results to multiple objects asynchronously

After using the QueryMultiple or QueryMultipleAsync method, you can use the following methods with the results:

MethodDescription
ReadReadAsync Returns an enumerable of dynamic types
Read<T>ReadAsync<T> Returns an enumerable of the type specified by the T parameter
ReadFirstReadFirstAsync Returns the first row as a dynamic type
ReadFirst<T>ReadFirstAsync<T> Returns the first row as an instance of the type specified by the T type parameter
ReadFirstOrDefaultReadFirstOrDefaultAsync Returns the first row as a dynamic type or null if no results are returned
ReadFirstOrDefault<T>ReadFirstOrDefaultAsync<T> Returns the first row as an instance of the type specified by the T type parameter or null if no results are returned
ReadSingleReadSingleAsync Use when only one row is expected to be returned. Returns a dynamic type
ReadSingle<T>ReadSingleAsync<T> Use when only one row is expected to be returned. Returns an instance of the type specified by the T type parameter
ReadSingleOrDefaultReadSingleOrDefaultAsync Use when zero or one row is expected to be returned. Returns a dynamic type or null
ReadSingleOrDefault<T>ReadSingleOrDefaultAsync<T> Use when zero or one row is expected to be returned. Returns an instance of the type specified by the T type parameter or null

Dapper QueryMultiple

The following example demonstrates how to use Dapper QueryMultiple to execute multiple SQL statements in one query.

language-csharp
 | 
string sql = @"
SELECT * FROM Invoices WHERE InvoiceID = @InvoiceID;
SELECT * FROM InvoiceItems WHERE InvoiceID = @InvoiceID;
";

using (var connection = new SqlConnection(connectionString))
{
    using (var multi = connection.QueryMultiple(sql, new {InvoiceID = 1}))
    {
        var invoice = multi.First<Invoice>();
        var invoiceItems = multi.Read<InvoiceItem>().ToList();
    }
}

With this, we can execute multiple SQL statements in one query and read each result set from the GridReader object returned by the QueryMultiple method. That can help increase the performance of your application in cases where multiple queries need to be executed and their results combined.

Using Dapper QueryMultiple is a great way to simplify data access and ensure that your application runs quickly and efficiently. With its easy-to-use API, you can quickly set up complex queries and access the data you need.

Dapper QueryMultipleAsync

The asynchronous version of the QueryMultiple<T> method is QueryMultipleAsync<T>:

language-csharp
 | 
string sql = @"
SELECT * FROM Invoices WHERE InvoiceID = @InvoiceID;
SELECT * FROM InvoiceItems WHERE InvoiceID = @InvoiceID;
";

using (var connection = new SqlConnection(connectionString))
{
    using (var multi = await connection.QueryMultipleAsync(sql, new {InvoiceID = 1}))
    {
        var invoice = await multi.FirstAsync<Invoice>();
        var invoiceItems = await multi.ReadAsync<InvoiceItem>().ToList();
    }
}

 


 

Multiple Result Sets for SQL-Server (C#) including Dapper

Working with database with C# (3 Part Series)

Learn how to read reference table from SQL-Server using a single method. What is shown provides an efficient way to either use a connection, command objects to read data via a SqlDataReader for conventional work using methods from SqlClient and also Dapper which requires two lines of code to read data and one line of code to store data into list.

Goal

To read from three reference table in a modified version of Microsoft NorthWind database, Categories, ContactType and Countries tables.

In all code samples all records are read from each table, in some cases not all records may be needed, simply change the SQL SELECT statement with a WHERE clause. Also, the same goes for columns.

Project type used

A console project is used although the code provided will work in any project type.

Creating the database

Open SSMS (SQL-Server Management Studio), create a new database named Northwind2020, run the script under the Scripts folder.

Required models

Each model has properties matching columns in the tables we will read from, not all columns are included in some tables.

The override ToString can be helpful in some cases for instance in a control or component that needs to know what to display, for example a ComboBox.

public class Categories
{
    public int CategoryId { get; set; }
    public string CategoryName { get; set; }
    public override string ToString() => CategoryName;
}
public class ContactType
{
    public int ContactTypeIdentifier { get; set; }
    public string ContactTitle { get; set; }
    public override string ToString() => ContactTitle;
}
public class Countries
{
    public int CountryIdentifier { get; set; }
    public string Name { get; set; }
    public override string ToString() => Name;
}
 

Master model

Which is used to return data from two methods which could return three classes rather than one although it makes sense to return just one instance of a class.

public class ReferenceTables
{
    public List<Categories> CategoriesList { get; set; } = new List<Categories>();
    public List<ContactType> ContactTypesList { get; set; } = new List<ContactType>();
    public List<Countries> CountriesList { get; set; } = new List<Countries>();
}
 

Connection string

First off, the connection string is stored in appsettings.json and read via a NuGet package ConfigurationLibrary.

Convention method to read multiple tables

The key is that the command object can handle multiple SELECT statements as done with the following. These statements are used for the following code sample and two other code samples.

  • await using var reader = await cmd.ExecuteReaderAsync(); reads the data.
  • The first while statements reads from Categories table as it is the first statement from the statements in SqlStatements.ReferenceTableStatements.
  • await reader.NextResultAsync(); tells the reader to work on ContactType table
  • The second while reads ContactType records
  • await reader.NextResultAsync(); tells the reader to work on Countries table
  • The third while reads Countries table data.
  • If there are no exceptions, referenceTables has all three tables data populated, otherwise the call to this method tells the caller a failure occurred and returns the Exception which can be logged.
internal class SqlStatements
{
    /// <summary>
    /// Statements to read reference tables for Categories, ContactType and Countries tables.
    /// </summary>
    public static string ReferenceTableStatements =>
        """
        SELECT CategoryID,CategoryName FROM dbo.Categories;
        SELECT ContactTypeIdentifier,ContactTitle FROM dbo.ContactType;
        SELECT CountryIdentifier,[Name] FROM dbo.Countries;
        """;
}
 
public static async Task<(bool success, Exception exception)> GetReferenceTables(ReferenceTables referenceTables)
{

    await using SqlConnection cn = new(ConnectionString());
    await using SqlCommand cmd = new()
    {
        Connection = cn, CommandText = SqlStatements.ReferenceTableStatements
    };


    try
    {
        await cn.OpenAsync();
        await using var reader = await cmd.ExecuteReaderAsync();

        while (await reader.ReadAsync())
        {
            referenceTables.CategoriesList.Add(new Categories()
            {
                CategoryId = reader.GetInt32(0), 
                CategoryName = reader.GetString(1)
            });
        }

        await reader.NextResultAsync();

        while (await reader.ReadAsync())
        {
            referenceTables.ContactTypesList.Add(new ContactType()
            {
                ContactTypeIdentifier = reader.GetInt32(0), 
                ContactTitle = reader.GetString(1)
            });
        }

        await reader.NextResultAsync();

        while (await reader.ReadAsync())
        {
            referenceTables.CountriesList.Add(new Countries()
            {
                CountryIdentifier = reader.GetInt32(0), 
                Name = reader.GetString(1)
            });
        }

        return (true, null);
    }
    catch (Exception localException)
    {
        return (false, localException);
    }
}
 

DataSet method to read multiple tables

In this sample, the same SQL statement is feed to the command object as done in the prior example, a SqlDataAdapter is linked to the command object which populates all three DataTables in a DataSet.

The adapter internally populates the three tables with a private method FillInternal.

public static async Task<(bool success, Exception exception, DataSet dataSet)> GetReferenceTablesDataSet()
{
    DataSet ds = new();

    try
    {
        SqlDataAdapter adapter = new();
        await using SqlConnection cn = new(ConnectionString());
        SqlCommand command = new(SqlStatements.ReferenceTableStatements, cn);
        adapter.SelectCommand = command;

        adapter.Fill(ds);

        ds.Tables[0].TableName = "Categories";
        ds.Tables[1].TableName = "ContactType";
        ds.Tables[2].TableName = "Countries";

        return (true, null, ds);
    }
    catch (Exception localException)
    {
        return (false, localException, null);
    }
}
 

Dapper method to read multiple tables

Using Dapper is the most efficient method, no command obect required as per below, instead we use QueryMultipleAsync to read data using the same SQL statements in the last two samples.

Once QueryMultipleAsync has executed the list are populated and available as in the first sample.

public static async Task GetReferenceTablesDapper(ReferenceTables referenceTables)
{
    await using SqlConnection cn = new(ConnectionString());
    SqlMapper.GridReader results = await cn.QueryMultipleAsync(SqlStatements.ReferenceTableStatements);
    referenceTables.CategoriesList = results.Read<Categories>().ToList();
    referenceTables.ContactTypesList = results.Read<ContactType>().ToList();
    referenceTables.CountriesList = results.Read<Countries>().ToList();
}
 

Program.cs

Each sample is broken down into separate methods but data is not displayed. Place a break point at the end of each method and view data in the local window.

using GitHubSamples.Classes;

namespace GitHubSamples;

internal partial class Program
{
    static async Task Main(string[] args)
    {
        await StandardSample();
        await DataSetSample();
        await DapperSample();

        AnsiConsole.MarkupLine("[yellow]Press ENTER to exit[/]");
        Console.ReadLine();
    }

    private static async Task StandardSample()
    {
        ReferenceTables referenceTables = new();
        var (success, exception) = await DataOperations.GetReferenceTables(referenceTables);
        Console.WriteLine(success
            ? "Success reading to classes"
            : $"Class operation failed with \n{exception.Message}");
    }

    private static async Task DapperSample()
    {
        ReferenceTables referenceTables = new();
        await DataOperations.GetReferenceTablesDapper(referenceTables);
    }

    private static async Task DataSetSample()
    {
        var (success, exception, dataSet) = await DataOperations.GetReferenceTablesDataSet();
        Console.WriteLine(success
            ? "Success reading to DataSet"
            : $"DataSet operation failed with \n{exception.Message}");
    }
}
 

Example for the Dapper sample

ScreenShot showing results from Dapper

Stored procedure sample

Some developer rather using stored procedures, Dapper makes this easy.

Some benefits

  • Execution Plan Retention and Reuse (SP are compiled and their execution plan is cached and used again to when the same SP is executed again)
  • Network bandwidth conservation
  • Improved security
  • Sharing of application logic between applications

Some disadvantages

  • Difficult to debug
  • A DBA may be required to access the SQL and write a better stored procedure. This will automatically incur added cost.
  • Complex stored procedures will not always port to upgraded versions of the same database. This is specially true in case of moving from one database type(Oracle) to another database type(MS SQL Server).

I have worked for several agencies/companies were the security was so tight a developer could not even see a stored procedure definition, that made it difficult to debug. Working as a contractor with pre-existing stored procedures can be a nightmare.

Here is the Stored Procedure

SET ANSI_NULLS ON;
GO
SET QUOTED_IDENTIFIER ON;
GO
CREATE PROCEDURE dbo.usp_SelectCatCountryContactType
AS
BEGIN
    SELECT CategoryID,
           CategoryName
    FROM dbo.Categories;
    SELECT ContactTypeIdentifier,
           ContactTitle
    FROM dbo.ContactType;
    SELECT CountryIdentifier,
           Name
    FROM dbo.Countries;
END;

GO
 

Tips on creating stored procedures for novice developer

  • Find a good course on a web site offering courses on stored procedures and/or on Pluralsight.
  • Purchase Redgate SQL-Prompt which will create stored procedures from an existing SQL statement in SSMS. My favorite features are intellisense for writing SQL, formatting of SQL, statement refactoring are just a few of the cool features in SQL-Prompt.

Code

public static async Task GetReferenceTablesDapperStoredProcedure(ReferenceTables referenceTables)
{
    await using SqlConnection cn = new(ConnectionString());

    SqlMapper.GridReader results = await cn.QueryMultipleAsync(
        "usp_SelectCatCountryContactType", 
        null,
        null,
        null,
        CommandType.StoredProcedure);

    referenceTables.CategoriesList = results.Read<Categories>().ToList();
    referenceTables.ContactTypesList = results.Read<ContactType>().ToList();
    referenceTables.CountriesList = results.Read<Countries>().ToList();
}
 

Produces the same results as the first Dapper example above.

Summary

What has been presented can assist with reading two or more reference tables at once without the need for multiple connection and command objects using multiple data readers and best of all for some an introduction into using Dappper.

NuGet packages

  • ConfigurationLibrary for reading connection strings from appsetting when using Windows forms, Consol project where a developer is not using dependency injection.
  • Dapper is an open-source object-relational mapping (ORM) library for .NET and .NET Core applications. The library allows developers quickly and easily access data from databases without the need to write tedious code.
  • Microsoft.Data.SqlClient namespace is the .NET Data Provider for SQL Server.

Source code

In the following project which can be cloned from the following GitHub repository.

See also

The art of Deconstructing which explains how in this article data is returned to methods in Program.cs

posted on 2023-06-19 10:42  不及格的程序员-八神  阅读(17)  评论(0编辑  收藏  举报