Saturday, December 7, 2019

EFCore 3.0 Code First: Define Relations with Fluent API

Here is the summary of how EntityFrame Core 3.0 define entity relations with Fluent API.

  1.  1-0:1

    1 student to 0:1 StudentAddress.
  • Model classes:
    public class Student
    {
        public int Id { get; set; }
        public string LastName { get; set; }
        public string FirstMidName { get; set; }
        public StudentAddress Address { get; set; }
    }

    public class StudentAddress
    {
        public int StudentId { get; set; }
        public string AddressLine1 { get; set; }
        public string AddressLine2 { get; set; }
        public string City { get; set; }
        public string State { get; set; }
        public string Zip { get; set; }
        public Student Student { get; set; }
    }
  • Mappings:
    public class StudentMap : IEntityTypeConfiguration<Student>
    {
        public void Configure(EntityTypeBuilder<Student> builder)
        {
            builder.ToTable(nameof(Student));
            builder.HasKey(x => x.Id);
        }
    }

    public class StudentAddressMap : IEntityTypeConfiguration<StudentAddress>
    {
        public void Configure(EntityTypeBuilder<StudentAddress> builder)
        {
            builder.ToTable(nameof(StudentAddress));
            builder.HasKey(u => u.StudentId);  // StudentAddress.StudentId is primary key
            builder.HasOne(u => u.Student)
                   .WithOne(b => b.Address)
                   .HasForeignKey<StudentAddress>(b => b.StudentId); // StudentAddress.StudentId is foreign key to Student.Id, too.
        }
    }

 public class SchoolContext : DbContext
    {
        public SchoolContext (DbContextOptions<SchoolContext> options)
            : base(options)
        {
        }

        public DbSet<Student> Students { get; set; }
        public DbSet<StudentAddress> StudentAddresses { get; set; }
     
        protected override void OnModelCreating(ModelBuilder modelBuilder)
        {
            modelBuilder.ApplyConfiguration(new StudentMap());
            modelBuilder.ApplyConfiguration(new StudentAddressMap());
        }
    }
  • Generated tables:
CREATE TABLE [Student] (
    [Id] int NOT NULL IDENTITY,
    [LastName] nvarchar(max) NULL,
    [FirstMidName] nvarchar(max) NULL,
    CONSTRAINT [PK_Student] PRIMARY KEY ([Id])
);
CREATE TABLE [StudentAddress] (
    [StudentId] int NOT NULL,
    [AddressLine1] nvarchar(max) NULL,
    [AddressLine2] nvarchar(max) NULL,
    [City] nvarchar(max) NULL,
    [State] nvarchar(max) NULL,
    [Zip] nvarchar(max) NULL,
    CONSTRAINT [PK_StudentAddress] PRIMARY KEY ([StudentId]),
    CONSTRAINT [FK_StudentAddress_Student_StudentId] FOREIGN KEY ([StudentId]) REFERENCES [Student] ([Id]) ON DELETE CASCADE
);

2. 0:1-m

  1. 1 Auther to 1:m Book.

  2. public class SampleContext : DbContext
  3. {
  4. public DbSet<Book> Books { get; set; }
  5. protected override void OnModelCreating(ModelBuilder modelBuilder)
  6. {
  7. modelBuilder.Entity<Author>()
  8. .HasMany(a => a.Books)
  9. .WithOne(b => b.Author);
  10. }
  11. }

  12. public class Author
  13. {
  14. public int AuthorId { get; set; }
  15. public string FirstName { get; set; }
  16. public string LastName { get; set; }
  17. public ICollection<Book> Books { get; set; }
  18. }

  19. public class Book
  20. {
  21. public int BookId { get; set; }
  22. public string Title { get; set; }
  23. public Author Author { get; set; }
  24. }

3. 1-1:m

  1. 1 Auther to 0:m Book.

Same as 1-0:m except IsRequired() is added in relation definition as below:

  1. public class SampleContext : DbContext
  2. {
  3. public DbSet<Book> Books { get; set; }
  4. protected override void OnModelCreating(ModelBuilder modelBuilder)
  5. {
  6. modelBuilder.Entity<Author>()
  7. .HasMany(a => a.Books)
  8. .WithOne(b => b.Author)
  9. .IsRequired();
  10. }
  11. }

Thursday, October 3, 2019

EF6 Code-First Conventions


1. Default Convention for Table, Primary Key, Foreign Key & Column

