繁体   English   中英

如何将属性从一个 object 复制到另一个具有不同值 C#

[英]how to copy properties from one object to another with different values C#

我想将给定 object ClassA中的 Properties 值复制到另一个名为ClassB的 object 实例中,这些类可能是也可能不是同一类型。

如果ClassB中的属性有值,而ClassA中对应的属性值为 null,则不要复制该值,因此仅复制ClassB中的当前属性为 null 的位置。

这不是克隆练习,目标 object ( ClassB ) 已经用部分定义的值实例化,我正在寻找一种可重用的方法来复制尚未设置的值的 rest。

想想我们有一个通用或默认测试数据值的测试场景,对于特定的测试我想设置一些特定的字段,然后从通用测试数据 object 中完成设置其他属性。

我想我正在寻找一个基于反射的解决方案,因为这样我们就不需要知道要复制的特定类型,这将使它可以在许多不同的场景中重用。

例如。

public class Employee
{
    public int EmployeeID { get; set; }
    public string EmployeeName { get; set; }
    public Address ContactAddress { get; set; }
}

public class Address
{
    public string Address1 { get; set; }
    public string City { get; set; }
    public string State { get; set; }
    public string ZipCode { get; set; }
}

测试例如。

public void TestMethod1()
{
    Employee employee = new Employee();
    employee.EmployeeID = 100;
    employee.EmployeeName = "John";
    employee.ContactAddress = new Address();
    employee.ContactAddress.Address1 = "Park Ave";
    employee.ContactAddress.City = "New York";
    employee.ContactAddress.State = "NewYork";
    employee.ContactAddress.ZipCode = "10002";
 
    Employee employeeCopy = new Employee();
    employeeCopy.EmployeeID = 101;
    employeeCopy.EmployeeName = "Tom";
    employeeCopy.ContactAddress = new Address();

    CopyPropertiesTo(employee, employeeCopy);
}

我想得到结果

员工复制员工ID = 101;
员工姓名="汤姆";
ContactAddress.Address1 = "公园大道";
ContactAddress.City = "纽约";
ContactAddress.State = "纽约";
ContactAddress.ZipCode = "10002"

因此,在这种情况下,由于没有设置employeeCopy.ContactAddress中的任何字段,因此只应复制原始employee object 中的那些字段。

我不知道如何编写方法:
CopyPropertiesTo(object sourceObject, object targetObject)

一种方法是简单地检查“to” Employee中的每个属性,如果它是null0 ,则为其分配来自“from” Employee的值:

/// <summary>
/// Copies values in 'from' to 'to' if they are null in 'to'
/// </summary>
public static void CopyProperties(Employee from, Employee to)
{
    if (from == null) return;
    if (to == null) to = new Employee();

    if (to.EmployeeID == 0) to.EmployeeID = from.EmployeeID;
    if (to.EmployeeName == null) to.EmployeeName = from.EmployeeName;

    if (from.ContactAddress == null) return;
    if (to.ContactAddress == null) to.ContactAddress = new Address();

    if (to.ContactAddress.Address1 == null)
        to.ContactAddress.Address1 = from.ContactAddress.Address1;
    if (to.ContactAddress.City == null)
        to.ContactAddress.City = from.ContactAddress.City;
    if (to.ContactAddress.State == null)
        to.ContactAddress.State = from.ContactAddress.State;
    if (to.ContactAddress.ZipCode == null)
        to.ContactAddress.ZipCode = from.ContactAddress.ZipCode;
}
public static void CopyPropertiesTo(Employee EP1, Employee EP2){
    
    Type eType=typeof(Employee);
    PropertyInfo[] eProps = eType.GetProperties();

    foreach(var p in eProps){
        if(p.PropertyType != typeof(String) && p.PropertyType != typeof(Int32)){
            //Merging Contact Address
            Type cType=p.PropertyType;
            PropertyInfo[] cProps = cType.GetProperties();
            
            foreach(var c in cProps){
                //Check if value is null
                if (String.IsNullOrEmpty((EP2.ContactAddress.GetType().GetProperty(c.Name).GetValue(EP2.ContactAddress) as string))){
                    //Assign Source to Target
                    EP2.ContactAddress.GetType().GetProperty(c.Name).SetValue(EP2.ContactAddress, (EP1.ContactAddress.GetType().GetProperty(c.Name).GetValue(EP1.ContactAddress)));
                }
            }
        }
        else{
            //Check if value is null or empty
            if (String.IsNullOrEmpty((EP2.GetType().GetProperty(p.Name).GetValue(EP2) as string))){
                //Assign Source to Target
                EP2.GetType().GetProperty(p.Name).SetValue(EP2, (EP1.GetType().GetProperty(p.Name).GetValue(EP1)));
            }
        }
    }
}

