-2

I have solved my problem in the way I will describe below but I want to find the best way.

Typescript has generic type Readonly that solved this problem.

Thanks for your suggests I used folowing codes (.net6) :

public interface IReadOnlyAbility
{
    bool IsReadOnly { get; }
    void ReadOnly();
    public T CheckForSetValue<T>(T PropertyValue)
    {
        if (IsReadOnly)
            throw new ReadOnlyException();
        return PropertyValue;
    }
}

public interface IUser : IReadOnlyAbility
{
    string UserName { get; set; }
    string Password { get; set; }
}

public class User : IUser
{
    private string userName = string.Empty;
    private string password = string.Empty;
    public bool IsReadOnly { get; private set; }
    public string UserName
    {
        get => userName;
        set => userName = CheckForSetValue(value);
    }
    public string Password
    {
        get => password;
        set => password = CheckForSetValue(value);
    }
    protected T CheckForSetValue<T>(T Value) => ((IReadOnlyAbility)this).CheckForSetValue(Value);
    public void ReadOnly() => IsReadOnly = true;
}

Then I added dependency injection >>> Services.AddTransient<IUser, User>();

Now , I used it :

var user = Services.GetService<IUser>();
user.UserName = "UserName";
user.Password = "Password";
user.ReadOnly();

BQF
  • 33
  • 7
  • This use case seems contrived, in that a `User` is not a service you'd register through DI -- normally. Such classes can also be built as `record`s (`record User(string Username, string Password)`) or using custom `init` properties, either of which give you instances that are immutable from the time of creation. Such instances can also be built by builder classes that are themselves not immutable (those would be a better candidate for DI, if there's at all a case for exchanging their implementation). – Jeroen Mostert Mar 14 '23 at 10:12
  • My code is an example, but there may be situations where we want to get our inputs based on an interface and according to certain conditions, this interface will be implemented, so maybe we need another class that does not know how to value this interface. How is the input interface created, as a result, it does not have access to the initial time because it did not create it Now what should be done to fix it? – BQF Mar 14 '23 at 10:55
  • In my experience, "this is just an example" is shorthand for "I haven't yet written out the actual problem in full with all the nuances that make it a problem, which is why we don't know the actual solutions". It's attractive to want to solve very general instances of problems so you've got it "once and for all", but it's generally better to solve one, specific, exact instance of your problem, then look at how (and if) it should generalize. What does the problem look like if you leave out the "maybes" and "certain conditions" and interfaces, and why would the current approach not be "best"? – Jeroen Mostert Mar 14 '23 at 11:40
  • Thanks for your comment, but always we are dealing with "maybes" and "conditions" and we have to consider them and work accordingly. My main question is: How can we design a class that has the following capabilities: 1- As long as it is not used for a specific operation, its components can be changed (in other words, its values are initial settings) 2- As soon as the main operation starts, its components cannot be changed – BQF Mar 14 '23 at 12:23
  • Well -- exactly as you've shown here. Give the class a method that promises to freeze its state, then implement that. The language has no specific support for this ("temporal" immutability), so writing that code will always be awkward, though you can pull in things like code generators to mitigate that. The language only supports full immutability (unchanging state from the time of creation), and that can be leveraged as well, through the builder pattern. There is no "best" way of doing this, every approach has its pros and cons. – Jeroen Mostert Mar 14 '23 at 12:31
  • I really dislike such hybrid classes. If you need read-only instances, write an immutable class with constructors taking all values. If you need a changeable class e.g. for property binding in a view and a read-only version for your business logic, create two classes and corresponding converters or use something like AutoMapper. – Oliver Jun 06 '23 at 10:31

2 Answers2

1

I would suggest having your properties implemented as private set properties and only allow to set values via method calls.

Though this is a personal preference, throwing an exception from a property setter seems to me like bad practice.
As someone that's going to use such a class, I would much rather deal with TrySet methods instead of potential exceptions from property setters:

public class User
{
    public string UserName { get; private set; }
    public string Password { get; private set; }
    public bool IsReadOnly {get; private set;}
 
    public void MakeReadOnly() => IsReadOnly = true; 

    public bool TrySetUserName(string userName) 
    {
        if(IsReadOnly) return false;
        UserName = userName;
        return true;
    }
}

Update

I've had some time to research on how to generalize this into an interface. I've had an idea to use a generic TrySetValue method that will take in the property to set as a lambda expression and the value, and will only set it if the instance is not yet readonly.
I have very little experience with using expression trees so it was also a way for me to learn some new cool tricks - and here's what I came up with: (based on this stackoverflow answer by Darin Dimitrov):

public interface IReadOnlyAbility
{
    bool IsReadOnly {get;}
    void MakeReadonly();
}

public class User : IReadOnlyAbility {
    
    public bool IsReadOnly {get; private set;}
    public string UserName { get; private set; }
    public string Password { get; private set; }
    
    public void MakeReadonly() => IsReadOnly = true;
}

and this is where the magic happens:

