How To

How to create a Data Model with C# .Net Core

Reading Time: 8 minutes

Welcome back, coders! In this article, we’re going create some Data Models with .Net Core.

Just in case you’re not familiar with the term Data Model, let’s go over the general concept. For purposes of this article: A Data Model is a C# class that represents a database table. Typically Data Models are what we might refer to as a POCO’s (plain old c-sharp objects), without any data access functionality.

For this article, we’ll focus on creating just the data models. The next article in this series will define our Data Store project which we’ll use Entity Framework to connect to the database and bind our data models to the actual data.

Having the separation of the Data Models and the Data Store is referred to a Separation of Concerns which is actually the first letter in the SOLID principals. If you’re not familiar with SOLID don’t worry you will learn all the principles in a nice step by step once this series of posts has concluded.

The Project

We’re going to keep it simple for this article and create two classes which will represent two tables in our database. Essentially you could think of this as a basic start to a larger project management system. The current requirement will be to track Users and Tasks.

Typically with Data Modes we keep the Model name singular but when referring to a set of data we’ll use the plural (e.g. Users, Tasks), so we’ll keep it singular for now and create the following classes:

  1. User
  2. Task

Step 1 Create the .Net Core Project

The first thing we need to do is create our project. This article assumes you have .Net Core installed. You can use Visual Studio (2017 or 2019) for Windows or the Mac, or VS Code. Personally, I like Visual Studio better for projects like these but either one works just fine.

If using visual studio, simply select New Project. Create a new .Net Standard Library project.

If using VS Code or if you want to do create it via the command line tools. Open a terminal / command prompt and issue the following:

dotnet new classlib -n DataModels

This will create a new Project of a type of Classlibrary with the name of DataModels. The target frame work is actually netstardard2.0, which means it could be used on .Net Core or other .Net frameworks.

We’re keeping it on netstardard2.0 so it can be used cross platform.

A default file of Class1 is created as well. You can either delete this or simply rename it to our first class which will represent a table for the User(s). Then add another class called Task. You should now have two classes

  1. User
  2. Task

Depending on how you made them, you may or may not have the `using System` and if you named project something other than DataModels your namespace may be different as well.

using System;

namespace DataModels
{
    public class User
    {
    }
}

namespace DataModels
{
    public class Task
    {
        
    }
}

The next step is to add the fields (aka properties in the class) we will want in our database. We’re going to keep our requirements pretty basic for this article but feel free to add more properties on your own.

A User will have the following:

Column NameData TYpeNullableMax CharsDescription
IdLongNo-Primary Key
UserNameStringNo250Email Address
FirstNameStringYes150First Name
LastNameStringYes150Last Name
PasswordStringNo250Hashed Password
CreatedDateTimeUtcDateTimeNo-Created Date Time in UTC Format
ModifedDateTimeUtcDateTimeYes-Modified Date Time in Utc
DeletedDateTimeUtcDateTimeYes-Deleted Date Time in UTC
IsDeletedBooleanNo-Is the user deleted

A Task will have the following:

Column NameData TYpeNullableMax CharsDescription
IdLongNoPrimary Key
UserIdLongNoForeign Key back to the user
SubjectStringNo250The Subject of the Task
DetailsStringNoDetail of the Task
PercentCompleteDecimalNoThe percentage completed
StartedDateTimeUtcDateTimeYes-The Date Started
CompletedDateTimeUtcDateTimeYes-The Date Completed
CreatedDateTimeUtcDateTimeNo-Created Date Time in UTC Format
ModifedDateTimeUtcDateTimeYes-Modified Date Time in Utc
DeletedDateTimeUtcDateTimeYes-Deleted Date Time in UTC
IsDeletedBooleanNo-Is the user deleted

Class Definition

Initially, our classes would look like this:

NOTE: If you didn’t have the using System; before, you will need it now. It’s part of the namespace for DateTime or you can make the datatype name use its fully qualified name on the property (System.DateTime).

A couple of small notes:  The odd syntax of DateTime? means it can be null.  You could write it in a longer format of Nullable<DateTime>, either one works.  For the longest time, I was stuck with using the longer format (just a personal preference) but at some point, I switched to the shorter syntax of ?.  It’s a bit easier and at the end of the day, they are the exact same thing.

using System;

namespace DataModels
{
    public class User
    {
        public long Id { get; set; }
        public string UserName { get; set; }
        public string FirstName { get; set; }
        public string LastName { get; set; }
        public string Password { get; set; }     
        public DateTime CreatedDateTimeUtc { get; set; } = DateTime.UtcNow;
        public DateTime? ModifiedDateTimeUtc { get; set; }
        public DateTime? DeletedDateTimeUtc { get; set; }
        public bool IsDeleted { get; set; } = false;
    }
}

using System;

