Integrate Generated Code With Hand-Written Features
If we build code generators for a standardised solution, we will sooner or later need something special that does not fit into the generator. When that happens, we do not need to forfeit the benefits of our automation approach. Instead, we can build on top with hand-written code. Let us explore how we can do this safely.
The reason for partial classes and interfaces
A few weeks ago, I wrote that we always should generate partial classes. Thay may have looked like a useless overhead, but now we can use that extra keyword to our advantage.
The benefit that partial classes offer us is that we can split a class definition over multiple files. We have our generated *.g.cs file and can now add a hand-written *.cs file for the same class next to the generated code.
What works with partial classes does also works with partial interfaces. Then when we define the methods in the interface, we need a way to separate the hand-written methods from the generated ones - not only in the class itself, but also in the interface. Then as with the generated classes, the generated interfaces will also be overwritten when we run the generator again.
If we want to add hand-written code to a generated class, we create a new partial class and most often a partial interface.
Extend our Customer table
To get some real-world use case, we need a bit more data inside our Customer table. We can add these new columns:
usingSystem;usingGenerateCodeFromDb.FromDb.Entities;namespaceGenerateCodeFromDb.TestDataGenerators;publicclassCustomerGenerator{publicstaticCustomerGetTestData(){returnnewCustomer(){LastName="Example",FirstName="Joe",CreatedOn=newDateTime(2024,4,1,12,30,45),Email="[email protected]",IsActive=true,StreetAndNumber="One Microsoft Way",City="Redmond",ZipCode="98052",State="WA",CountryCode="US"};}publicstaticCustomerGetDataForUpdate(){returnnewCustomer(){LastName="Updated",FirstName="Max",CreatedOn=newDateTime(2022,1,2,3,4,5),Email="[email protected]",IsActive=false,StreetAndNumber="One Apple Park Way",City="Cupertino",ZipCode="95014",State="CA",CountryCode="UK"};}}
Extend the Customer class
It would be great if we could add a method to the Customer class that gives us back a formatted address string. We cannot add this method to the generated code, then that would be gone if we regenerate the entity. Instead, we create a Customer.cs file next to the Customer.g.cs and add the method there:
usingSystem;usingFluentAssertions;usingNUnit.Framework;usingGenerateCodeFromDb.FromDb.Entities;namespaceGenerateCodeFromDb.FromDb.UnitTests{[TestFixture]publicpartialclassCustomerTests{[Test]publicvoidGetAddress_formats_US_address(){varcustomer=newCustomer{LastName="Example",FirstName="Joe",CreatedOn=newDateTime(2024,4,1,12,30,45),Email="[email protected]",IsActive=true,StreetAndNumber="One Microsoft Way",City="Redmond",ZipCode="98052",State="WA",CountryCode="US"};varformattedAddress=customer.GetAddress();formattedAddress.Should().Be("Joe Example"+"\nOne Microsoft Way"+"\nRedmond, WA 98052"+"\nUnited States");}}}
With that we can prevent bugs from showing up when we change the generator or rename the fields.
Extend the repository
If we want to search Customers by a country code, we can create a method in the repository. But as with the Customer.g.cs class, we cannot put it into the generated code. Instead, we add it to a new file CustomerRepository.cs next to CustomerRepository.g.cs:
usingDapper;usingSystem.Collections.Generic;usingSystem.Linq;usingGenerateCodeFromDb.FromDb.Entities;namespaceGenerateCodeFromDb.FromDb.Repositories{publicpartialclassCustomerRepository{publicList<Customer>FindByCountryCode(stringcountryCode){varsql=@" SELECT [Id] ,[LastName] ,[FirstName] ,[Email] ,[IsActive] ,[CreatedOn] ,[StreetAndNumber] ,[ZipCode] ,[City] ,[State] ,[CountryCode] FROM dbo.Customer WHERE CountryCode = @countryCode ";returnthis.connection.Query<Customer>(sql,new{countryCode}).ToList();}}}
Our generated repositories have interfaces, so we need to create a partial interface for our new method and name that new file ICustomerRepository.cs and put it next to ICustomerRepository.g.cs:
Our partial integration test class can reuse the setup code from the generated integration test file and does not need its own setup code.
If you dislike the long SQL statement in the hand-written method, you can always create a variable with the statement until the WHERE clause. You then can access that generated part from the hand-written method and save yourself the repetitive work of reproducing the SQL query.
Run the tests
Make sure to run the tests, then otherwise we have a test but still may not notice a bug:
Next
We now have additional methods available to us that build on top of our automation work that will not get deleted when we rerun our code generators. The tests help us to show how to use the new methods and they will prevent us from breaking the code with future changes.