public static class IReadOnlyAbilityExtensions
{
    public static bool TrySetValue<T, TValue>(this T target, Expression<Func<T, TValue>> memberLambda, TValue value)
        where T : IReadOnlyAbility
    {
        if(target.IsReadOnly) return false;
            
        if (memberLambda.Body is MemberExpression memberSelectorExpression
            && memberSelectorExpression.Member is PropertyInfo property 
            && property.Name != nameof(target.IsReadOnly))
        {
            property.SetValue(target, value, null);
            return true;
        }
        
        return false;
    }
}

Now this can be used as follows:

var user = new User();
// these will return true
user.TrySetValue(c => c.UserName, "User name"); 
user.TrySetValue(c => c.Password, "Password");

// this will return false, you can't use this method to change the IsReadOnly property
user.TrySetValue(c => c.IsReadOnly, true);

user.MakeReadonly();

// these will return false
user.TrySetValue(c => c.IsReadOnly, false);
user.TrySetValue(c => c.UserName, "some other value");
user.TrySetValue(c => c.Password, "some other password");
    

You can see a live demo on .net fiddle

Zohar Peled
  • 79,642
  • 10
  • 69
  • 121
0

I checked the proposed method, calling TrySetValue for each initialization was a bit confusing and I defined an alternative solution using Lib.Harmony for my project, which seems to me to be cleaner in terms of coding.

i use folowing nuget

NuGet\Install-Package Lib.Harmony -Version 2.2.2

IReadOnlyAbility is an interface whose implementation makes all configurable public properties of an object read-only by default.

public interface IReadOnlyAbility
{
    bool IsReadOnly { get; internal set; }
    void ReadOnly() ;
}

By using ReadOnlyAbilityAttribute it is possible to set attributes that should not be read-only.

[AttributeUsage(AttributeTargets.Property)]
public class ReadOnlyAbilityAttribute : Attribute
{
    public bool Check { get; }
    public ReadOnlyAbilityAttribute(bool check) => Check = check;
}

ReadOnlyAbilityInstaller is responsible for adding read-only checking code for classes that implement IReadOnlyAbility

public sealed class ReadOnlyAbilityInstaller
{
    private readonly Lazy<Harmony> HarmonyInstance = new Lazy<Harmony>(() => new Harmony($"Harmony{Guid.NewGuid().ToString().Replace("-", string.Empty)}"));
    public void Install(Assembly assembly)
    {
        var readOnlyAbilityType = typeof(IReadOnlyAbility);
        var readOnlyAbilityTypes = assembly.GetTypes().Where(readOnlyAbilityType.IsAssignableFrom).Where(q => q.IsClass || q.IsValueType).ToList();

        var prefix = typeof(ReadOnlyAbilityInstaller).GetMethod(nameof(PrefixMethod), BindingFlags.NonPublic | BindingFlags.Static);
        var prefixHarmonyMethod = new HarmonyMethod(prefix);
        foreach (var type in readOnlyAbilityTypes)
        {
            var properties = type.GetProperties().Where(q => q.GetCustomAttribute<ReadOnlyAbilityAttribute>()?.Check != false)
                                                 .Where(q => q.SetMethod is not null)
                                                 .Select(q => q.SetMethod!)
                                                 .Where(q => q.IsPrivate == false)
                                                 .ToList();

            foreach (var property in properties)
                HarmonyInstance.Value.Patch(property, prefixHarmonyMethod);

        }
    }
    private static bool PrefixMethod(IReadOnlyAbility __instance, MethodBase __originalMethod)
    {
        if (__instance.IsReadOnly)
            throw new ReadOnlyException($"Current instance is readonly cause «{__originalMethod.Name.Replace("set_", string.Empty)}» can not be change");
        return true;
    }
}

Now need to install its :

internal static class Program
{
    [STAThread]
    static void Main()
    {
        var assembly = Assembly.GetExecutingAssembly();
        var readOnlyAbilityInstaller = new ReadOnlyAbilityInstaller();
        readOnlyAbilityInstaller.Install(assembly);
        /*other source code*/
    }
}

For example :

public class ServiceConfig : IReadOnlyAbility
{
    public string IP { get; set; }
    public int Port { get; set; }
    public string UserName { get; set; }
    public string Password { get; set; }
    public bool IsReadOnly { get; private set; }
    public void ReadOnly() => IsReadOnly = true;
}

public class Service
{
    public readonly ServiceConfig Config;
    public Service(ServiceConfig config)
    {
        Config = config;
        Config.ReadOnly();
    }
}
BQF
  • 33
  • 7
  • Your prefix can return `void` instead of an unconditional `true`. Not using `.ToList()` for `readOnlyAbilityTypes` is a bit better. Much of your reflection code can be replaced by using Harmony's AccessTools (see documentation) – Andreas Pardeike Aug 17 '23 at 06:03
  • I know harmony is a hack tools but I need to make the object read only after receiving from a service. I think this problem is related to the language feature that is not supported by C# now. For example this feature supported by Type Script. – BQF Aug 18 '23 at 13:30