Como contornar o problema de referência circular com JSON e Entity

13

Eu tenho experimentado a criação de um site que aproveita o MVC com JSON para minha camada de apresentação e estrutura de entidade para modelo de dados / banco de dados. Meu problema entra em jogo ao serializar meus objetos Model em JSON.

Estou usando o primeiro método de código para criar meu banco de dados. Ao executar o método primeiro do código, um relacionamento um para muitos (pai / filho) exige que o filho tenha uma referência de volta ao pai. (Exemplo de código pode ser um erro de digitação, mas você obtém a imagem)

class parent
{
   public List<child> Children{get;set;}
   public int Id{get;set;}

}
class child
{
    public int ParentId{get;set;}
    [ForeignKey("ParentId")]
    public parent MyParent{get;set;}
    public string name{get;set;}
 }

Ao retornar um objeto "pai" por meio de um JsonResult, um erro de referência circular é gerado porque "filho" possui uma propriedade da classe pai.

Eu tentei o atributo ScriptIgnore, mas perdi a capacidade de examinar os objetos filho. Vou precisar exibir informações em uma visão pai-filho em algum momento.

Eu tentei criar classes base para pai e filho que não têm uma referência circular. Infelizmente, quando tento enviar baseParent e baseChild, eles são lidos pelo Analisador JSON como suas classes derivadas (tenho certeza de que esse conceito está me escapando).

Base.baseParent basep = (Base.baseParent)parent;
return Json(basep, JsonRequestBehavior.AllowGet);

A única solução que eu encontrei é criar modelos "View". Crio versões simples dos modelos de banco de dados que não incluem a referência à classe pai. Cada um desses modelos de exibição possui um método para retornar a Versão do Banco de Dados e um construtor que usa o modelo de banco de dados como parâmetro (viewmodel.name = databasemodel.name). Este método parece forçado, embora funcione.

NOTA: Estou postando aqui porque acho que isso vale mais para discussão. Eu poderia aproveitar um padrão de design diferente para superar esse problema ou poderia ser tão simples quanto usar um atributo diferente no meu modelo. Na minha pesquisa, não vi um bom método para superar esse problema.

Meu objetivo final seria ter um bom aplicativo MVC que aproveite bastante o JSON para se comunicar com o servidor e exibir dados. Mantendo um modelo consistente entre as camadas (ou da melhor maneira possível).

DanScan
fonte

Respostas:

6

Eu vejo dois assuntos distintos em sua pergunta:

  • Como gerenciar referências circulares ao serializar para JSON?
  • Quão seguro é usar entidades EF como entidades modelo em suas visualizações?

No que diz respeito às referências circulares, lamento dizer que não há uma solução simples. Primeiro porque o JSON não pode ser usado para representar referências circulares, o seguinte código:

var aParent = {Children : []}, aChild  = {Parent : aParent};
aParent.Children.push(aChild);
JSON.stringify(aParent);

Resulta em: TypeError: Converting circular structure to JSON

A única opção que você tem é manter apenas o componente composto -> parte da composição e descartar o componente "navegação posterior" -> composto, assim, no seu exemplo:

class parent
{
    public List<child> Children{get;set;}
    public int Id{get;set;}
}
class child
{
    public int ParentId{get;set;}
    [ForeignKey("ParentId"), ScriptIgnore]
    public parent MyParent{get;set;}
    public string name{get;set;}
}

Nada impede que você recomponha essa propriedade de navegação no lado do cliente, aqui usando o jQuery:

$.each(parent.Children, function(i, child) {
  child.Parent = parent;  
})

Mas você precisará descartá-lo novamente antes de enviá-lo de volta ao servidor, pois o JSON.stringify não poderá serializar a referência circular:

$.each(parent.Children, function(i, child) {
  delete child.Parent;  
})

Agora existe o problema de usar entidades EF como suas entidades de modelo de exibição.

Primeiro, é provável que o EF use Proxies dinâmicos da sua classe para implementar comportamentos como detecção de alterações ou carregamento lento, você precisará desativá-los se desejar serializar as entidades do EF.

Além disso, o uso de entidades EF na interface do usuário pode estar em risco, pois todo o fichário padrão mapeará todos os campos, da solicitação para os campos de entidades, incluindo aqueles que você não deseja que o usuário defina.

Portanto, se você deseja que o aplicativo MVC seja projetado adequadamente, recomendo o uso de um modelo de exibição dedicado para impedir que as "tripas" do seu modelo de negócios interno sejam expostas ao cliente, portanto, recomendamos um modelo de exibição específico.

