C# how to "register" class "plug-ins" into a service class? - As of today

Viewed 416

6 Years have passed since this question was made and I was expecting to have an easy solution today.. but seems not.

NOTE: please read the other question to understand the concept:

After a few minutes I tried to implement an easy example and I've almost accomplished it. Meanwhile I still see some problems. And I was wondering if someone has ideas on how to make it better.

Using .NET 6 (code bellow).

  • Issue 1: I don't like the fact that the generics where we say, use TTarget as User, we also need to pass the T ID type.... why by passing User is not enought for the compiler to know the ID data type? Example: class UserService : IBaseDBOperation1<User, Guid> why not class UserService : IBaseDBOperation1<User> ?

  • Issue 2: I understand that now we are allowed to have interfaces with methods with code, but why do we still need to define the variable type exactly with both data types and using var is not enough? Well, we can use var, but then the methods are not visible. instead of: IBaseDBOperation1<User, Guid> serviceU1 = new UserService(); ........ var serviceU2 = new UserService(); ...... this second variable will not see the other methods.

Final note: Everything would be so much easier if C# would allow us to extend a class with more than one other abstract class.... (as of today we are limited to 1).

Objective: Accomplish what was asked in the question made 6 years ago.... in other words.... avoid copy/paste, and somehow "inject/associate/register/define" more than one "operation class" into a service.... those "operation classes" will be reused a lot in multiple different services.... and I do want to have a "clean/pretty" way of setting this up, but at the same time, the consumer should not worry about "lower/deeper "lever inheritance generics.

Code

public abstract class BaseDBEntry<T> where T : struct
{
    protected BaseDBEntry()
    {
        CreatedOn = DateTime.Now;
    }

    public T Id { get; set; }
    public DateTime CreatedOn { get; set; }
    public DateTime? DeletedOn { get; set; }
}

public class User : BaseDBEntry<Guid>
{
    public User() { Id = Guid.NewGuid(); }
    public string Name { get; set; }
}

public class Color : BaseDBEntry<long>
{
    public Color() { Id = DateTime.Now.Ticks; }
    public string RGB { get; set; }
}

Services

public interface IBaseDBOperation1<in TTarget, out T> 
                 where TTarget : BaseDBEntry<T> where T : struct
{
    public bool IsValid(TTarget model) { return true; }

    T GiveMeId(TTarget model) { return model.Id; }
}

public interface IBaseDBOperation2<in TTarget, T> 
                 where TTarget : BaseDBEntry<T> where T : struct
{
    public bool IsValidToDoSomethingElse(TTarget model) { return false; }
}

public class UserService : IBaseDBOperation1<User, Guid>, IBaseDBOperation2<User, Guid> { }

public class ColorService : IBaseDBOperation1<Color, long>, IBaseDBOperation2<Color, long> { }

Consumer

public class Consumer
{
    public void Run()
    {
        IBaseDBOperation1<User, Guid> serviceU1 = new UserService();
        IBaseDBOperation2<User, Guid> serviceU2 = new UserService();
        var u = new User { Name = "Carl" };
        var resU1 = serviceU1.IsValid(u);
        var resU2 = serviceU1.GiveMeId(u);
        var resU3 = serviceU2.IsValidToDoSomethingElse(u);

        var serviceU3 = new UserService();
        //serviceU3.XXXXX() --> has no information about the methods we need

        IBaseDBOperation2<Color, long> serviceC1 = new ColorService();
        var c = new Color { RGB = "#FFFFFF" };
        var resC1 = serviceC1.IsValidToDoSomethingElse(c);


        var adasda = "";
    }
}



var consumer = new Consumer();
consumer.Run();
2 Answers

I will start with small remark - please try to follow standard naming conventions, in this case this one:

Interface names start with a capital I.

As for the issues:

Issue 1: I don't like the fact that the generics where we say, use TTarget as User, we also need to pass the T ID type.

Not much has changed here for the last 6 years, interface BaseDBOperation1<TTarget, T>.. still requires 2 generic type parameters and you can still have an interface with one type parameter, i.e. interface BaseDBOperation1<TTarget> which will be ambiguous for the compiler (so adding interface BaseDBOperation1<TTarget> will become a breaking change, which is a concern if those classes are distributed as library).

Possibly something like this could be achieved with something like higher-kinded types or similar language feature but ATM it is not available in C#.

Related issues to track:

Issue 2: ... this second variable will not see the other methods.

