All Enumerables, Interfaces and Core Utilities should be stored in this
project. No code in this project should be dependent on any other
project in the solution.
/// <summary>An enumerable of Cart Types</summary>
public enum CartType
{
/// <summary>The enumeration value representing Shopping Carts</summary>
[Description("Shopping Cart"]
Shopping = 0,
/// <summary>The enumeration value representing Samples Carts</summary>
[Description("Samples Cart"]
Samples = 1,
/// <summary>The enumeration value representing Quote Requests</summary>
[Description("Quote Request"]
QuoteRequest = 2
}
All code pertaining to the CEF Database as generated by EntityFramework
6.1.3 must be stored in this project. This project may not be dependent
on any other project in the solution except for the core
Clarity.Ecommerce project, which will contain Interfaces used by this
project.
// == File must have a copyright tag by Clarity unless the code is from a third party ==
// <copyright file="Account.cs" company="clarityventures.com">
// Copyright (c) 2015 Clarity Ventures, Inc. All rights reserved.
// </copyright>
// <summary>Implements the account class</summary>
namespace Clarity.Ecommerce.DataModel
{
// == Usings must be inside the namespace and sorted alphabetically, with System namespaces coming first ==
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
[Table("Accounts.Account")] // == Defines the Schema and the table name at once ==
public class Account // Future Versions would have this inherit from a shared entity base and an IAccount interface from Clarity.Ecommerce
{
public Account()
{
// Assigns default HashSets for use by EF
AccountAddresses = new HashSet<AccountAddress>();
AccountPricePoints = new HashSet<AccountPricePoint>();
AccountReviews = new HashSet<AccountReview>();
AccountAttributes = new HashSet<AccountAttribute>();
ChildAccounts = new HashSet<Account>();
NotificationMessages = new HashSet<NotificationMessage>();
PurchaseOrders = new HashSet<PurchaseOrder>();
QuoteRequests = new HashSet<QuoteRequest>();
Users = new HashSet<User>();
}
// Future versions will inherit these properties from SharedEntityBase
public int ID { get; set; }
[StringLength(100)]
public string CustomKey { get; set; }
[Column(TypeName = "smalldatetime")]
public DateTime CreatedDate { get; set; }
[Column(TypeName = "smalldatetime")]
public DateTime? UpdatedDate { get; set; }
public bool Active { get; set; }
// Future versions will inherit these properties from NameableSharedEntityBase
[StringLength(200)]
public string Name { get; set; }
public string Description { get; set; }
// Future versions will inherit these properties from ContactableSharedEntityBase
[StringLength(50)]
public string Phone { get; set; }
[StringLength(50)]
public string Fax { get; set; }
[StringLength(1000)]
public string Email { get; set; }
// Account Specific Properties
[StringLength(1000)]
public string Website { get; set; }
[StringLength(1000)]
public string SeoKeywords { get; set; }
[StringLength(1000)]
public string SeoUrl { get; set; }
// Related Objects == Group them together with their foreign key identifiers
public int? ParentAccountID { get; set; }
public virtual Account ParentAccount { get; set; }
public virtual Company Company { get; set; } // Note: Company.ID will match this.ID
public int AccountTypeID { get; set; }
public virtual AccountType AccountType { get; set; }
// Associated Objects
public virtual ICollection<Account> ChildAccounts { get; set; } // ChildAccounts[0].ParentID is how this is associated
public virtual ICollection<AccountAddress> AccountAddresses { get; set; }
public virtual ICollection<AccountPricePoint> AccountPricePoints { get; set; }
public virtual ICollection<AccountReview> AccountReviews { get; set; }
public virtual ICollection<AccountAttribute> AccountAttributes { get; set; }
public virtual ICollection<NotificationMessage> NotificationMessages { get; set; }
public virtual ICollection<PurchaseOrder> PurchaseOrders { get; set; }
public virtual ICollection<QuoteRequest> QuoteRequests { get; set; }
public virtual ICollection<User> Users { get; set; }
}
}
This project will contain all the Concrete models for transferring data
between EntityFramework's output and the Services Layer. ServiceStack
provides DTOs which will inherit from these to maintain property
matching with easy maintenance.
namespace Clarity.Ecommerce.Models.Account
{
using System;
using Clarity.Ecommerce.Models.Address;
using Clarity.Ecommerce.Models.Attribute;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using Clarity.Ecommerce.Models.Generic;
public class AccountModel
{
public int? ID { get; set; }
public int? ParentID { get; set; }
public string Name { get; set; }
public int AccountTypeID { get; set; }
public string CustomKey { get; set; }
public string Description { get; set; }
public string Phone { get; set; }
public string Fax { get; set; }
public string Email { get; set; }
public string Website { get; set; }
public string SEOUrl { get; set; }
public List<AddressModel> Addresses { get; set; }
public List<AttributeModel> Attributes { get; set; }
public List<FileModel> Images { get; set; }
#region Mappings
public static readonly Func<DataModel.Account, AccountModel> Map = a => new AccountModel
{
ID = a.ID,
Name = a.Name,
AccountTypeID = a.AccountTypeID,
CustomKey = a.CustomKey,
Description = a.Description,
Phone = a.Phone,
Fax = a.Fax,
Email = a.Email,
Website = a.Website,
ParentID = a.ParentID,
SEOUrl = a.SeoUrl,
Addresses = a.AccountAddresses.Where(AddressModel.ActiveAccountAddressesOnly).Select(AddressModel.Map).ToList(),
Attributes = a.AccountAttributes.Where(AttributeModel.ActiveAccountAttributesOnly).Select(AttributeModel.AccountAttributeMap).ToList(),
Images = a.Company != null?a.Company.CompanyImages.Where(FileModel.ActiveCompanyImagesOnly).Select(FileModel.CompanyImageMap).ToList():new List<FileModel>()
};
internal Func<DataModel.Account, AccountModel> MapLite = a => new AccountModel
{
ID = a.ID,
Name = a.Name,
AccountTypeID = a.AccountTypeID,
CustomKey = a.CustomKey,
Description = a.Description,
Phone = a.Phone,
Fax = a.Fax,
Email = a.Email,
Website = a.Website,
ParentID = a.ParentID,
SEOUrl = a.SeoUrl,
Addresses = a.AccountAddresses.Where(AddressModel.ActiveAccountAddressesOnly).Select(AddressModel.Map).ToList(),
};
#endregion
}
public static class AccountExtensions
{
public static IQueryable<DataModel.Account> FilterByActive(this IQueryable<DataModel.Account> query)
{
return query.Where(p => p.Active);
}
public static IQueryable<DataModel.Account> FilterByCustomKey(this IQueryable<DataModel.Account> query, string customKey)
{
return !string.IsNullOrWhiteSpace(customKey) ?
query.Where(p => p.CustomKey.ToLower() == customKey.ToLower())
: query;
}
public static IQueryable<DataModel.Account> FilterByName(this IQueryable<DataModel.Account> query, string name)
{
return !string.IsNullOrWhiteSpace(name) ?
query.Where(p => p.Name.ToLower().Contains(name.ToLower()))
: query;
}
public static IQueryable<DataModel.Account> FilterByID(this IQueryable<DataModel.Account> query, int ID)
{
return query.Where(p => p.ID == ID);
}
}
}
Integration for Payment is accomplished through the use of
PaymentProviders. PaymentProviders implement the IPaymentProvider
interface.
In addition to the main PaymentProvider interface there are additional
interfaces that may be implemented to provide additional features.
This project should contain all the business logic for the platform.
namespace Clarity.Ecommerce.Workflow.AccountWorkflows
{
using System;
using System.Linq;
using System.Collections.Generic;
using System.Data.Entity.Core.Objects.DataClasses;
using Clarity.Ecommerce.DataModel;
using Clarity.Ecommerce.Models.Account;
public class AccountWorkflow : WorkflowBase
{
#region Read
public AccountModel Get(int id)
{
return EntityContext.Accounts
.FilterByActive()
.FilterByID(id)
.Select(AccountModel.Map)
.FirstOrDefault();
}
public AccountModel Get(string customKey)
{
return EntityContext.Accounts
.FilterByActive()
.FilterByCustomKey(customKey)
.Select(AccountModel.Map)
.FirstOrDefault();
}
public List<AccountModel> Search(string customKey, string name)
{
return EntityContext.Accounts
.FilterByActive()
.FilterByCustomKey(customKey)
.FilterByName(name)
.Select(AccountModel.Map)
.ToList();
}
#endregion
#region Create
public AccountModel Create(AccountModel model)
{
var newAccount = new Account
{
Name = model.Name,
AccountTypeID = model.AccountTypeID,
CustomKey = model.CustomKey,
Description = model.Description,
Phone = model.Phone,
Fax = model.Fax,
Email = model.Email,
Website = model.Website,
ParentID = model.ParentID,
SeoUrl = model.SEOUrl,
CreatedDate = DateTime.Now,
Active = true
};
AssociateImages(newAccount, model);
AssociateAddresses(newAccount, model);
AssociateAttributes(newAccount, model);
EntityContext.Accounts.Add(newAccount);
EntityContext.SaveChanges();
return Get(newAccount.ID);
}
#endregion
#region Update
public AccountModel Update(AccountModel model)
{
var curAccount = EntityContext.Accounts.FirstOrDefault(a => a.ID == model.ID && a.Active);
if (curAccount == null) { return null; }
curAccount.Name = model.Name;
curAccount.AccountTypeID = model.AccountTypeID;
curAccount.CustomKey = model.CustomKey;
curAccount.Description = model.Description;
curAccount.Phone = model.Phone;
curAccount.Fax = model.Fax;
curAccount.Email = model.Email;
curAccount.Website = model.Website;
curAccount.ParentID = model.ParentID;
curAccount.SeoUrl = model.SEOUrl;
AssociateImages(curAccount, model);
AssociateAddresses(curAccount, model);
AssociateAttributes(curAccount, model);
EntityContext.SaveChanges();
return Get(curAccount.ID);
}
#endregion
#region Helpers
private void AssociateImages(Account account, AccountModel model)
{
if (model.Images != null && model.Images.Any())
{
// Update Existing Attributes
var IDsToUpdate = model.Images.Where(i => i.ID.HasValue).Select(i => i.ID).ToList();
foreach (var image in account.Company.CompanyImages.Where(i => i.Active && IDsToUpdate.Contains(i.ID)))
{
image.Library.Name = model.Images.Where(i => i.ID == image.ID).Select(i => i.Name).FirstOrDefault() ?? string.Empty;
image.UpdatedDate = DateTime.Now;
}
// Remove Images
foreach (var image in account.Company.CompanyImages.Where(i => i.Active && !IDsToUpdate.Contains(i.ID)))
{
image.UpdatedDate = DateTime.Now;
image.Active = false;
}
// Add New Attributes
if (model.Images.Any(i => !i.ID.HasValue) && account.Company.CompanyImages == null)
{
account.Company.CompanyImages = new EntityCollection<CompanyImage>();
}
foreach (var image in model.Images.Where(i => !i.ID.HasValue).ToList())
{
account.Company.CompanyImages.Add(
new CompanyImage
{
Library = new Library
{
MediaTypeID = 1, //Default value
Name = image.Name ?? string.Empty,
Image = new Image
{
ImageFormatID = 1, //Default value
// ReSharper disable once PossibleInvalidOperationException
FullImageFileID = image.FileID.Value,
ThumbImageFileID = image.FileID.Value,
CreatedDate = DateTime.Now,
Active = true
},
CreatedDate = DateTime.Now,
Active = true
},
CreatedDate = DateTime.Now,
Active = true
});
}
}
else
{
if (account.Company?.CompanyImages != null)
{
//Remove All Attributes If None Where Passed in that exists
foreach (var image in account.Company.CompanyImages.Where(a => a.Active))
{
image.UpdatedDate = DateTime.Now;
image.Active = false;
}
}
}
}
private void AssociateAddresses(Account account, AccountModel model)
{
if (model.Addresses != null && model.Addresses.Any())
{
//Update Existing Addresses
var IDsToUpdate = model.Addresses.Where(i => i.ID.HasValue).Select(i => i.ID).ToList();
foreach (var address in account.AccountAddresses.Where(i => i.Active && IDsToUpdate.Contains(i.Address.ID)))
{
address.Address.Name = model.Addresses.Where(a => a.ID == address.Address.ID).Select(a => a.Name).FirstOrDefault() ?? string.Empty;
address.Address.Street1 = model.Addresses.Where(a => a.ID == address.Address.ID).Select(a => a.Street1).FirstOrDefault() ?? string.Empty;
address.Address.Street2 = model.Addresses.Where(a => a.ID == address.Address.ID).Select(a => a.Street2).FirstOrDefault() ?? string.Empty;
address.Address.City = model.Addresses.Where(a => a.ID == address.Address.ID).Select(a => a.City).FirstOrDefault() ?? string.Empty;
address.Address.DistrictID = model.Addresses.Where(a => a.ID == address.Address.ID).Select(a => a.DistrictID).FirstOrDefault();
address.Address.RegionID = model.Addresses.Where(a => a.ID == address.Address.ID).Select(a => a.RegionID).FirstOrDefault();
address.Address.RegionCustom = model.Addresses.Where(a => a.ID == address.Address.ID).Select(a => a.RegionCustom).FirstOrDefault() ?? string.Empty;
address.Address.CountryID = model.Addresses.Where(a => a.ID == address.Address.ID).Select(a => a.CountryID).FirstOrDefault();
address.Address.CountryCustom = model.Addresses.Where(a => a.ID == address.Address.ID).Select(a => a.CountryCustom).FirstOrDefault() ?? string.Empty;
address.Address.PostalCode = model.Addresses.Where(a => a.ID == address.Address.ID).Select(a => a.PostalCode).FirstOrDefault() ?? string.Empty;
address.Address.UpdatedDate = DateTime.Now;
address.IsPrimary = address.IsPrimary;
address.IsBilling = address.IsBilling;
address.UpdatedDate = DateTime.Now;
}
// Remove Address
foreach (var address in account.AccountAddresses.Where(i => i.Active && !IDsToUpdate.Contains(i.Address.ID)))
{
address.Address.UpdatedDate = DateTime.Now;
address.Address.Active = false;
address.UpdatedDate = DateTime.Now;
address.Active = false;
}
// Add New Address
if (model.Addresses.Any(i => !i.ID.HasValue) && account.AccountAddresses == null)
{
account.AccountAddresses = new EntityCollection<AccountAddress>();
}
foreach (var address in model.Addresses.Where(a=>!a.ID.HasValue))
{
account.AccountAddresses.Add(new AccountAddress
{
Address = new Address
{
Name = address.Name,
Street1 = address.Street1,
Street2 = address.Street2,
City = address.City,
DistrictID = address.DistrictID,
RegionID = address.RegionID,
RegionCustom = address.RegionCustom,
CountryID = address.CountryID,
CountryCustom = address.CountryCustom,
PostalCode = address.PostalCode,
CreatedDate = DateTime.Now,
Active = true
},
IsPrimary = address.IsPrimary,
IsBilling = address.IsBilling,
CreatedDate = DateTime.Now,
Active = true
});
}
}
else
{
foreach (var address in account.AccountAddresses)
{
//Remove Associated Addresses
address.Address.UpdatedDate = DateTime.Now;
address.Address.Active = false;
//Remove Address
address.UpdatedDate = DateTime.Now;
address.Active = false;
}
}
}
private void AssociateAttributes(Account account, AccountModel model)
{
if (model.Attributes != null && model.Attributes.Any())
{
if (account.AccountAttributes == null) { account.AccountAttributes = new List<AccountAttribute>(); }
var IDsToUpdate = model.Attributes.Where(a => a.ID.HasValue).Select(i => i.ID).ToList();
// Remove Images
foreach (var attribute in account.AccountAttributes.Where(i => i.Active && !IDsToUpdate.Contains(i.ID)))
{
attribute.UpdatedDate = DateTime.Now;
attribute.Active = false;
}
foreach (var attribute in model.Attributes)
{
if (attribute.ID.HasValue)
{
var ID = attribute.ID.Value;
var attributeValue = account.AccountAttributes.Where(pa => pa.ID == ID).Select(av => av.AttributeValue).FirstOrDefault();
if (attributeValue == null || !attribute.AttributeID.HasValue) continue;
attributeValue.AttributeID = attribute.AttributeID.Value;
attributeValue.Value = attribute.Value;
attributeValue.UpdatedDate = DateTime.Now;
}
else if (attribute.ValueID.HasValue)
{
account.AccountAttributes.Add(new AccountAttribute { AttributeValueID = attribute.ValueID.Value, CreatedDate = DateTime.Now, Active = true });
}
else if (!attribute.ValueID.HasValue && attribute.AttributeID.HasValue && !string.IsNullOrWhiteSpace(attribute.Value))
{
account.AccountAttributes.Add(new AccountAttribute
{
AttributeValue = new AttributeValue
{
AttributeID = attribute.AttributeID.Value,
Value = attribute.Value,
CreatedDate = DateTime.Now,
UpdatedDate = DateTime.Now,
Active = true
},
CreatedDate = DateTime.Now,
UpdatedDate = DateTime.Now,
Active = true
});
}
}
}
else if (model.ID.HasValue && account.AccountAttributes != null)
{
foreach (var attribute in account.AccountAttributes.Where(pa => pa.Active))
{
attribute.UpdatedDate = DateTime.Now;
attribute.Active = false;
}
}
}
#endregion
#region Deactivate
public void Deactivate(int id)
{
var account = EntityContext.Accounts.FilterByID(id).FirstOrDefault();
if (account == null) throw new NullReferenceException($"No account found with ID: {id}");
account.UpdatedDate = DateTime.Now;
account.Active = false;
EntityContext.SaveChanges();
}
#endregion
}
}
This project contains all the ServiceStack code to generate an API for
communication between the UI and Business Workflows. This project's
folder is referenced as a virtual application in the website and a
configuration setting in the web.config must be updated to the path
where the virtual application full path is set. This VA allows the
website to directly access all the services endpoints here without
having to move them into a particular website folder, create a symbolic
link using Windows itself, or create a separate IIS instance with a
different subdomain.
namespace Clarity.Ecommerce.Framework.Accounts
{
using System;
using System.Collections.Generic;
using Models.Account;
using Models.Attribute;
using Models.Generic;
using ServiceStack;
[Route("/AccountTypes/", "GET", Summary = @"Use to get a account types")]
public class GetAccountTypes : IReturn<AccountTypeModel> { }
[Route("/Account/Create", "POST", Summary = @"Use to create a new account")]
public class CreateAccount : AccountModel, IReturn<AccountModel> { }
[Route("/Account/{ID}", "GET", Summary = @"Use to get a specific account")]
public class GetAccount : IReturn<AccountModel>
{
[ApiMember(Name = "ID", Description = "Account System Identification", IsRequired = true)]
public int ID { get; set; }
}
[Authenticate]
[Route("/Account/Accounts/", "GET", Summary = @"Get All Accounts")]
public class GetAccounts : IReturn<List<AccountModel>>
{
[ApiMember(Name = "CustomKey", Description = "Account Key", IsRequired = false)]
public string CustomKey { get; set; }
[ApiMember(Name = "Name", Description = "Account Name", IsRequired = false)]
public string Name { get; set; }
[ApiMember(Name = "AttributesToInclude", Description = "Attributes To Include", IsRequired = false)]
public List<string> AttributesToInclude { get; set; }
[ApiMember(Name = "ModifiedSince", Description = "Modified Since", IsRequired = false)]
public DateTime? ModifiedSince { get; set; }
}
[Authenticate]
[Route("/Account/{ID}", "POST", Summary = @"Use to update a specific account")]
public class UpdateAccount : AccountModel, IReturn<AccountModel> { }
[Authenticate]
[Route("/Account/{ID}", "POST", Summary = @"Use to update the account associated to the user currently logged into the system")]
public class UpdateCurrentAccount : IReturn<AccountModel>
{
[ApiMember(Name = "ID", Description = "Account System Identification", IsRequired = true)]
public int ID { get; set; }
[ApiMember(Name = "Name", Description = "Account Name", IsRequired = true)]
public string Name { get; set; }
[ApiMember(Name = "Email", Description = "Account Email", IsRequired = true)]
public string Email { get; set; }
[ApiMember(Name = "Website", Description = "Account Website", IsRequired = false)]
public string Website { get; set; }
[ApiMember(Name = "Fax", Description = "Main Fax For Account", IsRequired = false)]
public string Fax { get; set; }
[ApiMember(Name = "Phone", Description = "Main Phone Number For Account", IsRequired = false)]
public string Phone { get; set; }
[ApiMember(Name = "City", Description = "Account City", IsRequired = false)]
public string City { get; set; }
[ApiMember(Name = "PostalCode", Description = "Account PostalCode", IsRequired = false)]
public string PostalCode { get; set; }
[ApiMember(Name = "State", Description = "Account State", IsRequired = false)]
public int State { get; set; }
[ApiMember(Name = "Country", Description = "Account Country", IsRequired = false)]
public int Country { get; set; }
[ApiMember(Name = "Address1", Description = "Account Address 1", IsRequired = false)]
public string Address1 { get; set; }
[ApiMember(Name = "Address2", Description = "Account Address 2", IsRequired = false)]
public string Address2 { get; set; }
[ApiMember(Name = "ImageFileAttributes", Description = "Image File Attributes", IsRequired = false)]
public List<FileModel> ImageFileAttributes { get; set; }
[ApiMember(Name = "Attributes", Description = "Attributes", IsRequired = false)]
public List<AttributeModel> Attributes { get; set; }
}
[Authenticate]
[Route("/Account/Current/", "GET", Summary = @"Get account for the current user logged in")]
public class GetCurrentAccount : IReturn<AccountModel> { }
[Authenticate]
[Route("/Account/Current/Remove", "POST", Summary = @"Remove account for the account of the user currently logged into the system")]
public class RemoveCurrentAccount { }
[Authenticate]
[Route("/Account/{ID}/Remove", "POST", Summary = @"Remove a specific account from the system")]
public class RemoveAccount
{
[ApiMember(Name = "ID", Description = "Account System Identification", IsRequired = true)]
public int ID { get; set; }
}
[Authenticate]
[Route("/Account/Receivable/", "GET", Summary = @"Get All Accounts Receivable")]
public class GetAccountsReceivable : IReturn<List<AccountsReceivableSearchResultModel>> { }
[Authenticate]
[Route("/Account/Payable/", "GET", Summary = @"Get All Accounts Payable")]
public class GetAccountsPayable : IReturn<List<AccountsPayableSearchResultModel>> { }
}
namespace Clarity.Ecommerce.Framework.Accounts
{
using System;
using System.Collections.Generic;
using System.Linq;
using Models.Account;
using Models.Generic;
using Shared;
using ServiceStack;
public class AccountService : ClarityEcommerceServiceBase
{
#region Read
public AccountModel Any(GetAccount request)
{
return Workflows.Accounts.Get(request.ID);
}
public List<AccountModel> Any(GetAccounts request)
{
return Workflows.Accounts.Search(request.CustomKey,request.Name);
}
public List<AccountTypeModel> Any(GetAccountTypes request)
{
return Workflows.AccountTypes.Search();
}
public AccountModel Any(GetCurrentAccount request)
{
return Workflows.Accounts.Get(CurrentUserName);
}
public List<AccountsReceivableSearchResultModel> Any(GetAccountsPayable request)
{
return Workflows.AccountsReceivable.Search(new AccountsReceivableSearchModel());
}
public List<AccountsPayableSearchResultModel> Any(GetAccountsReceivable request)
{
return Workflows.AccountsPayable.Search(new AccountsPayableSearchModel());
}
#endregion
#region Create
public AccountModel Any(CreateAccount request)
{
return Workflows.Accounts.Create(request);
}
#endregion
#region Update
public AccountModel Any(UpdateAccount request)
{
return Workflows.Accounts.Update(request);
}
public AccountModel Any(UpdateCurrentAccount request)
{
var currentUserName = CurrentUserName;
if (!string.IsNullOrWhiteSpace(currentUserName))
{
var account = Workflows.Accounts.Get(currentUserName);
if (request.ImageFileAttributes == null)
{
request.ImageFileAttributes = new List<FileModel>();
}
if (account == null)
{
var curAccount = new AccountModel
{
Name = request.Name,
CustomKey = currentUserName,
Attributes = request.Attributes,
Images = request.ImageFileAttributes.Select(i => new FileModel
{
ID = i.ID,
Name = i.Name,
FileName = i.FileName,
IsDefault = i.IsDefault
}).ToList()
};
List<FileModel> accountImages;
lock (RecentUploadedFiles)
{
accountImages = RecentUploadedFiles.Where(f => f.Type == FileEntityType.AccountImage).ToList();
RecentUploadedFiles =
RecentUploadedFiles.Where(f => f.Type != FileEntityType.AccountImage).ToList();
}
curAccount.Images.AddRange(accountImages);
Workflows.Accounts.Create(curAccount);
}
else
{
var curAccount = new AccountModel
{
ID = account.ID,
Name = request.Name,
Attributes = request.Attributes,
Images = request.ImageFileAttributes.Select(i => new FileModel
{
ID = i.ID,
Name = i.Name,
FileName = i.FileName,
IsDefault = i.IsDefault
}).ToList()
};
List<FileModel> accountImages;
lock (RecentUploadedFiles)
{
accountImages = RecentUploadedFiles.Where(f => f.Type == FileEntityType.AccountImage).ToList();
RecentUploadedFiles = RecentUploadedFiles.Where(f => f.Type != FileEntityType.AccountImage).ToList();
}
curAccount.Images.AddRange(accountImages);
return Workflows.Accounts.Update(curAccount);
}
}
return null;
}
#endregion
#region Deactivate
public void Any(RemoveAccount request)
{
Workflows.Accounts.Delete(request.ID);
}
public void Any(RemoveCurrentAccount request)
{
throw new NotImplementedException();
}
#endregion
}
}
This project contains the core UI code for both the store and admin
portals and their related Angular directives. This project's folder is
referenced as a virtual directory in the website and a configuration
setting in the web.config must be updated to the path where the virtual
directory is set. This VD allows the website to directly access all the
files here without having to move them into a particular website folder
or create a symbolic link using Windows itself.
Client specific code should not be input to this project, instead all
storefront controls have a Transclude process where-in the front end
developer can specify a different location to pull the HTML layout
content file which overrides the base path. The base path file must
contain only the bare minimum content of what tags are available, though
not in any particular order or layout.
A requirement in the Coding Standards is that all C# code must have
Summary Information Tags. A Summary Information Tag (Summary Tag) is a
form of formal documentation used by developers to outline what a piece
of code (file, property, class or function) is for. These tags are
readable by various IDEs like VS's Intellisense feature and are often
compiled into external resources to assist with creating Help
documentation.
Atomineer is one of several plugins that assist developers with the
tedium of documenting their code. The difference between Atomineer and
several of their competitors is that Atomineer uses strong, customizable
language heuristics to generate summary tags for you, as well as
ensuring that the existing tags are uniformly formatted throughout your
full solution. While Clarity only utilizes Atomineer for the DocXml
standard of documenting C# code, they support a variety of languages
and a variety of standards.
Atomineer is a Licensed product meaning that each developer which needs
a license to use it (after a 30 day trial) will need to purchase a
license. Clarity will order licenses for all Back End developers and
ensure that the licensing subscription is maintained. Please contact
Chris or James G. for assistance with getting a license after your 30
day trial is over.
Open a C# solution in Visual Studio and create a new C# class file.
Paste the following contents in that file:
namespace RepositoryPattern.BusinessWorkflows.Authors
{
using System;
using System.Collections.Generic;
using System.Linq;
using Interfaces.BusinessWorkflows;
using Interfaces.DataModels;
using Interfaces.Mappers;
using Interfaces.Models;
using Interfaces.Repositories;
using Interfaces.SearchModels;
public class AuthorsBusinessWorkflow : IAuthorsBusinessWorkflow
{
public AuthorsBusinessWorkflow(IAuthorsRepository authorsRepository, IAuthorMapper authorMapper)
{
AuthorsRepository = authorsRepository;
AuthorMapper = authorMapper;
}
#region Private Variables
private IAuthorMapper AuthorMapper { get; }
private IAuthorsRepository AuthorsRepository { get; }
#endregion
#region Read
public IAuthorModel Get(int id)
{
BusinessWorkflowBase.ValidateRequiredID(id);
return AuthorMapper.MapToModel(AuthorsRepository.Get(id));
}
public IAuthorModel Get(string key)
{
BusinessWorkflowBase.ValidateRequiredKey(key);
return AuthorMapper.MapToModel(AuthorsRepository.Get(key));
}
public List<IAuthorModel> Search(IAuthorSearchModel searchModel, bool asListing = false)
{
var results = AuthorsRepository.Search(searchModel);
return asListing
? results.Select(AuthorMapper.MapToModelListing).ToList()
: results.Select(AuthorMapper.MapToModelLite).ToList();
}
#endregion
#region Create
public IAuthorModel Create(IAuthorModel model)
{
// Validate model
BusinessWorkflowBase.ValidateIDIsNull(model.ID);
BusinessWorkflowBase.ValidateRequiredString(model.Name, nameof(model.Name));
// Search for an Existing Record (Don't allow Duplicates
var results = Search(AuthorMapper.MapToSearchModel(model));
if (results.Any()) { return results.First(); } // Return the first that matches
// Map model to a new entity
var newEntity = AuthorMapper.MapToEntity(model);
newEntity.CreatedDate = BusinessWorkflowBase.GenDateTime;
newEntity.UpdatedDate = null;
newEntity.Active = true;
// Add it
AuthorsRepository.Add(newEntity);
// Try to Save Changes
AuthorsRepository.SaveChanges();
// Return the new value
return Get(newEntity.ID);
}
#endregion
#region Update
public IAuthorModel Update(IAuthorModel model)
{
// Validate model
BusinessWorkflowBase.ValidateRequiredNullableID(model.ID);
BusinessWorkflowBase.ValidateRequiredString(model.Name, nameof(model.Name));
// Find existing entity
// ReSharper disable once PossibleInvalidOperationException
var existingEntity = AuthorsRepository.Get(model.ID.Value);
// Check if we would be applying identical information, if we are, just return the original
// ReSharper disable once SuspiciousTypeConversion.Global
if (AuthorMapper.AreEqual(model, existingEntity))
{
return AuthorMapper.MapToModel(existingEntity);
}
// Map model to an existing entity
AuthorMapper.MapToEntity(model, ref existingEntity);
existingEntity.UpdatedDate = BusinessWorkflowBase.GenDateTime;
// Update it
AuthorsRepository.Update(AuthorMapper.MapToEntity(model));
// Try to Save Changes
AuthorsRepository.SaveChanges();
// Return the new value
return Get(existingEntity.ID);
}
#endregion
#region Deactivate
public bool Deactivate(int id)
{
BusinessWorkflowBase.ValidateRequiredID(id);
// Find existing Entity
var existingEntity = AuthorsRepository.Get(id);
if (existingEntity == null)
{
throw new InvalidOperationException($"Could not find an entity with id {id} to deactivate it");
}
// Do the Deactivate
return Deactivate(existingEntity);
}
public bool Deactivate(string key)
{
BusinessWorkflowBase.ValidateRequiredKey(key);
// Find existing Entity
var existingEntity = AuthorsRepository.Get(key);
if (existingEntity == null)
{
throw new InvalidOperationException($"Could not find an entity with key {key} to deactivate it");
}
// Do the Deactivate
return Deactivate(existingEntity);
}
protected bool Deactivate(IAuthor entity)
{
// Deactivate it
AuthorsRepository.Deactivate(entity);
// Try to Save Changes
AuthorsRepository.SaveChanges();
// Finished!
return true;
}
#endregion
#region Remove
public bool Remove(int id)
{
BusinessWorkflowBase.ValidateRequiredID(id);
// Find existing Entity
var existingEntity = AuthorsRepository.Get(id);
// Do the Remove
return Remove(existingEntity);
}
public bool Remove(string key)
{
BusinessWorkflowBase.ValidateRequiredKey(key);
// Find existing Entity
var existingEntity = AuthorsRepository.Get(key);
// Do the Remove
return Remove(existingEntity);
}
protected bool Remove(IAuthor entity)
{
if (entity == null) { return true; } // No entity found to remove, consider it passed
// Remove it
AuthorsRepository.Remove(entity);
// Try to Save Changes
AuthorsRepository.SaveChanges();
// Finished!
return true;
}
#endregion
}
}
Don't worry about all the unrecognizable symbols, they don't impact this
example.
You can see here we have some comments inside the functions but no
summary tags. Though this code is well-formed and appears
well-documented, it would actually fail Code Review because there are no
Summary Tags.
To go about adding the first Summary Tag. Click on line 12, where there
is a blank space between the Usings and the Class declaration. Press
Enter to make a new line and then press / three times in a row. By
default, without Atomineer, the following text will be created by Visual
Studio:
/// <summary>
///
/// </summary>
public class AuthorsBusinessWorkflow : IAuthorsBusinessWorkflow
You can see that this is just an empty summary tag. You would now need
to type in a summary of what this class is for between the <summary> and
</summary> tags like this.
/// <summary>
/// A business logic layer for Authors.
/// </summary>
public class AuthorsBusinessWorkflow : IAuthorsBusinessWorkflow
Now, imagine you need to do this for every Class, Property, Function,
Enum, etc in the entire code-base when there are none. That would be
tedious and time-consuming. In fact, a lot of what you would write is
really just an expanded form of what the item's name is. It's really
only where there is special logic happening that you need to expand the
documentation.
With Atomineer installed, you could click on the Class name (or anywhere
on that line) and instead of pressing / three times in a row, you would
press Ctrl+Shift+D. This would trigger Atomineer to document the line
using it's heuristics instead. Here is what Atomineer writes if I remove
the summary tag we just made and let it do it itself:
/// <summary>The authors business workflow.</summary>
/// <seealso cref="T:RepositoryPattern.Interfaces.BusinessWorkflows.IAuthorsBusinessWorkflow"/>
public class AuthorsBusinessWorkflow : IAuthorsBusinessWorkflow
Ok, so not the same as what we would have written in the summary tag
itself but you can see it directly relates to what the class name is.
Also, it's telling you in documentation that this class implements a
specific interface and where tho find that information with a <seealso/>
tag. If you go to another place in the code and start typing var w = new
AuthorsBusinessWorkflow(); in Visual Studio, the Intellisense would come
up automatically and provide a tooltip that says "The authors business
workflow." with a second part of the tooltip that would like over to the
documentation coming from the Interface that this is implementing and
its summary tag's content.
So now we see more information getting pre-populated by Atomineer than
we would have done or even known to do on our own. However, that just
tagged this one item of thousands throughout the solution. To document
more, Atomineer has friendly functions for doing more at the same time.
To document the entirety of the current file, press Ctrl+Shift+A then
Ctrl+Shift+F. Suddenly, this entire file is now documented with summary
tags. We can review the tags to make sure there's nothing extra we need
to add and save the file. There are additional commands which can be run
from the Tools > Atomineer Pro Documentation menu such as deleting
all the tags from a file and documenting an entire project or solution
at once (these can take a few minutes).
Atomineer provides this functionality as well as other customizations
like an entire dictionary of abbreviations to expand, such as Dnn to
DotNetNuke. Atomineer uses a set of settings files to maintain all of
the customized documentation setups. To get a copy of these settings,
please see James G.
Other projects in the solution must be handled on a case-by-case basis.