In the previous step, we found which calls to the legacy calculator actually run. Before we switch these calls to the new calculator, we want evidence that the new calculator returns the same results. In this step, we call both calculators for a sample of the calls, log the differences, and return the result of the legacy calculator. The behavior of the program does not change.
The new calculator
The new calculator has a different design: it is a static class. It also supports a new region, CH, with a different tax rate.
1public static class NewTaxCalculator
2{
3 public static decimal ComputeTax( decimal amount, string region )
4 => region switch
5 {
6 "EU" => amount * 0.2m,
7 "CH" => amount * 0.081m,
8 _ => amount * 0.1m
9 };
10}
11
Comparing the results
The TaxComparison class has a static method with the same parameters as LegacyTaxCalculator.ComputeTax. It also receives the calculator and the location of the call site:
15 public static decimal ComputeTax(
16 LegacyTaxCalculator calculator,
17 decimal amount,
18 string region,
19 [CallerMemberName] string callerMember = "",
20 [CallerLineNumber] int callerLine = 0 )
21 {
22 var legacyResult = calculator.ComputeTax( amount, region );
23
24 if ( Interlocked.Increment( ref _callCount ) % _samplingInterval != 0 )
25 {
26 return legacyResult;
27 }
28
29 var location = $"{callerMember}, line {callerLine}";
30
31 try
32 {
33 var newResult = NewTaxCalculator.ComputeTax( amount, region );
34
35 if ( newResult != legacyResult )
36 {
37 Console.WriteLine(
38 $" Difference at {location} for {amount:F2} in {region}: legacy {legacyResult:F2}, new {newResult:F2}." );
39 }
40 }
41 catch ( Exception e )
42 {
43 Console.WriteLine( $" The new calculator failed at {location} for {amount:F2} in {region}: {e.Message}" );
44 }
45
46 return legacyResult;
47 }The method always calls the legacy calculator and returns its result. Only one call out of three also calls the new calculator. This sampling limits the cost of the comparison when the component is called often. When the results differ, or when the new calculator throws an exception, the method logs the difference with the location of the call site. An exception in the new calculator does not affect the caller.
As in the previous step, the class has the ExcludeInterceptorsAttribute attribute, so the call to the legacy calculator in the interceptor is not intercepted.
Redirecting the calls
The project fabric is the same as in the previous step, except that it gives the TaxComparison.ComputeTax method:
1using Metalama.Extensions.Interceptors;
2using Metalama.Framework.Code;
3using Metalama.Framework.Fabrics;
4
5internal class Fabric : ProjectFabric
6{
7 public override void AmendProject( IProjectAmender amender )
8 {
9 var interceptor = typeof(TaxComparison).AsINamedType()
10 .Methods.OfName( nameof(TaxComparison.ComputeTax) )
11 .Single();
12
13 amender.InterceptMethods(
14 calls => calls
15 .Type( typeof(LegacyTaxCalculator) )
16 .Methods( nameof(LegacyTaxCalculator.ComputeTax) )
17 .RedirectToExistingMethod( interceptor ) );
18 }
19}
20
Result
The program produces the following output:
Invoice total in EU: 120.00
Shipping cost in EU: 12.00
Invoice total in US: 110.00
Shipping cost in US: 11.00
Invoice total in CH: 110.00
Difference at ComputeShippingCost, line 11 for 10.00 in CH: legacy 1.00, new 0.81.
Shipping cost in CH: 11.00
Annual tax in EU: 900.00
The results of the program are the same as with the legacy calculator. The log shows that the calculators differ for the CH region. You can now decide whether the difference is a defect of the new calculator or a correction of the legacy one.