This is by design (default interface methods draft spec):

Note that a class does not inherit members from its interfaces; that is not changed by this feature:

interface IA
{
    void M() { WriteLine("IA.M"); }
}
class C : IA { } // OK
new C().M(); // error: class 'C' does not contain a member 'M'

In order to call any method declared and implemented in the interface, the variable must be the type of the interface

i.e. for var serviceU2 = new UserService(); you will need to cast to corresponding interface:

var resU1 = ((BaseDBOperation1<User, Guid>)serviceU2).IsValid(u); 

Another reason for such behaviour can be similar to the so called brittle/fragile base class problem.

Personally I'm not a big fan of this feature both conceptually and due to some corner cases (for example this one).

As for an approach to implement such functionality and reducing code written manually (if you have A LOT of such repositories) - you can look at some compile time code generation with source generators but this is not definitely an easy option. At least for the first time.

Would the following work for you? Essentially making the User and the Color the holder of the service operations.

https://dotnetfiddle.net/zsjZfQ

using System;

var user = new User();
var color = new Color();

Console.WriteLine(user.DbOperation1.IsValid());
Console.WriteLine(user.DbOperation2.IsValidToDoSomethingElse());
Console.WriteLine(user.DbOperation3.ThisIsOnlyAvailableToSome());

Console.WriteLine(color.DbOperation1.IsValid());
Console.WriteLine(color.DbOperation2.IsValidToDoSomethingElse());

var userAsBaseDbEntry = (BaseDBEntry<Guid>)user;
Console.WriteLine(userAsBaseDbEntry.DbOperation1.IsValid());
Console.WriteLine(userAsBaseDbEntry.DbOperation2.IsValidToDoSomethingElse());

public abstract class BaseDBEntry<T> where T : struct
{
    public abstract IDbOperation1<T> DbOperation1 { get; init; }
    public abstract IDbOperation2<T> DbOperation2 { get; init; }
    
    public T Id { get; init; }
    public DateTime CreatedOn { get; init; } = DateTime.Now;
    public DateTime? DeletedOn { get; init; }
}

public class User : BaseDBEntry<Guid>
{
    public string Name { get; init; }
    override public sealed IDbOperation1<Guid> DbOperation1 { get; init; }
    override public sealed IDbOperation2<Guid> DbOperation2 { get; init; }
    public IDbOperation3 DbOperation3 { get; }

    
    public User()
    {
        DbOperation1 = new DbOperation1Impl<Guid>(this);
        DbOperation2 = new DbOperation2Impl<Guid>(this);
        DbOperation3 = new DbOperation3Impl(this);
        
        Id = Guid.NewGuid();
    }
}

public interface IDbOperation3
{
    bool ThisIsOnlyAvailableToSome();
}

public class DbOperation3Impl : IDbOperation3
{
    private readonly BaseDBEntry<Guid> _entry;

    public DbOperation3Impl(BaseDBEntry<Guid> entry)
    {
        _entry = entry;
    }

    public bool ThisIsOnlyAvailableToSome() => !_entry.DbOperation1.IsValid();
}

public class Color : BaseDBEntry<long>
{
    override public sealed IDbOperation1<long> DbOperation1 { get; init; }
    override public sealed IDbOperation2<long> DbOperation2 { get; init; }
    public string Rgb { get; init; }
    
    public Color()
    {
        DbOperation1 = new DbOperation1Impl<long>(this);
        DbOperation2 = new DbOperation2Impl<long>(this);
        
        Id = DateTime.Now.Ticks;
    }
}

public interface IDbOperation1<T> where T : struct
{
    bool IsValid();
}

public interface IDbOperation2<T> where T : struct
{
    bool IsValidToDoSomethingElse();
}

class DbOperation1Impl<T> : IDbOperation1<T> where T : struct
{
    private readonly BaseDBEntry<T> _entry;

    public DbOperation1Impl(BaseDBEntry<T> entry)
    {
        _entry = entry;
    }
    
    public bool IsValid() => _entry.CreatedOn < DateTime.Now;
}

class DbOperation2Impl<T> : IDbOperation2<T> where T : struct
{
    private readonly BaseDBEntry<T> _entry;

    public DbOperation2Impl(BaseDBEntry<T> entry)
    {
        _entry = entry;
    }
    
    public bool IsValidToDoSomethingElse() => _entry.DeletedOn != null;
}
Related