After reading/watching various introductions and blog posts and some code here and there, doing a bit of the Try F# tutorial and starting to read "Real World Functional Programming", I felt I should be able to write a first small real-world application in F#.
In order to be able to concentrate on getting to grips with the language, I decided not to try and solve a new problem, but rather port an existing C# application to what I hope is tolerably idiomatic F#.
The "application" is a so-called "plugin" for Microsoft Dynamics CRM. (A plugin is an implementation of the IPlugin
interface that is then registered to be called on certain platform operations, maybe best compared to a database trigger.)
For C# plugins, I have a base library for the general infrastructure stuff, while this particular plugin itself was (according to NCrunch) 613 lines of C# code. For the F# implementation, I decided not to use that other library, but rather reimplemented what I needed here in F# as well. The result was 68 lines of F# code for the plugin together with a new small F# library with 97 lines so far.
Porting the C# code wasn't that hard overall, because my SOLID classes pretty much map directly to functions. This would probably be much different for more "traditional" object-oriented code.
I have not been able to test it, but the code has become so concise and clear that I'm pretty sure it works; but that's not my main concern anyway.
I'm primarily interested in these things:
- How idiomatic is the F# code? I love the pattern matching and use any chance I can get to use it. Is that a good idea in all cases? What about naming of functions (some of those will certainly be odd, because I kept the class names from the C# version) and values?
- For the composed functions that end up taking no parameter of their own (those were just classes with constructor injected dependencies and a read-only public property in C#), would it be more natural to get the result on the spot and pass that on rather than passing the function?
- In my SOLID C# code, I build extensive object graphs via constructor injection, wired up using an IoC container in the application's composition root. I functional F# code, I have neither interfaces nor constructors, so I understand that constructor injection is essentially replaced by function composition, but I'm still unclear on how to do that in a real-world application in a way that is similarly maintainable and evolvable as what I have in C# with a composition root. I have posted an elaborate question on this here on Stack Overflow, so I won't repeat my further thoughts on that here, but I have employed what I described there as the "registry" in the
Composition
module. This is essentially a composition root using "poor man's DI" (as Mark Seemann calls it). Is this a good idea? What alternatives are there?
This is the library:
namespace Dymanix
module Mscrm =
open System
open Microsoft.Xrm.Sdk
open Microsoft.Xrm.Sdk.Messages
type CrmRecord =
| Entity of Entity
| Reference of EntityReference
type PluginMessage =
| Create
| Update
| Delete
| Other of string
type PreStage =
| PreValidation
| PreOperation
type Stage =
| Pre of PreStage
| PostOperation
type StepStage =
{ Message : PluginMessage
Stage : Stage }
type FullRecordImage = Entity
type TargetRecord = Entity
let MergeRecords (baseRecord : Entity) (toMerge : Entity) =
let mergeTarget = Microsoft.Xrm.Sdk.Entity(baseRecord.LogicalName, Id = baseRecord.Id)
toMerge.Attributes |> Seq.iter (fun att -> mergeTarget.[att.Key] <- att.Value)
mergeTarget
let GetMessage(context : IPluginExecutionContext) =
match context.MessageName with
| "Create" -> Create
| "Update" -> Update
| "Delete" -> Delete
| message -> Other(message)
let GetStage(context : IPluginExecutionContext) =
match context.Stage with
| 10 -> Pre(PreValidation)
| 20 -> Pre(PreOperation)
| 40 -> PostOperation
| _ -> failwith "Invalid plugin stage"
let GetStepStage(context : IPluginExecutionContext) =
{ Message = GetMessage context
Stage = GetStage context }
let GetTarget(context : IPluginExecutionContext) = context.InputParameters.["Target"] :?> TargetRecord
let GetFullRecordImage(context : IPluginExecutionContext) =
match GetStepStage context with
| { Message = Delete; Stage = _ } -> context.PreEntityImages.["Image"]
| { Message = Create; Stage = Pre(_) } -> GetTarget context
| { Message = Update; Stage = Pre(_) } -> MergeRecords context.PreEntityImages.["Image"] (GetTarget context)
| { Message = _; Stage = Pre(_) } -> context.PreEntityImages.["Image"]
| { Message = _; Stage = PostOperation } -> context.PostEntityImages.["Image"]
let DecomposeServiceProvider(serviceProvider : IServiceProvider) =
let organizationServiceFactory =
serviceProvider.GetService typeof<IOrganizationServiceFactory> :?> IOrganizationServiceFactory
let context = serviceProvider.GetService typeof<IPluginExecutionContext> :?> IPluginExecutionContext
let organizationService = organizationServiceFactory.CreateOrganizationService(System.Nullable(context.UserId))
let tracingService = serviceProvider.GetService typeof<ITracingService> :?> ITracingService
(context, organizationService, tracingService, organizationServiceFactory)
module Utility =
let GetPrimaryNameField (organizationService : IOrganizationService) entityName =
let request = RetrieveEntityRequest(LogicalName = entityName)
let response = organizationService.Execute(request) :?> RetrieveEntityResponse
response.EntityMetadata.PrimaryNameAttribute
let private getRecordName getPrimaryNameField (organizationService : IOrganizationService) record =
let reference =
match record with
| Entity(e) -> e.ToEntityReference()
| Reference(r) -> r
let primaryNameField = getPrimaryNameField organizationService reference.LogicalName
let entity =
match record with
| Entity(e) -> e
| Reference(r) ->
organizationService.Retrieve(r.LogicalName, r.Id, Query.ColumnSet([| primaryNameField |]))
string (entity.GetAttributeValue<string>(primaryNameField))
let GetRecordName(organizationService : IOrganizationService) =
getRecordName GetPrimaryNameField organizationService
And this is the plugin code - the actual "application":
namespace SetClassificationIdentificationFields
open System
open Microsoft.Xrm.Sdk
open Microsoft.Xrm.Sdk.Messages
open System.Xml.Linq
open Dymanix.Mscrm
open Dymanix.Mscrm.Utility
module Domain =
let (!<>) name = XName.Get(name)
let NameAddedRegardingObject getRecordName (innerRegardingObject : unit -> EntityReference option) =
let reference = innerRegardingObject()
match reference with
| Some(record) ->
if Guid.Empty <> record.Id then record.Name <- getRecordName ((Reference(record)))
| _ -> ()
reference
let ListFindingRegardingObject regardingLookups (image : FullRecordImage) =
let existingLookups = regardingLookups() |> List.filter image.Contains
match existingLookups with
| [] -> None
| [ a ] -> Some(image.GetAttributeValue<EntityReference>(a))
| _ -> failwith "Only one parent lookup may contain data"
let ConfiguredRegardingLookups(configuration : XElement) =
configuration.Element(!<>"regardinglookups").Elements(!<>"lookup")
|> List.ofSeq
|> List.map (fun e -> e.Attribute(!<>"name").Value)
let AllRequiredAttributesArePresent(target : TargetRecord) =
[ "gcnm_recordid"; "gcnm_recordtype"; "gcnm_recordname" ] |> List.forall target.Contains
let SetClassificationIdentificationValues (target : TargetRecord) (reference : EntityReference option) =
match reference with
| Some(record) ->
target.["gcnm_recordname"] <- record.Name
target.["gcnm_recordtype"] <- record.LogicalName
target.["gcnm_recordid"] <- record.Id.ToString()
| None -> ()
let Main requiredAttributesArePresent getTarget regardingObject setValues =
if (not (requiredAttributesArePresent getTarget)) then setValues (regardingObject)
module Composition =
open Domain
let Compose serviceProvider configuration =
let (context, organizationService, _, _) = DecomposeServiceProvider serviceProvider
let configurationXml = XElement.Parse configuration
let getRecordName = GetRecordName organizationService
let regardingLookups() = ConfiguredRegardingLookups configurationXml
let listFindingRegardingObject() = ListFindingRegardingObject regardingLookups (GetFullRecordImage(context))
let regardingObject = NameAddedRegardingObject getRecordName listFindingRegardingObject
let setValues = SetClassificationIdentificationValues(GetTarget context)
let allAttributesPresent = AllRequiredAttributesArePresent
let main() = Main allAttributesPresent (GetTarget context) regardingObject setValues
main
module Plugin =
type Plugin(configuration) =
member this.Configuration = configuration
interface IPlugin with
member this.Execute serviceProvider =
let main = Composition.Compose serviceProvider this.Configuration
main()