Earn More by Sharing What You Love
Do you create content for developers working with .NET, C#, Java, Python, or Node.js? Turn your expertise into extra income!

Tim Corey
1h 9m 11s
Os delegates em C# são um recurso poderoso, mas muitos desenvolvedores não estão familiarizados com seu uso eficaz. O vídeo de Tim Corey sobre " Delegates em C# - Uma demonstração prática, incluindo Action e Func " oferece uma explicação completa do que são delegates, como usá-los e por que são úteis.
Este artigo fornecerá a você as informações especializadas de Tim sobre delegates em C#, oferecendo uma explicação clara de seu uso e aplicações práticas. Você aprenderá como os delegates podem aumentar a flexibilidade e a eficiência do seu código, com exemplos como seu uso em um sistema de carrinho de compras.
Tim apresenta o conceito de delegates, destacando seu poder e versatilidade em C#. Ele garante aos espectadores que, apesar de alguma terminologia intimidante, a base dos delegados é simples. Tim pretende desmistificar os delegados e abordar tipos especiais como func e action.
Tim configura um aplicativo de demonstração para ilustrar o uso de delegados. A solução contém três projetos: uma interface de usuário de console, uma biblioteca de demonstração e uma interface de usuário WinForm. Inicialmente, o foco será na interface do usuário do console e na biblioteca de demonstrações.
using System;
using System.Collections.Generic;
using System.Linq;
namespace ConsoleUI
{
class Program
{
static ShoppingCartModel cart = new ShoppingCartModel();
static void Main(string[] args)
{
PopulateCartWithDemoData();
Console.WriteLine($"The total for the cart is {cart.GenerateTotal():C2}");
Console.ReadLine();
}
private static void PopulateCartWithDemoData()
{
cart.Items.Add(new ProductModel { ItemName = "Cereal", Price = 3.63M });
cart.Items.Add(new ProductModel { ItemName = "Milk", Price = 2.95M });
cart.Items.Add(new ProductModel { ItemName = "Strawberries", Price = 7.51M });
cart.Items.Add(new ProductModel { ItemName = "Blueberries", Price = 6.75M });
}
}
}
public class ShoppingCartModel
{
public List<ProductModel> Items { get; set; } = new List<ProductModel>();
public decimal GenerateTotal()
{
decimal subtotal = Items.Sum(x => x.Price);
if (subtotal > 100)
{
return subtotal * 0.80M;
}
else if (subtotal > 50)
{
return subtotal * 0.85M;
}
else if (subtotal > 10)
{
return subtotal * 0.90M;
}
else
{
return subtotal;
}
}
}
public class ProductModel
{
public string ItemName { get; set; }
public decimal Price { get; set; }
}Imports System
Imports System.Collections.Generic
Imports System.Linq
Namespace ConsoleUI
Class Program
Private Shared cart As New ShoppingCartModel()
Shared Sub Main(args As String())
PopulateCartWithDemoData()
Console.WriteLine($"The total for the cart is {cart.GenerateTotal():C2}")
Console.ReadLine()
End Sub
Private Shared Sub PopulateCartWithDemoData()
cart.Items.Add(New ProductModel With {.ItemName = "Cereal", .Price = 3.63D})
cart.Items.Add(New ProductModel With {.ItemName = "Milk", .Price = 2.95D})
cart.Items.Add(New ProductModel With {.ItemName = "Strawberries", .Price = 7.51D})
cart.Items.Add(New ProductModel With {.ItemName = "Blueberries", .Price = 6.75D})
End Sub
End Class
End Namespace
Public Class ShoppingCartModel
Public Property Items As List(Of ProductModel) = New List(Of ProductModel)()
Public Function GenerateTotal() As Decimal
Dim subtotal As Decimal = Items.Sum(Function(x) x.Price)
If subtotal > 100 Then
Return subtotal * 0.80D
ElseIf subtotal > 50 Then
Return subtotal * 0.85D
ElseIf subtotal > 10 Then
Return subtotal * 0.90D
Else
Return subtotal
End If
End Function
End Class
Public Class ProductModel
Public Property ItemName As String
Public Property Price As Decimal
End ClassTim explica a estrutura e a funcionalidade do aplicativo de demonstração:
Modelo de Carrinho de Compras : Representa o carrinho de compras com uma lista de itens (ProductModel) e calcula o custo total com descontos com base no subtotal.
Modelo de produto : Representa itens individuais com propriedades de nome e preço.
Aplicativo de console : Preenche o carrinho com dados de demonstração, calcula o total e o exibe.
Tim explica a lógica de desconto no método GenerateTotal, detalhando como o subtotal determina o desconto aplicado:
Tim utiliza um ponto de interrupção para demonstrar a lógica de cálculo e desconto, garantindo que os espectadores compreendam os fundamentos antes de apresentá-los aos participantes.
Nesta seção, Tim Corey explora o conceito de delegates em C#, explicando como funcionam e demonstrando seu uso com exemplos práticos de código.
Tim explica que um delegate é essencialmente uma forma de passar métodos como parâmetros. Em vez de passar uma variável ou uma propriedade, você passa um método, o que permite um código mais flexível e reutilizável.
Eis como Tim explica o processo de criação e utilização de um delegado:
Defina o Delegado :
public delegate void MentionDiscount(decimal subtotal);Public Delegate Sub MentionDiscount(subtotal As Decimal)Utilizando o Delegado em um Método :
public decimal GenerateTotal(MentionDiscount mentionDiscount)
{
decimal subtotal = Items.Sum(x => x.Price);
// Call a method passed as a delegate
mentionDiscount(subtotal);
if (subtotal > 100)
{
return subtotal * 0.80M;
}
else if (subtotal > 50)
{
return subtotal * 0.85M;
}
else if (subtotal > 10)
{
return subtotal * 0.90M;
}
else
{
return subtotal;
}
}Public Function GenerateTotal(mentionDiscount As MentionDiscount) As Decimal
Dim subtotal As Decimal = Items.Sum(Function(x) x.Price)
' Call a method passed as a delegate
mentionDiscount(subtotal)
If subtotal > 100 Then
Return subtotal * 0.80D
ElseIf subtotal > 50 Then
Return subtotal * 0.85D
ElseIf subtotal > 10 Then
Return subtotal * 0.90D
Else
Return subtotal
End If
End FunctionCriando um método para passar para o delegado :
private static void SubtotalAlert(decimal subtotal)
{
Console.WriteLine($"The subtotal is {subtotal:C2}");
}Private Shared Sub SubtotalAlert(ByVal subtotal As Decimal)
Console.WriteLine($"The subtotal is {subtotal:C2}")
End SubChamando o método GenerateTotal :
class Program
{
static ShoppingCartModel cart = new ShoppingCartModel();
static void Main(string[] args)
{
PopulateCartWithDemoData();
Console.WriteLine($"The total for the cart is {cart.GenerateTotal(SubtotalAlert):C2}");
Console.ReadLine();
}
private static void PopulateCartWithDemoData()
{
cart.Items.Add(new ProductModel { ItemName = "Cereal", Price = 3.63M });
cart.Items.Add(new ProductModel { ItemName = "Milk", Price = 2.95M });
cart.Items.Add(new ProductModel { ItemName = "Strawberries", Price = 7.51M });
cart.Items.Add(new ProductModel { ItemName = "Blueberries", Price = 6.75M });
}
}Class Program
Private Shared cart As New ShoppingCartModel()
Shared Sub Main(args As String())
PopulateCartWithDemoData()
Console.WriteLine($"The total for the cart is {cart.GenerateTotal(AddressOf SubtotalAlert):C2}")
Console.ReadLine()
End Sub
Private Shared Sub PopulateCartWithDemoData()
cart.Items.Add(New ProductModel With {.ItemName = "Cereal", .Price = 3.63D})
cart.Items.Add(New ProductModel With {.ItemName = "Milk", .Price = 2.95D})
cart.Items.Add(New ProductModel With {.ItemName = "Strawberries", .Price = 7.51D})
cart.Items.Add(New ProductModel With {.ItemName = "Blueberries", .Price = 6.75D})
End Sub
End ClassTim executa o aplicativo para demonstrar como o delegado funciona. A saída do console mostra o subtotal e o total do carrinho, indicando que o método SubtotalAlert foi passado e executado com sucesso por meio do delegado.