不是最漂亮的,但这应该可以让您更改 class 中的属性名称/数量。 我从来没有真正尝试过这样做,所以如果有人有一些反馈,我将不胜感激

查看以下链接以获取更多信息和示例PropertyInfo GetType GetProperty

如果还不算太晚,这也是我的建议,但也许会有所帮助。

    public class Source
    {
        [DefaultValueAttribute(-1)]
        public int Property { get; set; }

        public int AnotherProperty { get; set; }
    }

    public class Dedstination
    {
        public int Property { get; set; }

        [DefaultValueAttribute(42)]
        public int AnotherProperty { get; set; }
    }

    public void Main()
    {
        var source = new Source { Property = 10, AnotherProperty = 76 };
        var destination = new Dedstination();

        MapValues(source, destination);
    }

    public static void MapValues<TS, TD>(TS source, TD destination)
    {
        var srcPropsWithValues = typeof(TS)
            .GetProperties(BindingFlags.Public | BindingFlags.Instance)
            .ToDictionary(x => x.Name, y => y.GetValue(source));

        var dstProps = typeof(TD)
       .GetProperties(BindingFlags.Public | BindingFlags.Instance)
       .ToDictionary(key => key, value => value.GetCustomAttribute<DefaultValueAttribute>()?.Value
                                       ?? (value.PropertyType.IsValueType
                                       ? Activator.CreateInstance(value.PropertyType, null)
                                       : null));

        foreach (var prop in dstProps)
        {
            var destProperty = prop.Key;

            if (srcPropsWithValues.ContainsKey(destProperty.Name))
            {
                var defaultValue = prop.Value;
                var currentValue = destProperty.GetValue(destination);
                var sourceValue = srcPropsWithValues[destProperty.Name];

                if (currentValue.Equals(defaultValue) && !sourceValue.Equals(defaultValue))
                {
                    destProperty.SetValue(destination, sourceValue);
                }
            }
        }
    }

编辑:我编辑了我的解决方案,以消除对使用 DefaultValueAttribute 的依赖。 现在,您可以从指定的属性或类型默认值中获取默认值。

以前的解决方案如下:

        // This solution do not needs DefaultValueAttributes 
        var dstProps = typeof(TD)
           .GetProperties(BindingFlags.Public | BindingFlags.Instance)
           .ToDictionary(x => x, x => x.PropertyType.IsValueType ? Activator.CreateInstance(x.PropertyType, null) : null);

        // This solution needs DefaultValueAttributes 
        var dstProps = typeof(TD)
           .GetProperties(BindingFlags.Public | BindingFlags.Instance)
           .ToDictionary(x => x, x => x.GetCustomAttribute<DefaultValueAttribute>()?.Value ?? null);

在复制完成和/或实现 ICloneable 接口后对新实例进行更改。 https://docs.microsoft.com/en-us/dotnet/api/system.icloneable?view=netcore-3.1

深度克隆可以通过序列化轻松实现,但是仅跨非空字段复制需要更多条件逻辑,在这种情况下,我将其称为Coalesce ,因此我将我的方法命名为CoalesceTo 如果您愿意,您可以将其重构为扩展方法,但我不推荐它,而是将其放在 static 助手 class 中。 尽管这可能很有用,但我不鼓励将其作为生产业务运行时的“goto”。

对这些类型的解决方案使用反射通常是效率最低的机制,但它为我们提供了很大的灵活性,并且非常适合 mocking、原型设计和快速单元测试表达式。

  • 尽管不在此示例中,但可以轻松添加检查以排除高级场景的[Obsolete]属性

