Search code examples
c#.netarchitecture.net-coremicroservices

Proper way to create domain models and viewmodels in microservices


I am trying to create a microservice and I am trying to make sure i follow the best practices/patterns for designing my objects.

I plan on separating my objects so that there is one for returning (querying using dapper) to clients (class code below) and one that actually does state changes (code not in this question) that will commit changes to db

I have a entity CalendarEvents in the db and I have a viewmodel for this called CalendarEvent and it has basic properties for each of the entity fields for mapping and I have bunch of other properties for CalendarEvent that are aggregates of entity fields.

Questions:

  1. What is the proper way to fill this CalendarEvent object (viewmodel), should all the properties be passed through a constructor when creating from entity thats read from the db or is there a better way to do it.

  2. Should I be taking in the _dateformat or is there a more elegant way (this is coming from configuration file)

Below is my class for CalendarEntity (the class to be used as viewmodel)

Also if anyone can spot any other issues (probably many) with my approach, I would appreciate it.

public class CalendarEvent
{
    private readonly string _dateFormat;
    public CalendarEvent(string dateFormat)
    {
        _dateFormat = dateFormat;
    }

    public int EventId { get; set; }

    public string Title { get; set; }

    public DateTime? StartDateTimeUtc { get; set; }

    public DateTime? EndDateTimeUtc { get; set; }

    public string Summary { get; set; }

    public bool IsApproved { get; set; }

    public string TimeZoneId { get; set; }

    public bool IsDeleted { get; set; }

    public int ViewCount { get; set; }

    public DateTime CreatedDateUtc { get; set; }

    public DateTime? FeaturedStartDateUtc { get; set; }

    public DateTime? FeaturedEndDateUtc { get; set; }

    public string ContactEmailAddress { get; set; }

    public EventDateType DateType { get; set; }

    public int? Year { get; set; }

    public int? Period { get; set; }


    public EventCategory Category { get; set; }

    public string UrlFriendlyTitle => Helper.UrlFriendly(Title);

    public DateTime? EndDateTimeLocal { get; set; }

    public DateTime? StartDateTimeLocal { get; set; }

    public string FullEventDateString
    {
        get
        {
            string dateString;
            switch (DateType)
            {
                case EventDateType.Month:
                    {
                        var monthName = CultureInfo.CurrentCulture.DateTimeFormat.GetMonthName(Period.Value);
                        dateString = $"{monthName} {Year}";
                        break;

                    }
                case EventDateType.Quarter:
                    {
                        dateString = $"{Period}{Helper.GetNumberOrdinalSuffix(Period.Value)} Quarter {Year}";
                        break;
                    }
                case EventDateType.Specific:
                    {
                        if (StartDateTimeLocal.HasValue && EndDateTimeLocal.HasValue)
                        {
                            if (StartDateTimeLocal.Value.Date == EndDateTimeLocal.Value.Date)
                            {
                                if (StartDateTimeLocal.Value.Date == EndDateTimeLocal.Value.Date
                                    && StartDateTimeLocal.Value.Hour == 0 && StartDateTimeLocal.Value.Minute == 0 &&
                                    StartDateTimeLocal.Value.Second == 0 && EndDateTimeLocal.Value.Hour == 23)
                                {
                                    dateString = StartDateTimeLocal.Value.ToString(_dateFormat);
                                }
                                else
                                {

                                    var eventDateTime =
                                        $"{StartDateTimeLocal.Value.ToString(_dateFormat)} {StartDateTimeLocal.Value.ToShortTimeString()} - {EndDateTimeLocal.Value.ToShortTimeString()}";
                                    dateString = eventDateTime;
                                }
                            }
                            else
                            {
                                var eventDateTime =
                                    $"{StartDateTimeLocal.Value.ToString(_dateFormat)} - {EndDateTimeLocal.Value.ToString(_dateFormat)}";

                                dateString = eventDateTime;
                            }


                            break;

                        }
                        //TODO: fix this
                        throw new NotImplementedException();

                    }
                default:
                    throw new ArgumentOutOfRangeException();
            }


            return dateString;
        }
    }

    public EventStatuses Status
    {
        get
        {
            if (IsDeleted)
            {
                return EventStatuses.Deleted;
            }
            if (StartDateTimeUtc > DateTime.UtcNow)
            {
                return EventStatuses.NotStarted;
            }
            if (StartDateTimeUtc <= DateTime.UtcNow && EndDateTimeUtc > DateTime.UtcNow)
            {
                return EventStatuses.InProgress;
            }
            return EventStatuses.Completed;
        }
    }

