impedir que a propriedade seja serializada na API da web

174

Estou usando uma API da web MVC 4 e formulários da web asp.net 4.0 para criar uma API restante. Está funcionando muito bem:

[HttpGet]
public HttpResponseMessage Me(string hash)
{
    HttpResponseMessage httpResponseMessage;
    List<Something> somethings = ...

    httpResponseMessage = Request.CreateResponse(HttpStatusCode.OK, 
                                 new { result = true, somethings = somethings });

    return httpResponseMessage;
}

Agora preciso impedir que algumas propriedades sejam serializadas. Sei que posso usar algum LINQ na lista e obter apenas as propriedades necessárias, e geralmente é uma boa abordagem, mas no cenário atual o somethingobjeto é muito complexo e preciso de um conjunto diferente de propriedades em métodos diferentes, por isso é mais fácil marcar, em tempo de execução, cada propriedade a ser ignorada.

Existe uma maneira de fazer isso?

user1330271
fonte
Você pode adicionar ScriptIgnore à propriedade veja esta pergunta stackoverflow.com/questions/10169648/…
atbebtg

Respostas:

232

A API da Web do ASP.NET usa Json.Netcomo formatador padrão; portanto, se seu aplicativo usar apenas JSON como formato de dados, você poderá usar [JsonIgnore]para ignorar a propriedade para serialização:

public class Foo
{
    public int Id { get; set; }
    public string Name { get; set; }

    [JsonIgnore]
    public List<Something> Somethings { get; set; }
}

Mas, dessa maneira, não suporta o formato XML. Portanto, caso seu aplicativo precise suportar mais o formato XML (ou apenas o XML), em vez de usar Json.Net, você deve usar o [DataContract]que suporta JSON e XML:

[DataContract]
public class Foo
{
    [DataMember]
    public int Id { get; set; }
    [DataMember]
    public string Name { get; set; }

    //Ignore by default
    public List<Something> Somethings { get; set; }
}

Para mais compreensão, você pode ler o artigo oficial .

cuongle
fonte
Eu acho que vou ter que encontrar uma maneira de adicionar e remover atributos em tempo de execução usando jsonignore.
user1330271
TRABALHOU COMO UM ENCANTO! OBRIGADO :)
Paulo Rodrigues
Por que é triste que o atributo JsonIgnore não seja suportado com resposta XML?
Mukus 8/16
Datacontract é uma ótima solução. Isso me dá uma API REST limpa. Ao mesmo tempo em que salvo os dados em um no-sql, as propriedades ignoradas são mantidas, apesar dos objetos serem armazenados como json.
FrankyHollywood
1
@FedorSteeman O namespace do JsonIgnore é Newtonsoft.Json, precisa do pacote JSON.Net-nuget. DataContract e DataMember -attributes por outro lado necessidade System.Runtime.Serialization-namespace (e referência se estiver faltando)
Esko
113

De acordo com a página de documentação da API da Web JSON e XML Serialization na API da Web do ASP.NET para impedir explicitamente a serialização em uma propriedade, você pode usar [JsonIgnore]o serializador Json ou [IgnoreDataMember]o serializador XML padrão.

No entanto, no teste, notei que isso [IgnoreDataMember]impede a serialização para solicitações XML e Json, portanto, recomendo usá-la em vez de decorar uma propriedade com vários atributos.

Michael Mason
fonte
2
Essa é a melhor resposta. Abrange XML e JSON com um atributo.
21413 Oliver
17
Infelizmente, [IgnoreDataMember]parece não funcionar com objetos de proxy EF 6 carregados com preguiça (propriedades virtuais). [DataContract]e [DataMember]faça, no entanto.
Nick
32

Em vez de permitir que tudo seja serializado por padrão, você pode adotar a abordagem "aceitar". Nesse cenário, apenas as propriedades que você especificar podem ser serializadas. Você faz isso com o DataContractAttributee DataMemberAttribute, encontrado no espaço para nome System.Runtime.Serialization .

O DataContactAttributeé aplicado à classe e o DataMemberAttributeé aplicado a cada membro que você deseja que seja serializado:

[DataContract]
public class MyClass {

  [DataMember]
  public int Id { get; set;} // Serialized

  [DataMember]
  public string Name { get; set; } // Serialized

  public string DontExposeMe { get; set; } // Will not be serialized
}

Ouso dizer que essa é uma abordagem melhor, porque força você a tomar decisões explícitas sobre o que a fará ou não por meio da serialização. Também permite que suas classes de modelo morem em um projeto por si mesmas, sem depender do JSON.net, apenas porque em outro lugar você as serializa com o JSON.net.

