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.
Related
I have a below method where I am loop through the list of id's and getting the data from db based on id and then creating the material and then adding to material list
public Construction AddToOsm(Model model, APIDbContext dbContext)
{
var construction = new Construction(model);
var surfaceType = dbContext.IntendedSurfaceTypes.SingleOrDefault(s => s.Id == this.SurfaceTypeId);
construction.setName(surfaceType?.Name);
using var materials = new MaterialVector();
var fenestrationMaterialById = new Dictionary<Guid, FenestrationMaterial>();
var opaqueMaterialById = new Dictionary<Guid, StandardOpaqueMaterial>();
foreach (var materialId in this.LayerIds.Where(i => i != default))
{
var opaqueMaterial = dbContext.OpaqueMaterials.SingleOrDefault(o => o.Id == materialId);
if (opaqueMaterial != default)
{
materials.Add(opaqueMaterialById.GetOrCreate(opaqueMaterial.Id, () => opaqueMaterial.AddToOsm(model)));
}
else
{
var glazingMaterial = dbContext.GlazingMaterials.SingleOrDefault(o => o.Id == materialId);
if (glazingMaterial != default)
{
materials.Add(fenestrationMaterialById.GetOrCreate(glazingMaterial.Id, () => glazingMaterial.AddToOsm(model)));
}
else
{
var glazingSimpleMaterial = dbContext.SimpleGlazingMaterials.SingleOrDefault(s => s.Id == materialId);
if(glazingSimpleMaterial != default)
{
materials.Add(fenestrationMaterialById.GetOrCreate(glazingSimpleMaterial.Id, () => glazingSimpleMaterial.AddToOsm(model)));
}
else
{
var gasGlazingMaterials = dbContext.GasGlazingMaterials.SingleOrDefault(a => a.Id == materialId);
if(gasGlazingMaterials != default)
{
materials.Add(fenestrationMaterialById.GetOrCreate(gasGlazingMaterials.Id, () => gasGlazingMaterials.AddToOsm(model)));
}
}
}
}
}
construction.setLayers(materials);
return construction;
}
I am looking a way to avoid this much of if-else statements mainly refactoring this but could not find a way to do. Could any one please suggest any idea on how to achieve the same.
Thanks in advance.
update: sample entity structure
public class GasGlazingMaterial : ISourceOfData, IIdentity<Guid>
{
[Key]
public Guid Id { get; set; }
public string Name { get; set; }
[ForeignKey("SourceOfData")]
public Guid? SourceOfDataId { get; set; }
public virtual CodeStandardGuideline SourceOfData { get; set; }
......
.....
}
A simple fix would be to "continue" after each materials.add. This would mean you dont need to embed the rest in an else
I'm using Razor pages for my project on dotnet core 2.1, and the application doesn't seem to bind my properties correctly, the simple types (int and string types) binds correctly but not the list of complex types, is there a work around for this?
my page handler looks like this:
public async Task<IActionResult> OnGetDTResponseAsync(DataTableOptions options) {// Some Code}
When I step through with my debugger all simple type properties for "DataTableOptions options" are well populated but the complex type returns null.
my model looks like this :
public class DataTableOptions
{
public string Draw { get; set; }
public int Start { get; set; }
public int Length { get; set; }
public List<DataTableColumnOrder> Order { get; set; }
public List<DataTableColumn> Columns { get; set; }
public DataTableColumnSearch Search { get; set; }
public List<string> Params { get; set; }
public DataTableOptions() { }
public class DataTableColumn
{
public string Data { get; set; }
public string Name { get; set; }
public bool Searchable { get; set; }
public bool Orderable { get; set; }
public DataTableColumnSearch Search { get; set; }
public DataTableColumn() { }
}
public class DataTableColumnSearch
{
public string Value { get; set; }
public bool Regex { get; set; }
public DataTableColumnSearch() { }
}
public class DataTableColumnOrder
{
public int Column { get; set; }
public string Dir { get; set; }
public DataTableColumnOrder() { }
}
}
While trying to solve this, I tried using
public async Task<IActionResult> OnGetDTResponseAsync(List<Dictionary<string, string>> columns)
in my page handler in place of the columns property of DataTableOptions so i could manually bind the properties to my class: I got a full list of my columns with it's properties binded to it, except for the DataTableColumn's DataTableColumnSearch property which is also a complex type that came out as null.
public async Task<IActionResult> OnGetDTResponseAsync(List<Dictionary<string, object>> columns)
doesn't work either.
This is what the request looks like in fiddler:
GET /CMS/Index?handler=DTResponse&draw=1&columns%5B0%5D%5Bdata%5D=id&columns%5B0%5D%5Bname%5D=&columns%5B0%5D%5Bsearchable%5D=false&columns%5B0%5D%5Borderable%5D=false&columns%5B0%5D%5Bsearch%5D%5Bvalue%5D=&columns%5B0%5D%5Bsearch%5D%5Bregex%5D=false&columns%5B1%5D%5Bdata%5D=name&columns%5B1%5D%5Bname%5D=&columns%5B1%5D%5Bsearchable%5D=true&columns%5B1%5D%5Borderable%5D=true&columns%5B1%5D%5Bsearch%5D%5Bvalue%5D=&columns%5B1%5D%5Bsearch%5D%5Bregex%5D=false&columns%5B2%5D%5Bdata%5D=webPage.name&columns%5B2%5D%5Bname%5D=&columns%5B2%5D%5Bsearchable%5D=true&columns%5B2%5D%5Borderable%5D=true&columns%5B2%5D%5Bsearch%5D%5Bvalue%5D=&columns%5B2%5D%5Bsearch%5D%5Bregex%5D=false&columns%5B3%5D%5Bdata%5D=value&columns%5B3%5D%5Bname%5D=&columns%5B3%5D%5Bsearchable%5D=true&columns%5B3%5D%5Borderable%5D=true&columns%5B3%5D%5Bsearch%5D%5Bvalue%5D=&columns%5B3%5D%5Bsearch%5D%5Bregex%5D=false&columns%5B4%5D%5Bdata%5D=contentType.name&columns%5B4%5D%5Bname%5D=&columns%5B4%5D%5Bsearchable%5D=true&columns%5B4%5D%5Borderable%5D=true&columns%5B4%5D%5Bsearch%5D%5Bvalue%5D=&columns%5B4%5D%5Bsearch%5D%5Bregex%5D=false&columns%5B5%5D%5Bdata%5D=&columns%5B5%5D%5Bname%5D=&columns%5B5%5D%5Bsearchable%5D=false&columns%5B5%5D%5Borderable%5D=false&columns%5B5%5D%5Bsearch%5D%5Bvalue%5D=&columns%5B5%5D%5Bsearch%5D%5Bregex%5D=false&order%5B0%5D%5Bcolumn%5D=2&order%5B0%5D%5Bdir%5D=asc&start=0&length=10&search%5Bvalue%5D=&search%5Bregex%5D=false&_=1545122652329 HTTP/1.1
I had to build a custom model binding class to handle this scenario.
For some reason a Collection List of a complex object that has another complex object as part of it's properties can't be automatically binded correctly in core 2.1 -Razor pages.
See my solution below:
using Microsoft.AspNetCore.Mvc.ModelBinding;
using RestaurantDataModel.Data.Requests;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace ExampleDataModel.Data
{
public class CustomDataTableEntityBinder : IModelBinder
{
public Task BindModelAsync(ModelBindingContext bindingContext)
{
if (bindingContext == null)
{
throw new ArgumentNullException(nameof(bindingContext));
}
var allValues = bindingContext.HttpContext.Request.Query;
DataTableOptions DTOs = new DataTableOptions {
Draw = allValues.FirstOrDefault(a => a.Key == "draw").Value,
Start = Convert.ToInt32(allValues.FirstOrDefault(a => a.Key == "start").Value),
Length = Convert.ToInt32(allValues.FirstOrDefault(a => a.Key == "length").Value)
};
if (allValues.Any(a => a.Key.Length > 9 && a.Key.Substring(0, 9).Contains("columns")))
{
var myListIndex = 0;
var myListIndexComparer = 0;
var allColumns = allValues.Where(a => a.Key.Length > 9 && a.Key.Substring(0, 9).Contains("columns")).ToList();
DataTableColumn DTC = new DataTableColumn();
DataTableColumnSearch DTCS = new DataTableColumnSearch();
DTOs.columns = new List<DataTableColumn>();
foreach (var column in allColumns)
{
var perColumnArray = column.Key.Split(new char[] { '[', ']' }, StringSplitOptions.RemoveEmptyEntries);
var rawIndex = perColumnArray[1];
if (!int.TryParse(rawIndex, out myListIndex))
{
return Task.CompletedTask;
}
if (myListIndexComparer != myListIndex)
{
DTC.search = DTCS;
DTOs.columns.Add(DTC);
DTC = new DataTableColumn();
DTCS = new DataTableColumnSearch();
}
myListIndexComparer = myListIndex;
switch (perColumnArray[2])
{
case "data":
DTC.data = column.Value;
break;
case "name":
DTC.name = column.Value;
break;
case "searchable":
DTC.searchable = String.IsNullOrWhiteSpace(column.Value) ? false : Convert.ToBoolean(column.Value);
break;
case "orderable":
DTC.orderable = String.IsNullOrWhiteSpace(column.Value) ? false : Convert.ToBoolean(column.Value);
break;
case "search":
if (perColumnArray[3] == "regex")
{
DTCS.regex = String.IsNullOrWhiteSpace(column.Value) ? false : Convert.ToBoolean(column.Value);
}
if (perColumnArray[3] == "value")
{
DTCS.value = column.Value;
}
break;
}
if(allColumns.IndexOf(column) == allColumns.IndexOf(allColumns.Last()))
{
DTC.search = DTCS;
DTOs.columns.Add(DTC);
}
}
}
if (allValues.Any(a => a.Key.Length > 7 && a.Key.Substring(0, 7).Contains("order")))
{
var myListIndex = 0;
var myListIndexComparer = 0;
var allOrders = allValues.Where(a => a.Key.Length > 7 && a.Key.Substring(0, 7).Contains("order")).ToList();
DataTableColumnOrder DTCO = new DataTableColumnOrder();
DTOs.order = new List<DataTableColumnOrder>();
foreach (var order in allOrders)
{
var perColumnArray = order.Key.Split(new char[] { '[', ']' }, StringSplitOptions.RemoveEmptyEntries);
var rawIndex = perColumnArray[1];
if (!int.TryParse(rawIndex, out myListIndex))
{
return Task.CompletedTask;
}
if (myListIndexComparer != myListIndex)
{
DTOs.order.Add(DTCO);
DTCO = new DataTableColumnOrder();
}
myListIndexComparer = myListIndex;
switch (perColumnArray[2])
{
case "column":
DTCO.Column = Convert.ToInt32(order.Value);
break;
case "dir":
DTCO.Dir = order.Value;
break;
}
if(allOrders.IndexOf(order) == allOrders.IndexOf(allOrders.Last()))
{
DTOs.order.Add(DTCO);
}
}
}
if (allValues.Any(a => a.Key.Length > 7 && a.Key.Substring(0, 8).Contains("search")))
{
var allSearches = allValues.Where(a => a.Key.Length > 8 && a.Key.Substring(0, 8).Contains("search")).ToList();
DataTableColumnSearch DTCS = new DataTableColumnSearch();
DTOs.search = new DataTableColumnSearch();
foreach (var search in allSearches)
{
var perColumnArray = search.Key.Split(new char[] { '[', ']' }, StringSplitOptions.RemoveEmptyEntries);
switch (perColumnArray[1])
{
case "value":
DTCS.value = search.Value;
break;
case "regex":
DTCS.regex = String.IsNullOrWhiteSpace(search.Value) ? false : Convert.ToBoolean(search.Value);
break;
}
if(allSearches.IndexOf(search) == allSearches.IndexOf(allSearches.Last()))
{
DTOs.search = DTCS;
}
}
}
bindingContext.Result = ModelBindingResult.Success(DTOs);
return Task.CompletedTask;
}
}
}
And then I added this to the top of my model class:
[ModelBinder(BinderType = typeof(CustomDataTableEntityBinder))]
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 have to insert multiple relations and having issues with the Context.SaveChanges action which takes like forever to complete. I already tried multiple ways to add these entities to database but nothing seems to help me out.
My models are build in the following way:
public class Agreement : GdSoftDeleteEntity
{
public DateTime Date { get; set; }
public AgreementType AgreementType { get; set; }
public virtual ICollection<PersonAgreementRelation> PersonAgreementRelations { get; set; }
public virtual ICollection<ImageSearchAppointment> ImageSearchAppointments { get; set; }
}
public class Person : GdSoftDeleteEntity
{
public string Name { get; set; }
public string FirstName { get; set; }
// E-mail is in identityuser
//public string EmailAddress { get; set; }
public virtual PersonType PersonType { get; set; }
public virtual ICollection<PersonAgreementRelation> PersonAgreementRelations { get; set; }
public virtual ICollection<PersonPersonRelation> PersonMasters { get; set; }
public virtual ICollection<PersonPersonRelation> PersonSlaves { get; set; }
}
public class PersonAgreementRelation : GdSoftDeleteEntity
{
public int PersonId { get; set; }
public virtual Person Person { get; set; }
public int AgreementId { get; set; }
public virtual Agreement Agreement { get; set; }
public virtual PersonAgreementRole PersonAgreementRole { get; set; }
}
public class ImageSearchAppointment : GdSoftDeleteEntity
{
public string Name { get; set; }
public bool ShowResultsToCustomer { get; set; }
public bool HasImageFeed { get; set; }
public int AgreementId { get; set; }
public virtual Agreement Agreement { get; set; }
public Periodicity Periodicity { get; set; }
public PeriodicityCategory PeriodicityCategory { get; set; }
public virtual ICollection<ImageSearchCommand> ImageSearchCommands { get; set; }
public virtual ICollection<ImageSearchAppointmentWebDomainWhitelist> ImageSearchAppointmentWebDomainWhitelists { get; set; }
public virtual ICollection<ImageSearchAppointmentWebDomainExtension> ImageSearchAppointmentWebDomainExtensions { get; set; }
}
public class ImageSearchCommand : GdSoftDeleteEntity
{
public int ImageSearchAppointmentId { get; set; }
public virtual ImageSearchAppointment ImageSearchAppointment { get; set; }
public int? ImageSearchAppointmentCredentialsId { get; set; }
public virtual ImageSearchAppointmentCredentials ImageSearchAppointmentCredentials { get; set; }
public DateTime Date { get; set; }
//public bool Invoiced { get; set; }
public int NumberOfImages { get; set; }
public DateTime ImageCollectionProcessedDate { get; set; }
public virtual ICollection<ImageSearchExecution> ImageSearchExecutions { get; set; }
}
In my service, I have written following code:
public int AddAgreement(int personId, AgreementDto agreementDto)
{
Context.Configuration.LazyLoadingEnabled = false;
//var person = Context.Persons.SingleOrDefault(el => el.Id == personId);
var person = Context.Persons
.SingleOrDefault(x => x.Id == personId);
if (person == null)
{
throw new GraphicsDetectiveInvalidDataTypeException($"No person found for Id: {personId}");
}
if (agreementDto == null)
{
throw new GraphicsDetectiveInvalidDataTypeException("Invalid agreementDto");
}
//TODO: Check if OKAY!!!
if (agreementDto.ImageSearchAppointmentDto.Count == 0)
{
throw new GraphicsDetectiveInvalidDataTypeException("Count of imagesearchappointments can't be lower than 0");
}
//set agreement properties
var agreement = new Agreement
{
Date = agreementDto.DateTime,
AgreementType = AgreementType.WwwImageSearch,
//ImageSearchAppointments = new List<ImageSearchAppointment>(),
//IsDeleted = false
};
Context.Agreements.Add(agreement);
Context.SaveChanges();
//var personAdminId = Context.Users.Single(x => x.Email == ConfigurationManager.AppSettings["DefaultGdAdminEmail"]).PersonId;
// Dit werkt niet. Moet in 2 stappen
//set personagreementrelations for new agreement
var adminEmail = ConfigurationManager.AppSettings["DefaultGdAdminEmail"];
var personAdminId = Context.Users
.SingleOrDefault(x => x.Email == adminEmail)
.PersonId;
var personPmId = Context.Persons.Single(x => x.Name == "My name").Id;
var personAgreementRelations = new List<PersonAgreementRelation>()
{
new PersonAgreementRelation
{
AgreementId = agreement.Id,
PersonId = personId,
PersonAgreementRole = PersonAgreementRole.Client,
},
new PersonAgreementRelation
{
AgreementId = agreement.Id,
PersonAgreementRole = PersonAgreementRole.Supplier,
PersonId = personPmId,
},
new PersonAgreementRelation
{
AgreementId = agreement.Id,
PersonAgreementRole = PersonAgreementRole.Admin,
PersonId = personAdminId,
}
};
foreach (var personAgreementRelation in personAgreementRelations)
{
Context.PersonAgreementRelations.Add(personAgreementRelation);
}
Context.Configuration.ValidateOnSaveEnabled = false;
Context.Configuration.AutoDetectChangesEnabled = false;
Context.SaveChanges();
Context.Configuration.ValidateOnSaveEnabled = true;
Context.Configuration.AutoDetectChangesEnabled = true;
Context.Configuration.LazyLoadingEnabled = true;
return agreement.Id;
}
public void AddFirstImageSearchAppointmentToAgreement(int agreementId, ImageSearchAppointmentDto imageSearchAppointmentDto)
{
Context.Configuration.LazyLoadingEnabled = false;
var agreement = Context.Agreements.SingleOrDefault(x => x.Id == agreementId);
if (agreement == null)
{
throw new GraphicsDetectiveInvalidDataTypeException($"No agreement found for id {agreementId}");
}
var appointmentType = imageSearchAppointmentDto;
if (appointmentType == null)
{
throw new GraphicsDetectiveInvalidDataTypeException($"No valid imageSearchAppointment");
}
if (appointmentType.ImageSearchCommandDto.Count == 0)
{
throw new GraphicsDetectiveInvalidDataTypeException("No imageSearchCommand");
}
var imageSearchAppointment = new ImageSearchAppointment
{
AgreementId = agreement.Id,
Agreement = agreement,
Name = appointmentType.Name,
Periodicity = appointmentType.Periodicity,
PeriodicityCategory = appointmentType.PeriodicityCategory,
ShowResultsToCustomer = appointmentType.ShowResultsToCustomer,
ImageSearchAppointmentWebDomainExtensions = new List<ImageSearchAppointmentWebDomainExtension>(),
ImageSearchCommands = new List<ImageSearchCommand>(),
ImageSearchAppointmentWebDomainWhitelists = new List<ImageSearchAppointmentWebDomainWhitelist>(),
IsDeleted = false
};
var imageSearchCommandDto = appointmentType.ImageSearchCommandDto.Single();
var imageSearchCommand = new ImageSearchCommand()
{
ImageSearchAppointment = imageSearchAppointment,
Date = imageSearchCommandDto.Date,
NumberOfImages = imageSearchCommandDto.NumberOfImages,
ImageCollectionProcessedDate = imageSearchCommandDto.ImageCollectionProcessedDate,
IsDeleted = false
};
if (imageSearchCommandDto.ImageSearchAppointmentCredentialsDto != null)
{
imageSearchCommand.ImageSearchAppointmentCredentials = new ImageSearchAppointmentCredentials
{
FtpProfileType = imageSearchCommandDto.ImageSearchAppointmentCredentialsDto.FtpProfileType,
Location = imageSearchCommandDto.ImageSearchAppointmentCredentialsDto.Location,
Username = imageSearchCommandDto.ImageSearchAppointmentCredentialsDto.Username,
Password = imageSearchCommandDto.ImageSearchAppointmentCredentialsDto.Password,
UsePassive = imageSearchCommandDto.ImageSearchAppointmentCredentialsDto.UsePassive,
IsDeleted = false
};
}
imageSearchAppointment.ImageSearchCommands.Add(imageSearchCommand);
if (!imageSearchAppointment.ShowResultsToCustomer)
{
var webDomainExtensions = appointmentType.WebDomainExtensionDtos
.Select(x => new ImageSearchAppointmentWebDomainExtension()
{
ImageSearchAppointment = imageSearchAppointment,
WebDomainExtensionId = x.Id
})
.ToList();
imageSearchAppointment.ImageSearchAppointmentWebDomainExtensions = webDomainExtensions;
}
Context.ImageSearchAppointments.Add(imageSearchAppointment);
Context.SaveChanges();
Context.Configuration.LazyLoadingEnabled = true;
}
I used dotTrace to profile these functions and it takes about 9 minutes to add the new entities to my database.
The database is an Azure SQL database, tier S3
I tried the proposed solution and adapted my code as follow:
public int AddAgreement(int personId, AgreementDto agreementDto)
{
var agreementId = 0;
using (var context = new GdDbContext())
{
GdDbConfiguration.SuspendExecutionStrategy = true;
context.Configuration.LazyLoadingEnabled = true;
//var person = Context.Persons.SingleOrDefault(el => el.Id == personId);
var person = context.Persons
.SingleOrDefault(x => x.Id == personId);
if (person == null)
{
throw new GraphicsDetectiveInvalidDataTypeException($"No person found for Id: {personId}");
}
//var personAdminId = Context.Users.Single(x => x.Email == ConfigurationManager.AppSettings["DefaultGdAdminEmail"]).PersonId;
// Dit werkt niet. Moet in 2 stappen
//set personagreementrelations for new agreement
var adminEmail = ConfigurationManager.AppSettings["DefaultGdAdminEmail"];
var personAdminId = context.Users
.Where(x => x.Email == adminEmail)
.Include(x => x.Person)
.First()
.Person.Id;
var personPmId = context.Persons.First(x => x.Name == "My name").Id;
using (var dbContextTransaction = context.Database.BeginTransaction())
{
try
{
if (agreementDto == null)
{
throw new GraphicsDetectiveInvalidDataTypeException("Invalid agreementDto");
}
//TODO: Check if OKAY!!!
if (agreementDto.ImageSearchAppointmentDto.Count == 0)
{
throw new GraphicsDetectiveInvalidDataTypeException("Count of imagesearchappointments can't be lower than 0");
}
//set agreement properties
var agreement = new Agreement
{
Date = agreementDto.DateTime,
AgreementType = AgreementType.WwwImageSearch,
//ImageSearchAppointments = new List<ImageSearchAppointment>(),
//IsDeleted = false
};
context.Agreements.Add(agreement);
//Context.SaveChanges();
var personAgreementRelations = new List<PersonAgreementRelation>()
{
new PersonAgreementRelation
{
//Agreement = agreement,
AgreementId = agreement.Id,
PersonId = personId,
//Person = person,
PersonAgreementRole = PersonAgreementRole.Client,
//IsDeleted = false
},
new PersonAgreementRelation
{
//Agreement = agreement,
AgreementId = agreement.Id,
PersonAgreementRole = PersonAgreementRole.Supplier,
PersonId = personPmId,
//Person = personPm,
//IsDeleted = false
},
new PersonAgreementRelation
{
//Agreement = agreement,
AgreementId = agreement.Id,
PersonAgreementRole = PersonAgreementRole.Admin,
PersonId = personAdminId,
//Person = personAdmin,
}
};
foreach (var personAgreementRelation in personAgreementRelations)
{
context.PersonAgreementRelations.Add(personAgreementRelation);
}
//agreement.PersonAgreementRelations = personAgreementRelations;
//Context.Agreements.Add(agreement);
context.Configuration.ValidateOnSaveEnabled = false;
context.Configuration.AutoDetectChangesEnabled = false;
//await Context.SaveChangesAsync();
context.SaveChanges();
dbContextTransaction.Commit();
//await Task.Run(async () => await Context.SaveChangesAsync());
context.Configuration.ValidateOnSaveEnabled = true;
context.Configuration.AutoDetectChangesEnabled = true;
context.Configuration.LazyLoadingEnabled = false;
agreementId = agreement.Id;
}
catch (Exception ex)
{
dbContextTransaction.Rollback();
throw ex;
}
}
GdDbConfiguration.SuspendExecutionStrategy = false;
}
return agreementId;
}
but it's taking as much time as before
You can follow below mentioned suggestions to improve the performance of above methods.
Use FirstOrDefault() instead of SingleOrDefault().FirstOrDefault() is the fastest method.
I can see that you have used Context.SaveChanges() method number of times on the same method.That will degrade the performnce of the method.So you must avoid that.Instead of use Transactions.
Like this : EF Transactions
using (var context = new YourContext())
{
using (var dbContextTransaction = context.Database.BeginTransaction())
{
try
{
// your operations here
context.SaveChanges(); //this called only once
dbContextTransaction.Commit();
}
catch (Exception)
{
dbContextTransaction.Rollback();
}
}
}
You can think about the implementaion of stored procedure if above will not give the enough improvement.
There are some performance issues with your code
Add Performance
foreach (var personAgreementRelation in personAgreementRelations)
{
Context.PersonAgreementRelations.Add(personAgreementRelation);
}
Context.Configuration.ValidateOnSaveEnabled = false;
Context.Configuration.AutoDetectChangesEnabled = false;
You add multiple entities then disabled AutoDetectChanges. You normally do the inverse
Depending on the number of entities in your context, it can severely hurt your performance
In the method "AddFirstImageSearchAppointmentToAgreement", it seems you use an outside context which can be very bad if it contains already multiple thousands of entities.
See: Improve Entity Framework Add Performance
Badly used, adding an entity to the context with the Add method take more time than saving it in the database!
SaveChanges vs. Bulk Insert vs. BulkSaveChanges
SaveChanges is very slow. For every record to save, a database round-trip is required. This is particularly the case for SQL Azure user because of the extra latency.
Some library allows you to perform Bulk Insert
See:
Entity Framework Bulk Insert Library
Entity Framework Bulk SaveChanges Library
Disclaimer: I'm the owner of the project Entity Framework Extensions
This library has a BulkSaveChanges features. It works exactly like SaveChanges but WAY FASTER!
// Context.SaveChanges();
Context.BulkSaveChanges();
EDIT: ADD additional information #1
I pasted my new code in Pastebin: link
Transaction
Why starting a transaction when you select your data and add entities to your context? It simply a VERY bad use of a transaction.
A transaction must be started as late as possible. In since BulkSaveChanges is already executed within a transaction, there is no point to create it.
Async.Result
var personAdminId = context.Users.FirstOrDefaultAsync(x => x.Email == adminEmail).Result.PersonId;
I don't understand why you are using an async method here...
In best case, you get similar performance as using non-async method
In worse case, you suffer from some performance issue with async method
Cache Item
var adminEmail = ConfigurationManager.AppSettings["DefaultGdAdminEmail"];
var personAdminId = context.Users.FirstOrDefaultAsync(x => x.Email == adminEmail).Result.PersonId;
I don't know how many time you call the AddAgreement method, but I doubt the admin will change.
So if you call it 10,000 times, you make 10,000 database round-trip to get the same exact value every time.
Create a static variable instead and get the value only once! You will for sure save a lot of time here
Here is how I normally handle static variable of this kind:
var personAdminId = My.UserAdmin.Id;
public static class My
{
private static User _userAdmin;
public static User UserAdmin
{
get
{
if (_userAdmin == null)
{
using (var context = new GdDbContext())
{
var adminEmail = ConfigurationManager.AppSettings["DefaultGdAdminEmail"];
_userAdmin = context.Users.FirstOrDefault(x => x.Email == adminEmail);
}
}
return _userAdmin;
}
}
}
LazyLoadingEnabled
In the first code, you have LazyLoadingEnabled to false but not in your Pastebin code,
Disabling LazyLoading can help a little bit since it will not create a proxy instance.
Take 10m instead of 9m
Let me know after removing the transaction and disabling again LazyLoading if the performance is a little bit better.
The next step will be to know some statistics:
Around how many time the AddAgreement method is invoked
Around how many persons do you have in your database
Around how many entities in average is Saved by the AddAgreement method
EDIT: ADD additional information #2
Currently, the only way to improve really the performance is by reducing the number of database round-trip.
I see you are still searching the personAdminId every time. You could save maybe 30s to 1 minute just here by caching this value somewhere like a static variable.
You still have not answered the three questions:
Around how many time the AddAgreement method is invoked
Around how many persons do you have in your database
Around how many entities in average is Saved by the AddAgreement method
The goal of theses questions is to understand what's slow!
By example, if you call the AddAgreement method 10,000 times and you only have 2000 persons in the database, you are probably better to cache in two dictionary theses 2000 persons to save 20,000 database round-trip (Saving one to two minutes?).
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 });
}
}