Telerik blogs

Generic types are part of the fundamentals of the C# language and can help the developer to save time by reusing code. Learn more about generic types and how to create an API in .NET 7 using the generic repository pattern.

Generic types are a valuable feature available in C# since the earliest versions of .NET. Despite being part of the fundamentals of the language, many developers avoid using them—maybe because they don’t know exactly how to use them or maybe even don’t know about them.

In this post, you will learn a little about generics in the context of .NET and how to use them in a real scenario.

What Are Generics in .NET?

Generics in the .NET context are classes, structures, interfaces and methods with placeholders for one or more of the types they store or use.

Imagine that you need to create an application that will perform the registration of new customers. However, when performing the registration, it is necessary to save this information in other databases such as a history table—another table from an external supplier—and so we have at least three inserts to do in each table. So as not to repeat the same method and duplicate the code, we can create a method that accepts a generic class, and that way the three inserts can be done using the same method.

Among the benefits of generics are the reduction of repeated code, performance gains and type safety.

Pros and Cons of Generics

Pros:

  • Type security: The data type is checked during runtime, eliminating the need to create extra code to check.
  • Less code writing: With generic types, it is possible to reuse classes and methods, making less code to be written.
  • Better performance: Generic types generally perform best when storing and manipulating value types.
  • Simplification of dynamically generated code: Dispenses with type generation when dynamically generated. So it is possible to use lightweight dynamic methods instead of generating entire assemblies.

Cons:

  • There are no cons to using generics, but there are some limitations such as enumerations that cannot have generic type parameters and the fact that .NET does not support context-bound generic types.

When to Use Generics

Generic classes and methods combine reuse, type safety and efficiency in a way that non-generic alternatives cannot. So whenever you want to utilize any of these benefits, consider using generics.

The Generic Repository Pattern

In small projects with few entities, it is common to find one repository for each of the entities, perform CRUD operations for each of them, and repeat the code several times.

However, imagine a project where it is necessary to implement the CRUD of several entities. Besides the repetition of code, the maintenance of this project would also be expensive—after all, there would be several classes to be changed.

With the generic repository pattern, we eliminate these problems by creating a single repository that can be shared with as many entities as needed.

Practical Example

To demonstrate the use of generics in a real example, we will create a .NET application in a scenario where it will be necessary to implement a CRUD for two entities—one for the Product entity and another for the Seller entity. Thus we’ll see in practice how it is possible to reuse code through generics.

To develop the application, we’ll use .NET 7. You can access the project’s source code here.

Project Dependencies

You need to add the project dependencies—either directly in the project code “ProductCatalog.csproj”:

 <PackageReference Include="Microsoft.EntityFrameworkCore" Version="7.0.0" />
    <PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="7.0.0">
      <PrivateAssets>all</PrivateAssets>
      <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
    </PackageReference>
    <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="7.0.0" />
    <PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="7.0.0">
      <PrivateAssets>all</PrivateAssets>
      <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
    </PackageReference>

or by downloading the NuGet packages:

Create the Application

First, let’s create the solution and the project. So, in Visual Studio:

  • Create a new project
  • Choose ASP. NET Core Web API
  • Name it (“ProductCatalog” is my suggestion)
  • Choose .NET 7.0 (Standard Term Support)
  • Uncheck “Use controllers”
  • Click Create

Create the Entities Classes

Let’s create two model classes that will represent the Product and Seller entities. So, create a new folder called “Models” and, inside it, create the classes below:

  • Product
using System.ComponentModel.DataAnnotations;

namespace ProductCatalog.Models;
public class Product
{
    [Key]
    public Guid Id { get; set; }
    public string? Name { get; set; }
    public string? Description { get; set; }
    public string? Type { get; set; }
    public string? DisplayName { get; set; }
    public string? Brand { get; set; }
    public string? Category { get; set; }
    public bool Active { get; set; }
}
  • Seller
using System.ComponentModel.DataAnnotations;

namespace ProductCatalog.Models;
public class Seller
{
    [Key]
    public Guid Id { get; set; }
    public string? Name { get; set; }
}

Create the Context Class

The context class will be used to add the database settings which in this case will be SQLite. So, create a new folder called “Data” and, inside it, create the class below:

  • ProductCatalogContext
using Microsoft.EntityFrameworkCore;
using ProductCatalog.Models;

namespace ProductCatalog.Data;
public class ProductCatalogContext : DbContext
{
    protected override void OnConfiguring(DbContextOptionsBuilder options) =>
       options.UseSqlite("DataSource = productCatalog; Cache=Shared");

    public DbSet<Product> Products { get; set; }
    public DbSet<Seller> Sellers { get; set; }
}

Create the Generic Repository

The repository will contain the methods responsible for executing the CRUD functions. As we will use the generic repository pattern, we will have only one class and one interface that will be shared by the Product and Seller entities.

So inside the “Data” folder, create a new folder called “Repository.” Inside it, create a new folder called “Interfaces,” and inside that create the interface below:

  • IGenericRepository
namespace ProductCatalog.Data.Repository.Interfaces;
public interface IGenericRepository<T> where T : class
{
    IEnumerable<T> GetAll();
    Task<T> GetById(Guid id);
    Task Create(T entity);
    void Update(T entity);
    Task Delete(Guid id);
    Task Save();
}

💡 Note that the methods expect as a parameter a generic type represented in C# with the expression “<T>” and in the case of getting methods they also return generic types.

The next step is to create the class that will implement the interface methods. Then inside the “Repository” folder create the class below:

  • GenericRepository
using Microsoft.EntityFrameworkCore;
using ProductCatalog.Data.Repository.Interfaces;

