too much to handle
We have been creating proposals by hand since the foundation of our company. As time passed, it became more and more frustrating to create these proposals using templates, modifying them by hand, and paying attention to data consistency (e.g. checking if the sum of the items equals to the number in the total price field). This led us to create an internal project called SnaProposal, to make our life easier.
This tool is capable of handling proposal related data (clients, technologies, etc.) and assets like pictures to generate a proposal in the end in the form of a Microsoft Word document.
getting the puzzle done
In the beginning we created a monolithic application, that contained both the Web API and the Word Generator functionalities. As we progressed, we wanted to generalize the components as much as possible to create a robust application which can evolve someday into a framework for document generation. In the process we separated the word generation into a microservice, which can handle multiple plugin-like assemblies, that implement the common IWordGenerator interface.
Our first step was to create a RESTful application to handle the data necessary for making a proposal. For that we used ASP .NET Core 3.1 as the main framework. We stored the data in a PostgreSQL database. To simplify the data manipulation, we used Entity Framework Core to access the data.
For the frontend application, we chose an Angular 8 web application. This is responsible for gathering and managing all the necessary data, which will be inserted into the Word document.
Writing an application to create a Microsoft Word document could seem to be an overkill. However, when you create a few, 1-4 pages of documents every day it can consume a lot of your precious time. This led us to the conclusion to make an application like this.
Making a word document from a C# application can be a tricky task, but once you get your head around using the Microsoft.Office.Interop.Word library, it becomes easier. It has the full potential of what you would expect when you open the Microsoft Word.
We had to store a few files to generate the proposals, for example corporate logos, team member images, etc. For that we created a sub-module named FileManager. It was capable of saving, deleting and moving our files in the file system. We tried to make it as general as possible, because we wanted to use it in other projects, and as the word generator microservice was born, we also used it there to save the generated word files.
The initial architecture was pretty simple: a frontend application communicates with a backend service which creates a Word document, just like that. As the project started to get more and more feature, the backend service became crowded. Managing other entities, communicating with the database, and the Word generator part just looked like it would stand on its own. It only needs one data object, to know what to generate. Feels like a separate service.
We created a new service to create the Word files, and connected it to our original backend service via RabbitMQ. The service is designed to be modular, including a web application, a core service, and a project with a specific implementation. Ideally, the implementation can be switched without bigger issues, containing independent solutions.
Two message queues were integrated between our backend and Word generator service as the communication channel. One queue is for sending the data that needs to be in the Word document, the other channel is to signal back to the backend service, that the requested document is completed, or something went wrong during the process.
Every message has a unique identifier, so when a generation response arrives in the response queue, the backend service is able to check, which document belongs to which request.
There is a minor two-way HTTP communication between the Web API and the Word Generator. It was necessary because both the assets of the document and the generated word file were too big to send them in the message queue. Hence, the Word Generator downloads the assets in the generation process from the backend service via a RESTful endpoint, and the main application downloads the result the same way from the Word Generator in the end of the generation process.
how to build a framework
As mentioned before, the main plan is to prepare the Word Generator service to be a bit more modular. The project has a Core service, which has all the tools to create any word document. There is another separate service, containing the business logic to create a proposal for us.
We would like to make an architecture, where switching services is easy and lightweight while it has no direct dependencies on the main project. Currently this functionality is handled with configuration options, which tells the main application which assembly will contain the final implementation. We plan to improve this concept, and make it more robust.
As our first iteration comes to its end, we have gained useful and important insights. Our services integrate properly and the communication works as intended. The next step could be to containerize our backend services along with the RabbitMQ and database solution, so the future deployments can be done more smoothly.
backend, frontend and microservice
#Entity Framework Core