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.
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.
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.
Using File Structure from ReSharper here is what is coded in Program.cs
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; }
}
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; }
}
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; }
}
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; }
}
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}";
}
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
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
}
}
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();
}
}
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();
}
In the code above there are validation issues so our code is working as expected.
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.
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.
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
- 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
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)
Hi Karen Payne,
Top, very nice and helpful !
Thanks for sharing.
Your welcome.
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.