namespace DataModels
{
    public class Task
    {
        public long Id { get; set; }
        public long UserId { get; set; }
        public string Subject { get; set; }
        public string Details { get; set; }
        public decimal PercentComplete { get; set; }
        public DateTime? StartedDateTimeUtc { get; set; }
        public DateTime? CompetedDateTimeUtc { get; set; }    
        public DateTime CreatedDateTimeUtc { get; set; } = DateTime.UtcNow;
        public DateTime? ModifiedDateTimeUtc { get; set; }
        public DateTime? DeletedDateTimeUtc { get; set; }
        public bool IsDeleted { get; set; } = false;  
    }
}

 

Too Much Replicated Code – Follow DRY

As you can see, with only two tables we’re already starting to duplicate a lot of fields. If we start adding several more tables, the repetition becomes tedious, error prone and often inconsistent in the naming.  For example CreatedDateTimeUtc become CreatedDate in some classes and CreateDate in others.  The bigger your team the more likely this will become a mess. Let’ take care of that now by use inheritance.

BTW- DRY stands for Don’t Repeat Yourself.  It’s a good mentality to have when writing code.  It helps to centralize your code.  The less you repeat the same code, the less often you will need to fix the same bugs/mistakes, etc.

Step 2 – Create a base class for the replicated code

First Create a base class. In this example, we’re going to actually name it BaseDataModel but the word Base is not required, it just makes it a little more explicit to the intentions of the class. We’re also going to make this class an abstract class, which means it can’t be created on its own.

This class will contain all the duplicated properties/fields we want out tables to have. It will look like this:

using System;

namespace DataModels
{
    public abstract class BaseDataModel
    {
        public long Id { get; set; }
        public DateTime CreatedDateTimeUtc { get; set; } = DateTime.UtcNow;
        public DateTime? ModifiedDateTimeUtc { get; set; }
        public DateTime? DeletedDateTimeUtc { get; set; }
        public bool IsDeleted { get; set; } = false;

    }
}

Next, we want our other two classes to inherit the BaseDataModel, which means in addition to there properties they will have all the properties of the BaseDataModel too.
Inheriting is done by adding : BaseClassName to your class declaration.

public class User

becomes

public class User : BaseDataModel

Once you add the base class, depending on your editor, you will see some warnings about the properties with the same name as the Base Class hiding the inherited member. Don’t worry about it, because we are going to remove them from our User and Task classes.

Now you’re classes should look like this:
NOTE: Now the User class doesn’t need the using System since none of its DataTypes require its namespace.

using System;

namespace DataModels
{
    public class User: BaseDataModel
    {
        public string UserName { get; set; }
        public string FirstName { get; set; }
        public string LastName { get; set; }
        public string Password { get; set; }
    }
}

using System;

namespace DataModels
{
    public class Task : BaseDataModel
    {
        public long UserId { get; set; }    
        public string Subject { get; set; }
        public string Details { get; set; }
        public decimal PercentComplete { get; set; }
        public DateTime? StartedDateTimeUtc { get; set; }
        public DateTime? CompetedDateTimeUtc { get; set; }
    }
}

Minor Drawbacks

The only minor drawback when using Inheritance is the order in which your fields are listed in your object and thus in the database table.  This will become more apparent later and their are some ways around it, however this really isn’t a big deal, it’s just a visual thing.

In a visual representation,  prior to using the inherited structure, our data model would look something like the section below.  Everything is in the order we defined it within our class:

 

User Object
{
   "Id": 1,
  "UserName": "demo@geekcafe.com",
  "FirstName": "Eric",
  "LastName": "Wilson",
  "Password": null,
  "CreatedDateTimeUtc": "2019-03-24T14:34:30.839589Z",
  "ModifiedDateTimeUtc": null,
  "DeletedDateTimeUtc": null,
  "IsDeleted": false
}

But after the inherited models, it will look something like section below.  As you can see, the objects are combined at runtime and the fields will be merged in order of the objects being added to it.   Again this is just a visual thing (mostly when debugging or looking at the table definitions), however don’t worry everything still works as it’s expected to.

User Object
{
  "UserName": "demo@geekcafe.com",
  "FirstName": "Eric",
  "LastName": "Wilson",
  "Password": null,
  "Id": 1,
  "CreatedDateTimeUtc": "2019-03-24T14:34:30.839589Z",
  "ModifiedDateTimeUtc": null,
  "DeletedDateTimeUtc": null,
  "IsDeleted": false
}

Step 3 – Adding Annotations

The final step, is to add some DataAnnotations, which isn’t required here but I like to do it as general practice. This assumes I always want my Data Models represented the same even if used across other projects. The annotations allows us to define attributes on our properties such as the MaxLength, if it’s Required, ForeignKeys, we can also define or override the Table name and the Column name. Typically an ORM will simply use the name of the Class and it’s Properties but if you want to override or be more declarative you can.

