Several features of the SCIM component require mapping SCIM attributes to properties on the relating type in an IScimStore
. During filtering, the mappings are used in the implementation of IScimQueryBuilderFactory
to filter on the correct property. When using the IPatchCommandExecutor
, they map the paths in an operation to their respective properties.
Mapping Properties
.MapScimAttributes<AppUser>(ScimSchemas.User, mapper =>
{
mapper
.Map("id", u => u.Id)
.Map("userName", u => u.Username)
.Map("name.familyName", u => u.LastName)
.Map("name.givenName", u => u.FirstName)
.Map("active", u => u.IsActive)
.Map("locale", u => u.Locale);
})
.MapScimAttributes<AppUser>(ScimSchemas.EnterpriseUser, mapper =>
{
mapper
.Map("department", u => u.Department);
})
.MapScimAttributes<AppRole>(ScimSchemas.Group, mapper =>
{
mapper
.Map("id", r => r.Id)
.Map("displayName", r => r.Name)
.MapCollection<Member>("members", r => r.Members, member =>
{
member
.Map("value", m => m.Value)
.Map("display", m => m.Display)
.Map("type", m => m.Type);
});
});
The above mapping works for directly mapping attribute values onto store properties. When the storage format for the attribute in the store is different, the comparison or assignment value used needs to be modified. Consider the following representation of a user.
public class AppUser
{
public string Id { get; }
public string? Username { get; set; }
public string? FirstName { get; set; }
public string? LastName { get; set; }
public string? Department { get; set; }
public bool IsDisabled { get; set; }
public string? NormalizedUsername { get; set; }
}
IsDisabled
models the inverse of the active attribute.
NormalizedUsername
models the username capitalized, to enable case insensitive matching.
In this case, a conversion must be performed during comparison or assignment relating to the username or active SCIM attributes. By supplying a converter when creating a mapping, when the property needs to be assigned to or compared against, a conversion will happen first:
.Map("active", u => u.IsDisabled , ScimFilterAttributeConverters.Inverse)
.Map("userName", u => u.NormalizedUsername , ScimFilterAttributeConverters.ToUpper)
The third optional argument provides the conversion logic. It's a function that takes a value and transforms it into a form compatible with the store's schema.
When compiling a SCIM filter of active eq true and username sw "Andy"
, the compiled filter will compare IsDisabled
with false
and NormalizedUsername
starts with "ANDY"
.
During patching with Add or Updates commands, when assigning to a property, the converter will be called before assignment. Given the patch command:
{
"op": "replace",
"path": "name",
"value": {
"active": true
}
}
When performing the assignment, the ScimFilterAttributeConverters.Inverse
converter would convert the value true
to false
before assigning to the IsDisabled
field.
An alternative to using the mapper is to provide an implementation of IScimAttributeToPropertyMapper
and register it with the DI container. You can register many instances with the DI container, and the compiler will try each one, in turn, to map a SCIM attribute into a .NET property.
public delegate object LiteralConverter(object toConvert);
/// <summary>
/// Used by the ScimQueryCompiler to map SCIM attributes into .NET properties
/// </summary>
public interface IScimAttributeToPropertyMapper
{
/// <summary>
/// Maps an attribute to a property expression, used to build a LINQ expression for filtering
/// resources
/// </summary>
/// <param name="schema">Schema of the attribute</param>
/// <param name="attribute">attribute path</param>
/// <param name="root">An expression describing how to get to the root .NET object for the attribute container</param>
/// <param name="expression">The expression that returns the value of the attriubte</param>
/// <returns>true if the map was sucessfull</returns>
bool TryMapAttribute(string schema,string attribute, Expression root , out MemberExpression expression);
/// <summary>
/// Returns a literal value converter for the attribute, used to modify a literal value used in a filter
/// prior to any comparison
/// </summary>
/// <param name="type">Return type of the .NET property</param>
/// <param name="schema">Schema of the attribute</param>
/// <param name="attribute">attribute path</param>
/// <param name="converter">conversion function</param>
/// <returns>true if a converter exists</returns>
bool TryMapAttributeValueConverter(Type type , string schema, string attribute, out LiteralConverter converter);
}
New Mapping Functions
Since version 4.0 complex types can be mapped inline.
.MapScimAttributes<AppUser>(ScimSchemas.User, mapper =>
{
mapper.MapComplex("name",u=>u.Name,im=>
{
im.Map("givenName", i => i.GivenName)
.Map("familyName", i => i.FamilyName);
})
});
Complex types can also be mapped in advanced and mapped onto multiple root attributes. The code fragment below describes how to map a complex attribute that describes a location onto a .NET type Location
. The home and office map statements, map onto properties Home and Office of type Location
.
e.g.
home.house => AppUser.Home.House
office.house => AppUser.Office.House
.MapScimAttributes<AppUser>(ScimSchemas.User, mapper =>
{
mapper.MapType<Location>(lm =>
{
lm.Map("house", h => h.House)
.Map("street", h => h.Street)
.Map("town", h => h.Town);
})
.Map("home", u => u.Home)
.Map("office", u => u.Office)
});
The same technique can also be used for mapping collections. Use MapType
for the collection type, and use the Map
function to map the navigation to the collection.