以下示例使用属性名称比较,因此您不必传入相同类型的对象。 请注意,已创建IsNullIsValueType方法来封装这些概念,从而简化您可能希望对此方法进行的调整。

  • 此方法还在继续之前检查是否可以读取/写入属性,这允许我们在源 object 上支持只读属性,当然我们不会尝试写入只读属性。
  • 解析和写入的最终值包含在抑制任何错误的 try catch 语句中,需要进行一些调整才能使这样的代码普遍工作,但对于简单的类型定义应该可以正常工作。
/// <summary>
/// Deep Copy the top level properties from this object only if the corresponding property on the target object IS NULL.
/// </summary>
/// <param name="source">the source object to copy from</param>
/// <param name="target">the target object to update</param>
/// <returns>A reference to the Target instance for chaining, no changes to this instance.</returns>
public static void CoalesceTo(object source, object target, StringComparison propertyComparison = StringComparison.OrdinalIgnoreCase)
{
    var sourceType = source.GetType();
    var targetType = target.GetType();
    var targetProperties = targetType.GetProperties();
    foreach(var sourceProp in sourceType.GetProperties())
    {
        if(sourceProp.CanRead)
        {
            var sourceValue = sourceProp.GetValue(source);

            // Don't copy across nulls or defaults
            if (!IsNull(sourceValue, sourceProp.PropertyType))
            {
                var targetProp = targetProperties.FirstOrDefault(x => x.Name.Equals(sourceProp.Name, propertyComparison));
                if (targetProp != null && targetProp.CanWrite)
                {
                    if (!targetProp.CanRead)
                        continue; // special case, if we cannot verify the destination, assume it has a value.
                    else if (targetProp.PropertyType.IsArray || targetProp.PropertyType.IsGenericType // It is ICollection<T> or IEnumerable<T>
                                                                && targetProp.PropertyType.GenericTypeArguments.Any()
                                                                && targetProp.PropertyType.GetGenericTypeDefinition() != typeof(Nullable<>) // because that will also resolve GetElementType!
                            )
                        continue; // special case, skip arrays and collections...
                    else
                    {
                        // You can do better than this, for now if conversion fails, just skip it
                        try
                        {
                            var existingValue = targetProp.GetValue(target);
                            if (IsValueType(targetProp.PropertyType))
                            {
                                // check that the destination is NOT already set.
                                if (IsNull(existingValue, targetProp.PropertyType))
                                {
                                    // we do not overwrite a non-null destination value
                                    object targetValue = sourceValue;
                                    if (!targetProp.PropertyType.IsAssignableFrom(sourceProp.PropertyType))
                                    {
                                        // TODO: handle specific types that don't go across.... or try some brute force type conversions if neccessary
                                        if (targetProp.PropertyType == typeof(string))
                                            targetValue = targetValue.ToString();
                                        else 
                                            targetValue = Convert.ChangeType(targetValue, targetProp.PropertyType);
                                    }

                                    targetProp.SetValue(target, targetValue);
                                }
                            }
                            else if (!IsValueType(sourceProp.PropertyType))
                            {
                                // deep clone
                                if (existingValue == null)
                                    existingValue = Activator.CreateInstance(targetProp.PropertyType);

                                CoalesceTo(sourceValue, existingValue);
                            }
                        }
                        catch (Exception)
                        {
                            // suppress exceptions, don't set a field that we can't set
                        }

                    }
                }
            }
        }
    }
}

/// <summary>
/// Check if a boxed value is null or not
/// </summary>
/// <remarks>
/// Evaluate your own logic or definition of null in here.
/// </remarks>
/// <param name="value">Value to inspect</param>
/// <param name="valueType">Type of the value, pass it in if you have it, otherwise it will be resolved through reflection</param>
/// <returns>True if the value is null or primitive default, otherwise False</returns>
public static bool IsNull(object value, Type valueType = null)
{
    if (value is null)
        return true;

    if (valueType == null) valueType = value.GetType();

    if (valueType.IsPrimitive || valueType.IsEnum || valueType.IsValueType)
    {
        // Handle nullable types like float? or Nullable<Int>
        if (valueType.IsGenericType)
            return value is null;
        else
            return Activator.CreateInstance(valueType).Equals(value);
    }

    // treat empty string as null!
    if (value is string s)
        return String.IsNullOrWhiteSpace(s);

    return false;
}
/// <summary>
/// Check if a type should be copied by value or if it is a complexe type that should be deep cloned
/// </summary>
/// <remarks>
/// Evaluate your own logic or definition of Object vs Value/Primitive here.
/// </remarks>
/// <param name="valueType">Type of the value to check</param>
/// <returns>True if values of this type can be straight copied, false if they should be deep cloned</returns>
public static bool IsValueType(Type valueType)
{
    // TODO: any specific business types that you want to treat as value types?

    // Standard .Net Types that can be treated as value types
    if (valueType.IsPrimitive || valueType.IsEnum || valueType.IsValueType || valueType == typeof(string))
        return true;

    // Support Nullable Types as Value types (Type.IsValueType) should deal with this, but just in case
    if (valueType.HasElementType // It is array/enumerable/nullable
        && valueType.IsGenericType && valueType.GetGenericTypeDefinition() == typeof(Nullable<>))
        return true;


    return false;
}

