EF 6 not detecting existing subclass when creating a new parent class - c#

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.

Related

How to load a related entity after call AddAsync without making another roundtrip to the database

How to load a related entity after calling AddAsync?
I have a repository method that looks like this
public virtual async Task<TEntity> AddAsync(TEntity entity)
{
if (entity == null)
throw new ArgumentNullException(nameof(entity));
try
{
entity.CreatedOn = entity.UpdatedOn = DateTime.Now;
var newEntity = await Entities.AddAsync(entity);
var newEntityToRet = newEntity.Entity;
_context.SaveChanges();
return newEntityToRet;
}
catch (DbUpdateException exception)
{
//ensure that the detailed error text is saved in the Log
throw new Exception(GetFullErrorTextAndRollbackEntityChanges(exception), exception);
}
}
Trying to insert an Order for example that looks like this, and only passing the StatusId and the TradingActionId makes the Add safe
public class Order
{
public int Id { get; set; }
public bool IsDeleted { get; set; }
public string CreatedBy { get; set; }
public string UpdatedBy { get; set; }
public DateTime? CreatedOn { get; set; }
public DateTime? UpdatedOn { get; set; }
public string Symbol { get; set; }
public int Quantity { get; set; }
public decimal Price { get; set; }
public int StatusId { get; set; }
public OrderStatus Status { get; set; }
public int TradingActionId { get; set; }
public TradingAction TradingAction { get; set; }
public string Notes { get; set; }
}
var order = new Order
{
TradingActionId = 1,
StatusId = 1,
Notes = source.Notes,
Price = source.Price,
Symbol = source.Symbol,
Quantity = source.Quantity,
CreatedOn = dateTimeNow,
UpdatedOn = dateTimeNow,
UpdatedBy = "test",
CreatedBy = "test"
};
The problem with this is that if I need to return the new entity with certain navigation properties. My following approach doesn't work but shows the idea of what I need to save the instance and at the same time return the child properties.
public virtual async Task<TEntity> AddAsync(TEntity entity, string[] include = null)
{
if (entity == null)
throw new ArgumentNullException(nameof(entity));
try
{
entity.CreatedOn = entity.UpdatedOn = DateTime.Now;
var newEntity = await Entities.AddAsync(entity);
var newEntityToRet = newEntity.Entity;
_context.SaveChanges();
if(include != null)
{
foreach (var navProp in include)
{
try
{
var memberEntry = _context.Entry(newEntityToRet).Member(navProp);
}
catch (Exception e)
{
Console.WriteLine(e);
throw;
}
if (memberEntry is DbCollectionEntry collectionMember)
collectionMember.Load();
if (memberEntry is DbReferenceEntry referenceMember)
referenceMember.Load();
}
}
return newEntityToRet;
}
catch (DbUpdateException exception)
{
//ensure that the detailed error text is saved in the Log
throw new Exception(GetFullErrorTextAndRollbackEntityChanges(exception), exception);
}
}
The version I am using of EF Core is Microsoft.EntityFrameworkCore.SqlServer 3.1.4
Any suggestions of how to create the generic repo method and return the data needed without making another roundtrip to the database?
After inserting a new entity you can always load its related entities explicitly with the Load() or LoadAsync() method. But even though it doesn't execute a typical LINQ method/query (like the one you would write to fetch some related data), EF needs to submit new query to the database for each explicit loading. Therefore, it really doesn't save you any database trip.
Since you are already trying to use the Load() method in your code, I'm assuming your intention is just to avoid writing the new LINQ queries (required to fetch the related data), and not to avoid a database trip. If that is the case, you can try something like bellow -
public async Task<TEntity> AddAsync(TEntity entity, params string[] includes)
{
entity.CreatedOn = entity.UpdatedOn = DateTime.Now;
var newEntry = Entities.Add(entity);
await _context.SaveChangesAsync(); // trip to database
foreach (var navProp in includes)
{
if (newEntry.Navigation(navProp).Metadata.IsCollection())
{
await newEntry.Collection(navProp).LoadAsync(); // trip to database
}
else
{
await newEntry.Reference(navProp).LoadAsync(); // trip to database
}
}
return newEntry.Entity;
}
which you can use like -
var addedOrder = await orderRepository.AddAsync(order, "Status", "TradingAction");
Notice -
I'm using params to pass one or more parameters
To add/insert a new entity use the Add or Attach method. Unless you are dealing with value generators like SequenceHiLo, you really don't need to use the AddAsync method. For details - AddAsync<TEntity>
A type-safe implementation would be -
public async Task<TEntity> AddAsync(TEntity entity, params Expression<Func<TEntity, object>>[] includes)
{
entity.CreatedOn = entity.UpdatedOn = DateTime.Now;
var newEntry = Entities.Add(entity);
await _context.SaveChangesAsync(); // trip to database
foreach (var navProp in includes)
{
string propertyName = navProp.GetPropertyAccess().Name;
if (newEntry.Navigation(propertyName).Metadata.IsCollection())
{
await newEntry.Collection(propertyName).LoadAsync(); // trip to database
}
else
{
await newEntry.Reference(propertyName).LoadAsync(); // trip to database
}
}
return newEntry.Entity;
}
so that you can pass the navigation properties like -
var addedOrder = await orderRepository.AddAsync(order, p => p.Status, p => p.TradingAction);

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

Check for existing record in ICustomValidate of ASP.NET Boilerplate

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

