DEV Community

Cover image for C# different way to do a proof of concept
Karen Payne
Karen Payne

Posted on

C# different way to do a proof of concept

Preface

The reader should have a basic understanding of working with classes, interfaces and generics.

Introduction

This is unorthodox style of writing code that may not be for everyone. The reasoning is to hash out an idea that may or may not pan out which may be started out as an experiment that gets trashed or turn into an actual application.

If the experiment is trashed than less of an effort is required while on the other hand utilizing Microsoft Visual Studio features to assist with refactoring can make the final refactoring easy than conventional thinking of recreating code in another project rather than simply moving code manually.

What can assist in refactoring code over what is provided by Microsoft Visual Studio is using Jetbrains ReSharper which has many features to assist with refactoring code. Jetbrains offers a 30-day free trial and the cost of ReSharper can pay for itself in some cases in hours.

Mentions the union of Visual Studio and ReSharper

Focus

Learn how to write code that resides in a console project Program.cs that will then be refactored into separate classes in the project followed by refactoring code so that base code will be refactored into a class project.

When using this technique, each step consider, is the code heading in the right direction and if not consider aborting or to move forward.

Never feel bad to trash code that is leading no where

Objective

Using FluentValidation NuGet package, create generic validation against an interface rather than a class along with some tips on setting up validators that are reusable for different classes.

Proof of concept

Since the code is rather large, its best to point to the code in a repository.

Open the code in another window and follow along.

Console project

Using File Structure from ReSharper here is what is coded in Program.cs

Program.cs code

Stepping through first code run

Start out with a Person class.

public class Person
{
    public int Id { get; set; }
    public string FirstName { get; set; }
    public string LastName { get; set; }
    public DateOnly BirthDate { get; set; }
    public Address Address { get; set; }
}

public class Address
{
    public string Line1 { get; set; }
    public string Line2 { get; set; }
    public string Town { get; set; }
    public string Country { get; set; }
    public string Postcode { get; set; }
}
Enter fullscreen mode Exit fullscreen mode

Do not get hung up on Line1 and Line2 names, they can change down the road or use data annotations to give names for web projects. Below shows using data annotations but lets keep to the version above as some project types may not support data annotations and renaming those properties may be a better option.

public class Address
{
    [Display(Name = "Street")]
    public string Line1 { get; set; }
    [Display(Name = "Addition street information")]
    public string Line2 { get; set; }
    public string Town { get; set; }
    public string Country { get; set; }
    public string Postcode { get; set; }
}
Enter fullscreen mode Exit fullscreen mode

What about the need to have other classes using the same properties? Here is where an interface is a must.

public interface IPerson
{
    int Id { get; set; }
    string FirstName { get; set; }
    string LastName { get; set; }
    DateOnly BirthDate { get; set; }
    Address Address { get; set; }
}
Enter fullscreen mode Exit fullscreen mode

Now by using an interface code is cleaner, repeatable and can by reference by the interface for common properties.

public class Person : IPerson
{
    public int Id { get; set; }
    public string FirstName { get; set; }
    public string LastName { get; set; }
    public DateOnly BirthDate { get; set; }
    public Address Address { get; set; }
}

public class Citizen : IPerson
{
    public int Id { get; set; }
    public string FirstName { get; set; }
    public string LastName { get; set; }
    public DateOnly BirthDate { get; set; }
    public DateOnly Since { get; set; }
    public Address Address { get; set; }

}
Enter fullscreen mode Exit fullscreen mode

Next we decide that there should be a override for ToString. This could happen now or later. Some developers might change from an interface to an abstract class. Here a simple trick is used, implement IFormattable.

public interface IPerson  : IFormattable
{
    int Id { get; set; }
    string FirstName { get; set; }
    string LastName { get; set; }
    DateOnly BirthDate { get; set; }
    Address Address { get; set; }
}

public class Person : IPerson
{
    public int Id { get; set; }
    public string FirstName { get; set; }
    public string LastName { get; set; }
    public DateOnly BirthDate { get; set; }
    public Address Address { get; set; }
    string IFormattable.ToString(string format, IFormatProvider formatProvider) 
        => $"{FirstName} {LastName}";
}

