【问题标题】:Ensuring anonymous objects in C# all have certain common properties确保 C# 中的匿名对象都具有某些公共属性
【发布时间】:2022-01-23 05:33:01
【问题描述】:

我对 C# 中的匿名对象有疑问。情况如下:

我有一个 C# Web 应用程序,它不使用传统的 ASP.NET Razor 引擎,而是使用 RazorEngine 开源项目 (https://github.com/Antaris/RazorEngine)。我不确定这是否相关,但可能是。

我将模型对象传递给我正在显示的每个页面。每个模型对象都是不同的,有很多页面,因此有很多不同的模型对象,但我宁愿不必为每个模型声明单独的类,这就是我一直使用匿名类的原因:

// In method which displays page A:
var model = new {
  Lang  = _lang,
  PropX = "foo",
  PropY = "bar"
};
RazorEngine.Run("templateA", model);

// In a different method which displays page B:
var model = new {
  Lang = _lang,
  PropZ = "smu"
};
RazorEngine.Run("templateB", model);

您可能会注意到,botn(实际上是所有)这些模型都有一个共同的属性(“Lang”属性),实际上有一些共同的属性(Lang 是上面示例中为简化问题而显示的唯一一个)。

我的主要问题是我试图确保将这些属性添加到所有模型中,以保证它们包含在所有页面中,如果我后来决定添加一个新的公共属性,那么我可以在一个地方完成。

当然,一种方法是删除匿名类,并使用全部继承自一个基类的类型化类,该基类将声明公共属性。但这将是很多样板代码,如果有其他解决方案,我会更喜欢。

另一种解决方案是在模型对象的子属性中声明公共属性,或者在子对象中声明各个页面属性:

// Either like this:
var model = new {
    Common = GetCommonModelProperties(),
    PropX  = "foo",
    PropY  = "bar"
};
public object GetCommonModelProperties()
{
  return new {
    Lang = _lang
  };
}
// etc.

// or like this:

var pageModel = new {
  PropX = "foo",
  PropY = "bar
};
var model = CreateModel(pageModel);
RazorEngine.Run("templateA", model);

// where CreateModel could be implemented like this:

public object CreateModel(object pageModel)
{
  return new 
  {
    Lang = _lang,
    // etc., whatever common properties there exist
    Data = pageModel
  };
}

这种方法的问题是我必须修改我的所有模板,或者这些页面引用公共属性的所有实例(我必须将所有 Model.Lang 实例重命名为 Model.Common.Lang),或者到单个页面数据(将 Model.AnyProperty 修改为 Model.Data.AnyProperty)。当然,当这种重写发生时,出错的风险很大。

那么:有没有办法创建一个匿名对象,它的许多属性始终相同,但其余的可以动态指定?

我尝试创建两个单独的对象,然后将它们合并为一个,使用以下问题中的代码:Merging anonymous types

var commonModel = new {
  Lang = _lang
};
var pageModel = new {
  PropX = "foo",
  PropY = "bar"
};
var model = Merge(commonModel, pageModel);

是的,这行得通。直到我不得不使用类类型的 Lang 对象(我可以完全控制它),并且这个类重载了 operator[]。如果我使用此解决方法,过载将停止工作,并出现错误:

无法将带有 [] 的索引应用于“对象”类型的表达式

注意如果我只是将 Lang 属性包含在常规匿名对象中,则索引工作得非常好。

