APP8

Download as pdf or txt
Download as pdf or txt
You are on page 1of 46

Blazor Server Course

Dapper

2023
Introduction

• Dapper is a Micro-ORM which helps to map plain query output to domain classes.
• It can run plain SQL queries with great performance.
• Dapper is a lightweight framework that supports all major databases like SQLite, Firebird, Oracle, MySQL,
and SQL Server.
• It does not have database-specific implementation.
• Dapper is built by the StackOverflow team and released as open source.
Create a Class Library

• Code reusability is very important in the software development process.


• Using the already-written code can save time and resources and reduce redundancy.
• A Class library is a good example of code reusability.
• In object-oriented programming, a class library is a collection of prewritten classes or coded templates
which contains the related code.
• When we finish a class library, we can decide whether you want to distribute it as a third-party
component or whether you want to include it as a DLL with one or more applications.

http://www.authorcode.com/creating-a-class-library-with-c-and-net-core-in-visual-studio-code/
Traditional ORM [Entity Framework] vs Micro ORM
Traditional ORM [Entity Framework] vs Micro ORM

• Performance of SELECT mapping over 500 iterations


Database setup

• Create a Cards Database and run the following script:

DROP TABLE IF EXISTS users;


CREATE TABLE users (
id int PRIMARY KEY IDENTITY, DROP TABLE IF EXISTS tags;
name varchar(200), CREATE TABLE tags(
email varchar(300) UNIQUE id int PRIMARY KEY IDENTITY,
);
DROP TABLE IF EXISTS cards;
name varchar(100) NOT NULL
CREATE TABLE cards ( )
id int PRIMARY KEY IDENTITY, DROP TABLE IF EXISTS card_tags;
_id varchar(300), CREATE TABLE card_tags(
name varchar(200) NOT NULL, id_card int,
description varchar(300), id_tag int,
image varchar(max),
date datetime NOT NULL, PRIMARY KEY(id_card,id_tag),
time_in_minutes int NOT NULL, FOREIGN KEY (id_card) REFERENCES cards(id),
owner int NOT NULL, FOREIGN KEY (id_tag) REFERENCES tags(id)
image_action int, )
FOREIGN KEY (owner) REFERENCES users(id)
);

https://gist.github.com/brunobmo/df1c0096080dbb4eac2c8bbc9afed199
Create a Class Library

• Create the class library in the solution:


• dotnet new classlib -o DataLayer
• Add Reference to the main Project:
• dotnet add app/app.csproj reference DataLayer/DataLayer.csproj
Dapper setup

• Install Dapper in the class library:


• dotnet add package Dapper --version 2.0.123
• Additionally, install the following package:
• dotnet add package Microsoft.Extensions.Configuration --version 7.0.0
AppSettings

• Add to the appsettings.json file the connection string to access your SQL Server database:
"ConnectionStrings": {
"Default": ”(...)"
}
SqlDataAcess

• Let’s create a class to encapsulate the logic behind configuring acess to database:
• The purpose of this class, SqlDataAccess, is to provide a means of accessing a SQL database and
performing data operations.
• It implements the ISqlDataAccess interface, indicating that it adheres to a specific contract defined by
that interface.
SqlDataAcess

public class SqlDataAccess : ISqlDataAccess{


private readonly IConfiguration _config;
public string ConnectionStringName { get; set; } = "Default"; LoadData<T, U> is used to execute a SQL query
public SqlDataAccess(IConfiguration config){ and return the resulting data as a list of objects
_config = config; of type T.
}
public async Task<List<T>> LoadData<T, U>(string sql, U parameters){
string? connectionString = _config.GetConnectionString(ConnectionStringName);
using (IDbConnection connection = new SqlConnection(connectionString)){
var data = await connection.QueryAsync<T>(sql, parameters);
return data.ToList();
}
}
public async Task SaveData<T>(string sql, T parameters){
SaveData method executes an SQL
string? connectionString = _config.GetConnectionString(ConnectionStringName);
query asynchronously to save
using (IDbConnection connection = new SqlConnection(connectionString)){
data to the database. It retrieves
await connection.ExecuteAsync(sql, parameters);
the connection string from the
}
configuration
}
}

https://gist.github.com/brunobmo/6400ce751f23c1947bd1bb9c0d610992
SqlDataAcess

• Let’s create an interface for the SqlDataAcess class:

namespace DataLayer;

public interface ISqlDataAccess{


string ConnectionStringName { get; set; }
Task<List<T>> LoadData<T, U>(string sql, U parameters);
Task SaveData<T>(string sql, T parameters);
}

