MongoDB C# UpdateBuilder not ignoring nulls on set - c#

I have the following two classes where a user sets preferences. The user may not have any preferences, only likes, only dislikes, or both. I slimmed down the User model for simplicity in this example.
[BsonIgnoreExtraElements]
public class User : Base
{
public string FirstName { get; set; }
public string LastName { get; set; }
[BsonIgnoreIfNull]
public UserPreferences Preferences { get; set; }
}
[BsonIgnoreExtraElements]
public class UserPreferences
{
[BsonIgnoreIfNull]
public List<string> Likes { get; set; }
[BsonIgnoreIfNull]
public List<string> Dislikes { get; set; }
}
I have a helper function which uses reflection to construct an UpdateBuilder. When given a user object it sets a value for non-null fields since I don't want to specifically write out which fields have been updated on a call. However the helper function fails in my current situation.
public override User Update(User model)
{
var builder = Builders<User>.Update.Set(x => x.Id, model.Id);
foreach(PropertyInfo prop in model.GetType().GetProperties())
{
var value = model.GetType().GetProperty(prop.Name).GetValue(model, null);
if ((prop.Name != "Id") & (value != null))
{
builder = builder.Set(prop.Name, value);
}
}
var filter = Builders<User>.Filter;
var filter_def = filter.Eq(x => x.Id, model.Id);
Connection.Update(filter_def, builder);
return model;
}
Problem: When supplying Preferences with only Likes or only Dislikes, it will make the other property null in MongoDB.
Desired Result: I want MongoDB to ignore either the Likes or Dislikes property if the list is null like it does for other properties in my code.

I think the best way is to unset the field if its value is null
public override User Update(User model)
{
var builder = Builders<User>.Update.Set(x => x.Id, model.Id);
foreach(PropertyInfo prop in model.GetType().GetProperties())
{
var value = model.GetType().GetProperty(prop.Name).GetValue(model, null);
if (prop.Name != "Id")
{
if(value != null)
{
builder = builder.Set(prop.Name, value);
}
else
{
builder = builder.Unset(prop.Name);
}
}
}
var filter = Builders<User>.Filter;
var filter_def = filter.Eq(x => x.Id, model.Id);
Connection.Update(filter_def, builder);
return model;
}
I hope this will solve your issue

Related

Doing an UpdateAsync in MongoDb