因为我们在这里使用反射,所以我们无法利用Generics可以为我们提供的优化。 如果您想将其应用于生产环境,请考虑使用 T4 模板来编写此逻辑的通用类型版本作为您的业务类型的扩展方法。

深度克隆 -

你会注意到我特别跳过了 arrays 和其他 IEnumerable 结构......有一大堆蠕虫在支持它们,最好不要让一种方法尝试深度复制,所以把嵌套调用CoalesceTo拿出来,然后在树中的每个 object 上调用 clone 方法。

数组/集合/列表的问题在于,在克隆之前,您需要确定一种将源中的集合与目标中的集合同步的方法,您可以根据 Id 字段或某种形式进行约定像[KeyAttribute]这样的属性,但这种实现需要高度特定于您的业务逻辑,并且不在这个已经很可怕的帖子的 scope 之外;)

DecimalDateTime这样的类型在这些类型的场景中是有问题的,它们不应该与 null 进行比较,而是我们必须将它们与它们的默认类型状态进行比较,同样我们不能在这种情况下使用通用的default运算符或值,因为类型只能在运行时解析。

因此,我更改了您的类,以包含一个示例,说明此逻辑如何处理 DateTimeOffset:

public class Employee
{
    public int EmployeeID { get; set; }
    public string EmployeeName { get; set; }
    public DateTimeOffset Date { get; set; }
    public float? Capacity { get; set; }
    Nullable<int> MaxShift { get; set; }
    public Address ContactAddress { get; set; }
}

public class Address
{
    public string Address1 { get; set; }
    public string City { get; set; }
    public string State { get; set; }
    public string ZipCode { get; set; }
}

public static  void TestMethod1()
{
    Employee employee = new Employee();
    employee.EmployeeID = 100;
    employee.EmployeeName = "John";
    employee.Capacity = 26.2f;
    employee.MaxShift = 8;
    employee.Date = new DateTime(2020,1,22);
    employee.ContactAddress = new Address();
    employee.ContactAddress.Address1 = "Park Ave";
    employee.ContactAddress.City = "New York";
    employee.ContactAddress.State = "NewYork";
    employee.ContactAddress.ZipCode = "10002";

    Employee employeeCopy = new Employee();
    employeeCopy.EmployeeID = 101;
    employeeCopy.EmployeeName = "Tom";
    employeeCopy.ContactAddress = new Address();

    CoalesceTo(employee, employeeCopy);
}

这会产生以下 object 图:

{
  "EmployeeID": 101,
  "EmployeeName": "Tom",
  "Date": "2020-01-22T00:00:00+11:00",
  "Capacity":26.2,
  "MaxShift":8,
  "ContactAddress": {
    "Address1": "Park Ave",
    "City": "New York",
    "State": "NewYork",
    "ZipCode": "10002"
  }
}

如果您首先进行完整的深度克隆然后设置您的值,那么这些类型的问题通常会更容易且资源消耗更少,而不是尝试进行深度复制

SO上有很多关于深度克隆的帖子,我的偏好只是使用 JSON.Net 进行序列化然后反序列化。

 public static T Clone<T>(T value, Newtonsoft.Json.JsonSerializerSettings settings = null) { var objectType = value.GetType(); var cereal = Newtonsoft.Json.JsonConvert.SerializeObject(value, settings); return (T)Newtonsoft.Json.JsonConvert.DeserializeObject(cereal, objectType, settings); }

