I have C# ASP.Net Core Webapi application that makes some calculations for physical models. I have this project class:
public class Project {
public IEnumerable<GenericComponent> components;
private ValueService valueService = new ValueService();
public Project(string norm) {
components = FactoryGenerator.GetFactory(norm).GetComponents(valueService);
}
public IEnumerable<ValueViewModel> GetResults() {
return valueService.GetResults();
}
public void SetValues(IEnumerable<ValueViewModel> values) {
values.ToList().ForEach(t => valueService.SetValue(t.Key, t.Value));
}
public bool Valid(string componentName) {
return components.Where(s => s.Title == componentName).Single().Valid();
}
}
That creates smaller components that do some calculations. Depending on the norm
parameter, another set of components is created (via the factory). Each component has a list of variables that may exist (the frontend know which ones) and it can do some calculations on those variables and on variables that are being initialized in other components (with 3 example implementations of GenericComponent
).
public abstract class GenericComponent : IObserver<Tuple<string, object>> {
public readonly IValueService valueService;
protected IDisposable cancellation;
public List<string> variables;
public List<string> observingVariables;
public GenericComponent(IValueService _valueService) {
valueService = _valueService;
}
public string Title { get; set; }
public void OnCompleted() { throw new NotImplementedException(); }
public void OnError(Exception error) {// no implementation
}
public abstract void OnNext(Tuple<string, object> value);
public abstract bool Valid();
}
public class LoadComponent : GenericComponent {
public LoadComponent(IValueService valueService) : base(valueService) {
variables = new List<string>() { "load" };
variables.ForEach(s => valueService.RegisterValue(s));
cancellation = valueService.Subscribe(this, variables);
}
public override bool Valid() => valueService.GetValue("load")?.Valid ?? false;
public override void OnNext(Tuple<string, object> value) {
double load = (double)value.Item2;
if (Math.Abs(load) < 1) {
valueService.SetWarning(value.Item1, "Small load, should be larger");
}
}
}
public class ValidationComponent : GenericComponent {
public ValidationComponent(IValueService valueService) : base(valueService) {
variables = new List<string>() { "validation.load" };
variables.ForEach(s => valueService.RegisterValue(s));
observingVariables = new List<string>() { "load", "area" };
cancellation = valueService.Subscribe(this, observingVariables);
}
private double CalcMargin() {
return 0.5 * (double)valueService.GetValue("area").Value
- (double)valueService.GetValue("load").Value;
}
public override bool Valid() => CalcMargin() > 0;
public override void OnNext(Tuple<string, object> value) {
if (valueService.GetValue("load").Value != null &&
valueService.GetValue("area").Value != null) {
double margin = CalcMargin();
bool valid = margin > 0;
valueService.SetValue("validation.load", "margin is: " + margin.ToString(),
valid);
if (margin < 1) {
valueService.SetWarning("validation.load", "Very tight margin");
}
}
}
}
public class CrossSectionComponent : GenericComponent {
public CrossSectionComponent(IValueService valueService) : base(valueService) {
variables = new List<string>() { "area", "radius" };
variables.ForEach(s => valueService.RegisterValue(s));
cancellation = valueService.Subscribe(this, variables);
}
public override bool Valid() => valueService.GetValue("radius")?.Valid ?? false;
public override void OnNext(Tuple<string, object> value) {
if (value.Item1 == "radius") {
double radius = (double)value.Item2;
if (radius < 0) {
valueService.SetError("radius", "Radius is must be larger than 0");
} else if (radius < 1) {
valueService.SetWarning("radius", "Radius is very small");
}
valueService.SetValid("radius", true);
valueService.SetValue("area", radius * radius * Math.PI, true);
}
}
}
Each component has a list of values with keys that are unique over the entire model (here I probably need more checks, whether truly all keys are unique).
At the moment I use a ValueService
, where each component registers its values with the respective keys. The ValueService
can be used to get values from other components in order to do validation calculations or intermediate steps.
public class ValueService : IValueService {
private IDictionary<string, ValueViewModel> values = new Dictionary<string, ValueViewModel>();
private IDictionary<string, List<IObserver<Tuple<string, object>>>> observers =
new Dictionary<string, List<IObserver<Tuple<string, object>>>>();
public void RegisterValue(string key) {
if (!values.ContainsKey(key)) {
values.Add(key, new ValueViewModel() {
Key = key,
Value = null
});
observers.Add(key, new List<IObserver<Tuple<string, object>>>());
}
}
public IEnumerable<ValueViewModel> GetResults() {
return values.Where(s => s.Value.Value != null).Select(s => s.Value);
}
public void SetValue(string key, object value) {
if (values.ContainsKey(key)) {
values[key].Value = value;
observers[key].ForEach(t => {
t.OnNext(new Tuple<string, object>(key, value));
});
}
}
public void SetValue(string key, object value, bool status) {
SetValue(key, value);
SetValid(key, status);
}
public void SetValid(string key, bool status) {
if (values.ContainsKey(key)) {
values[key].Valid = status;
}
}
public ValueViewModel GetValue(string key) {
if (values.ContainsKey(key))
return values[key];
return null;
}
public IDisposable Subscribe(IObserver<Tuple<string, object>> observer) {
return new Unsubscriber(observers.Values.ToList().SelectMany(s => s).ToList(), observer);
}
public IDisposable Subscribe(IObserver<Tuple<string, object>> observer, IEnumerable<string> keys) {
keys.ToList().ForEach(key => {
if (!observers.ContainsKey(key))
observers.Add(key, new List<IObserver<Tuple<string, object>>>());
if (!observers[key].Contains(observer))
observers[key].Add(observer);
});
return new Unsubscriber(observers.Values.ToList().SelectMany(s => s).ToList(), observer);
}
public void SetWarning(string key, string warning) {
if (values.ContainsKey(key)) {
values[key].Warning = warning;
values[key].Valid = true;
}
}
public void SetError(string key, string error) {
if (values.ContainsKey(key)) {
values[key].Error = error;
values[key].Valid = false;
}
}
}
The ValueViewModel is used to communicate with the front end
public class ValueViewModel
{
public string Key { get; set; }
public object Value { get; set; }
public bool Valid { get; set; }
public string Warning { get; set; }
public string Error { get; set; }
}
This is part of a stateless API, i.e. for each HTTP request (at the moment) a new Project
object is generated and used to make validations (via an array of ValueViewModels).
My API controller looks like this:
public IActionResult postData([FromRoute] string norm, [FromRoute] string component, [FromBody] ValueViewModel[] data)
{
Project project = new Project(norm);
project.SetValues(data);
return Ok(project.GetResults()); // <<-- previously mistake, it should be GetResults, that returns a ValueViewModel with populated values (where possible)
}
In order to make some local tests, I created the following console application
class Program {
static void Main(string[] args) {
Console.WriteLine("Hello world");
Project project = new Project("Eurocode_CH");
List<ValueViewModel> values = new List<ValueViewModel> {
new ValueViewModel() {
Key = "radius",
Value = 12.0
}
};
project.SetValues(values);
Console.WriteLine(Newtonsoft.Json.JsonConvert.SerializeObject(project.GetResults()));
values[0].Value = -0.4;
project.SetValues(values);
Console.WriteLine(Newtonsoft.Json.JsonConvert.SerializeObject(project.GetResults()));
values[0].Value = 0.4;
project.SetValues(values);
Console.WriteLine(Newtonsoft.Json.JsonConvert.SerializeObject(project.GetResults()));
values.Add(new ValueViewModel() {
Key = "load",
Value = 1.2
});
project.SetValues(values);
Console.WriteLine(Newtonsoft.Json.JsonConvert.SerializeObject(project.GetResults()));
}
}
This is the output
[{"Key":"area","Value":452.38934211693021,"Valid":true,"Warning":null,"Error":null},{"Key":"radius","Value":12.0,"Valid":true,"Warning":null,"Error":null}]
[{"Key":"area","Value":0.50265482457436694,"Valid":true,"Warning":null,"Error":null},{"Key":"radius","Value":-0.4,"Valid":true,"Warning":null,"Error":"Radius is must be larger than 0"}]
[{"Key":"area","Value":0.50265482457436694,"Valid":true,"Warning":null,"Error":null},{"Key":"radius","Value":0.4,"Valid":true,"Warning":"Radius is very small","Error":"Radius is must be larger than 0"}]
[{"Key":"area","Value":0.50265482457436694,"Valid":true,"Warning":null,"Error":null},{"Key":"radius","Value":0.4,"Valid":true,"Warning":"Radius is very small","Error":"Radius is must be larger than 0"},{"Key":"load","Value":1.2,"Valid":false,"Warning":null,"Error":null}]
My concerns are
- When the project continues, the number of components will get quite large, I fear there will be a performance issue if for each request the whole set of components needs to be generated --> should I use LazyLoading?
- Is the
ValueService
approach good to share data between the components? Is there a better solution? - I still need to implement a specific value-getter, e.g. a user requests the ValueViewModel for a specific key, the components needs to yield an error or warning if not all values were set.
- I think "AbstractComponent" is a better name than "GenericComponent", on a side note.
getresults
. Because atm you only havePost
implemented. \$\endgroup\$load
andradius
parameters. What problem does having a component system solve? \$\endgroup\$GetResult
method that returns a more populatedValueViewModel
. TheValid
method can be used further in the project, where you want to use single validations for certain components (mainly in the project object) \$\endgroup\$