empêcher la sérialisation d'une propriété dans l'API Web

174

J'utilise une API Web MVC 4 et des formulaires Web asp.net 4.0 pour créer une API de repos. Cela fonctionne très bien:

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

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

    return httpResponseMessage;
}

Maintenant, je dois empêcher certaines propriétés d'être sérialisées. Je sais que je peux utiliser du LINQ sur la liste et obtenir uniquement les propriétés dont j'ai besoin, et généralement c'est une bonne approche, mais dans le scénario actuel, lesomething objet est trop complexe, et j'ai besoin d'un ensemble différent de propriétés dans différentes méthodes, donc c'est plus facile à marquer, lors de l'exécution, chaque propriété à ignorer.

Y-a-t-il un moyen de faire ça?

user1330271
la source
Vous pouvez ajouter ScriptIgnore à la propriété. voir cette question stackoverflow.com/questions/10169648/…
atbebtg

Réponses:

232

L'API Web ASP.NET utilise Json.Netcomme formateur par défaut, donc si votre application utilise uniquement JSON comme format de données, vous pouvez utiliser [JsonIgnore]pour ignorer la propriété pour la sérialisation:

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

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

Mais cette méthode ne prend pas en charge le format XML. Ainsi, au cas où votre application doit davantage prendre en charge le format XML (ou uniquement prendre en charge XML), au lieu d'utiliser Json.Net, vous devez utiliser [DataContract]ce qui prend en charge à la fois JSON et 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; }
}

Pour plus de compréhension, vous pouvez lire l' article officiel .

cuongle
la source
Je pense que je devrai trouver un moyen d'ajouter et de supprimer des attributs au moment de l'exécution en utilisant jsonignore.
user1330271
TRAVAILLÉ COMME UN CHARME! MERCI :)
Paulo Rodrigues
Pourquoi est-il triste que l'attribut JsonIgnore ne soit pas pris en charge avec la réponse XML?
Mukus
Datacontract est une excellente solution. Cela me donne une API REST propre. En même temps, lorsque j'enregistre les données dans un no-sql, les propriétés ignorées sont conservées malgré le stockage des objets en tant que json.
FrankyHollywood
1
@FedorSteeman L'espace de noms de JsonIgnore est Newtonsoft.Json, nécessite le package JSON.Net-nuget. DataContract et DataMember -attributes, d'autre part, ont besoin de System.Runtime.Serialization-namespace (et d'une référence si elle est manquante)
Esko
113

Selon la page de documentation de l'API Web Sérialisation JSON et XML dans l'API Web ASP.NET pour empêcher explicitement la sérialisation sur une propriété, vous pouvez soit utiliser [JsonIgnore]pour le sérialiseur Json, soit[IgnoreDataMember] pour le sérialiseur XML par défaut.

Cependant, lors des tests, j'ai remarqué que cela [IgnoreDataMember]empêche la sérialisation pour les requêtes XML et Json, donc je recommanderais de l'utiliser plutôt que de décorer une propriété avec plusieurs attributs.

Michael Mason
la source
2
C'est la meilleure réponse. Il couvre XML et JSON avec un seul attribut.
Oliver
17
Malheureusement, [IgnoreDataMember]ne semble pas fonctionner avec les objets proxy EF 6 chargés paresseusement (propriétés virtuelles). [DataContract]et [DataMember]faire, cependant.
Nick
32

Au lieu de laisser tout être sérialisé par défaut, vous pouvez adopter l'approche «opt-in». Dans ce scénario, seules les propriétés que vous spécifiez sont autorisées à être sérialisées. Pour ce faire, utilisez DataContractAttributeet DataMemberAttribute, qui se trouve dans l' espace de noms System.Runtime.Serialization .

Le DataContactAttributeest appliqué à la classe et le DataMemberAttributeest appliqué à chaque membre que vous souhaitez sérialiser:

[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
}

Oserais-je dire que c'est une meilleure approche, car elle vous oblige à prendre des décisions explicites sur ce qui le fera ou ne le fera pas grâce à la sérialisation. Cela permet également à vos classes de modèles de vivre par elles-mêmes dans un projet, sans prendre de dépendance sur JSON.net simplement parce qu'ailleurs, vous les sérialisez avec JSON.net.

CBono
la source
2
La seule approche qui a fonctionné dès le départ avec .Net Core pour masquer les membres hérités. Fonctionne à la fois pour la sérialisation XML et Json. Kudos
Piou
J'ai besoin de la même fonctionnalité, mais les propriétés sont incluses ou exclues dépend de la méthode api invoquée, c'est-à-dire que des données différentes sont nécessaires pour différents appels API. Toutes suggestions
Nithin Chandran
Cela fonctionne très bien, mais mon principal problème est que ces configurations disparaissent avec chaque échafaudage dbcontext dans EF Core, est-ce que quelqu'un a une solution de contournement pour cela? Ces attributs pourraient-ils être dans une classe partielle ou être définis par programme?
Naner
20

Cela a fonctionné pour moi: créez un résolveur de contrat personnalisé qui a une propriété publique appelée AllowList de type tableau de chaînes. Dans votre action, modifiez cette propriété en fonction de ce que l'action doit renvoyer.

1. créez un résolveur de contrat personnalisé:

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. Utilisez le résolveur de contrat personnalisé en action

[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;
}

Cette approche m'a permis d'autoriser / interdire des demandes spécifiques au lieu de modifier la définition de classe. Et si vous n'avez pas besoin de la sérialisation XML, n'oubliez pas de la désactiver dans votre App_Start\WebApiConfig.csou votre API renverra des propriétés bloquées si le client demande xml au lieu de json.

//remove xml serialization
var appXmlType = config.Formatters.XmlFormatter.SupportedMediaTypes.FirstOrDefault(t => t.MediaType == "application/xml");
config.Formatters.XmlFormatter.SupportedMediaTypes.Remove(appXmlType);
joym8
la source
Quelque chose a dû changer avec les versions plus récentes, mais je n'ai pas pu faire fonctionner cela. Je pourrais le faire fonctionner en faisant "new" au lieu de "as" lors de la modification du résolveur. Le type JsonContractResolver n'est pas compatible pour une raison quelconque. Le problème avec le fait de faire du nouveau est qu'il l'écrase pour tous au lieu d'un seul.
Kalel Wade
J'ai réussi à faire fonctionner cela en utilisant la méthode Request.CreateResponse () qui reçoit un MediaTypeFormatter, comme ceci: var jsonMediaTypeFormatter = new JsonMediaTypeFormatter {SerializerSettings = new JsonSerializerSettings {ContractResolver = new PublicDomainJsonContractResolverOptIn {"AllowList = new string" Octets "," MimeType "," Largeur "," Hauteur "}}}}; return Request.CreateResponse (HttpStatusCode.OK, image, jsonMediaTypeFormatter);
Paul
Que faire si nous voulons également que les propriétés bloquées soient ignorées dans une réponse XML?
Carlos P
À moins que le résolveur de contrat de données ne soit attribué une fois par requête, ce n'est pas thread-safe. Je pense que cela est attribué une fois, dans la classe de démarrage.
Sprague
2
Pire encore, ive a testé cela et l'appel createproperties est mis en cache par le résolveur de contrat. Cette réponse est au mieux naïve, au pire dangereuse.
Sprague
19

Je vais vous montrer 2 façons d'accomplir ce que vous voulez:

Première façon: Décorez votre champ avec l'attribut JsonProperty afin d'ignorer la sérialisation de ce champ s'il est nul.

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

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

Deuxième méthode: si vous négociez avec certains scénarios complexes, vous pouvez utiliser la convention Web Api ("ShouldSerialize") afin de sauter la sérialisation de ce champ en fonction d'une logique spécifique.

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; 
    }
}

