Subsonic 3 - What is the difference between these approaches? - subsonic

This method works fine:
Person p = new Person(3);
p.Name = "Bob";
p.Update();
However if I have an IQueryable foreign key collection the below fails
var foreignItems = Person.Find(x => x.ID == 3)
foreach(Person p in foreignItems)
{
p.Name = "Bob";
p.Update(); /*THROWS EXCEPTION */
}
Exception is thrown in Repository Update as it executes a query from BuildUpdateQuery such as - UPDATE PERSON WHERE ID = {0} which is wrong syntax!

This looks like a bug, you should report it to github (the new host for SubSonic source).
In the meantime does calling p.Save() may work around the issue.

Related

RevitAPI: How can I change a family's part type?

I am trying to change a family's part type via Revit's API after changing the family category.
I can retrieve the corresponding parameter and set its value, but (though the transaction is successfully committed) the part type is not changed.
Since the 'Part type' UI element showed an empty string, I checked via "Revit Lookup" what value the parameter had after the attempted change. It was still the old part type which doesn't even exist for that family category.
This is my code so far:
Family f = familyDocument.OwnerFamily;
Category c = f.FamilyCategory;
Parameter p = f.get_Parameter(BuiltInParameter.FAMILY_CONTENT_PART_TYPE);
f.FamilyCategoryId = new ElementId(BuiltInCategory.OST_LightingFixture);
p.Set((int) PartType.Normal);
I also tried it with two separate transactions (first one setting the category, second one setting the part type). No success there either.
Update:
Turned out, this code already worked. It was the surrounding code that created the error.
I tried the same in Revit 2018.3 and 2020.2 with success. Just create any family type (I used a lighting fixture template), and insert the following snippet into a new macro.
var f = Document.OwnerFamily;
var c = f.FamilyCategory;
var partTypeParam = f.get_Parameter(BuiltInParameter.FAMILY_CONTENT_PART_TYPE);
using(var t = new Transaction(Document, "Change part type"))
{
t.Start();
f.FamilyCategoryId = new ElementId(BuiltInCategory.OST_DuctAccessory);
partTypeParam.Set((int)PartType.Elbow);
t.Commit();
}
Compile and execute, then observe that the type is changed to duct accessory with part type elbow. Seems to work fine.
The only difference is that you seem to be in a slightly different context. You opened your family from a document context. If there is no non-obvious glitch in your implementation, this may point to an API bug. However, in my experiments this scenario posed no problems, so if there is any bug it cannot be systematic.
My aim was to change an arbitrary window family to a duct elbow. I post only the relevant parts (tested in Revit 2020.2):
internal class FamilyOption : IFamilyLoadOptions
{
bool IFamilyLoadOptions.OnFamilyFound(bool familyInUse, out bool overwriteParameterValues)
{
overwriteParameterValues = false;
return true;
}
bool IFamilyLoadOptions.OnSharedFamilyFound(Family sharedFamily, bool familyInUse, out FamilySource source, out bool overwriteParameterValues)
{
source = FamilySource.Family;
overwriteParameterValues = false;
return true;
}
}
public void PartTypeTester()
{
var f = new FilteredElementCollector(Document)
.OfClass(typeof(Family))
.First(ff => ff.Name == "ExampleFamily")
as Family;
var familyDoc = Document.EditFamily(f);
f = familyDoc.OwnerFamily;
var c = f.FamilyCategory;
var partTypeParam = f.get_Parameter(BuiltInParameter.FAMILY_CONTENT_PART_TYPE);
using(var t = new Transaction(familyDoc, "Change part type"))
{
t.Start();
f.FamilyCategoryId = new ElementId(BuiltInCategory.OST_DuctAccessory);
partTypeParam.Set((int)PartType.Elbow);
t.Commit();
}
var opt = new FamilyOption();
f = familyDoc.LoadFamily(Document, opt);
familyDoc.Close(false);
}
Works like a charm. You should not expect the resulting family to behave like a duct accessory though ;-).

