Modern Web Development with ASP.NET Core 3 Discount Code

Modern Web Development with ASP.NET Core 3 - Second Edition

If you are interested in my book, Modern Web Development with ASP.NET Core 3, from Packt Publishing, you can use this code for a 25% discount: 25MODERNWEB.

Get it from Amazon: https://www.amazon.com/gp/mpc/A37G5RAWO3DSQX or Packt: https://www.packtpub.com/product/modern-web-development-with-asp-net-core-3-second-edition/9781789619768.

SharedFlat and Databases

Introduction

This post is part of a series on SharedFlat. See here the first (introduction) and here the second (UI). This time I will be explaining how SharedFlat handles multitenant databases.

There are essentially three strategies for doing multitenancy when it comes to databases:

  1. One connection string per tenant, which effectively means a different database for each tenant
  2. One schema per tenant, meaning, one table will exist on a schema for each tenant
  3. A single database where multitenant tables have a column that is used to distinguish tenants

Let’s see how we can have this with SharedFlat! First, it’s needed to say that this uses Entity Framework Core 3.1 and that there is a TenantDbContext abstract class that we should inherit from.

Database per Tenant

Let’s start with database per tenant. In this case, it is expected that we store in the configuration (typically the appsettings.json file) one connection string for each for each tenant. For example:

{ “ConnectionStrings”: {

    “abc”: “<connection string for abc>”,

    “xpto”: “<connection string for xpto>”

}

}

We just need, when registering the services, to use the DifferentConnectionPerTenant extension method:

services

.AddTenantService()

.AddTenantDbContextIdentification()

.DifferentConnectionPerTenant();

And SharedFlat will know what to do. If, for any reason, you wish to use a differently-named connection string, there is an overload that lets you customize this:

services

.AddTenantService()

.AddTenantDbContextIdentification()

.DifferentConnectionPerTenant(options =>

{

options.Mapping[“xpto”] = “xpto2”;

});

Schema per Tenant

Next is different schema per tenant. The extension method to call is DifferentSchemaPerTenant:

services

.AddTenantService()

.AddTenantDbContextIdentification()

.DifferentSchemaPerTenant();

By default, the schema will be identical to the tenant; should we need to change that, there is an overload:

services

.AddTenantService()

.AddTenantDbContextIdentification()

.DifferentSchemaPerTenant(options =>

{

options.Mapping[“xpto”] = “xpto2”;

});

This will set the schema on each entity that implements the ITenantEntity interface (a simple marker interface) when the TenantDbContext -derived class is initialized. To be clear, using SQL Server as an example, it will change the default (“dbo”) for the tenant value, so a table named “dbo.Product” will become “abc.Product”, “xpto.Product”, etc.

Filter by Tenant

The last technique depends on having a column on each table that we want to make multitenant, as dictated by its entity implementing ITenantEntity. The method to call is FilterByTenant, unsurprisingly:

services

.AddTenantService()

.AddTenantDbContextIdentification()

.FilterByTenant();

As is usual, the method has some defaults, like, the name of the column (“Tenant”), but we can change it:

services

.AddTenantService()

.AddTenantDbContextIdentification()

.FilterByTenant(“TenanantCol”);

Or, in the eventual case where you need to change the mapping:

services

.AddTenantService()

.AddTenantDbContextIdentification()

.FilterByTenant(options =>

{

options.Mapping[“xpto”] = “xpto2”;

});

Filtering by tenant means that whenever the context is querying for a multitenant-aware entity, a filter is added:

SELECT p.[ProductId], p.[Name], p.[Price]

FROM [dbo].[Product] p

WHERE p.[Tenant] = ‘abc’

Conclusion

And this is it for multitenant databases. Keep tuned for more on SharedFlat, and, as always, let me hear your thoughts! Winking smile

SharedFlat and Multitenant UI

Introduction

In my previous post, I introduced SharedFlat, a library for making multitenant ASP.NET Core apps easier to build. This time I’m going to talk about how can we customize the UI per tenant.

