This content originally appeared on Telerik Blogs and was authored by Assis Zang
Classes are fundamental to object-oriented programming, while records bring several advantages mainly for the data transport function. Learn the main functions of classes vs. records and which contexts each is best suited to.
Despite being a basic subject, classes and records are important for both beginners and experienced developers to understand, as these structures are fundamental for organizing and manipulating data in object-oriented systems (OOP).
In this post, we will explore in more depth their differences, advantages and practical applications, and understand in which context each one is most suitable.
The source code for the post examples is available in this repository: Classes and Records Source Code.
Classes
Classes are the foundation of object-oriented programming. Through them, we can create objects that encapsulate data and behaviors. This means that object-oriented systems will be composed primarily of classes.
Classes implement the main features present in OOP:
- Encapsulation: Allows the protection of the object’s state and exposes only what is necessary through access modifiers that can be public, private, protected, etc.
- Inheritance: Allows classes to obtain properties and behaviors from existing classes, allowing code reuse.
- Polymorphism: Allows objects of different classes to be treated equally.
- Abstraction: Allows complexity to be easily handled by hiding implementation details.
In ASP.NET Core, classes represent data models, services, controllers and configurations. Next, we will check some common examples of classes in ASP.NET Core.
Model Classes
Model classes represent domain entities—for example, a Product
class for an ecommerce system:
public class Product
{
public int Id { get; set; }
public string Name { get; set; }
public string Description { get; set; }
public decimal Price { get; set; }
private int Stock { get; set; }
public Product()
{
}
public Product(int id, string name, string description, decimal price, int stock)
{
Id = id;
Name = name;
Description = description;
Price = price;
Stock = stock;
}
public void ApplyDiscount(decimal percentage)
{
if (percentage < 0 || percentage > 100)
throw new ArgumentException("Percentage must be between 0 and 100");
Price -= Price * (percentage / 100);
}
public void UpdateStock(int quantity)
{
if (quantity < 0)
throw new ArgumentException("Quantity cannot be negative");
Stock += quantity;
}
public int GetStock()
{
return Stock;
}
public void SetStock(int stock)
{
if (stock < 0)
throw new ArgumentException("Stock cannot be negative");
Stock = stock;
}
}
Note that the class is declared as public, which makes it accessible to any member of the project. Within it, public and private properties are declared. In addition, there are methods for manipulating the data, filling in the properties and implementing validations to the input data. This shows the versatility of a class in object-oriented systems, enabling developers to shape it according to the project’s needs.
Classes That Implement Inheritance
Inheritance is an object-oriented concept that allows one class (the derived class or subclass) to inherit the members (methods, properties and others) of another class (the base class or superclass).
The derived class can then use the inherited properties and behaviors, except private members, and the derived class can also have its specific members.
As an example, we can use a class called Employee
, which has properties and methods common to other classes such as Manager
, Engineer
and Salesperson
:
public class Employee
{
public string Name { get; set; }
public int Age { get; set; }
public string Department { get; set; }
public Employee(string name, int age, string department)
{
Name = name;
Age = age;
Department = department;
}
public virtual void DisplayDetails()
{
Console.WriteLine($"Name: {Name}, Age: {Age}, Department: {Department}");
}
public virtual void Work()
{
Console.WriteLine("The employee is working.");
}
}
public class Engineer : Employee
{
public string Specialization { get; set; }
public Engineer(string name, int age, string department, string specialization)
: base(name, age, department)
{
Specialization = specialization;
}
public override void DisplayDetails()
{
base.DisplayDetails();
Console.WriteLine($"Specialization: {Specialization}");
}
public override void Work()
{
Console.WriteLine("The engineer is working on a project.");
}
}
public class Manager : Employee
{
public int TeamSize { get; set; }
public Manager(string name, int age, string department, int teamSize)
: base(name, age, department)
{
TeamSize = teamSize;
}
public override void DisplayDetails()
{
base.DisplayDetails();
Console.WriteLine($"Team Size: {TeamSize}");
}
public override void Work()
{
Console.WriteLine("The manager is managing the team.");
}
}
public class Salesperson : Employee
{
public int SalesMade { get; set; }
public Salesperson(string name, int age, string department, int salesMade)
: base(name, age, department)
{
SalesMade = salesMade;
}
public override void DisplayDetails()
{
base.DisplayDetails();
Console.WriteLine($"Sales Made: {SalesMade}");
}
public override void Work()
{
Console.WriteLine("The salesperson is making sales calls.");
}
}
In the code above, there is a hierarchy of classes. The base class is Employee
, which has properties common to all employee types, such as name, age and department. It also has two virtual methods: DisplayDetails()
and Work()
, which can be overridden by specific methods in derived classes.
The derived classes are Engineer
, Manager
and Salesperson
, inherited from the Employee
class. Each adds specific properties and behaviors to represent different types of employees.
The Engineer
class adds the Specialization
property, which represents the engineer’s specialization, such as software, mechanics, etc. Its Work()
method is overridden to reflect that the engineer is working on a project.
The Manager
class adds the TeamSize
property, which indicates the size of the team managed by the manager. Its Work()
method is overridden to reflect that the manager manages the team.
The Salesperson
class adds the SalesMade
property, which represents the sales the salesperson makes. Its Work()
method is overridden to reflect that the salesperson is making sales calls.
Note that this class structure allows you to represent different types of employees with specific properties and behaviors, while maintaining the ability to access and manipulate information common to all employees through the Employee
base class.
This is possible thanks to the inheritance mechanism, which in C# we implement as follows: DerivedClass : BaseClass
. Without inheritance, all properties and behaviors would be duplicated in the code, making it impossible to reuse components and making the code more difficult to maintain.
Class Initialization and Constructors
In the examples above, we saw how to declare classes in C#, but for a class to be ready for use, such as the Product
class, it needs to be initialized. A common example of initialization is by creating an instance of the class, which is created through the new
operator followed by the class name and two parentheses:
var product = new Product();
Now, the product
variable has an instance of the Product
class, and is ready to initialize its properties:
product.Name = "Smartphone red";
It is also possible to initialize properties directly along with the class:
var product = new Product()
{
Name = "Smartphone red"
};
Note that when initializing the Product
class, we use two parentheses. This refers to the class constructor, which in this case is an empty constructor, as no value is passed as a parameter between the parentheses.
In C#, a constructor is a special class method executed when a new instance of that class is created. The purpose of a constructor is mainly to initialize class objects by setting their members.
To create a constructor, simply declare an access restriction that can be public
followed by the class name and two parentheses ()
and two braces { }
:
public Product()
{
}
No parameters were created in this constructor—that is, it is empty, so it will be useful if you want to create a Product
instance without the obligation to fill in any properties.
But it is also possible to force some class properties to be filled when creating a new instance. The example below demonstrates how to create a constructor that requires filling in properties.
public Product(int id, string name, string description, decimal price, int stock)
{
Id = id;
Name = name;
Description = description;
Price = price;
Stock = stock;
}
To create the new instance by filling in the properties you can do this:
Product newProduct = new Product(1, "Laptop", "High-performance laptop", 2500.00m, 10);
Note that now the values are passed through the class parameters. If they were not passed in their entirety, the compiler would report an error.
The image below shows the constructor declaration in Visual Studio Code. However, the stock
parameter is not passed to the constructor, which results in a compilation error.
Another example of a very common use of constructors is the dependency injection function. Dependency injection is a design pattern that helps you create more modular and maintainable code.
In the example below, the constructor of the Service
class receives the ProductRepository
class interface as a parameter. Within the constructor, the private variable _productRepository
is assigned to the instance of the class generated in the constructor. This allows the variable to be available for use in all methods of the ProductService
class, depending on how the dependency injection was created—in this case, the AddSingleton
method is used.
public class ProductService
{
private readonly IProductRepository _productRepository;
public ProductService(IProductRepository productRepository)
{
_productRepository = productRepository;
}
}
//Configuring dependency injection in the Program class
builder.Services.AddSingleton<IProductRepository, ProductRepository>();
Controllers
Controllers are used in ASP.NET Core and MVC (Model View Controller) projects where API endpoints exist and are responsible for responding to requests made to the API.
Despite being a class, controllers usually inherit from a special class such as Controller or ControllerBase. The example below demonstrates the structure of a Controller class.
using ClassesAndRecordsExample.Models;
using ClassesAndRecordsExample.Services;
using Microsoft.AspNetCore.Mvc;
namespace ClassesAndRecordsExample.Controllers;
public class ProductController : Controller
{
private readonly IProductService _productService;
public ProductController(IProductService productService)
{
_productService = productService;
}
[HttpGet]
public async Task<ActionResult<List<Product>>> GetAllProducts()
{
return Ok(await _productService.GetAllProducts());
}
}
Note that despite being a special class, the ProductController
class implements a constructor to have an instance of the service class available.
Static Classes
Static classes are a special type of class and their main characteristic is that they cannot be instantiated. They are used to contain methods, properties and other members that can be accessed without the need for the class to be instantiated.
The code below shows the declaration and use of a static class:
public static class ProductUtils
{
public static string SerializeProduct(Product product)
{
return JsonSerializer.Serialize(product);
}
public static Product DeserializeProduct(string json)
{
return JsonSerializer.Deserialize<Product>(json);
}
}
var productSerialized = ProductUtils.SerializeProduct(product);
string productString = "id: 1, name: Laptop, Description: High-performance laptop, price: 2500.00m, stock: 10";
var productDeserialized = ProductUtils.DeserializeProduct(productString);
Note that there is no need to create an instance of the ProductUtils
class. Just declare it, and its methods are ready to use.
Records
Like classes, records are a type of data structure used for modeling. However, records have some restrictions compared to classes.
Introduced in C# 9.0, records offer an alternative to classes, defining simple data types—perfect for storing values.
Below are the main characteristics of records:
- They are a reference type: This means that records use a pointer or a reference to an object stored in heap memory, instead of directly accessing the data, as in the case of value types.
- They are immutable: When creating a record, its values cannot be changed directly like classes. It is necessary to use the
init
directive instead ofset
for properties to be possible. - They have value equality: This means that two records with the same property values are considered equal, unlike normal classes, which implement reference equality.
- They have a simplified syntax: Records use a simple syntax that reduces the boilerplate code necessary for their implementation.
Creating a Record
The example below demonstrates creating a record to represent a ProductDto
entity.
public record ProductDto(string Name, decimal Price, string Category);
The code below demonstrates how to assign values to the ProductDto
record properties.
var productDto = new ProductDto("Laptop", 1500.00m, "Electronics");
Modifying a Record
By definition, records are immutable. This means that, unlike classes, if you try to change the value of a property, it will result in a compilation error:
The error occurs because the init-only
properties cannot be modified after creating the record object. To make this possible you can use the with
expression:
var productDtoModified = productDto with { Category = "Laptops" };
Note that to be able to modify the property of a record, which is an immutable object, it is necessary to use the with
expression, copying the value of productDto
to the productDtoModified
variable with the changed property.
Records as Structs
Starting with version 10 of C#, it is possible to define records as structs:
public record struct Address(string Street, string City, string State, string ZipCode);
An advantage of using records as structs is that they are value types, which means they are stored on the stack and not the heap, resulting in better performance in terms of memory allocation and access, especially in scenarios where high performance is required or on systems with memory constraints.
The example below demonstrates how to instantiate a record struct and how to deconstruct it to be able to use its properties separately:
Address address = new Address("123 Main St", "Springfield", "IL", "62701");
var (street, city, state, zipCode) = address;
Console.WriteLine($"Street: {street}, City: {city}, State: {state}, ZipCode: {zipCode}");
Record structs combine the advantages of structs (performance and efficient use of memory) with the benefits of records (immutability and automatic equality).
Hierarchy in Records
Just like classes, it is possible to implement an inheritance mechanism in records, enabling a hierarchical structure with subtypes.
To implement inheritance with records, the sub-record must have all the requirements of the base record constructor, as can be seen in the example below:
public record CustomerAddress(string Street, string City, string State, string ZipCode);
public record ResidentialAddress(string ResidentName, string Street, string City, string State, string ZipCode) :
CustomerAddress(Street, City, State, ZipCode);
Abstract and Sealed Records
Defining records as abstract means that they cannot be instantiated directly. To instantiate them, we need to create a concrete type and from it create an instance:
public abstract record Customer(string Name, string Email, string Phone);
public record InternalCustomer(decimal Discount, string Name, string Email, string Phone) :
Customer(Name, Email, Phone);
var customer = new Customer("John Smith", "john@example.com", "95940033");
var internalCustomer = new InternalCustomer(10, "John Smith", "john@example.com", "95940033");
Note in the image below that it was impossible to create an instance of the Customer
record because it was abstract. But it is possible when using the sub-record (InternalCustomer
), as it indirectly accesses the Customer
record.
To prevent records from being used as subtypes, simply use the reserved word sealed
:
public sealed record SealedCustomer(string Name, string Email, string Phone);
If the sealed record is used as a superior type, the IDE will report an error:
Classes or Records: When to Use Each One?
In ASP.NET Core, the choice between classes and records must depend on the specific needs within the scenario in which your project is inserted. Below are some considerations that can be made when choosing classes or records.
Mutability
Use classes when you need objects that can be constantly changed after creation. Web APIs are an example where object values are updated frequently through CRUD operations.
Behaviors and Methods Coupled to the Model
Classes are appropriate in scenarios where there is complex logic and it is coherent to create methods together with the model class. For example, suppose you have a Customer
class, and you need to create discount logic that depends on data associated with the model. You can implement the calculation method within the Customer
class. Furthermore, you can implement some logic for filling in the value of properties within the class, allowing you to encapsulate properties with access modifiers, such as Private, Protected, Internal, etc.
Immutability and Simplicity
Records are a good choice for scenarios where the use of immutable objects is relevant and it is necessary to prevent the object’s properties from being changed later. Furthermore, records are mainly recommended for creating data models, such as Data Transfer Objects (DTOs) due to their simplicity of implementation through their cleaner syntax than classes.
Equality of Objects in Values
Records use value semantics, which bases the equality of objects on values and not on the reference. They are compared by value across all their properties, unlike classes, which are compared by reference by default. Thus, records are instrumental in scenarios where equality of objects based on values is relevant.
Conclusion
Classes are basic data types in object-oriented languages like C#. ASP.NET Core is based on class structures, such as the MVC (Model View Controler) pattern, where classes are commonly used to create model classes and application controllers.
Furthermore, classes allow web systems to be implemented modularly, contributing to the sharing of resources between applications.
Records are a data structure similar to classes. They act as an alternative in some specific scenarios such as the creation of DTOs, allowing a cleaner and simpler syntax to be implemented, in addition to its restrictions such as immutability by default.
In this post, we saw several examples of classes and records and analyzed the highlights of each one, considering in which scenario each is best suited. So, before using the standard classes, consider whether a record might be a better fit.
This content originally appeared on Telerik Blogs and was authored by Assis Zang
Assis Zang | Sciencx (2024-08-22T09:13:24+00:00) ASP.NET Core Basics: Classes and Records—How and When to Use Them. Retrieved from https://www.scien.cx/2024/08/22/asp-net-core-basics-classes-and-records-how-and-when-to-use-them/
Please log in to upload a file.
There are no updates yet.
Click the Upload button above to add an update.