namespace ProductCatalog.Data.Repository;
public class GenericRepository<T> : IGenericRepository<T> where T : class
{
    private readonly ProductCatalogContext _context;
    private readonly DbSet<T> _entities;

    public GenericRepository(ProductCatalogContext context)
    {
        _context = context;
        _entities = context.Set<T>();
    }

    public IEnumerable<T> GetAll() =>
        _entities.ToList();

    public async Task<T> GetById(Guid id) =>
         await _entities.FindAsync(id);

    public async Task Create(T entity) =>
        await _context.AddAsync(entity);

    public void Update(T entity)
    {
        _entities.Attach(entity);
        _context.Entry(entity).State = EntityState.Modified;
    }

    public async Task Delete(Guid id)
    {
        T existing = await _entities.FindAsync(id);
        _entities.Remove(existing);
    }

    public async Task Save() =>
      await _context.SaveChangesAsync();
}

💡 Note that in the class above we have the global variable “_entities” that receives the value of data collections, depending on which entity is being passed as a parameter.

We also have the methods that perform CRUD operations through the EF Core extension methods like “FindAsync,” “AddAsync,” etc.

Add the Dependencies Injections

To add dependency injections, in the Program.cs file add the following lines of code just below the “builder.Services.AddSwaggerGen()” snippet:

builder.Services.AddScoped<ProductCatalogContext>();
builder.Services.AddTransient<IGenericRepository<Product>, GenericRepository<Product>>();
builder.Services.AddTransient<IGenericRepository<Seller>, GenericRepository<Seller>>();

Create the API Endpoints

Below is the code of all API endpoints, both Product and Seller. Then, still in the Program.cs file, add the code below before the snippet “app.Run()”:

#region Product API

app.MapGet("productCatalog/product/getAll", (IGenericRepository<Product> service) =>
{
    var products = service.GetAll();
    return Results.Ok(products);
})
.WithName("GetProductCatalog")
.WithOpenApi();

app.MapGet("productCatalog/product/getById", (IGenericRepository<Product> service, Guid id) =>
{
    var products = service.GetById(id);
    return Results.Ok(products);
})
.WithName("GetProductCatalogById")
.WithOpenApi();

app.MapPost("productCatalog/product/create", (IGenericRepository<Product> service, Product product) =>
{
    service.Create(product);
    service.Save();
    return Results.Ok();
})
.WithName("CreateProductCatalog")
.WithOpenApi();

app.MapPut("productCatalog/product/update", (IGenericRepository<Product> service, Product product) =>
{
    service.Update(product);
    service.Save();
    return Results.Ok();
})
.WithName("UpdateProductCatalog")
.WithOpenApi();

app.MapDelete("productCatalog/product/delete", (IGenericRepository<Product> service, Guid id) =>
{
    service.Delete(id);
    service.Save();
    return Results.Ok();
})
.WithName("DeleteProductCatalog")
.WithOpenApi();

#endregion

#region Seller API

app.MapGet("productCatalog/seller/getAll", (IGenericRepository<Seller> service) =>
{
    var products = service.GetAll();
    return Results.Ok(products);
})
.WithName("GetSeller")
.WithOpenApi();

app.MapGet("productCatalog/seller/getById", (IGenericRepository<Seller> service, Guid id) =>
{
    var products = service.GetById(id);
    return Results.Ok(products);
})
.WithName("GetSellerById")
.WithOpenApi();

app.MapPost("productCatalog/seller/create", (IGenericRepository<Seller> service, Seller seller) =>
{
    service.Create(seller);
    service.Save();
    return Results.Ok();
})
.WithName("CreateSeller")
.WithOpenApi();

app.MapPut("productCatalog/seller/update", (IGenericRepository<Seller> service, Seller seller) =>
{
    service.Update(seller);
    service.Save();
    return Results.Ok();
})
.WithName("UpdateSeller")
.WithOpenApi();

app.MapDelete("productCatalog/seller/delete", (IGenericRepository<Seller> service, Guid id) =>
{
    service.Delete(id);
    service.Save();
    return Results.Ok();
})
.WithName("DeleteSeller")
.WithOpenApi();

#endregion

💡 Note that in each endpoint when the interface “IGenericRepository” is declared, we are passing as an argument the entity corresponding to the scope of the API. That is, in the Product API we declare IGenericRepository<Product>, and in the Seller API, IGenericRepository<Seller>. This way we can use the same interface for any entity that our code may have, as it expects a generic type, regardless of what it is.

Generate the Migrations

To generate the database migrations, we need to run the EF Core commands. For that, we need to install the .NET CLI tools. Otherwise, the commands will result in an error.

The first command will create a migration called InitialModel and the second will have EF create a database and schema from the migration.

More information about migrations is available in Microsoft’s official documentation.

You can run the commands below in a project root terminal.

dotnet ef migrations add InitialModel
dotnet ef database update

Alternatively, run the following commands from the Package Manager Console in Visual Studio:

Add-Migration InitialModel
Update-Database

Test the Application

To test the application, just run the project and perform the CRUD operations. The GIF below demonstrates using the Swagger interface to perform the create functions in both APIs.

Application test

Conclusion

Through the example taught in the article, we can see in practice how the use of generics can be a great option when the objective is to save time through code reuse.

So always consider making use of generics when the opportunity arises. Doing so, you will be acquiring all the advantages of generic types in addition to demonstrating that you care about creating reusable code.

ASP.NET Core REPL: Share code snippets, edit demos on the spot with ASP.NET Core REPL


assis-zang-bio
About the Author

Assis Zang

Assis Zang is a software developer from Brazil, developing in the .NET platform since 2017. In his free time, he enjoys playing video games and reading good books. You can follow him at: LinkedIn and Github.

Related Posts

Comments

Comments are disabled in preview mode.