接口/继承类在负载中 + 在一个属性中序列化/反序列化多个模型

huangapple go评论79阅读模式
英文:

interface/inherited class in paylaod + serialize/deserialize multiple model in one property

问题

以下是您要翻译的内容:

"Let me introduce the problem, we have an API that accepts multiple values in one field, let's assume we can have a request model and inside there is a field named animal like:

  // other properties
  "animal":
  {
    "type":"dog",
    // other specific dog properties
  }
}

or

{
  // other properties
  "animal":
  {
    "type":"cat",
    // other specific cat properties
  }
}

Now, we have layers, where we receive view model -> we transform it into dto for application layer -> and finally to db model, and vice versa for reads: db model -> dto model -> view model. We have created extension methods to map those to different layer model, eg.

{
  // mapping other properties
  Animal = MapAnimal(dbModel)
}

private static IAnimal MapAnimal(IAnimalDb dbAnimal)
{
  return dbAnimal switch
  {
    DogDb dog => new DogDto {//mapping dog specific props},
    CatDb cat => new CatDto {//mapping cat specific props}
  }
}```
and we do that for all layers, so they look extremely similar, but we have to multiply Cat/Dog/Other in all layers, when we want to add a new animal we also need to change all those places. This seems like a major SOLID issue we would like to solve.

And as a bonus, we have asynchronous communication between services where we send a byte array, and in this solution we have used a JSON converter (system.text.json) for another different model (but also very similar) like:
```public class AnimalQueueModelConverter : JsonConverter<IAnimalQueueModel>
{
  public override IAnimalQueueModel Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
  {
    var jsonObject = JsonElement.ParseValue(ref reader);
    var animal = jsonObject.GetProperty("type").ToString() switch
    {
      AnimalQueueType.Dog => jsonObject.Deserialize<DogQueueModel>(),
      AnimalQueueType.Cat => jsonObject.Deserialize<CatQueueModel>()
      // if more will come we need extend this as well
    }
  }

  public override void Write(Utf8JsonWriter writer, IAnimalQueueModel value, JsonSerializerOptions options)
  {
    switch (value)
    {
      case DogQueueModel:
        // here write this model using writer
      case CatQueueModel:
        // here write this model using writer
    }
  }
}```
And even though, in the "reader" class after someone deserializes the byte[], they still need to create this stupid `if cat()`, `if dog()`, `if hippo()`..

So we have so many places to edit when we need to add another animal to our system. Can someone give me a pattern, lib, example what we could use to make this code better to maintain? I was thinking about discriminators, but not sure how to propagate them. I think someone already had a similar problem and there is a better solution to this.

Thanks"

<details>
<summary>英文:</summary>

Let me introduce the problem, we have an API that accepts multiple values in one field, let&#39;s assume we can have a request model and inside there is a field named `animal` like:

{
// other properties
"animal":
{
"type":"dog",
// other specific dog properties
}
}

or

{
// other properties
"animal":
{
"type":"cat",
// other specific catproperties
}
}


Now, we have layers, where we receive view model -&gt; we transform it into dto for application layer -&gt; and finally to db model, and vice versa for reads: db model -&gt; dto model -&gt; view model. We have created extension methods to map those to different layer model, eg. 

internal static AnimalResponse ToDto(this AnimalReadModel dbModel)
{
// mapping other properties
Animal = MapAnimal(dbModel)
}

private static IAnimal MapAnimal(IAnimalDb dbAnimal)
{
return dbAnimal switch
{
DogDb dog => new DogDto {//mapping dog specific props},
CatDb cat => new CatDto {//mapping cat specific props}
}
}

and we do that for all layers, so they look extremelly similar, but we have to multiply Cat/Dog/Other in all layers, when we want to add new animal we also need to change all those places. This seems like major solid issue we would like to solve.

And as a bonus, we have asynchronous communication between services where we send byte array, and in this solution we have used json converter (system.text.json) for another different model (but also very similar) like:

public class AnimalQueueModelConverter : JsonConverter<IAnimalQueueModel>
{
public override IAnimalQueueModel Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
var jsonObject = JsonElement.ParseValue(ref reader);
var animal = jsonObject.GetProperty("type").ToString() switch
{
AnimalQueueType.Dog => jsonObject.Deserialize<DogQueueModel>(),
AnimalQueueType.Cat => jsonObject.Deserialize<CatQueueModel>()
// if more will come we need extend this as well
}
}

public override void Write(Utf8JsonWriter writer, IAnimalQueueModel value, JsonSerializerOptions options)
{
switch (value)
{
case DogQueueModel:
// here write this model using writer
case CatQueueModel:
// here write this model using writer
}
}
}


And even tho, in the &quot;reader&quot; class after someone deserialize the byte[] they still need to create this stupid `if cat()`, `if dog()`, `if hippo()`.. 
So we have so many places to edit when we need to add another animal to our system. Can someone give me a pattern, lib, example what we could use to make this code better to maintain? I was thinking about disctiminators, but not sure how to propagate them. I think someone already had similar problem and there is a better solution to this.
Thanks
</details>
# 答案1
**得分**: 2
关于**JSON(反)序列化**
从版本`7.0.0`开始,`System.Text.Json`支持[多态序列化](https://learn.microsoft.com/en-us/dotnet/standard/serialization/system-text-json/polymorphism)。
(此版本也在`.NET 6`上运行。)
简而言之,JSON有效负载包含一个(可配置的)鉴别器属性。
下面的JSON显示了狗和猫的示例。
{ "type" : "dog", "name" : "Max", "color": "gold" }
{ "type" : "cat", "name" : "Felix", "favoriteFood": "fish" }
文档对此鉴别器有以下重要说明。
> 鉴别器类型必须放在JSON对象的开头
`JsonSerializer`负责这一切。
接下来,定义一个所有具体模型都实现的接口(或基类)。
应用的[`JsonPolymorphic`](https://learn.microsoft.com/en-us/dotnet/api/system.text.json.serialization.jsonpolymorphicattribute)属性定义了鉴别器属性,这里是`type`。
对于每个具体类型,都会应用一个[`JsonDerivedType`](https://learn.microsoft.com/en-us/dotnet/api/system.text.json.serialization.jsonderivedtypeattribute)属性,定义了具体类型和鉴别器的相应值。
在下面的示例中,`CatQueueModel`链接到类型`cat`,而`DogQueueModel`链接到`dog`。
[JsonPolymorphic(TypeDiscriminatorPropertyName = "type")]
[JsonDerivedType(typeof(CatQueueModel), "cat")]
[JsonDerivedType(typeof(DogQueueModel), "dog")]
public interface IAnimalQueueModel
{ 
public string Name { get; set; }
}
所有具体类型都实现了该`IAnimalQueueModel`接口。
注意,`type`鉴别器属性不需要是模型本身的属性。
public class CatQueueModel : IAnimalQueueModel
{
public string Name { get; set; }
public string FavoriteFood { get; set; }
}
public class DogQueueModel : IAnimalQueueModel
{
public string Name { get; set; }
public string Color { get; set; } 
}
在反序列化时,在调用`JsonSerializer.Deserialize<T>(...)`时,将`IAnimalQueueModel`接口指定为`T`。
以下示例将给定的`JSON`反序列化为`DogQueueModel`实例。是`JSON`有效负载中的`type`决定具体结果类型。
var options = new JsonSerializerOptions
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
};
var dog = JsonSerializer.Deserialize<IAnimalQueueModel>(
@"{ ""type"":""dog"", ""name"": ""Max"", ""color"": ""gold"" }",
options);
Console.WriteLine($"{dog.Name} ({dog.GetType().Name})"); // Max (DogQueueModel)
---
这种多态处理不仅限于与`JSON`有效负载匹配的根模型,还适用于类上的属性。
下面的示例显示了一个具有`Animal`属性的模型,反序列化给定的`JSON`字符串后,该属性将保存一个`CatQueueModel`实例。
public class Data
{
public IAnimalQueueModel Animal { get; set; }
public string Code { get; set; }
}
var data = JsonSerializer.Deserialize<Data>(
@"{
""code"": ""foo"",
""animal"": { ""type"":""cat"", ""name"": ""Felix"", ""favoriteFood"": ""fish"" }
}", 
options
);
---
如果基于属性的设置不切实际,则可以使用[合同模型](https://learn.microsoft.com/en-us/dotnet/standard/serialization/system-text-json/polymorphism?#configure-polymorphism-with-the-contract-model)配置相同的设置。
---
关于**映射**
您可能想要查看[`AutoMapper`](https://automapper.org)([文档](https://docs.automapper.org))。
使用`AutoMapper`,您定义一个映射配置,指定要映射到其他类型的哪些类型 - 类和接口,可选择包括有关如何处理属性的详细信息。
public interface IAnimalDto
{
public string Name { get; set; }
}
public class CatDto : IAnimalDto
{ 
public string Name { get; set; }
public string FavoriteFood { get; set; }
}
public class DogDto : IAnimalDto
{
public string Name { get; set; }
public string Color { get; set; }
}
对于多态映射设置,您定义接口映射(下面示例中的第一个)以及每个具体类型的映射(下面代码中的第二个和第三个)。
下面的示例显示了如何将实现`IAnimalQueueModel`接口的类映射到实现`IAnimalDto`接口的类,以实现`DogQueueModel`到`DogDto`和`CatQueueModel`到`CatDto`的映射。
var config = new MapperConfiguration(
cfg => 
{
cfg.CreateMap<IAnimalQueueModel, IAnimalDto>();
cfg.CreateMap<DogQueueModel, DogDto>()
.IncludeBase<IAnimalQueueModel, IAnimalDto>();
cfg.CreateMap<CatQueueModel, CatDto>()
.IncludeBase<IAnimalQueueModel, IAnimalDto>();
});
要应用映射,您需要一个`IMapper`实例 - 可以进行依赖注入。
以下代码显示了如何将一个具体类型为`DogQueueModel`的`IAnimalQueueModel`实例映射到`IAnimalDto`类型,从而得到一个`DogDto`类型的实例。
var mapper = config.CreateMapper();
IAnimalQueueModel dog = ... /* 上面的DogQueueModel */
var dogDto = mapper.Map<IAnimalDto>(dog);
Console.WriteLine($"{dog.Name} ({dogDto.GetType().Name})"); // Max (DogDto)
---
**结论**
当引入新的动物类型时,您“*只需*”添加`JsonDerivedType`属性并设置必要的`AutoMapper`映射。
<details>
<summary>英文:</summary>
About **the JSON (de)serialization**  
Starting from version `7.0.0` `System.Text.Json` supports [polymorphic serialization](https://learn.microsoft.com/en-us/dotnet/standard/serialization/system-text-json/polymorphism).  
(This version also runs on `.NET 6`.)
In a nutshell, the JSON payload contains a (configurable) discriminator property.  
Below JSON shows an example for a dog and a cat.
{ &quot;type&quot; : &quot;dog&quot;, &quot;name&quot; : &quot;Max&quot;, &quot;color&quot;: &quot;gold&quot; }
{ &quot;type&quot; : &quot;cat&quot;, &quot;name&quot; : &quot;Felix&quot;, &quot;favoriteFood&quot;: &quot;fish&quot; }
The documentation has the following important note about this discriminator.
&gt; The type discriminator must be placed at the start of the JSON object
The `JsonSerializer` takes care of that.
Next you define an interface (or base class) that all concrete models implement.   
The applied [`JsonPolymorphic`](https://learn.microsoft.com/en-us/dotnet/api/system.text.json.serialization.jsonpolymorphicattribute) attribute defines the discriminator property, here `type`.
For each concrete type a [`JsonDerivedType`](https://learn.microsoft.com/en-us/dotnet/api/system.text.json.serialization.jsonderivedtypeattribute) attribute gets applied defining that concrete type and the corresponding value for the discriminator.
In below example the `CatQueueModel` gets linked to the type `cat` and `DogQueueModel` to `dog`.
[JsonPolymorphic(TypeDiscriminatorPropertyName = &quot;type&quot;)]
[JsonDerivedType(typeof(CatQueueModel), &quot;cat&quot;)]
[JsonDerivedType(typeof(DogQueueModel), &quot;dog&quot;)]
public interface IAnimalQueueModel
{ 
public string Name { get; set; }
}
All concrete types implement that `IAnimalQueueModel` interface.  
Note that the `type` discriminator property doesn&#39;t need to be a property on the model itself.
public class CatQueueModel : IAnimalQueueModel
{
public string Name { get; set; }
public string FavoriteFood { get; set; }
}
public class DogQueueModel : IAnimalQueueModel
{
public string Name { get; set; }
public string Color { get; set; } 
}
When deserialing, specify the `IAnimalQueueModel` interface as `T` when calling `JsonSerializer.Deserialize&lt;T&gt;(...)`.
The following example deserializes given `JSON` into a `DogQueueModel` instance. It&#39;s the `type` in the `JSON` payload that decides over the concrete result type.
var options = new JsonSerializerOptions
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
};
var dog = JsonSerializer.Deserialize&lt;IAnimalQueueModel&gt;(
@&quot;{ &quot;&quot;type&quot;&quot;:&quot;&quot;dog&quot;&quot;, &quot;&quot;name&quot;&quot;: &quot;&quot;Max&quot;&quot;, &quot;&quot;color&quot;&quot;: &quot;&quot;gold&quot;&quot; }&quot;,
options);
Console.WriteLine($&quot;{dog.Name} ({dog.GetType().Name})&quot;); // Max (DogQueueModel)
---
This polymorphic handling is not limited to the root model matching a `JSON` payload, it&#39;s also applicable for properties on a class.
Below example shows a model with an `Animal` property, which will hold a `CatQueueModel` instance after deserialization of given `JSON` string.
public class Data
{
public IAnimalQueueModel Animal { get; set; }
public string Code { get; set; }
}
var data = JsonSerializer.Deserialize&lt;Data&gt;(
@&quot;{
&quot;&quot;code&quot;&quot;: &quot;&quot;foo&quot;&quot;,
&quot;&quot;animal&quot;&quot;: { &quot;&quot;type&quot;&quot;:&quot;&quot;cat&quot;&quot;, &quot;&quot;name&quot;&quot;: &quot;&quot;Felix&quot;&quot;, &quot;&quot;favoriteFood&quot;&quot;: &quot;&quot;fish&quot;&quot; }
}&quot;, 
options
);
---
In case an attribute based setup is not practical, then you can configure the same with the [contract model](https://learn.microsoft.com/en-us/dotnet/standard/serialization/system-text-json/polymorphism?#configure-polymorphism-with-the-contract-model).
---
About **the mappings**
You might want to take a look at [`AutoMapper`](https://automapper.org) ([documentation](https://docs.automapper.org)).
With `AutoMapper` you define a mapping configuration that specifies which types - classes and interfaces - you want to map to other types, optionally including details about how properties should be handled.
public interface IAnimalDto
{
public string Name { get; set; }
}
public class CatDto : IAnimalDto
{ 
public string Name { get; set; }
public string FavoriteFood { get; set; }
}
public class DogDto : IAnimalDto
{
public string Name { get; set; }
public string Color { get; set; }
}
For a polymorphic mapping setup, you define the interface mapping (1st one in below example) and a mapping for each concrete type (2nd and 3rd in below code) .
Below example shows how to map classes implementing the `IAnimalQueueModel` interface to classes that implement the `IAnimalDto` interface to achieve the mapping of a `DogQueueModel` to a `DogDto` and a `CatQueueModel` to a `CatDto`.
var config = new MapperConfiguration(
cfg =&gt; 
{
cfg.CreateMap&lt;IAnimalQueueModel, IAnimalDto&gt;();
cfg.CreateMap&lt;DogQueueModel, DogDto&gt;()
.IncludeBase&lt;IAnimalQueueModel, IAnimalDto&gt;();
cfg.CreateMap&lt;CatQueueModel, CatDto&gt;()
.IncludeBase&lt;IAnimalQueueModel, IAnimalDto&gt;();
});
To apply a mapping, you need an `IMapper` instance - dependency injection is possible. 
The code below shows how to map an `IAnimalQueueModel` instance being of concrete type `DogQueueModel` to an `IAnimalDto` type, which will result in an instance of type `DogDto`.
var mapper = config.CreateMapper();
IAnimalQueueModel dog = ... /* The DogQueueModel from above */
var dogDto = mapper.Map&lt;IAnimalDto&gt;(dog);
Console.WriteLine($&quot;{dog.Name} ({dogDto.GetType().Name})&quot;); // Max (DogDto)
---
**Conclusion**
When you introduce a new animal type you &quot;*just*&quot; have to add a `JsonDerivedType` attribute and set up the necessary `AutoMapper` mapping(s).
</details>
# 答案2
**得分**: 0
@Trinny,嗨 - 更详细地回复我的评论。
你的主要“抱怨”是必须添加一个实现`IAnimal`接口的动物类,然后进入`AnimalQueueModelConverter`(`write`方法)和`MapAnimal`方法并添加“if”/“switch”选项。
为了不需要这样做,我提出了两种解决方法。其中一种是使用这个:
```csharp
private static IList<Type> loadAllImplementingTypes(Type[] interfaces)
{
IList<Type> implementingTypes = new List<Type>();
// find all types
foreach (var interfaceType in interfaces)
foreach (var currentAsm in AppDomain.CurrentDomain.GetAssemblies())
try
{
foreach (var currentType in currentAsm.GetTypes())
if (interfaceType.IsAssignableFrom(currentType) && currentType.IsClass && !currentType.IsAbstract)
implementingTypes.Add(currentType);
}
catch { }
return implementingTypes;
}

你可以在这里找到 - https://stackoverflow.com/a/49006805/3271820 - 在两个需要循环遍历动物的方法中使用此列表。

通过使用这个,你永远不需要添加其他东西,只需要动物类。但是,要通过类型实例化动物,你必须使用System.Activator类(https://learn.microsoft.com/en-us/dotnet/api/system.activator?view=net-7.0)。我对这种方法不太喜欢(循环遍历类型也不是最好的工作)。

然而,在“工厂”方法中,你可以创建一个“工厂”类,该类将具有用于循环遍历的“if”/“switch”。或者更好的是,一个类型和委托的列表,你只需添加到其中:

public static class AnimalFactory
{
    public delegate TAnimal constructNew<TAnimal>() where TAnimal : IAnimal;

    public class Binding
    {
        public IAnimalDb whenDB;
        public IAnimalQueueModel whenView;
        public AnimalFactory.constructNew action;
    }

    public static List<AnimalFactory.Binding> animals = new List<AnimalFactory.Binding>();
}

然后,每次添加一个动物类时,只需将其添加到该列表中(在程序启动时),并使writeMapAnimal方法循环遍历该列表(根据需要查找whenDBwhenView),并调用动作委托以获取其新实例。然后,我会将属性映射和基础写入(你评论的部分)作为IAnimal接口要实现的方法并调用它们。

执行这些操作之一将允许你只需添加一个动物类,实现IAnimal接口,要么只需这样做,要么在启动时将该类添加到列表中。这将是你添加新动物时唯一需要做的事情,而不必查找所有基于动物类型条件的地方。

英文:

@Trinny, hi - following up on my comment in more detail.

Your main "complaint" is to have to add an animal class (that implements the IAnimal interface) and then go to the AnimalQueueModelConverter (write method) and the MapAnimal method and add the "if"/"switch" options.

To not need to do that i gave 2 approachs to solve it. One is to use this:

private static IList&lt;Type&gt; loadAllImplementingTypes(Type[] interfaces)
{
IList&lt;Type&gt; implementingTypes = new List&lt;Type&gt;();
// find all types
foreach (var interfaceType in interfaces)
foreach (var currentAsm in AppDomain.CurrentDomain.GetAssemblies())
try
{
foreach (var currentType in currentAsm.GetTypes())
if (interfaceType.IsAssignableFrom(currentType) &amp;&amp; currentType.IsClass &amp;&amp; !currentType.IsAbstract)
implementingTypes.Add(currentType);
}
catch { }
return implementingTypes;
}

As you can find here - https://stackoverflow.com/a/49006805/3271820 - use that list on both methods that need to cicle through the animals.

By using this you'll never need to add anthing else, but the animal class. But to instanciate the animal by it's type you'll have to use the System.Activator class (https://learn.microsoft.com/en-us/dotnet/api/system.activator?view=net-7.0). I dislike this approach a bit because of it (and cicling through the types is also not the best job to do).

In the "factory" approach however, you can make 1 "fatory" class, that will have the "if"/"switch" to cicle through. Or better yet, a list of type and delegate that you can just add to: something like this:

public static class AnimalFactory
{
public delegate TAnimal constructNew&lt;TAnimal&gt;() where TAnimal : IAnimal;
public class Binding
{
public IAnimalDb whenDB;
public IAnimalQueueModel whenView;
public AnimalFactory.constructNew action;
}
public static List&lt;AnimalFactory.Binding&gt; animals = new List&lt;AnimalFactory.Binding&gt;();
}

Then every time you add an animal class you just add it to that list (when the program starts) and have both the write and MapAnimal methods just cicle through that list (seeking for the whenDB or whenView as they need to) and calling the action delegate to get a new instance of it. Then i'd add the prop mapping and base writing (parts that you commented) as methods of the IAnimal interface to implement and call for then.

Doing any of those would allow you to just add an animal class, implement IAnimal and either do just that or add that class to a list on startup. That would be all you'll ever need to do when adding a new animal, and never having to find all the places where you have conditions based on animal types.

huangapple
  • 本文由 发表于 2023年2月24日 14:56:11
  • 转载请务必保留本文链接:https://go.coder-hub.com/75553420.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定