I have the below class structure. I'm trying to call UpdateAsync by passing only a part of the object. For some reason it is respecting the BsonIgnoreIfDefault only at the root object level TestObject class, but not on TestProduct.
public class TestObject
{
[BsonId]
[BsonRepresentation(BsonType.ObjectId)]
[BsonIgnoreIfDefault]
public string Id { get; set; }
[Required]
public string KoId { get; set; }
[BsonIgnoreIfDefault]
public string Summary { get; set; }
public TestProduct Product { get; set; }
}
public class TestProduct
{
[BsonIgnoreIfDefault]
public string Name { get; set; }
[BsonIgnoreIfDefault]
public List<string> Skus { get; set; }
}
Here's a snippet of my integration test:
public async Task EndToEndHappyPath()
{
const string summary = "This is a summary";
var obj = new TestObject
{
Summary = summary,
KoaId = "1234",
Product = new TestProduct
{
Name = "laptop",
Skus = new List<string>
{
"Memory"
}
}
};
// CREATE
await _mongoAsyncRepository.CreateAsync(obj);
obj = new TestObject
{
KoaId = koaId,
Description = description,
Product = new TestProduct
{
Skus = new List<string>
{
"RAM"
}
}
};
// UPDATE
var response = await _mongoAsyncRepository.UpdateAsync(koaId, obj);
response.ShouldBeTrue();
// RETRIEVE
result = await _mongoAsyncRepository.RetrieveOneAsync(koaId);
testObject = (result as TestObject);
testObject.Product.ShouldNotBeNull();
// this is failing; Name value is null in MongoDb
testObject.Product.Name.ShouldBe("laptop");
testObject.Product.Skus.ShouldNotBeNull();
testObject.Product.Skus.Count.ShouldBe(1);
testObject.Product.Skus[0].ShouldBe("RAM");
}
public async Task<bool> UpdateAsync(string id, T obj)
{
try
{
_logger.Log(new KoaLogEntry(KoaLogLevel.Debug, $"Attempting to update a {typeof(T)} {id} document."));
//var actionResult = await GetMongoCollection()?.ReplaceOneAsync(new BsonDocument("KoaId", id), obj);
var updated = new BsonDocument
{
{
"$set", bsonDoc
}
};
UpdateDefinition<BsonDocument> updatedObj = UpdateBuilder.DefinitionFor(updated);
var actionResult = await GetMongoCollection()?.UpdateOneAsync(new BsonDocument("KoaId", id), updated);
_logger.Log(new KoaLogEntry(KoaLogLevel.Debug, $"Updated a {typeof(T)} {id} document. IsAcknowledged = {actionResult.IsAcknowledged}; ModifiedCount = {actionResult.ModifiedCount}"));
return actionResult.IsAcknowledged
&& actionResult.ModifiedCount > 0;
}
catch (Exception exc)
{
_logger.Log(new KoaLogEntry(KoaLogLevel.Error, exc.Message, exc));
throw;
}
}
private readonly IMongoClient _client;
protected IMongoCollection<T> GetMongoCollection()
{
var database = _client.GetDatabase(this.DatabaseName);
return database.GetCollection<T>(typeof(T).Name);
}
For some reason Name is getting overwritten to null though I have put the BsonIgnoreIfDefault attribute on it.
Please let me know what I'm missing.
Thanks
Arun
I did some research and it seems that this is not supported out of the box.
BsonIgnoreIfDefault means "do not include in document in db if default" it does NOT mean "ignore in updates".
Your update command
var actionResult = await GetMongoCollection()?.UpdateOneAsync(new BsonDocument("KoaId", id), updated);
should have the same behavior as this:
await GetMongoCollection().ReplaceOneAsync(_ => _.KoaId == id, obj);
It will replace the the existing document.
The docs say (and I assume, that the c# driver does no magic):
If the document contains only field:value expressions, then:
The update() method replaces the matching document with the document. The update() method does not replace the _id value. For an example, see Replace All Fields.
https://docs.mongodb.com/manual/reference/method/db.collection.update/
So you're doing a replace and all properties having default values will not be written to new new document:
// document after replace without annotations (pseudocode, fragment only)
{
KoaId: "abc",
Summary: null
}
// with Summary annotated with BsonIgnoreIfDefault
{
KoaId: "abc"
}
The only solution I found, is to write a builder that creates UpdateDefinitions from an object and add custom attributes. This is my first version that may help as a start:
/// <summary>
/// Ignore property in updates build with UpdateBuilder.
/// </summary>
[AttributeUsage(AttributeTargets.Property)]
public class BsonUpdateIgnoreAttribute : Attribute
{
}
/// <summary>
/// Ignore this property in UpdateBuild if it's value is null
/// </summary>
[AttributeUsage(AttributeTargets.Property)]
public class BsonUpdateIgnoreIfNullAttribute : Attribute
{
}
public static class UpdateBuilder
{
public static UpdateDefinition<TDocument> DefinitionFor<TDocument>(TDocument document)
{
if (document == null) throw new ArgumentNullException(nameof(document));
var updates = _getUpdateDefinitions<TDocument>("", document);
return Builders<TDocument>.Update.Combine(updates);
}
private static IList<UpdateDefinition<TDocument>> _getUpdateDefinitions<TDocument>(string prefix, object root)
{
var properties = root.GetType().GetProperties();
return properties
.Where(p => p.GetCustomAttribute<BsonUpdateIgnoreAttribute>() == null)
.Where(p => p.GetCustomAttribute<BsonUpdateIgnoreIfNullAttribute>() == null || p.GetValue(root) != null)
.Select(p => _getUpdateDefinition<TDocument>(p, prefix, root)).ToList();
}
private static UpdateDefinition<TDocument> _getUpdateDefinition<TDocument>(PropertyInfo propertyInfo,
string prefix,
object obj)
{
if (propertyInfo.PropertyType.IsClass &&
!propertyInfo.PropertyType.Namespace.AsSpan().StartsWith("System") &&
propertyInfo.GetValue(obj) != null)
{
prefix = prefix + propertyInfo.Name + ".";
return Builders<TDocument>.Update.Combine(
_getUpdateDefinitions<TDocument>(prefix, propertyInfo.GetValue(obj)));
}
return Builders<TDocument>.Update.Set(prefix + propertyInfo.Name, propertyInfo.GetValue(obj));
}
}
Please not that this is not optimized for performance.
You can use it like so:
var updateDef = UpdateBuilder.DefinitionFor(updatedDocument);
await Collection.UpdateOneAsync(_ => _.Id == id, updateDef);

mvc 5 custom data annotations GetCustomAttributes not found results

I am implementing a validation for a group of fields, in my case one of these three fields is mandatory.
I am implementing a custom data annotation.
My metadata is the following. In my metadata I have put this data annotation which is the one I want to customize.
public class document
{
[RequireAtLeastOneOfGroupAttribute("group1")]
public long? idPersons { get; set; }
[RequireAtLeastOneOfGroupAttribute("group1")]
public int? idComp { get; set; }
[RequireAtLeastOneOfGroupAttribute("group1")]
public byte? idRegister { get; set; }
public bool other1{ get; set; }
.
public string otherx{ get; set; }
}
On the other hand I have created a class called RequireAtLeastOneOfGroupAttribute
with the following code.
[AttributeUsage(AttributeTargets.Property)]
public class RequireAtLeastOneOfGroupAttribute : ValidationAttribute, IClientValidatable
{
public string GroupName { get; private set; }
public RequireAtLeastOneOfGroupAttribute(string groupName)
{
ErrorMessage = string.Format("You must select at least one value from group \"{0}\"", groupName);
GroupName = groupName;
}
protected override ValidationResult IsValid(object value, ValidationContext validationContext)
{
foreach (var property in GetGroupProperties(validationContext.ObjectType))
{
var propertyValue = (bool)property.GetValue(validationContext.ObjectInstance, null);
if (propertyValue)
{
// at least one property is true in this group => the model is valid
return null;
}
}
return new ValidationResult(FormatErrorMessage(validationContext.DisplayName));
}
private IEnumerable<PropertyInfo> GetGroupProperties(Type type)
{
IEnumerable<PropertyInfo> query1 = type.GetProperties().Where(a => a.PropertyType == typeof(int?) || a.PropertyType == typeof(long?) || a.PropertyType == typeof(byte?));
var metadataType = type.GetCustomAttributes(typeof(MetadataTypeAttribute), true).OfType<MetadataTypeAttribute>().FirstOrDefault();
var metaData = (metadataType != null)
? ModelMetadataProviders.Current.GetMetadataForType(null, metadataType.MetadataClassType)
: ModelMetadataProviders.Current.GetMetadataForType(null, type);
var propertMetaData = metaData.Properties
.Where(e =>
{
var attribute = metaData.ModelType.GetProperty(e.PropertyName)
.GetCustomAttributes(typeof(RequireAtLeastOneOfGroupAttribute), false)
.FirstOrDefault() as RequireAtLeastOneOfGroupAttribute;
return attribute == null || attribute.GroupName == GroupName;
})
.ToList();
RequireAtLeastOneOfGroupAttribute MyAttribute =
(RequireAtLeastOneOfGroupAttribute)Attribute.GetCustomAttribute(type, typeof(RequireAtLeastOneOfGroupAttribute));
if (MyAttribute == null)
{
Console.WriteLine("The attribute was not found.");
}
else
{
// Get the Name value.
Console.WriteLine("The Name Attribute is: {0}.", MyAttribute.GroupName);
}
return consulta1;
}
public IEnumerable<ModelClientValidationRule> GetClientValidationRules(ModelMetadata metadata, ControllerContext context)
{
var groupProperties = GetGroupProperties(metadata.ContainerType).Select(p => p.Name);
var rule = new ModelClientValidationRule
{
ErrorMessage = this.ErrorMessage
};
rule.ValidationType = string.Format("group", GroupName.ToLower());
rule.ValidationParameters["propertynames"] = string.Join(",", groupProperties);
yield return rule;
}
}
The code works correctly when the form is called the GetClientValidationRules function is called.
My problem is in the MyAttribute query because it always returns null.
The GetCustomAttribute function never gets results.
I have tried different variants without success, it is as if that data annotation did not exist.
I just implemented the query with the view part and javascript, which is the following.
jQuery.validator.unobtrusive.adapters.add(
'group',
['propertynames'],
function (options) {
options.rules['group'] = options.params;
options.messages['group'] = options.message;
});
jQuery.validator.addMethod('group', function (value, element, params) {
var properties = params.propertynames.split(',');
var isValid = false;
for (var i = 0; i < properties.length; i++) {
var property = properties[i];
if ($('#' + property).is(':checked')) {
isValid = true;
break;
}
}
return isValid; }, '');
and the view
#Html.TextBoxFor(x=> x.document.idPersons, new { #class= "group1" })
#Html.TextBoxFor(x=> x.document.idComp, new { #class= "group1" })
#Html.TextBoxFor(x=> x.document.idRegister, new { #class= "group1" })
#Html.TextBoxFor(x=> x.document.other1)
#Html.TextBoxFor(x=> x.document.otherx)

C# Data Annotations - Set properties Model with Display name

I have a list with Strings of Display names of Model:
public class TVSystemViewData : BaseViewData
{
[Display(Name = "AccountId", Description = "")]
public String AccountId { get; set; }
[Display(Name = "AllocatedManagedMemory", Description = "")]
public String AllocatedManagedMemory { get; set; }
[Display(Name = "AllocatedPhysicalMemory", Description = "")]
public String AllocatedPhysicalMemory { get; set; }
[Display(Name = "AudioMute", Description = "")]
public String AudioMute { get; set; }
}
I need to set the properties with a foreach loop to add the values to my Model:
This is how get values from POST the application
var model.AccountId = shell.getParameter("AccountId")
var model.AllocatedManagedMemory = shell.getParameter("AllocatedManagedMemory");
The shell.GetParameter get the value from a POST.
this is how i want:
I have a a Method to get all Display attr
public List<String> GetTVSystemProperties()
{
return typeof(TVSystemViewData)
.GetProperties()
.SelectMany(x => x.GetCustomAttributes(typeof(DisplayAttribute), true) //select many because can have multiple attributes
.Select(e => ((DisplayAttribute)e))) //change type from generic attribute to DisplayAttribute
.Where(x => x != null).Select(x => x.Name) //select not null and take only name
.ToList();
}
My collection is a list of Strings
ex: collection {"AccountId","AllocatedManagedMemory"...}
My model is TVSystemViewData
foreach (item in collection)
{
if(item == modelProperty name){
// i don know how
model.property = shell.getParameter(item)
}
}
[UPDATED]
I am using this:
foreach (var property in UtilsHandler.getConfigAsList("sysDataSource"))
{
//Set Values to Model
try
{
model.GetType().GetProperty(property).SetValue(model, shell.getParameter(property), null);
}
catch (Exception)
{
Have issue with data types
}
}
I have issues with data types.
But i use one foreach loop.
Still looking for a best method
you need to make the class inherit from IEnumerator, or add a GetEnumerator method yourself.
var model = new TVSystemViewData();
foreach(var item in model)
{
item.AccountId = shell.getParameter("AccountId");
//item.AllocatedManagedMemory ...
}
Please refer to this post for more information : How to make the class as an IEnumerable in C#?
Check this article out: https://support.microsoft.com/en-us/help/322022/how-to-make-a-visual-c-class-usable-in-a-foreach-statement
//EDIT. Forgot this part :
List<TVSystemViewData> model;
[Display(Name = "AccountId", Description = "")]
public String AccountId { get; set; }
[Display(Name = "AllocatedManagedMemory", Description = "")]
public String AllocatedManagedMemory { get; set; }
[Display(Name = "AllocatedPhysicalMemory", Description = "")]
public String AllocatedPhysicalMemory { get; set; }
[Display(Name = "AudioMute", Description = "")]
public String AudioMute { get; set; }
public IEnumerator<TVSystemViewData> GetEnumerator()
{
foreach (var item in model)
{
yield return item;
}
}
EDIT According to your update question: I don't know if this is the way to go but it should work.
var model = new TVSystemViewData();
PropertyInfo[] properties = typeof(TVSystemViewData).GetProperties();
List<string> items = new List<string> { "AccountId", "AllocatedManagedMemory" }; //your collection of strings
foreach (var item in items)
{
foreach (var property in properties)
{
if (item == property.Name)
{
property.SetValue(model, shell.getParameter(item));
}
}
}

Create expression tree to assign to property of list

This has been plaguing me for days now....
If I have a list of my own object SearchResults and SearchResults contains multiple lists of objects, all of which have a match (bool) property, How can I recreate an expression tree to achieve the following:
//searchResults is a List<SearchResults>
searchResults[i].Comments = searchResults[i].Comments.Select(p1 =>
{
p1.Match = ListOfStringVariable.All(p2 =>
{
string value = (string)typeof(CommentData).GetProperty(propertyName).GetValue(p1);
return value.Contains(p2);
});
return p1;
}).OrderByDescending(x => x.Match);
....
public class SearchResults
{
public IEnumerable<CommentData> Comments { get; set; }
public IEnumerable<AdvisorData> Advisors { get; set; }
}
public class CommentData
{
public string CommentText { get; set; }
public bool Match { get; set; }
}
public class AdvisorData
{
public string FirstName { get; set; }
public string LastName { get; set; }
public bool Match { get; set; }
}
The expression tree is needed as I won't know the property at compile-time that needs to be assigned, whether it is Comments, Advisors, etc (As this is a simplification of a larger problem). The above example is just for Comments, so how could the same code be used to assign to Advisors as well without having a conditional block?
Many thanks
Update:
So far using reflection we have the below from StriplingWarrior
var searchResult = searchResults[i];
foreach (var srProperty in searchResultsProperties)
{
var collectionType = srProperty.PropertyType;
if(!collectionType.IsGenericType || collectionType.GetGenericTypeDefinition() != typeof(IEnumerable<>))
{
throw new InvalidOperationException("All SearchResults properties should be IEnumerable<Something>");
}
var itemType = collectionType.GetGenericArguments()[0];
var itemProperties = itemType.GetProperties().Where(p => p.Name != "Match");
var items = ((IEnumerable<IHaveMatchProperty>) srProperty.GetValue(searchResult))
// Materialize the enumerable, in case it's backed by something that
// would re-create objects each time it's iterated over.
.ToList();
foreach (var item in items)
{
var propertyValues = itemProperties.Select(p => (string)p.GetValue(item));
item.Match = propertyValues.Any(v => searchTerms.Any(v.Contains));
}
var orderedItems = items.OrderBy(i => i.Match);
srProperty.SetValue(srProperty, orderedItems);
}
However orderedItems is of type System.Linq.OrderedEnumerable<IHaveMatchProperty,bool> and needs to be cast to IEnumerable<AdvisorData>. The below throws error:
'System.Linq.Enumerable.CastIterator(System.Collections.IEnumerable)' is a 'method' but is used like a 'type'
var castMethod = typeof(Enumerable).GetMethod("Cast").MakeGenericMethod(new[] {propertyType});
var result = castMethod.Invoke(null, new[] { orderedItems });
where propertyType is type AdvisorData
First, make your types implement this interface so you don't have to do quite so much reflection:
public interface IHaveMatchProperty
{
bool Match { get; set; }
}
Then write code to do something like this. (I'm making a lot of assumptions because your question wasn't super clear on what your intended behavior is.)
var searchResult = searchResults[i];
foreach (var srProperty in searchResultsProperties)
{
var collectionType = srProperty.PropertyType;
if(!collectionType.IsGenericType || collectionType.GetGenericTypeDefinition() != typeof(IEnumerable<>))
{
throw new InvalidOperationException("All SearchResults properties should be IEnumerable<Something>");
}
var itemType = collectionType.GetGenericArguments()[0];
var itemProperties = itemType.GetProperties().Where(p => p.Name != "Match");
var items = ((IEnumerable<IHaveMatchProperty>) srProperty.GetValue(searchResult))
// Materialize the enumerable, in case it's backed by something that
// would re-create objects each time it's iterated over.
.ToList();
foreach (var item in items)
{
var propertyValues = itemProperties.Select(p => (string)p.GetValue(item));
item.Match = propertyValues.Any(v => searchTerms.Any(v.Contains));
}
var orderedItems = items.OrderBy(i => i.Match);
srProperty.SetValue(srProperty, orderedItems);
}

How to get a list of the field names in a table in EF 5

I have my entities defined as follows,
[Table("AuditZone")]
public class AuditZone
{
public AuditZone()
{
AuditZoneUploadedCOESDetails = new List<UploadedCOESDetails>();
AuditZonePostcode = new List<Postcodes>();
}
[Key]
[DoNotAudit]
public int Id { get; set; }
public string Description { get; set; }
public bool Valid { get; set; }
[DoNotAudit]
public DateTime CreatedDate { get; set; }
[DoNotAudit]
public int? CreatedBy { get; set; }
[DoNotAudit]
public DateTime? ModifiedDate { get; set; }
[DoNotAudit]
public int? ModifiedBy { get; set; }
public virtual UserProfile CreatedByUser { get; set; }
public virtual UserProfile ModifiedByUser { get; set; }
public virtual ICollection<UploadedCOESDetails> AuditZoneUploadedCOESDetails { get; set; }
public virtual ICollection<Postcodes> AuditZonePostcode { get; set; }
}
}
Some of the fields have an attribute called DoNotAudit.
I need to be able to get a list of the fields in the table that do not have that DoNotAudit attribute.
I tried the following, but it doesn't work. Any ideas ?
public IEnumerable<string> GetFields(string tableName)
{
var table = typeof(AISDbContext).GetProperties().Select(n => n.PropertyType) // here we get all properties of contect class
.Where(n => n.Name.Contains("DbSet") && n.IsGenericType) // here we select only DBSet collections
.Select(n => n.GetGenericArguments()[0])
.Where(n => n.Name == tableName);
var doNotAuditList = table.GetType()
.GetProperties()
.Where(p => p.GetCustomAttributes(typeof(DoNotAudit), false).Any())
.Select(p => p.Name)
.ToList();
return doNotAuditList;
}
Updated query
var doNotAuditList = table.First().GetProperties()
.Where(p=> p.PropertyType.FindInterfaces(new TypeFilter((t,o) => t == typeof(IEnumerable)), null).Length == 0)
.Where(n => n.GetCustomAttributes(true).OfType<DoNotAudit>().FirstOrDefault() == null)
.Select(p => p.Name)
.ToList();
table is already the type you need, so table.GetType() is not correct. Just use table.GetProperties() as below:
var doNotAuditList = table.First()
.GetProperties()
.Where(p => p.GetCustomAttributes(typeof(DoNotAudit), false).Any() == false
&& p.GetGetMethod().IsVirtual == false)
.Select(p => p.Name)
.ToList();
EDIT:
table is of type IQueriable<Type>, so a call to .First() is needed to get the actual object.
EDIT
Updated Linq query to ignore all virtual properties and properties marked DoNotAudit.
Reflection of the Type typeof(Poco).GetProperties() can lead to different results to what is actually tracked in EF. EF will ignore some types, some types may have Ignore annotations.
Complex Types need special attention.
If you want the EF view of the model, then access the MetadataWorkspace.
Some basic dump Metadata to debug console code...
[TestMethod]
public void EFToolsTest() {
// http://msdn.microsoft.com/en-us/library/system.data.metadata.edm.dataspace(v=vs.110).aspx
var context = new YourContext(); // DbContext type
ObjectContext objContext = ((IObjectContextAdapter)context).ObjectContext;
MetadataWorkspace workspace = objContext.MetadataWorkspace;
var xyz = workspace.GetItems<EntityType>(DataSpace.SSpace);
foreach (var ET in xyz) {
foreach (var sp in ET.Properties) {
Debug.WriteLine(sp.Name + ":" + sp.MaxLength);// just as an example
}
}
}
or via the DataSpace.OSpace
public static void DumpContextManagedTypeProps() {
var context = new YourContent();
ObjectContext objContext = ((IObjectContextAdapter)context).ObjectContext;
MetadataWorkspace workspace = objContext.MetadataWorkspace;
IEnumerable<EntityType> managedTypes = workspace.GetItems<EntityType>(DataSpace.OSpace);
foreach (var managedType in managedTypes.Where(mt=>mt.Ful) {
Console.WriteLine(managedType.FullName);
// propertyInfo and other useful info is available....
foreach ( var p in managedType.Properties) {
Console.WriteLine(p.Name );
}
}
return result;
}

Categories

Resources