Using Conditional Logic in Views

One way to make UI modifications is to have conditional logic in views. There is an extension method, IsEnabledForTenant, that can be used to check if a specific property is set to true for the current tenant:

@if (this.IsEnabledForTenant(“Foo”))

{

<p>Bar</p>

}

This will look for an option registered using the Options Pattern that was described in the previous post.

Another option is to check for a specific tenant, using the IsTenant extension method:

@if (this.IsTenant(“abc”))

{

<p>abc</p>

}

For the exact same purpose we can also use the <tenant> tag helper:

<tenant name=”xpto”>

<p>xpto!</p>

</tenant>

If we wish to load a partial view, there’s another tag helper just for that, <tenant-partial>:

<tenant-partial tenant=”xyz” name=”xyzView”></tenant-partial>

This will conditionally load the xyzView file if the current tenant is xyz.

Using Different Files

A different approach would be to have different files (in different folders), one for each tenant. Say, for example, that you want to have a different folder per each tenant in your Views\<controller> folder. You just need to call AddTenantLocations when registering the services:

services

.AddTenantService()

.AddTenantLocations();

After this, if you have this folder structure:

  • Views
    • Home
      • abc
      • xyz

ASP.NET Core will load the views from the tenant-specific folder, (abc or xyz) and only if it cannot find the files there ASP.NET Core will it resort to the default ones (in Home or Shared).

Finally, if you wish to load static files from a well-known location that refers to the tenant, you can do this:

<script src=”~/@this.GetTenant()/index.js”></script>

Conclusion

This is as much as it goes for now for making the UI multitenant with SharedFlat. On the next post I will be talking about data, how to access databases in a multitenant way.

Introducing SharedFlat

Introduction

A multitenant web application is one that responds differently depending on how it is addressed (the tenant). This kind of architecture has become very popular, because a single code base and deployment can serve many different tenants.

There are a few libraries out there that help ASP.NET Core developers implement such architectures, but, since I wrote a book on ASP.NET Multitenant Applications a few years ago, I decided to write something from scratch for ASP.NET Core – and here is SharedFlat!

For the sake of the examples, let’s assume two tenants that correspond to two different domain names, abc.com (abc) and xyz.net (xyz).

Concepts

SharedFlat is available at GitHub and NuGet and you are free to have a look, fork and suggest improvements. It is still in early stages, mind you!

It was designed around the following key tenets:

What is a tenant?

A tenant is a unique entity, identified by a name, and the application must respond appropriately to in, in terms of:

  • User interface: each tenant may have one or more different UI aspects
  • Data: each tenant should have access to different data
  • Behavior: the application should behave (maybe slightly) differently, for each tenant

The current tenant is always returned by the implementation of the ITenantService interface:

public interface ITenantService

{

string GetCurrentTenant();

}

Simple, don’t you think? The returned tenant information is the tenant’s name (or code, if you prefer).

How do we identify a tenant?

Now, the first thing to know is, how do we identify the current tenant? Well, there are different strategies for this, but they all implement this interface:

public interface ITenantIdentificationService

{

string GetCurrentTenant(HttpContext context);

}

The strategies itself can be various:

  • Using the request’s HTTP Host header: useful for scenarios where we have multiple domains pointing to the same IP address and we want to distinguish the tenant by the requested host
  • Using a query string field: more for debugging purposes
  • Using the source IP of the request: for when we want to force IPs to be of a specific tenant
  • Static: for testing purposes only
  • Dynamic: when we decide in code, at runtime, what the tenant is
  • Some header’s value

TenantIdentificationServices

The ITenantService that we saw first will have to somehow delegate to one of these implementations for getting the current tenant. We need to register one implementation by using one of the following extension methods:

services.AddTenantIdentification()

.DynamicTenant(ctx => “abc.com”) //dynamic

.TenantForQueryString() //query string

.TenantForSourceIP() //source IP

.TenantForHost() //host header

.TenantForHeader() //some header

