Shared Code, Reduced Stress: Simplifying WPF Solutions with Mediator Pattern and Direct Library Referencing
Enterprises and companies are aiming towards the consolidation of their systems and consequently bundle business logic in complex service-oriented APIs that are accessible by internal web or client applications. The reason for that is simple: In order to be ready for the next step in enterprise digitalization, organizations need to get their current software architecture “in shape” to serve and support further complexity and sophistication when it comes to digital services.
The Advantages of combining Direct Library Referencing Over REST API Calls for a Unified Business Logic Approach by design and keeping the architecture clean
Problem Statement
On a daily basis, this means bundling and consolidating business logic in a single location as opposed to having it dispersed over several separate tools and services that are running on questionable under-the-desk servers.
The business goal is to achieve process management agility and quickly adjust to the volatility of today’s markets.
However, moving everything to a centralized location and a common service-oriented access point also creates a big dependency and loads on a particular component of the infrastructure. It will require constant online access to the asset and its performance under various conditions.
Let’s elaborate further on the benefits of utilizing Clean Architecture in combination with the Mediator Pattern in order to build a common logic lawyer that can be easily distributed to an Enterprise API Hub as a server as well as a library dependency in the WPF Client application.
Our Approach
At Povio, we provide our engineering teams with robust tools and resources to build software applications that are scalable, maintainable, and follow industry standards. With that in mind, we have decided to extend our collection with a new project template for desktop solutions specifically aimed at simplifying WPF Solutions through Mediator Pattern and Direct Library Referencing.
Our project templates offer versatility by providing two distinct modes:
- Endpoint Application Mode
- API Connectivity Mode
In the Endpoint application mode, you can build a standalone client-style application with a complex desktop user interface, that communicates with the core domain logic (dll reference) using MediatR as an efficient message-handling provider.
Alternatively, in API connectivity mode, you can seamlessly connect to an existing API to get the needed data. With this approach, the engineers can easily decide which methods of the domain layer they will use from the directly referenced libraries (DLLs) or which to call from the API hub. The implementation logic is the same, and because we utilize the messaging approach implemented with the Mediator pattern, we achieve code that stays clean and maintainable.
This approach allows us to combine domain-specific logic in one location and use it separately or integrate it depending on the needs of the use case the application is attempting to address.
MediatR Summary
Clean architecture emphasizes the separation of concerns and the establishment of clear boundaries between different components in a software system. To accomplish this, we used MediatR, which is an open-source library in C# that facilitates the implementation of the mediator pattern, a key component of clean architecture. MediatR simplifies communication between these components by centralizing the interaction logic, promoting loose coupling, and enhancing modularity. With MediatR, you can define and handle application-specific messages or commands, encapsulating the request and the corresponding handler logic. The library provides a mediator object that acts as a single point of contact for message dispatching and handling.
The library supports different types of messages, including commands, queries, and notifications. It also offers features like request/response pipelines, pipeline behaviors, and the ability to handle multiple handlers for a single message.
Solution Description
To achieve the desired goal, we used the HostBuilder, which encapsulates the environment, configuration, and lifetime management. It allows for centralized configuration and setup of resources such as logging, dependency injection, and other application services. It is important to register all the services in the app startup process, regardless of the template's final use, which is set in the configuration.
In a WPF (Windows Presentation Foundation) application, the entry point of the application is typically defined in the App.xaml file and the corresponding App.xaml.cs file. This is the correct place to create a host builder and register all services.
public partial class App : System.Windows.Application
{
public static IHost? AppHost { get; private set; }
public App()
{
AppHost = Host.CreateDefaultBuilder()
.ConfigureServices((hostContext, services) =>
{
services.AddPresentationServices();
})
.Build();
}
}
The services registered include a variety of sub-services, but the most important ones are shown in the code snippet below.
public static class ConfigurationServices
{
public static IServiceCollection AddPresentationServices(
this IServiceCollection services)
{
//mediatr
services.AddMediatR(Assembly.GetExecutingAssembly());
//sending implementation
services.AddSingleton<IApiService, ApiService>();
services.AddSingleton<ISenderService, SenderService>();
//Application services
services.AddApplicationServices();
//integration services
services.AddInfrastructureServices(
ConfigurationHelper.Instance.ConfigManager);
... //other services registered
return services;
}
}
To add MediatR, we used the AddMediatR extension method provided by the MediatR library. This method requires specifying the assembly that contains the request and handler types. The ApiService provides the necessary logic to communicate with the API endpoint, and the SenderService decides, based on the configuration, the type of communication between the application and core logic.
public interface ISenderService
{
Task<TResponse> Send<TResponse>(
IRequest<TResponse> request,
CancellationToken cancellationToken = default);
}
public class SenderService : ISenderService
{
private ISender _mediatr;
private IApiService _apiService;
private bool _useApi;
public SenderService(
ISender sender,
IApiService apiService)
{
_mediatr = sender;
_apiService = apiService;
_useApi = false; //can be set by reading app config,etc..
}
Task<TResponse> ISenderService.Send<TResponse>(
IRequest<TResponse> request,
CancellationToken cancellationToken)
{
try
{
if (_useApi)
return _apiService.Send(request, cancellationToken);
else
return _mediatr.Send(request, cancellationToken);
}
catch (Exception ex)
{
...//handle exception
return default!;
}
}
}
How to Use - Sample of ISendingSErvice
To complete the puzzle, we need to demonstrate the utilization of the SenderService. The service is designed to provide a flexible solution by returning a generic Tasks object, where the specific return type is determined by the type of request object used. This versatility allows us to utilize the service for various purposes. As an example, the code snippet below illustrates two methods. The first method sends a request to add an object to the database, while the second method retrieves a list of objects. This showcases the SenderService's capability to handle different operations based on the specific request made.
public class FlowersService : IFlowersService
{
private ISenderService _senderService;
private IMapperService _mapperService;
public FlowersService(
ISenderService senderService,
IMapperService mapperService)
{
_senderService = senderService;
_mapperService = mapperService;
}
public async Task<Guid?> AddFlower(FlowerModel model)
{
Guid? newGuid = await _senderService.Send(
new CreateFlowerCommand(model.Name, model.Type));
if (newGuid != null && newGuid != Guid.Empty)
return newGuid;
else
return null;
}
public async Task<List<FlowerModel>> GetFlowers()
{
var result = new List<FlowerModel>();
var flowersFromDb = await _senderService.Send(
new GetFlowersQuery());
foreach (var item in flowersFromDb)
{
result.Add(_mapperService.Map(item));
}
return result;
}
...
}
Pros and Cons
Pros
The existing codebase can be utilized as a foundation for an API Services that can act as an endpoint application or as a direct dependency for the client application in WPF. By adopting the latter approach, the client can avoid direct communication with the API (endpoint solution), alleviating pressure on the infrastructure. In this scenario, the business logic is executed on the client side, enabling a more decentralized approach.
Opting for a shared codebase also streamlines the testing process. It simplifies the testing efforts, allowing for efficient and comprehensive testing of the shared functionality. As a result, both the API and client components can be thoroughly tested, ensuring their reliability and robustness.
Even when there are modifications to the business logic, the utilization of CI/CD pipelines can be customized to effectively distribute the updates to both the application servers and the app.
For client applications, it is crucial to have an app update service in place to guarantee that they receive the most up-to-date version that incorporates the revised business logic. The optimal implementation of such a service varies depending on the specific scenario in which the application will be used and the requirements of the clients. The best approach is determined by considering these factors and tailoring the solution accordingly.
Cons
One of the potential challenges with this concept is ensuring the timely update of the revised business logic on client applications. Failure to update the client application can lead to issues when it comes to communicating with the core business logic.
To solve this issue, a mechanism needs to be devised that will ensure that the client application uses the same version of business logic. Various methods exist for implementing updates, such as performing updates on client application startup or incorporating a background update service that periodically checks for updates. Among these options, one of the most crucial implementations should be integrated within the SenderService. To be specific, it is important to focus on the exception-handling mechanism in the service when sending data, as this is the initial point of interaction with the core business logic that might potentially be outdated. In addition to handling the exception, it is necessary to incorporate a check for the version of the core business logic and trigger an update if required.
Conclusion
In this blog post, we explored the benefits of simplifying WPF solutions through the mediator pattern and direct library referencing. By adopting a unified business logic approach, organizations can consolidate their systems and achieve greater agility in managing processes. The use of Clean Architecture and MediatR facilitates the separation of concerns, enhances modularity, and promotes loose coupling between components.
By utilizing direct library referencing in combination with REST API calls, developers can leverage a shared codebase and reduce their dependency on a centralized infrastructure component. This approach allows for the bundling and consolidation of business logic in a common location, ensuring a clean and maintainable codebase. Whether it's building a standalone client-style application or connecting to an existing API, the MediatR pattern enables flexibility and adaptability in implementing the desired functionality.
Furthermore, the adoption of direct library referencing and the mediator pattern simplifies the testing process and improves the reliability of both the API and client components. With a streamlined testing effort, organizations can ensure the robustness and efficiency of the shared functionality.
However, it is important to address the challenge of timely updates to the client applications when modifications are made to the business logic. Implementing a mechanism within the SenderService to handle exceptions, check for outdated versions, and trigger updates as needed is crucial to maintaining compatibility between the client application and the core business logic.
In conclusion, by embracing direct library referencing and the mediator pattern, organizations can simplify WPF solutions, achieve a clean architecture, and enhance the agility and scalability of their software systems. This approach empowers engineers to create maintainable and robust applications while promoting a unified business logic approach.