Using Microsoft CoPilot to Create API Contracts from Other Smaller Contracts

“I choose a lazy person to do a hard job. Because a lazy person will find an easy way to do it.”
― Bill Gates

Problem

We have an established API with a lot of existing functionality. Part of that functionality was the ability to build up requests that could be posted to external APIs for further processing. Data collection was done via a wizard-like browser application that walked the user through all required sections and submitted appropriate data at different times.

The client requested the ability to supply all required data via a single entry point for use outside the existing browser application. For that, we had to expose an API Contract (ContractA) that needed to be transformed into numerous other complex contracts (ContractsX) on the server. The ContractsX shared some properties with one another and used common contracts for some properties.

When looking at the combined object of all known ContractsX, we ended with an object that contained over 9400 properties. It contained nested objects and arrays of all sorts of combinations.

Approach

We knew that a human would have a hard time analyzing all those properties without mistakes. Instead of using brute force, we wanted to try something different. This solution required several tools to get the job done, but straightforward steps once you had a plan.

In addition, we needed an automated way to verify that what we did worked now and will work in the future.

Tools Used

Note: To make our approach a little easier, we ensured that all our contracts in question used common contracts under the hood.

Solution

Step 1: Create valid data models of your contracts.

Generate a valid data example of each contract that you want to support. Microsoft CoPilot and Bogus made this task rather simple.

For example, Person has over 30 properties, and some complex objects like Address.

To use CoPilot for the next step, make sure the file is actually open (i.e., not Preview), highlight the class name, and then ask CoPilot: “Generate the full Bogus implementation for this class and use Faker<>, assume all other classes will have fakers too”.

It’s possible you will have to prompt CoPilot to give you a better answer. It may only partially implement the faker or use a different notation. If it doesn’t give you the correct answer right away, try to tell it what you want. Once it gets the correct answer, it will have learned what you want, and the rest of the process should be pretty easy.

You should end up with something like this:

Save that snippet as a new file.

Note that it included other Fakers for properties with complex objects like AddressFaker. You will have to generate them the same way and do this for each contract you are using.

At this point, you can effectively rely on CoPilot to give you what you want. All you have to do is open file -> highlight class name -> ask CoPilot -> copy -> repeat.

At the time of writing this, we have not been able to make CoPilot generate the faker for the contract and all dependencies at the same time. If you found a way to do that, please leave it in the comments section.

Step 2: Build the worst-case scenario data contract

I recommend keeping this function located somewhere handy. Unit tests felt like a good place to start.

All you need to do is use the Faker classes you have created in step 1, then use Newtonsoft’s JsonConvert.SerializeObject and combine the generated JSON.

The beauty of this approach is that you can easily extend this worst-case scenario JSON object when you need to add another supported contract.

Step 3: Shrink the Contract (Remove duplicates)

Take the generated JSON from step 2 and paste it here: https://json2csharp.com/. Then, let it convert into C# (or any other option you like).

The result is a very simple-to-read class definition. Because we are using common contracts in our situation, the class names match and have automatically been reduced to a unique subset. If you are in the same boat, you can use the “Root” object only because all other classes should be the common contracts from your solution. We did find that not all property names and classes match perfectly with the common contracts from the solution, so a small amount of class name cleanup may be needed.

If you are in a different situation, you will need to take all generated classes, compare them to existing classes in your solution, and match them up as well as you can. Since the class names should reflect your original property names, it should be relatively easy to find matches. We have done some of this because we wanted to find out how close this contract was to another contract we saved in the application.

You will likely be in a different situation, so the steps below will be different. But there may be some useful first steps, so I want to share the list anyway.

  • Repeat all the previous steps for the other known contracts, and get the class list (current step 3)
  • Reduce the class list to only name (CoPilot desktop is good at this)
  • Sort that reduced list
  • Use https://www.diffchecker.com/text-compare/ to compare those lists
    • If your class names are very different, you may have to use the full generated list of classes and properties, sort it alphabetically, and then find the differences.
    • CoPilot can also help analyze the commonalities and differences of your classes.

Build a valid data model for the Generated Root

It’s probably a good idea to name the Root something more sensible, maybe CombinedDataModel. Now follow steps 1 and 2 to generate a new valid JSON Object for this contract.

Check if you can generate all ContractsX (Putting it all together)

All we do is Serialize the object into JSON with JsonConvert and then DeserializeObject <> that JSON into all the contracts we want to support. There are certainly other ways to do this, like AutoMapper, but we found that JSON is the most flexible option for us.

Validate

At a bare minimum, we wanted to know if we had built an object to handle the worst-case scenario. I asked CoPilot to generate a function that can validate that all properties of an object are filled, including nested objects and classes:

You can be confident that if all properties in your ContractsX are filled, you at least have all properties mapped/supplied. This does not mean that they all have the correct data, but this is a business-specific problem you must test with other approaches.

Expandability

One of my favorite results of the above approach is that it’s super easy to add and verify new fields or contracts.

For example:

  1. Add any new property to your CombinedDataModel and run the test. It should fail, which makes it pretty easy for you to find the missing property.
  2. Are you wondering if you can also generate a different contract? Update the test and JsonConvert.DeserializeObject<> that contract, and debug the results.

At this step, running through the initial steps 1-5 is probably unnecessary; you can expand your model via the unit test approach.


Related posts