WebApi utilise JSON.Net et utilise la réflexion pour la sérialisation, donc quand il a détecté (par exemple) la méthode ShouldSerializeFieldX (), le champ avec le nom FieldX ne sera pas sérialisé.

renard
la source
Ce n'est pas fait par l'API Web, l'API Web utilise Json.NET par défaut pour la sérialisation. Ce processus est effectué par Json.NET et non par l'API Web
Hamid Pourjam
1
La deuxième solution est intéressante car elle permet de garder la technologie d'objet de domaine indépendante sans avoir besoin de réécrire les DTO juste pour masquer certains champs.
Raffaeu
17

Je suis en retard au jeu, mais un objet anonyme ferait l'affaire:

[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
la source
11

Essayez d'utiliser la IgnoreDataMemberpropriété

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

Presque identique à la réponse de greatbear302, mais je crée ContractResolver par requête.

1) Créer un ContractResolver personnalisé

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) Utilisez le résolveur de contrat personnalisé en action

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>"),
            }
        }
    });
}

Éditer:

Cela n'a pas fonctionné comme prévu (isoler le résolveur par requête). J'utiliserai des objets anonymes.

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
la source
4

Vous pourrez peut-être utiliser AutoMapper et utiliser le .Ignore()mappage, puis envoyer l'objet mappé

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

Fonctionne très bien en ajoutant simplement le: [IgnoreDataMember]

En plus du type de propriété, comme:

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

Cela fonctionne avec ApiController. Le code:

[Route("api/Context/UserSettings")]
    [HttpGet, HttpPost]
    public UserSettingsModel UserSettings()
    {
        return _contextService.GetUserSettings();
    }
Dannejaha
la source
Peut-être aussi une meilleure solution est-elle d'isoler les modèles de vue des modèles "Back-end", afin que vous puissiez ignorer cette déclaration. Je me trouve souvent mieux dans cette situation.
Dannejaha
0

Pour une raison quelconque, [IgnoreDataMember]cela ne fonctionne pas toujours pour moi, et je reçois parfois StackOverflowException(ou similaire). Donc, à la place (ou en plus), j'ai commencé à utiliser un modèle ressemblant à quelque chose comme ça lorsque je suis POSTentré dans Objectsmon 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);
}

Donc, fondamentalement, je passe dans un JObject et le convertis après qu'il a été reçu en évitant les problèmes causés par le sérialiseur intégré qui provoquent parfois une boucle infinie lors de l'analyse des objets.

Si quelqu'un connaît une raison pour laquelle c'est une mauvaise idée, faites-le moi savoir.

Il peut être intéressant de noter que c'est le code suivant pour une propriété de classe EntityFramework qui provoque le problème (si deux classes se réfèrent l'une à l'autre):

[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
la source