Tim Corey então explora o uso de funções e delegados de ação em C#. Esses são tipos especiais de delegados fornecidos pela Microsoft para simplificar o uso de delegados com genéricos.
Tim destaca um problema comum: a lógica de desconto codificada diretamente no método GenerateTotal. Essa abordagem é inflexível e exige alterações de código, recompilação e reimplementação sempre que as regras de desconto mudam.
public decimal GenerateTotal()
{
decimal subtotal = Items.Sum(x => x.Price);
if (subtotal > 100)
{
return subtotal * 0.80M;
}
else if (subtotal > 50)
{
return subtotal * 0.85M;
}
else if (subtotal > 10)
{
return subtotal * 0.90M;
}
else
{
return subtotal;
}
}Public Function GenerateTotal() As Decimal
Dim subtotal As Decimal = Items.Sum(Function(x) x.Price)
If subtotal > 100 Then
Return subtotal * 0.80D
ElseIf subtotal > 50 Then
Return subtotal * 0.85D
ElseIf subtotal > 10 Then
Return subtotal * 0.90D
Else
Return subtotal
End If
End FunctionTim apresenta o delegado func para resolver o problema do desconto fixo no código. O delegado func é um delegado genérico que representa a assinatura de um método com um tipo de retorno e até 16 parâmetros de entrada.
Definindo o Delegado Func :
public decimal GenerateTotal(Func<List<ProductModel>, decimal, decimal> calculateDiscountedTotal)
{
decimal subtotal = Items.Sum(x => x.Price);
MentionDiscount(subtotal);
return calculateDiscountedTotal(Items, subtotal);
}Public Function GenerateTotal(calculateDiscountedTotal As Func(Of List(Of ProductModel), Decimal, Decimal)) As Decimal
Dim subtotal As Decimal = Items.Sum(Function(x) x.Price)
MentionDiscount(subtotal)
Return calculateDiscountedTotal(Items, subtotal)
End FunctionCriação do método de cálculo de desconto :
private static decimal CalculateLevelDiscount(List<ProductModel> items, decimal subtotal)
{
if (subtotal > 100)
{
return subtotal * 0.80M;
}
else if (subtotal > 50)
{
return subtotal * 0.85M;
}
else if (subtotal > 10)
{
return subtotal * 0.90M;
}
else
{
return subtotal;
}
}Private Shared Function CalculateLevelDiscount(items As List(Of ProductModel), subtotal As Decimal) As Decimal
If subtotal > 100 Then
Return subtotal * 0.80D
ElseIf subtotal > 50 Then
Return subtotal * 0.85D
ElseIf subtotal > 10 Then
Return subtotal * 0.90D
Else
Return subtotal
End If
End FunctionPassando o método para o delegado da função :
class Program
{
static ShoppingCartModel cart = new ShoppingCartModel();
static void Main(string[] args)
{
PopulateCartWithDemoData();
Console.WriteLine($"The total for the cart is {cart.GenerateTotal(CalculateLevelDiscount):C2}");
Console.ReadLine();
}
private static void PopulateCartWithDemoData()
{
cart.Items.Add(new ProductModel { ItemName = "Cereal", Price = 3.63M });
cart.Items.Add(new ProductModel { ItemName = "Milk", Price = 2.95M });
cart.Items.Add(new ProductModel { ItemName = "Strawberries", Price = 7.51M });
cart.Items.Add(new ProductModel { ItemName = "Blueberries", Price = 6.75M });
}
private static decimal CalculateLevelDiscount(List<ProductModel> items, decimal subtotal)
{
if (subtotal > 100)
{
return subtotal * 0.80M;
}
else if (subtotal > 50)
{
return subtotal * 0.85M;
}
else if (subtotal > 10)
{
return subtotal * 0.90M;
}
else
{
return subtotal;
}
}
}Class Program
Shared cart As New ShoppingCartModel()
Shared Sub Main(args As String())
PopulateCartWithDemoData()
Console.WriteLine($"The total for the cart is {cart.GenerateTotal(AddressOf CalculateLevelDiscount):C2}")
Console.ReadLine()
End Sub
Private Shared Sub PopulateCartWithDemoData()
cart.Items.Add(New ProductModel With {.ItemName = "Cereal", .Price = 3.63D})
cart.Items.Add(New ProductModel With {.ItemName = "Milk", .Price = 2.95D})
cart.Items.Add(New ProductModel With {.ItemName = "Strawberries", .Price = 7.51D})
cart.Items.Add(New ProductModel With {.ItemName = "Blueberries", .Price = 6.75D})
End Sub
Private Shared Function CalculateLevelDiscount(items As List(Of ProductModel), subtotal As Decimal) As Decimal
If subtotal > 100 Then
Return subtotal * 0.80D
ElseIf subtotal > 50 Then
Return subtotal * 0.85D
ElseIf subtotal > 10 Then
Return subtotal * 0.90D
Else
Return subtotal
End If
End Function
End ClassTim demonstra a aplicação modificada, mostrando que ela funciona corretamente e calcula dinamicamente o desconto com base na lógica fornecida.