session.queryObjects does not support secondary types

Reading this https://chemistry.apache.org/docs/cmis-samples/samples/properties/index.html#retrieving-properties, I thought it would be possible to retrieve secondary types using queryObjects method, but it does not. For example, I'm trying to get cm:author from Alfresco, it returns null. Here is my piece of code:
OperationContext oc = OperationContextUtils.createMaximumOperationContext();
ItemIterable<CmisObject> results = session.queryObjects(task.getCmisType(), where, false, oc);
...
Object value = cmisObject.getPropertyValue("cm:author");
Am I missing something?
P.S: I'm using Chemistry 1.0.0, CMIS 1.1, Binding: Browser
UPDATE:
Okay I found something interesting, In order to retrieve cm:author, I have to reload the cmisObject to make it work:
results = session.queryObjects("cmis:document", "IN_FOLDER('" + folder.getId() + "')", false, oc);
results.each { it ->
object = session.getObject(it.getId());
author = object.getPropertyValue("cm:author");
if(author != null) {
println object.getId() + " => " + author;
}
Bug?
First make sure cm:author is what you want. That is not the person who created the document node in Alfresco. That is an editable property that anyone can set to anything, and by default it is null.
If what you want is the actual username of the person who created the document node, you should use cmis:createdBy which is mapped to alfresco's cm:creator property.
Assuming cm:author is definitely what you want, you have two choices regarding how to get it. First, you can get it from the object. But in order to get it from the object you must first fetch the object. Your query returns QueryResult objects, not CmisObjects.
So you should do something like:
ItemIterable<QueryResult> results = session.query(queryString, false);
for (QueryResult qResult : results) {
String objectId = "";
PropertyData<?> propData = qResult.getPropertyById("cmis:objectId");
if (propData != null) {
objectId = (String) propData.getFirstValue();
}
CmisObject obj = session.getObject(session.createObjectId(objectId));
// Dump the object here
System.out.println("Author: " + obj.getPropertyValue("cm:author");
}
Your second option would be to get the property value from the query result. Your ability to do this depends on the query you ran. The author property is defined on an aspect, so you must do a join in order to get it back. The query might look something like:
queryString = "select content.cmis:name, content.cmis:objectId, author.cm:author from cmis:document content JOIN cm:author author ON content.cmis:objectId = author.cmis:objectId WHERE content.cmis:objectId is not null AND author.cm:author = 'Jeff'";
If you use that query, then you can grab the author using the QueryResult, like this:
System.out.println("Author: " + qResult.getPropertyValueByQueryName("author.cm:author"));
Hopefully that explains the difference between fetching the value from a query result and fetching a property value from the object itself.

Getting an error creating a Query object in SubSonic

I am getting the following error in one of our environments. It seems to occur when IIS is restarted, but we haven't narrowed down the specifics to reproduce it.
A DataTable named 'PeoplePassword' already belongs to this DataSet.
at System.Data.DataTableCollection.RegisterName(String name, String tbNamespace)
at System.Data.DataTableCollection.BaseAdd(DataTable table)
at System.Data.DataTableCollection.Add(DataTable table)
at SubSonic.SqlDataProvider.GetTableSchema(String tableName, TableType tableType)
at SubSonic.DataService.GetSchema(String tableName, String providerName, TableType tableType)
at SubSonic.DataService.GetTableSchema(String tableName, String providerName)
at SubSonic.Query..ctor(String tableName)
at Wad.Elbert.Data.Enrollment.FetchByUserId(Int32 userId)
Based on the stacktrace, I believe the error is happening on the second line of the method while creating the query object.
Please let me know if anyone else has this problem.
Thanks!
The code for the function is:
public static List<Enrollment> FetchByUserId(int userId)
{
List<Enrollment> enrollments = new List<Enrollment>();
SubSonic.Query query = new SubSonic.Query("Enrollment");
query.SelectList = "userid, prompt, response, validationRegex, validationMessage, responseType, enrollmentSource";
query.QueryType = SubSonic.QueryType.Select;
query.AddWhere("userId", userId);
DataSet dataset = query.ExecuteDataSet();
if (dataset != null &&
dataset.Tables.Count > 0)
{
foreach (DataRow dr in dataset.Tables[0].Rows)
{
enrollments.Add(new Enrollment((int)dr["userId"], dr["prompt"].ToString(), dr["response"].ToString(), dr["validationRegex"] != null ? dr["validationRegex"].ToString() : string.Empty, dr["validationMessage"] != null ? dr["validationMessage"].ToString() : string.Empty, (int)dr["responseType"], (int)dr["enrollmentSource"]));
}
}
return enrollments;
}
This is a threading issue.
Subsonic loads it's schema on the first call of SubSonic.DataService.GetTableSchema(...) but this is not Thread safe.
Let me demonstrate this with a little example
private static Dictionary<string, DriveInfo> drives = new Dictionary<string, DriveInfo>;
private static DriveInfo GetDrive(string name)
{
if (drives.Count == 0)
{
Thread.Sleep(10000); // fake delay
foreach(var drive in DriveInfo.GetDrives)
drives.Add(drive.Name, drive);
}
if (drives.ContainsKey(name))
return drives[name];
return null;
}
this explains well what happens, on the first call to this method the dictionary is empty
If that's the case the method will preload all drives.
For every call the requested drive (or null) is returned.
But what happens if you fire the method two times directly after the start? Then both executions try to load the drives in the Dictionary. The first one to add a drive wins the second will throw an ArgumentException (element already exists).
After the initial preload, everything works fine.
Long story short, you have two choices.
Modify subsonic source to make SubSonic.DataService.GetTableSchema(...) thread safe.
http://msdn.microsoft.com/de-de/library/c5kehkcz(v=vs.80).aspx
"Warmup" subsonic before accepting requests. The technic to achive this depends on your application design. For ASP.NET you have an Application_Start method that is only executed once during your application lifecycle
http://msdn.microsoft.com/en-us/library/ms178473(v=vs.100).aspx
So you can basically put a
var count = new SubSonic.Query("Enrollment").GetRecordCount();
in the method to force subsonic to init the table schema itself.

Entity Framework 5 deep copy/clone of an entity

I am using Entity Framework 5 (DBContext) and I am trying to find the best way to deep copy an entity (i.e. copy the entity and all related objects) and then save the new entities in the database. How can I do this? I have looked into using extension methods such as CloneHelper but I am not sure if it applies to DBContext.
One cheap easy way of cloning an entity is to do something like this:
var originalEntity = Context.MySet.AsNoTracking()
.FirstOrDefault(e => e.Id == 1);
Context.MySet.Add(originalEntity);
Context.SaveChanges();
the trick here is AsNoTracking() - when you load an entity like this, your context do not know about it and when you call SaveChanges, it will treat it like a new entity.
If MySet has a reference to MyProperty and you want a copy of it too, just use an Include:
var originalEntity = Context.MySet.Include("MyProperty")
.AsNoTracking()
.FirstOrDefault(e => e.Id == 1);
Here's another option.
I prefer it in some cases because it does not require you to run a query specifically to get data to be cloned. You can use this method to create clones of entities you've already obtained from the database.
//Get entity to be cloned
var source = Context.ExampleRows.FirstOrDefault();
//Create and add clone object to context before setting its values
var clone = new ExampleRow();
Context.ExampleRows.Add(clone);
//Copy values from source to clone
var sourceValues = Context.Entry(source).CurrentValues;
Context.Entry(clone).CurrentValues.SetValues(sourceValues);
//Change values of the copied entity
clone.ExampleProperty = "New Value";
//Insert clone with changes into database
Context.SaveChanges();
This method copies the current values from the source to a new row that has been added.
This is a generic extension method which allows generic cloning.
You have to fetch System.Linq.Dynamic from nuget.
public TEntity Clone<TEntity>(this DbContext context, TEntity entity) where TEntity : class
{
var keyName = GetKeyName<TEntity>();
var keyValue = context.Entry(entity).Property(keyName).CurrentValue;
var keyType = typeof(TEntity).GetProperty(keyName, System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Instance).PropertyType;
var dbSet = context.Set<TEntity>();
var newEntity = dbSet
.Where(keyName + " = #0", keyValue)
.AsNoTracking()
.Single();
context.Entry(newEntity).Property(keyName).CurrentValue = keyType.GetDefault();
context.Add(newEntity);
return newEntity;
}
The only thing you have to implement yourself is the GetKeyName method. This could be anything from return typeof(TEntity).Name + "Id" to return the first guid property or return the first property marked with DatabaseGenerated(DatabaseGeneratedOption.Identity)].
In my case I already marked my classes with [DataServiceKeyAttribute("EntityId")]
private string GetKeyName<TEntity>() where TEntity : class
{
return ((DataServiceKeyAttribute)typeof(TEntity)
.GetCustomAttributes(typeof(DataServiceKeyAttribute), true).First())
.KeyNames.Single();
}
I had the same issue in Entity Framework Core where deep clone involves multiple steps when children entities are lazy loaded. One way to clone the whole structure is the following:
var clonedItem = Context.Parent.AsNoTracking()
.Include(u => u.Child1)
.Include(u => u.Child2)
// deep includes might go here (see ThenInclude)
.FirstOrDefault(u => u.ParentId == parentId);
// remove old id from parent
clonedItem.ParentId = 0;
// remove old ids from children
clonedItem.Parent1.ForEach(x =>
{
x.Child1Id = 0;
x.ParentId= 0;
});
clonedItem.Parent2.ForEach(x =>
{
x.Child2Id = 0;
x.ParentId= 0;
});
// customize entities before inserting it
// mark everything for insert
Context.Parent.Add(clonedItem);
// save everything in one single transaction
Context.SaveChanges();
Of course, there are ways to make generic functions to eager load everything and/or reset values for all keys, but this should make all the steps much clear and customizable (e.g. all for some children to not be cloned at all, by skipping their Include).

Windows Azure: "An item with the same key has already been added." exception thrown on Select

I'm getting a strange error while trying to select a row from a table under Windows Azure Table Storage. The exception "An item with the same key has already been added." is being thrown even though I'm not inserting anything. The query that is causing the problem is as follows:
var ids = new HashSet<string>() { id };
var fields = new HashSet<string> {"#all"};
using (var db = new AzureDbFetcher())
{
var result = db.GetPeople(ids, fields, null);
}
public Dictionary<string, Person> GetPeople(HashSet<String> ids, HashSet<String> fields, CollectionOptions options)
{
var result = new Dictionary<string, Person>();
foreach (var id in ids)
{
var p = db.persons.Where(x => x.RowKey == id).SingleOrDefault();
if (p == null)
{
continue;
}
// do something with result
}
}
As you can see, there's only 1 id and the error is thrown right at the top of the loop and nothing is being modified.
However, I'm using "" as the Partition Key for this particular row. What gives?
You probably added an object with the same row key (and no partition key) to your DataServiceContext before performing this query. Then you're retrieving the conflicting object from the data store, and it can't be added to the context because of the collision.
The context tracks all object retrieved from the Tables. Since entities are uniquely identified by their partitionKey/rowKey combination, a context, like the tables, cannot contain duplicate partitionkey/rowkey combinations.
Possible causes of such a collison are:
Retrieving an entity, modifying it, and then retrieving it again using the same context.
Adding an entity to the context, and then retrieving one with the same keys.
In both cases, the context the encounters it's already tracking a different object which does however have the same keys. This is not something the context can sort out by itself, hence the exception.
Hope this helps. If you could give a little more information, that would be helpful.

Resources