Working on a Dynamics CRM Custom Workflow that updates "Modified By" field -- need help debugging - dynamics-crm

I am working on a Dynamics CRM CWA that updates the "Modified By" field based on a text field called "Prepared By". I currently have 3 errors that I need some help debugging (see below). They may be pretty easy fixes but I am fairly new to coding. Any help de-bugging would be greatly appreciated. Thanks!
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Globalization;
using System.Linq;
using System.Text;
using System.ServiceModel;
using System.Threading.Tasks;
using System.Activities;
using Microsoft.Xrm.Sdk;
using Microsoft.Xrm.Sdk.Workflow;
using System.Runtime.Serialization;
namespace KED365.Workflows
{
/// </summary>
public class ModifiedBy : WorkFlowActivityBase
{
private Guid contactid;
[Input("User Full Name")]
public InArgument<string> UserFullName { get; set; }
/// <summary>
/// Executes the WorkFlow.
/// </summary>
/// <param name="crmWorkflowContext">The <see cref="LocalWorkflowContext"/> which contains the
/// <param name="executionContext" > <see cref="CodeActivityContext"/>
/// </param>
/// <remarks>
/// For improved performance, Microsoft Dynamics 365 caches WorkFlow instances.
/// The WorkFlow's Execute method should be written to be stateless as the constructor
/// is not called for every invocation of the WorkFlow. Also, multiple system threads
/// could execute the WorkFlow at the same time. All per invocation state information
/// is stored in the context. This means that you should not use global variables in WorkFlows.
/// </remarks>
protected override void Execute(CodeActivityContext activityContext, IWorkflowContext workflowContext, IOrganizationService orgService, ITracingService tracingService)
{
//get entity record for which plugin was fired
Entity _target = (Entity)workflowContext.InputParameters["Target"];
//check if portaluser name is to be obtained from custom createby or from custom modifiedby
if (workflowContext.MessageName.ToUpper() == "CREATE")
{
contactid = _target.Attributes.Contains("new_createdby") ? _target.GetAttributeValue<EntityReference>("new_createdby").Id : Guid.Empty;
}
else
{
contactid = _target.Attributes.Contains("new_modifiedby") ? _target.GetAttributeValue<EntityReference>("new_modifiedby").Id : Guid.Empty;
}
//retrieve contact fullname from contactid
var _contact = activityContext.CreateQuery("contact").Where(c => c.GetAttributeValue<Guid>("contactid").Equals(contactid)).FirstOrDefault();
if (_contact != null)
{
if (_contact.Attributes.Contains("fullname"))
{
fullname = _contact.GetAttributeValue<string>("fullname");
}
//retrieve Systemuser that has same name as that of new_portalcreatedby/ //new_portalmodifiedby
Entity _user = context.CreateQuery("systemuser").Where(e => e.GetAttributeValue<string>("fullname").Equals(fullname)).FirstOrDefault();
if (_user != null)
{
//check if we need to update createdby or modifiedby
if (workflowContext.MessageName.ToUpper() == "CREATE")
{
_target["createdby"] = _user.ToEntityReference();
}
else
{
_target["modifiedby"] = _user.ToEntityReference();
}
//assign new target to plugin executioncontext
workflowContext.InputParameters["Target"] = _target;
}
}
}
}
}
Error 1 :
Severity Code Description Project File Line Suppression State
Error CS1061 'CodeActivityContext' does not contain a definition for 'CreateQuery' and no extension method 'CreateQuery' accepting a first argument of type 'CodeActivityContext' could be found (are you missing a using directive or an assembly reference?) Workflows C:\Users\tgiard\Downloads\GetUserByName-master\GetUserByName-master\Workflows\ModifiedBy.cs 68 Active
Error 2 :
Severity Code Description Project File Line Suppression State
Error CS0103 The name 'fullname' does not exist in the current context Workflows C:\Users\tgiard\Downloads\GetUserByName-master\GetUserByName-master\Workflows\ModifiedBy.cs 75 Active
Error 3 :
Severity Code Description Project File Line Suppression State
Error CS0103 The name 'context' does not exist in the current context Workflows C:\Users\tgiard\Downloads\GetUserByName-master\GetUserByName-master\Workflows\ModifiedBy.cs 79 Active

