When the user registers in my web application, they have to enter three things: Their username, company name and password. Now, what I to do next with that information is entirely dependent how I go about implementing this. Ideally, I want to the user to have two profiles: User and company profile. Sensenet already comes with a built in user profile and looking at the content type definition, this is how it is defined.
<?xml version="1.0" encoding="utf-8"?>
<ContentType name="UserProfile" parentType="Workspace" handler="SenseNet.ContentRepository.UserProfile" xmlns="http://schemas.sensenet.com/SenseNet/ContentRepository/ContentTypeDefinition">
<DisplayName>$Ctd-UserProfile,DisplayName</DisplayName>
<Description>$Ctd-UserProfile,Description</Description>
<Icon>UserProfile</Icon>
<AllowedChildTypes>
Blog,DocumentLibrary,EventList,MemoList,LinkList,TaskList,ImageLibrary,Posts,CustomList
</AllowedChildTypes>
<Fields>
<Field name="IsWallContainer" type="Boolean">
<Configuration>
<VisibleBrowse>Advanced</VisibleBrowse>
<VisibleEdit>Advanced</VisibleEdit>
<VisibleNew>Show</VisibleNew>
<DefaultValue>true</DefaultValue>
</Configuration>
</Field>
<Field name="IsCritical" type="Boolean">
<Configuration>
<VisibleBrowse>Hide</VisibleBrowse>
<VisibleEdit>Hide</VisibleEdit>
<VisibleNew>Hide</VisibleNew>
</Configuration>
</Field>
<Field name="Manager" type="Reference">
<Configuration>
<VisibleBrowse>Hide</VisibleBrowse>
<VisibleEdit>Hide</VisibleEdit>
<VisibleNew>Hide</VisibleNew>
</Configuration>
</Field>
<Field name="Deadline" type="DateTime">
<Configuration>
<VisibleBrowse>Hide</VisibleBrowse>
<VisibleEdit>Hide</VisibleEdit>
<VisibleNew>Hide</VisibleNew>
</Configuration>
</Field>
<Field name="IsActive" type="Boolean">
<Configuration>
<VisibleBrowse>Hide</VisibleBrowse>
<VisibleEdit>Hide</VisibleEdit>
<VisibleNew>Hide</VisibleNew>
</Configuration>
</Field>
<Field name="User" type="Reference">
<DisplayName>$Ctd-UserProfile,User-DisplayName</DisplayName>
<Configuration>
<AllowMultiple>false</AllowMultiple>
<AllowedTypes>
<Type>User</Type>
</AllowedTypes>
<SelectionRoot>
<Path>/Root/IMS</Path>
</SelectionRoot>
</Configuration>
</Field>
</Fields>
</ContentType>
What catches my attention is the reference field that references the user that is attached to the user profile. So with that knowledge, maybe it would best to create a separate content type definition with the same reference field as this one so that it is tied to the same user. Another direction that I can go is to extend the User Profile content-type definition xml file to have the company's information embedded into it instead of having a separate definition file for it but the problem is that now I'll be change as to what a user profile is and probably include way too much information.
The most important thing is that no other user can be tied to the same company as it has to be unique. Considering all of this, which of these methods do you think is the best way going about doing this?
So I've made some progress on making a company profile for the user. I've created this content handler for a company profile.
using SenseNet.ContentRepository.Schema;
using SenseNet.ContentRepository.Storage;
using SenseNet.ContentRepository.Workspaces;
namespace DerAssistantService.ContentHandlers
{
[ContentHandler]
public class CompanyProfile : Workspace
{
public CompanyProfile(Node parent) : this(parent, null) { }
public CompanyProfile(Node parent, string nodeTypeName) : base(parent, nodeTypeName) { }
protected CompanyProfile(NodeToken nt) : base(nt) { }
public override string Name
{
get { return base.Name;}
set { base.Name = value;}
}
[RepositoryProperty("Address")]
public string Address
{
get { return GetProperty<string>("Address"); }
set { this["Address"] = value; }
}
[RepositoryProperty("City")]
public string City
{
get { return GetProperty<string>("City"); }
set { this["City"] = value; }
}
[RepositoryProperty("State")]
public string State
{
get { return GetProperty<string>("State"); }
set { this["State"] = value; }
}
public override object GetProperty(string name)
{
switch (name)
{
case "Address":
return Address;
case "City":
return City;
case "State":
return State;
default:
return base.GetProperty(name);
}
}
public override void SetProperty(string name, object value)
{
switch (name)
{
case "Address":
Address = (string)value;
break;
case "City":
City = (string)value;
break;
case "State":
State = (string)value;
break;
default:
base.SetProperty(name, value);
break;
}
}
}
}
When they register, the company profile gets created under the domain Company.
using System;
using System.Linq;
using SenseNet.ApplicationModel;
using SenseNet.ContentRepository;
using SenseNet.ContentRepository.Storage;
using SenseNet.ContentRepository.Storage.Data;
using SenseNet.ContentRepository.Storage.Security;
namespace DerAssistantService.Actions
{
public static class UserActions
{
[ODataAction]
public static Content RegisterUser(Content content, string email, string companyname, string password)
{
if (string.IsNullOrEmpty(email))
throw new ArgumentNullException(nameof(email));
if (string.IsNullOrEmpty(companyname))
throw new ArgumentNullException(nameof(companyname));
if (string.IsNullOrEmpty(password))
throw new ArgumentNullException(nameof(password));
var username = email.Split('@').First();
var isUserCreated = Node.LoadNode("Root/IMS/Public/" + username);
var isCompanyProfileCreated = Node.LoadNode("Root/Profiles/Company" + companyname);
if (isUserCreated != null)
{
throw new NodeAlreadyExistsException("There already exists a user with this name.");
}
if (isCompanyProfileCreated != null)
{
throw new NodeAlreadyExistsException("There already exists a company with this name.");
}
using (new SystemAccount())
{
var user = Content.CreateNew("User", content.ContentHandler, username);
user["FullName"] = username;
user["Email"] = email;
user["LoginName"] = email;
user["Enabled"] = true;
user["Password"] = password;
user.Save();
var parent = Node.LoadNode("Root/Profiles/Company");
var companyProfile = Content.CreateNew("CompanyProfile", parent, companyname);
companyProfile["Name"] = companyname;
companyProfile.Save();
var identifiedUsers = Node.Load<Group>("/Root/IMS/BuiltIn/Portal/IdentifiedUsers");
identifiedUsers.AddMember(user.ContentHandler as IUser);
identifiedUsers.Save();
return user;
}
}
}
}
and I created this content type definition for it.
<ContentType name="CompanyProfile" parentType="Workspace" handler="DerAssistantService.ContentHandlers.CompanyProfile" xmlns="http://schemas.sensenet.com/SenseNet/ContentRepository/ContentTypeDefinition">
<DisplayName>CompanyProfile</DisplayName>
<Description>This content contains basic information on a company</Description>
<Icon>Company</Icon>
<Fields>
<Field name="Name" type="ShortText">
<DisplayName>Company</DisplayName>
<Description>The name of the company</Description>
<Configuration>
<VisibleBrowse>Show</VisibleBrowse>
<VisibleEdit>Show</VisibleEdit>
<VisibleNew>Show</VisibleNew>
<Compulsory>true</Compulsory>
</Configuration>
</Field>
<Field name="Address" type="ShortText">
<DisplayName>Address</DisplayName>
<Description>The location of the company</Description>
<Configuration>
<VisibleBrowse>Show</VisibleBrowse>
<VisibleEdit>Show</VisibleEdit>
<VisibleNew>Show</VisibleNew>
<Compulsory>true</Compulsory>
</Configuration>
</Field>
<Field name="City" type="ShortText">
<DisplayName>City</DisplayName>
<Description>The city where the company is located at</Description>
<Configuration>
<VisibleBrowse>Show</VisibleBrowse>
<VisibleEdit>Show</VisibleEdit>
<VisibleNew>Show</VisibleNew>
<Compulsory>true</Compulsory>
</Configuration>
</Field>
<Field name="State" type="ShortText">
<DisplayName>State</DisplayName>
<Description>The state the company resides in</Description>
<Configuration>
<VisibleBrowse>Show</VisibleBrowse>
<VisibleEdit>Show</VisibleEdit>
<VisibleNew>Show</VisibleNew>
<Compulsory>true</Compulsory>
</Configuration>
</Field>
</Fields>
</ContentType>
The problem, there is no way I can load the company profile easily. There is no connection between because there is both in separate domains with no information on each other. How do I reconcile this?
I finally have a working solution after a bit of experimentation.
Here is how my company profile looks now.
<?xml version="1.0" encoding="utf-8"?>
<ContentType name="CompanyProfile" parentType="UserProfile" handler="DerAssistantService.ContentHandlers.CompanyProfile" xmlns="http://schemas.sensenet.com/SenseNet/ContentRepository/ContentTypeDefinition">
<DisplayName>CompanyProfile</DisplayName>
<Description>This profile contains contains a single reference to a company that has registered itself in the web app.</Description>
<Icon>UserProfile</Icon>
<AllowedChildTypes>DocumentLibrary,EventList,MemoList,LinkList,TaskList,ImageLibrary,CustomList</AllowedChildTypes>
<Fields>
<Field name="Company" type="Reference">
<DisplayName>Company</DisplayName>
<Configuration>
<AllowMultiple>false</AllowMultiple>
<AllowedTypes>
<Type>Company</Type>
</AllowedTypes>
<SelectionRoot>
<Path>/Root/IMS</Path>
</SelectionRoot>
</Configuration>
</Field>
</Fields>
</ContentType>
Now the company profile extends the user profile so that I still have a reference to the user object and has a new reference property for a company object.
Now I have a separate content type definition file for a company.
<?xml version="1.0" encoding="utf-8"?>
<ContentType name="Company" parentType="Workspace" handler="DerAssistantService.ContentHandlers.Company" xmlns="http://schemas.sensenet.com/SenseNet/ContentRepository/ContentTypeDefinition">
<DisplayName>Company</DisplayName>
<Description>This content contains basic information on a particular company</Description>
<Icon>Company</Icon>
<AllowedChildTypes>Image</AllowedChildTypes>
<Fields>
<Field name="Address" type="ShortText">
<DisplayName>Address</DisplayName>
<Description>The location of the company</Description>
<Configuration>
<VisibleBrowse>Show</VisibleBrowse>
<VisibleEdit>Show</VisibleEdit>
<VisibleNew>Show</VisibleNew>
<Compulsory>true</Compulsory>
</Configuration>
</Field>
<Field name="City" type="ShortText">
<DisplayName>City</DisplayName>
<Description>The city where the company resides in</Description>
<Configuration>
<VisibleBrowse>Show</VisibleBrowse>
<VisibleEdit>Show</VisibleEdit>
<VisibleNew>Show</VisibleNew>
<Compulsory>true</Compulsory>
</Configuration>
</Field>
<Field name="State" type="ShortText">
<DisplayName>State</DisplayName>
<Description>The state the company resides in</Description>
<Configuration>
<VisibleBrowse>Show</VisibleBrowse>
<VisibleEdit>Show</VisibleEdit>
<VisibleNew>Show</VisibleNew>
<Compulsory>true</Compulsory>
</Configuration>
</Field>
</Fields>
</ContentType>
This is how my content handler for CompanyProfile looks
using SenseNet.ContentRepository;
using SenseNet.ContentRepository.Schema;
using SenseNet.ContentRepository.Storage;
namespace DerAssistantService.ContentHandlers
{
[ContentHandler]
public class CompanyProfile : UserProfile
{
public CompanyProfile(Node parent) : this(parent, null) { }
public CompanyProfile(Node parent, string nodeTypeName) : base(parent, nodeTypeName) { }
protected CompanyProfile(NodeToken token) : base(token) { }
[RepositoryProperty("Company", RepositoryDataType.Reference)]
public Company Company
{
get { return GetReference<Company>("Company"); }
set { SetReference("Company", value); }
}
public override object GetProperty(string name)
{
switch (name)
{
case "Company":
return Company;
default:
return base.GetProperty(name);
}
}
public override void SetProperty(string name, object value)
{
switch (name)
{
case "Company":
Company = (Company)value;
break;
default:
base.SetProperty(name, value);
break;
}
}
}
}
and how the content handler for my company class looks like.
using SenseNet.ContentRepository.Schema;
using SenseNet.ContentRepository.Storage;
using SenseNet.ContentRepository.Workspaces;
namespace DerAssistantService.ContentHandlers
{
[ContentHandler]
public class Company : Workspace
{
public Company(Node parent) : this(parent, null) { }
public Company(Node parent, string nodeTypeName) : base(parent, nodeTypeName) { }
protected Company(NodeToken token) : base(token) { }
[RepositoryProperty("Address", RepositoryDataType.String)]
public string Address
{
get { return GetProperty<string>("Address"); }
set { this["Address"] = value; }
}
[RepositoryProperty("City", RepositoryDataType.String)]
public string City
{
get { return GetProperty<string>("City"); }
set { this["City"] = value; }
}
[RepositoryProperty("State", RepositoryDataType.String)]
public string State
{
get { return GetProperty<string>("State"); }
set { this["State"] = value; }
}
public override object GetProperty(string name)
{
switch (name)
{
case "Address":
return Address;
case "City":
return City;
case "State":
return State;
default:
return base.GetProperty(name);
}
}
public override void SetProperty(string name, object value)
{
switch (name)
{
case "Address":
Address = (string)value;
break;
case "City":
City = (string)value;
break;
case "State":
State = (string)value;
break;
default:
base.SetProperty(name, value);
break;
}
}
}
}
I separated the logic in my RegisterUser Method as it was doing too much.
My RegisterUser function now looks like this.
[ODataAction]
public static Content RegisterUser(Content content, string email, string password)
{
using (new SystemAccount())
{
var username = email.Split('@').First();
var user = CreateUser("Public", email, password, username, true);
var identifiedUsers = Node.Load<Group>("/Root/IMS/BuiltIn/Portal/IdentifiedUsers");
identifiedUsers.AddMember(user.ContentHandler as IUser);
identifiedUsers.Save();
return user;
}
}
private static Content CreateUser(string domainName, string username, string password, string fullname, bool enabled, Dictionary<string, object> properties = null)
{
var domainPath = RepositoryPath.Combine(RepositoryStructure.ImsFolderPath, domainName);
var domain = Node.LoadNode(domainPath);
var user = Content.CreateNew("User", domain, username);
user["Name"] = username;
user["Password"] = password;
user["FullName"] = fullname;
user["Enabled"] = enabled;
if (properties != null)
{
foreach (var key in properties.Keys)
{
user[key] = properties[key];
}
}
user.Save();
return user;
}
}
I created a separate function that is in charge of register the company after the user gets created simply because I need the user object to get created so that it can create the company profile.
[ODataAction]
public static Content RegisterCompany(Content content, string companyName, string userEmail)
{
using (new SystemAccount())
{
CompanyProfile companyProfile = Node.LoadNode("/Root/Profiles/Public/" + userEmail) as CompanyProfile;
Company company = CreateCompany(companyName, companyProfile);
var companyContent = Content.Create(company);
companyContent.Save();
companyProfile.Company = company;
companyProfile.Save();
return companyContent;
}
}
private static Company CreateCompany(string companyName, CompanyProfile companyProfile)
{
var parent = Node.LoadNode("/Root/IMS/Company");
Company company = new Company(parent);
company.Name = companyName;
company.Address = "N/A";
company.City = "N/A";
company.State = "N/A";
company.VersionCreatedBy = companyProfile.User;
company.VersionModifiedBy = companyProfile.User;
company.CreatedBy = companyProfile.User;
company.ModifiedBy = companyProfile.User;
company.Owner = companyProfile.User;
return company;
}
}
With this, If I were to make a request to RegisterUser and RegisterCompany in that order, the company profile will now have a reference to the company object that was created. Please, tell me if there is any other way I can restructure this.
I think you need to reverse the reference direction. For example, the user works at a company, the company can have a profile. So the user can use a single reference field for targeting a company or the company's profile. This mechanism provides easy access in both directions: if you have the user instance user.Company
or user.CompanyProfile
returns the target object. Backward direction can be accessed with a simple query something like this: Content.All.OfType<User>().Where(c => c.Company == company).FirstOrDefault()
.