public class Citizen : IPerson
{
    public int Id { get; set; }
    public string FirstName { get; set; }
    public string LastName { get; set; }
    public DateOnly BirthDate { get; set; }
    public DateOnly Since { get; set; }
    public Address Address { get; set; }
    string IFormattable.ToString(string format, IFormatProvider formatProvider)
        => $"{FirstName} {LastName}";
}
Enter fullscreen mode Exit fullscreen mode

So how did the ToString get figured out? This can be done by simply knowing how to code it or using A.I. such as Copilot or Gemini.

Lets look at Gemini.

The prompt where pound symbols are used to tell Gemini what to use as a resource. On the other hand if the developer has a Copilot subscription and the file with IPerson is open (can not be closed), Copilot will use the interface to create a response.

###
public interface IPerson  : IFormattable
{
    int Id { get; set; }
    string FirstName { get; set; }
    string LastName { get; set; }
    DateOnly BirthDate { get; set; }
    Address Address { get; set; }
}
###

Create a Person class that implements IPerson 
Enter fullscreen mode Exit fullscreen mode
public class Person : IPerson
{
    public int Id { get; set; }
    public string FirstName { get; set; }
    public string LastName { get; set; }
    public DateOnly BirthDate { get; set; }
    public Address Address { get; set; }

    public string ToString(string format, IFormatProvider formatProvider)
    {
        // Implement your own formatting logic here based on the format string and format provider
        // For example, you could return a formatted string containing the person's full name, birth date, and address.
        return $"{FirstName} {LastName}"; // A simple example
    }
}
Enter fullscreen mode Exit fullscreen mode

Creating validation

Since in Program.cs, we want to create validation for FirstName, LastName, BirthDate and the Address property.

Note that is rather than a single address is required FluentValidation can handle this and is easy to add in later. See Collections. Also, a neat feature of FluentValidation is Include which allows reuse of rules.

  • Classes are setup for each property to validate
  • In PersonValidator is used to validate any class which implements IPerson.
public class PersonValidator : AbstractValidator<IPerson>
{
    public PersonValidator()
    {
        Include(new FirstNameValidator());
        Include(new LastNameValidator());
        Include(new BirthDateValidator());
        Include(new AddressValidator());
    }
}

public class FirstNameValidator : AbstractValidator<IPerson>
{
    public FirstNameValidator()
    {
        RuleFor(person => person.FirstName)
            .NotEmpty()
            .MinimumLength(3);
    }
}

public class LastNameValidator : AbstractValidator<IPerson>
{
    public LastNameValidator()
    {
        RuleFor(person => person.LastName)
            .NotEmpty()
            .MinimumLength(3);
    }
}

public class BirthDateValidator : AbstractValidator<IPerson>
{

    public BirthDateValidator()
    {

        var value = JsonRoot().GetSection(nameof(ValidationSettings)).Get<ValidationSettings>().MinYear;
        var minYear = DateTime.Now.AddYears(value).Year;

        RuleFor(x => x.BirthDate)
            .Must(x => x.Year > minYear && x.Year <= DateTime.Now.Year)
            .WithMessage($"Birth date must be greater than {minYear} " +
                         $"year and less than or equal to {DateTime.Now.Year} ");
    }
}

public class AddressValidator : AbstractValidator<IPerson>
{
    public AddressValidator()
    {
        RuleFor(item => item.Address.Line1).NotNull()
            .WithName("Street")
            .WithMessage("Please ensure you have entered your {PropertyName}");
        RuleFor(item => item.Address.Town).NotNull();
        RuleFor(item => item.Address.Country).NotNull();
        RuleFor(item => item.Address.Postcode).NotNull();
    }
}
Enter fullscreen mode Exit fullscreen mode

Testing code

  • A list is created of type IPerson, one Person and one Citizen. We could had gone with creating the list with Bogus but in this case is overkill. In some cases with GitHub Copilot subscription Copilot may (and did in this case) offer to create the list. The list Copilot created was good but needed some tweaks for property values.
  • Spectre.Console NuGet package provides the ability to colorize data using AnsiConsole.MarkupLine