Here is some feedback on your issues:
Error 1 - 'CodeActivityContext' does not contain a definition for 'CreateQuery'
This issue is related to the lines:
var _contact = activityContext.CreateQuery("contact").Where(c => c.GetAttributeValue<Guid>("contactid").Equals(contactid)).FirstOrDefault();
and
Entity _user = context.CreateQuery("systemuser").Where(e => e.GetAttributeValue<string>("fullname").Equals(fullname)).FirstOrDefault();
I don't know what that method is but you have better options; for the contact you already have the guid, so you can simply use a Retrieve():
var _contact = orgService.Retrieve("contact", contactid, new ColumnSet("fullname"));
And for the system user write a QueryExpression filtering by fullname:
var query = new QueryExpression("systemuser"):
query.Criteria.AddCondition("fullname", ConditionOperator.Equal, fullname);
var _user = orgService.RetrieveMultiple(query).Entities.FirstOrDefault();
Error 2: The name 'fullname' does not exist in the current contex
This is basic C#, you must instantiate your variable before you use it:
string fullname;
Error 3: The name 'context' does not exist in the current context
Ironic and true. This should be activityContext, but we have already fixed this issue in the change we made for Error 1.
Entity _user = context.CreateQuery("systemuser").Where(e => e.GetAttributeValue<string>("fullname").Equals(fullname)).FirstOrDefault();

As Zach Mast correctly pointed out, using a Pre-Operation is recommended. It also seems an odd case where you retrieve a contacts name and match it with a user. Instead, You could change the type of the field to a user reference, add a user field to the contact you retrieve or add a code to match the Contact to the User. This way, you won't have issues with users with the same name or typo's.
Please find below your workflow activity converted to a Pre-Operation plugin.
using Microsoft.Xrm.Sdk;
using Microsoft.Xrm.Sdk.Query;
using System;
using System.Linq;
namespace KED365.Plugins
{
public class CreateUpdateContact : IPlugin
{
public void Execute(IServiceProvider serviceProvider)
{
var tracingService = (ITracingService)serviceProvider.GetService(typeof(ITracingService));
var context = (IPluginExecutionContext)serviceProvider.GetService(typeof(IPluginExecutionContext));
var factory = (IOrganizationServiceFactory)serviceProvider.GetService(typeof(IOrganizationServiceFactory));
var service = factory.CreateOrganizationService(context.UserId);
tracingService.Trace("Start plugin");
tracingService.Trace("Validate Target");
if (!context.InputParameters.Contains("Target") || !(context.InputParameters["Target"] is Entity))
return;
tracingService.Trace("Retrieve Target");
var target = (Entity)context.InputParameters["Target"];
String message = context.MessageName.ToLower();
SetCreatedByAndModifiedBy(tracingService, service, target, message);
}
private void SetCreatedByAndModifiedBy(ITracingService tracingService, IOrganizationService service, Entity target, string message)
{
tracingService.Trace("Start SetPriceList");
tracingService.Trace("Validate Message is Create or Update");
if (!message.Equals("create", StringComparison.OrdinalIgnoreCase) && !message.Equals("update", StringComparison.OrdinalIgnoreCase))
return;
tracingService.Trace("Retrieve Attributes");
var createdByReference = target.GetAttributeValue<EntityReference>("new_createdby");
var modifiedByReference = target.GetAttributeValue<EntityReference>("new_modifiedby");
tracingService.Trace("Retrieve And Set User for Created By");
RetrieveAndSetUser(tracingService, service, target, createdByReference, "createdby");
tracingService.Trace("Retrieve And Set User for Modified By");
RetrieveAndSetUser(tracingService, service, target, modifiedByReference, "modifiedby");
}
private void RetrieveAndSetUser(ITracingService tracingService, IOrganizationService service, Entity target, EntityReference reference, string targetAttribute)
{
tracingService.Trace("Validating Reference");
if (reference == null)
return;
tracingService.Trace("Retrieving and Validating User");
var user = RetrieveUserByName(service, reference.Name, new ColumnSet(false));
if (user == null)
return;
tracingService.Trace("Setting Target Attribute");
target[targetAttribute] = user.ToEntityReference();
}
private Entity RetrieveUserByName(IOrganizationService service, string name, ColumnSet columns)
{
var query = new QueryExpression
{
EntityName = "systemuser",
ColumnSet = columns,
Criteria = new FilterExpression
{
FilterOperator = LogicalOperator.And,
Conditions =
{
new ConditionExpression
{
AttributeName = "fullname",
Operator = ConditionOperator.Equal,
Values = { name }
}
}
}
};
var retrieveResponse = service.RetrieveMultiple(query);
if (retrieveResponse.Entities.Count == 1)
{
return retrieveResponse.Entities.FirstOrDefault();
}
else
{
// Alternatively you can thrown an error as you have unexpectedly multiple matches
return null;
}
}
}
}

Related

ABP.IO - MultiTenancy - Setting Tenant from External IDP