CBono
fonte
2
A única abordagem que funcionou imediatamente com o .Net Core para ocultar membros herdados. Funciona para serialização XML e Json. Kudos
Piou
Eu preciso da mesma funcionalidade, mas as propriedades são incluídas ou excluídas depende de qual método api é chamado, ou seja, dados diferentes são necessários para chamadas de API diferentes. Alguma sugestão
Nithin Chandran 15/02/19
Isso funciona muito bem, mas meu principal problema é que essas configurações desaparecem com todos os andaimes do dbcontext no EF Core. Alguém tem uma solução alternativa para isso? Esses atributos podem estar em uma classe parcial ou podem ser definidos programaticamente?
Naner 22/04/19
20

Isso funcionou para mim: Crie um resolvedor de contrato personalizado que tenha uma propriedade pública chamada AllowList do tipo de matriz de string. Em sua ação, modifique essa propriedade dependendo do que a ação precisa retornar.

1. crie um resolvedor de contrato personalizado:

public class PublicDomainJsonContractResolverOptIn : DefaultContractResolver
{
    public string[] AllowList { get; set; }

    protected override IList<JsonProperty> CreateProperties(Type type, MemberSerialization memberSerialization)
    {
        IList<JsonProperty> properties = base.CreateProperties(type, memberSerialization);

        properties = properties.Where(p => AllowList.Contains(p.PropertyName)).ToList();
        return properties;
    }
}

2. use o resolvedor de contrato personalizado em ação

[HttpGet]
public BinaryImage Single(int key)
{
    //limit properties that are sent on wire for this request specifically
    var contractResolver = Configuration.Formatters.JsonFormatter.SerializerSettings.ContractResolver as PublicDomainJsonContractResolverOptIn;
    if (contractResolver != null)
        contractResolver.AllowList = new string[] { "Id", "Bytes", "MimeType", "Width", "Height" };

    BinaryImage image = new BinaryImage { Id = 1 };
    //etc. etc.
    return image;
}

Essa abordagem me permitiu permitir / não permitir solicitações específicas em vez de modificar a definição de classe. E se você não precisar de serialização XML, não se esqueça de desativá-lo App_Start\WebApiConfig.csou sua API retornará propriedades bloqueadas se o cliente solicitar xml em vez de json.