static void Main(string[] args)
{

    List<IPerson> people =
    [
        new Person
        {
            Id = 1,
            FirstName = "John",
            LastName = "Doe",
            BirthDate = new DateOnly(1790, 12, 1),
            Address = new Address
            {
                Line1 = "123 Main St",
                Line2 = "Apt 101",
                Town = "Any town",
                Country = "USA",
                Postcode = "12345"
            }
        },

        new Citizen
        {
            Id = 1,
            FirstName = "Anne",
            LastName = "Doe",
            BirthDate = new DateOnly(1969, 1, 11),
            Since = new DateOnly(2020, 1, 1),
            Address = new Address
            {
                Line2 = "Apt 101",
                Town = "Any town",
                Country = "USA"
            }
        }
    ];

    PersonValidator validator = new();

    foreach (var person in people)
    {
        AnsiConsole.MarkupLine($"{person} [cyan]{person.GetType().Name}[/]");

        var result = validator.Validate(person);
        if (result.IsValid)
        {
            Console.WriteLine(true);
        }
        else
        {
            foreach (var error in result.Errors)
            {
                AnsiConsole.MarkupLine($"[red]   {error.ErrorMessage}[/]");
            }
        }
    }

    Console.ReadLine();

}
Enter fullscreen mode Exit fullscreen mode

In the code above there are validation issues so our code is working as expected.

Console output from code above

Refactor code

Note
Source code represents the finished product.

Frontend project code Backend project code

Using the code in Program.cs above, create folders for Classes, Interfaces, Models and Validators.

Revised code

The best path is to open another instance of Visual Studio and if possible in a another monitor.

  • Create a new project
  • Create folders
  • Add required NuGet packages.

The next part is to extract each class, interface and validators from Program.cs.

The hard way, in the original project copy the class (or interface) name and create a new class in the secondary project and copy the code, change the namespace.

The easy way, with ReSharper installed, highlight each class (or interface) and select Move to as shown below. This will place the class or interface into the root folder of the project.

Shows ReSharper Move to option

Next, select the newly created file and drag to the appropriate folder in the secondary project in the other instance of Visual Studio.

Open the file, place the mouse over the namespace and ReSharper will recommend changing the namespace. Another option is to repeat the move to operation and drag operation then select the project name in Visual Studio from ReSharper menu/Refactor select Adjust Namespaces to fix all namespaces at once.

Move code to a class project

Before continuing commit the new project changes to source control e.g. GitHub in the event something goes wrong.

  • Create a new class project, same name as the original project with Library tagged to the project name.

Note
Current project name, UsingIncludeInValidation, class project name UsingIncludeInValidationLibrary. Do not get hung up on the name, this can be changed later.

  • Copy each folder from the console project to the class project
  • Adjust namespace names either manually or using Resharper as mentioned above.
  • In the console project, remove the folders which were copied to the class project.
  • In solution explorer, single click on the class project and drag/release on the console project which adds a project reference.
  • First build the class project followed by the console project, if both are successful run the console project and the same output is expected as in the original project.

See the following for copying code from one instance of Visual Studio to another instance of Visual Studio.

What can go wrong

Comical what can go wrong

  • Classes and interfaces that were possibly scoped as internal need to be scoped to public.
  • If a GlobalUsings.cs was originally used in the original project, copy and paste to the new project then open the file and remove any usings that are not needed.

Next level Unit testing

Unit test

Using your favorite test framework create test to ensure all possible cases are covered.

A recommendation is to use Gherkin to plan out test. Or use GitHub Copilot to create test which can be learned here. With Resharper installed see the following.

One benefit of creating unit test is when a frontend project has issues or errors unit test can assist with figuring out an issue or error. If the test run successfully than jump to frontend code else figure out the backend code by running failed test in debug mode.

Summary

Provided instructions show how to create a proof of concept that can later become an application or part of an application using a class project or trash written code before the code becomes a total reck. As mentioned above, this style is not going to appeal to everyone although some aspects will.

Credits

Article image from www.freshbooks.com

Top comments (3)

Collapse
 
jangelodev profile image
João Angelo

Hi Karen Payne,
Top, very nice and helpful !
Thanks for sharing.

Collapse
 
karenpayneoregon profile image
Karen Payne

Your welcome.

Collapse
 
tbm0115 profile image
Trais McAllister

As alternative to Spectre, I wrote the Consoul library which allows you to manage "Views" and a ton of helper methods specifically for Proof-of-concept console projects.