I am trying to configure Auth0 as an external login provider in my ABP.IO application (MVC with integrated identity server). I've got it working so that I can log in fine, but what I can't figure out is how to set the tenant in the ABP side.
What I came up with is a rule on the Auth0 side to populate the TenantId as a claim in the id token, so I can parse that in my custom SingInManager in the GetExternalLoginInfoAsync method, like so:
string tenantId = auth.Principal.FindFirstValue("https://example.com/tenantId");
I'm just having a hard time figuring out what to do with it from there. The assumption is that users will be configured to authenticate via Auth0 and the users will get created locally on first login (which, again, is working EXCEPT for the Tenant part).
Alright, here is the workaround I have in place, and it SHOULD be transferable to any external login system that you are depending on. I'm not sure if this is the correct way of doing this, so if anybody else wants to chip in with a more efficient system I am all ears.
Anyway, my workflow assumes that you have, like I did, created a mechanism for the TenantId to be sent from the external IDP. For this, I used the Organizations feature in Auth0 and added the TenantId as metadata, then I created an Action in Auth0 to attach that metadata as a claim to be used on the ABP side.
In ABP, I followed this article to override the SignInManager: https://community.abp.io/articles/how-to-customize-the-signin-manager-3e858753
As in the article, I overrode the GetExternalLoginInfoAsync method of the sign in manager and added the following lines to pull the TenantId out of the Auth0 claims and add it back in using the pre-defined AbpClaimTypes.TenantId value.
EDIT: I also had to override the ExternalLoginSignInAsync method to account for multi-tenancy (otherwise it kept trying to recreate the users and throwing duplicate email errors). I'll post the full class below with my added stuff in comments:
public class CustomSignInManager : Microsoft.AspNetCore.Identity.SignInManager<Volo.Abp.Identity.IdentityUser>
{
private const string LoginProviderKey = "LoginProvider";
private const string XsrfKey = "XsrfId";
private readonly IDataFilter _dataFilter;
public CustomSignInManager(
IDataFilter dataFilter,
Microsoft.AspNetCore.Identity.UserManager<Volo.Abp.Identity.IdentityUser> userManager,
Microsoft.AspNetCore.Http.IHttpContextAccessor contextAccessor,
Microsoft.AspNetCore.Identity.IUserClaimsPrincipalFactory<Volo.Abp.Identity.IdentityUser> claimsFactory,
Microsoft.Extensions.Options.IOptions<Microsoft.AspNetCore.Identity.IdentityOptions> optionsAccessor,
Microsoft.Extensions.Logging.ILogger<Microsoft.AspNetCore.Identity.SignInManager<Volo.Abp.Identity.IdentityUser>> logger,
Microsoft.AspNetCore.Authentication.IAuthenticationSchemeProvider schemes,
Microsoft.AspNetCore.Identity.IUserConfirmation<Volo.Abp.Identity.IdentityUser> confirmation)
: base(userManager, contextAccessor, claimsFactory, optionsAccessor, logger, schemes, confirmation)
{
_dataFilter = dataFilter;
}
/// <summary>
/// Gets the external login information for the current login, as an asynchronous operation.
/// </summary>
/// <param name="expectedXsrf">Flag indication whether a Cross Site Request Forgery token was expected in the current request.</param>
/// <returns>The task object representing the asynchronous operation containing the <see name="ExternalLoginInfo"/>
/// for the sign-in attempt.</returns>
public override async Task<Microsoft.AspNetCore.Identity.ExternalLoginInfo> GetExternalLoginInfoAsync(string expectedXsrf = null)
{
var auth = await Context.AuthenticateAsync(IdentityConstants.ExternalScheme);
var items = auth?.Properties?.Items;
if (auth?.Principal == null || items == null || !items.ContainsKey(LoginProviderKey))
{
return null;
}
if (expectedXsrf != null)
{
if (!items.ContainsKey(XsrfKey))
{
return null;
}
var userId = items[XsrfKey] as string;
if (userId != expectedXsrf)
{
return null;
}
}
var providerKey = auth.Principal.FindFirstValue(ClaimTypes.NameIdentifier);
var provider = items[LoginProviderKey] as string;
if (providerKey == null || provider == null)
{
return null;
}
var providerDisplayName = (await GetExternalAuthenticationSchemesAsync()).FirstOrDefault(p => p.Name == provider)?.DisplayName
?? provider;
/* Begin tenantId claim search */
string tenantId = auth.Principal.FindFirstValue("https://example.com/tenantId"); //pull the tenantId claim if it exists
if(!string.IsNullOrEmpty(tenantId))
{
auth.Principal.Identities.FirstOrDefault().AddClaim(new Claim(AbpClaimTypes.TenantId, tenantId)); //if there is a tenantId, add the AbpClaimTypes.TenantId claim back into the principal
}
/* End tenantId claim search */
var eli = new ExternalLoginInfo(auth.Principal, provider, providerKey, providerDisplayName)
{
AuthenticationTokens = auth.Properties.GetTokens(),
AuthenticationProperties = auth.Properties
};
return eli;
}
/// <summary>
/// Signs in a user via a previously registered third party login, as an asynchronous operation.
/// </summary>
/// <param name="loginProvider">The login provider to use.</param>
/// <param name="providerKey">The unique provider identifier for the user.</param>
/// <param name="isPersistent">Flag indicating whether the sign-in cookie should persist after the browser is closed.</param>
/// <param name="bypassTwoFactor">Flag indicating whether to bypass two factor authentication.</param>
/// <returns>The task object representing the asynchronous operation containing the <see name="SignInResult"/>
/// for the sign-in attempt.</returns>
public override async Task<SignInResult> ExternalLoginSignInAsync(string loginProvider, string providerKey, bool isPersistent, bool bypassTwoFactor)
{
Volo.Abp.Identity.IdentityUser user = null; //stage the user variable as null
using (_dataFilter.Disable<IMultiTenant>()) //disable the tenantid filters so we can search all logins for the expected key
{
user = await UserManager.FindByLoginAsync(loginProvider, providerKey); //search logins for the expected key
}
if (user == null)
{
return SignInResult.Failed;
}
var error = await PreSignInCheck(user);
if (error != null)
{
return error;
}
return await SignInOrTwoFactorAsync(user, isPersistent, loginProvider, bypassTwoFactor);
}
}
Once that was done, I tracked down where the GetExternalLoginInfoAsync was being utilized and figured out I had to override the CreateExternalUserAsync method inside of the LoginModel for the Login page. To that end, I followed the directions in this article for creating a CustomLoginModel.cs and Login.cshtml : https://community.abp.io/articles/hide-the-tenant-switch-of-the-login-page-4foaup7p
So, my Auth0LoginModel class looks like this:
public class Auth0LoginModel : LoginModel
{
public Auth0LoginModel(IAuthenticationSchemeProvider schemeProvider, IOptions<AbpAccountOptions> accountOptions, IOptions<IdentityOptions> identityOptions) : base(schemeProvider, accountOptions, identityOptions)
{
}
protected override async Task<IdentityUser> CreateExternalUserAsync(ExternalLoginInfo info)
{
await IdentityOptions.SetAsync();
var emailAddress = info.Principal.FindFirstValue(AbpClaimTypes.Email);
/* Begin TenantId claim check */
var tenantId = info.Principal.FindFirstValue(AbpClaimTypes.TenantId);
if (!string.IsNullOrEmpty(tenantId))
{
try
{
CurrentTenant.Change(Guid.Parse(tenantId));
}
catch
{
await IdentitySecurityLogManager.SaveAsync(new IdentitySecurityLogContext()
{
Identity = IdentitySecurityLogIdentityConsts.IdentityExternal,
Action = "Unable to parse TenantId: " + tenantId
}) ;
}
}
/* End TenantId claim check */
var user = new IdentityUser(GuidGenerator.Create(), emailAddress, emailAddress, CurrentTenant.Id);
CheckIdentityErrors(await UserManager.CreateAsync(user));
CheckIdentityErrors(await UserManager.SetEmailAsync(user, emailAddress));
CheckIdentityErrors(await UserManager.AddLoginAsync(user, info));
CheckIdentityErrors(await UserManager.AddDefaultRolesAsync(user));
return user;
}
}
The code added is between the comments, the rest of the method was pulled from the source. So I look for the AbpClaimTypes.TenantId claim being present, and if it does I attempt to use the CurrentTenant.Change method to change the tenant prior to the call to create the new IdentityUser.
Once that is done, the user gets created in the correct tenant and everything flows like expected.

