13

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();
Dryadwoods
  • 2,875
  • 5
  • 42
  • 72
  • 1
    `var` is not enough where? Can you be clearer? – ProgrammingLlama Aug 30 '22 at 06:31
  • @DiplomacyNotWar instead of: BaseDBOperation1 serviceU1 = new UserService(); ........ var serviceU2 = new UserService(); ...... this second variable will not see the other methods. – Dryadwoods Aug 30 '22 at 06:32
  • The methods on `BaseDBOperation1` are effectively explicit implementations, so you can only reference them through the lens of `BaseDBOperation1`. See [this question](https://stackoverflow.com/questions/63413132/why-do-i-need-to-cast-this-to-an-interface-with-a-default-implementation-in-c). If you want to just use `var`, you may be better served with an abstract base class. – ProgrammingLlama Aug 30 '22 at 06:34
  • @DiplomacyNotWar I undertand why I need to write it. :) my question is: how to avoid it? :D – Dryadwoods Aug 30 '22 at 06:36
  • 1
    I don't think you can avoid it if you wish to use default interface implementations. – ProgrammingLlama Aug 30 '22 at 06:37
  • I don't entirely follow your train of thought with issue 1. How would the compiler know that `Id` is the correct property of `User`? And how would the compiler known the correct return type for `GiveMeId`? – ProgrammingLlama Aug 30 '22 at 06:38
  • I assume that you read and understood the question of 6 years ago...... The idea if to make the bottom layers as generic and abstract as possible.... for the above layers to be also so generic... and no matter if I am talking about potatos or airplanes (object types), all the middle layers will know what I am talking about... and I want to offer to each service a range of "service methods"... – Dryadwoods Aug 30 '22 at 06:40
  • I think that what you're asking for 2 is probably plausible, but whether Microsoft would implement it - who knows? Would it even make it into positive figures from [-100 points](https://learn.microsoft.com/en-gb/archive/blogs/ericgu/minus-100-points)? I expect it throws up challenges: The compiler would have to go deeper to ascertain the type. What would the return type be? What if there are multiple methods vying for "T"? How would you represent that as a method argument type or a return type? Will it be confusing for developers? – ProgrammingLlama Aug 30 '22 at 06:52
  • Let us [continue this discussion in chat](https://chat.stackoverflow.com/rooms/247668/discussion-between-dryadwoods-and-diplomacynotwar). – Dryadwoods Aug 30 '22 at 06:55
  • Can you be clearer? – Hediye_seza Sep 11 '22 at 16:21
  • @Hediye_seza Can I be clear? Well... I do believe that the description text of two questions, plus comments, etc... witten during a timespan of 6 years if clear enough..... – Dryadwoods Sep 12 '22 at 05:05

2 Answers2

4

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.

Guru Stron
  • 102,774
  • 10
  • 95
  • 132
1

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;
}
Peheje
  • 12,542
  • 1
  • 21
  • 30
  • 1
    Thanks for the suggestion, but in that way we are not following the diagram concept that I wrote on the question 6 years ago.... In your code you are really defining the actions already inside of the object User... I do not want that.... Anyway. I really appreciate your help. Thank you. – Dryadwoods Sep 07 '22 at 15:27