.StaticTenant(“abc.com”); //static

Of course, only one of these methods (DynamicTenant, TenantForQueryString, TenantForSourceIP, TenantForHost, TenantForHeader, StaticTenant) should be called.

Some of these different strategies need configuration. For example, to map a host name to a tenant name, we use code like this:

services.AddTenantIdentification()

.TenantForSourceIP(options =>

{

options.Mapping.Default = “abc”;

options.Mapping

.Add(“192.168.1.0”, “xyz”)

.Add(“10.0.0.0”, “abc”);

});

Some tenant identification services also expose an interface ITenantsEnumerationService, which allows listing all the known tenants (their name, or code). This service can be retrieved from the DI, but only if the registered identification service supports it:

public IActionResult Index([FromServices] ITenantsEnumerationService svc)

{

var tenants = svc.GetAllTenants();

return View(tenants);

}

Configuration per tenant

It is also very important to get different configuration depending on the current tenant. Almost all of the tenant identification strategies (except StaticTenantIdentificationService and DynamicTenantIdentificationService) support a mapping between “something” and a tenant code. This “something” may be the source IP (SourceIPTenantIdentificationService), the request’s Host header (HostTenantIdentificationService), etc. There is a standard configuration section that you can add to your appsettings.json file:

{

“Tenants”: {

“Default”: “”,

“Tenants”: {

“xyz.net”: “xyz”,

“abc.com”: “abc”

}

}

}

You can load the configuration using an extension method:

services.AddTenantIdentification()

.TenantForHost(options =>

{

Configuration.BindTenantsMapping(options.Mapping);

});

Or you can just set the values manually, as we’ve seen before.

If you want to set some per-tenant configuration, you should use the Options Pattern with a named configuration (one per tenant):

services.Configure<SomeClass>(“abc”, options =>

{

options.Foo = “bar”;

});

And then you need to retrieve it like this:

public HomeController(IOptionsSnapshot<SomeClass> optionsSnapshot, ITenantService tenantService)

{

var tenant = tenantService.GetCurrentTenant();

var options = optionsSnapshot.Get(tenant);

//…

}

There is yet another option: you can have a class that implements ITenantConfiguration, which is defined as:

public interface ITenantConfiguration

{

string Tenant { get; }

void ConfigureServices(IConfiguration configuration, IServiceCollection services);

}

You can have one class per tenant and in there you can register services that are specific for it. In order for it to work, you need to call the AddTenantConfiguration extension method:

services.AddTenantIdentification()

.TenantForHost(options =>

{

Configuration.BindTenantsMapping(options.Mapping);

})

.AddTenantConfiguration();

Conclusion

This is all I have for now, stay tuned for more on SharedFlat!

Modern Web Development with ASP.NET Core 3 – Second Edition

Modern Web Development with ASP.NET Core 3 - Second Edition

So, my new book is out now! The title changed recently from Mastering ASP.NET Core 3 – Second Edition to Modern Web Development with ASP.NET Core 3 – Second Edition. It was Packt’s decision, and I didn’t really bother about that, although I think the former was more accurate. It’s around 700 pages, and it is divided in three parts:

  • The Fundamentals of ASP.NET Core
  • Improving Productivity
  • Advanced Topics

On the first part, we can find the following chapters:

  1. Getting started with ASP.NET Core
  2. Configuration
  3. Routing
  4. Controllers and Actions
  5. Views

On the second part, we have:

  1. Using Forms and Models
  2. Implementing Razor Pages
  3. API Controllers
  4. Reusable Components
  5. Understanding Filters
  6. Security

Finally, on the third and last:

  1. Logging, Tracing, and Diagnostics
  2. Understanding How Testing Works
  3. Client-Side Development
  4. Improving Performance and Scalability
  5. Real-Time Communication
  6. Introducing Blazor
  7. gRPC and Other Topics
  8. Application Deployment

There is also an appendix on the dotnet tool.

I’d say the highlights are:

  • Configuration and feature management
  • API controllers including versioning and OData
  • Razor Pages
  • SignalR
  • Blazor
  • gRPC

The technical reviewers were Alvin Ashcraft (Microsoft MVP and world famous for Morning Dew) and Prakash Tripathi (Microsoft MVP too).

Should you wish to buy it in the next days, there is a discount code that you can use that will grant you a 50% discount: WEBDEV50.

I hope you enjoy it and I’d like to hear from you about the book! Winking smile

ASP.NET Core OData Part 3

Introduction

This will be the third post on OData and ASP.NET Core 3. Please find the first post (basics) here and the second post (querying) here. This time, I will talk about actions and functions. For demo purposes, let’s consider this domain model:

ClassDiagram1

Functions

A function in OData is like a pre-built query that may take some parameters and either return a single value or a collection of values, which may be entities. An action is similar, but, unlike functions, an action can have side effects, that is, it can make modifications to the underlying data.

Some examples of function calls might be

  • /odata/blogs/FindByDate(date=2009-08-01) – returning a collection of entities
  • /odata/blogs/CountPosts(blogid=1) – returning a single value
  • /odata/blogs/Blog(1)/Count – returning a single value
  • /odata/CountBlogPosts – returning a single value

As you can see in these example links, most of them are associated with the “blogs” entity set, these are called bound functions, but one of them is not, it is therefore an unbound function.

Registering these functions can be a bit tricky because there are a few things involved:

  • The [ODataRoutePrefix] attribute in a controller
  • The way we define the function when we build the EDM Data Model
  • The [ODataRoute] applied to the action method

But I digress. Let’s start with the first function.

Bound Function with Parameters Returning a Collection of Entities

For this we need to define a function in our model, associated with an entity set:

var findByCreation = builder
    .EntitySet<Blog>("Blogs")
    .EntityType
    .Collection
    .Function("FindByCreation");
findByCreation.ReturnsCollectionFromEntitySet<Blog>("Blogs"); findByCreation.Parameter<DateTime>("date").Required();

This will register a function named FindByCreation in the Blogs entity set, which returns a collection of Blog entities and takes a parameter of type DateTime named date.

Its implementation in a controller is:

[ODataRoutePrefix("Blogs")]
public class BlogController : ODataController
{
    private readonly BlogContext _ctx;

    public BlogController(BlogContext ctx)
    {       
this._ctx = ctx;
    }

[EnableQuery]
[ODataRoute("FindByCreation(date={date})")]
    [HttpGet]
    public IQueryable<Blog> FindByCreation(DateTime date)
    {
        return _ctx.Blogs.Where(x => x.Creation.Date == date);
    }
}

Notice the Blogs prefix applied to the BlogController class, this ties this class to the Blogs entity set.

The [EnableQuery] attribute, discussed on the previous post, allows the results of this function to be queried too.

Now you can browse to https://localhost:5001/odata/blogs/FindByCreation(date=2009-08-01) and see the results!

Bound Function with Parameters Returning a Single Value

Another example, this time, returning a single value:

var countPosts = builder
    .EntitySet<Blog>("Blogs")
    .EntityType
    .Collection
    .Function("CountPosts");

countPosts.Parameter<int>("id").Required();
countPosts.Returns<int>();

The function CountPosts is registered to the Blogs entity set, taking a single required parameter named id.

As for the implementation, in the same BlogController:

[ODataRoute("CountPosts(id={id})")]
[HttpGet]
public int CountPosts(int id)
{
    return _ctx.Blogs.Where(x => x.BlogId == id).Select(x => x.Posts).Count();
}

This will be available as https://localhost:5001/odata/blogs/CountPosts(id=1)

Unbound Function

Next up, an unbound function, that is, one that is not associated with an entity set:

var countBlogPosts = builder.Function("CountBlogPosts");
countBlogPosts.Returns<int>();

This function, as you can see, is not attached to any entity set.

Its implementation must be done in a controller that is also not tied to any entity set (no [ODataRoutePrefix] attribute):

