mitesh1612 / blog

My personal developer blog made using Fastpages.
Apache License 2.0
0 stars 1 forks source link

How to Create a Fluent API in C# | Mitesh Shah’s Blog #6

Open utterances-bot opened 2 years ago

utterances-bot commented 2 years ago

How to Create a Fluent API in C# | Mitesh Shah’s Blog

A introductory post talking about fluent apis, its pros and cons and how to design one for yourself from scratch.

https://mitesh1612.github.io/blog/2021/08/11/how-to-design-fluent-api

palaplan commented 2 years ago

Very nicely written article. You explain how to implement a fluent interface but also clearly state the pros and cons and the (toy) problem has a very good complexity level for such an article, I like it a lot! Your diagrams look great, how did you create them? I am also becoming a fan of fluent assertions, especially for my unit tests, it is a nice and very readable way of initializing, running and asserting tests.

mitesh1612 commented 2 years ago

Thank you so much for your wonderful feedback! 😊 I created my diagrams using Excalidraw which is a free web based tool to create diagrams that look hand drawn. Yeah, I have been using MSTest Assertions earlier, but slowly incorporating Fluent Assertions, its a wonderful library!

lneshev commented 2 years ago

Thank you for this good article! I'd like to ask you how the code can be changed in a way that it forces to call at least one of "ICanSetContainerProperties" methods, before calling the method that implements "ICanAddRunCommand"? Now it is possible to do something like:

FluentDockerfileGenerator
                .CreateBuilder()
                .FromImage("node:12")
                .WithCommand("npm start");

I'd like to achieve something like:

FluentDockerfileGenerator
                .CreateBuilder()
                .FromImage("node:12")
                .CopyFiles("Package*.json", "./")
                .WithCommand("npm start");

or:

FluentDockerfileGenerator
                .CreateBuilder()
                .FromImage("node:12")
                .RunCommand("npm install")
                .WithCommand("npm start");

And btw, you have to update the diagrams to show that this is currently possible by adding an arrow pointing from "CanSetBaseImage" (Stage 1) to "CanAddRunCommand" (Stage 3).

mitesh1612 commented 2 years ago

@lneshev In the current implementation, the ICanSetContainerProperties inherits ICanAddRunCommand which is causing this. If we remove that, then essentially you will need to fix the return types of ICanSetContainerProperties's methods to essentially allow returning a type that can be either ICanSetContainerProperties or ICanAddRunCommand (and the only way I knew how to do it was using inheritance, there might be other ways). I can think about this a bit more and come up with a solution, but my hunch is that adding a couple more interfaces might help with this and other logical issues (like you can call CopyFiles n number of times or so). Reducing the number of interfaces was done to keep the example simple, but this seems doable!

Let me think about how to achieve this and come back with a solution. 😄

m31coding commented 2 months ago

Hi @mitesh1612,

Thank you very much for this article; it's a great write-up and your diagrams are extraordinary!

I have created a code generator for creating Fluent APIs you may want to have a look at. Your example API can be generated in the following way:

using System.Text;
using M31.FluentApi.Attributes;

namespace ExampleProject;

[FluentApi]
public class DockerFile
{
    private StringBuilder _dockerFileContent;

    private DockerFile()
    {
        _dockerFileContent = new StringBuilder();
    }

    [FluentMethod(0)]
    private void FromImage(string imageName)
    {
        this._dockerFileContent.AppendLine($"FROM {imageName}");
    }

    [FluentMethod(1)]
    [FluentContinueWith(1)]
    private void CopyFiles(string source, string destination)
    {
        this._dockerFileContent.AppendLine($"COPY {source} {destination}");
    }

    [FluentMethod(1)]
    [FluentContinueWith(1)]
    private void RunCommand(string command)
    {
        this._dockerFileContent.AppendLine($"RUN {command}");
    }

    [FluentMethod(1)]
    [FluentContinueWith(1)]
    private void ExposePort(int port)
    {
        this._dockerFileContent.AppendLine($"EXPOSE {port}");
    }

    [FluentMethod(1)]
    [FluentContinueWith(1)]
    private void WithEnvironmentVariable(string variableName, string variableValue)
    {
        this._dockerFileContent.AppendLine($"ENV {variableName}={variableValue}");
    }

    [FluentMethod(1)]
    private void WithCommand(string command)
    {
        var commandList = command.Split(" ");
        this._dockerFileContent.Append("CMD [ ");
        foreach (var c in commandList)
        {
            this._dockerFileContent.Append($"\"{c}\", ");
        }

        this._dockerFileContent.Remove(this._dockerFileContent.Length - 2, 1);
        this._dockerFileContent.Append("]");
    }

    public override string ToString()
    {
        return this._dockerFileContent.ToString();
    }
}

Code is generated in the background and you can immediately use the API like this:

string dockerFile = CreateDockerFile
    .FromImage("node:12")
    .CopyFiles("package*.json", "./")
    .RunCommand("npm install")
    .WithEnvironmentVariable("PORT", "8080")
    .ExposePort(8080)
    .WithCommand("npm start")
    .ToString();

While the code above with the attributes is only around 30 lines shorter than your explicit implementation, the code generator can be particularly convenient for APIs with a larger number of methods. Additionally, using the code generator eliminates the need to manually define interfaces.

Happy coding!, Kevin

mitesh1612 commented 2 months ago

Hi Kevin/ @m31coding ,

This is actually pretty cool, and I was searching or planning to implement something similar. You are correct, it would be great in cases where we dont want to self-implement a lot of interfaces and have a lot of methods. I'll read your source code as well, it seems really interesting! :)