Entity Framework code-first conventions

2. Convention 1 (0:1-m relationship)

EF 6 infers the 0:1-n relationship using the navigation property by default convention.

Relationship: Grade(0:1)-Student(many)
Parent class: Grade
Children class: Student


public class Student
{
    public int Id { get; set; }
    public string Name { get; set; }
    public Grade Grade { get; set; }
}

public class Grade
{
    public int GradeId { get; set; }
    public string GradeName { get; set; }
    public string Section { get; set; }
}
The Student.Grade property is reference navigation property of Grade class. So, there can be many students in a single grade. This will result in a one-to-many relationship between the Students and Grades table in the database, where the Students table includes foreign key Grade_GradeId .

Notices:
  • The reference property Student.Grade is nullable, so it creates a nullable foreign key column Grade_GradeId in the Students table. This is called as optional one-to-many relationship, too.
These codes below falls into 0:1-m relation as above, too:
public class Student
{
    public int StudentId { get; set; }
    public string StudentName { get; set; }
}

public class Grade
{
    public int GradeId { get; set; }
    public string GradeName { get; set; }
    public string Section { get; set; }

    public ICollection<Student> Students { get; set; } 
}
or

public class Student
{
    public int Id { get; set; }
    public string Name { get; set; }
    public Grade Grade { get; set; }
}

public class Grade
{
    public int GradeID { get; set; }
    public string GradeName { get; set; }
    public string Section { get; set; }
    
    public ICollection<Student> Students { get; set; }
}

3. Convention 2 (1-m relationship)

Relationship: Grade(1)-Student(many)
Parent class: Grade
Children class: Student



modelBuilder.Entity<Student>()
    .HasRequired<Grade>(s => s.Grade)
    .WithMany(g => g.Students);
or start configuring the relationship with Grade class:
modelBuilder.Entity<Grade>()
    .HasMany<Student>(g => g.Students)
    .WithRequired(s => s.Grade);


  • Method 2: Add non-nullable foreign key property GradeId with its reference property Grade in Student class as below:

public class Student
{
    public int Id { get; set; }
    public string Name { get; set; }
    
    public int GradeId { get; set; } //non-nullable foreign key property
    public Grade Grade { get; set; } //reference property
}

4. Convention 3 (m-m relationship)

Relationship: Student(many)-Course(many)

  • Method 1: by default

public class Student
{
    public Student() 
    {
        this.Courses = new HashSet<Course>();
    }

    public int StudentId { get; set; }
    [Required]
    public string StudentName { get; set; }

    public virtual ICollection<Course> Courses { get; set; }
}
        
public class Course
{
    public Course()
    {
        this.Students = new HashSet<Student>();
    }

    public int CourseId { get; set; }
    public string CourseName { get; set; }

    public virtual ICollection<Student> Students { get; set; }
}
  • Method 2: Fluent API

modelBuilder.Entity<Student>()
                .HasMany<Course>(s => s.Courses)
                .WithMany(c => c.Students);
EF will create a joining table StudentCourses in database for above example.

5. Convention 4 (1-0:1 relationship)

Relationship: Student(1)-StudentAddress(0:1)
  • Method 1: Using data annotation attributes

public class Student
{
    public int StudentId { get; set; }
    public string StudentName { get; set; }

    public virtual StudentAddress Address { get; set; }
}
     
public class StudentAddress 
{
    [ForeignKey("Student")]
    public int StudentAddressId { get; set; } //It is PK and FK
        
    public string Address1 { get; set; }
    public string Address2 { get; set; }
    public string City { get; set; }
    public int Zipcode { get; set; }
    public string State { get; set; }
    public string Country { get; set; }

    public virtual Student Student { get; set; }
}
Method 2: Fluent API
   // Configure Student & StudentAddress entity
    modelBuilder.Entity<Student>()
                .HasOptional(s => s.Address) // Mark Address property optional in Student entity
                .WithRequired(ad => ad.Student); // mark Student property as required in StudentAddress entity. Cannot save StudentAddress without Student
one-to-one relationship in code first

6. Convention 6 (1-1 relationship)

Relationship: Student(1)-StudentAddress(1)
  • Method: Using Fluent API where both ends are required
    // Configure StudentId as FK for StudentAddress
    modelBuilder.Entity<Student>()
                .HasRequired(s => s.Address) 
                .WithRequiredPrincipal(ad => ad.Student); 

