Dan7el
Dan7el

Reputation: 2047

Document Update Using Driver Ignoring _id

I'd like to be able to update a key/value pair within a Document in a Collection without regard to the _id. This SO response appears to provide a way to do what I want, but it requires a focus on the _id key.

Essentially, what can I do to get my code, as shown below, to work without adding the [BsonIgnoreExtraElements] attribute to the class?

Based on the code which I'll show below, I think I'm close, and I do have a possible workaround, using the attribute, which I'll also show. However, I'd like to do this without decorating the classes with any attributes if possible.

Why no _id? Personally, I find the _id field just gets in the way. It's not part of any of my classes, and MongoDB isn't relational, so I simply don't use it. I freely admit that I may be completely missing the intent and idea behind the use of the _id field, but I simply don't see a need for it in MongoDB. Nevertheless, if possible, I'd like to focus on working without a focus on the _id field.

With that said, I have methods for retrieving a single document or an entire collection along with inserting a single document or entire collection, generically, without regard to the _id field.

I found that the key to ignoring the _id in a "select" is to use a Projection. I'm not all that familiar with the driver, but here are the lines of code that do the magic:

public const string ELEMENT_ID = "_id";
ProjectionDefinition<T> projection = Builders<T>.Projection.Exclude(ELEMENT_ID);
IFindFluent<T, T> found = mongoCollection.Find(bsonDocument).Project<T>(projection);

For background info, here is a retrieval method, ignoring _id:

public T GetSingle<T>(string property, string value) where T : class, new()
        {
            T tObject = null;
            try
            {
                if (MongoContext.MongoClient != null && MongoContext.MongoDatabase != null)
                {
                    string className = typeof(T).ToString();
                    int lastPeriod = className.LastIndexOf('.');
                    int length = className.Length - lastPeriod;
                    className = className.Substring(lastPeriod + 1, length - 1);
                    if (!string.IsNullOrEmpty(className))
                    {
                        IMongoCollection<T> mongoCollection = MongoContext.MongoDatabase.GetCollection<T>(className);
                        if (mongoCollection != null)
                        {
                            BsonDocument bsonDocument = new BsonDocument();
                            ProjectionDefinition<T> projection = Builders<T>.Projection.Exclude(ELEMENT_ID);
                            PropertyInfo[] propertyInfo = typeof(T).GetProperties(BindingFlags.Instance | BindingFlags.Public);
                            if (propertyInfo != null && propertyInfo.Length > 0)
                            {
                                IEnumerable<PropertyInfo> piExisting = propertyInfo.Where(pi => pi.Name.Equals(property, StringComparison.CurrentCultureIgnoreCase));
                                if (piExisting != null && piExisting.Any())
                                {
                                    BsonValue bsonValue = BsonValue.Create(value);
                                    BsonElement bsonElement = new BsonElement(property, bsonValue);
                                    if (bsonElement != null)
                                    {
                                        bsonDocument.Add(bsonElement);
                                    }
                                    IFindFluent<T, T> found = mongoCollection.Find(bsonDocument).Project<T>(projection);
                                    if (found != null)
                                    {
                                        tObject = found.FirstOrDefault<T>();
                                    }
                                }
                            }
                        }
                    }
                }
            }
            catch (Exception ex)
            {
                Logger.WriteToLog(Logger.LoggerMessage(ex));
            }
            return tObject;
        }

Similar to other another SO response, I've tried to use FindOneAndUpdate, but I receive the following error:

Element '_id' does not match any field or property of class ClassThingy.Suffix.

If I could apply the Projection to the FindOneAndUpdate somehow, I think that might resolve my issue, but I'm not able to find a way to do that application.

Here is my code:

public T UpdateSingle<T>(T item, string property, object originalValue, object newValue) where T : class, new()
{
    string className = string.Empty;
    T updatedDocument = null;
    try
    {
        if (MongoContext.MongoClient != null && MongoContext.MongoDatabase != null)
        {
            className = ClassUtility.GetClassNameFromObject<T>(item);
            if (!string.IsNullOrEmpty(className))
            {
                IMongoCollection<T> mongoCollection = MongoContext.MongoDatabase.GetCollection<T>(className);
                if (mongoCollection != null)
                {
                    BsonDocument bsonDocument = new BsonDocument();
                    ProjectionDefinition<T> projection = Builders<T>.Projection.Exclude(ELEMENT_ID);
                    PropertyInfo[] propertyInfo = typeof(T).GetProperties(BindingFlags.Instance | BindingFlags.Public);
                    if (propertyInfo != null && propertyInfo.Length > 0)
                    {
                        IEnumerable<PropertyInfo> piExisting = propertyInfo.Where(pi => pi.Name.Equals(property, StringComparison.CurrentCultureIgnoreCase));
                        if (piExisting != null && piExisting.Any())
                        {
                            BsonValue bsonValue = BsonValue.Create(originalValue);
                            BsonElement bsonElement = new BsonElement(property, bsonValue);
                            if (bsonElement != null)
                            {
                                bsonDocument.Add(bsonElement);
                            }
                            IFindFluent<T, T> found = mongoCollection.Find(bsonDocument).Project<T>(projection);
                            if (found != null)
                            {
                                FilterDefinition<T> filterDefinition = Builders<T>.Filter.Eq(property, originalValue);
                                UpdateDefinition<T> updateDefinition = Builders<T>.Update.Set(property, newValue);
                                updatedDocument = mongoCollection.FindOneAndUpdate<T>(filterDefinition, updateDefinition);
                            }
                        }
                    }
                }
            }
        }
    }
    catch (Exception ex)
    {
        Logger.WriteToLog(Logger.LoggerMessage(ex));
    }
    return updatedDocument;
}

Interestingly enough, while the FindOneAndUpdate method actually appears to succeed and the "Suffix" collection does, in fact, get modified. Also, the return doesn't contain the modification. Instead, it's the "original" (when I use the workaround as shown below).

More Info:

Suffix Class:

public class Suffix
{
    public string Code { get; set; }
    public string Description { get; set; }
}

enter image description here

Suffix suffix = new Suffix();
MongoRepository.MongoRepository mongoRepository = new MongoRepository.MongoRepository("MyDataBase");
mongoRepository.UpdateSingle<Suffix>(suffix, "Description", "Jr", "Junior");

Workaround:

[BsonIgnoreExtraElements]
public class Suffix
{
    public string Code { get; set; }
    public string Description { get; set; }
}

But, much rather not use the attribute if at all possible.

Upvotes: 1

Views: 1322

Answers (1)

mickl
mickl

Reputation: 49945

One thing you're missing here is the third parameter of .FindOneAndUpdate() method which is of type FindOneAndUpdateOptions<T,T>. That's the place where you can define if you want to get document After or Before the modification. Before is the default value Moreover you can specify the projection and exclude _id property. Try:

FilterDefinition<T> filterDefinition = Builders<T>.Filter.Eq(property, originalValue);
UpdateDefinition<T> updateDefinition = Builders<T>.Update.Set(property, newValue);
ProjectionDefinition<T, T> projection = Builders<T>.Projection.Exclude("_id");
var options = new FindOneAndUpdateOptions<T>()
{
    Projection = projection,
    ReturnDocument = ReturnDocument.After
};
updatedDocument = mongoCollection.FindOneAndUpdate<T>(filterDefinition, updateDefinition, options);

Upvotes: 1

Related Questions