//remove xml serialization
var appXmlType = config.Formatters.XmlFormatter.SupportedMediaTypes.FirstOrDefault(t => t.MediaType == "application/xml");
config.Formatters.XmlFormatter.SupportedMediaTypes.Remove(appXmlType);
joym8
fonte
Algo deve ter mudado com as versões mais recentes, mas não consegui fazer isso funcionar. Eu poderia fazê-lo funcionar 'novo' em vez de 'como' ao modificar o resolvedor. O tipo JsonContractResolver não é compatível por algum motivo. O problema de fazer novo é que ele o substitui para todos, em vez de apenas um.
Kalel Wade
Consegui fazer isso funcionar usando o método Request.CreateResponse () que recebe um MediaTypeFormatter, assim: var jsonMediaTypeFormatter = new JsonMediaTypeFormatter {SerializerSettings = new JsonSerializerSettings {ContractResolver = new PublicDomainJsonContractResolverOptIn {"AllowList = string" Bytes "," MimeType "," Largura "," Altura "}}}}; return Request.CreateResponse (HttpStatusCode.OK, image, jsonMediaTypeFormatter);
Paul
E se também queremos que as propriedades bloqueadas sejam ignoradas em uma resposta XML?
Carlos P
A menos que o resolvedor do contrato de dados seja designado uma vez por solicitação, isso não é seguro para threads. Eu acho que isso é atribuído uma vez, na classe de inicialização.
Sprague
2
Pior ainda, eu testei isso e a chamada createproperties é armazenada em cache pelo resolvedor de contratos. Essa resposta é ingênua na melhor das hipóteses, perigosa na pior.
Sprague
19

Mostrarei duas maneiras de realizar o que você deseja:

Primeira maneira: Decore seu campo com o atributo JsonProperty para ignorar a serialização desse campo, se ele for nulo.

public class Foo
{
    public int Id { get; set; }
    public string Name { get; set; }

    [JsonProperty(NullValueHandling = NullValueHandling.Ignore)]
    public List<Something> Somethings { get; set; }
}

Segunda maneira: se você estiver negociando com alguns cenários complexos, poderá usar a convenção de API da Web ("ShouldSerialize") para ignorar a serialização desse campo, dependendo de alguma lógica específica.

public class Foo
{
    public int Id { get; set; }
    public string Name { get; set; }

    public List<Something> Somethings { get; set; }

    public bool ShouldSerializeSomethings() {
         var resultOfSomeLogic = false;
         return resultOfSomeLogic; 
    }
}

O WebApi usa JSON.Net e usa reflexão para serialização; assim, quando detectar (por exemplo) o método ShouldSerializeFieldX (), o campo com o nome FieldX não será serializado.

foxhard
fonte
Isso não é feito pela API da Web, a API da Web usa o Json.NET por padrão para serializar. Este processo é feito por Json.NET não web api
Hamid Pourjam
1
A segunda solução é boa porque permite manter a tecnologia de objetos do Domínio independente, sem a necessidade de reescrever DTOs apenas para ocultar alguns campos.
Raffaeu 22/06
17

Estou atrasado para o jogo, mas um objeto anônimo faria o truque:

[HttpGet]
public HttpResponseMessage Me(string hash)
{
    HttpResponseMessage httpResponseMessage;
    List<Something> somethings = ...

    var returnObjects = somethings.Select(x => new {
        Id = x.Id,
        OtherField = x.OtherField
    });

    httpResponseMessage = Request.CreateResponse(HttpStatusCode.OK, 
                                 new { result = true, somethings = returnObjects });

    return httpResponseMessage;
}
Tim Hoolihan
fonte
11

Tente usar a IgnoreDataMemberpropriedade

public class Foo
    {
        [IgnoreDataMember]
        public int Id { get; set; }
        public string Name { get; set; }
    }
Kavi
fonte
5

Quase o mesmo que a resposta de greatbear302, mas eu crio ContractResolver por solicitação.

1) Crie um ContractResolver personalizado

public class MyJsonContractResolver : DefaultContractResolver
{
    public List<Tuple<string, string>> ExcludeProperties { get; set; }

    protected override JsonProperty CreateProperty(MemberInfo member, MemberSerialization memberSerialization)
    {
        JsonProperty property = base.CreateProperty(member, memberSerialization);

        if (ExcludeProperties?.FirstOrDefault(
            s => s.Item2 == member.Name && s.Item1 == member.DeclaringType.Name) != null)
        {
            property.ShouldSerialize = instance => { return false; };
        }

        return property;
    }
}

2) Use o resolvedor de contrato personalizado em ação

public async Task<IActionResult> Sites()
{
    var items = await db.Sites.GetManyAsync();

    return Json(items.ToList(), new JsonSerializerSettings
    {
        ContractResolver = new MyJsonContractResolver()
        {
            ExcludeProperties = new List<Tuple<string, string>>
            {
                Tuple.Create("Site", "Name"),
                Tuple.Create("<TypeName>", "<MemberName>"),
            }
        }
    });
}

Editar:

Não funcionou como esperado (isolar o resolvedor por solicitação). Vou usar objetos anônimos.

public async Task<IActionResult> Sites()
{
    var items = await db.Sites.GetManyAsync();

    return Json(items.Select(s => new
    {
        s.ID,
        s.DisplayName,
        s.Url,
        UrlAlias = s.Url,
        NestedItems = s.NestedItems.Select(ni => new
        {
            ni.Name,
            ni.OrdeIndex,
            ni.Enabled,
        }),
    }));
}
tsu1980
fonte
4

Você pode usar o AutoMapper e usar o .Ignore()mapeamento e, em seguida, enviar o objeto mapeado

CreateMap<Foo, Foo>().ForMember(x => x.Bar, opt => opt.Ignore());
Kenwarner
fonte
3

Funciona bem apenas adicionando o: [IgnoreDataMember]

No topo do propertyp, como:

public class UserSettingsModel
{
    public string UserName { get; set; }
    [IgnoreDataMember]
    public DateTime Created { get; set; }
}

Isso funciona com o ApiController. O código:

[Route("api/Context/UserSettings")]
    [HttpGet, HttpPost]
    public UserSettingsModel UserSettings()
    {
        return _contextService.GetUserSettings();
    }
Dannejaha
fonte
Talvez uma solução melhor seja isolar os modelos de exibição dos modelos "back-end", para que você possa pular esta declaração. Eu me encontro melhor nessa situação frequentemente.
Dannejaha
0

Por alguma razão [IgnoreDataMember], nem sempre funciona para mim, e às vezes recebo StackOverflowException(ou similar). Então, em vez disso (ou além disso), comecei a usar um padrão parecido com este quando POSTacessando Objectsminha API:

[Route("api/myroute")]
[AcceptVerbs("POST")]
public IHttpActionResult PostMyObject(JObject myObject)
{
    MyObject myObjectConverted = myObject.ToObject<MyObject>();

    //Do some stuff with the object

    return Ok(myObjectConverted);
}

Então, basicamente eu passo um JObjecte o converto depois de ter sido recuperado para aviod problemas causados ​​pelo serializador interno que às vezes causam um loop infinito ao analisar os objetos.

Se alguém souber por que isso é uma má ideia, entre em contato.

Vale a pena notar que é o código a seguir para uma propriedade de classe EntityFramework que causa o problema (se duas classes se referirem uma à outra):

[Serializable]
public partial class MyObject
{
   [IgnoreDataMember]
   public MyOtherObject MyOtherObject => MyOtherObject.GetById(MyOtherObjectId);
}

[Serializable]
public partial class MyOtherObject
{
   [IgnoreDataMember]
   public List<MyObject> MyObjects => MyObject.GetByMyOtherObjectId(Id);
}
Arg0n
fonte