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);
Related
I have a Topic class:
public class Topic : BaseEntity
{
public string Name { get; set; }
public bool Active { get; set; }
public IList<Content>? Contents { get; set; }
}
And a Content class:
public class Content : BaseEntity
{
public string Name { get; set; }
public string URL { get; set; }
public string StartingVersion { get; set; }
public string EndingVersion { get; set; }
public string Summary { get; set; }
public bool Active { get; set; }
public IList<Topic> Topics { get; set; }
}
The BaseEntity looks like this:
public class BaseEntity
{
[Key]
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public int Id { get; set; }
public int CreatedBy { get; set; }
public DateTime CreatedDate { get; set; }
public int ModifiedBy { get; set; }
public DateTime ModifiedDate { get; set; }
}
My DataContext looks like this:
public DataContext(DbContextOptions<DataContext> options) : base(options) { }
private DbSet<Topic> Topics { get; set; }
private DbSet<Content> Contents { get; set; }
protected override void OnModelCreating(ModelBuilder builder)
{
base.OnModelCreating(builder);
}
And I'm trying to use a generic Repository. The saveEntity looks like this:
public async Task<T> SetEntity<T>(T entity) where T : BaseEntity
{
using (var scope = _serviceProvider.CreateScope())
{
entity.CreatedDate = DateTime.Now;
var _dbContext = scope.ServiceProvider.GetRequiredService<DataContext>();
_dbContext.Add(entity);
await _dbContext.SaveChangesAsync();
return entity;
}
}
And the Content Service method that does the creation of Contents looks like this:
public async Task<ContentDTO> AddContentAsync(ContentDTO content)
{
_modelHelper.ModelValidation(content);
await Checks(content, false);
foreach (var item in content.Topics)
{
Expression<Func<Topic, bool>> expTopic = i => i.Id == item.Id && i.Active == true;
var topic = await _dataRepository.GetEntityAsync(expTopic);
if (topic == null)
{
throw new KeyNotFoundException($"Topic with ID {item.Id} not found");
}
}
Content toSaveContent = Mapping.Mapper.Map<Content>(content);
toSaveContent.Active = true;
Content newContent = await _dataRepository.SetEntity(toSaveContent);
return Mapping.Mapper.Map<ContentDTO>(newContent);
}
My problem is that when I try to create a new Content EF fails to detect that the Topics included in the body of the Content are existing ones and tries to add them as new in the DB. Obviously, this raises a SQL exception saying I can't define the Id of the Topic.
What I'm missing??
Thank you for your help
EDIT:
Also tried to retrieve the Topics from context, but didn't work either:
public async Task<ContentDTO> AddContentAsync(ContentDTO content)
{
Expression<Func<Content, bool>> exp = i => i.URL == content.URL && i.Active == true;
if (_dataRepository.GetEntities(exp).Any())
{
throw new DuplicateWaitObjectException("Object already exist");
}
CheckObjectives(content.Objectives);
Content toSaveContent = Mapping.Mapper.Map<Content>(content);
_modelHelper.ModelValidation(toSaveContent);
toSaveContent.Active = true;
toSaveContent.Topics = new List<Topic>();
foreach (var item in content.Topics)
{
Expression<Func<Topic, bool>> expTopic = i => i.Id == item.Id && i.Active == true;
var topic = await _dataRepository.GetEntity(expTopic);
if(topic == null)
{
throw new KeyNotFoundException($"Topic with ID {item.Id} not found");
}
toSaveContent.Topics.Add(topic);
}
Content newContent = await _dataRepository.SetEntity(toSaveContent);
return Mapping.Mapper.Map<ContentDTO>(newContent);
}
EDIT2:
You are right, Guru Stron, I'll extract the GetEntity from the foreach and just take them all before.
This is my GetEntity method in the generic repository:
public async Task<T> GetEntity<T>(Expression<Func<T, bool>> predicate) where T : BaseEntity
{
using (var scope = _serviceProvider.CreateScope())
{
var _dbContext = scope.ServiceProvider.GetRequiredService<DataContext>();
return _dbContext.Set<T>().Where(predicate).FirstOrDefault();
}
}
EDIT3:
I'm sorry for the long delay. I'm not sure if this is a context issue. When I try to save a Content with the following JSON:
{
"name": "Style",
"url": "https://player.vimeo.com/video/41513143?h=6215248d63",
"startingVersion": "3.10.1",
"endingVersion": "3.10.1",
"summary": "This is a very nice content",
"topics": [
{
"id": 2,
"name": "NewTopic"
}
],
"objectives": [
{
"id": 1,
"index": 1,
"description": "This is a nice Objective"
}
]
}
I can see in the saving method of the repository that the Topic with ID 2 indeed exists:
It looks like the object Topic with Id 2 exists in the context but EF can't find it??
EDIT4: Edited for clarity
EDIT 5:
Tried to add the DataContext as Scoped in the ServiceCollection, and inject it in the Repository:
public static IServiceCollection AddDependencyInjectionConfiguration(this IServiceCollection services)
{
services.AddScoped<IDataRepository, DataRepository>();
services.AddScoped<DataContext>();
[...]
}
Used DI in the Repository and removed Scopes for using the DataContext:
[...]
public DataRepository(IServiceProvider serviceProvider, IHttpContextAccessor contextAccesor, DataContext dataContext)
{
_serviceProvider = serviceProvider;
_httpContextAccessor = contextAccesor;
_dbContext = dataContext;
}
[...]
public async Task<T> SetEntity<T>(T entity) where T : BaseEntity
{
entity.CreatedDate = DateTime.UtcNow;
entity.CreatedBy = _currentUserId;
_dbContext.Add(entity);
await _dbContext.SaveChangesAsync();
return entity;
}
[...]
And removed the Topic search in the service method to avoid the exception of "object already use in reading operation"
public async Task<ContentDTO> AddContentAsync(ContentDTO content)
{
_modelHelper.ModelValidation(content);
await Checks(content, false);
Content toSaveContent = Mapping.Mapper.Map<Content>(content);
toSaveContent.Active = true;
Content newContent = await _dataRepository.SetEntity(toSaveContent);
return Mapping.Mapper.Map<ContentDTO>(newContent);
}
But the result is still the same... EF is trying to save the Topic...
EDIT 6:
I tried to update Topics before saving the Content, but it is still trying to save the same Topic:
public async Task<ContentDTO> AddContentAsync(ContentDTO content)
{
await Checks(content, false);
Content toSaveContent = Mapping.Mapper.Map<Content>(content);
_modelHelper.ModelValidation(content);
toSaveContent.Active = true;
foreach (var item in content.Topics)
{
Topic? topic = await _dataRepository.GetEntityAsync<Topic>(x => x.Id == item.Id);
if (topic == null)
{
throw new KeyNotFoundException($"Topic with ID {item.Id} not found");
}
if (topic.Contents == null) {
topic.Contents = new List<Content>() { toSaveContent };
}
else {
topic.Contents.Add(toSaveContent);
}
await _dataRepository.UpdateEntityAsync(topic, topic.Id);
}
Content newContent = await _dataRepository.SetEntity(toSaveContent);
return Mapping.Mapper.Map<ContentDTO>(newContent);
}
EDIT 7:
As #rjs123431 suggested I cleared the Topics list of the Content object to save and stored the reference to the Content in the Topics and updated the objects.
public async Task<ContentDTO> AddContentAsync(ContentDTO content)
{
await Checks(content, false);
_modelHelper.ModelValidation(content);
Content toSaveContent = Mapping.Mapper.Map<Content>(content);
toSaveContent.Active = true;
toSaveContent.Topics = new List<Topic>();
List<Topic> topicsToSave = new List<Topic>();
foreach (var item in content.Topics)
{
Expression<Func<Topic, bool>> expTopic = i => i.Id == item.Id && i.Active == true;
var topic = await _dataRepository.GetEntityAsync(expTopic);
if (topic == null)
{
throw new KeyNotFoundException($"Topic with ID {item.Id} not found");
}else
{
if (topic.Contents == null)
topic.Contents = new List<Content>() { toSaveContent };
else
topic.Contents.Add(toSaveContent);
topicsToSave.Add(topic);
}
}
await _dataRepository.UpdateEntitiesAsync(topicsToSave);
Content newContent = await _dataRepository.SetEntity(toSaveContent);
return Mapping.Mapper.Map<ContentDTO>(newContent);
}
But with this code, the Content is saved, but in the ContentTopic table nothing is saved, therefore I lose the reference to the Topics.
EF Core uses concept of change tracking to manage data changes.
You should not create scope inside you generic repository (assuming you have default scoped context registration) - each scope will have it's own database context with it's own tracking, so the context which performs saving will have no idea about related entities and consider them as new ones (as you observe).
Usual approach is to have the outside control to control over scope, for example in ASP.NET Core the framework will create a scope on per request level and usually the dbcontext is shared on per request/scope basis.
So you need to remove the manual scope handling in the repository and use constructor injection so the repository shares the change tracking information between get and save queries, otherwise you will need to write some cumbersome code which will find and attach existing related entities in all the navigation properties of the saved entity.
Since you have a many-to-many relationship, and want to link your content to topics that are already save, you should not add topic to your content.
foreach (var item in content.Topics)
{
Expression<Func<Topic, bool>> expTopic = i => i.Id == item.Id && i.Active == true;
var topic = await _dataRepository.GetEntity(expTopic);
if(topic == null)
{
throw new KeyNotFoundException($"Topic with ID {item.Id} not found");
}
//toSaveContent.Topics.Add(topic); // no need for this line
}
Content newContent = await _dataRepository.SetEntity(toSaveContent);
Instead, after you save your content, loop through the topics and add the newly saved content to it and update the topic so content will be linked to that topic and vice versa.
Something like this:
foreach (var topic in content.Topics)
{
var topicEntity = await _topicRepository.GetAllIncluding(x => x.Contents)
.FirstOrDefaultAsync(x => x.Id == topic.Id);
if (topicEntity != null)
{
topicEntity.Contents.Add(content);
await _topicRepository.UpdateAsync(topicEntity);
}
}
Update 2:
You can even get the topic and add the content to it without having to save the content first. Content should have an empty topics of course.
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
For ICustomValidate in ASP.NET Boilerplate, we can validate the value for the field.
I am wondering whether it is able and recommended to check whether the added name of the Student already exists, in the ICustomValidate.
For example, when creating a new student, we will check whether the student with the same name already exists. Can we move this logic to ICustomValidate?
You can:
public class CreateStudentDto : ICustomValidate
{
public string Name { get; set; }
public void AddValidationErrors(CustomValidationContext context)
{
using (var scope = context.IocResolver.CreateScope())
{
using (var uow = scope.Resolve<IUnitOfWorkManager>().Begin())
{
var studentRepository = scope.Resolve<IRepository<Student, long>>();
var nameExists = studentRepository.GetAll()
.Where(s => s.Name == Name)
.Any();
if (nameExists)
{
var key = "A student with the same name already exists";
var errorMessage = context.Localize("sourceName", key);
var memberNames = new[] { nameof(Name) };
context.Results.Add(new ValidationResult(errorMessage, memberNames));
}
uow.Complete();
}
}
}
}
But such validation is usually done in a domain manager, e.g. AbpUserManager
Custom Validation in the DTO would be recommended for invariant conditions:
public class CreateTaskInput : ICustomValidate
{
public int? AssignedPersonId { get; set; }
public bool SendEmailToAssignedPerson { get; set; }
public void AddValidationErrors(CustomValidatationContext context)
{
if (SendEmailToAssignedPerson && (!AssignedPersonId.HasValue || AssignedPersonId.Value <= 0))
{
var errorMessage = "AssignedPersonId must be set if SendEmailToAssignedPerson is true!";
context.Results.Add(new ValidationResult(errorMessage));
}
}
}
I'm writing a simple messaging module so one process can publish messages and another can subscribe to them. I'm using EF/SqlServer as the out of process communication mechanism. A "Server" is just a name that a publisher/subscriber pair have in common (could have been called a "Channel").
I have the following method which adds a row to the database representing a named "Server"
public void AddServer(string name)
{
if (!context.Servers.Any(c => c.Name == name))
{
context.Servers.Add(new Server { Name = name });
}
}
The problem I'm having is that when I start two clients at the same time, only one is supposed to add a new Server entry, however, that is not how it's working out. I'm actually getting the very wrong result of two entries with the same name, and realizing that an Any() guard is not sufficient for this.
The Entity for Server uses an int PK and supposedly my repository would enforce the uniqueness of the Name field. I'm starting to think this isn't going to work though.
public class Server
{
public int Id { get; set; }
public string Name { get; set; }
}
The two ways I think I could fix this both seem less than ideal:
String primary keys
Ignoring Exception
This is the issue of concurrency, right?
How can I deal with it in this situation where I want two clients to call the repository with the same Name but get a result of only one row with that name in the database?
Update: Here is the Repository Code
namespace MyBus.Data
{
public class Repository : IDisposable
{
private readonly Context context;
private readonly bool autoSave;
public delegate Chain Chain(Action<Repository> action);
public static Chain Command(Action<Repository> action)
{
using (var repo = new Data.Repository(true))
{
action(repo);
}
return new Chain(next => Command(next));
}
public Repository(bool autoSave)
{
this.autoSave = autoSave;
context = new Context();
}
public void Dispose()
{
if (autoSave)
context.SaveChanges();
context.Dispose();
}
public void AddServer(string name)
{
if (!context.Servers.Any(c => c.Name == name))
{
context.Servers.Add(new Server { Name = name });
}
}
public void AddClient(string name, bool isPublisher)
{
if (!context.Clients.Any(c => c.Name == name))
{
context.Clients.Add(new Client
{
Name = name,
ClientType = isPublisher ? ClientType.Publisher : ClientType.Subscriber
});
}
}
public void AddMessageType<T>()
{
var typeName = typeof(T).FullName;
if (!context.MessageTypes.Any(c => c.Name == typeName))
{
context.MessageTypes.Add(new MessageType { Name = typeName });
}
}
public void AddRegistration<T>(string serverName, string clientName)
{
var server = context.Servers.Single(c => c.Name == serverName);
var client = context.Clients.Single(c => c.Name == clientName);
var messageType = context.MessageTypes.Single(c => c.Name == typeof(T).FullName);
if (!context.Registrations.Any(c =>
c.ServerId == server.Id &&
c.ClientId == client.Id &&
c.MessageTypeId == messageType.Id))
{
context.Registrations.Add(new Registration
{
Client = client,
Server = server,
MessageType = messageType
});
}
}
public void AddMessage<T>(T item, out int messageId)
{
var messageType = context.MessageTypes.Single(c => c.Name == typeof(T).FullName);
var serializer = new XmlSerializer(typeof(T));
var sb = new StringBuilder();
using (var sw = new StringWriter(sb))
{
serializer.Serialize(sw, item);
}
var message = new Message
{
MessageType = messageType,
Created = DateTime.UtcNow,
Data = sb.ToString()
};
context.Messages.Add(message);
context.SaveChanges();
messageId = message.Id;
}
public void CreateDeliveries<T>(int messageId, string serverName, string sendingClientName, T item)
{
var messageType = typeof(T).FullName;
var query = from reg in context.Registrations
where reg.Server.Name == serverName &&
reg.Client.ClientType == ClientType.Subscriber &&
reg.MessageType.Name == messageType
select new
{
reg.ClientId
};
var senderClientId = context.Clients.Single(c => c.Name == sendingClientName).Id;
foreach (var reg in query)
{
context.Deliveries.Add(new Delivery
{
SenderClientId = senderClientId,
ReceiverClientId = reg.ClientId,
MessageId = messageId,
Updated = DateTime.UtcNow,
DeliveryStatus = DeliveryStatus.Sent
});
}
}
public List<T> GetDeliveries<T>(string serverName, string clientName, out List<int> messageIds)
{
messageIds = new List<int>();
var messages = new List<T>();
var clientId = context.Clients.Single(c => c.Name == clientName).Id;
var query = from del in context.Deliveries
where del.ReceiverClientId == clientId &&
del.DeliveryStatus == DeliveryStatus.Sent
select new
{
del.Id,
del.Message.Data
};
foreach (var item in query)
{
var serializer = new XmlSerializer(typeof(T));
using (var sr = new StringReader(item.Data))
{
var t = (T)serializer.Deserialize(sr);
messages.Add(t);
messageIds.Add(item.Id);
}
}
return messages;
}
public void ConfirmDelivery(int deliveryId)
{
using (var context = new Context())
{
context.Deliveries.First(c => c.Id == deliveryId).DeliveryStatus = DeliveryStatus.Received;
context.SaveChanges();
}
}
}
}
You could keep the int primary key, but also define a unique index on the Name column.
This way, in concurrency situations only the first insert would be successful; any subsequent clients that attempt to insert the same server name would fail with an SqlException.
I'm currently using this solution:
public void AddServer(string name)
{
if (!context.Servers.Any(c => c.Name == name))
{
context.Database.ExecuteSqlCommand(#"MERGE Servers WITH (HOLDLOCK) AS T
USING (SELECT {0} AS Name) AS S
ON T.Name = S.Name
WHEN NOT MATCHED THEN
INSERT (Name) VALUES ({0});", name);
}
}
As an exercise in thoroughness I (think I) solved this problem another way, which preserves the type safety of the EF context but adds a bit of complexity:
First, this post, I learned how to add a unique constraint to the Server table:
Here's the Context code:
public class Context : DbContext
{
public DbSet<MessageType> MessageTypes { get; set; }
public DbSet<Message> Messages { get; set; }
public DbSet<Delivery> Deliveries { get; set; }
public DbSet<Client> Clients { get; set; }
public DbSet<Server> Servers { get; set; }
public DbSet<Registration> Registrations { get; set; }
public class Initializer : IDatabaseInitializer<Context>
{
public void InitializeDatabase(Context context)
{
if (context.Database.Exists() && !context.Database.CompatibleWithModel(false))
context.Database.Delete();
if (!context.Database.Exists())
{
context.Database.Create();
context.Database.ExecuteSqlCommand(
#"alter table Servers
add constraint UniqueServerName unique (Name)");
}
}
}
}
Now I need a way to selectively ignore exception when saving. I did this by adding the following members to my repository:
readonly List<Func<Exception, bool>> ExceptionsIgnoredOnSave =
new List<Func<Exception, bool>>();
static readonly Func<Exception, bool> UniqueConstraintViolation =
e => e.AnyMessageContains("Violation of UNIQUE KEY constraint");
Along with a new extension method to loop keep from depending on the position of the text in the inner exception chain:
public static class Ext
{
public static bool AnyMessageContains(this Exception ex, string text)
{
while (ex != null)
{
if(ex.Message.Contains(text))
return true;
ex = ex.InnerException;
}
return false;
}
}
And I modified the Dispose method of my Repository to check if the exception should be ignored or re-thrown:
public void Dispose()
{
if (autoSave)
{
try
{
context.SaveChanges();
}
catch (Exception ex)
{
if(!ExceptionsIgnoredOnSave.Any(pass => pass(ex)))
throw;
Console.WriteLine("ignoring exception..."); // temp
}
}
context.Dispose();
}
Finally, in the method which invokes the Add, I add the acceptable condition to the list:
public void AddServer(string name)
{
ExceptionsIgnoredOnSave.Add(UniqueConstraintViolation);
if (!context.Servers.Any(c => c.Name == name))
{
var server = context.Servers.Add(new Server { Name = name });
}
}
I have 2 classes:
public class Vote
{
public string VoteId { get; set; }
public string Question { get; set; }
public List<VoteAnswer> AnswerList { get; set; }
}
And:
public class VoteOption
{
public string OptionId { get; set; }
public string OptionName { get; set; }
public double VoteCount { get; set; }
}
How can i update/delete a VoteOption in a Vote where VoteId = voteId and OptionId = optionId? Using C# driver.
First I get VoteOption by:
var v = col.FindOneAs<Vote>(Query.EQ("VoteID", voteId));
VoteOption vo = v.AnswerList.Find(x => x.OptionId == optionId);
End set some value to it:
vo.OptionName = "some option chose";
vo.VoteCount = 1000;
But i don't know what next step to update this vo to Vote parent.
And, if i want to delete this vo, show me that way!
Data in MongoDB like that:
{
"_id" : "460b3a7ff100",
"Question" : "this is question?",
"AnswerList" : [{
"OptionId" : "1",
"OptionName" : "Option 1",
"VoteCount" : 0.0
}, {
"OptionId" : "2",
"OptionName" : "Option 2",
"VoteCount" : 0.0
}, {
"OptionId" : "3",
"OptionName" : "Option 3",
"VoteCount" : 0.0
}
}]
}
To update subdocument you can use this:
var update = Update.Set("AnswerList.$.OptionName", "new").Set("AnswerList.$.VoteCount", 5);
collection.Update(Query.And(Query.EQ("_id", new BsonObjectId("50f3c313f216ff18c01d1eb0")), Query.EQ("AnswerList.OptionId", "1")), update);
profiler:
"query" : { "_id" : ObjectId("50f3c313f216ff18c01d1eb0"), "AnswerList.OptionId" : "1" },
"updateobj" : { "$set" : { "AnswerList.$.OptionName" : "new", "AnswerList.$.VoteCount" : 5 } }
And to remove:
var pull = Update<Vote>.Pull(x => x.AnswerList, builder => builder.EQ(q => q.OptionId, "2"));
collection.Update(Query.And(Query.EQ("_id", new BsonObjectId("50f3c313f216ff18c01d1eb0")), Query.EQ("AnswerList.OptionId", "2")), pull);
profiler:
"query" : { "_id" : ObjectId("50f3c313f216ff18c01d1eb0"), "AnswerList.OptionId" : "2" },
"updateobj" : { "$pull" : { "AnswerList" : { "OptionId" : "2" } } }
Another way is to update parent document with modified child collection.
// Example function for update like count add like user using c#
public PostModel LikeComment(LikeModel like)
{
PostModel post = new PostModel();
_client = new MongoClient();
_database = _client.GetDatabase("post");
var collection = _database.GetCollection<PostModel>("post");
var _filter = Builders<PostModel>.Filter.And(
Builders<PostModel>.Filter.Where(x => x.PostId == like.PostId),
Builders<PostModel>.Filter.Eq("Comments.CommentId", like.CommentId));
var _currentLike = collection.Find(Builders<PostModel>.Filter.Eq("PostId", like.PostId)).FirstOrDefault().Comments.Find(f => f.CommentId == like.CommentId).Like;
var update = Builders<PostModel>.Update.Set("Comments.$.Like", _currentLike + 1);
collection.FindOneAndUpdate(_filter, update);
var addUser = Builders<PostModel>.Update.Push("Comments.$.LikeUsers", like.UserId);
collection.FindOneAndUpdate(_filter, addUser);
var _findResult = collection.Find(_filter).FirstOrDefault();
return _findResult;
}
//Delete comment
public PostModel delcomment(int postId, int commentId)
{
_client = new MongoClient();
_database = _client.GetDatabase("post");
var collection = _database.GetCollection<PostModel>("post");
var filter = Builders<PostModel>.Filter.Eq("PostId", postId);
var update = Builders<PostModel>.Update.PullFilter("Comments",
Builders<Comments>.Filter.Eq("CommentId", commentId));
collection.FindOneAndUpdate(filter, update);
var _findResult = collection.Find(filter).FirstOrDefault();
return _findResult;
}
Late answer but this is how you do it without having strings. If you modify your properties code will not compile. First time using expression tries in production code! They are awesome!
Models:
class Phone
{
public string _id { get; set; }
public string Name { get; set; }
public DateTime DateCreated { get; set; }
// Contain multiple lines as subdocument
public List<Line> Lines { get; set; }
}
class Line
{
public string Name { get; set; }
public string PhoneNumber { get; set; }
}
Code: this is how I create my update statements without depending on strings.
var update = new UpdateDocument<Phone>();
// set filter
update.SetFilter(x => x._id == "123456789");
update.AddValueToUpdate(p => p.Name, "New Name");
update.AddValueToUpdate(p => p.Lines[0].Name, "Line 1");
update.AddValueToUpdate(p => p.Lines[1].Name, "Line 2");
update.AddValueToUpdate(p => p.DateCreated, DateTime.UtcNow);
var updateQuery = update.Build();
This creates this! That is what you need to pass to mondo in order to do the update
{ "_id" : "123456789" },
{$set:
{"Name":"New Name","Lines.0.Name":"Line 1","Lines.1.Name":"Line 2","DateCreated":ISODate("2021-04-30T16:04:59.332Z")}
}
If you wish that code to work here are the helper classes:
using MongoDB.Bson;
using System.Linq.Expressions;
using MongoDB.Bson.Serialization;
class UpdateDocument<T>
{
/// <summary>
/// _id of document to update.
/// </summary>
private string _filter;
/// <summary>
/// Example:
/// FirstName, Antonio
/// Education.Elementary.Year, 2004
/// </summary>
private List<KeyValuePair<string, object>> _valuesToUpdate { get; set; } = new List<KeyValuePair<string, object>>();
public void SetFilter(Expression<Func<T, bool>> filterDefinition)
{
var documentSerializer = BsonSerializer.SerializerRegistry.GetSerializer<T>();
var where = Builders<T>.Filter.Where(filterDefinition).Render(documentSerializer, BsonSerializer.SerializerRegistry);
_filter = where.ToJson();
}
public void AddValueToUpdate(string name, object value)
{
_valuesToUpdate.Add(new KeyValuePair<string, object>(name, value));
}
public void AddValueToUpdate(Expression<Func<T, object>> name, object value)
{
var memberExpression = name.Body as MemberExpression;
if (memberExpression == null)
{
var unaryExpression = name.Body as UnaryExpression;
if (unaryExpression != null && unaryExpression.NodeType == ExpressionType.Convert)
memberExpression = unaryExpression.Operand as MemberExpression;
}
var result = memberExpression.ToString();
result = result.Substring(result.IndexOf('.') + 1);
if (result.Contains("get_Item"))
result = Regex.Replace(result, #"(?x) get_Item \( (\d+) \)", m => $"{m.Groups[1].Value}");
AddValueToUpdate(result, value);
}
public string Build()
{
if (_valuesToUpdate.Any() == false)
{
// nothing to update
return null;
}
/*
update({
_id: 7,
"comments._id": ObjectId("4da4e7d1590295d4eb81c0c7")
},{
$set: {"comments.$.type": abc}
}, false, true
);
*/
StringBuilder sb = new StringBuilder();
sb.Append(_filter);
sb.Append(',');
sb.Append("{");
{
sb.Append("$set:{");
foreach (var item in _valuesToUpdate)
{
sb.Append('"');
sb.Append(item.Key);
sb.Append('"');
sb.Append(':');
var value = BsonExtensionMethods.ToJson(item.Value);
sb.Append(value);
sb.Append(',');
}
// remove last comma
sb.Length--;
sb.Append('}');
}
sb.Append("}");
return sb.ToString();
}
}