Thursday, November 21, 2024
HomeProductsADO.NET Data ProvidersWorking with Data Transfer Objects in ASP.NET Core 

Working with Data Transfer Objects in ASP.NET Core 

One of the major design constraints or flaws in an application is exposing the models of the application to the presentation layer or the external world (external apps, services, etc.). Here’s where Data Transfer Objects (DTOs) can help!

DTOs minimize coupling between different layers of the application, facilitate API versioning, improve scalability and performance, ensure efficient and accurate data mapping, foster a clean seperation of concerns and enable secure data transfer between the various layers of the application.

This article presents a discussion on Data Transfer Objects, why they are used and how you can work with DTOs in ASP.NET Core applications. This article builds a simple application to demonstrate the concepts covered and uses PostgreSql as the database to store data and dotConnect for PostgreSQL as the data provider for PostgreSQL.

In this article, we’ll connect to PostgreSQL using dotConnect for PostgreSQL  which is high performance and enhanced data provider for PostgreSQL that is built on top of ADO.NET and can work on both connected and disconnected modes.

Pre-requisites

You’ll need the following tools to deal with code examples:

Contents

In this article we’ll learn what Data Transfer Objects are, why they are needed and how to work in ASP.NET Core. Here are the steps we’ll follow throughout this article to accomplish this:

  1. What is a Data Transfer Object (DTO)?
  2. Benefits of Using DTOs
  3. What is AutoMapper? Why should we use it?
  4. Create a new ASP.NET 6 Core Web API Project
  5. Create the AuthorRepository Class
  6. Create the AuthorDTO Class
  7. Use AutoMapper to Map a Model to a DTO
  8. Create the AuthorController Class
  9. Best Practices

In the following sections of this article, we will explore the significance of Data Transfer Objects (DTOs) in ASP.NET Core development. Additionally, we will examine recommended practices and techniques for effectively implementing and utilizing DTOs.

What is a Data Transfer Object (DTO)?

In an application, a Data Transfer Object (DTO) is an object of a class that transports data between layers. These lightweight objects represent specific portions of the data (i.e., a subset of the actual data) pertaining to a domain model. Once you’ve created DTOs, you can use them as responses to your API endpoints or in your views to display a subset of the actual data. By using DTOs, you can easily transfer data between layers, such as the presentation layer and API endpoints. DTOs are a great choice when you need to pass objects over a bandwidth-constrained medium.

DTOs facilitate loose coupling between the components of your application. This leads to reduced data transfer and improved performance and maintainability. Additionally, DTOs afford greater control over the specific data that is transferred and consumed by different parts of an application, thanks to their customized represe­ntation of the data.

Benefits of Using DTOs

There are several benefits of using DTOs:

Reduced coupling: DTOs facilitate modularity and help separate concerns in an application by eliminating tight dependencies between the components thus making your application scalable, maintainable, and easily updatable. By using DTOs for passing data between the layers of an application, coupling between the components of an application are reduced considerably. This is because, direct dependencies are eliminated, enabling the components of an application to evolve independent of one another.

Encapsulation: DTOs simplify data handling by concealing internal implementation details, by encapsulating data. This practice ensures that only the essential data is accessible to the external world, hence preventing the exposure of unnecessary and sensitive information.

Data transfer: DTOs encompass only the pertinent information required for a specific operation or a use case. With lean objects and reduced payloads, this approach facilitates reduction in network bandwidth usage and enhances overall performance by transmitting only the data that is needed. 

Security: You can use DTOs to check for validations and security violations. Before processing or persisting data in the DTOs, data validation rules and strict security measures can be applied.

Challenges of using DTOs

When working with DTOs you might need to handle several challenges such as versioning, data validation, mapping DTOs with domain models, etc.

Versioning

As your application evolves, so does the models and DTOs since new features are added or existing features are enhanced. You should be able to version your APIs to ensure backward compatibility when changes to the existing DTOs are introduced in the application. However, modifying DTOs might result in compatibility issues as the existing implementations of the application might be using the DTOs.

Maintenance overhead

The number and complexity of DTOs may increase as your application grows and evolves. It can result in increased maintenance overhead, since you need to manage and update the DTOs whenever the corresponding data structures or models change.

Data redundancy

DTOs oftem duplicate data from the domain model. In the absence of proper synchronization between the DTOs and domain model, this duplication can result in redundancy and inconsistencies.