但是,此代码需要Newtonsoft.Json nuget package 参考。

克隆 object首先设置所有通用/默认值,然后我们只修改此特定测试或代码块所需的那些属性。

public void TestMethod1()
{
    Employee employee = new Employee();
    employee.EmployeeID = 100;
    employee.EmployeeName = "John";
    employee.ContactAddress = new Address();
    employee.ContactAddress.Address1 = "Park Ave";
    employee.ContactAddress.City = "New York";
    employee.ContactAddress.State = "NewYork";
    employee.ContactAddress.ZipCode = "10002";
 
    // Create a deep clone of employee
    Employee employeeCopy = Clone(employee);

    // set the specific fields that we want to change
    employeeCopy.EmployeeID = 101;
    employeeCopy.EmployeeName = "Tom";

}

如果我们愿意改变我们的方法,我们通常可以找到更简单的解决方案,这个解决方案将具有相同的 output,就好像我们有条件地复制了属性值一样,但没有进行任何比较。

如果您有条件副本的其他原因,在这篇文章的其他解决方案中称为MergeCoalesce ,那么我使用反射的其他答案将完成这项工作,但它不如这个强大。

[TestClass]
public class UnitTest11
{
    [TestMethod]
    public void TestMethod1()
    {

        Employee employee = new Employee();
        employee.EmployeeID = 100;
        employee.EmployeeName = "John";
        employee.Date = DateTime.Now;
        employee.ContactAddress = new Address();
        employee.ContactAddress.Address1 = "Park Ave";
        employee.ContactAddress.City = "New York";
        employee.ContactAddress.State = "NewYork";
        employee.ContactAddress.ZipCode = "10002";

        Employee employeeCopy = new Employee();
        employeeCopy.EmployeeID = 101;
        employeeCopy.EmployeeName = "Tom";
        employeeCopy.ContactAddress = new Address();
        employeeCopy.ContactAddress.City = "Bei Jing";
        //copy all properties from employee to employeeCopy
        CoalesceTo(employee, employeeCopy);

        Console.ReadLine();
    }

    /// Deep Copy the top level properties from this object only if the corresponding property on the target object IS NULL.
    /// </summary>
    /// <param name="source">the source object to copy from</param>
    /// <param name="target">the target object to update</param>
    /// <returns>A reference to the Target instance for chaining, no changes to this instance.</returns>
    public static void CoalesceTo(object source, object target, StringComparison propertyComparison = StringComparison.OrdinalIgnoreCase)
    {
        var sourceType = source.GetType();
        var targetType = target.GetType();
        var targetProperties = targetType.GetProperties();
        foreach (var sourceProp in sourceType.GetProperties())
        {
            if (sourceProp.CanRead)
            {
                var sourceValue = sourceProp.GetValue(source);

                // Don't copy across nulls or defaults
                if (!IsNull(sourceValue, sourceProp.PropertyType))
                {
                    var targetProp = targetProperties.FirstOrDefault(x => x.Name.Equals(sourceProp.Name, propertyComparison));
                    if (targetProp != null && targetProp.CanWrite)
                    {
                        if (!targetProp.CanRead)
                            continue; // special case, if we cannot verify the destination, assume it has a value.
                        else if (targetProp.PropertyType.IsArray || targetProp.PropertyType.IsGenericType // It is ICollection<T> or IEnumerable<T>
                                                                    && targetProp.PropertyType.GenericTypeArguments.Any()
                                                                    && targetProp.PropertyType.GetGenericTypeDefinition() != typeof(Nullable<>) // because that will also resolve GetElementType!
                                )
                            continue; // special case, skip arrays and collections...
                        else
                        {
                            // You can do better than this, for now if conversion fails, just skip it
                            try
                            {
                                var existingValue = targetProp.GetValue(target);
                                if (IsValueType(targetProp.PropertyType))
                                {
                                    // check that the destination is NOT already set.
                                    if (IsNull(existingValue, targetProp.PropertyType))
                                    {
                                        // we do not overwrite a non-null destination value
                                        object targetValue = sourceValue;
                                        if (!targetProp.PropertyType.IsAssignableFrom(sourceProp.PropertyType))
                                        {
                                            // TODO: handle specific types that don't go across.... or try some brute force type conversions if neccessary
                                            if (targetProp.PropertyType == typeof(string))
                                                targetValue = targetValue.ToString();
                                            else
                                                targetValue = Convert.ChangeType(targetValue, targetProp.PropertyType);
                                        }

                                        targetProp.SetValue(target, targetValue);
                                    }
                                }
                                else if (!IsValueType(sourceProp.PropertyType))
                                {
                                    // deep clone
                                    if (existingValue == null)
                                        existingValue = Activator.CreateInstance(targetProp.PropertyType);

                                    CoalesceTo(sourceValue, existingValue);
                                }
                            }
                            catch (Exception)
                            {
                                // suppress exceptions, don't set a field that we can't set
                            }

                        }
                    }
                }
            }
        }
    }