Julien Ch.
fonte
Existe uma maneira elegante de técnicas orientadas a objetos que eu possa contornar tanto a referência circular quanto a questão da EF.
DanScan 27/03
Existe uma maneira elegante com técnicas orientadas a objetos que eu possa contornar tanto a referência circular quanto a questão da EF? Como BaseObject é herdado por entityObject e por viewObject. Então entityObject teria a referência circular, mas viewObject não teria a referência circular. Eu consegui contornar isso criando viewObject a partir de entityObject (viewObject.name = entityObject.name), mas isso parece ser uma perda de tempo. Como posso contornar esse problema?
DanScan 27/03
Eles você muito . Sua explicação foi muito clara e fácil de entender.
Nick
2

Uma alternativa mais simples para tentar serializar os objetos seria desativar a serialização de objetos pai / filho. Em vez disso, você pode fazer uma chamada separada para buscar os objetos pai / filho associados, conforme e quando precisar deles. Isso pode não ser ideal para o seu aplicativo, mas é uma opção.

Para fazer isso, você pode configurar um DataContractSerializer e configurar a propriedade DataContractSerializer.PreserveObjectReferences como 'false' no construtor de sua classe de modelo de dados. Isso especifica que as referências de objeto não devem ser preservadas na serialização das respostas HTTP.

Exemplos:

Formato Json:

var json = GlobalConfiguration.Configuration.Formatters.JsonFormatter;
json.SerializerSettings.PreserveReferencesHandling = 
    Newtonsoft.Json.PreserveReferencesHandling.None;

Formato XML:

var xml = GlobalConfiguration.Configuration.Formatters.XmlFormatter;
var dcs = new DataContractSerializer(typeof(Employee), null, int.MaxValue, 
    false, /* preserveObjectReferences: */ false, null);
xml.SetSerializer<Employee>(dcs);

Isso significa que, se você buscar um item que tenha objetos filhos mencionados, os objetos filhos não serão serializados.

Veja também a classe DataContractsSerializer .

Ciaran Gallagher
fonte
1

Serializador JSON que lida com referências circulares

Aqui está um exemplo de Jackson personalizado JSONSerializerque lida com referências circulares serializando a primeira ocorrência e armazenando um * referencena primeira ocorrência em todas as ocorrências subsequentes.

Lidando com referências circulares ao serializar objetos com Jackson

Snippet parcial relevante do artigo acima:

private final Set<ObjectName> seen;

/**
 * Serialize an ObjectName with all its attributes or only its String representation if it is a circular reference.
 * @param on ObjectName to serialize
 * @param jgen JsonGenerator to build the output
 * @param provider SerializerProvider
 * @throws IOException
 * @throws JsonProcessingException
 */
@Override
public void serialize(@Nonnull final ObjectName on, @Nonnull final JsonGenerator jgen, @Nonnull final SerializerProvider provider) throws IOException, JsonProcessingException
{
    if (this.seen.contains(on))
    {
        jgen.writeString(on.toString());
    }
    else
    {
        this.seen.add(on);
        jgen.writeStartObject();
        final List<MBeanAttributeInfo> ais = this.getAttributeInfos(on);
        for (final MBeanAttributeInfo ai : ais)
        {
            final Object attribute = this.getAttribute(on, ai.getName());
            jgen.writeObjectField(ai.getName(), attribute);
        }
        jgen.writeEndObject();
    }
}

fonte
0

A única solução que eu encontrei é criar modelos "View". Crio versões simples dos modelos de banco de dados que não incluem a referência à classe pai. Cada um desses modelos de exibição possui um método para retornar a Versão do Banco de Dados e um construtor que usa o modelo de banco de dados como parâmetro (viewmodel.name = databasemodel.name). Este método parece forçado, embora funcione.

Enviar o mínimo de dados é a única resposta correta. Quando você envia dados do banco de dados, geralmente não faz sentido enviar todas as colunas com todas as associações. Os consumidores não devem precisar lidar com associações e estruturas de banco de dados, ou seja, com bancos de dados. Isso não apenas economizará largura de banda, mas também será muito mais fácil manter, ler e consumir. Consulte os dados e modele-os para o que você realmente precisa enviar eq. o mínimo.

Dante
fonte
É necessário mais tempo de processamento quando se fala em big data, agora você precisa transformar tudo duas vezes.
David van Dugteren
-2

.Include(x => x.TableName ) não retornando relacionamentos (da tabela principal para a tabela dependente) ou retornando apenas uma linha de dados, corrija aqui:

/programming/43127957/include-not-working-in-net-core-returns-one-parent

Além disso, no Startup.cs, verifique se você tem isso na parte superior:

using Microsoft.EntityFrameworkCore; 
using Newtonsoft.Json; 
using Project_Name_Here.Models;
Joe Hoeller
fonte
filho wat? erm .. wat?
Amel 19/02