7. Configure properties with Fluent API

    //Configure Null Column
    modelBuilder.Entity<Student>()
                .Property(p => p.Heigth)
                .IsOptional();
                        
    //Configure NotNull Column
    modelBuilder.Entity<Student>()
                .Property(p => p.Weight)
                .IsRequired();

    //Set StudentName column size to 50
    modelBuilder.Entity<Student>()
                .Property(p => p.StudentName)
                .HasMaxLength(50);

    //Set byte[] type property as a concurrency column
    modelBuilder.Entity<Student>()
                .Property(p => p.StudentName)
                .IsRowVersion();
8. EF Power Tools

Entity Framework 6 Power Tools are design-time utilities used in Visual Studio when working with the code-first development approach. It can create read-only entity data model.

    Download and install EF 6 Power Tools for Visual Studio 2015/2017 from Visual Studio Marketplace.

    Thursday, June 27, 2019

    WebRTC tutorial - socket.io Study Notes


    1.   Namespace

    Represents a pool of sockets connected under a given scope identified by a pathname (eg: /chat).
    A client always connects to / (the main namespace), then potentially connect to other namespaces (while using the same underlying connection).
    Default namespace
    We call the default namespace / and it’s the one Socket.IO clients connect to by default, and the one the server listens to by default.
    This namespace is identified by io.sockets or simply io:
    // the following two will emit to all the sockets connected to `/`
    io.sockets.emit(
    'hi', 'everyone');
    io.emit(
    'hi', 'everyone'); // short form
    Each namespace emits a connection event that receives each Socket instance as a parameter
    io.on('connection', function(socket){
      socket.on(
    'disconnect', function(){
      });
    });
    Custom namespaces
    To set up a custom namespace, you can call the of function on the server-side:
    const nsp = io.of('/my-namespace');
    nsp.on(
    'connection', function(socket){
     
    console.log('someone connected');
    });
    nsp.emit(
    'hi', 'everyone!');
    On the client side, you tell Socket.IO client to connect to that namespace:
    const socket = io('/my-namespace');

    Event: ‘connect’ or ‘connection’

    • socket (Socket) socket connection with client
    Fired upon a connection from client.
    io.on('connect', (socket) => {
    
        // ...
    
      });
    
      
    
      io.of('/admin').on('connect', (socket) => {
    
        // ...
    
      });

    Flag: ‘volatile’
    Sets a modifier for a subsequent event emission that the event data may be lost if the clients are not ready to receive messages (because of network slowness or other issues, or because they’re connected through long polling and is in the middle of a request-response cycle).
    io.volatile.emit('an event', { some: 'data' }); // the clients may or may not receive it

    Flag: ‘binary’
    Specifies whether there is binary data in the emitted data. Increases performance when specified. Can be true or false.
    io.binary(false).emit('an event', { some: 'data' });

    namespace.to(room) or namespace.in(toom)

    • room (String)
    • Returns Namespace for chaining
    Sets a modifier for a subsequent event emission that the event will only be broadcasted to clients that have joined the given room.
    To emit to multiple rooms, you can call to several times.
    const io = require('socket.io')();
    
      const adminNamespace = io.of('/admin');
    
      
    
      adminNamespace.to('level1').emit('an event', { some: 'data' });

     

    namespace.emit(eventName[, …args])

    • eventName (String)
    • args
    Emits an event to all connected clients. The following two are equivalent:
    const io = require('socket.io')();
    
      io.emit('an event sent to all connected clients'); // main namespace
    
      
    
      const chat = io.of('/chat');
    
      chat.emit('an event sent to all connected clients in chat namespace');
    Note: acknowledgements are not supported when emitting from namespace.

    2.   Rooms

    Within each namespace, you can also define arbitrary channels that sockets can join and leave.

    Joining and leaving

    You can call join to subscribe the socket to a given channel:
    io.on('connection', function(socket){
    
        socket.join('some room');
    
      });
    And then simply use to or in (they are the same) when broadcasting or emitting:
    io.to('some room').emit('some event');
    To leave a channel you call leave in the same fashion as join.

    3.   Socket

    Socket is the fundamental class for interacting with browser clients. A Socket belongs to a certain Namespace (by default /) and uses an underlying Client to communicate.

    socket.id

    A unique identifier for the session, that comes from the underlying Client.

    socket.rooms

    A hash of strings identifying the rooms this client is in, indexed by room name.
    io.on('connection', (socket) => {
    
        socket.join('room 237', () => {
    
          let rooms = Object.keys(socket.rooms);
    
          console.log(rooms); // [ <socket.id>, 'room 237' ]
    
        });
    
      });

    socket.client A reference to the underlying Client object.

    socket.emit(eventName[, …args][, ack])

    • eventName (String)
    • args
    • ack (Function)
    • Returns Socket
    Emits an event to the socket identified by the string name. Any other parameters can be included. All serializable datastructures are supported, including Buffer.
    socket.emit('hello', 'world');
    
      socket.emit('with-binary', 1, '2', { 3: '4', 5: new Buffer(6) });
    The ack argument is optional and will be called with the client’s answer.
    io.on('connection', (socket) => {
    
        socket.emit('an event', { some: 'data' });
    
      
    
        socket.emit('ferret', 'tobi', (data) => {
    
          console.log(data); // data will be 'woot'
    
        });
    
      
    
        // the client code
    
        // client.on('ferret', (name, fn) => {
    
        //   fn('woot');
    
        // });
    
      
    
      });

    socket.send([…args][, ack])
    • args
    • ack (Function)
    • Returns Socket

    socket.on(eventName, callback)

    (inherited from EventEmitter)
    • eventName (String)
    • callback (Function)
    • Returns Socket
    Register a new handler for the given event.
    socket.on('news', (data) => {
    
        console.log(data);
    
      });
    
      // with several arguments
    
      socket.on('news', (arg1, arg2, arg3) => {
    
        // ...
    
      });
    
      // or with acknowledgement
    
      socket.on('news', (data, callback) => {
    
        callback(0);
    
      });

    socket.join(room[, callback])
    • room (String)
    • callback (Function)
    • Returns Socket for chaining
    Adds the client to the room, and fires optionally a callback with err signature (if any).
    io.on('connection', (socket) => {
    
        socket.join('room 237', () => {
    
          let rooms = Object.keys(socket.rooms);
    
          console.log(rooms); // [ <socket.id>, 'room 237' ]
    
          io.to('room 237').emit('a new user has joined the room'); // broadcast to everyone in the room
    
        });
    
      });
    For your convenience, each socket automatically joins a room identified by its id (see Socket#id). This makes it easy to broadcast messages to other sockets:
    io.on('connection', (socket) => {
    
        socket.on('say to someone', (id, msg) => {
    
          // send a private message to the socket with the given id
    
          socket.to(id).emit('my message', msg);
    
        });
    
      });

    socket.join(rooms[, callback])
    • rooms (Array)
    • callback (Function)
    • Returns Socket for chaining
    Adds the client to the list of room, and fires optionally a callback with err signature (if any).

    socket.leave(room[, callback])

    • room (String)
    • callback (Function)
    • Returns Socket for chaining
    Removes the client from room, and fires optionally a callback with err signature (if any).
    Rooms are left automatically upon disconnection.

    socket.to(room) or socket.in(room)

    • room (String)
    • Returns Socket for chaining
    Sets a modifier for a subsequent event emission that the event will only be broadcasted to clients that have joined the given room (the socket itself being excluded).
    To emit to multiple rooms, you can call to several times.
    io.on('connection', (socket) => {
    
      
    
        // to one room
    
        socket.to('others').emit('an event', { some: 'data' });
    
      
    
        // to multiple rooms
    
        socket.to('room1').to('room2').emit('hello');
    
      
    
        // a private message to another socket
    
        socket.to(/* another socket id */).emit('hey');
    
      
    
        // WARNING: `socket.to(socket.id).emit()` will NOT work, as it will send to everyone in the room
    
        // named `socket.id` but the sender. Please use the classic `socket.emit()` instead.
    
      });
    Note: acknowledgements are not supported when broadcasting.

    socket.compress(value)

    • value (Boolean) whether to following packet will be compressed
    • Returns Socket for chaining
    Sets a modifier for a subsequent event emission that the event data will only be compressed if the value is true. Defaults to true when you don’t call the method.
    io.on('connection', (socket) => {
    
        socket.compress(false).emit('uncompressed', "that's rough");
    
      });

    socket.disconnect(close)
    • close (Boolean) whether to close the underlying connection
    • Returns Socket
    Disconnects this client. If value of close is true, closes the underlying connection. Otherwise, it just disconnects the namespace.
    io.on('connection', (socket) => {
    
        setTimeout(() => socket.disconnect(true), 5000);
    
      });

    Flag: ‘broadcast’
    Sets a modifier for a subsequent event emission that the event data will only be broadcast to every sockets but the sender.
    io.on('connection', (socket) => {
    
        socket.broadcast.emit('an event', { some: 'data' }); // everyone gets it but the sender
    
      });

    Flag: ‘volatile’
    Sets a modifier for a subsequent event emission that the event data may be lost if the client is not ready to receive messages (because of network slowness or other issues, or because they’re connected through long polling and is in the middle of a request-response cycle).
    io.on('connection', (socket) => {
    
        socket.volatile.emit('an event', { some: 'data' }); // the client may or may not receive it
    
      });

    Flag: ‘binary’
    Specifies whether there is binary data in the emitted data. Increases performance when specified. Can be true or false.
    var io = require('socket.io')();
    
      io.on('connection', function(socket){
    
        socket.binary(false).emit('an event', { some: 'data' }); // The data to send has no binary data
    
      });

    Event: ‘disconnect’
    • reason (String) the reason of the disconnection (either client or server-side)
    Fired upon disconnection.
    io.on('connection', (socket) => {
    
        socket.on('disconnect', (reason) => {
    
          // ...
    
        });
    
      });
    Possible reasons:
    Reason
    Side
    Description
    transport error
    Server Side
    Transport error
    server namespace disconnect
    Server Side
    Server performs a socket.disconnect()
    client namespace disconnect
    Client Side
    Got disconnect packet from client
    ping timeout
    Client Side
    Client stopped responding to pings in the allowed amount of time (per the pingTimeout config setting)
    transport close
    Client Side
    Client stopped sending data

    Event: ‘error’

    • error (Object) error object
    Fired when an error occurs.
    io.on('connection', (socket) => {
    
        socket.on('error', (error) => {
    
          // ...
    
        });
    
      });

    Event: ‘disconnecting’
    • reason (String) the reason of the disconnection (either client or server-side)
    Fired when the client is going to be disconnected (but hasn’t left its rooms yet).
    io.on('connection', (socket) => {
    
        socket.on('disconnecting', (reason) => {
    
          let rooms = Object.keys(socket.rooms);
    
          // ...
    
        });
    
      });
    These are reserved events (along with connectnewListener and removeListener) which cannot be used as event names.

    4.   Client API

    IO

    Exposed as the io namespace in the standalone build, or the result of calling require('socket.io-client').
    <script src="/socket.io/socket.io.js"></script>
    
      <script>
    
        const socket = io('http://localhost');
    
      </script>
     

    const io = require('socket.io-client');
    
      // or with import syntax
    
      import io from 'socket.io-client';

     

    io([url][, options])

    • url (String) (defaults to window.location)
    • options (Object)
      • forceNew (Boolean) whether to reuse an existing connection
    • Returns Socket
    Creates a new Manager for the given URL, and attempts to reuse an existing Manager for subsequent calls, unless the multiplex option is passed with false. Passing this option is the equivalent of passing 'force new connection': true or forceNew: true.
    A new Socket instance is returned for the namespace specified by the pathname in the URL, defaulting to /. For example, if the url is http://localhost/users, a transport connection will be established to http://localhost and a Socket.IO connection will be established to /users.
    Query parameters can also be provided, either with the query option or directly in the url (example: http://localhost/users?token=abc).
    See new Manager(url[, options]) for the the list of available options.

    Initialization examples

    With multiplexing

    By default, a single connection is used when connecting to different namespaces (to minimize resources):
    const socket = io();
    
      const adminSocket = io('/admin');
    
      // a single connection will be established
    That behaviour can be disabled with the forceNew option:
    const socket = io();
    
      const adminSocket = io('/admin', { forceNew: true });
    
      // will create two distinct connections
    Note: reusing the same namespace will also create two connections
    const socket = io();
    
      const socket2 = io();
    
      // will also create two distinct connections

    Manager

    manager.open([callback]) or manager.connect([callback])

    • callback (Function)
    • Returns Manager
    If the manager was initiated with autoConnect to false, launch a new connection attempt.
    The callback argument is optional and will be called once the attempt fails/succeeds.

    5.   Setup WebRTC Signaling Server with socket.io

    See the details below.

    WebRTC 入门教程(一)| 搭建WebRTC信令服务器