Tim compara o delegado personalizado e o delegado funcional:
Delegado : Requer definição explícita da assinatura, fornecendo documentação e estrutura claras.
Func : Mais conciso, mas exige a especificação dos tipos de entrada e saída a cada vez, o que pode ser menos claro.
Ambas as abordagens oferecem flexibilidade, mas a escolha depende do caso de uso específico e da complexidade da aplicação.
Tim Corey aborda uma pergunta comum sobre o uso de delegados: Por que ter um delegado se todo o trabalho parece ser feito em outro lugar?
Tim explica que o objetivo dos delegates é proporcionar flexibilidade e extensibilidade ao código. O método GenerateTotal na classe ShoppingCartModel pode fazer mais do que apenas calcular descontos. Também pode lidar com tarefas como verificar a disponibilidade de estoque, validar o conteúdo do carrinho ou outras lógicas de negócios. Os delegates permitem que você passe métodos específicos para tarefas únicas ou comportamentos personalizados sem alterar o método principal. Isso torna o código mais modular e mais fácil de manter.
Os delegados são especialmente úteis em cenários onde você deseja:
Tim apresenta o delegado Action, outro tipo especial de delegado em C#. O delegado Action é semelhante ao Func, mas não retorna um valor (ou seja, retorna void).
Criando o Delegado de Ação :
public decimal GenerateTotal(Func<List<ProductModel>, decimal, decimal> calculateDiscountedTotal, Action<string> tellUserWeAreDiscounting)
{
decimal subtotal = Items.Sum(x => x.Price);
MentionSubtotal(subtotal);
tellUserWeAreDiscounting("We are applying your discount.");
return calculateDiscountedTotal(Items, subtotal);
}Public Function GenerateTotal(calculateDiscountedTotal As Func(Of List(Of ProductModel), Decimal, Decimal), tellUserWeAreDiscounting As Action(Of String)) As Decimal
Dim subtotal As Decimal = Items.Sum(Function(x) x.Price)
MentionSubtotal(subtotal)
tellUserWeAreDiscounting("We are applying your discount.")
Return calculateDiscountedTotal(Items, subtotal)
End FunctionCriando o método de alerta :
private static void AlertUser(string message)
{
Console.WriteLine(message);
}Private Shared Sub AlertUser(message As String)
Console.WriteLine(message)
End SubPassando o método para o delegado de ação :
class Program
{
static ShoppingCartModel cart = new ShoppingCartModel();
static void Main(string[] args)
{
PopulateCartWithDemoData();
Console.WriteLine($"The total for the cart is {cart.GenerateTotal(CalculateLevelDiscount, AlertUser):C2}");
Console.ReadLine();
}
private static void PopulateCartWithDemoData()
{
cart.Items.Add(new ProductModel { ItemName = "Cereal", Price = 3.63M });
cart.Items.Add(new ProductModel { ItemName = "Milk", Price = 2.95M });
cart.Items.Add(new ProductModel { ItemName = "Strawberries", Price = 7.51M });
cart.Items.Add(new ProductModel { ItemName = "Blueberries", Price = 6.75M });
}
private static decimal CalculateLevelDiscount(List<ProductModel> items, decimal subtotal)
{
if (subtotal > 100)
{
return subtotal * 0.80M;
}
else if (subtotal > 50)
{
return subtotal * 0.85M;
}
else if (subtotal > 10)
{
return subtotal * 0.90M;
}
else
{
return subtotal;
}
}
private static void AlertUser(string message)
{
Console.WriteLine(message);
}
}Class Program
Shared cart As New ShoppingCartModel()
Shared Sub Main(args As String())
PopulateCartWithDemoData()
Console.WriteLine($"The total for the cart is {cart.GenerateTotal(AddressOf CalculateLevelDiscount, AddressOf AlertUser):C2}")
Console.ReadLine()
End Sub
Private Shared Sub PopulateCartWithDemoData()
cart.Items.Add(New ProductModel With {.ItemName = "Cereal", .Price = 3.63D})
cart.Items.Add(New ProductModel With {.ItemName = "Milk", .Price = 2.95D})
cart.Items.Add(New ProductModel With {.ItemName = "Strawberries", .Price = 7.51D})
cart.Items.Add(New ProductModel With {.ItemName = "Blueberries", .Price = 6.75D})
End Sub
Private Shared Function CalculateLevelDiscount(items As List(Of ProductModel), subtotal As Decimal) As Decimal
If subtotal > 100 Then
Return subtotal * 0.80D
ElseIf subtotal > 50 Then
Return subtotal * 0.85D
ElseIf subtotal > 10 Then
Return subtotal * 0.90D
Else
Return subtotal
End If
End Function
Private Shared Sub AlertUser(message As String)
Console.WriteLine(message)
End Sub
End ClassTim mostra como usar métodos anônimos, permitindo que você defina métodos dinamicamente sem precisar nomeá-los.
Definindo Métodos Anônimos :
Em vez de criar métodos nomeados, você pode definir os métodos diretamente onde eles são necessários.
class Program
{
static ShoppingCartModel cart = new ShoppingCartModel();
static void Main(string[] args)
{
PopulateCartWithDemoData();
Console.WriteLine($"The total for the cart is {cart.GenerateTotal((items, subtotal) =>
{
if (subtotal > 100) return subtotal * 0.80M;
else if (subtotal > 50) return subtotal * 0.85M;
else if (subtotal > 10) return subtotal * 0.90M;
else return subtotal;
},
(message) => Console.WriteLine(message)):C2}");
Console.ReadLine();
}
private static void PopulateCartWithDemoData()
{
cart.Items.Add(new ProductModel { ItemName = "Cereal", Price = 3.63M });
cart.Items.Add(new ProductModel { ItemName = "Milk", Price = 2.95M });
cart.Items.Add(new ProductModel { ItemName = "Strawberries", Price = 7.51M });
cart.Items.Add(new ProductModel { ItemName = "Blueberries", Price = 6.75M });
}
}Class Program
Shared cart As New ShoppingCartModel()
Shared Sub Main(args As String())
PopulateCartWithDemoData()
Console.WriteLine($"The total for the cart is {cart.GenerateTotal(Function(items, subtotal)
If subtotal > 100 Then
Return subtotal * 0.80D
ElseIf subtotal > 50 Then
Return subtotal * 0.85D
ElseIf subtotal > 10 Then
Return subtotal * 0.90D
Else
Return subtotal
End If
End Function,
Sub(message) Console.WriteLine(message)):C2}")
Console.ReadLine()
End Sub
Private Shared Sub PopulateCartWithDemoData()
cart.Items.Add(New ProductModel With {.ItemName = "Cereal", .Price = 3.63D})
cart.Items.Add(New ProductModel With {.ItemName = "Milk", .Price = 2.95D})
cart.Items.Add(New ProductModel With {.ItemName = "Strawberries", .Price = 7.51D})
cart.Items.Add(New ProductModel With {.ItemName = "Blueberries", .Price = 6.75D})
End Sub
End ClassCompreendendo a sintaxe :
=> (expressão lambda) para definir o corpo do método diretamente no lugar.Ao utilizar delegates, incluindo Func, Action e métodos anônimos, os desenvolvedores podem criar um código mais dinâmico e modular, permitindo componentes flexíveis e reutilizáveis.
Neste segmento, Tim Corey demonstra o poder dos delegates, estendendo seu uso a um aplicativo WinForms. Isso destaca como os delegados podem facilitar diferentes comportamentos em vários contextos de interface do usuário (IU).
Interface de usuário WinForm com dois botões :
O formulário possui dois botões: um para exibir caixas de mensagem e outro para exibir caixas de texto.
O modelo ShoppingCartModel e um método para preenchê-lo com dados de demonstração também estão incluídos.
public partial class Dashboard : Form
{
ShoppingCartModel cart = new ShoppingCartModel();
public Dashboard()
{
InitializeComponent();
PopulateCartWithDemoData();
}
private void PopulateCartWithDemoData()
{
cart.Items.Add(new ProductModel { ItemName = "Cereal", Price = 3.63M });
cart.Items.Add(new ProductModel { ItemName = "Milk", Price = 2.95M });
cart.Items.Add(new ProductModel { ItemName = "Strawberries", Price = 7.51M });
cart.Items.Add(new ProductModel { ItemName = "Blueberries", Price = 6.75M });
}
private void messageBoxDemoButton_Click(object sender, EventArgs e)
{
decimal total = cart.GenerateTotal(SubtotalAlert, CalculateLevelDiscount, PrintOutDiscountAlert);
MessageBox.Show($"The total is {total:C2}");
}
private void textBoxDemoButton_Click(object sender, EventArgs e)
{
// Code for TextBox demo will go here
}
}Public Partial Class Dashboard
Inherits Form
Private cart As New ShoppingCartModel()
Public Sub New()
InitializeComponent()
PopulateCartWithDemoData()
End Sub
Private Sub PopulateCartWithDemoData()
cart.Items.Add(New ProductModel With {.ItemName = "Cereal", .Price = 3.63D})
cart.Items.Add(New ProductModel With {.ItemName = "Milk", .Price = 2.95D})
cart.Items.Add(New ProductModel With {.ItemName = "Strawberries", .Price = 7.51D})
cart.Items.Add(New ProductModel With {.ItemName = "Blueberries", .Price = 6.75D})
End Sub
Private Sub messageBoxDemoButton_Click(sender As Object, e As EventArgs) Handles messageBoxDemoButton.Click
Dim total As Decimal = cart.GenerateTotal(AddressOf SubtotalAlert, AddressOf CalculateLevelDiscount, AddressOf PrintOutDiscountAlert)
MessageBox.Show($"The total is {total:C2}")
End Sub
Private Sub textBoxDemoButton_Click(sender As Object, e As EventArgs) Handles textBoxDemoButton.Click
' Code for TextBox demo will go here
End Sub
End ClassPrintOutDiscountAlert :
private void PrintOutDiscountAlert(string message)
{
MessageBox.Show(message);
}Private Sub PrintOutDiscountAlert(ByVal message As String)
MessageBox.Show(message)
End SubSubtotalAlert :
private void SubtotalAlert(decimal subtotal)
{
MessageBox.Show($"The subtotal is {subtotal:C2}");
}Private Sub SubtotalAlert(ByVal subtotal As Decimal)
MessageBox.Show($"The subtotal is {subtotal:C2}")
End SubCalcularDescontoPorNível :
private decimal CalculateLevelDiscount(List<ProductModel> items, decimal subtotal)
{
if (items.Count > 3)
{
return subtotal - 3M;
}
return subtotal - items.Count;
}Private Function CalculateLevelDiscount(items As List(Of ProductModel), subtotal As Decimal) As Decimal
If items.Count > 3 Then
Return subtotal - 3D
End If
Return subtotal - items.Count
End FunctionUtilizando Delegados no Evento de Clique de Botão :
messageBoxDemoButton_Click demonstra como passar os delegados para o método GenerateTotal e manipular os resultados usando caixas de mensagem.private void messageBoxDemoButton_Click(object sender, EventArgs e)
{
decimal total = cart.GenerateTotal(SubtotalAlert, CalculateLevelDiscount, PrintOutDiscountAlert);
MessageBox.Show($"The total is {total:C2}");
}Private Sub messageBoxDemoButton_Click(sender As Object, e As EventArgs) Handles messageBoxDemoButton.Click
Dim total As Decimal = cart.GenerateTotal(AddressOf SubtotalAlert, AddressOf CalculateLevelDiscount, AddressOf PrintOutDiscountAlert)
MessageBox.Show($"The total is {total:C2}")
End SubExecutando o aplicativo :

Tim Corey explica os delegates em C# de forma clara, abordando os conceitos básicos, o uso avançado e exemplos práticos, como a utilização de delegates em um carrinho de compras. Ele demonstra como os delegates permitem um código flexível e reutilizável, incluindo Func, Action e métodos anônimos. Assista ao vídeo completo para aprender como aplicar delegados de forma eficaz em seus projetos!
Do you create content for developers working with .NET, C#, Java, Python, or Node.js? Turn your expertise into extra income!
Join our newsletter, you’ll get exclusive access on article updates. We value your privacy