DHTMLX Scheduler recurring events

I have trouble with the DHTMLX scheduler specifically around recurring events.
I have tried to follow the documentation found here http://blog.scheduler-net.com/post/recurring-events-calendar-view-asp-net.aspx. However can't seem to get it working.
I can create the basic scheduler without any issues. The issue I now have is that any event that gets created won't save to the DB. This is what I have so far.
Model:
[Key]
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
[DHXJson(Alias = "id")]
public int Id { get; set; }
[DHXJson(Alias = "text")]
public string Description { get; set; }
[DHXJson(Alias = "start_date")]
public DateTime StartDate { get; set; }
[DHXJson(Alias = "end_date")]
public DateTime EndDate { get; set; }
[DHXJson(Alias="event_length")]
public int event_length { get; set; }
[DHXJson(Alias = "rec_type")]
public string rec_type { get; set; }
[DHXJson(Alias = "event_pid")]
public int event_pid { get; set; }
Controller:
public ActionResult Save(int? id, FormCollection actionValues)
{
var action = new DataAction(actionValues);
ApplicationDbContext data = new ApplicationDbContext();
try
{
var changedEvent = (Appointment)DHXEventsHelper.Bind(typeof(Appointment), actionValues);
//operations with recurring events require some additional handling
bool isFinished = deleteRelated(action, changedEvent, data);
if (!isFinished)
{
switch (action.Type)
{
case DataActionTypes.Insert:
data.Appointment.Add(changedEvent);
if (changedEvent.rec_type == "none")//delete one event from the serie
action.Type = DataActionTypes.Delete;
break;
case DataActionTypes.Delete:
changedEvent = data.Appointment.SingleOrDefault(ev => ev.Id == action.SourceId);
data.Appointment.Remove(changedEvent);
break;
default:// "update"
var eventToUpdate = data.Appointment.SingleOrDefault(ev => ev.Id == action.SourceId);
DHXEventsHelper.Update(eventToUpdate, changedEvent, new List<string>() { "id" });
break;
}
}
data.SaveChanges();
action.TargetId = changedEvent.Id;
}
catch
{
action.Type = DataActionTypes.Error;
}
return (new AjaxSaveResponse(action));
}
protected bool deleteRelated(DataAction action, Appointment changedEvent, ApplicationDbContext context)
{
bool finished = false;
if ((action.Type == DataActionTypes.Delete || action.Type == DataActionTypes.Update) && !string.IsNullOrEmpty(changedEvent.rec_type))
{
// context.Recurrings.DeleteAllOnSubmit(from ev in context.Recurrings where ev.event_pid == changedEvent.id select ev);
}
if (action.Type == DataActionTypes.Delete && (changedEvent.event_pid != 0 && changedEvent.event_pid != null))
{
// Recurring changed = (from ev in context.Recurrings where ev.id == action.TargetId select ev).Single();
// changed.rec_type = "none";
finished = true;
}
return finished;
}
Any help or ideas?
Try changing the change save method return value to "ContentResult". Also Look into your ApplicationDbContext and see if you can pull some hard-coded db values from that table when your index loads. Here is a copy of mine. I had the same problems until I used linq to classes to create the model/EF and my context is based on that. I was having the same issue when I created my own "light-weight" interface because I didn't want to use EF.
public ContentResult Save(int? id, FormCollection actionValues)
{
var action = new DataAction(actionValues);
var context = new SchedulerDataContext();
Int64 source_id = Int64.Parse(actionValues["id"]);
try
{
var changedDelEvent = (Delivery)DHXEventsHelper.Bind(typeof(Delivery), actionValues);
var changedRecEvent = (Recurring)DHXEventsHelper.Bind(typeof(Recurring), actionValues);
//operations with recurring events require some additional handling
bool isFinished = deleteRelated(action, changedRecEvent, context);
if (!isFinished)
{
switch (action.Type)
{
case DataActionTypes.Insert:
context.Recurrings.InsertOnSubmit(changedRecEvent);
context.SubmitChanges();
break;
case DataActionTypes.Delete:
changedRecEvent = context.Recurrings.SingleOrDefault(d => d.id == source_id);
if (changedRecEvent != null)
{
context.Recurrings.DeleteOnSubmit(changedRecEvent);
}
context.SubmitChanges();
break;
default:// "update"
var eventToUpdate = context.Deliveries.SingleOrDefault(d => d.DeliveryID == source_id);
DHXEventsHelper.Update(eventToUpdate, changedRecEvent, new List<string> { "id" });
if (eventToUpdate != null && eventToUpdate.RouteID != changedRecEvent.id)
{
var routeToUpdate = context.Routes.SingleOrDefault(d => d.RouteID == changedRecEvent.id);
eventToUpdate.Route = routeToUpdate;
}
context.SubmitChanges();
break;
}
action.TargetId = changedRecEvent.id;
}
}
catch
{
action.Type = DataActionTypes.Error;
}
return (new AjaxSaveResponse(action));
}
The recurring extension (dhtmlxscheduler_recurring.js) doesn't recognize the DHXJson Alias annotations that you are using on your entity class properties (extremely frustrating). Therefore, you must name your entity class columns/properties exactly how the dhtmlxscheduler_recurring.js is expecting them, even though the base scheduler API gives you the option for custom naming using the DHXJson alias annotations.

How do you deal with race between two client upserts?

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

Categories

Resources