Related to this question, I am modelling the identities for a trade logging/management system.
It will be multi-tenanted with users only able to see tenants they have been given rights to, so I have a partition key called TenantId.
The other fields are as follows for a Trade that has taken place:
internal class Trade : ValueObject
{
public Account Account { get; init; } // See below - belongs to a Tenant / Account / Broker
public TradeId TradeId { get; init; } // Unique tradeId (string i.e. 0000d695.234242.01.01)
public UserId UserId { get; init; } // A user potentially could create trades in other tenants, with appropriate claims/roles as part of an access control context.
public Contract Contract { get; init; } // Exact same contract can be traded on many different brokers
public string Exchange { get; init; }
public string BrokerContractId { get; init; }
public string BucketName { get; init; }
public decimal Position { get; init; }
public double Average { get; init; }
public DateTime Timestamp { get; init; }
protected override IEnumerable<object> GetEqualityComponents()
{
yield return Account;
yield return TradeId;
}
}
I don't see a trade as an entity as such, as there are no operations to perform - it is merely a log entry. I use the following identities to identify it:
public class TradeId : Identity
{
public TradeId(string fillId) : base(fillId) { } // Must be unique for each and every fill.
}
public class UserId : Identity
{
public UserId(string username) : base(username) { }
}
I have an account, which is unique to a broker, and coupled strongly as part of a tenant (one tenant would never see anothers brokerage account):
public class AccountId : Identity
{
public AccountId(string accountNumber) : base(accountNumber) { }
}
// An account belongs to broker, and within a tenant (that can have multiple accounts).
public class Account : EntityWithCompositeId
{
public AccountId AccountId { get; init; }
public TenantId TenantId { get; init; }
public BrokerId BrokerId { get; init; }
public Account(TenantId tenantId, BrokerId brokerId, AccountId accountId)
{
TenantId = tenantId;
BrokerId = brokerId;
AccountId = accountId;
}
protected override IEnumerable<object> GetIdentityComponents()
{
yield return TenantId;
yield return BrokerId;
yield return AccountId;
}
}
Question 1: I have noticed that if TenantId is part of the AccountId (since an account cannot exist without the Tenant), it would save me adding TenantId in all my entitities - are there any disadvantages to this?
Question 2: Given the below contract, even if i did include unique data such as FIGI composite ID, regardless of it being unique, it is still a value object as it wouldn't ever have business logic inside - is this the right school of thought?
// The same contract can exist on more than one broker.
public class Contract : ValueObject
{
public string Symbol { get; init; }
public string Currency { get; init; }
public override string ToString() => Symbol;
protected override IEnumerable<object> GetEqualityComponents()
{
yield return Symbol;
yield return Currency;
}
}
I have started to flesh this out, an AccountID is ok as the three components form the identity:
// An account belongs to broker, and within a tenant (that can have multiple accounts).
public class AccountId : ValueObject
{
public string Id { get; init; }
public TenantId TenantId { get; init; }
public BrokerId BrokerId { get; init; }
public AccountId(TenantId tenantId, BrokerId brokerId, string id)
{
TenantId = tenantId;
BrokerId = brokerId;
Id = id;
}
protected override IEnumerable<object> GetEqualityComponents()
{
yield return TenantId;
yield return BrokerId;
yield return Id;
}
}
Executions are value objects, with no lifetime as they cannot exist outside of an account:
public class FillDetail : ValueObject
{
public string FillId { get; init; }
// Must be unique for each and every fill.
public FillDetail(string fillId) => FillId = fillId;
protected override IEnumerable<object> GetEqualityComponents()
{
yield return FillId;
}
}
public class Execution : ValueObject
{
public FillDetail FillDetail { get; init; }
public UserId UserId { get; init; } // A user potentially could create trades in other tenants, with appropriate claims/roles.
public Contract Contract { get; init; }
public string Exchange { get; init; }
public string BrokerContractId { get; init; }
public string BucketName { get; init; }
public decimal Size { get; init; }
public double Price { get; init; } // The execution price, excluding commissions.
public DateTime Timestamp { get; init; }
public Action Action { get; init; }
public bool IsForcedLiquidation { get; init; }
public Side Side { get; init; }
public Liquidity Liquidity { get; init; }
public FillDetail? Correction { get; init; }
protected override IEnumerable<object> GetEqualityComponents()
{
yield return FillDetail;
yield return UserId;
yield return Contract;
yield return Exchange;
yield return BrokerContractId;
yield return BucketName;
yield return Size;
yield return Price;
yield return Timestamp;
yield return Action;
yield return IsForcedLiquidation;
yield return Side;
yield return Liquidity;
if (Correction != null) yield return Correction;
}
}
The account is a Entity as it evolves over time, and would have business logic to for example:
Calculate P+L when executions are entered on the ledger
Prevent trades from taking place if current positions (derived from the execution log) are open, or additional orders, hedges etc.
This logic would belong in the Account:
public sealed class Account : Entity
{
public AccountId AccountId { get; init; }
readonly List<Execution> executions = new();
public void LogExecution(Execution execution)
{
// TODO: Check if this execution already exists, if so ignore it.
this.executions.Add(execution);
// This is where we would scan list of prior trades, calculate open positions, P+L etc.
}
public void PlaceOrder(Contract contract)
{
// We could have limit of no more than 10% of account balance allocated to any one position.
}
public IEnumerable<Execution> GetExecutions()
{
throw new NotImplementedException();
}
protected override IEnumerable<ValueObject> GetIdentityComponents()
{
yield return AccountId;
}
}
I think this is more likely a better model, whilst I now have concerns on the massive execution log (and managing that performance) however I will save that for a further question.
Feel free to provide your own answers, pointers much appreciated.
Related
In a clean architecture project the domain layer contains: DTO interfaces, Events, Factories, Models, Exceptions, etc...
Every domain object contains a constructor with arguments through which data is passed.
I am using factories which accept a DTO interface from which domain objects are created.
The data models in the infrastructure layer implement the DTO interfaces in the domain layer.
DTO:
namespace Acme.Core.Domain.Identity.DTO
{
public interface IBuyerDto : IPersonDto
{
IAddressDto BillingAddress { get; set; }
IAddressDto ShippingAddress { get; set; }
}
}
Domain Models:
namespace Acme.Core.Domain.Identity.Models.BuyerAggregate
{
public sealed class Buyer : Aggregate<Buyer, BuyerId>, IPerson
{
public Buyer(BuyerId id, PersonName name, DateOfBirth dateOfBirth, Gender gender, string pictureUrl, Address billingAddress, Address shippingAddress, Account account) : base(id)
{
Name = name;
DateOfBirth = dateOfBirth;
Gender = gender;
BillingAddress = billingAddress;
ShippingAddress = shippingAddress;
Account = Guard.Against.Null(account, nameof(account));
PictureUrl = pictureUrl;
}
public Account Account { get; private set; }
public PersonName Name { get; private set; }
public DateOfBirth DateOfBirth { get; private set; }
public string PictureUrl { get; private set; }
public Gender Gender { get; private set; }
public Address BillingAddress { get; private set; }
public Address ShippingAddress { get; private set; }
public void UpdateName(PersonName personName)
{
Name = personName;
}
public void UpdateBillingAddress(Address billingAddress)
{
BillingAddress = billingAddress;
}
public void UpdateShippingAddress(Address shippingAddress)
{
ShippingAddress = shippingAddress;
}
}
}
namespace Acme.Core.Domain.Identity.Models
{
public class Account : Entity<Account, AccountId>
{
public Account(AccountId id, string userName, string normalizedUserName, string passwordHash, string concurrencyStamp, string securityStamp, string email, string normalizedEmail, bool emailConfirmed, string phoneNumber, bool phoneNumberConfirmed, bool twoFactorEnabled, DateTimeOffset? lockoutEnd, bool lockoutEnabled, int accessFailedCount, AccountStatus status, List<RoleId> roles, List<AccountClaim> accountClaims, List<AccountLogin> accountLogins, List<AccountToken> accountTokens) : base(id)
{
UserName = Guard.Against.NullOrWhiteSpace(userName, nameof(userName));
NormalizedUserName = Guard.Against.NullOrWhiteSpace(normalizedUserName, nameof(normalizedUserName));
PasswordHash = Guard.Against.NullOrWhiteSpace(passwordHash, nameof(passwordHash));
ConcurrencyStamp = concurrencyStamp;
SecurityStamp = securityStamp;
Email = Guard.Against.NullOrWhiteSpace(email, nameof(email));
NormalizedEmail = Guard.Against.NullOrWhiteSpace(normalizedEmail, nameof(normalizedEmail));
EmailConfirmed = emailConfirmed;
PhoneNumber = phoneNumber;
PhoneNumberConfirmed = phoneNumberConfirmed;
TwoFactorEnabled = twoFactorEnabled;
LockoutEnd = lockoutEnd;
LockoutEnabled = lockoutEnabled;
AccessFailedCount = accessFailedCount;
Status = Guard.Against.Null(status, nameof(status));
_roles = Guard.Against.Null(roles, nameof(roles));
_accountClaims = accountClaims;
_accountLogins = accountLogins;
_accountTokens = accountTokens;
}
public string UserName { get; private set; }
public string NormalizedUserName { get; private set; }
public string PasswordHash { get; private set; }
public string ConcurrencyStamp { get; private set; }
public string SecurityStamp { get; private set; }
public string Email { get; private set; }
public string NormalizedEmail { get; private set; }
public bool EmailConfirmed { get; private set; }
public string PhoneNumber { get; private set; }
public bool PhoneNumberConfirmed { get; private set; }
public bool TwoFactorEnabled { get; private set; }
public DateTimeOffset? LockoutEnd { get; private set; }
public bool LockoutEnabled { get; private set; }
public int AccessFailedCount { get; private set; }
public AccountStatus Status { get; private set; }
private List<RoleId> _roles;
public IReadOnlyCollection<RoleId> Roles
{
get
{
return _roles;
}
}
private List<AccountClaim> _accountClaims;
public IReadOnlyCollection<AccountClaim> AccountClaims
{
get
{
return _accountClaims;
}
}
private List<AccountLogin> _accountLogins;
public IReadOnlyCollection<AccountLogin> AccountLogins
{
get
{
return _accountLogins;
}
}
private List<AccountToken> _accountTokens;
public IReadOnlyCollection<AccountToken> AccountTokens
{
get
{
return _accountTokens;
}
}
public void AddRole(long roleId)
{
var role = _roles.Where(x => x.GetValue().Equals(roleId)).FirstOrDefault();
if (role == null)
{
_roles.Add(new RoleId(roleId));
}
}
public void RemoveRole(long roleId)
{
var role = _roles.Where(x => x.GetValue().Equals(roleId)).FirstOrDefault();
if (role == null)
{
_roles.Remove(role);
}
}
public void ActivateAccount()
{
Status = AccountStatus.Active;
}
public void BanAccount()
{
Status = AccountStatus.Banned;
}
public void CloseAccount()
{
Status = AccountStatus.Closed;
}
public void LockAccount()
{
Status = AccountStatus.Locked;
}
public void NewAccount()
{
Status = AccountStatus.New;
}
}
}
Factories:
namespace Acme.Core.Domain.Identity.Factories
{
public class BuyerAggregateFatory : IBuyerAggregateFactory
{
private readonly IPersonNameFactory _personNameFactory;
private readonly IDateOfBirthFactory _dateOfBirthFactory;
private readonly IGenderFactory _genderFactory;
private readonly IAccountFactory _accountFactory;
private readonly IAddressFactory _addressFactory;
public BuyerAggregateFatory(IPersonNameFactory personNameFactory,
IDateOfBirthFactory dateOfBirthFactory,
IGenderFactory genderFactory,
IAccountFactory accountFactory,
IAddressFactory addressFactory)
{
_personNameFactory = Guard.Against.Null(personNameFactory);
_dateOfBirthFactory = Guard.Against.Null(dateOfBirthFactory);
_genderFactory = Guard.Against.Null(genderFactory);
_accountFactory = Guard.Against.Null(accountFactory);
_addressFactory = Guard.Against.Null(addressFactory);
}
public Buyer Create(IBuyerDto dto)
{
BuyerId aggregateId = new BuyerId(dto.Id);
PersonName name = _personNameFactory.Create(dto.Name);
DateOfBirth dob = _dateOfBirthFactory.Create(dto.DateOfBirth);
Gender gender = _genderFactory.Create(dto.GenderId);
Address billingAddress = _addressFactory.Create(dto.BillingAddress);
Address shippingAddress = _addressFactory.Create(dto.ShippingAddress);
Account account = _accountFactory.Create(dto.Account);
return new Buyer(aggregateId, name, dob, gender, dto.PictureUrl, billingAddress, shippingAddress, account);
}
}
}
From the application layer a service class does the orchestration for the use case, using the repository interface and factory interface.
Use case 1: During an update operation I fetch existing data of the aggregate, from the database using a repository. I need to update one or two properties of a domain aggregate root object. Example: I need to update billing address or shipping address.
Use case 2: During an update operation, I fetch existing data of the aggregate, from the database using a repository. I need to update the account status. I am calling the status update method from domain aggregate root object. Example: buyerAggregate.Account.ActivateAccount()
Am i updating the domain aggregate root object and its properties in right way?
In use case 2, your aggregate would be the Account, not the Buyer. There's no need for the Buyer to be involved in the transaction.
So, for this case, you would retrieve Account from the repository and then call ActivateAccount() directly.
Any aggregate that you have designed for a use case should provide the full interface for making changes to the aggregate. In other words, your application layer will only work with properties and methods on the aggregate root. If a child entity needs changing that method should be implemented on your aggregate root. You should not directly interact with child properties of an aggregate. It is the aggregate's responsibility to avoid any invariants within its scope. If you change a child object directly, you may put the whole aggregate in an invalid state because the aggregate was not able to enforce controls.
I'm developing an Azure Mobile App service to interface to my Xamarin application.
I've created, connected and successfully populated an SQL Database, but when I try to add some filters to my request, for example an orderby() or where() clauses, it returns me a Bad Request error.
For example, this request: https://myapp.azurewebsites.net/tables/Race?$orderby=iRound%20desc,iYear%20desc&$top=1&ZUMO-API-VERSION=2.0.0 gives me {"message":"The query specified in the URI is not valid. Could not find a property named 'IYear' on type 'MyType'."}.
My configuration method is this:
HttpConfiguration config = new HttpConfiguration();
new MobileAppConfiguration()
.AddTablesWithEntityFramework()
.ApplyTo(config);
config.MapHttpAttributeRoutes();
Database.SetInitializer(new CreateDatabaseIfNotExists<MainDataContext>());
app.UseWebApi(config);
and my DbContext is this:
public class MainDataContext : DbContext
{
private const string connectionStringName = "Name=MS_TableConnectionString";
public MainDataContext() : base(connectionStringName)
{
Database.Log = s => WriteLog(s);
}
public void WriteLog(string msg)
{
System.Diagnostics.Debug.WriteLine(msg);
}
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
modelBuilder.Conventions.Add(
new AttributeToColumnAnnotationConvention<TableColumnAttribute, string>(
"ServiceTableColumn", (property, attributes) => attributes.Single().ColumnType.ToString()));
}
public DbSet<Race> Race { get; set; }
public DbSet ...ecc...
}
Following this guide, I added a migration after creating my TableControllers. So the TableController for the example type shown above is pretty standard:
[EnableQuery(AllowedQueryOptions = AllowedQueryOptions.All)]
public class RaceController : TableController<Race>
{
protected override void Initialize(HttpControllerContext controllerContext)
{
base.Initialize(controllerContext);
MainDataContext context = new MainDataContext();
DomainManager = new EntityDomainManager<Race>(context, Request);
}
// GET tables/Race
[EnableQuery(AllowedQueryOptions = AllowedQueryOptions.All)]
public IQueryable<Race> GetAllRace()
{
return Query();
}
// GET tables/Race/48D68C86-6EA6-4C25-AA33-223FC9A27959
public SingleResult<Race> GetRace(string id)
{
return Lookup(id);
}
// PATCH tables/Race/48D68C86-6EA6-4C25-AA33-223FC9A27959
public Task<Race> PatchRace(string id, Delta<Race> patch)
{
return UpdateAsync(id, patch);
}
// POST tables/Race
public async Task<IHttpActionResult> PostRace(Race item)
{
Race current = await InsertAsync(item);
return CreatedAtRoute("Tables", new { id = current.Id }, current);
}
// DELETE tables/Race/48D68C86-6EA6-4C25-AA33-223FC9A27959
public Task DeleteRace(string id)
{
return DeleteAsync(id);
}
}
As you can see, I already tried to add the EnableQuery attribute to my TableController, as seen on Google. I also tried to add these filters to the HttpConfiguration object, without any success:
config.Filters.Add(new EnableQueryAttribute
{
PageSize = 10,
AllowedArithmeticOperators = AllowedArithmeticOperators.All,
AllowedFunctions = AllowedFunctions.All,
AllowedLogicalOperators = AllowedLogicalOperators.All,
AllowedQueryOptions = AllowedQueryOptions.All
});
config.AddODataQueryFilter(new EnableQueryAttribute
{
PageSize = 10,
AllowedArithmeticOperators = AllowedArithmeticOperators.All,
AllowedFunctions = AllowedFunctions.All,
AllowedLogicalOperators = AllowedLogicalOperators.All,
AllowedQueryOptions = AllowedQueryOptions.All
});
I don't know what to investigate more, as things seems to be changing too fast for a newbie like me who's first got into Azure.
EDIT
I forgot to say that asking for the complete table, so for example https://myapp.azurewebsites.net/tables/Race?ZUMO-API-VERSION=2.0.0, returns correctly the entire dataset. The problem occurs only when adding some clauses to the request.
EDIT 2
My model is like this:
public class Race : EntityData
{
public int iRaceId { get; set; }
public int iYear { get; set; }
public int iRound { get; set; }
ecc..
}
and the database table that was automatically created is this, including all the properties inherited from EntityData:
Database table schema
Digging into the source code, Azure Mobile Apps sets up camelCase encoding of all requests and responses. It then puts them back after transmission accordign to rules - so iRaceId becomes IRaceId on the server.
The easiest solution to this is to bypass the auto-naming and use a JsonProperty attribute on each property within your server-side DTO and client-side DTO so that they match and will get encoding/decoded according to your rules.
So:
public class Race : EntityData
{
[JsonProperty("id")]
public string Id { get; set; }
[JsonProperty("raceId")]
public int iRaceId { get; set; }
[JsonProperty("year")]
public int iYear { get; set; }
[JsonProperty("round")]
public int iRound { get; set; }
etc..
}
Using a slightly modified version of the default ASP.NET MVC 5 template (with Individual Accounts), I am trying to get a subset of users based on an intermediary table. I have already built up an administration UI that can return a list of all users, but now I need to limit the set of users returned based on the currently logged in user's access privileges defined in the intermediary table.
Essentially, each user will have access to 1 or more clinics, so there will be one record for each clinic to which they have access.
If the currently logged in user belongs to a given role (e.g., "Clinic Admin"), then they should have the ability to retrieve a list of any users who belong to any of the clinics to which they have access.
Can anyone help point me in the right direction? This is my first Anything.NET application, so please feel free to explain like I'm five. :-)
Thank you in advance for any help you can offer.
Additional information:
Visual Studio 2013 Update 5
Entity Framework 6
MS SQL Server 2008 R2
Here is the intermediary table's class (ClinicUser):
[Table("clinic_users")]
public class ClinicUser
{
[Key]
public virtual ApplicationUser ApplicationUsers { get; set; }
[Required]
public string Id { get; set; }
[Required]
public System.Guid provider_id { get; set; }
[Required]
public System.Guid health_system_id { get; set; }
[Required]
public System.Guid clinic_id { get; set; }
}
Here is my ApplicationUser class:
public class ApplicationUser : IdentityUser
{
public string FirstName { get; set; }
public string LastName { get; set; }
public string FullName
{
get { return FirstName + " " + LastName; }
}
[ForeignKey("ClinicUsers")]
public override string Id
{
get
{
return base.Id;
}
set
{
base.Id = value;
}
}
public virtual ClinicUser ClinicUsers { get; set; }
public IEnumerable<SelectListItem> RolesList { get; set; }
public async Task<ClaimsIdentity> GenerateUserIdentityAsync(UserManager<ApplicationUser> manager)
{
// Note the authenticationType must match the one defined in CookieAuthenticationOptions.AuthenticationType
var userIdentity = await manager.CreateIdentityAsync(this, DefaultAuthenticationTypes.ApplicationCookie);
// Add custom user claims here
userIdentity.AddClaims(ClinicClaimsProvider.GetClaims(userIdentity));
return userIdentity;
}
}
In case it wasn't clear, what I'm really trying to do is narrow the list of ApplicationUsers to return only the list of users to which I have access to based on the clinics we have have in common.
If I were writing this as a SQL query, this would be one way to accomplish what I want (I just can't seem to quite get what I want with LINQ):
SELECT *
FROM AspNetUsers au
WHERE Id IN (
SELECT Id
FROM clinic_users
WHERE clinic_id IN (
SELECT clinic_id
FROM clinic_users
WHERE Id = 'CurrentUserId'
)
)
First of all do not user much properties in ApplicationUser class, you can manage user profiles table and connect it with application user class, so you can put lot of information about user in profile table.
Next task is organize table of clinics, branches etc... and asociate application users with them.
Next you have 2 ways:
1. asociate application users with clinics or branches.
or
2. Manage them with roles.
Here is example with Application users:
[Table("Clinics")]
public class Clinic
{
[Key]
public string Id { get; set; }
public virtual ICollection<ClinicUser> ClinicUsers { get; set; }
}
[Table("ClinicUsers")]
public class ClinicUser
{
[Key]
[Column(Order = 0)]
public string ClinicId { get; set; }
[Key]
[Column(Order = 1)]
public string UserId { get; set; }
}
So next you need Other ViewModels to display them hope this help.
UPDATE
// GET: ClinicUsers by Clinic
public async Task<ActionResult> ViewCurrentClinicUsers(string id) // This is clinis ID
{
if (id == null)
{
return new HttpStatusCodeResult(HttpStatusCode.BadRequest);
}
Clinic clinic = await db.clinic.FindAsync(id); // Get Selectedclinic
if (clinic == null)
{
return HttpNotFound();
}
ClinicUsers model = new ClinicUsers() // ClinicUsers model
{
clinic = clinic, // View Currentclinic
ClinicUsers = await db.ClinicUsers.Were(x => x.clinicid == clinic.id)ToListAsync()) // Get Users that asigned to current clinic
};
return View(model);
}
UPDATE 2
And Finaly if you want display clinics were is assigned current loged user
// GET: Clinics by currentuser
public async Task<ActionResult> ViewClinicsWithCurrentUserAccess()
{
var currentuserId = User.Identity.GetUserId(); // This gets currentloged user id
var currentuser = await db.Users.SingleOrDefaultAsync(x => x.Id == myUserId); // This gets currentloged user virtual
return View(await db.Clinics.Were(x => x.clinicuserid == currentuserId).ToListAsync());
}
I solved this a while back, but I thought I had better come back here and update my question with an answer, in case this might help someone else.
I updated my Clinic and ClinicUser classes accordingly:
Clinic.cs
[Table("clinics")]
public class Clinic
{
[Key]
public System.Guid ClinicId { get; set; }
public List<ClinicUser> ClinicUsers { get; set; }
}
ClinicUser.cs
[Table("clinic_users")]
public class ClinicUser
{
[Key, Column(Order = 0)]
public string UserId { get; set; }
[Key, Column(Order = 1)]
public System.Guid ClinicId { get; set; }
[ForeignKey("UserId")]
public virtual ApplicationUser ApplicationUser { get; set; }
[ForeignKey("ClinicId")]
public Clinic Clinic { get; set; }
}
Also, I updated the following excerpt of my ApplicationUser class from this:
[ForeignKey("ClinicUsers")]
public override string Id
{
get
{
return base.Id;
}
set
{
base.Id = value;
}
}
public virtual ClinicUser ClinicUsers { get; set; }
to this:
public List<ClinicUser> ClinicUsers { get; set; }
Finally, in my ApplicationUsersController's Index() action, I was able to use this:
public async Task<ActionResult> Index()
{
if (User.IsInRole("Admin")) return View(await UserManager.Users.ToListAsync());
var userId = User.Identity.GetUserId();
//Get the Ids of the current user's clinics
var userClinics = db.ClinicUsers.Where(cu => cu.UserId == userId).Select(cu => cu.ClinicId).ToList();
//Get all userIds of the user at the current user's clinics
var clinicUserIds = db.ClinicUsers.Where(cu => userClinics.Contains(cu.ClinicId)).ToList().Select(cu => cu.UserId);
var users = UserManager.Users.Where(u => clinicUserIds.Contains(u.Id));
return View(await users.ToListAsync());
}
In essence, if the user has the "Admin" role, then they will get a list of all users in the database. If they aren't, they will only get a list of the users that also belong to the clinics they have in common.
It may not be perfect, but it works. If anyone has any suggestions on how to improve this, I would be glad to hear it.
Again, my thanks to Archil (https://stackoverflow.com/users/4089212/archil-labadze) for his helpful responses.
So, I'm building a system for managing contacts. My contact domain model has quite a few string properties, as well as booleans. In the spirit of keeping behavior inside of the domain models, I've gone down the path of creating "update methods." I'm starting to feel like it's getting a bit burdensome. In the past, CRUD apps would just have a single update method and it would set all of the properties in one shot.
Am I on the right path? I'm concerned about having 10 - 15 update methods on my domain service and domain entities.
FYI, the example given is a bit contrived, so imagine a model with lots of string and boolean properties.
// Application Layer Stuff
public class UpdateContactCommand
{
public UpdateNamePredicate UpdateName { get; set; }
public UpdatePhonePredicate UpdatePhone { get; set; }
public int ContactId { get; set; }
}
public class UpdateNamePredicate
{
public string NewFirstName { get; set; }
public string NewLastName { get; set; }
}
public class UpdatePhonePredicate
{
public string NewPHone { get; set; }
}
public class UpdateContactResponse
{
public bool Success { get; set; }
public string Message { get; set; }
}
public interface IWcfService
{
UpdateContactResponse UpdateContact(UpdateContactCommand updateContactCommand);
}
public class WcfService : IWcfService
{
private readonly IContactService _contactService;
public WcfService(IContactService contactService)
{
_contactService = contactService;
}
public UpdateContactResponse UpdateContact(UpdateContactCommand updateContactCommand)
{
if (updateContactCommand.UpdateName != null)
{
_contactService.UpdateName(updateContactCommand.ContactId, updateContactCommand.UpdateName.NewFirstName,
updateContactCommand.UpdateName.NewLastName);
}
if (updateContactCommand.UpdatePhone != null)
{
_contactService.UpdatePhone(updateContactCommand.ContactId, updateContactCommand.UpdatePhone.NewPHone);
}
return new UpdateContactResponse();
}
}
// Domain Layer
public interface IContactService
{
// There are lots more of these
void UpdateName(int contactId, string newFirstName, string newLastName);
void UpdatePhone(int contactId, string newPhone);
}
public class ContactService : IContactService
{
private readonly IContactRepository _contactRepository;
public ContactService(IContactRepository contactRepository)
{
_contactRepository = contactRepository;
}
public void UpdateName(int contactId, string newFirstName, string newLastName)
{
var contact = _contactRepository.GetById(contactId);
contact.SetName(newFirstName, newLastName);
_contactRepository.Commit();
}
public void UpdatePhone(int contactId, string newPhone)
{
var contact = _contactRepository.GetById(contactId);
contact.SetPhone(newPhone);
_contactRepository.Commit();
}
}
public interface IContact
{
int Id { get; set; }
// There are lots more of these
void SetName(string newFirstName, string newLastName);
void SetPhone(string newPhone);
}
public class Contact : IContact
{
public int Id { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
public string Phone { get; set; }
public void SetName(string newFirstName, string newLastName)
{
FirstName = newFirstName;
LastName = newLastName;
}
public void SetPhone(string newPhone)
{
Phone = newPhone;
}
}
public interface IContactRepository
{
IContact GetById(int id);
void Commit();
}
public class ContactRepository : IContactRepository
{
public IContact GetById(int id)
{
// Not important
throw new NotImplementedException();
}
public void Commit()
{
// Not important
throw new NotImplementedException();
}
}
First of all, not all applications lend themselves well to a DDD approach. If you say your application could pretty much have been implemented in a CRUDish way before, chances are it's still CRUD now. Don't try to apply DDD on any app because it's the shiny new thing.
That being said, you don't just write "update methods" for the fun of it. They have to reflect the domain tasks your user wants to perform. Why does the user want to update a Contact ? Has the contact moved or just changed phone number ? Changed marital status and name ? Has the point of contact in a company been taken over by another employee ?
Usually, you won't have tons of update methods for a given entity. There's always a way to group changes in operations that are meaningful for the domain. Good ways to force yourself to do it are :
Think about the maximum number of form fields you can reasonably display to the user. Couldn't you split that complex UI into smaller, more meaningful screens ? From there you have to start reasoning (preferably with the help of a domain expert) about the tasks these should reflect.
Make your entity fields immutable from the outside. Thus you'll have to think harder about their true nature -- what should be in the constructor ? what should some other manipulation methods be ?
Let's say I have a ListingEvent class and a UserAccount class.
A ListingEvent can have many UsersAttending and a UserAccount can attend many ListingEvents.
The classes look like:
public class UserAccount
{
[AutoIncrement]
[PrimaryKey]
public int Id {
get ;
set;
}
public string Name {
get ;
set;
}
public UserAccount()
{
ListingEventsAttending = new List<UserAccountListingEvent> ();
}
[Reference]
public List<UserAccountListingEvent> ListingEventsAttending {
get;
set;
}
}
public class UserAccountListingEvent{
[AutoIncrement]
[PrimaryKey]
public int Id {
get;
set;
}
public Model.AttendingStatus Status { get; set; }
[References(typeof(UserAccount))]
public int UserAccountId {
get;
set;
}
[References(typeof(ListingEvent))]
public int ListingEventId {
get;
set;
}
}
public class ListingEvent
{
public ListingEvent()
{
UsersAttending = new List<UserAccountListingEvent>();
}
[AutoIncrement]
[PrimaryKey]
public int Id {
get ;
set;
}
public string Name { get; set; }
[Reference]
public List<UserAccountListingEvent> UsersAttending { get; set; }
public void RemoveUserAttending(UserAccount user)
{
if (user == null)
{
return;
}
UsersAttending.RemoveAll(u => u.UserAccountId == user.Id);
}
}
And I get a ListingEvent that has my UserAccount attending with:
var listingEvent = db.LoadSingleById<Model.ListingEvent> (request.Id);
And I can see that the user with the correct Id is attending so call RemoveUserAttending to remove the user. I can now see the user is not attending so I call:
db.Save (listingEvent, references: true);
But - now when I go to fetch that ListingEvent again the user is back to attending.
So my question is:
should the above work as expected?
if not - how should I be doing this?
db.Save() only INSERT or UPDATE entities i.e. it doesn't DELETE them.
To delete, retrieve the entities or entity Ids you want to delete and use OrmLite's db.Delete* API's explicitly, e.g. something like:
var removeUsersAttendingIds = listingEvent.UsersAttending
.Where(u => u.UserAccountId == user.Id)
.Select(u => u.Id);
db.DeleteByIds<UserAccountListingEvent>(removeUsersAttendingIds);