    /// <summary>
    /// Check if a boxed value is null or not
    /// </summary>
    /// <remarks>
    /// Evaluate your own logic or definition of null in here.
    /// </remarks>
    /// <param name="value">Value to inspect</param>
    /// <param name="valueType">Type of the value, pass it in if you have it, otherwise it will be resolved through reflection</param>
    /// <returns>True if the value is null or primitive default, otherwise False</returns>
    public static bool IsNull(object value, Type valueType = null)
    {
        if (value is null)
            return true;

        if (valueType == null) valueType = value.GetType();

        if (valueType.IsPrimitive || valueType.IsEnum || valueType.IsValueType)
            return value.Equals(Activator.CreateInstance(valueType));

        // treat empty string as null!
        if (value is string s)
            return String.IsNullOrWhiteSpace(s);

        return false;
    }
    /// <summary>
    /// Check if a type should be copied by value or if it is a complexe type that should be deep cloned
    /// </summary>
    /// <remarks>
    /// Evaluate your own logic or definition of Object vs Value/Primitive here.
    /// </remarks>
    /// <param name="valueType">Type of the value to check</param>
    /// <returns>True if values of this type can be straight copied, false if they should be deep cloned</returns>
    public static bool IsValueType(Type valueType)
    {
        // TODO: any specific business types that you want to treat as value types?

        // Standard .Net Types that can be treated as value types
        if (valueType.IsPrimitive || valueType.IsEnum || valueType.IsValueType || valueType == typeof(string))
            return true;

        // Support Nullable Types as Value types (Type.IsValueType) should deal with this, but just in case
        if (valueType.HasElementType // It is array/enumerable/nullable
            && valueType.IsGenericType && valueType.GetGenericTypeDefinition() == typeof(Nullable<>))
            return true;


        return false;
    }
}


public class Employee
{
    public int EmployeeID { get; set; }
    public string EmployeeName { get; set; }
    public DateTimeOffset Date { get; set; }
    public float? check { get; set; }
    public Address ContactAddress { get; set; }
}

public class Address
{
    public string Address1 { get; set; }
    public string City { get; set; }
    public string State { get; set; }
    public string ZipCode { get; set; }
}

非常感谢大家,尤其是@Chris Schaller,我在上面发布了代码

private Employee Check(Employee employee,Employee employeeCopy)
        {
if (employeeCopy.EmployeeID==0 && employee.EmployeeID !=0)
  {
     employeeCopy.EmployeeID = employee.EmployeeID;
  }
if (employeeCopy.EmployeeName == null && employee.EmployeeName != null)
  {
     employeeCopy.EmployeeName = employee.EmployeeName;
  }
if (employeeCopy.ContactAddress == null)
{
if (employeeCopy.ContactAddress.Address1 == null && employee.ContactAddress.Address1 != null)
  {
     employeeCopy.ContactAddress.Address1 = employee.ContactAddress.Address1;
  }
if (employeeCopy.ContactAddress.City == null && employee.ContactAddress.City != null)
 {
     employeeCopy.ContactAddress.City = employee.ContactAddress.City;
 }
if (employeeCopy.ContactAddress.State == null && employee.ContactAddress.State != null)
 {
     employeeCopy.ContactAddress.State = employee.ContactAddress.State;
 }
if (employeeCopy.ContactAddress.ZipCode == null && employee.ContactAddress.ZipCode != null)
 {
    employeeCopy.ContactAddress.ZipCode = employee.ContactAddress.ZipCode;
 }
}
            return employeeCopy;

}

这是你想要的?

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM