Build server – client contracts with dotnet CLI

Now days type safety is common within the web-world with TypeScript and on the horizon WebAssembly with .NET Core (Blazor etc). I have for a long time advocated for the importance of this, especially when we are talking the contract between server and client. For example this T4 template that spits out C# CQS types as javascript.

T4 doesn’t play well with Dot Net Core, but we now have the dotnet CLI toolset we can use instead.

First we need to create the tool project, its just a standard .NET Core console project, but the output must be prefixed with dotnet- by convention for the dotnet CLI to understand what todo with it. Edit the .csproj and add <PackageType>DotnetCliTool</PackageType>. This will tell the host project that the included program is a CLI toolset. Now you build and package the console project. Configure a local nuget repo for testing.

Now edit your host project .csproj add

<DotNetCliToolReference Include="dotnet.my-cli" Version="x.x.x" />

Where dotnet.my-cli is your CLI package ID. This will only include the CLI project. To execute we need to add a build target. For example.

  <Target Name="MyCliTarget" AfterTargets="Build">
    <Exec Command="dotnet mycli arg1 arg2" />
  </Target>

Notice that we have removed dotnet- prefix. This will call your tool when you build the project and with two arguments. You can use standard msbuild variables here such as $(OutputPath). Thats it, now your program will be called and the maqic can begin.

Building the contracts CLI program

The target for our CLI could look like this

  <Target Name="MyCliTarget" AfterTargets="Build">
    <Exec Command="dotnet cqsgen $(OutputPath)Core.Contracts.dll $(MSBuildProjectDirectory)\wwwroot\js\cqs.contracts.js Core.Contracts.Commands.Command;Core.Contracts.Queries.Query" />
  </Target>

Msbuild paths are relative from the project root. Lucky for us Directory.GetCurrentDirectory() points to this directory. So to get a project resource we can do $”{Directory.GetCurrentDirectory()}\\{args[0]}”;. Full code of our Program.cs

    class Program
    {
        static void Main(string[] args)
        {
            var ignoreBaseClassProperties = args.Any(a => a.ToLower() == "ignore-properties-on-base");

            Console.WriteLine("Starting parsing cqs contracts!");
            var assemblyPath = $"{Directory.GetCurrentDirectory()}\\{args[0]}";
            var outputPath = args[1];

            var parser = new Parser(args[2].Split(";"), assemblyPath, ignoreBaseClassProperties);
            var result = parser.Parse();
            
            Console.WriteLine($"Parsing complete, saving: {outputPath}");
            File.WriteAllText(outputPath, result);
        }
    }

We basically parse the args and pass them to the Parser class. I’m not going into full detail about the parsing, but it will output a javascript like this.

(function() {
   window.Core = (window.Core || {});
   window.Core.Contracts = (window.Core.Contracts || {});
   window.Core.Contracts.Queries = (window.Core.Contracts.Queries || {});
   window.Core.Contracts.Queries.Payments = (window.Core.Contracts.Queries.Payments || {});
   window.Core.Contracts.Rest = (window.Core.Contracts.Rest || {});

   Core.Contracts.Rest.PaymentStatus = {
      WaitingApproval: 0, 
      Approved: 1, 
      Cancelled: 3, 
      Error: 4
   };
   Contracts.Queries.Payments.PaymentsQuery=function(status) {
      this.status = status;
   };
   Core.Contracts.Queries.Payments.PaymentsQuery.constructor.type="Core.Contracts.Queries.Payments.PaymentsQuery";
})();

It makes sure no closures are overwritten. Also this way we can benefit from full Visual Studio code completion. It also adds the type name to the constructor. This enabled us to lookup the C# type in runtime. For example like.

MyApp.cqs = {
    sendQuery: function (query) {
        return $.getJSON(url + toQueryString(query));
    },
    sendCommand: function (command) {
        $.post(url, { type: command.constructor.type, data: command});
    }
};

MyApp.cqs.sendQuery(new Core.Contracts.Queries.Payments.PaymentsQuery(Core.Contracts.Rest.PaymentStatus.Approved)); 

Full code can be found here!

Advertisements

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s