Data validation

DTOs typically transfer data between components, making data validation crucial to ensuring data integrity. Implementing validation logic across different DTOs may require additional effort, since validation rules may differ between DTOs or need to be synchronized with domain model validations.

Data consistency

Maintaining data consistency between domain models and DTOs can be challenging. As your application evolves, changes to the domain model may require changes to the DTOs. Data inconsistencies or compatibility problems can occur if they are not kept in sync. 

What is AutoMapper? Why should we use it?

AutoMappеr is a popular C# library that еnablе dеvеlopers to define mappings using codе or convеntion and have thе library pеrform thе mapping procеss automatically.  This еliminatеs unnеcеssary, rеpеtitivе codе and strеamlinеs the dеvеlopmеnt process.  

Hеrе’s because you should usе AutoMappеr: 

1.  Simplified mapping: AutoMappеr strеamlinеs thе task of mapping bеtwееn different object types by seamlessly transferring values from source to target objects. As a rеsult, this can hеlp you eliminate codе redundancy and minimizе chancеs of human еrrors in your application.  

2.  Enhanced Productivity: AutoMappеr incrеasеs productivity by automating thе objеct mapping procеss, saving dеvеlopеrs timе and еffort.  This allows thеm to focus on dеvеloping corе businеss logic and rеly on AutoMappеr to handlе objеct mapping еfficiеntly.  

3.  Reduced Codе Redundancy: AutoMappеr consolidatеs mapping logic in a cеntral location,  еliminating thе nееd to duplicatе codе. This simplifiеs codе maintеnancе and еnsurеs consistеncy throughout thе mapping procеss.  

4.  Complеx Mapping Scеnarios: AutoMappеr providеs flеxibility in handling complеx mapping situations such as mapping nеstеd objеcts, collеctions, conditional mappings, and custom-typе convеrtеrs. This еnablеs sеamlеss mapping bеtwееn objеcts with diffеrеnt structurеs and еffеctivеly handlеs еdgе casеs.  

5.  Promotes faster rеfactoring: You can lеvеragе AutoMappеr to simplify rеstructuring objеcts, such as adding or rеmoving propеrtiеs. Instеad of manually updating mapping codе throughout thе application, dеvеlopеrs can еasily changе mapping configurations in onе cеntral location.  

6.  Promotes loosе coupling: AutoMappеr facilitatеs loosе coupling bеtwееn diffеrеnt layеrs of an application, such as thе prеsеntation layеr and thе data layеr using DTOs to transmit data bеtwееn thе layеrs of your application.

Create a new ASP.NET 6 Core Web API Project

In this section we’ll learn how to create a new ASP.NET 6 Core Web API project in Visual Studio 2022. 

Now, follow the steps outlined below: 

  1. Open Visual Studio 2022.
  2. Click Create a new project.
  3. Select ASP.NET Core Web API and click Next.
  4. Specify the project name and location to store that project in your system. Optionally, checkmark the Place solution and project in the same directory checkbox.
  5. Click Next.
  6. In the Additional information window, select .NET 6.0 (Long-term support) as the project version.
  7. Disable the Configure for HTTPS and Enable Docker Support options (uncheck them).
  8. Since we’ll not be using authentication in this example, select the Authentication type as None.
  9. Since we won’t use Open API in this example, deselect the Enable OpenAPI support checkbox.
  10. Since we’ll not be using minimal APIs in this example, ensure that the Use controllers (uncheck to use minimal APIs) is checked.
  11. Leave the Do not use top-level statements checkbox unchecked.
  12. Click Create to finish the process.

We’ll use this project in this article.

Install NuGet Package(s) into the API Project

In your API project, i.e., the project you just created, you should install the dotConnect for PostgreSql package in your project. dotConnect for PostgreSQL is a high-performance data provider for PostgreSQL built on ADO.NET technology that provides a comprehensive solution for building PostgreSQL-based database applications.

You can install this package either from the NuGet Package Manager tool inside Visual Studio or, from the NuGet Package Manager console using the following command:

PM> Install-Package Devart.Data.PostgreSQL

We would also need to install the AutoMapper package since we’ll be using AutoMapper to map a model instance with an instance of a DTO later in this article. We can install this package similar to how we installed the previous package:

PM> Install-Package AutoMapper

Create the Test Database