To use Data Annotations you need to Reference the System.ComponentModel.Annotations as a Package Reference. You use the Visual Studio and right click on the Dependencies, then Add Package and search for System.ComponentModel.Annotations or you can run a command line in your project:
dotnet add package System.ComponentModel.Annotations

If you’re ever unsure of a package you can go to www.nuget.org and search for the packages (or use the search in Visual Studio).

The Nuget site, it has examples on various ways to get the packages (e.g. Package Manager, .NET CLI, Paket CLI).
In the end, whether you use the Visual Studio tools, the dotnet command or another command line, they all do the same thing, which is add the reference to your .csproj file, then typically do a restore, which loads it into your project.

Which means, if you know the exact name of the package you want to add, you can manually add it to your project file .csproj then restore it.

<PackageReference Include="System.ComponentModel.Annotations" Version="4.5.0" />

Now that we have it loaded we can add some DataAnnotations to our class.

Annotations can be added on a single line of their own or in a comma-delimited list.

[Required]
[Key]
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]

Is the same as

[Required, Key, DatabaseGenerated(DatabaseGeneratedOption.Identity)]

The can be mixed and matched

[Required, Key][DatabaseGenerated(DatabaseGeneratedOption.Identity)]

Here’s the final class definitions:

using System;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

namespace DataModels
{
    public abstract class BaseDataModel
    {
        [Required, Key, DatabaseGenerated(DatabaseGeneratedOption.Identity)]
        public long Id { get; set; }
        [Required]
        public DateTime CreatedDateTimeUtc { get; set; } = DateTime.UtcNow;
        public DateTime? ModifiedDateTimeUtc { get; set; }
        public DateTime? DeletedDateTimeUtc { get; set; }
        public bool IsDeleted { get; set; } = false;

    }
}

using System;
using System.ComponentModel.DataAnnotations;

namespace DataModels
{
    public class User: BaseDataModel
    {
        [Required, MaxLength(250)]
        public string UserName { get; set; }
        [MaxLength(150)]
        public string FirstName { get; set; }
        [MaxLength(150)]
        public string LastName { get; set; }
        [Required, MaxLength(250)]
        public string Password { get; set; }
    }
}

using System;
using System.ComponentModel.DataAnnotations;

namespace DataModels
{
    public class Task : BaseDataModel
    {
        [Required]
        public long UserId { get; set; }
        [Required, MaxLength(250)]
        public string Subject { get; set; }
        public string Details { get; set; }
        public decimal PercentComplete { get; set; }
        public DateTime? StartedDateTimeUtc { get; set; }
        public DateTime? CompetedDateTimeUtc { get; set; }
    }
}

Testing Our Data Model

We’ll build some actual unit tests later but for now, if you want to see it in action, we can add another project, then add a reference to the DataModels.

Visual Studio:

  • Add New Project: Type Console Application
  • Right Click on the Dependencies and Edit References
    • Find the DataModels project and check it.

VS Code

  • From the command line
    • Move up one director from your DataModels Project (cd ..)
    • dotnet new console -n ConsoleApp
    • cd ConsoleApp
    • dotnet add reference ../DataModels

Add the Newtonsoft.Json package.  I’ll let you try this out on your own as a learning experience but if you get stuck, send me a message in the contact us form and I’ll shoot you a reply.  Trust me, if you try to this out yourself and send a little time figuring it out – you’ll never forget it.

Add the following code and run the console app:

Hint: if it fails, you’ll need to make sure you’ve referenced the Newtonsoft.Json package, both in the project and in the using statement.  Your editor may give you some help but if not make sure you have `using Newtonsoft.Json;` somewhere near the `using System;` line – at the top of the program.cs file.

 

Feel free to modify the code to use your information:

static void Main(string[] args)
        {
            var user = new DataModels.User();

            user.Id = 1;
            user.FirstName = "Eric";
            user.LastName = "Wilson";
            user.UserName = "demo@geekcafe.com";


            var task = new DataModels.Task();

            task.Id = 1;
            task.UserId = user.Id;
            task.Subject = "Create Data Models";
            task.Details = "Creating Data Models was easier that I thought!";

            Console.ForegroundColor = ConsoleColor.Blue;
            Console.WriteLine("User Object");
            Console.ForegroundColor = ConsoleColor.White;
            Console.WriteLine(JsonConvert.SerializeObject(user, Formatting.Indented));

            Console.WriteLine("");
            Console.ForegroundColor = ConsoleColor.Blue;
            Console.WriteLine("Task Object");
            Console.ForegroundColor = ConsoleColor.White;
            Console.WriteLine(JsonConvert.SerializeObject(task, Formatting.Indented));
        }
    }

 

Summary

To recap:

  • We created a simple POCO for a table definitions in our CSharp classes.
  • We refactored the classes to use inheritance and reduced repetitive sections of code
  • Finally we added Data Annotations to make our intentions clear.

Till Next Time

We could also add Foreign Key references, but we’ll leave that for another article.

Until next time. Remember to keep your code clean!