Home > Enterprise >  Refactoring classes into multi-layered generic classes in C#
Refactoring classes into multi-layered generic classes in C#

Time:05-18

I have a problem with C# generics, and I'm not sure about the most elegant solution. I've been programming a while but am new to the C# ecosystem so don't know common terminology for searching.

I'm trying to refactor code to reduce existing copy-paste duplication of classes. It is easy to resolve with one level of generics, but I can't get it working with two.

A very simplified example is below. The core issue is that BaseProfile cannot use any implementation details relating to DetailsA or DetailsB as it does not know the type. So UpdateDetailsId() has to be duplicated in 2 derived classes, instead of having a single Profile class handle it. Keep in mind this is a toy example just to express the relationships. The real classes have tens of fields, but a common subset which we are using in the class in question, so even if DetailsA and DetailsB look identical assume we need both.

public abstract class BaseProfile<TypeOfPerson>
{
    public TypeOfPerson Person { get; set; }
}

public class Profile1 : BaseProfile<PersonA>
{
    public void UpdateDetailsId(int id)
    {
        this.Person.Details.Id = id;
    }
}

public class Profile2 : BaseProfile<PersonB>
{
    public void UpdateDetailsId(int id)
    {
        this.Person.Details.Id = id;
    }
}

public class PersonA
{
    public DetailsA Details { get; set; }
}

public class PersonB
{
    public DetailsB Details { get; set; }
}

public class DetailsA
{
    public int Id { get; set; }
}

public class DetailsB
{
    public int Id { get; set; }
}

I can add interfaces as it is referring to all the same fields for each type. However, C# will not allow an interface to include another interface and automatically resolve it in the implementation, because the member has to exactly match i.e. I thought I could just add IDetails Details to the IPerson interface but the fields now need to be type IDetails instead of DetailsA which implements IDetails. If I do that then I lose compiler type safety and can put the wrong Details on the wrong Person.

I have had success doing a public/private field pair like below, but this only validates and throws at runtime when casting value to DetailsA. I'd prefer something safer but I don't know if this is the best option. The goal of this example is a single Profile class, handling multiple Person classes, each with their own Details type that has an int Id field.

public class PersonA : IPerson
{

    public IDetails Details
    {
        get { return _details; }
        set { _details = (DetailsA)value; }
    }
    private DetailsA _details { get; set; }
}

CodePudding user response:

One way of achieving this is by defining the type relationship between PersonA to DetailsA in a generic way, and specify a second generic type on BaseProfile.

Profile1 : BaseProfile<PersonA, DetailsA>

Consider the following code (note that I'm using Net6, so I have all these nullable reference type operators):

public abstract class BaseProfile<TPerson, TDetails>
    where TDetails : IDetails, new()
    where TPerson : PersonDetails<TDetails>, new()
{
    public TPerson? Person { get; set; } = new TPerson();

    public virtual void UpdateDetailsId(int id)
    {
        Person!.Details!.Id = id;
    }
}

public class Profile1 : BaseProfile<PersonA, DetailsA>
{    
}

public class Profile2 : BaseProfile<PersonB, DetailsB>
{    
}

public abstract class PersonDetails<TDetails>
    where TDetails : IDetails, new()
{
    public virtual TDetails? Details { get; set; } = new TDetails();
}

public class PersonA : PersonDetails<DetailsA> 
{ 
}

public class PersonB : PersonDetails<DetailsB> 
{ 
}

public interface IDetails
{
    int Id { get; set; }
}

public class DetailsA : IDetails
{
    public int Id { get; set; }
    public string? FirstName { get; set; }
}

public class DetailsB : IDetails
{
    public int Id { get; set; }
    public string? LastName { get; set; }
}

Testing with the following snippet

var profile1 = new Profile1();
var profile2 = new Profile2();

profile1.UpdateDetailsId(10);
profile2.UpdateDetailsId(12);

Console.WriteLine(profile1.Person!.Details!.Id);
Console.WriteLine(profile2.Person!.Details!.Id);

Console.WriteLine();

Update:

Because you included explicit casting in your snippet for Details property getters and setter, I also want to show a pattern using a concrete type inheriting on these generic types -- then demonstrate implicit/explicit operator user-defined conversion patterns.

Add the following declarations:


public abstract class BaseProfile<TPerson>
    where TPerson : PersonDetails<GenericDetails>, new()
{
    public TPerson? Person { get; set; } = new TPerson();

    public virtual void UpdateDetailsId(int id)
    {
        Person!.Details!.Id = id;
    }
    
    public static explicit operator Profile1(BaseProfile<TPerson> details)
    {
        var profile = new Profile1();
        profile.Person!.Details = (GenericDetails)details.Person!.Details!;
        return profile;
    }

    public static explicit operator Profile2(BaseProfile<TPerson> details)
    {
        var profile = new Profile2();
        profile.Person!.Details = (GenericDetails)details.Person!.Details!;
        return profile;
    }
}

public class GenericProfile : BaseProfile<GenericPerson>
{   
}

public abstract class GenericPersonDetails : PersonDetails<GenericDetails> 
{ 
}

public class GenericPerson : GenericPersonDetails 
{ 
}

public class GenericDetails : IDetails
{
    public int Id { get; set; }

    public static implicit operator DetailsA(GenericDetails details)
    {
        return new DetailsA() { Id = details.Id };
    }

    public static implicit operator DetailsB(GenericDetails details)
    {
        return new DetailsB() { Id = details.Id };
    }
}

and, update the testing functional scope:


var profile1 = new Profile1();
var profile2 = new Profile2();
var genericProfile = new GenericProfile();

profile1.UpdateDetailsId(10);
profile2.UpdateDetailsId(12);
genericProfile.UpdateDetailsId(20);

Console.WriteLine(profile1.Person!.Details!.Id);
Console.WriteLine(profile1.Person!.Details!.FirstName ?? "No First Name");

Console.WriteLine(profile2.Person!.Details!.Id);
Console.WriteLine(profile2.Person!.Details!.LastName ?? "No Last Name");

Console.WriteLine(genericProfile.Person!.Details!.Id);

Console.WriteLine(((Profile1)genericProfile).Person!.Details!.FirstName ?? "No First Name");
Console.WriteLine(((Profile2)genericProfile).Person!.Details!.LastName ?? "No Last Name");


Console.WriteLine();
  • Related