I am learning DDD and trying to model articles, its variants and parameters.
Article can be on it's own without variants
Variant must be child of an article
both article and variant can have some parameters (colors, brands, sizes...), physical quantities (width, length, some article-specific like inner length)
If you set some parameter on an article, it can be "synchronized" to it's children variants
you can override this in a variant by setting that parameter as "unlinked", then this variant would have different parameter value than article
some parameters can be set multiple times (color: red, blue), but some only once (brand)
those parameters are dynamically create, it's not a Color or Brand property but key-value selected from preconfigured values
I think my main aggregate roots will be Article and Variant.
My current code looks like this:
internal class Article : AggregateRoot<ArticleId>
{
private readonly ISet<VariantId> _variants = new HashSet<VariantId>();
private readonly ISet<AssignedParameter> _parameters = new HashSet<AssignedParameter>();
private readonly ISet<AssignedPhysicalQuantity> _physicalQuantities = new HashSet<AssignedPhysicalQuantity>();
public string Name { get; private set; }
public string Catalog { get; private set; }
public IReadOnlySet<VariantId> Variants => _variants.AsReadOnly();
public IReadOnlySet<AssignedParameter> Parameters => _parameters.AsReadOnly();
public IReadOnlySet<AssignedPhysicalQuantity> PhysicalQuantities => _physicalQuantities.AsReadOnly();
private Article(ArticleId id, string name, string catalog)
: base(id)
{
Name = name;
Catalog = catalog;
}
public static Article Register(ArticleId id, string name, string catalog)
{
var article = new Article(id, name, catalog);
article.AddEvent(new ArticleRegistered(article.Id, article.Name, article.Catalog));
return article;
}
public void AssignParameter(Parameter parameter, ParameterValue parameterValue, bool syncToVariants)
{
if (!parameter.CanBeAssignedMultipleTimes && _parameters.Any(p => p.ParameterId == parameter.Id))
{
throw new ParameterCanBeAssignedOnlyOnceException($"Parameter {parameter.Id} can by assigned only once.");
}
var assignedParameter = new AssignedParameter(parameter.Id, parameterValue.Id, syncToVariants);
if (!_parameters.Add(assignedParameter))
{
throw new ParameterIsAlreadyAssignedException($"Parameter {parameter.Id} with value {parameterValue.Id} is already assigned.");
}
AddEvent(new ArticleParameterAssigned(Id, assignedParameter.ParameterId, assignedParameter.ParameterValueId));
}
public void UnassignParameter(Parameter parameter, ParameterValue parameterValue)
{
var assignedParameter = _parameters.FirstOrDefault(p => p.ParameterId == parameter.Id && p.ParameterValueId == parameterValue.Id);
if (assignedParameter is null)
{
throw new ParameterIsNotAssignedException($"Parameter {parameter.Id} is not assigned.");
}
_parameters.Remove(assignedParameter);
AddEvent(new ArticleParameterUnassigned(Id, assignedParameter.ParameterId, assignedParameter.ParameterValueId));
}
// physical quantity assign / unassign are similar to parameters
}
internal class Variant : AggregateRoot<VariantId>
{
private readonly ISet<AssignedParameter> _parameters = new HashSet<AssignedParameter>();
private readonly ISet<AssignedPhysicalQuantity> _physicalQuantities = new HashSet<AssignedPhysicalQuantity>();
public string Name { get; private set; }
public string Catalog { get; private set; }
public EanCode Ean { get; private set; }
public decimal Weight { get; private set; }
public IReadOnlySet<AssignedParameter> Parameters => _parameters.AsReadOnly();
public IReadOnlySet<AssignedPhysicalQuantity> PhysicalQuantities => _physicalQuantities.AsReadOnly();
internal Variant(VariantId id, string name, string catalog, EanCode ean, decimal weight)
: base(id)
{
Name = name;
Catalog = catalog;
Ean = ean;
Weight = weight;
}
// parameter and physical quantity assignment methods
}
Parameters:
internal class Parameter : AggregateRoot<ParameterId>
{
private readonly ISet<ParameterValue> _values = new HashSet<ParameterValue>();
public string Code { get; private set; }
public string Name { get; private set; }
public bool CanBeAssignedMultipleTimes { get; private set; }
public IReadOnlySet<ParameterValue> Values => _values.AsReadOnly();
public Parameter(ParameterId id, string code, string name, bool canBeAssignedMultipleTimes)
: base(id)
{
Code = code;
Name = name;
CanBeAssignedMultipleTimes = canBeAssignedMultipleTimes;
}
}
internal class ParameterValue : Entity<ParameterValueId>
{
public string Code { get; private set; }
public string Name { get; private set; }
public Parameter Parameter { get; private init; } = null!;
public ParameterValue(ParameterValueId id, string code, string name)
: base(id)
{
Code = code;
Name = name;
}
}
Value objects:
// for Article, variant doesn't have SyncToVariants property and has some other
internal class AssignedParameter : ValueObject
{
public ParameterId ParameterId { get; private init; }
public ParameterValueId ParameterValueId { get; private init; }
public bool SyncToVariants { get; private init; }
public AssignedParameter(ParameterId parameterId, ParameterValueId parameterValueId, bool syncToVariants)
{
ParameterId = parameterId;
ParameterValueId = parameterValueId;
SyncToVariants = syncToVariants;
}
protected override IEnumerable<object> GetEqualityComponents()
{
yield return ParameterId;
yield return ParameterValueId;
}
}
internal class AssignedPhysicalQuantity : ValueObject { ... }
My questions:
What would be the best way to notify variants of the parameter change? I can think of two ways using events.
First would be using ArticleParameterChanged(ArticleId, parameter.Id, parameterValue.Id). I would handle this event and changed all variants at once in the handler - I don't think this is the way, but I wouldn't need to hold variants collection in article.
Second would be to loop through variant IDs and create ArticleVariantParameterChanged(ArticleId, VariantId, parameterId, parameterValueId) event. This seems more correct to me?
if (syncToVariants)
{
foreach (var variantId in _variants)
{
AddEvent(new ArticleVariantParameterChanged(Id, variantId, parameter.Id, parameterValue.Id);
}
}
How do I add new variant to article? The easiest way would be to create new variant and update the article in one transaction.
// Article method
public Variant RegisterVariant(VariantId variantId, ...)
{
var variant = new Variant(variantId, ...);
_variants.Add(variantId);
return variant;
}
// command handler? or domain service?
var article = await _articleRepo.GetAsync(articleId);
var variant = article.RegisterVariant(variantId, ...);
await _variantRepo.AddAsync(variant);
await _articleRepo.UpdateAsync(article);
Or using events?
// Article method
public Variant RegisterVariant(VariantId variantId, ...)
{
var variant = Variant.Register(variantId, this.Id, ...);
return variant;
}
// Variant static method
public Variant Register(VariantId variantId, ArticleId articleId, ...)
{
var variant = new Variant(variantId, articleId, ...);
variant.AddEvent(new VariantRegistered(variantId, articleId));
return variant;
}
// command handler
var variant = article.RegisterVariant(...);
await _variantRepo.AddAsync(variant);
// VariantRegisteredHandler
article.AddVariant(variantId);
However here it seems kind of confusing to me, article.RegisterVariant and article.AddVariant... Maybe it's just wrong naming?
Also here can occur condition race between adding new variant and assigning a new parameter, when someone adds new parameter before the VariantRegistered event was handled, so it wouldn't sync that parameter.
So I'm thinking, is it even good idea to store those shared parameters in each variant? Maybe it would be enough to just have variant specific parameters there and merge everything in the read model? However this would be harder to prevent duplications - if the article already has a parameter "color - red", assigning "color - red" to variant would need to check the article parameters too and there can be another race condition.
I read that entities without any domain business logic could be treated as CRUD, that means they wouldn't even inherit AggregateRoot and each of them would have own repository, right?
Let's say someone really wants to delete some parameter value, for example blue color. This wouldn't (hopefully) happen in my app, but I'm still curious how this would be handled. He confirms he really wants to delete it and I need to go through all articles and unassign it from them. How?
My idea would be either to have ParameterValueDeleted event and ParameterValueDeletedHandler would query for all articles and variants and unassign it one by one, this handler would take really long time to execute.
Or ParameterValueDeletedHandler would query for all IDs, create some event for them and that handler would unassign it later. However in the latter case I don't know how that event would be named to make sense. UnassignArticleParameter seems more like command than event and ArticleParameterUnassigned is something coming from article. Also I read that commands indicate something that can be rejected, so I would say command doesn't fit here.
Also I see a problem when someone deletes that parameter and someone else queries for an article which doesn't have it unassigned yet - database join would fail because it would join to non existent parameter (considering single database for read and write model).
If I wanted to have mandatory parameters, where would be the best place to validate that all of them are set? Move the article registration logic to ArticleFactory and check it there? And for variants maybe ArticleService or VariantFactory? This seems kinda inconsistent to me, but maybe it's right?
var article = await _articleRepo.GetAsync(articleId);
_articleService.RegisterVariant(article, /* variant creation data */);
_variantFactory.Register(article, /* variant creation data */);
I think this should be all, I hope I explained everything well.
I would appreciate any help with this!
Little introduction: we have a complex entity and overgrown business logic related to it. With various fields that we can change and fields that updates from external project management software (PMS) like MS Project and some others.
The problem is that it's hard to centralize business logic for changing every fields cause that changes can offend other fields some fields are calculated but should be calculated only in several business scenarios. And different synchronization processes uses different business logic that depends on external data of specific PMS.
At this moment we have such ways to change the fields in our solution:
Constructor with parameters and private parameterless constructor
public class SomeEntity
{
public string SomeField;
private SomeEntity ()
{
}
public SomeEntity (string someField)
{
SomeField = someField;
}
}
Private set with public method to change field value
public class SomeEntity
{
public string SomeField {get; private set;}
public void SetSomeField(string newValue)
{
// there may be some checks
if (string.IsNullOrEmpty(newValue))
{
throw new Exception();
}
SomeField = newValue;
}
}
Event methods that perform operations and set some fields
public class SomeEntity
{
public string SomeField { get; private set; }
public string SomePublishedField { get; private set; }
public void PublishEntity(string publishValue)
{
SomeField = publishValue;
SomePublishedField = $"{publishValue} {DateTime.Now()}";
}
}
Public setters
public class SomeEntity
{
public string SomeField { get; set; }
}
Services that implements business logic:
public class SomeService : ISomeService
{
private DbContext _dbContext;
private ISomeApprovalsService _approvalsService;
public async Task UpdateFromMspAsync (MspSomeEntity mspEntity,
CancellationToken cancellationToken = default)
{
var entity = await _dbContext.SomeEntities
.Include (e => e.Process)
.SingleAsync (e => e.MspId == mspEntity.Id, cancellationToken);
switch mspEntity.Status:
case MspStatusEnum.Cancelled:
entity.Process.State = ProcessStateEnum.Rejected;
entity.Status = EntityStatusEnum.Stopped;
break;
case MspStatusEnum.Accepted:
_approvalsService.SendApprovals (entity.Process);
entity.Status = EntityStatusEnum.Finished;
break;
await _dbContext.SaveChangesAsync (cancellationToken);
}
}
State machine inside entity
public class SomeEntity
{
private StateMachine<TriggerEnum, StateEnum> _stateMachine;
public SomeEntity()
{
ConfigureStateMachine();
}
public string SomeField1 { get; set; }
public string SomeField2 { get; set; }
public string SomeField3 { get; set; }
private void ConfigureStateMachine()
{
_statusStateMachine.Configure(StateEnum.Processing)
.OnEntry(s=>SomeField1 = null)
.Permit(TriggerEnum.Approve, StateEnum.Approved);
_statusStateMachine.Configure(StateEnum.Approved)
.OnEntry(s=> SomeField1 = SomeField2 + SomeField3)
.Permit(TriggerEnum.Publish, StateEnum.Finished)
.Permit(TriggerEnum.Cancel, StateEnum.Canceled);
// etc
}
public void Trigger (TriggerEnum trigger) => _statusStateMachine.Fire(trigger);
}
State machine as service to prevent buisness logic leaks inside of entity.
var machine = _services.GetService<IStateMachine<SomeEntity, TriggerEnum>>();
var entity = await _dbContext.SomeEntities.FirstAsync();
IAttachedStateMachine<TriggerEnum> attachedMachine = machine.AttachToEntity(entity);
attachedMachine.Trigger(TriggerEnum.Publish);
It's wrong by architecture to have so many ways of changing values and we want to refactor this but to change approach, best practice must be chosen.
Please share your experience of resolving similar situation.
Update: found approach for DDD that called "aggregation root". It's looks good but only on paper (in theory) and works good with simple examples like "User, customer, shopping cart, order". On practice on every private setter you will create setter method (like in #2 of my examples). Also different methods for every system you work with. Not even talking about business logic inside database entity that violates SOLID's "single responsibility principle".
I am developing a Blazor app and in this app, I need to store the state of a List of user-selected Items.
when the user presses the 'Save Changes' button I would like to store the list in the state.
So far I have written the four mandatory classes that are written in the Fluxor doc:
ServiceState:
public record ServiceState
{
public List<ServiceModel> SelectedService { get; init; }
}
ServiceFeature
public override string GetName() => nameof(ServiceState);
protected override ServiceState GetInitialState()
{
return new ServiceState
{
SelectedService = new List<ServiceModel>()
};
}
SelectServiceAction:
public class SelectServiceAction
{
public List<ServiceModel> _serviceList;
public SelectServiceAction(List<ServiceModel> choosenServices)
{
_serviceList = choosenServices;
}
}
and SelectServiceReducer:
public class SelectServiceReducer
{
[ReducerMethod]
public static ServiceState OnSelectService(ServiceState state, SelectServiceAction action)
{
return state with
{
SelectedService = action._serviceList
};
}
}
I have tried many things and nothing seems to work the List stored in the state appears always empty
but the funny thing is that in the SelectServiceAction class:
public SelectServiceAction(List<ServiceModel> choosenServices)
{
_serviceList = choosenServices;
}
if I put a breakpoint in the last } _serviceList contains correctly all the items that were contained in the list I passed to the dispatcher. It seems like the problem is in the ServiceState itself,
Do you happen to know what am I doing wrong?
If you need me to show more code, I will post it
I thank you kindly in advance.
I found a way to do this. I don't know if this is the best way but here we are.
Your SelectServiceAction should have a ServiceModel in the constructor. I also changed the name of your method. I think its good to place the verb in the method name because you're likely to have a remove as well.
public class SelectServiceAddAction
{
public ServiceModel _service {get; set; }
public SelectServiceAddAction(ServiceModel service)
{
_service = service;
}
}
then in your reducer you call the method.
public static class SelectServiceReducer
{
[ReducerMethod]
public static ServiceState OnSelectService(ServiceState state, SelectServiceAddAction action)
{
var SelectedService = state.SelectedService;
SelectedService.Add(action._service);
return state with
{
SelectedService = SelectedService
};
}
}
Also consider changing "SelectedService" to a name that involves the state such as "CurrentSelectedServices" hope this helps!
I want to make a class to retrieve data from session state, am doing as following is it correct way?
I have make a static class and static methods to get data
[AuthorizeMember]
public class TestController : Controller
{
public ActionResult list()
{
Session["memberid"] = 12345;
return View();
}
public ActionResult iteminfo(string prodcode)
{
int memberid=SessionData.MemberTableId();
}
}
public static class SessionData
{
public static int MemberTableId()
{
return Convert.ToInt32(HttpContext.Current.Session[memberid]);
}
}
Static class will serve same contents on all logged in sessions.
Avoid using static classes in web application unless you completely understand how it is going to work.
Option 1:
You can either directly put values in session as shown in your code sample.
The advantage is you dont need to write any wrapper.
The disadvantage is anybody will keep on adding any value to session and after your application grows, nobody will know which data you are keeping in session.
Option 2:
OR you can create a single class to hold all session values together.
Whenever you want to read session value always read from this class.
Mark this class as serializable, so that in future, if you want to keep your session state in SQL Server or on some other server like REDIS cache, then it would just be minimal configuration change.
That way you will always know what all values you put in the session.
For ex. You can have a class to hold all your session values
[Serializable] //// This is required when using out of proc session state
public class MySessionState
{
public string Username {get; set;}
public string Role {get; set;}
public string FirstName{get; set;}
public string LastName {get; set;}
public int Age{get; set;}
}
Create a const key to refer this class object
public const string MySessionObjectKey = "mysessionstate";
Then in your controller actions you can use it like this:
public void MyAction()
{
var sessionState = Session[MySessionObject] as MySessionState;
if (sessionState == null)
{
sessionState = new MySessionState();
}
sessionState.FirstName = "SomeName";
sessionState.LastName = "SomeOtherName";
/// Process this session object
Session[MySessionObjectKey] = sessionState;
}
Hope this helps you.
In my app, i have a edmx file with some partial classes representing my tables, and a context class wich i have the methods i need i.e GetMessages() and GetMessageById(long idMessage).
In that case, i use the GetMessages() method to fill a grid. Everything normal.
The Message entity class is something like this:
[Table("Message")]
public partial class Message
{
public long IdMessage{get;set;}
public long IdStatus{get;set;}
}
The problem is that i have another table that i have the StatusDescription that i need to get using the IdStatus.
I created another partial class with this property:
public partial class Message
{
private static readonly MessageRepository MessageRepository ;
static Message()
{
MessageRepository = new MessageRepository();
}
public string StatusDescription
{
get { return MessageRepository .GetMessageDescription(this.Cd_SchedulerStatus); }
}
}
And the method in the MessageRepository:
public MessageRepository()
{
_appContext= AppContext.GetContext();
}
public string GetMessageStatusDescription(int statusId)
{
var status = _appContext.Message.FirstOrDefault(id => id.IdStatus.Equals(statusId));
return status != null ? status.StatusDescription : string.Empty;
}
I know that it generates problems and it is not the best approach to deal with it, because im acessing the data inside the entity class, im having the n+1 problem, each time i send a new query to the database.
I would like to know if somebody have this problem and whats the best solution?
I suggest you create a new context for each message description request:
public string GetMessageStatusDescription(int statusId)
{
using (var appContext = AppContext.GetContext())
{
var status = appContext.Message.FirstOrDefault(id => id.IdStatus == statusId);
return status != null ? status.StatusDescription : string.Empty;
}
}