Search code examples
c#oopdesign-patternscode-cleanup

What is the best way to write a class with editable properties that becomes read-only after the method is called?


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();


Solution

  • 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();
        }
    }