Copy a note from a task to a case

I have a manually invoked process which is tied to the account entity. This process has a number of steps. One of the first steps is to create a task and assign it to someone. It's expected that this person will add some notes and complete the task.
Further down the process, I have a step to create a service case. After this is created, I want to copy the note(s) from the task above to the newly created service case.
I have created a custom workflow activity to try and accomplish this. I have gotten as far as deploying it and using it within my process without any errors and it does copy content into the notes field of the service case, however it copies the title of the task, not the note content and I can't quite fathom out why.
public class CopyNotes : CodeActivity
{
protected override void Execute(CodeActivityContext executionContext)
{
//Create the context
IWorkflowContext context = executionContext.GetExtension<IWorkflowContext>();
IOrganizationServiceFactory serviceFactory = executionContext.GetExtension<IOrganizationServiceFactory>();
IOrganizationService service = serviceFactory.CreateOrganizationService(context.UserId);
//get the notes associated with the source entity
Guid copyFromId = CopyFrom.Get(executionContext).Id;
Guid copyToId = CopyTo.Get(executionContext).Id;
EntityCollection copyFromNotes = RetrieveNotes(service, copyFromId);
if (copyFromNotes.Entities.Any())
{
foreach (Entity e in copyFromNotes.Entities)
{
Entity newNote = new Entity("annotation");
newNote.Attributes["subject"] = e.Attributes["subject"];
newNote.Attributes["notetext"] = e.Attributes["notetext"];
newNote.Attributes["objectid"] = new EntityReference() { Id = copyToId, LogicalName = CopyTo.Get(executionContext).LogicalName };
}
}
}
private EntityCollection RetrieveNotes(IOrganizationService service, Guid relatedObject)
{
ConditionExpression condition = new ConditionExpression();
condition.AttributeName = "objectid";
condition.Operator = ConditionOperator.Equal;
condition.Values.Add(relatedObject.ToString());
ColumnSet columns = new ColumnSet("subject", "notetext");
QueryExpression query = new QueryExpression();
query.ColumnSet = columns;
query.EntityName = "annotation";
query.Criteria.AddCondition(condition);
EntityCollection results = service.RetrieveMultiple(query);
return results;
}
[RequiredArgument]
[ReferenceTarget("task")]
[Input("Copy notes from item")]
public InArgument<EntityReference> CopyFrom { get; set; }
[RequiredArgument]
[ReferenceTarget("incident")]
[Input("Copy notes to item")]
public InArgument<EntityReference> CopyTo { get; set; }
}
I think you need to actually create the newNote after defining it.
foreach (Entity e in copyFromNotes.Entities)
{
Entity newNote = new Entity("annotation");
newNote.Attributes["subject"] = e.Attributes["subject"];
newNote.Attributes["notetext"] = e.Attributes["notetext"];
newNote.Attributes["objectid"] = new EntityReference() { Id = copyToId, LogicalName = CopyTo.Get(executionContext).LogicalName };
service.Create(newNote);
}
Once I did that your code worked just fine creating a new note with both the title and note text.
You could do this with a standard workflow, if Async creation is fast enough.
Andreas