我还尝试为所有模型创建一个单独的基类,声明该类中的所有公共属性,但也从 System.Dynamic.DynamicObject 派生它,并覆盖将动态查找的 TryGetMember 方法字典中的页面属性(这会起作用,因为这些属性通常是简单的对象,即它们不会覆盖索引运算符,因此我可以在运行时动态添加这些属性:

var pageModel = new {
  PropX = "foo",
  PropY = "bar"
};

var model = CreateMainModel(pageModel);

public object CreateMainModel(object pageModel)
{
   var mainModel = new BaseModel()
   mainModel.Lang = _lang;

    foreach (System.Reflection.PropertyInfo fi in pageModel.GetType().GetProperties())
    {
      mainModel.PageProperties[fi.Name] = fi.GetValue(pageModel, null);
    }

    return mainModel;
}

class BaseModel : DynamicObject
{
  public LanguageMap Lang { get; set; }
  public Dictionary<string, object> PageProperties { get; set; }
  
  public override bool TryGetMember(GetMemberBinder binder, out object result)
  {
    if (PageProperties.ContainsKey(binder.Name))
    {
      result = PageProperties[binder.Name];
      return true;
    }
    return false;
  }
}

这样做的问题是,在派生自 DynamicObject 的类中声明的属性在页面模板中不可见,似乎只有从 TryGetMember 返回的属性是可见的。如果我让成员在 TryGetMember 中显式检查他们的名字可见,那么索引将停止工作,就像上面的情况一样。

如果这是 C++,我可以创建一个预处理器宏:

#define COMMON_MODEL_PROPERTIES \
    Lang = _lang
    More = _otherProperty

// Where models are declared:

var model = new 
{
  COMMON_MODEL_PROPERTIES,
  PropX = "foo",
  PropY = "bar"
}

但这不是 C++……这是 C#。

有什么想法吗?

【问题讨论】:

  • 我认为您使用组合的直觉可能是正确的,下面的问题是因为我很好奇会提出什么建议
  • 哇,真是一团糟。如果 javascript 开发人员决定使用 C#,这看起来有点像会发生的事情。我认为我的第一个行动方案是交出通知并逃跑
  • 怎么样..records?易于声明和实例化,在语义上与 AT(可变性、等价性)非常相似,可以参与继承层次结构,查找/替换可能会做很多笨拙的工作......
  • 一堆样板代码或在运行时使用一些 hacky 反射工作让你的脚被吹走,很难的

标签: c# templates properties anonymous-class dynamicobject


【解决方案1】:

好的,根据我最初的评论……哇。我强烈建议您咬紧牙关并重构以使用静态类型。对于其他阅读本文的人,请不要这样做。

但是,如果你真的真的真的真的真的真的真的真的很想通过最小的改变来实现这一点,我认为这对你有用。

向您的代码库添加扩展方法:

internal static class DynamicCommonPropertyExtensions
{
    public static object AddCommonProperties(this object obj)
    {
        dynamic expando = new ExpandoObject();
        foreach (var prop in obj.GetType().GetProperties(BindingFlags.Instance | BindingFlags.Public))
        {
            if (expando is IDictionary<string, object?> dict)
            {
                dict[prop.Name] = prop.GetValue(obj, null);
            }
        }
        //Add common properties here
        expando.CommonProp1 = "CommonPropValue1";
        expando.CommonProp2 = "CommonPropValue2";
        return expando;
    }
}

然后可以按如下方式使用(以您的示例)

var model = new
{
    Lang = _lang,
    PropX = "foo",
    PropY = "bar"
};
var addedCommonProperties = model.AddCommonProperties();
RazorEngine.Run("templateA", addedCommonProperties);

甚至“更好”而不使用其他变量

var model = new
{
    Lang = "Test",
    PropX = "foo",
    PropY = "bar"
}.AddCommonProperties();
RazorEngine.Run("templateA", model);

如果您需要在通过之前访问属性,您可以将其声明为动态

dynamic model = new
{
    Lang = "Test",
    PropX = "foo",
    PropY = "bar"
}.AddCommonProperties();

var a = model.CommonProp1;

写完这篇我现在觉得很脏

【讨论】:

  • 这在善恶两方面都很聪明。太棒了。
猜你喜欢
  • 1970-01-01
  • 2015-06-07
  • 1970-01-01
  • 2012-12-27
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2012-01-09
  • 2021-04-17
相关资源
最近更新 更多