https://gist.github.com/brunobmo/6d4d56757b5ea1b44b6f3793b469da47
SqlDataAcess

• Let’s now create CardModel DTO to support data handling:

namespace DataLayer; The CardModel.cs class is a Data Transfer Object (DTO) since it
public record CardModel is primarily used for transferring data between the class
{ library and the Blazor app.
public string? _Id { get; set; }
public int Id { get; set; }
public string Name { get; set; } = "";
public string Description { get; set; } = "";
public string Image { get; set; } = "";
public DateTime? Date { get; set; }
public int TimeInMinutes { get; set; }
public UserModel? Owner { get; set; } = new UserModel();

public List<TagModel>? Tags { get; set; } = new List<TagModel>();


}

https://gist.github.com/brunobmo/1047be85df7935f796f9198c9eaf1495
SqlDataAcess

• Let’s now create TagModel DTO to support data handling:

namespace DataLayer;

public record TagModel


{
public int Id { get; set; }

public string Name { get; set; } = "";


}

https://gist.github.com/brunobmo/f1a3baec1cf929c51f0535ddfa48aa4c
SqlDataAcess

• Let’s now create User DTO to support data handling:

namespace DataLayer;
public record UserModel
{
int Id { get;}
string _Id { get; set;} = "";
string Name { get; set;} = "";
string Email { get; set;} = "";
}

https://gist.github.com/brunobmo/ce3e780fce276c836bd9ba30d3a77070
Repository Skeleton

• We are using a repository pattern to encapsulate data access.


• Many people like the repository pattern because it provides good separation of concerns, easy mocking,
and good encapsulation.
• It's going to allow to easily swap out different implementations
• The idea with this pattern is to have a generic abstract way for the app to work with the data layer
without being bothered if the implementation is towards a local database or towards an online API.

https://medium.com/@pererikbergman/repository-design-pattern-e28c0f3e4a30
Repository Skeleton

• Let’s create the interface for the repository:


namespace DataLayer;

public interface ICardRepository{


Task<CardModel> Find(int id);

Task<List<CardModel>> FindAll();

Task Update(CardModel card);

Task Remove(int id);

Task<CardModel> Add(CardModel card);


}

https://gist.github.com/brunobmo/5fd3a4728161e20c73ffa738f0d65496
Repository Skeleton

• Let’s implement the class with, for now, the (limited) findAll behavior:
namespace DataLayer;
public class CardRepository : ICardRepository{
private ISqlDataAccess _db;
public CardRepository(ISqlDataAccess db){
_db = db;
}
public Task<CardModel> Find(int id){
throw new NotImplementedException();
}
public Task<List<CardModel>> FindAll(){
string sql = "select * from Cards";
return _db.LoadData<CardModel, dynamic>(sql, new { });
}
(...)

https://gist.github.com/brunobmo/d18a372636ac170caf8bbb71897a8c44
Configure main app – Program.cs

• Let’s add to Program.cs file the SQLDataAcess and the repository:

builder.Services.AddTransient<ISqlDataAccess, SqlDataAccess>();
builder.Services.AddTransient<ICardRepository, CardRepository>();

Transient objects are always different; a new instance is


provided to every controller and every service
Configure main app – Program.cs

• Now we can test if it works!


• Modify FetchData.razor do see the results.
• Example:
Configure main app – Program.cs

• Now we can test if it works!


• In FetchData.razor page changes fetch data from the database:
@page "/fetchdata"
@using DataLayer
@inject ICardRepository _db
(...)
@if (cards == null){
(...) <p><em>Loading...</em></p>
}else{
<table class="table"><thead>

@code {
private List<DataLayer.CardModel> cards;

protected override async Task OnInitializedAsync()


{
cards = await _db.FindAll();
}
}
Loading data with filter

• Let’s create the find method (CardRepository) for a Card:

public async Task<CardModel> Find(int id)


{
string sql = "SELECT * FROM Cards WHERE id = @Id";
var card = await _db.LoadData<CardModel, dynamic>(sql, new { Id = id });
return card.SingleOrDefault();
}

https://gist.github.com/brunobmo/511d69489d69ad4b3d6ded9cafd1d02f
Adding data

• Let’s create the Add method (CardRepository):


public async Task<CardModel> Add(CardModel card)
{
string sql =
@"INSERT INTO dbo.cards(name,description,image, date, time_in_minutes, owner)
VALUES(@Name,@Description, @Image, @Date, @TimeInMinutes,1);" For now, we are hardcoding
+ "SELECT CAST(SCOPE_IDENTITY() as int)"; ownerId
int id = await _db.SaveDataWithReturn<int, CardModel>(sql, card);
return new CardModel()
{
Id = id, Records are immutable We are executing two
Name = card.Name, statements, the first one to
Description = card.Description, insert and the second one
Image = card.Image, to retrieve the inserted id
Date = card.Date,
TimeInMinutes = card.TimeInMinutes
};
}

https://gist.github.com/brunobmo/7021ac189b3fc2f66be4261c3431e59f
Update Data

• Let’s create the update method (CardRepository):

public async Task Update(CardModel card)


{
string sql =
"UPDATE dbo.cards SET name = @Name, description = @Description, image = @Image, date =
@Date, time_in_minutes = @TimeInMinutes WHERE id = @Id";
await _db.SaveData<CardModel>(sql, card);
}

• Modify FectData.razor and test FindAll, Find, Add, Update for simple objects.
• Implement the Remove.

https://gist.github.com/brunobmo/4b33ecdfc9a13047de7390557d877ade
Using Relationships With Dapper

• Notice that we don’t have the Owner data or the Tags data.
• Dapper provides a feature called Multi mapping to map data to multiple objects explicitly and nested
objects.
• The splitOn parameter tells Dapper what column(s) to use to split the data into multiple objects.

https://www.learndapper.com/relationships
Using Relationships With Dapper - One to Many

• Let’s add LoadData to SqlDataAccess and ISqlDataAccess for loading Cards and the User.

<T1, T2, U>: These are generic type parameters. They allow the
Task<List<T1>> LoadData<T1, T2, U>( method to work with different types without specifying them directly.
string sql, T1 and T2 represent the types of objects that the method will operate
U parameters, on, while U represents the type of the parameters parameter.
Func<T1, T2, T1> mappingFunction,
string splitOn,
Func<T1, object> groupByFunc = null,
Func<IGrouping<object, T1>, T1> selectFunc = null
);

It represents a grouping Func<IGrouping<object, T1>, T1> selectFunc = null: This


Func<T1, T2, T1> mappingFunction: This
function that can be used to parameter is another optional delegate (function) that
parameter is a delegate (function) that
group the data by a specific takes an IGrouping<object, T1> and returns an object
represents a mapping function that is
property. It is set to null by of type T1. It represents a selection function that can
responsible for mapping the data retrieved from
default, indicating that be used to perform a final selection on the grouped
the data source into an instance of type T1.
grouping is not required data.
https://gist.github.com/brunobmo/9a5ec86ea0b6488c5ea2f32b0da69300
Using Relationships With Dapper – One to Many

• Let’s create FindAll method to return Cards and Users


public async Task<List<CardModel>> FindAll()
{
string sql = "SELECT a.id, a.name, description, image, date, time_in_minutes, b.id, b.name,
b.email
FROM [dbo].[cards] AS a JOIN dbo.users AS b ON a.[owner]=b.id ";
return await _db.LoadData<CardModel, UserModel, dynamic>(
sql,
a. The sql variable contains the SQL query to execute.
new { },
b. The second argument is an anonymous object { } representing
(card, user) =>{
any parameters that might be passed to the SQL query.
card.Owner = user;
c. The third argument is a lambda function (card, user) => { ... } that
return card;
maps the result rows to CardModel and UserModel objects. Inside
},
the lambda function, the card and user parameters represent the
"Id”
columns retrieved from the respective tables.
);
d. The fourth argument is a string "Id", which specifies the
}
property to be used as the identifier for the parent-child
relationship. It indicates that the Id property of the CardModel
should be used.

https://gist.github.com/brunobmo/442633482221029c6f6a4dec2d5bf866
Using Relationships With Dapper Many to Many

• The multi-mapping works with multiple relationships. we are loading Cards, Tags and User
public async Task<List<CardModel>> FindAll(){
string sql = @"SELECT a.id, a.name, description, image, date, time_in_minutes, u.id,
u.name, u.email, c.id, c.name
FROM [dbo].[cards] AS a LEFT JOIN dbo.card_tags AS b ON a.[id]=b.id_card
LEFT JOIN dbo.tags AS c ON b.id_tag = c.id
LEFT JOIN dbo.users AS u ON u.id = a.[owner]";
return await _db.LoadData<CardModel, UserModel , TagModel, dynamic>(sql,new { },(card,
owner, tag) =>{
card.Owner= owner; specifying the column names to be used
card.Tags.Add(tag); for grouping the results.
return card;},
"id, id", specifies how to extract the ID from a
p => p.Id, grouped set of records.
g =>{ responsible for grouping the
var groupedCard = g.First(); cards and tags based on the
groupedCard.Tags = g.Select(p => p.Tags.Single()).ToList(); specified column names
return groupedCard;
});
}
https://gist.github.com/brunobmo/b3d589410f220d3bdc13fe5821c35500
Using Relationships With Dapper Many to Many

• Let’s add the new method to SqlDataAccess:


public async Task<List<T1>> LoadData<T1, T2, T3, U>(
string sql,
U parameters,
Func<T1, T2, T3, T1> mappingFunction,
string splitOn,
Func<T1, object> groupByFunc = null,
Func<IGrouping<object, T1>, T1> selectFunc = null
){
string? connectionString = _config.GetConnectionString(ConnectionStringName);
using (IDbConnection connection = new SqlConnection(connectionString)){
var data = await connection.QueryAsync<T1, T2, T3,
T1>(sql,mappingFunction,splitOn: splitOn);
if (groupByFunc != null && selectFunc != null){
var result = data.GroupBy(groupByFunc).Select(selectFunc);
return result.ToList();
}
return data.ToList();
}
}
https://gist.github.com/brunobmo/82bb5c926e2fe0251c231f913071c461
Using Relationships With Dapper Many to Many

• Let’s add the new method to ISqlDataAccess:

Task<List<T1>> LoadData<T1, T2, T3, U>(


string sql,
U parameters,
Func<T1, T2, T3, T1> mappingFunction,
string splitOn,
Func<T1, object> groupByFunc = null,
Func<IGrouping<object, T1>, T1> selectFunc = null
);
• Test these new methods using FetchData.razor

https://gist.github.com/brunobmo/52f3f7a4642c60942d06deb74b8c66cd
Updating Find (one) using Multiple results

• The Dapper QueryMultiple method allows you to select multiple results from a database query.
• That feature is very useful for selecting multiple result sets at once, thus avoiding unnecessary round trips to
the database server.
• Let’s add a new method to SqlDataAccess and respective interface in ISqlDataAccess :

public async Task<Tuple<T1, List<T2>, T3>> LoadData<T1 , T2, T3, U>(string sql, U parameters){
string? connectionString = _config.GetConnectionString(ConnectionStringName);

using (IDbConnection connection = new SqlConnection(connectionString)){


using(var multipleResults = await connection.QueryMultipleAsync(sql, parameters)){
var _t1 = multipleResults.Read<T1>();
var _t2 = multipleResults.Read<T2>().ToList();
var _t3 = multipleResults.Read<T3>();
if(_t1 != null && _t2 != null){
return new Tuple<T1, List<T2>, T3>(_t1.SingleOrDefault(), _t2,_t3.SingleOrDefault());
}
}

}
return null;
}

https://gist.github.com/brunobmo/52f3f7a4642c60942d06deb74b8c66cd
Updating Find (one) using Multiple results

• Let’s change Find method in CardRepository to get Card, User and Tags using QueryMultiple method
public async Task<CardModel> Find(int id){
string sql =
"SELECT * FROM Users WHERE id = (SELECT owner FROM Cards WHERE id=@Id);"
+ "SELECT * FROM Card_Tags WHERE id_card = @Id;"
+ "SELECT id,name,description,image,date, time_in_minutes FROM Cards WHERE id = @Id;";

Tuple<UserModel, List<TagModel>, CardModel> result = await this._db.LoadData<


UserModel,
TagModel,
CardModel,
dynamic
>(sql, new { Id = id }); Queries should match the types

CardModel card = result.Item3;


card.Tags.AddRange(result.Item2);
card.Owner = result.Item1;
return card;
}

https://gist.github.com/brunobmo/5bdf88d034754032c8e6b73a6a288b67
Create-update-delete with complex objects

• Add method will deal with simple objects while save method will deal with complex objects
• First we are going to create a new field in CardModel and Tag class (we are assuming the card owner
always exist and was not changed in this context):

public bool IsNew => this.Id == default(int);

Default value for int is 0


If it is 0, the record is new
Create-update-delete with complex objects

• In TagModel we are adding IsNew and IsDeleted properties to control when Tags are added or removed
from a card

public bool IsNew {get;set;} = true;


public bool IsDeleted { get; set; } = false;
Create-update-delete with complex objects

• We are creating a new method in CardRepository named: Save;


• Before that, let’s create TagRepository class with methods for Add Tag, Update, Delete and Find Tag :

namespace DataLayer;

public interface ITagRepository


{
Task<TagModel> Add(TagModel tag);

Task Update(TagModel tag, CardModel car);

Task Delete(TagModel tag, CardModel car);

Task<TagModel> Find(TagModel tag);


}

https://gist.github.com/brunobmo/2516ddcd30261ff8f93607e31f331a67
Create-update-delete with complex objects

• TagRepository

namespace DataLayer;

public class TagRepository : ITagRepository


{
private ISqlDataAccess _db;

public TagRepository(ISqlDataAccess db)


{
_db = db;
}
(...)
}
Create-update-delete with complex objects

• Add method
public async Task<TagModel> Add(TagModel tag)
{
string sql =
@"INSERT INTO dbo.tags(name) VALUES(@name);" +
"SELECT CAST(SCOPE_IDENTITY() as int)";
int id = await _db.SaveData<int, TagModel>(sql, tag);
return new TagModel() { Id = id, Name = tag.Name };
}

• Update

public async Task Update(TagModel tag, CardModel car)


{
string sql = "INSERT INTO dbo.card_tags(id_card, id_tag) VALUES(@id_card, @id_tag)";
await _db.SaveData<dynamic>(sql, new { id_card = car.Id, id_tag = tag.Id });
}
Create-update-delete with complex objects

• Delete method
public async Task Delete(TagModel tag, CardModel car)
{
string sql = "DELETE FROM dbo.card_tags WHERE id_card = @id_card AND id_tag = @id_tag";
await _db.SaveData<dynamic>(sql, new { id_card = car.Id, id_tag = tag.Id });
} Ensure you have ON
DELETE CASCADE in the
FK
• Find method

public async Task<TagModel> Find(TagModel tag)


{
string sql = "SELECT * FROM Tags WHERE id = @Id";
var returnedTag = await _db.LoadData<TagModel, dynamic>(sql, new { Id = tag.Id });
return returnedTag.FirstOrDefault();
}

https://gist.github.com/brunobmo/c2e61edca8d1ccdf32d703fe7c7f6b14
Create-update-delete with complex objects

• Let’s change CardRepository to use TagRepository instance:


(...)
private ITagRepository _tagRepository;

public CardRepository(ISqlDataAccess db)


{
_db = db;
_tagRepository = new TagRepository(db);
}
(...)
Create-update-delete with complex objects

• Let’s create Save method in the CardRepository


public async Task<CardModel> Save(CardModel card){
if (card.IsNew){
CardModel result = await this.Add(card);
card.Id = result.Id;
}else{
await this.Update(card);
}
List<TagModel> updatedTags = new List<TagModel>();
foreach (var tag in card.Tags){
if (tag.IsNew){
TagModel result = await _tagRepository.Add(tag);
await _tagRepository.Update(result, card);
updatedTags.Add(result);
}else if (!tag.IsDeleted){
updatedTags.Add(tag);
}
}
(...)
Create-update-delete with complex objects

• Let’s create Save method in the CardRepository


(...)
foreach (var tag in card.Tags.Where(t => t.IsDeleted)){
await _tagRepository.Delete(tag, card);
}
card.Tags = updatedTags;
return card;
}

https://gist.github.com/brunobmo/338a5c5dbcd1ae31686d30c674b2fc4b
Create-update-delete with complex objects

• Everything may have worked just now, but remember, we're updating multiple rows here.
• So we need to wrap this method in a transaction in case anything goes wrong.
• Let's use a TransactionScope for this.

public async Task<CardModel> Save(CardModel card)


{
using (var txScope = new
TransactionScope(TransactionScopeAsyncFlowOption.Enabled)){
(...)
}
}

https://gist.github.com/brunobmo/3d9988eff989ff8475819654caca517b
Create-update-delete with complex objects

• Test all methods using FetchData.razor.


Find
• Use test Data to test each method: Update with pre-
Delete
defined data

Add Card with pré-


deifned data
Summary

• Dapper is a Micro-ORM which helps to map plain query output to domain classes.
• It can run plain SQL queries with great performance.
• A class library is used for reusability and separation of concerns;
• A DataAcess library can be used to separate application logic and data layer
Summary

• The repository pattern provides good separation of concerns, easy mocking, and good encapsulation.
• Dapper provides several ways for mapping objects, QueryMultiple is one of those methods
Blazor Server Course
Dapper

2023

You might also like