How can i force the DbUpdateConcurrencyException to be raised even if i am passing the FormCollection to my Post action method instead of an object

i have the following action method inside my asp.net mvc web application , which will raise a DbUpdateConcurrencyException as intended to handle any concurrent conflicts that might happen:-
[HttpPost]
public ActionResult Edit(Assessment a)
{ try
{
if (ModelState.IsValid)
{
elearningrepository.UpdateAssessment(a);
elearningrepository.Save();
return RedirectToAction("Details", new { id = a.AssessmentID });
}
}
catch (DbUpdateConcurrencyException ex)
{
var entry = ex.Entries.Single();
var clientValues = (Assessment)entry.Entity;
ModelState.AddModelError(string.Empty, "The record you attempted to edit was"
+ "modified by another user after you got the original value.");
}
catch (DataException)
{
ModelState.AddModelError(string.Empty, "Unable to save changes. Try again, and if the problem persists contact your system administrator.");
}
return View(a);}
but to avoid any over binding attacks i have define a [Bind(Include = "Date, Title")] on the object class, but this raised a problem to me as the above action method will return an exception even if no concurrent conflict occur becuase the model binder will not be able to bind the object ID and other values ,, so i have changed my action method to the following:-
[HttpPost]
public ActionResult Edit(int id, FormCollection collection)
{
Assessment a = elearningrepository.GetAssessment(id);
try
{
if (TryUpdateModel(a))
{
elearningrepository.UpdateAssessment(a);
elearningrepository.Save();
return RedirectToAction("Details", new { id = a.AssessmentID });
}
}
catch (DbUpdateConcurrencyException ex)
{
var entry = ex.Entries.Single();
var clientValues = (Assessment)entry.Entity;
ModelState.AddModelError(string.Empty, "The record you attempted to edit was"
+ "modified by another user after you got the original value.");
}
catch (DataException)
{ ModelState.AddModelError(string.Empty, "Unable to save changes. Try again, and if the problem persists contact your system administrator.");
}return View(a);
but writting the action method as in the second approach will not raise the DbUpdateConcurrencyException under any situation (even if a concurrency conflict occurs!!!).
so me question is how i can make sure that the DbUpdateConcurrencyException will be raised if any conflict occur and at the same time to make sure that no over binding attack might occur by defining [Bind(Include = "Date, Title")]?
thanks in advance for any help and suggestions .
BR
Stop using forms collection and use a view model, thats a far better approach.
Also I have an action filter I wrote to handle the concurrency exceptions (MVC4 handles entity exceptions now finally has pat of validation just not the concurrency exceptions). Its a work in progress but should work ok as is, that much has been tested : )
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Web.Mvc;
using System.Data.Entity.Infrastructure;
using System.Reflection;
namespace Gecko.Framework.Mvc.ActionFilters
{
/// <summary>
/// Author: Adam Tuliper
/// adam.tuliper#gmail.com
/// completedevelopment.blogspot.com
/// www.secure-coding.com
/// Use freely, just please retain original credit.
///
/// This attribute attempts to intercept DbUpdateConcurrencyException to write out original/new values
/// to the screen for the user to review.
/// It assumes the following:
/// 1. There is a [Timestamp] attribute on an entity framework model property
/// 2. The only differences that we care about from the posted data to the record currently in the database are
/// only yhe model state field. We do not have access to a model at this point, as an exception was raised so there was no
/// return View(model) that we have a model to process from.
/// As such, we have to look at the fields in the modelstate and try to find matching fields on the entity and then display the differences.
/// This may not work in all cases.
/// This class will look at your model to get the property names. It will then check your
/// Entities current values vs. db values for these property names.
/// The behavior can be changed.
/// </summary>
public class HandleConcurrencyException : FilterAttribute, IExceptionFilter //ActionFilterAttribute
{
private PropertyMatchingMode _propertyMatchingMode;
/// <summary>
/// This defines when the concurrencyexception happens,
/// </summary>
public enum PropertyMatchingMode
{
/// <summary>
/// Uses only the field names in the model to check against the entity. This option is best when you are using
/// View Models with limited fields as opposed to an entity that has many fields. The ViewModel (or model) field names will
/// be used to check current posted values vs. db values on the entity itself.
/// </summary>
UseViewModelNamesToCheckEntity = 0,
/// <summary>
/// Use any non-matching value fields on the entity (except timestamp fields) to add errors to the ModelState.
/// </summary>
UseEntityFieldsOnly = 1,
/// <summary>
/// Tells the filter to not attempt to add field differences to the model state.
/// This means the end user will not see the specifics of which fields caused issues
/// </summary>
DontDisplayFieldClashes = 2
}
/// <summary>
/// The main method, called by the mvc runtime when an exception has occured.
/// This must be added as a global filter, or as an attribute on a class or action method.
/// </summary>
/// <param name="filterContext"></param>
public void OnException(ExceptionContext filterContext)
{
if (!filterContext.ExceptionHandled && filterContext.Exception is DbUpdateConcurrencyException)
{
//Get original and current entity values
DbUpdateConcurrencyException ex = (DbUpdateConcurrencyException)filterContext.Exception;
var entry = ex.Entries.Single();
//problems with ef4.1/4.2 here because of context/model in different projects.
//var databaseValues = entry.CurrentValues.Clone().ToObject();
//var clientValues = entry.Entity;
//So - if using EF 4.1/4.2 you may use this workaround
var clientValues = entry.CurrentValues.Clone().ToObject();
entry.Reload();
var databaseValues = entry.CurrentValues.ToObject();
List<string> propertyNames;
filterContext.Controller.ViewData.ModelState.AddModelError(string.Empty, "The record you attempted to edit "
+ "was modified by another user after you got the original value. The "
+ "edit operation was canceled and the current values in the database "
+ "have been displayed. If you still want to edit this record, click "
+ "the Save button again to cause your changes to be the current saved values.");
PropertyInfo[] entityFromDbProperties = databaseValues.GetType().GetProperties(BindingFlags.FlattenHierarchy | BindingFlags.Public | BindingFlags.Instance);
if (_propertyMatchingMode == PropertyMatchingMode.UseViewModelNamesToCheckEntity)
{
//We dont have access to the model here on an exception. Get the field names from modelstate:
propertyNames = filterContext.Controller.ViewData.ModelState.Keys.ToList();
}
else if (_propertyMatchingMode == PropertyMatchingMode.UseEntityFieldsOnly)
{
propertyNames = databaseValues.GetType().GetProperties(BindingFlags.Public).Select(o => o.Name).ToList();
}
else
{
filterContext.ExceptionHandled = true;
UpdateTimestampField(filterContext, entityFromDbProperties, databaseValues);
filterContext.Result = new ViewResult() { ViewData = filterContext.Controller.ViewData };
return;
}
UpdateTimestampField(filterContext, entityFromDbProperties, databaseValues);
//Get all public properties of the entity that have names matching those in our modelstate.
foreach (var propertyInfo in entityFromDbProperties)
{
//If this value is not in the ModelState values, don't compare it as we don't want
//to attempt to emit model errors for fields that don't exist.
//Compare db value to the current value from the entity we posted.
if (propertyNames.Contains(propertyInfo.Name))
{
if (propertyInfo.GetValue(databaseValues, null) != propertyInfo.GetValue(clientValues, null))
{
var currentValue = propertyInfo.GetValue(databaseValues, null);
if (currentValue == null || string.IsNullOrEmpty(currentValue.ToString()))
{
currentValue = "Empty";
}
filterContext.Controller.ViewData.ModelState.AddModelError(propertyInfo.Name, "Current value: "
+ currentValue);
}
}
//TODO: hmm.... how can we only check values applicable to the model/modelstate rather than the entity we saved?
//The problem here is we may only have a few fields used in the viewmodel, but many in the entity
//so we could have a problem here with that.
}
filterContext.ExceptionHandled = true;
filterContext.Result = new ViewResult() { ViewData = filterContext.Controller.ViewData };
}
}
public HandleConcurrencyException()
{
_propertyMatchingMode = PropertyMatchingMode.UseViewModelNamesToCheckEntity;
}
public HandleConcurrencyException(PropertyMatchingMode propertyMatchingMode)
{
_propertyMatchingMode = propertyMatchingMode;
}
/// <summary>
/// Searches the database loaded entity values for a field that has a [Timestamp] attribute.
/// It then writes a string version of ther byte[] timestamp out to modelstate, assuming
/// we have a timestamp field on the page that caused the concurrency exception.
/// </summary>
/// <param name="filterContext"></param>
/// <param name="entityFromDbProperties"></param>
/// <param name="databaseValues"></param>
private void UpdateTimestampField(ExceptionContext filterContext, PropertyInfo[] entityFromDbProperties, object databaseValues)
{
foreach (var propertyInfo in entityFromDbProperties)
{
var attributes = propertyInfo.GetCustomAttributesData();
//If this is a timestamp field, we need to set the current value.
foreach (CustomAttributeData attr in attributes)
{
if (typeof(System.ComponentModel.DataAnnotations.TimestampAttribute).IsAssignableFrom(attr.Constructor.DeclaringType))
{
//This currently works only with byte[] timestamps. You can use dates as timestampts, but support is not provided here.
byte[] timestampValue = (byte[])propertyInfo.GetValue(databaseValues, null);
//we've found the timestamp. Add it to the model.
filterContext.Controller.ViewData.ModelState.Add(propertyInfo.Name, new ModelState());
filterContext.Controller.ViewData.ModelState.SetModelValue(propertyInfo.Name,
new ValueProviderResult(Convert.ToBase64String(timestampValue), Convert.ToBase64String(timestampValue), null));
break;
}
}
}
}
}
}
Do you know why I had to change
if (propertyInfo.GetValue(databaseValues, null) != propertyInfo.GetValue(clientValues, null))
to
if (!Equals(propertyInfo.GetValue(databaseValues,null), propertyInfo.GetValue(clientValues,null)))
It was giving me false positives for anything that was not a string.
After I made that change it worked for me.
Here is the complete modified function
public void OnException(ExceptionContext filterContext)
{
if (!filterContext.ExceptionHandled && filterContext.Exception is DbUpdateConcurrencyException)
{
//Get original and current entity values
DbUpdateConcurrencyException ex = (DbUpdateConcurrencyException)filterContext.Exception;
var entry = ex.Entries.Single();
//problems with ef4.1/4.2 here because of context/model in different projects.
//var databaseValues = entry.CurrentValues.Clone().ToObject();
//var clientValues = entry.Entity;
//So - if using EF 4.1/4.2 you may use this workaround
var clientValues = entry.CurrentValues.Clone().ToObject();
entry.Reload();
var databaseValues = entry.CurrentValues.ToObject();
List<string> propertyNames;
filterContext.Controller.ViewData.ModelState.AddModelError(string.Empty, "The record you attempted to edit "
+ "was modified by another user after you got the original value. The "
+ "edit operation was canceled and the current values in the database "
+ "have been displayed. If you still want to edit this record, click "
+ "the Save button again to cause your changes to be the current saved values.");
PropertyInfo[] entityFromDbProperties = databaseValues.GetType().GetProperties(BindingFlags.FlattenHierarchy | BindingFlags.Public | BindingFlags.Instance);
if (_propertyMatchingMode == PropertyMatchingMode.UseViewModelNamesToCheckEntity)
{
//We dont have access to the model here on an exception. Get the field names from modelstate:
propertyNames = filterContext.Controller.ViewData.ModelState.Keys.ToList();
}
else if (_propertyMatchingMode == PropertyMatchingMode.UseEntityFieldsOnly)
{
propertyNames = databaseValues.GetType().GetProperties(BindingFlags.Public).Select(o => o.Name).ToList();
}
else
{
filterContext.ExceptionHandled = true;
UpdateTimestampField(filterContext, entityFromDbProperties, databaseValues);
filterContext.Result = new ViewResult() { ViewData = filterContext.Controller.ViewData };
return;
}
UpdateTimestampField(filterContext, entityFromDbProperties, databaseValues);
//Get all public properties of the entity that have names matching those in our modelstate.
foreach (var propertyInfo in entityFromDbProperties)
{
//If this value is not in the ModelState values, don't compare it as we don't want
//to attempt to emit model errors for fields that don't exist.
//Compare db value to the current value from the entity we posted.
if (propertyNames.Contains(propertyInfo.Name))
{
//if (propertyInfo.GetValue(databaseValues, null) != propertyInfo.GetValue(clientValues, null))
if (!Equals(propertyInfo.GetValue(databaseValues,null), propertyInfo.GetValue(clientValues,null)))
{
var currentValue = propertyInfo.GetValue(databaseValues, null);
if (currentValue == null || string.IsNullOrEmpty(currentValue.ToString()))
{
currentValue = "Empty";
}
filterContext.Controller.ViewData.ModelState.AddModelError(propertyInfo.Name, "Database value: "
+ currentValue);
}
}
//TODO: hmm.... how can we only check values applicable to the model/modelstate rather than the entity we saved?
//The problem here is we may only have a few fields used in the viewmodel, but many in the entity
//so we could have a problem here with that.
}
filterContext.ExceptionHandled = true;
filterContext.Result = new ViewResult() { ViewData = filterContext.Controller.ViewData };
}
}

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.