public class BlogPostController : ODataController
{
private readonly BlogContext _ctx;

public BlogPostController(BlogContext ctx)
{
this._ctx = ctx;
}

[HttpGet]
[ODataRoute("CountBlogPosts()")]
    public int CountBlogPosts()
{
return _ctx.Posts.Count();
}
}

And to call this function, just navigate to https://localhost:5001/odata/CountBlogPosts().

Actions

The difference between actions and functions is that the former may have side effects, such as modifying data. It should come as no surprise, following REST principles, that actions need to be called by POST or PUT. Let’s see a couple examples

Bound Function with Key Parameter and No Payload Returning a Single Value

Here we want the URL to reflect the fact that we are invoking this function on a specific entity. The definition of the function in the EDM Data Model is:

var count = builder
    .EntitySet<Blog>("Blogs")
    .EntityType
    .Collection
    .Action("Count");
count.Parameter<int>("id").Required(); count.Returns<int>();

Now we are using Action instead of Function to define the Count action!

As for the definition:

[ODataRoute("({id})/Count")]
[HttpPost]
public int Count([FromODataUri] int id)
{
    return _ctx.Blogs.Where(x => x.BlogId == id).Select(x => x.Posts).Count();
}

Notice that we applied the [FromODataUri] attribute to the id parameter, this is required.

To call this action, you will need to POST to https://localhost:5001/odata/blogs/Blog(1)/Count.

Bound Function with Key Parameter and Payload Returning an Entity

The difference between this one and the previous is that this receives an entity as its payload. The definition first:

var update = builder
    .EntitySet<Blog>("Blogs")
    .EntityType
    .Collection
    .Action("Update");

update.EntityParameter<Blog>("blog").Required();
update.ReturnsFromEntitySet<Blog>("Blogs");

Notice how I replaced Parameter by EntityParameter.

The action method implementation is:

[ODataRoute("({id})/Update")]
[HttpPost]
public Blog Update([FromODataUri] int id, ODataActionParameters parameters)
{
var blog = parameters["blog"] as Blog;
_ctx.Entry(blog).State = EntityState.Modified;
_ctx.SaveChanges();
    return blog;
}

And to call this, you need to POST to https://localhost:5001/odata/blogs(1)/Replace with a payload containing a blog property with a value that is the Blog that you wish to update:

{
    "blog" : {
        "BlogId": 1,
        "Name": "New Name",
        "Url": "http://blog.url",
        "CreationDate": "2009-08-01"
    }
}

Conclusion

This concludes the topic of functions and actions. On the next post I will be talking about some more advanced features of OData.

ASP.NET Core OData Part 2

Update: see the third post here.

Introduction

This is the second post on my series on using OData with ASP.NET Core 3. You can find the first here.

Querying

We’ve seen how we can expose an object model to OData. In the first post I used Entity Framework Core, but you don’t need to use any ORM.

Where OData really excels is in querying: you can perform LINQ-style queries over the URL. These include:

  • Filtering
  • Sorting
  • Projections
  • Pagination
  • Counting
  • Navigation property expansions

By default, when you access the entity set’s endpoint, you get all records, but you can enable querying over them. This needs to be done globally first, when you define the endpoint:

app.UseEndpoints(endpoints =>
{
    endpoints.MapODataRoute("odata", "odata", GetEdmModel(app.ApplicationServices));
    endpoints.Select().Expand().OrderBy().Filter().Count();
});

Let’s have a look at the extension methods after the route registration. Don’t worry, I will show examples in a moment.

  • The Select extension method allows projections over an entity, that is, selecting only parts of it
  • Expand is used to allow expansions, for example, while retrieving an entity, similar to what Include does in Entity Framework Core – in fact, it translates to it
  • OrderBy is self explanatory. without it you won’t be able to sort the results of your query
  • Filter is what permits querying
  • Finally, Count is used to allow returning only the count, not the actual results

These are global settings, but we also need to enable them at the configuration, per entity, when we build the EDM Data Model (the GetEdmModel method I’ve shown previously):

