Weird Combination but Effective: Repository and Adapter Patterns in .NET Core Using an Item API
In the world of software development, flexibility and maintainability are key. As our applications grow, they often need to interact with multiple data sources: databases, APIs, or even legacy systems. But how can we design our code so that it remains flexible without being tightly coupled to specific data sources?
Enter the Repository and Adapter patterns. While powerful on their own, combining these two patterns can take your architecture to the next level by decoupling data access logic and adapting it to different sources effortlessly. Let’s explore how to combine these patterns using an example with Items.
Embark on a journey of continuous learning and exploration with DotNet-FullStack-Dev. Uncover more by visiting our https://dotnet-fullstack-dev.blogspot.com reach out for further information.
What Are the Repository and Adapter Patterns?
- Repository Pattern: This pattern abstracts the logic for accessing data. It acts as a mediator between the application and the data source, hiding the complexities of data retrieval and management.
- Adapter Pattern: This pattern is like a translator, allowing two incompatible interfaces to work together. If your application expects data in a certain format, but the data source provides it differently, the adapter ensures they still communicate effectively.
Now, let’s see how we can combine these two patterns in a real-world scenario.
Scenario: Building an Item Management System
Imagine you are building a system to manage Items (products, inventory, etc.), and you need to interact with multiple data sources: a relational database, an external API, and maybe even a legacy system. Your application doesn’t need to know the specifics of how the data is retrieved or where it’s stored — it just needs to access and manipulate items.
Let’s dive into how to combine these patterns to make this possible.
Step 1: Create the Repository Interface
First, we define a common repository interface that our service layer will interact with. This interface abstracts the operations we need for managing items.
public interface IItemRepository
{
Item GetById(int id);
IEnumerable<Item> GetAll();
void Add(Item item);
void Update(Item item);
void Delete(int id);
}
This interface defines the basic operations we expect to perform on items, regardless of the underlying data source.
Step 2: Implement the Repository with the Adapter Pattern
Now, let’s assume we need to interact with two different data sources: a SQL database and an external API. To handle this, we’ll create adapters that adapt each data source to the interface expected by the repository.
SQL Adapter:
public class SqlItemAdapter : IItemRepository
{
private readonly SqlDbContext _context;
public SqlItemAdapter(SqlDbContext context)
{
_context = context;
}
public Item GetById(int id)
{
return _context.Items.Find(id);
}
public IEnumerable<Item> GetAll()
{
return _context.Items.ToList();
}
public void Add(Item item)
{
_context.Items.Add(item);
_context.SaveChanges();
}
public void Update(Item item)
{
_context.Items.Update(item);
_context.SaveChanges();
}
public void Delete(int id)
{
var item = _context.Items.Find(id);
if (item != null)
{
_context.Items.Remove(item);
_context.SaveChanges();
}
}
}
API Adapter:
Let’s say we also need to fetch data from an external API. The external API may return items in a slightly different format, so we use an adapter to transform this data.
public class ApiItemAdapter : IItemRepository
{
private readonly HttpClient _httpClient;
public ApiItemAdapter(HttpClient httpClient)
{
_httpClient = httpClient;
}
public Item GetById(int id)
{
var response = _httpClient.GetAsync($"api/items/{id}").Result;
return JsonConvert.DeserializeObject<Item>(response.Content.ReadAsStringAsync().Result);
}
public IEnumerable<Item> GetAll()
{
var response = _httpClient.GetAsync("api/items").Result;
return JsonConvert.DeserializeObject<List<Item>>(response.Content.ReadAsStringAsync().Result);
}
public void Add(Item item)
{
var content = new StringContent(JsonConvert.SerializeObject(item), Encoding.UTF8, "application/json");
_httpClient.PostAsync("api/items", content).Wait();
}
public void Update(Item item)
{
var content = new StringContent(JsonConvert.SerializeObject(item), Encoding.UTF8, "application/json");
_httpClient.PutAsync($"api/items/{item.Id}", content).Wait();
}
public void Delete(int id)
{
_httpClient.DeleteAsync($"api/items/{id}").Wait();
}
}
Step 3: Use Dependency Injection to Switch Between Data Sources
Now that we have our repository interface and adapters for different data sources, we can easily switch between them using dependency injection. For instance, you might use the SQL adapter in a production environment and the API adapter in a testing or distributed environment.
public class Startup
{
public void ConfigureServices(IServiceCollection services)
{
// Use SQL adapter by default
services.AddScoped<IItemRepository, SqlItemAdapter>();
// Alternatively, switch to API adapter:
// services.AddScoped<IItemRepository, ApiItemAdapter>();
}
}
This setup makes it easy to switch between different data sources without changing your service or business logic. The repository pattern provides a clean interface, while the adapter pattern adapts the underlying data source to meet the needs of the repository.
Step 4: Interacting with the Repository in Your Services
In your business logic, the code remains the same, no matter which data source you’re using. This is the beauty of combining the repository and adapter patterns.
public class ItemService
{
private readonly IItemRepository _itemRepository;
public ItemService(IItemRepository itemRepository)
{
_itemRepository = itemRepository;
}
public Item GetItem(int id)
{
return _itemRepository.GetById(id);
}
public IEnumerable<Item> GetAllItems()
{
return _itemRepository.GetAll();
}
public void AddItem(Item item)
{
_itemRepository.Add(item);
}
public void UpdateItem(Item item)
{
_itemRepository.Update(item);
}
public void DeleteItem(int id)
{
_itemRepository.Delete(id);
}
}
The service doesn’t care whether the data is coming from SQL, an API, or any other data source. The repository pattern ensures that your business logic remains data-agnostic, and the adapter pattern guarantees smooth communication with the data source.
Conclusion: Why Combine These Patterns?
By combining the Repository and Adapter patterns, you achieve a flexible, decoupled architecture that’s easy to maintain and extend. If you ever need to switch data sources — say, moving from a SQL database to a NoSQL database or an external API — you can do so without altering your core business logic.
In this Item API example, the Repository Pattern keeps the application focused on business needs, while the Adapter Pattern ensures compatibility with diverse data sources. Together, they form a powerful duo that prepares your application for growth and change.
What’s Next?
Try applying this pattern combination in your own projects. Whether you’re managing a product catalog, user profiles, or any data-centric service, the flexibility provided by these patterns will save you headaches down the road. Experiment with new data sources and enjoy the freedom to adapt quickly!
Let me know in the comments — how do you plan to use these patterns in your next project?