I have a comments type structure where users are able to post replies to an Article. (One article can have many discussion replies). When a user adds a reply, I want the parent articles last updated date to also change so that the article is placed at the top of the list when viewed from the frontend indicating that it has had recent activity. To achieve this, the comment is added through a custom controller and then I have used the ContentService Published event to update the parent though am finding my event to be a bit of a bottle neck and taking up to six seconds to run
public void OnApplicationStarted(UmbracoApplicationBase umbracoApplication, ApplicationContext applicationContext)
{
ContentService.Published += ContentServicePublished;
}
private void ContentServicePublished(IPublishingStrategy sender, PublishEventArgs<IContent> e)
{
foreach (var node in e.PublishedEntities)
{
//Handle updating the parent nodes last edited date to address ordering
if (node.ContentType.Alias == "DiscussionReply")
{
var contentService = new Umbraco.Core.Services.ContentService();
var parentNode = contentService.GetById(node.ParentId);
int intSiblings = parentNode.Children().Count() + 1;
if(parentNode.HasProperty("siblings"))
{
parentNode.SetValue("siblings", intSiblings);
contentService.SaveAndPublishWithStatus(parentNode, 0, false);
}
}
}
}
Is there anything obvious with this code that may be causing the performance issue?
Many thanks,
You should be using the Services Singleton for accessing the various services including ContentService.
One way to do so is to access the Services on ApplicationContext.Current like so:
var contentService = ApplicationContext.Current.Services.ContentService;
However, your bottleneck is going to be in retrieving the parent node and it's properties which requires multiple calls to the database. On top of that, you're retrieving the parent's children here:
int intSiblings = parentNode.Children().Count() + 1;
The better solution is to use the PublishedContent cache which doesn't hit the database at all and provides significantly superior performance.
If you're using a SurfaceController use it's Umbraco property (and you also have access to Services as well):
// After you've published the comment node:
var commentNode = Umbraco.TypedContent(commentNodeId);
// We already know this is a DiscussionReply node, no need to check.
int intSiblings = commentNode.Parent.Children.Count() + 1;
if (commentNode.Parent.HasProperty("siblings"))
{
// It's only now that we really need to grab the parent node from the ContentService so we can update it.
var parentNode = Services.ContentService.GetById(commentNode.ParentId);
parentNode.SetValue("siblings", intSiblings);
contentService.SaveAndPublishWithStatus(parentNode, 0, false);
}
If you're implementing a WebApi based on UmbracoApiController then the same Umbraco and Services properties are available to you there as well.
I'm using Umbraco 7.3.4 and here's my solution:
// Create a list of objects to be created or updated.
var newContentList = new List<MyCustomModel>() {
new MyCustomModel {Id: 1, Name: "Document 1", Attribute1: ...},
new MyCustomModel {Id: 2, Name: "Document 2", Attribute1: ...},
new MyCustomModel {Id: 3, Name: "Document 3", Attribute1: ...}
};
// Get old content from cache
var oldContentAsIPublishedContentList = (new UmbracoHelper(UmbracoContext.Current)).TypedContent(ParentId).Descendants("YourContentType").ToList();
// Get only modified content items
var modifiedItemIds = from x in oldContentAsIPublishedContentList
from y in newContentList
where x.Id == y.Id
&& (x.Name != y.Name || x.Attribute1 != y.Attribute1)
select x.Id;
// Get modified items as an IContent list.
var oldContentAsIContentList = ApplicationContext.Services.ContentService.GetByIds(modifiedItemIds).ToList();
// Create final content list.
var finalContentList= new List<IContent>();
// Update or insert items
foreach(var item in newContentList) {
// For each new content item, find an old IContent by the ID
// If the old IContent is found and the values are modified, add it to the finalContentList
// Otherwise, create a new instance using the API.
IContent content = oldContentAsIContentList.FirstOrDefault(x => x.Id == item.Id) ?? ApplicationContext.Services.ContentService.CreateContent(item.Name, ParentId, "YourContentType");
// Update content
content.Name = item.Name;
content.SetValue("Attribute1", item.Attribute1);
finalContentList.Add(content);
// The following code is required
content.ChangePublishedState(PublishedState.Published);
content.SortOrder = 1;
}
// if the finalContentList has some items, call the Sort method to commit and publish the changes
ApplicationContext.Services.ContentService.Sort(finalContentList);
Related
We have a customised TFS workflow, I want to be able to access the Reasons I can close a Bug (change the state from Active to Closed) from TFS so that we don't have to update our code every time we want to tweak our process.
This is what I have so far:
WorkItemType wiType = this.GetWorkItemStore().Projects[this.ProjectName].WorkItemTypes["Bug"];
var reason = wiType.FieldDefinitions["Reason"];
var state = wiType.FieldDefinitions["State"];
var filterList = new FieldFilterList();
FieldFilter filter = new FieldFilter(wiType.FieldDefinitions[CoreField.State], "Active");
filterList.Add(filter);
var allowedReasons = reason.FilteredAllowedValues(filterList);
However I'm not getting any results. I'd like to get a list of all the reasons why I can close a bug (Not Reproduceable, Fixed etc)
There isn't any easy way to get the transition via API directly as I know since the API read the allowed values from database directly.
The alternative way would be export the workitemtype definition via WorkItemType.Export() method and then get the information from it. Vaccano's answer in this question provided the entire code sample you can use.
Edited to give an example of how I solved this using the above recommendation:
public static List<Transition> GetTransistions(this WorkItemType workItemType)
{
List<Transition> currentTransistions;
// See if this WorkItemType has already had it's transistions figured out.
_allTransistions.TryGetValue(workItemType, out currentTransistions);
if (currentTransistions != null)
return currentTransistions;
// Get this worktype type as xml
XmlDocument workItemTypeXml = workItemType.Export(false);
// Create a dictionary to allow us to look up the "to" state using a "from" state.
var newTransistions = new List<Transition>();
// get the transistions node.
XmlNodeList transitionsList = workItemTypeXml.GetElementsByTagName("TRANSITIONS");
// As there is only one transistions item we can just get the first
XmlNode transitions = transitionsList[0];
// Iterate all the transitions
foreach (XmlNode transition in transitions)
{
XmlElement defaultReasonNode = transition["REASONS"]["DEFAULTREASON"];
var defaultReason = defaultReasonNode.Attributes["value"].Value;
var otherReasons = new List<string>();
XmlNodeList otherReasonsNodes = transition["REASONS"].SelectNodes("REASON");
foreach (XmlNode reasonNode in otherReasonsNodes)
{
var reason = reasonNode.Attributes["value"].Value;
otherReasons.Add(reason);
}
// save off the transistion
newTransistions.Add(new Transition
{
From = transition.Attributes["from"].Value,
To = transition.Attributes["to"].Value,
DefaultReason = defaultReason,
OtherReasons = otherReasons
});
}
// Save off this transition so we don't do it again if it is needed.
_allTransistions.Add(workItemType, newTransistions);
return newTransistions;
}
We're trying to get the list and hierarchy of all linked external files. Right now we tried the following code:
FilteredElementCollector collectorI = new FilteredElementCollector(DocChild);
IList<Element> elemsI = collectorI.OfCategory(BuiltInCategory.OST_RvtLinks).OfClass(typeof(RevitLinkInstance)).ToElements();
foreach (Element eI in elemsI)
{
if (eI is RevitLinkInstance)
{
RevitLinkInstance InstanceType = eI as RevitLinkInstance;
RevitLinkType type = DocChild.GetElement(InstanceType.GetTypeId()) as RevitLinkType;
TaskDialog.Show("Debug", "IsNestedLink=" + type.IsNestedLink.ToString() + " IsLinked=" + DocChild.IsLinked.ToString());
if (!type.IsNestedLink)
{
TaskDialog.Show("Debug", "Children=" + InstanceType.GetLinkDocument().PathName.ToString());
}
}
}
We succeed to get the list of all linked files but there's no hierarchy. We don't know which file is a children of which parent.
This is the Link structure we're trying to get:
enter image description here
You need to play with GetParentId and GetChilds methods to read the hierarchy. Here is a code:
public Result Execute(
ExternalCommandData commandData,
ref string message,
ElementSet elements)
{
// get active document
Document mainDoc = commandData.Application.ActiveUIDocument.Document;
// prepare to show the results...
TreeNode mainNode = new TreeNode();
mainNode.Text = mainDoc.PathName;
// start by the root links (no parent node)
FilteredElementCollector coll = new FilteredElementCollector(mainDoc);
coll.OfClass(typeof(RevitLinkInstance));
foreach (RevitLinkInstance inst in coll)
{
RevitLinkType type = mainDoc.GetElement(inst.GetTypeId()) as RevitLinkType;
if (type.GetParentId() == ElementId.InvalidElementId)
{
TreeNode parentNode = new TreeNode(inst.Name);
mainNode.Nodes.Add(parentNode);
GetChilds(mainDoc, type.GetChildIds(), parentNode);
}
}
// show the results in a form
System.Windows.Forms.Form resultForm = new System.Windows.Forms.Form();
TreeView treeView = new TreeView();
treeView.Size = resultForm.Size;
treeView.Anchor |= AnchorStyles.Bottom | AnchorStyles.Top;
treeView.Nodes.Add(mainNode);
resultForm.Controls.Add(treeView);
resultForm.ShowDialog();
return Result.Succeeded;
}
private void GetChilds(Document mainDoc, ICollection<ElementId> ids,
TreeNode parentNode)
{
foreach (ElementId id in ids)
{
// get the child information
RevitLinkType type = mainDoc.GetElement(id) as RevitLinkType;
TreeNode subNode = new TreeNode(type.Name);
parentNode.Nodes.Add(subNode);
// then go to the next level
GetChilds(mainDoc, type.GetChildIds(), subNode);
}
}
And the result should look like:
Original source blog post.
Thank you for the great answer. It helped foxing a big part of my problem but I just have a remaining issue which is: how to get the full path name of all instances. In some cases the same file is re-used two time in the Revit Link Hierarchy.
Regards
I've got:
my-app
community-list
On attached, my-app gets the user and loads the app.user. In the meantime, community-list is attached (even before app.user is loaded) and so I haven't been able to get the user's starred communities yet. Therefore, the solution I'm working on is as follows.
In community-list.attached():
app.changes.listen((List<ChangeRecord> records) {
if (app.user != null) {
getUserStarredCommunities();
}
});
Elsewhere in community-list is said metho:
// This is triggered by an app.changes.listen.
void getUserStarredCommunities() {
// Determine if this user has starred the community.
communities.forEach((community) {
var starredCommunityRef = new db.Firebase(firebaseLocation + '/users/' + app.user.username + '/communities/' + community['id']);
starredCommunityRef.onValue.listen((e) {
if (e.snapshot.val() == null) {
community['userStarred'] = false;
} else {
community['userStarred'] = true;
}
});
});
}
Note that communities is an observable list in community-list:
#observable List communities = toObservable([]);
Which is initially populated in community-list.attached():
getCommunities() {
var f = new db.Firebase(firebaseLocation + '/communities');
var communityRef = f.limit(20);
communityRef.onChildAdded.listen((e) {
var community = e.snapshot.val();
// If no updated date, use the created date.
if (community['updatedDate'] == null) {
community['updatedDate'] = DateTime.parse(community['createdDate']);
}
// snapshot.name is Firebase's ID, i.e. "the name of the Firebase location"
// So we'll add that to our local item list.
community['id'] = e.snapshot.name();
// Insert each new community into the list.
communities.add(community);
// Sort the list by the item's updatedDate, then reverse it.
communities.sort((m1, m2) => m1["updatedDate"].compareTo(m2["updatedDate"]));
communities = communities.reversed.toList();
});
}
In summary, I load the list of communities even before I have a user, but once I have a user I want to update each community (Map) in the list of communities with the userStarred = true/false, which I then use in my community-list template.
Alas, it doesn't seem like the List updates. How do I achieve this?
This whole app.changes.listen business is expensive. What's the proper practice in a case like this, where an element is loaded before I load objects (like app.user) that will modify it in some way.
1)
toList() creates a copy of the list. You need to apply toObservable again to get an observable list.
communities = toObservable(communities.reversed.toList());
This also assigns a new list to communities which is covered by #observable.
I think it should trigger anyway
2) You update your communities explicitly. It shouldn't be necessary to listen for changes. You can call a method containing
if (app.user != null) {
getUserStarredCommunities();
}
explicitly each time you change the list.
You also call Firebase for each community when a change in communities occurs. I don't know Firebase but it seems you send a request to a server each time which is of course expensive.
You should remember for what user+community combination you already made the call and use the remembered result instead.
With app.changes.listen you listen to any updated of any #observable field in your component. If you have other observable fields beside communities this method might be called too often.
If you are only interested in changes to communities you should put this code into a method like
communitiesChanged(oldVal, newVal) {
if (app.user != null) {
getUserStarredCommunities();
}
}
but the better option is to not listen to changes and another method name and call it explicitly as state above anyways if possible.
There appears to be two ways to update a disconnected Entity Framework entity using the "attach" method.
Method One is to simply set the disconnected entity's state as modified:
myDbContext.Dogs.Attach(dog);
myDbContext.Entry(dog).State = EntityState.Modified;
myDbContext.SaveChanges();
This will save all fields on the "dog" object. But say you are doing this from an mvc web page where you only allow editing of Dog.Name, and the only Dog property contained on the page is Name. Then one could do Method Two:
myDbContext.Dogs.Attach(dog);
myDbContext.Entry(dog).Property(o => o.Name).CurrentValue = dog.Name;
myDbContext.Entry(dog).Property(o => o.Name).IsModified = true;
myDbContext.SaveChanges();
Method Two could get quite verbose when there are a lot of properties to update. This prompted me to attempt Method Three, setting IsModified = false on the properties I don't want to change. This does not work, throwing the runtime error "Setting IsModified to false for a modified property is not supported":
myDbContext.Dogs.Attach(dog);
myDbContext.Entry(dog).State = EntityState.Modified;
myDbContext.Entry(dog).Property(o => o.Owner).IsModified = false;
myDbContext.SaveChanges();
I'd much prefer to use Method One everywhere, but there are many instances where my asp.net mvc view does not contain every scalar property of the Dog class.
My questions are:
Are there any attributes I could use on the POCO class that would tell Entity Framework that I never want the property to up updated? Eg, [NeverUpdate]. I am aware of the [NotMapped] attribute, but that is not what I need.
Failing that, is there any way I can use Method One above (myDbContext.Entry(dog).State = EntityState.Modified;
) and exclude fields that I don't want updated?
P.S. I am aware of another way, to not use "attach" and simply fetch a fresh object from the database, update the desired properties, and save. That is what I am doing, but I'm curious if there is a way to use "attach," thus avoiding that extra trip to the database, but do it in a way that is not so verbose as Method Two above. By "fetch a fresh object" I mean:
Dog dbDog = myDbContext.Dogs.FirstOrDefault(d => d.ID = dog.ID);
dbDog.Name = dog.Name;
myDbContext.SaveChanges();
The following may work works.
myDbContext.Dogs.Attach(dog);
myDbContext.Entry(dog).State = EntityState.Modified;
var objectContext = ((IObjectContextAdapter) myDbContext).ObjectContext;
foreach (var entry in objectContext.ObjectStateManager.GetObjectStateEntries(EntityState.Modified).Where(entity => entity.Entity.GetType() == typeof(Dogs)))
{
// You need to give Foreign Key Property name
// instead of Navigation Property name
entry.RejectPropertyChanges("OwnerID");
}
myDbContext.SaveChanges();
If you want to do it in a single line, use the following extension method:
public static void DontUpdateProperty<TEntity>(this DbContext context, string propertyName)
{
var objectContext = ((IObjectContextAdapter) context).ObjectContext;
foreach (var entry in objectContext.ObjectStateManager.GetObjectStateEntries(EntityState.Modified).Where(entity => entity.Entity.GetType() == typeof(TEntity)))
{
entry.RejectPropertyChanges(propertyName);
}
}
And use it like this
// After you modify some POCOs
myDbContext.DontUpdateProperty<Dogs>("OwnerID");
myDbContext.SaveChanges();
As you can see, you can modify this solution to fit your needs, e.g. use string[] properties instead of string propertyName as the argument.
Suggested Approach
A better solution would be to use an Attribute as you suggested ([NeverUpdate]). To make it work, you need to use SavingChanges event (check my blog):
void ObjectContext_SavingChanges(object sender, System.Data.Objects.SavingChangesEventArgs e)
{
ObjectContext context = sender as ObjectContext;
if(context != null)
{
foreach(ObjectStateEntry entry in context.ObjectStateManager.GetObjectStateEntries(EntityState.Modified))
{
var type = typeof(entry.Entity);
var properties = type.GetProperties();
foreach( var property in properties )
{
var attributes = property.GetCustomAttributes(typeof(NeverUpdateAttribute), false);
if(attributes.Length > 0)
entry.RejectPropertyChanges(property.Name);
}
}
}
}
// Check Microsoft documentation on how to create custom attributes:
// http://msdn.microsoft.com/en-us/library/sw480ze8(v=vs.80).aspx
public class NeverUpdateAttribute: SystemAttribute
{
}
//In your POCO
public class Dogs
{
[NeverUpdate]
public int OwnerID { get; set; }
}
Warning: I did not compile this code. I'm not at home :/
Warning 2: I have just read the MSDN documentation and it says:
ObjectStateEntry.RejectPropertyChanges Method
Rejects any changes made to the property with the given name since the
property was last loaded, attached, saved, or changes were accepted.
The orginal value of the property is stored and the property will no
longer be marked as modified.
I am not sure what its behavior would be in the case of attaching a modified entity. I will try this tomorrow.
Warning 3: I have tried it now. This solution works. Property that is rejected with RejectPropertyChanges() method are not updated in the persistence unit (database).
HOWEVER, if the entity that is updated is attached by calling Attach(), the current context remains dirty after SaveChanges(). Assume that the following row exists in the database:
Dogs
ID: 1
Name: Max
OwnerID: 1
Consider the following code:
var myDog = new Dogs();
myDog.ID = 1;
myDog.Name = Achilles;
myDog.OwnerID = 2;
myDbContext.Dogs.Attach(myDog);
myDbContext.Entry(myDog).State = EntityState.Modified;
myDbContext.SaveChanges();
The current state of database after SaveChanges():
Dogs:
ID: 1
Name: Achilles
OwnerID: 1
The current state of myDbContext after SaveChanges():
var ownerId = myDog.OwnerID; // it is 2
var status = myDbContext.Entry(myDog).State; // it is Unchanged
So what you should do? Detach it after SaveChanges():
Dogs myDog = new Dogs();
//Set properties
...
myDbContext.Dogs.Attach(myDog);
myDbContext.Entry(myDog).State = EntityState.Modified;
myDbContext.SaveChanges();
myDbContext.Entry(myDog).State = EntityState.Detached;
I have an action
[HttpPost]
public string Edit(Member member)
and Member has a collection of children entities ICollection<AgeBracket> AgeBrackets.
Currently I do retrieve all AgeBrackets associated with the member, mark everyone as deleted, then loop through new collection and create a new entry for each. Then I update my parent entity. It works, but there should be a better way to do it:
for example, if I would wrote SQL, I could delete all existing children with just one line
DELETE FROM AgeBrackets WHERE MemberId = #MemberId
In my situation it makes a select to retrieve existing items, then generate delete for each of them, then generate insert for each new child and then it generates update for parent.
Here is how my code looks now:
IList<AgeBracket> ageBrackets = db.AgeBrackets.Where<AgeBracket>(x => x.MemberId == member.MemberId).ToList();
foreach (AgeBracket ab in ageBrackets)
db.Entry(ab).State = EntityState.Deleted;
if (member.AgeBrackets != null)
foreach (AgeBracket ab in member.AgeBrackets)
{
ab.MemberId = member.MemberId;
db.AgeBrackets.Add(ab);
}
db.Entry(member).State = EntityState.Modified;
Initially I was trying to query existing children and compare each of them to new set, but it seems to be over-complicated.
What is the best way to update member and all it's children?
There's another way to do
var originalAgeBrackets = db.AgeBrackets.Where(x => x.MemberId == member.MemberId).ToArray();
var currentAgeBrackets = member.AgeBrackets;
foreach (var original in originalAgeBrackets) {
// check if the original age brackets were modified ou should be removed
var current = currentAgeBrackets.FirstOrDefault(c => c.AgeBracketId == original.AgeBracketId);
if(current != null) {
var entry = db.Entry(original);
entry.OriginalValues.SetValues(original);
entry.CurrentValues.SetValues(current);
} else {
db.Entry(original).State = EntityState.Deleted;
}
}
// add all age brackets not listed in originalAgeBrackets
foreach (var current in currentAgeBrackets.Where(c => !originalAgeBrackets.Select(o => o.AgeBracketId).Contains(c.AgeBracketId))) {
db.AgeBrackets.Add(current);
}
db.SaveChanges();
Unfortunately what you want to do haven't native support to EF Code First. What will help you would be EntityFramework.Extended. This will allow you to do something like:
db.AgeBrackets.Delete(a => a.MemberId == member.MemberId);
You should take care of change-tracks by yourself.
Hope it helps you.