Why does Resolver behave differently in Query vs Mutation? - graphql

I've been struggling with the issue/behaviour described below for while now and can't seem to figure out what's going on. This is all based on inherited/adapted code, so it's completely possible that I'm missing something fundamental. It's been literally years since I've asked a question on SO, so please bear that in mind if I'm not complying with the current expectations on formatting and content/detail.
I've got a number of domain classes involved in this problem, but at a high level, what I'm trying to do is to count the number of users with a specific role in relation to a group.
The behaviour that I'm seeing when I execute a GraphQL query (See below), is that the attribute in question (capacityUsed) is populated as expected, which makes me believe that the wiring is correct.
However, when I try to access the attribute within a GraphQL mutation, the underlying data to populate the value isn't retrieved, so the default value is returned, which isn't accurate or what I want.
Ultimately what I'm trying to do is to access (and make business decisions based on) the GroupRoleCapacity.CapacityUsed attribute. I'd like to sort out what's wrong with the config/setup for the mutation so that my resolver works as expected in both situations.
I'd appreciate any help or insight into what might be causing this. I've gone through the related HotCholocate and GraphL documentation a number of times and tried just about every iteration of related keywords that I can think of searching for an explanation of what's going on, and haven't been able to find a solution.
Here's how things are currently wired up (pulling out extraneous info/attributes to keep it short):
//GraphQL Entity Configuration
public class GroupRoleCapacityType : ObjectType<GroupRoleCapacity>
{
protected override void Configure(IObjectTypeDescriptor<GroupRoleCapacity> descriptor)
{
descriptor
.Field(grc => grc.Group)
.ResolveWith<Resolvers>(r => r.GetGroup(default!, default!))
.UseDbContext<AppDbContext>()
.Description("This is the Group for this GroupRoleCapacity.");
descriptor
.Field(grc => grc.Role)
.ResolveWith<Resolvers>(r => r.GetRole(default!, default!))
.UseDbContext<AppDbContext>()
.Description("This is the Role for this GroupRoleCapacity.");
descriptor
.Field(grc => grc.CapacityUsed)
.ResolveWith<Resolvers>(r => r.GetCapacityUsed(default!, default!))
.UseDbContext<AppDbContext>()
.Description("This is the number of Users with the indicated Role for this GroupRoleCapacity.");
}
protected class Resolvers
{
public Group GetGroup([Parent] GroupRoleCapacity GroupRoleCapacity, [ScopedService] AppDbContext context)
{
return context.Groups.FirstOrDefault(p => p.Id == GroupRoleCapacity.GroupId);
}
public Role GetRole([Parent] GroupRoleCapacity GroupRoleCapacity, [ScopedService] AppDbContext context)
{
return context.Roles.FirstOrDefault(p => p.Id == GroupRoleCapacity.RoleId);
}
public int GetCapacityUsed([Parent] GroupRoleCapacity GroupRoleCapacity, [ScopedService] AppDbContext context)
{
return context.Users.Count(u => u.GroupId == GroupRoleCapacity.GroupId &&
u.UserRoles.Any(ur => ur.RoleId == GroupRoleCapacity.RoleId));
}
}
}
//GraphQL Service Startup
public class Startup
{
public IConfiguration Configuration { get; }
public Startup(IConfiguration configuration) { Configuration = configuration; }
public void ConfigureServices(IServiceCollection services) {
services.AddScoped(p => p.GetRequiredService<IDbContextFactory<AppDbContext>>().CreateDbContext());
services
.AddGraphQLServer()
.AddQueryType<Query>()
.AddMutationType<Mutation>()
.AddType<GroupType>()
.AddType<RoleType>()
.AddType<UserType>()
.AddType<UserRoleType>()
.AddType<GroupRoleCapacityType>()
}
}
Using the query below seems to work as expected and the GroupRoleCapacity.CapacityUsed attribute is populated appropriately when accessing the data via a query
{ groupRoleCapacity { groupId roleId capacityUsed }}
//GraphQL Query
public class Query
{
[UseDbContext(typeof(AppDbContext))]
[GraphQLType(typeof(NonNullType<ListType<NonNullType<GroupRoleCapacities.GroupRoleCapacityType>>>))]
public IQueryable<GroupRoleCapacity> GetGroupRoleCapacity([ScopedService] AppDbContext context)
{
return context.GroupRoleCapacities;
}
}
However, when I use a mutation similar to below, GroupRoleCapacity.CapacityUsed isn't being set correctly, which is what's making me suspect a problem with either the Resolver implementation or the configuration.
mutation { addUserToGroup(input: { userId: 1, groupId: 1, roleId: 1}) { result { userId, groupId, roleId }}}
//GraphQL Mutation
public class Mutation
{
public Mutation(IConfiguration configuration) { }
[UseDbContext(typeof(AppDbContext))]
public async Task<AddUserToGroupPayload> AddUserToGroupAsync(AddUserToGroupInput input,
[ScopedService] AppDbContext context
)
{
int someLimit = 5;
var roleCapacity = context.GroupRoleCapacities.First(grc =>
grc.GroupId == input.GroupId &&
grc.RoleId == input.RoleId);
if(roleCapacity.CapacityUsed > someLimit) {
throw ApplicationException("limit exceeded");
}
//add user to group and save etc
return new AddUserToGroupPayload(result);
}
}