Silverlight localized custom validation using DataAnnotations with RIA Services

I've implemented localized validation, client-side, using the DataAnnotations attributes successfully. Now, I want to implement custom validation running server-side using the CustomValidationAttribute but my problem is that I can't find a way to get the client-side culture while executing the validation.
Here's the setup for the custom validation method:
public static ValidationResult ValidateField( string fieldValue, ValidationContext validationContext )
{
#if !SILVERLIGHT
// Get the message from the ValidationResources resx.
return new ValidationResult( ValidationResources.Message, new string[]{ "Field" } );
#else
return ValidationResult.Success;
#endif
}
This code returns the message but from the culture that the server is currently set.
I also tried to set the attribute on the property this way with same result:
[CustomValidation( typeof( CustomValidation ), "ValidateField", ErrorMessageResourceName = "Message", ErrorMessageResourceType = typeof( ValidationResources ) )]
I also tried to expose a method on my DomainService to change the Culture on the ValidationResources resx but this seems to be changing the culture not only or the current connection but for all the connections.
Since the validation is ran by Ria Services and not something I am calling directly, how can I tell the validation method to use a specific culture?
I came across this thread and I was able to fix my issue and have the culture name pass to every request made by the DomainContext (client) to the server.
First, we need to create a custom IClientMessageInspector that will be responsible to set a parameter for the CurrentUICulture for every requests.
public class AppendLanguageMessageInspector : IClientMessageInspector
{
#region IClientMessageInspector Members
public void AfterReceiveReply( ref Message reply, object correlationState )
{
// Nothing to do
}
public object BeforeSendRequest( ref Message request, IClientChannel channel )
{
var property = request.Properties[ HttpRequestMessageProperty.Name ] as HttpRequestMessageProperty;
if( property != null )
{
property.Headers[ "CultureName" ] = Thread.CurrentThread.CurrentUICulture.Name;
}
return null;
}
#endregion // IClientMessageInspector Members
}
Next, we need to create a custom WebHttpBehavior that will inject our custom IClientMessageInspector.
public class AppendLanguageHttpBehavior : WebHttpBehavior
{
public override void ApplyClientBehavior( ServiceEndpoint endpoint, System.ServiceModel.Dispatcher.ClientRuntime clientRuntime )
{
clientRuntime.MessageInspectors.Add( _inspector );
}
private readonly AppendLanguageMessageInspector _inspector = new AppendLanguageMessageInspector();
}
Finally, we extend the client DomainContext.OnCreate method to add our custom WebHttpBehavior. NOTE: The namespace of the extended DomainContext class must be the same as the generated one...
public partial class DomainService1
{
partial void OnCreated()
{
var domainClient = this.DomainClient as WebDomainClient<IDomainService1Contract>;
if( domainClient != null )
{
domainClient.ChannelFactory.Endpoint.Behaviors.Add( DomainService1.AppendLanguageHttpBehavior );
}
}
private static readonly AppendLanguageHttpBehavior AppendLanguageHttpBehavior = new AppendLanguageHttpBehavior();
}
Now, on the server-side, when we want to get the language code we can simply access it like this:
var cultureName = System.Web.HttpContext.Current.Request.Headers[ "CultureName" ];
To enjoy even more of the DataAnnotation magic, we can even change the CurrentUICulture in the Initialize of the DomainService like this:
public override void Initialize( DomainServiceContext context )
{
var cultureName = System.Web.HttpContext.Current.Request.Headers[ "UICultureName" ];
Thread.CurrentThread.CurrentUICulture = new CultureInfo( cultureName );
base.Initialize( context );
}

Resources