    public string UrlFriendlyTitleForDisplay
    {
        get
        {
            var textInfo = new CultureInfo("en-US", false).TextInfo;
            return textInfo.ToTitleCase(UrlFriendlyTitle.ToLower().Replace("-", " "));

        }
    }

    private void GenerateLocalTimes()
    {

        if (DateType == EventDateType.Specific && StartDateTimeUtc.HasValue && EndDateTimeUtc.HasValue)
        {

            var tz = TimeZoneInfo.FindSystemTimeZoneById(TimeZoneId);

             //TODO: make sure ticks is zero in this case, this use to be totalSeconds comparison
            if (tz.BaseUtcOffset.Ticks == 0)
            {
                EndDateTimeLocal = EndDateTimeUtc;
                StartDateTimeLocal = StartDateTimeUtc;
            }
            else
            {
                EndDateTimeLocal = TimeZoneInfo.ConvertTimeFromUtc(EndDateTimeUtc.Value, tz);
                StartDateTimeLocal = TimeZoneInfo.ConvertTimeFromUtc(StartDateTimeUtc.Value, tz);
            }
        }
        else
        {

            //TODO: fix this
            throw new NotImplementedException();
        }
    }

    public string GoogleCalendarLink
    {
        get
        {
            if (DateType == EventDateType.Specific && StartDateTimeUtc.HasValue && EndDateTimeUtc.HasValue)
            {
                var text = string.Empty;
                if (!string.IsNullOrEmpty(Title))
                {
                    text = Uri.EscapeUriString(Title);
                }

                var startDate = StartDateTimeUtc.Value.ToString("yyyyMMddTHHmmssZ");
                var endDate = EndDateTimeUtc.Value.ToString("yyyyMMddTHHmmssZ");
                var details = string.Empty;
                if (!string.IsNullOrEmpty(Summary))
                {
                    details = Uri.EscapeUriString(Summary);
                }

                return
                    $"http://www.google.com/calendar/event?action=TEMPLATE&text={text}&dates={startDate}/{endDate}&details={details}&location=";
            }

            //TODO: fix this
            throw new NotImplementedException();

        }
    }
}

Solution

  • I am going to assume that you are using a Web Api but the same principles apply to an MVC app. I normally have a mapper layer that takes a domain object and maps it to a view model or dto. This way you can reuse the mapper anytime you want to return the same view model. So if you make a change to the view model it is all located in one place.

    See code below to answer question 1.

    Question 2: Why does your mircoservice care about what date the client wants. If you send back utc date then if a client wants it in a format then its up to them to format it.

    //Fake view model
    public class CalendarEventViewModel
    {
        public int EventId { get; set; }
    
        public string Title { get; set; }
    
        public DateTime? StartDateTimeUtc { get; set; }
    
        public DateTime? EndDateTimeUtc { get; set; }
    
        public string Summary { get; set; }
    
        public bool IsApproved { get; set; }
    
        public string TimeZoneId { get; set; }
    }
    
    
    public interface IMapper<in TIn, out TOut>
    {
            TOut Map(TIn model);
    }
    
    public class CalendarEventViewModelMapper : IMapper<CalendarEvent, CalendarEventViewModel>
    {
        public CalendarEventViewModel Map(CalendarEvent model)
        {
            return new CalendarEventViewModel
            {
                EndDateTimeUtc = model.EndDateTimeUtc,
                EventId = model.EventId,
                IsApproved = model.IsApproved,
                StartDateTimeUtc = model.StartDateTimeUtc,
                Summary = model.Summary,
                TimeZoneId = model.TimeZoneId,
                Title = model.Title
            };
        }
    }
    
    [Route("api/Values")]
    public class ValuesController 
    { 
           public ValuesController( IMapper<CalendarEvent, CalendarEventViewModel> calendarMapper)
           {
               _calendarMapper = calendarMapper;
           }
    
            // GET api/values/5
            [HttpGet("{id}")]
            public IActionResult Get(int id)
            {
    
                var calendarEvent = GetMyCalendarEventFromDB(id);
    
                return this.Ok(_calendarMapper.Map(calendarEvent));
            }
    
            private CalendarEvent GetMyCalendarEventFromDB(int id)
            {
                return new CalendarEvent("yyyy-dd-MM")
                {
                    EndDateTimeUtc = DateTime.UtcNow.AddHours(3),
                    EventId = id,
                    IsApproved = true,
                    StartDateTimeUtc = DateTime.UtcNow.AddHours(2),
                    Summary = "My magical Event",
                    TimeZoneId = "UTC",
                    Title = "Magical Event"
                };
            }
        }
    }
    

    Folder structure