builder
    .EntitySet<Parent>(“Parents”)
    .EntityType
    .Select()
    .Expand()
    .OrderBy()
    .Filter()
    .Count();

Finally, we also need to enable it in the action method that returns a query (IQueryable<T>), regardless of whether or not it is wrapped in an IActionResult:

[ODataRoute]

[EnableQuery]

public IQueryable<Parent> Get()

Once we do this, we can now query the entity set on the URL, but first, we need the [EnableQuery] attribute. This is what allows us to query over the results!

If you don’t want to decorate all your action methods with [EnableQuery], we can also do this globally, for any queryable action methods:

services.AddODataQueryFilter();

This has the advantage (or disadvantage) that it applies to all methods, unless we specifically tell OData that querying is not allowed.

As for querying, we have a number of options, I’m going to show just the simplest:

Filter by a property’s value:

/odata/Parents?$filter=Name eq ‘Ricardo Peres’

Sort by one property’s values descending:

/odata/Parents?$orderby=Name desc

Select just a single property:

/odata/Parents?$select=Name

Skip 10 records and retrieve the next 5, ordered by a property:

/odata/Parents?$skip=10&$top=5&$orderby=Name

Filter by a property’s value and return the count:

/odata/Parents?$filter=contains(Name,’Peres’)&$count=true

Expand a collection property:

/odata/Parents?$expand=Children

The main keywords are:

  • $filter: used for specifying conditions
  • $orderby: for sorting, either ascending or descending
  • $select: projections
  • $top: getting only some records
  • $skip: skipping some records
  • $count: getting the count of the returned records together with them
  • $expand: expansions (include navigation properties)

Some of the options can be enhanced, for example:

Filter by several conditions:

/odata/Parents?$filter=Id eq 1 or Id eq 2

/odata/Parents?$filter=Id eq 1 and Name eq ‘abc’

Where property value is in list:

/odata/Parents$filter=Id in (1, 2)

Sort by two property’s values, one descending and the other ascending:

/odata/Parents?$orderby=Name desc,Id asc

Filtering an expansion:

/odata/Parents?$expand=Children($filter=Name eq ‘A Child’)

I won’t go through all of the possible expressions, but the full OData specification is available here.

Setting Limits

We’ve seen in the beginning, while defining the OData route, that we can tell OData what should it support (select, filter, orderby, expand). We can also define the maximum amount of records to return:

app.UseEndpoints(endpoints =>
{
    endpoints.MapODataRoute("odata", "odata", GetEdmModel(app.ApplicationServices));
    endpoints.Select().Expand().OrderBy().Filter().Count().MaxTop(100);
});

Notice the MaxTop extension method, it defines the global maximum amount of records that can be returned by an OData endpoint. But we can also configure it on the action method, using the [EnableQuery] attribute:

[ODataRoute]

[EnableQuery(MaxTop = 10)]

public IQueryable<Parent> Get()

The [EnableQuery] attribute allows you to define a lot of other restrictions:

All of these options can also be specified through the AddODataQueryFilter extension method, but for global restrictions. It is at least advisable to define a maximum number of return records (MaxTop) and also the maximum number of expansions (MaxExpansionDepth).

Return Result

Querying will work both if you are returning an IQueryable<T> collection or an IEnumerable<T>. The thing is, with the former, your query will be executed in the server (the database), whereas with the latter, it will be executed in memory (LINQ to Objects). You will still be able to query, but it won’t be the same!

Inspecting Query Options

In your action methods, whenever querying is allowed, we have a way to get the query options passed to the URL ($filter, $orderby, etc), and then choose whether or not we want to apply them. The key lies in the ODataQueryOptions<T> class:

[ODataRoute]
[EnableQuery]
public IQueryable<Parent>Get(ODataQueryOptions options)
{
if (options.Top != null && options.Top.Value == 0) { options.Top.Value = 10; } var parents = _ctx.Parents.AsQueryable(); parents = options.ApplyTo(parents) as IQueryable<Parent>; return parents;
}