You can create a database using the pgadmin tool. To create a database using this Launch this tool, follow the steps given below:

  1. Launch the pgadmin tool
  2. Expand the Servers section
  3. Select Databases
  4. Right-click and click Create -> Database…
  5. Specify the name of the database and leave the other options to their default values
  6. Click Save to complete the process

Alternatively, you can use the following script to create the database:

-- Database: demo
DROP DATABASE IF EXISTS demo;
CREATE DATABASE demo
    WITH
    OWNER = postgres
    ENCODING = 'UTF8'
    LC_COLLATE = 'English_India.1252'
    LC_CTYPE = 'English_India.1252'
    TABLESPACE = pg_default
    CONNECTION LIMIT = -1
    IS_TEMPLATE = False;

Create a database test table

Select and expand the database you just created

Select Schemas -> Tables

Right-click on Tables and select Create -> Table…

The table script is given below for your reference:

CREATE TABLE author (
    author_id serial NOT NULL,
    first_name VARCHAR (255) NOT NULL,
    last_name VARCHAR (255) NOT NULL,
address VARCHAR (255) NOT NULL,
email VARCHAR (255) NOT NULL,
phone VARCHAR (255) NOT NULL,
CONSTRAINT author_pk PRIMARY KEY (author_id)
);

We’ll use this database in the subsequent sections of this article to demonstrate how we can work with Integration Tests in ASP.NET Core using dotConnect for PostgreSql.

Add a few records to the Author table

Now, run the following script in your database to insert a few records in the author table:

INSERT INTO author (
    first_name,
    last_name,
    address,
    email,
    phone
) 
VALUES
('Joydip', 'Kanjilal', 'Hyderabad, India', [email protected]','1234567890'),
('Debanjan', 'Banerjee', 'Kolkata,India', ’[email protected]','0987654321'),
('Rohit', 'Sharma', 'Bangalore, India', '[email protected]','5566778899');

Figure 1 below illustrates the pgAdmin editor with the scripts:


Figure 1: Displaying the records inserted into the author table

Create the Model Class

Create a solution folder in the Solution Explorer window and name it as Models. Next, create a .cs file called Author.cs with the following code in there:

public class Author
    {
        public int Author_Id { get; set; }
        public string First_Name { get; set; }
        public string Last_Name { get; set; }
        public string Address { get; set; }
   public string Email { get; set; }
   public string Phone { get; set; }
    }

Create the AuthorRepository Class

The IAuthorRepository interface would look like this:

public interface IAuthorRepository
    {
        public List<Author> GetAuthors();
    }

The AuthorRepository class implements the GetAuthors method of the IAuthorRepository interface and encapsulates all database operations.

public class AuthorRepository: IAuthorRepository
    {
     public List <Author> GetAuthors()
     {
      try
      {
       List <Author> authors = new List <Author> ();
       using(PgSqlConnection pgSqlConnection =
        new PgSqlConnection("User Id = postgres; 
        Password = Specify the Db password here" +
         "host=localhost;database=demo;
        License Key=Specify your license key here"))
       {
        using(PgSqlCommand pgSqlCommand = new PgSqlCommand())
        {
         pgSqlCommand.CommandText =
          "Select * From author";
         pgSqlCommand.Connection = pgSqlConnection;
         if (pgSqlConnection.State !=
          System.Data.ConnectionState.Open)
          pgSqlConnection.Open();
         using(var pgSqlReader =
          pgSqlCommand.ExecuteReader())
         {
          while (pgSqlReader.Read())
          {
           Author author = new Author ();
           author.Author_Id =
            int.Parse(pgSqlReader.GetValue(0).ToString());
           author.First_Name =
            pgSqlReader.GetValue(1).ToString();
           author.Last_Name =
            pgSqlReader.GetValue(2).ToString();
           author.Address =
            pgSqlReader.GetValue(3).ToString();
           author.Email =
            pgSqlReader.GetValue(4).ToString();
           author.Phone =
            pgSqlReader.GetValue(5).ToString();
           authors.Add(author);
          }
         }
        }
       }
       return authors;
      }
      catch
      {
       throw;
      }
     }
    }

Create the AuthorDTO Class

In this example, we’ll use a data transfer object to return data from the action methods of the controller. A data transfer object is represented by a class with properties that correspond to the fields of the database table that they represent. To create a data transfer object, create a new .cs file named AuthorDTO.cs and write the following code in there:

public class AuthorDTO
{
   public int Author_Id { get; set; }
   public string Author_Name { get; set; }
}

Use AutoMapper to Map a Model to a DTO

The properties of the Author and the AuthorDTO classes are not the same. The AuthorDTO class contains only two properties. To map properties when the property names are not identical, you should use the .ForMember method. Here is an example that creates a class with a static method that initializes AutoMapper:

public class MapperConfig
{
    public static Mapper InitializeAutoMapper()
    {
        var config = new MapperConfiguration(cfg =>
        {
            cfg.CreateMap<Author, AuthorDTO>()
            .ForMember(a => a.Author_Name, act => 
            act.MapFrom(src => src.First_Name + " " + src.Last_Name));
        });
        var mapper = new Mapper(config);
        return mapper;
    }
}

Note how the properties of the two different types namely, Author and AuthorDTO have been mapped using the ForMember method. 

Create the AuthorController Class

Next, select and right-click on the Controllers solution folder and create a new controller class called AuthorController with the following code in there: 

[Route("api/[controller]")]
    [ApiController]
    public class AuthorController : ControllerBase
    {
        private readonly IAuthorRepository _authorRepository;
        public AuthorController(IAuthorRepository authorRepository)
        {
            _authorRepository = authorRepository;
        }
        [HttpGet]
        public List<AuthorDTO> Get()
        {
            return GetAuthorDTOs(_authorRepository.GetAuthors());
        }
        private List<AuthorDTO> GetAuthorDTOs(List<Author> authors)
        {
            var dtos = new List<AuthorDTO>();
            var mapper = MapperConfig.InitializeAutoMapper();
            foreach (var source in authors)
            {   
                var destination = mapper.Map<Author, AuthorDTO>(source);
                dtos.Add(destination);
            }
            return dtos;
        }
    }

Note how an instance of type IAuthorRepository is injected in the constructor of the AuthorController class. Remember that you must add an instance of type IAuthorRepository to the services container using the following piece of code in the Program.cs file:

builder.Services.AddScoped<IAuthorRepository, AuthorRepository>();

The GetAuthors method of the author repository returns a list of authors. The HttpGet action method in the AuthorController invokes the GetAuthorDTOs method to convert a list of authors to a list of AuthorDTO objects. When you execute the application and run the HttpGet endpoint of the AuthorController class, you’ll see the records of the author database table displayed in the web browser:

Figure 2: The records of the author database table are displayed in the web browser

Best Practices of Using DTOs

When designing data transfer objects, it is crucial to consider the following recommended approaches.

Keep DTOs simple: DTOs should be simple and only include the necessary properties, avoiding unnecessary complexity. A recommended approach is to use separate DTOs for different use cases or operations.

Avoid writing business logic in DTOs: The practice of avoiding business logic in DTOs is essential. DTOs, which serve the purpose of facilitating data transfer, should solely focus on carrying properties and re­frain from incorporating methods or intricate logic within them. By adhering to this principle, DTOs can maintain their primary function without becoming convoluted with unnecessary complexities.

Create DTOs that are immutable: Considering immutability can be beneficial when designing DTOs. By making them immutable objects with read-only properties, it promotes thread safety and helps prevent unintended modifications.

Select the appropriate data type: Appropriate data types should be used when selecting properties for a Data Transfer Object (DTO). This ensures consistency and compatibility with the consuming components. You can use a class or a struct to represent a DTO in C#. However, the best option would be to use a record type since it is immutable.

Group related DTOs: When multiple DTOs have common properties, you should group them together. Consider creating a base DTO class and derive specific DTOs from it to promote code reuse and simplify maintainability.

License Key Validation Error

When you execute the application, you might run into license validation errors if no valid license key is available. If the license key validation fails, you will encounter a Devart.Common.LicenseException. To resolve this error, you must either have a license key and already be a user, or install the installation file which will install a trial key into the system.

Summary

There is some initial effort involved in creating and implementing DTOs, but the benefits far outweigh the costs. Using DTOs, you can control what information should be transferred over the wire thus enhancing security of your application. By mapping DTOs to the appropriate models in your application you can customize the way data is prеsеntеd without changing the underlying database structure. This allows you to build APIs that expose only the data your clients require, giving you far greater control and flexibility.

RELATED ARTICLES

Whitepaper

Social

Topics

Products