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.
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:
Cons:
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.
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.
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.
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:
First, let’s create the solution and the project. So, in Visual Studio:
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:
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; }
}
using System.ComponentModel.DataAnnotations;
namespace ProductCatalog.Models;
public class Seller
{
[Key]
public Guid Id { get; set; }
public string? Name { get; set; }
}
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:
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; }
}
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:
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:
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.
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>>();
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.
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
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.
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.