Here you can inspect the current filter (Filter), sort order (OrderBy), expand (SelectExpand), skip (Skip), count (Count) and even make changes. When you’re happy with then, just ApplyTo a base queryable collection.

Conclusion

This covers the basic querying options of OData. In the next post, functions and actions!

References

Please refer to the following links for additional information:

Mastering ASP.NET Core 3.0 – Second Edition

Mastering ASP.NET Core 3.0 - Second Edition

My new book, Mastering ASP.NET Core 3.0 – Second Edition, for Packt Publishing, will be available from June 19. It is a almost total rewrite from my previous one, Mastering ASP.NET Core 2.0, and it covers all of the goodies that ASP.NET Core 3.0 and 3.1 brought along. I cover Razor Pages, Blazor, OData, gRPC and some less-known features as well.

For personal reasons, it was difficult to write, and, in fact, it should have come out long ago, but, if we think about it, if it had, I wouldn’t have had the chance to cover some technologies – such as Blazor WebAssembly – that were only released recently.

I will come back to this with more information, including the table of contents, soon.

In the end, I can say that I am happy with the result, and hope you, dear readers, enjoy it too!

ASP.NET Core OData Part 1

Update: see the second post here.

Introduction

OData is an open standard for making an object-oriented domain model available as an HTTP REST interface.In a nutshell, it provides a specification for returning domain models as result of HTTP requests, querying them over the URL and even creating functions and actions over the domain model. In what .NET Core is concerned, it is a way by which you can expose your Entity Framework Core – or any other ORM that has a LINQ interface – to the web, without writing too much boilerplate code, as an ASP.NET Core Web API.

It is surprising that not many people know about OData, also, there are some caveats to it and not much documentation on using it with ASP.NET Core, so I decided to write a few posts on the subject. Here is the first!

Setting Up

Let’s pretend you have a domain model with just two classes, Parent and Child:

image

We should also have an Entity Framework Core context that exposes them:

public class ParentChildContext : DbContext

{

public ParentChildContext(DbContextOptions options) : base(options) { }

public DbSet<Parent> Parents { get; set; }

public DbSet<Child> Children { get; set; }

}

I won’t go into details as to explain this, I’m pretty sure you all know about Entity Framework Core contexts! Just make sure you add a reference to the Microsoft.EntityFrameworkCore NuGet package, or, if you’re using SQL Server, Microsoft.EntityFrameworkCore.SqlServer. Now register your context to the DI framework, in the ConfigureServices method:

services.AddDbContext<ParentChildContext>(options =>

{

options.UseSqlServer(“<connection string>”);

});

In your ASP.NET Core’s project you need to add a reference to the Microsoft.AspNetCore.OData NuGet package. This includes the server-side implementation of OData version 4 for ASP.NET Core.

You need to add its required services in the ConfigureServices method:

services.AddOData();

This registers the services, but now we need to add an endpoint for an actual domain model. ASP.NET Core OData now supports endpoint routing, so everything can be done smoothly:

app.UseEndpoints(endpoints =>

{ endpoints.MapODataRoute(“odata”, “odata”, GetEdmModel(app.ApplicationServices)); });

We registered this endpoint with name odata (first parameter) and also with the same prefix (second parameter), you can happily change this. As you can see, in this route, we are returning an EDM Data Model. We build one as this:

private static IEdmModel GetEdmModel(IServiceProvider serviceProvider)

{

var builder = new ODataConventionModelBuilder(serviceProvider);

builder.EntitySet<Parent>(“Parents”);

builder.EntitySet<Child>(“Children”);

return builder.GetEdmModel();

}

One thing that you’ll notice is that this is a conventions-based model builder, ODataConventionModelBuilder. this one takes care of some things for you, such as inferring the id property for each entity, that’s why you don’t have to explicitly state it. If your entity has an id property with a funny name, other than Id or EntityId, then you will need to specify it:

builder

.EntitySet<Parent>(“Parents”)

.EntityType

.HasKey(x => x.Id);

There is no need to specify the other properties, because they are all inferred automatically too from the generic parameter.

Do keep in mind the entity set name that you give, it is common to have a pluralized form of the entity name, but it is not required.

Now we need to create a controller that exposes this. At the very least, we will need two methods:

[ODataRoutePrefix(“Parents”)]

public class ParentController : ODataController

{

private ParentChildContext _ctx;

public ParentController(ParentChildContext ctx)

{

this._ctx = ctx;

}

[ODataRoute]

public IQueryable<Parent> Get()

{

return this._ctx.Parents.AsQueryable();

}

[ODataRoute(“{id}”)]

public Parent Get([FromODataUri] int id)

{

return this._ctx.Parents.Find(id);

}

}

This controller is specific to the Parents entity set, so, if you wish, you need to have another one for the Children. This is specified in the [ODataRoutePrefix] attribute.

Also notice how we are returning IQueryable<Parent> from the Get action method that does not take parameters and a single Parent from the other. You can also declare IActionResult or ActionResult<T>:

[ODataRoute]

public IActionResult Get()

{

return this.Ok(this._ctx.Parents.AsQueryable());

}

//alternative

[ODataRoute]

public ActionResult<IQueryable<Parent>> Get()

{

return this.Ok(this._ctx.Parents.AsQueryable());

}

The latter, the one that uses ActionResult<T>, has some advantages, which you can read about here.

And, of course, all of the methods can be made asynchronous too:

[ODataRoute]

public async Task<IQueryable<Parent>> Get()

{

return await this._ctx.Parents.ToListAsync();

}

[ODataRoute(“{id}”)]

public async Task<Parent> Get([FromODataUri] int id)

{

return await this._ctx.Parents.FindAsync(id);

}

We will see a problem with the implementation of the first method in the next post.

Now, the first method, the one without parameters, returns all of the entities in the database, and the second, as one would expect, returns possibly one from its id property.

Conclusion

We’re all done here, so we can now access the endpoints we just created, try to navigate to the following URLs:

  • /odata: returns information about the exposed entity sets (Parents and Children)

image

  • /odata/parents –> ParentController.Get(): returns all records for the Parent entity
  • /odata/parents(1) –> ParentController.Get(1): returns possibly one record, if it exists in the database for the given primary key of the Parent entity

This is the very basics, in the future posts we will explore other options, such as querying over the URL and creating functions and actions.

References

You can find more information in the following pages:

Dynamic Payloads in ASP.NET Core

It has always been possible (but a tad problematic) to submit dynamic contents to an ASP.NET Core controller action. In the dark, pre-Core days, we had to implement a custom model binder for this. Now it is no longer the case.

All we need is to declare an action method as having a dynamic parameter, obtained from the request body, and accepting the request through POST:

[HttpPost]
public IActionResult Post([FromBody] dynamic payload)
{
    return Json(new { Ok = true });
}

This will accept any content that we POST, if we use the Content Type application/json, and it will by default be turned into a System.Text.Json.JsonElement, a type of the new JSON API, which is the default serializer of ASP.NET Core 3.

If, for some reason, we want to go back to the previous serializer, JSON.NET (Newtonsoft.Json), we need to do a couple of things:

  1. Install the Microsoft.AspNetCore.Mvc.NewtonsoftJson Nuget package (it will bring all of the necessary dependencies)
  2. Configure it as the default serializer (AddMvc or AddControllers or AddRazorPages or AddControllersWithViews all allow this):

    services

    .AddMvc()

    .AddNewtonsoftJson();

If you do, the payload will instead by a Newtonsoft.Json.Linq.JObject instance. The main difference between the two serializer is outlined here. In the end, System.Text.Json supports most of the usual cases and is already included.

In general, I prefer using strongly-typed models, but there may be use cases for this, particularly when implementing routers, API gateways or similar functionality.

As always, hope this is useful! Smile