2

Sample input:

// remove sensitive data    
var user = db.Users.Where(x => x.Id == '123').ToList()
      .Omit("PasswordHash", "PasswordSalt", "SecretDataAsInteger");

Output:

{
  FirstName: "...",
  LastName: "...",
  PasswordHash: "", // default value instead of real value
  PasswordSalt: "",
  SeretDataAsInteger: 0
}

Similar to: https://lodash.com/docs/4.16.4#omit

Preferences:

  • I prefer solutions without selecting a new object and setting each property except the ones i want to omit i.e.

var user = ..Users.Select(x => new User {/* here I set each property */})

since this lead to update all reference, in case I have updated the User model properties later

  • Passing object in the omit function instead of properties name as string is also acceptable
amd
  • 20,637
  • 6
  • 49
  • 67
  • 1
    What exactly is the question? How to the method `Omit` could be implemented? – Codor Oct 14 '16 at 10:55
  • @JonSkeet I don't think the omit method modifies anything, I think it's basically "give me all the objects without these specified properties", useful for excluding password fields or sensitive data I suppose. – DavidG Oct 14 '16 at 11:02
  • @DavidG: It's not a matter of omitting the *objects*, but the *properties*, as far as I can tell... so yes, it could still be done as an equivalent to `Select`, but it's that rather than `Where`... – Jon Skeet Oct 14 '16 at 11:03
  • It sounds like you basically want something that will copy properties via reflection, but omit certain ones. http://stackoverflow.com/questions/1198886 looks relevant. – Jon Skeet Oct 14 '16 at 11:06
  • @hvd: Yes, I agree, I misread that part. So it *is* a reasonable thing to do with LINQ. A short but complete example would certainly have made it all clearer, IMO... (Will remove the comment.) – Jon Skeet Oct 14 '16 at 11:07
  • @JonSkeet I deleted my comment, because after you edited yours, your comment was perfectly clear. No need to delete yours. –  Oct 14 '16 at 11:09

3 Answers3

1

Below is a class that I have created for you that should using a list of provided Property names should set them to be the default value of the Property type. Using your example:

// remove sensitive data    
var user = db.Users.Where(x => x.Id == '123').ToList()
  .OmitProperties("PasswordHash", "PasswordSalt", "SecretDataAsInteger");

Remember to include System.Reflection.

public static class EnumerableExtensions {
        public static IEnumerable<T> OmitProperties<T>(this IEnumerable<T> enumerable, params string[] propertyNames) {
            PropertyInfo[] propertiesToOmit = typeof(T).GetProperties()
                .Where(p => propertyNames.Contains(p.Name))
                .ToArray();

            foreach (var item in enumerable) {
                foreach (var property in propertiesToOmit) {
                    property.SetValue(item, null);
                }

                yield return item;
            }
        }
    }
Luke
  • 554
  • 1
  • 5
  • 10
  • I didn't write high level C# for a while, and I want to ask: Isn't using reflection in this way extremely slow? e.g, if I'd write something like this instead `db.Users.Where(x => x.Id == 123).Select(x => new User { Name = x.Name, Id = x.Id })` - (manually omitting), wouldn't that be faster? (Although not answering the answer...) – Mark Segal Oct 14 '16 at 11:17
  • I would agree that manually omitting the Properties instead of using this would be quicker. However in creating a new instance of the `User` class every time arguably would take up more memory (in GC or object creation) and may cause it to run slower. Depends is the answer. – Luke Oct 14 '16 at 11:20
  • @LukeFisher Creating a new instance of the `User` class, or a new instance of an anonymous type, is cheap: it reduces the cost of the original `User` object, as that would become short-lived without any references to it. The GC is very good at getting rid of those. Certainly cheaper than using reflection for each individual user. –  Oct 14 '16 at 11:25
  • @hvd never wen't into .NET allocator's internals, but this sound plausible. Anyway, the fastest way would probably to manually loop over and set all needed properties, I guess. – Mark Segal Oct 14 '16 at 11:28
  • Based on the question, I think they will require to use the Reflection approach to setting the Properties to be 'default'. I assumed that they were looking to keep the existing `User` for whatever purpose and just 'nullify' properties that they specify. – Luke Oct 14 '16 at 11:33
  • @LukeFisher, yes u r right, but maybe I am a little biased on my question since I am getting from a JS background, maybe in C# this is not the best way to do it – amd Oct 18 '16 at 06:48
1

If you have control over the User class, you can add a constructor taking User. Make this constructor copy all the fields. You may trivially do so using reflection. If you implement this by dynamically generating a helper method that does the work you need, the helper method only needs to be generated once. Given this constructor, you can write

var user = db.Users.Where(x => x.Id == 123).AsEnumerable()
  .Select(x => new User(x) {
    PasswordHash = "",
    PasswordSalt = "",
    SecretDataAsInteger = 0
  }).ToList();

This keeps it type-safe: you cannot accidentally omit properties that User doesn't have (say, you attempt to omit PasswordSlat rather than PasswordSalt), and you cannot accidentally omit properties by setting them to a value that the property type doesn't support (say, attempting to omit SecretDataAsInteger by setting it to null), and attempts to do so anyway will be flagged by the compiler rather than blowing up at runtime.

  • thanks for mentioning this, but in my situation I prefer to not create a ctor just for this. – amd Oct 17 '16 at 08:14
  • There should be a whole new object not just a constructor if you are crossing a security boundary. – Stilgar Oct 17 '16 at 10:27
1

Here's an option that isn't Linq and actually updates the base objects. Might not be quite what you want, but it does the job:

public static IEnumerable<T> ApplyToAll<T>(this IEnumerable<T> sequence, 
    params Action<T>[] action)
{
    foreach (var element in sequence)
    {
        foreach (var action in actions)
        {
            omission(action);
        }
        yield return element;
    }
}

And instead of passing the string names of the properties, you pass in some actions that act on the input:

var redactedUsers = users.ApplyToAll(
    u => u.Password = "",
    u => u.Name = "Hello " + u.Name);
DavidG
  • 113,891
  • 12
  • 217
  • 223
  • thanks for mentioning this, However in this case I prefer to name it `Apply` or something else instead of `Omit` – amd Oct 17 '16 at 08:25
  • Actually you are right, a much better name would be that or perhaps `ApplyToAll` – DavidG Oct 17 '16 at 09:00