Difference here that Hot Chocolate is used only for GraphQL querying, but your code calls EF Core context explicitly. You need some common part.
What you can do:
Better expose DTO instead of database entity
Write common query which returns this DTO
Feed Hot Chocolate with this query. In this case you do not need resolvers at all (if I understand this library correctly)
Sample query (without DTO, never used Hot Chocolate and not familiar with it's attributes)
public static class MyQueries
{
public static IQueryable<GroupRoleCapacity> GetGroupRoleCapacity(AppDbContext context)
{
return context.GroupRoleCapacities.Select(c => new GroupRoleCapacity
{
GroupId = c.GroupId,
Group = c.Group, // I assume you have defined navigation properties
Role = c.Role,
RoleId = c.RoleId,
CapacityUsed = await context.Users.Count(u => u.GroupId == c.GroupId &&
u.UserRoles.Any(ur => ur.RoleId == c.RoleId))
);
}
}
Query:
//GraphQL Query
public class Query
{
[UseDbContext(typeof(AppDbContext))]
[GraphQLType(typeof(NonNullType<ListType<NonNullType<GroupRoleCapacities.GroupRoleCapacityType>>>))]
public IQueryable<GroupRoleCapacity> GetGroupRoleCapacity([ScopedService] AppDbContext context)
{
return MyQueries.GetGroupRoleCapacity(context);
}
}
Mutation:
//GraphQL Mutation
public class Mutation
{
public Mutation(IConfiguration configuration) { }
[UseDbContext(typeof(AppDbContext))]
public async Task<AddUserToGroupPayload> AddUserToGroupAsync(AddUserToGroupInput input,
[ScopedService] AppDbContext context
)
{
int someLimit = 5;
var roleCapacity = MyQueries.GetGroupRoleCapacity(context)
.FirstAsync(grc =>
grc.GroupId == input.GroupId &&
grc.RoleId == input.RoleId);
if (roleCapacity.CapacityUsed > someLimit)
{
throw ApplicationException("limit exceeded");
}
//add user to group and save etc
return new AddUserToGroupPayload(result);
}
}

Related

Holding classes in Dictionary

I have a question about holding a class in dictionary.So I am working on a project about a university.There are more than one faculty names.When user types a faculty name,I am directing user to appropriate faculty class with using context.call
So in here,if user enters show me computer engineering,user directed to the ShowComp class.
But using if-else makes code really unreadable.I thought that I can put these keywords to dictionary
But this time context.Call gives an error about the value type.What should I put dictionary value type.I couldn't figure it out.Can anyone help me please?
Since Dialogs inherit from IDialog<object>, you can put that in the dictionary:
private readonly Dictionary<string, IDialog<object>> options
= new Dictionary<string, IDialog<object>>
{ { "computer", new ShowComp() }, { "law", new ShowLaw() } };
public async Task GetFacilities(IDialogContext context, LuisResult result)
{
var entity = result.Entities.FirstOrDefault(e => options.ContainsKey(e.Entity));
if (entity != null)
{
IDialog<object> dialog = null;
if (options.TryGetValue(entity.Entity, out dialog))
{
context.Call(dialog, this.AfterResume);
}
}
}
Do it like this:
static Dictionary<String, Type> FACULTY_CLASS_MAP;
/**
* faculty class mapping.
*/
FACULTY_CLASS_MAP= new Dictionary<String, Type>
{
{ "Text", typeof(FacultyClass) }
}

Sitecore7 Linq to Sitecore only works with SearchResultItem but not with Custom Mapped Class

I have this very weird problem that I cannot get my head around. Perhaps someone could point out what I am doing wrong.
Basically, I am just trying to search items using Linq to Sitecore.
So, my classes look like ( I am using glass too)
[SitecoreType(TemplateId = "{TEMPLATE_GIUD}")]
public class MyMappedClass : SharedFieldClass
{
[SitecoreField(FieldName = "mylist")]
public virtual IEnumerable<SharedFieldClass> MyMultilistField { get; set; }
[SitecoreField(FieldName = "field1")]
[IndexField("field1")]
public virtual MyKeyValue field1 { get; set; }
}
[SitecoreType]
public class MyKeyValue
{
public virtual Sitecore.Data.ID Id {get;set;}
public virtual string MyValue{get;set;}
}
So when I do the below query it works as it's supposed to.
var results = context.GetQueryable<SearchResultItem>()
.Where(c => ((string)c["field1"]) == "{GUID}").GetResults();
But, when I do the below it returns 0 result.
List<MyMappedClass> results = context.GetQueryable<MyMappedClass>()
.Where(c => c.field1.MyValue == "{GUID}").ToList();
I have read this link . And I have followed the 2nd process described here for Glass to work with Sitecore7 Search (the "SharedFieldClass" contains all the basic index fields).
This is a pretty obvious scenario and I'm sure lots of people have done it already and I am doing something silly here.
Thanks in advance.
/## EDIT ##/
Okay so I've done a bit more digging on this. Not sure if it's a bug in ContentSearch/Luncene.NET API or I am missing something BUT seems like what was posted here is probably not TRUE and if you have a complex field type ( which you will ) you can not query with a mapped class against Lucene. ( not sure if this is also the case for Solr). If you are doing search against simple type like string and int then it works like a charm.
SO here're my findings:
After enabling DEBUG and LOG on for contentsearch I found that if I query like this context.GetQueryable<MyMappedClass>().Where(c => c.field1.MyValue == "{GUID}") what it gets translated into is DEBUG Executing lucene query: field1.value:7e9ed2ae07194d83872f9836715eca8e and as there's no such thing in the index named "field1.value" the query doesn't return anything. The name of the index is actually just "field1". Is this a bug ??
However, query like this context.GetQueryable<SearchResultItem>() .Where(c => ((string)c["field1"]) == "{GUID}").GetResults(); works because it gets translated into "DEBUG Executing lucene query: +field1:7e9ed2ae07194d83872f9836715eca8e".
I also did write a method in my mapped class like below:
public string this[string key]
{
get
{
return key.ToLowerInvariant();
}
set { }
}
Which allowed me write the below query with my MyMappedClass.
results2 = context.GetQueryable<MyMappedClass>().Where(c => c["filed1"]== "{GUID}").ToList();
This returned expected result. BUT the values of the fields in MyMappedClass are not filled ( in fact they're all null except the core/shared values like templateid, url etc which are populated in the filled document). So the result list are pretty much useless. I could do a loop over all of them and manually get the values populated as I have the itemid. But imagine the cost for a large result set.
Lastly I did this:
<fieldType fieldTypeName="droplink" storageType="YES" indexType="TOKENIZED" vectorType="NO" boost="1f" type="System.String" settingType="Sitecore.ContentSearch.LuceneProvider.LuceneSearchFieldConfiguration, Sitecore.ContentSearch.LuceneProvider" />
So this returned populated "field1" with the itemid in lucene query using "IndexViewer2.0". BUT this fails for MyMappedClass too as the value of "field1" in the document is a System.string .. but it is mapped as "MyKeyValue" in MyMappedClass SO it throws the below exception
Exception: System.InvalidCastException
Message: Invalid cast from 'System.String' to 'MyLib.MyKeyValue'.
SO, the big question is:
How does one query using his/her mapped class using the cool ContentSearch API ?
I bit more further digging got me to a working solution. Posting it here just in case anyone runs into this issue.
This is how my "SharedFieldClass" looked like ( which was somewhat wrong )
public abstract class SharedFieldClass
{
[SitecoreId]
[IndexField("_id")]
[TypeConverter(typeof(IndexFieldIDValueConverter))]
public virtual ID Id { get; set; }
[SitecoreInfo(SitecoreInfoType.Language)]
[IndexField("_language")]
public virtual string Language { get; set; }
[SitecoreInfo(SitecoreInfoType.Version)]
public virtual int Version
{
get
{
return Uri == null ? 0 : Uri.Version.Number;
}
}
[TypeConverter(typeof(IndexFieldItemUriValueConverter))]
[XmlIgnore]
[IndexField("_uniqueid")]
public virtual ItemUri Uri { get; set; }
}
And there's a class in Glass that does the mapping. Which looks like below:
var sitecoreService = new SitecoreService("web");
foreach (var r in results)
{
sitecoreService.Map(r);
}
for me this class was failing to map because of this line:
[SitecoreId]
[IndexField("_id")]
[TypeConverter(typeof(IndexFieldIDValueConverter))]
public virtual ID Id { get; set; }
It was throwing a NULL exception at sitecoreService.Map(r); line
So I changed it to below:
[SitecoreId]
[IndexField("_group")]
[TypeConverter(typeof(IndexFieldIDValueConverter))]
public virtual ID Id { get; set; }
And it worked. I'm not sure when the index field for ItemId in sitecore changed from "_id" to "_group" or whether it was always like that. But it is "_group" in the SearchResultItem class. So I used it and mapping was successful.
So to Sum it all the solution looks like this:
The Mapped Class:
[SitecoreType(TemplateId = "{TEMPLATE_GIUD}")]
public class MyMappedClass : SharedFieldClass
{
[SitecoreField(FieldName = "mylist")]
public virtual IEnumerable<SharedFieldClass> MyMultilistField { get; set; }
[SitecoreField(FieldName = "field1")]
[IndexField("field1")]
public virtual MyKeyValue field1 { get; set; }
// Will be set with key and value for each field in the index document
public string this[string key]
{
get
{
return key.ToLowerInvariant();
}
set { }
}
}
[SitecoreType]
public class MyKeyValue
{
public virtual Sitecore.Data.ID Id {get;set;}
public virtual string MyValue{get;set;}
}
And the query is:
List<MyMappedClass> results = context.GetQueryable<MyMappedClass>()
.Where(c => c["field1"] == "{GUID}").ToList();
That's it.
/* edited */
OH!! Wait that's not it. This will still fail to cast the complex type "MyKeyValue" when the "field1" is populated with guid in the index document.
So to avoid this I had to write my custom converter as #Michael Edwards suggested.
I had to modify the class slightly to fit my needs ..
public class IndexFieldKeyValueModelConverter : TypeConverter
{
public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType)
{
var config = Glass.Mapper.Context.Default.GetTypeConfiguration<SitecoreTypeConfiguration>(sourceType, true);
if (config != null && sourceType == typeof(MyLib.IKeyValue))
{
return true;
}
else
return base.CanConvertFrom(context, sourceType);
}
public override bool CanConvertTo(ITypeDescriptorContext context, Type destinationType)
{
if (destinationType == typeof(string))
return true;
else
return base.CanConvertTo(context, destinationType);
}
public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value)
{
var scContext = new SitecoreContext();
Guid x = Guid.Empty;
if(value is string)
{
x = new Guid((string)value);
}
var item = scContext.Database.GetItem(x.ToString());
if (item == null)
return null;
return scContext.CreateType(typeof(MyLib.IKeyValue), item, true, false, new Dictionary<string, object>());
}
public override object ConvertTo(ITypeDescriptorContext context, CultureInfo culture, object value, Type destinationType)
{
var config = Glass.Mapper.Context.Default.GetTypeConfiguration<SitecoreTypeConfiguration>(value.GetType(), true);
ID id = config.GetId(value);
return id.ToShortID().ToString().ToLowerInvariant();
}
}
Not sure if it were the expected behaviour or not .. but for some reason, in the convertfrom method the value of the "object value" parameter was short string id format. So for scContext.Database.GetItem to work I had to convert it to a proper GUID and then it started returning proper item rather than null.
AND then I wrote my query like this:
results = context.GetQueryable<MyMappedGlassClass>().Where(c => c["field1"] == field1value && c["field2"] == field2value && c["_template"] == templateId).Filter(selector => selector["_group"] != currentId).ToList();
Looks like a fair bit of work to get it to work. I guess using the LinqHelper.CreateQuery method is the easy way out .. but as Mr. Pope suggested here that this method is not be used as this is an internal method.
Not sure what's the balance here.
/* end edited */
Please see my question description section for explanation on why I had to do things this way.
Also, (I bias opinion ) is the trick described here may not be valid anymore (please see my question description's edit section for the reason behind).
Also, index field for itemid in the Glass Mapper tutorial here is I think wrong (unless otherwise proven).
Hope it helps someone saving/wasting time.
You could create a custom field mapper for lucene that would convert from the Guid in the index to a glass model. I hacked this out but I haven't tested it:
public class IndexFieldDateTimeValueConverter : TypeConverter
{
public Type RequestedType { get; set; }
public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType)
{
var config = Glass.Mapper.Context.Default.GetTypeConfiguration<SitecoreTypeConfiguration>(sourceType, true);
if (config != null)
{
RequestedType = sourceType;
return true;
}
else
return base.CanConvertFrom(context, sourceType);
}
public override bool CanConvertTo(ITypeDescriptorContext context, Type destinationType)
{
if (destinationType == typeof(string))
return true;
else
return base.CanConvertTo(context, destinationType);
}
public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value)
{
var scContext = new SitecoreContext();
return scContext.CreateType(RequestedType, scContext.Database.GetItem(value.ToString()),true, false, new Dictionary<string, object>());
}
public override object ConvertTo(ITypeDescriptorContext context, CultureInfo culture, object value, Type destinationType)
{
var config = Glass.Mapper.Context.Default.GetTypeConfiguration<SitecoreTypeConfiguration>(value.GetType(), true);
ID id =config.GetId(value);
return id.ToShortID().ToString().ToLowerInvariant();
}
My concern is that the ConvertFrom method does not get passed the type requested so we have to store this as property on the class to pass it from the CanConvertFrom method to the ConvertFrom method. This makes this class not thread safe.
Add this to the indexFieldStorageValueFormatter section of the sitecore config.
The problem here is that SearchResultItem is not actually an Item, but does have the GetItem() method to get the Sitecore item. What you need to do is the following:
List<MyMappedClass> results = context.GetQueryable<SearchResultItem>()
.Select(sri => sri.GetItem())
.Where(i => i != null)
.Select(i => i.GlassCast<MyMappedClass>())
.Where(c => c.field1.MyValue == "{GUID}").ToList();
I haven't worked with Glass specifically, but if you change your parent class to SearchResultItem, does it begin working? If so, that would indicate an issue with the SharedFieldClass parent class.

Understanding Get Method Overrides

I am totally new to Web API an am not understanding how to Filter get calls.
This method returns all items in my database.
// GET: api/LogEntries
public IQueryable<LogEntry> GetLogEntries()
{
return db.LogEntries;
}
This method returns a specific item in my database.
// GET: api/LogEntries/5
[ResponseType(typeof(LogEntry))]
public IHttpActionResult GetLogEntry(int id)
{
LogEntry logEntry = db.LogEntries.Find(id);
if (logEntry == null)
{
return NotFound();
}
return Ok(logEntry);
}
So now I want to filter the returned records so I created this method but it won't work because the specific item method gets called. I seem to be missing a concept and am hoping you can point me to more clear understanding. Thanks
// GET: api/LogEntries
public IQueryable<LogEntry> GetLogEntries(string levelID)
{
int levIdInt;
if (Int32.TryParse(levelID, out levIdInt))
{
return db.LogEntries.Take(300).Where(l => (int)l.Level == levIdInt).OrderByDescending(d => d.TimeStamp);
}
return db.LogEntries.Where(i => i.ID < 0);
}
You need to specify the route for that method
[Route("api/LogEntries/Level/{levelID}"]
public IQueryable<LogEntry> GetLogEntries(string levelID)
{}
More on routing is available here http://www.asp.net/web-api/overview/web-api-routing-and-actions/attribute-routing-in-web-api-2

Entity Framework Many to Many query

I want to write a simple query, but there are some problems.
I have 2 tables M to N:
Users -> Events.
I want to get all users of a specific event (get this event by eventId).
public IQueryable<User> GetUsersByEventId(int eventId)
{
IQueryable<User> query = this.Context.Users.AsQueryable();
return query.Where(x => x.Events.SingleOrDefault(e => e.EventId == eventId)); ??
}
Something is missing and I dont know what, can someone help me? Thanks a lot !
If I understand you correctly (adding your models would help), I think you want Any
public IQueryable<User> GetUsersByEventId(int eventId)
{
return Context.Users
.Where(u => u.Events.Any(e => e.EventId == eventId));
}
This should return all users who have any event matching the given id.
Note: If you set up your relationships correctly, you should be able to get this directly from the Event.
public class Event
{
...
public virtual ICollection<User> Users { get; set; }
}
So then, you'd get the Event by id and access it's user collection.
var evt = repo.GetEventById(id);
var users = evt.Users;
I suggest you do that in your Event model itself. AFAIK you are using Event, User and EventUsers tables which is standard stuff for many2many.
public class Event
{
public int Id { get; set; }
// ...
public virtual ICollection<EventUsers> EventUsers { get; set; } // This is table that holds EventId, UserId (many2many)
public IQueryable<User> Users { get { return this.EventUsers.Select(x => x.User); } } // Get all users that are in this event
}

LINQ-To-Sharepoint Multiple content types for a single list

I'm using SPMetal in order to generate entity classes for my sharepoint site and I'm not exactly sure what the best practice is to use when there are multiple content types for a single list. For instance I have a task list that contains 2 content types and I'm defining them via the config file for SPMetal. Here is my definition...
<List Member="Tasks" Name="Tasks">
<ContentType Class="LegalReview" Name="LegalReviewContent"/>
<ContentType Class="Approval" Name="ApprovalContent"/>
</List>
This seems to work pretty well in that the generated objects do inherit from WorkflowTask but the generated type for the data context is a List of WorkflowTask. So when I do a query I get back a WorkflowTask object instead of a LegalReview or Approval object. How do I make it return an object of the correct type?
[Microsoft.SharePoint.Linq.ListAttribute(Name="Tasks")]
public Microsoft.SharePoint.Linq.EntityList<WorkflowTask> Tasks {
get {
return this.GetList<WorkflowTask>("Tasks");
}
}
UPDATE
Thanks for getting back to me. I'm not sure how I recreate the type based on the SPListItem and would appreciate any feedback.
ContractManagementDataContext context = new ContractManagementDataContext(_url);
WorkflowTask task = context.Tasks.FirstOrDefault(t => t.Id ==5);
Approval a = new Approval(task.item);
public partial class Approval{
public Approval(SPListItem item){
//Set all properties here for workflowtask and approval type?
//Wouldn't there be issues since it isn't attached to the datacontext?
}
public String SomeProperty{
get{ //get from list item};
set{ //set to list item};
}
Linq2SharePoint will always return an object of the first common base ContentType for all the ContentTypes in the list. This is not only because a base type of some description must be used to combine the different ContentTypes in code but also it will then only map the fields that should definitely exist on all ContentTypes in the list. It is however possible to get access to the underlying SPListItem returned by L2SP and thus from that determine the ContentType and down cast the item.
As part of a custom repository layer that is generated from T4 templates we have a partial addition to the Item class generated by SPMetal which implements ICustomMapping to get the data not usually available on the L2SP entities. A simplified version is below which just gets the ContentType and ModifiedDate to show the methodology; though the full class we use also maps Modified By, Created Date/By, Attachments, Version, Path etc, the principle is the same for all.
public partial class Item : ICustomMapping
{
private SPListItem _SPListItem;
public SPListItem SPListItem
{
get { return _SPListItem; }
set { _SPListItem = value; }
}
public string ContentTypeId { get; internal set; }
public DateTime Modified { get; internal set; }
public virtual void MapFrom(object listItem)
{
SPListItem item = (SPListItem)listItem;
this.SPListItem = item;
this.ContentTypeId = item.ContentTypeId.ToString();
this.Modified = (DateTime)item["Modified"];
}
public virtual void MapTo(object listItem)
{
SPListItem item = (SPListItem)listItem;
item["Modified"] = this.Modified == DateTime.MinValue ? this.Modified = DateTime.Now : this.Modified;
}
public virtual void Resolve(RefreshMode mode, object originalListItem, object databaseObject)
{
SPListItem originalItem = (SPListItem)originalListItem;
SPListItem databaseItem = (SPListItem)databaseObject;
DateTime originalModifiedValue = (DateTime)originalItem["Modified"];
DateTime dbModifiedValue = (DateTime)databaseItem["Modified"];
string originalContentTypeIdValue = originalItem.ContentTypeId.ToString();
string dbContentTypeIdValue = databaseItem.ContentTypeId.ToString();
switch(mode)
{
case RefreshMode.OverwriteCurrentValues:
this.Modified = dbModifiedValue;
this.ContentTypeId = dbContentTypeIdValue;
break;
case RefreshMode.KeepCurrentValues:
databaseItem["Modified"] = this.Modified;
break;
case RefreshMode.KeepChanges:
if (this.Modified != originalModifiedValue)
{
databaseItem["Modified"] = this.Modified;
}
else if (this.Modified == originalModifiedValue && this.Modified != dbModifiedValue)
{
this.Modified = dbModifiedValue;
}
if (this.ContentTypeId != originalContentTypeIdValue)
{
throw new InvalidOperationException("You cannot change the ContentTypeId directly");
}
else if (this.ContentTypeId == originalContentTypeIdValue && this.ContentTypeId != dbContentTypeIdValue)
{
this.ContentTypeId = dbContentTypeIdValue;
}
break;
}
}
}
Once you have the ContentType and the underlying SPListItem available on your L2SP entity it is simply a matter of writing a method which returns an instance of the derived ContentType entity from a combination of the values of the base type and the extra data for the missing fields from the SPListItem.
UPDATE: I don't actually have an example converter class as we don't use the above mapping extension to Item in this way. However I could imagine something like this would work:
public static class EntityConverter
{
public static Approval ToApproval(WorkflowTask wft)
{
Approval a = new Approval();
a.SomePropertyOnWorkflowTask = wft.SomePropertyOnWorkflowTask;
a.SomePropertyOnApproval = wft.SPListItem["field-name"];
return a;
}
}
Or you could put a method on a partial instance of WorkflowTask to return an Approval object.
public partial class WorkflowTask
{
public Approval ToApproval()
{
Approval a = new Approval();
a.SomePropertyOnWorkflowTask = this.SomePropertyOnWorkflowTask;
a.SomePropertyOnApproval = this.SPListItem["field-name"];
return a;
}
public LegalReview ToLegalReview()
{
// Create and return LegalReview as for Approval
}
}
In either situation you would need to determine the method to call to get the derived type from the ContentTypeId property of the WorkflowTask. This is the sort of code I would normally want to generate in one form or another as it will be pretty repetitive but that is a bit off-topic.

Resources